From 4e5f2a715cb62f100deed51b48e7c30c7c8f6b85 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 12 Jul 2018 11:30:45 -0400 Subject: [PATCH 0001/2020] Updates CONTRIBUTING with notice about integration testing. (#587) --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac78fdca5b..ee0674066c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,9 @@ again. 3. Please include unit tests (and integration tests if applicable) for all new code. 4. Make sure all existing tests pass. * In `jib-core`, run `./gradlew clean goJF build integrationTest` - * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` - * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -Pintegration-tests` + * In `jib-gradle-plugin`, run `./gradlew clean goJF build` * + * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify` * 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! + +\* *Note that you will not be able to run the integration tests for `jib-gradle-plugin` or `jib-maven-plugin` because those push to our integration-testing GCP project. If you would like to run integration tests, change all uses of the `jib-integration-testing` project to your own GCP project and run `./gradlew integrationTest` for `jib-gradle-plugin` and `./mvnw verify -Pintegration-tests` for `jib-maven-plugin`.* From 5577f1027120e3fa90e8823dc30f6b70f76f5ad7 Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Thu, 12 Jul 2018 13:16:09 -0400 Subject: [PATCH 0002/2020] Update readme to v0.9.6 (#589) --- README.md | 4 ++-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- jib-gradle-plugin/README.md | 4 ++-- jib-maven-plugin/README.md | 16 ++++++++-------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ad7658b7a3..234cc0a27e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) [![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.4-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.6-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib @@ -37,7 +37,7 @@ See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). ## How Jib Works -Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. +Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 85058498f9..08aad9b5f8 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.4' + id 'com.google.cloud.tools.jib' version '0.9.6' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index b203e0adb2..3d76167bab 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -6,7 +6,7 @@ 1 - 0.9.4 + 0.9.6 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 0fdbbbbe1a..927de64e29 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,6 +1,6 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) [![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-gradle-plugin)](https://github.com/igrigorik/ga-beacon) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.4-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.6-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Gradle Java project @@ -27,7 +27,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.4' + id 'com.google.cloud.tools.jib' version '0.9.6' } ``` diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 489dfce61b..da303e0063 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -22,7 +22,7 @@ These features are not currently supported but will be added in later releases. You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.4:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.6:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -30,7 +30,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.4:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.6:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -48,7 +48,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.4 + 0.9.6 myimage @@ -108,7 +108,7 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b ``` -#### *TODO: Add more examples for common registries.* +#### *TODO: Add more examples for common registries.* ### Build your image @@ -181,7 +181,7 @@ You can then build your image with Docker: ```shell docker build -t myimage my/docker/context/ -``` +``` ## Extended Usage @@ -277,7 +277,7 @@ Some common credential helpers include: Configure credential helpers to use by specifying them as a `credHelper` for their respective image. -*Example configuration:* +*Example configuration:* ```xml ... @@ -295,7 +295,7 @@ Configure credential helpers to use by specifying them as a `credHelper` for the #### Using Maven Settings -Registry credentials can be added to your [Maven settings](https://maven.apache.org/settings.html). These credentials will be used if credentials could not be found in any specified Docker credential helpers. +Registry credentials can be added to your [Maven settings](https://maven.apache.org/settings.html). These credentials will be used if credentials could not be found in any specified Docker credential helpers. If you're considering putting credentials in Maven, we highly *recommend* using [maven password encryption](https://maven.apache.org/guides/mini/guide-encryption.html). @@ -314,7 +314,7 @@ If you're considering putting credentials in Maven, we highly *recommend* using ``` -* The `id` field should be the registry server these credentials are for. +* The `id` field should be the registry server these credentials are for. * We *do not* recommend putting your raw password in `settings.xml`. ## How Jib Works From bd0aee03eeb95517a579f64809f365a4a170fbe1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 12 Jul 2018 13:49:15 -0400 Subject: [PATCH 0003/2020] Add build steps for building tarball (#567) --- .../builder/BuildStepsIntegrationTest.java | 28 +++++ .../cloud/tools/jib/builder/BuildSteps.java | 40 +++++++ .../tools/jib/builder/steps/StepsRunner.java | 18 +++ .../jib/builder/steps/WriteTarFileStep.java | 104 ++++++++++++++++++ .../tools/jib/frontend/BuildStepsRunner.java | 23 ++++ 5 files changed, 213 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index a7449efa3d..0e49e8fa68 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -48,6 +48,8 @@ public class BuildStepsIntegrationTest { @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); + @Rule public TemporaryFolder temporaryTarOutput = new TemporaryFolder(); + private SourceFilesConfiguration sourceFilesConfiguration; @Before @@ -148,6 +150,32 @@ public void testSteps_forBuildToDockerDaemon() "Hello, world. An argument.\n", new Command("docker", "run", "testdocker").run()); } + @Test + public void testSteps_forBuildToTarball() + throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, + CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + BuildConfiguration buildConfiguration = + BuildConfiguration.builder(logger) + .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) + .setTargetImage(ImageReference.of(null, "testtar", null)) + .setMainClass("HelloWorld") + .setJavaArguments(Collections.singletonList("An argument.")) + .build(); + + Path outputPath = temporaryTarOutput.newFolder().toPath().resolve("test.tar"); + Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + BuildSteps.forBuildToTar( + outputPath, + buildConfiguration, + sourceFilesConfiguration, + new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) + .run(); + + new Command("docker", "load", "--input", outputPath.toString()).run(); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", "testtar").run()); + } + private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws IOException { Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); return BuildSteps.forBuildToDockerRegistry( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 55303dd848..c100eca3da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.cache.Caches; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.nio.file.Path; import java.util.concurrent.ExecutionException; /** Steps for building an image. */ @@ -51,6 +52,13 @@ private interface StepsRunnerConsumer { private static final String SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON = "Built image to Docker daemon as \u001B[36m%s\u001B[0m"; + private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; + private static final String STARTUP_MESSAGE_FORMAT_FOR_TARBALL = + "Containerizing application to file at '%s'..."; + // String parameter (target file) in cyan. + private static final String SUCCESS_MESSAGE_FORMAT_FOR_TARBALL = + "Built image tarball at \u001B[36m%s\u001B[0m"; + /** * All the steps to build an image to a Docker registry. * @@ -122,6 +130,38 @@ public static BuildSteps forBuildToDockerDaemon( .waitOnLoadDockerStep()); } + /** + * All the steps to build an image tarball. + * + * @param outputPath the path to output the tarball to + * @param buildConfiguration the configuration parameters for the build + * @param sourceFilesConfiguration the source/destination file configuration for the image + * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache + * @return a new {@link BuildSteps} for building a tarball + */ + public static BuildSteps forBuildToTar( + Path outputPath, + BuildConfiguration buildConfiguration, + SourceFilesConfiguration sourceFilesConfiguration, + Caches.Initializer cachesInitializer) { + return new BuildSteps( + DESCRIPTION_FOR_TARBALL, + buildConfiguration, + sourceFilesConfiguration, + cachesInitializer, + String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), + String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), + stepsRunner -> + stepsRunner + .runPullBaseImageStep() + .runPullAndCacheBaseImageLayersStep() + .runBuildAndCacheApplicationLayerSteps() + .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) + .runFinalizingBuildStep() + .runWriteTarFileStep(outputPath) + .waitOnWriteTarFileStep()); + } + /** Creates the container entrypoint for a given configuration. */ private static ImmutableList getEntrypoint( BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 1ec77262be..12a226b411 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -64,6 +65,7 @@ public class StepsRunner { @Nullable private PushContainerConfigurationStep pushContainerConfigurationStep; @Nullable private PushImageStep pushImageStep; @Nullable private LoadDockerStep loadDockerStep; + @Nullable private WriteTarFileStep writeTarFileStep; public StepsRunner( BuildConfiguration buildConfiguration, @@ -202,6 +204,18 @@ public StepsRunner runLoadDockerStep() { return this; } + public StepsRunner runWriteTarFileStep(Path outputPath) { + writeTarFileStep = + new WriteTarFileStep( + listeningExecutorService, + outputPath, + buildConfiguration, + Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), + Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), + Preconditions.checkNotNull(buildImageStep)); + return this; + } + public void waitOnPushImageStep() throws ExecutionException, InterruptedException { Preconditions.checkNotNull(pushImageStep).getFuture().get(); } @@ -210,6 +224,10 @@ public void waitOnLoadDockerStep() throws ExecutionException, InterruptedExcepti Preconditions.checkNotNull(loadDockerStep).getFuture().get(); } + public void waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { + Preconditions.checkNotNull(writeTarFileStep).getFuture().get(); + } + /** * @return the layers cached by {@link #pullAndCacheBaseImageLayersStep} * @throws ExecutionException if {@link #pullAndCacheBaseImageLayersStep} threw an exception diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java new file mode 100644 index 0000000000..6520f13a99 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -0,0 +1,104 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.image.Image; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +public class WriteTarFileStep implements AsyncStep, Callable { + + private final Path outputPath; + private final BuildConfiguration buildConfiguration; + private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; + private final ImmutableList buildAndCacheApplicationLayerSteps; + private final BuildImageStep buildImageStep; + + private final ListeningExecutorService listeningExecutorService; + private final ListenableFuture listenableFuture; + + WriteTarFileStep( + ListeningExecutorService listeningExecutorService, + Path outputPath, + BuildConfiguration buildConfiguration, + PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, + ImmutableList buildAndCacheApplicationLayerSteps, + BuildImageStep buildImageStep) { + this.listeningExecutorService = listeningExecutorService; + this.buildConfiguration = buildConfiguration; + this.outputPath = outputPath; + this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; + this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; + this.buildImageStep = buildImageStep; + + listenableFuture = + Futures.whenAllSucceed( + pullAndCacheBaseImageLayersStep.getFuture(), buildImageStep.getFuture()) + .call(this, listeningExecutorService); + } + + @Override + public ListenableFuture getFuture() { + return listenableFuture; + } + + @Override + public Void call() throws ExecutionException, InterruptedException { + ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); + for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : + NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { + dependenciesBuilder.add(pullAndCacheBaseImageLayerStep.getFuture()); + } + for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : + buildAndCacheApplicationLayerSteps) { + dependenciesBuilder.add(buildAndCacheApplicationLayerStep.getFuture()); + } + dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); + return Futures.whenAllSucceed(dependenciesBuilder.build()) + .call(this::afterPushBaseImageLayerFuturesFuture, listeningExecutorService) + .get(); + } + + private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, IOException { + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + + // Build the image to a tarball + buildConfiguration.getBuildLogger().lifecycle("Building image to tar file..."); + Files.createDirectories(outputPath.getParent()); + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outputPath))) { + new ImageToTarballTranslator(image) + .toTarballBlob(buildConfiguration.getTargetImageReference()) + .writeTo(outputStream); + } + + return null; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index dc1bb950cd..466a2389a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -34,6 +34,7 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.UnknownHostException; +import java.nio.file.Path; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; @@ -72,6 +73,28 @@ public static BuildStepsRunner forBuildToDockerDaemon( buildConfiguration, sourceFilesConfiguration, getCacheInitializer(buildConfiguration))); } + /** + * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. + * + * @param outputPath the path to output the tarball to + * @param buildConfiguration the configuration parameters for the build + * @param sourceFilesConfiguration the source/destination file configuration for the image + * @return a {@link BuildStepsRunner} for building a tarball + * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created + */ + public static BuildStepsRunner forBuildTar( + Path outputPath, + BuildConfiguration buildConfiguration, + SourceFilesConfiguration sourceFilesConfiguration) + throws CacheDirectoryCreationException { + return new BuildStepsRunner( + BuildSteps.forBuildToTar( + outputPath, + buildConfiguration, + sourceFilesConfiguration, + getCacheInitializer(buildConfiguration))); + } + // TODO: Move this up to somewhere where defaults for cache location are provided and ownership is // checked rather than in Caches.Initializer. private static Initializer getCacheInitializer(BuildConfiguration buildConfiguration) From 9af66b807fc071a064a86d1a911034c7a029c650 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 12 Jul 2018 18:04:51 -0400 Subject: [PATCH 0004/2020] Add support for SNAPSHOT dependencies layer (#584) * Add support for SNAPSHOT dependencies layer --- .../builder/BuildStepsIntegrationTest.java | 8 +- .../jib/builder/SourceFilesConfiguration.java | 8 +- .../BuildAndCacheApplicationLayerStep.java | 14 ++ .../jib/docker/DockerContextGenerator.java | 6 + .../builder/TestSourceFilesConfiguration.java | 76 +++++++++-- ...BuildAndCacheApplicationLayerStepTest.java | 124 ++++++++++++++++-- .../docker/DockerContextGeneratorTest.java | 7 + .../dependency-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes jib-core/src/test/resources/sampleDockerfile | 1 + jib-gradle-plugin/CHANGELOG.md | 1 + .../GradleSourceFilesConfiguration.java | 14 +- .../GradleSourceFilesConfigurationTest.java | 13 ++ .../dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes jib-maven-plugin/CHANGELOG.md | 1 + jib-maven-plugin/pom.xml | 14 ++ .../maven/MavenSourceFilesConfiguration.java | 14 +- .../MavenSourceFilesConfigurationTest.java | 36 +++-- .../cloud/tools/jib/maven/TestRepository.java | 69 ++++++++++ .../dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../dependency/1.0.0/_remote.repositories | 4 + .../dependency/1.0.0/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../dependency/1.0.0/dependency-1.0.0.pom | 9 ++ .../test/dependency/maven-metadata-local.xml | 12 ++ .../1.0.0-SNAPSHOT/_remote.repositories | 4 + .../dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../dependencyX-1.0.0-SNAPSHOT.pom | 9 ++ .../1.0.0-SNAPSHOT/maven-metadata-local.xml | 24 ++++ .../test/dependencyX/maven-metadata-local.xml | 11 ++ 28 files changed, 439 insertions(+), 40 deletions(-) create mode 100644 jib-core/src/test/resources/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar create mode 100644 jib-gradle-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java create mode 100644 jib-maven-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml create mode 100644 jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 0e49e8fa68..0320e4d851 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -54,7 +54,13 @@ public class BuildStepsIntegrationTest { @Before public void setUp() throws IOException, URISyntaxException { - sourceFilesConfiguration = new TestSourceFilesConfiguration(); + sourceFilesConfiguration = + TestSourceFilesConfiguration.builder() + .withClasses() + .withDependencies() + .withSnapshotDependencies() + .withResources() + .build(); } @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java index 15de722be6..695227c751 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java @@ -35,6 +35,12 @@ public interface SourceFilesConfiguration { */ ImmutableList getDependenciesFiles(); + /** + * @return the source files for snapshot dependencies. These files should be in a deterministic + * order + */ + ImmutableList getSnapshotDependenciesFiles(); + /** * @return the source files for the resources layer. These files should be in a deterministic * order. @@ -48,7 +54,7 @@ public interface SourceFilesConfiguration { /** * @return the Unix-style path where the dependencies source files are placed in the container - * filesystem. Must end with slash. + * filesystem. Must end with slash. This includes both regular and snapshot dependencies. */ String getDependenciesPathOnImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index bff71a426b..0536ea01f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -86,6 +86,20 @@ static ImmutableList makeList( .build(), cache)); + // Adds a snapshot dependencies layer, if snapshot files present. + if (!sourceFilesConfiguration.getSnapshotDependenciesFiles().isEmpty()) { + buildLayerStepsBuilder.add( + new BuildAndCacheApplicationLayerStep( + "snapshot-dependencies", + listeningExecutorService, + buildConfiguration, + LayerConfiguration.builder() + .addEntry( + sourceFilesConfiguration.getSnapshotDependenciesFiles(), + sourceFilesConfiguration.getDependenciesPathOnImage()) + .build(), + cache)); + } // Adds the extra layer to be built, if configured. if (buildConfiguration.getExtraFilesLayerConfiguration() != null) { buildLayerStepsBuilder.add( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java index 91bbafd809..83275229f2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java @@ -136,14 +136,18 @@ public void generate(Path targetDirectory) throws IOException { // Creates the directories. Path dependenciesDir = targetDirectory.resolve("libs"); + Path snapshotDependenciesDir = targetDirectory.resolve("snapshot-libs"); Path resourcesDIr = targetDirectory.resolve("resources"); Path classesDir = targetDirectory.resolve("classes"); Files.createDirectory(dependenciesDir); + Files.createDirectories(snapshotDependenciesDir); Files.createDirectory(resourcesDIr); Files.createDirectory(classesDir); // Copies dependencies. FileOperations.copy(sourceFilesConfiguration.getDependenciesFiles(), dependenciesDir); + FileOperations.copy( + sourceFilesConfiguration.getSnapshotDependenciesFiles(), snapshotDependenciesDir); FileOperations.copy(sourceFilesConfiguration.getResourcesFiles(), resourcesDIr); FileOperations.copy(sourceFilesConfiguration.getClassesFiles(), classesDir); @@ -179,6 +183,8 @@ String makeDockerfile() throws JsonProcessingException { .append(Preconditions.checkNotNull(baseImage)) .append("\n\nCOPY libs ") .append(sourceFilesConfiguration.getDependenciesPathOnImage()) + .append("\nCOPY snapshot-libs ") + .append(sourceFilesConfiguration.getDependenciesPathOnImage()) .append("\nCOPY resources ") .append(sourceFilesConfiguration.getResourcesPathOnImage()) .append("\nCOPY classes ") diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java index 49d27ce489..33396e3295 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java @@ -28,16 +28,70 @@ /** Implementation of {@link SourceFilesConfiguration} that uses test resources. */ public class TestSourceFilesConfiguration implements SourceFilesConfiguration { + public static class Builder { + private ImmutableList dependenciesSourceFiles = ImmutableList.of(); + private ImmutableList snapshotDependenciesSourceFiles = ImmutableList.of(); + private ImmutableList resourcesSourceFiles = ImmutableList.of(); + private ImmutableList classesSourceFiles = ImmutableList.of(); + + public Builder withDependencies() throws IOException, URISyntaxException { + dependenciesSourceFiles = getFilesList("application/dependencies"); + return this; + } + + public Builder withSnapshotDependencies() throws IOException, URISyntaxException { + snapshotDependenciesSourceFiles = getFilesList("application/snapshot-dependencies"); + return this; + } + + public Builder withResources() throws IOException, URISyntaxException { + resourcesSourceFiles = getFilesList("application/resources"); + return this; + } + + public Builder withClasses() throws IOException, URISyntaxException { + classesSourceFiles = getFilesList("application/classes"); + return this; + } + + public TestSourceFilesConfiguration build() { + return new TestSourceFilesConfiguration( + dependenciesSourceFiles, + snapshotDependenciesSourceFiles, + resourcesSourceFiles, + classesSourceFiles); + } + + /** Lists the files in the {@code resourcePath} resources directory. */ + private ImmutableList getFilesList(String resourcePath) + throws URISyntaxException, IOException { + try (Stream fileStream = + Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { + return fileStream.collect(ImmutableList.toImmutableList()); + } + } + } + + public static Builder builder() { + return new Builder(); + } + private static final String EXTRACTION_PATH = "/some/extraction/path/"; private final ImmutableList dependenciesSourceFiles; + private final ImmutableList snapshotDependenciesSourceFiles; private final ImmutableList resourcesSourceFiles; private final ImmutableList classesSourceFiles; - public TestSourceFilesConfiguration() throws URISyntaxException, IOException { - dependenciesSourceFiles = getFilesList("application/dependencies"); - resourcesSourceFiles = getFilesList("application/resources"); - classesSourceFiles = getFilesList("application/classes"); + private TestSourceFilesConfiguration( + ImmutableList dependenciesSourceFiles, + ImmutableList snapshotDependenciesSourceFiles, + ImmutableList resourcesSourceFiles, + ImmutableList classesSourceFiles) { + this.dependenciesSourceFiles = dependenciesSourceFiles; + this.snapshotDependenciesSourceFiles = snapshotDependenciesSourceFiles; + this.resourcesSourceFiles = resourcesSourceFiles; + this.classesSourceFiles = classesSourceFiles; } @Override @@ -45,6 +99,11 @@ public ImmutableList getDependenciesFiles() { return dependenciesSourceFiles; } + @Override + public ImmutableList getSnapshotDependenciesFiles() { + return snapshotDependenciesSourceFiles; + } + @Override public ImmutableList getResourcesFiles() { return resourcesSourceFiles; @@ -69,13 +128,4 @@ public String getResourcesPathOnImage() { public String getClassesPathOnImage() { return EXTRACTION_PATH + "classes/"; } - - /** Lists the files in the {@code resourcePath} resources directory. */ - private ImmutableList getFilesList(String resourcePath) - throws URISyntaxException, IOException { - try (Stream fileStream = - Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { - return fileStream.collect(ImmutableList.toImmutableList()); - } - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 0c2049f855..eacaf0659b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.builder.TestBuildLogger; import com.google.cloud.tools.jib.builder.TestSourceFilesConfiguration; import com.google.cloud.tools.jib.cache.Cache; @@ -37,6 +38,7 @@ import java.nio.file.Paths; import java.util.concurrent.ExecutionException; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -49,30 +51,35 @@ @RunWith(MockitoJUnitRunner.class) public class BuildAndCacheApplicationLayerStepTest { + private static final String EXTRA_FILES_LAYER_EXTRACTION_PATH = "/extra"; + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; + private Path temporaryCacheDirectory; - @Test - public void testRun() - throws LayerPropertyNotFoundException, IOException, CacheMetadataCorruptedException, - URISyntaxException, ExecutionException { + @Before + public void setUp() throws IOException, URISyntaxException { Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestBuildLogger()); - TestSourceFilesConfiguration testSourceFilesConfiguration = new TestSourceFilesConfiguration(); - Path temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); + temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); + } - // Adds an extra file layer. + private ImmutableList configureExtraFilesLayer() throws URISyntaxException { ImmutableList extraFilesLayerSourceFiles = ImmutableList.of( Paths.get(Resources.getResource("fileA").toURI()), Paths.get(Resources.getResource("fileB").toURI())); - String extraFilesLayerExtractionPath = "/extra"; Mockito.when(mockBuildConfiguration.getExtraFilesLayerConfiguration()) .thenReturn( LayerConfiguration.builder() - .addEntry(extraFilesLayerSourceFiles, extraFilesLayerExtractionPath) + .addEntry(extraFilesLayerSourceFiles, EXTRA_FILES_LAYER_EXTRACTION_PATH) .build()); + return extraFilesLayerSourceFiles; + } + private ImageLayers configureAvailableLayers( + SourceFilesConfiguration testSourceFilesConfiguration) + throws CacheMetadataCorruptedException, IOException, ExecutionException { ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); ImageLayers applicationLayers; @@ -91,8 +98,30 @@ public void testRun() applicationLayers = applicationLayersBuilder.build(); cache.addCachedLayersWithMetadataToMetadata(applicationLayers.getLayers()); - Assert.assertEquals(4, applicationLayers.size()); } + return applicationLayers; + } + + @Test + public void testRun() + throws LayerPropertyNotFoundException, IOException, CacheMetadataCorruptedException, + URISyntaxException, ExecutionException { + + TestSourceFilesConfiguration testSourceFilesConfiguration = + TestSourceFilesConfiguration.builder() + .withDependencies() + .withSnapshotDependencies() + .withClasses() + .withResources() + .build(); + + // Adds an extra file layer. + ImmutableList extraFilesLayerSourceFiles = configureExtraFilesLayer(); + + // Populate the cache + ImageLayers applicationLayers = + configureAvailableLayers(testSourceFilesConfiguration); + Assert.assertEquals(5, applicationLayers.size()); // Re-initialize cache with the updated metadata. Cache cache = Cache.init(temporaryCacheDirectory); @@ -102,6 +131,11 @@ public void testRun() new LayerEntry( testSourceFilesConfiguration.getDependenciesFiles(), testSourceFilesConfiguration.getDependenciesPathOnImage())); + ImmutableList snapshotDependenciesLayerEntry = + ImmutableList.of( + new LayerEntry( + testSourceFilesConfiguration.getSnapshotDependenciesFiles(), + testSourceFilesConfiguration.getDependenciesPathOnImage())); ImmutableList resourcesLayerEntry = ImmutableList.of( new LayerEntry( @@ -113,7 +147,8 @@ public void testRun() testSourceFilesConfiguration.getClassesFiles(), testSourceFilesConfiguration.getClassesPathOnImage())); ImmutableList extraFilesLayerEntry = - ImmutableList.of(new LayerEntry(extraFilesLayerSourceFiles, extraFilesLayerExtractionPath)); + ImmutableList.of( + new LayerEntry(extraFilesLayerSourceFiles, EXTRA_FILES_LAYER_EXTRACTION_PATH)); // Verifies that the cached layers are up-to-date. CacheReader cacheReader = new CacheReader(cache); @@ -128,6 +163,11 @@ public void testRun() cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntry).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(3).getBlobDescriptor(), + cacheReader + .getUpToDateLayerByLayerEntries(snapshotDependenciesLayerEntry) + .getBlobDescriptor()); + Assert.assertEquals( + applicationLayers.get(4).getBlobDescriptor(), cacheReader.getUpToDateLayerByLayerEntries(extraFilesLayerEntry).getBlobDescriptor()); // Verifies that the cache reader gets the same layers as the newest application layers. @@ -139,6 +179,66 @@ public void testRun() Assert.assertEquals( applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(classesLayerEntry)); Assert.assertEquals( - applicationLayers.get(3).getContentFile(), cacheReader.getLayerFile(extraFilesLayerEntry)); + applicationLayers.get(3).getContentFile(), + cacheReader.getLayerFile(snapshotDependenciesLayerEntry)); + Assert.assertEquals( + applicationLayers.get(4).getContentFile(), cacheReader.getLayerFile(extraFilesLayerEntry)); + } + + @Test + public void testRun_emptyLayersIgnored() + throws IOException, URISyntaxException, CacheMetadataCorruptedException, ExecutionException { + + TestSourceFilesConfiguration testSourceFilesConfiguration = + TestSourceFilesConfiguration.builder() + .withDependencies() + .withClasses() + .withResources() + .build(); + + // Populate the cache + ImageLayers applicationLayers = + configureAvailableLayers(testSourceFilesConfiguration); + Assert.assertEquals(3, applicationLayers.size()); + + ImmutableList dependenciesLayerEntry = + ImmutableList.of( + new LayerEntry( + testSourceFilesConfiguration.getDependenciesFiles(), + testSourceFilesConfiguration.getDependenciesPathOnImage())); + ImmutableList resourcesLayerEntry = + ImmutableList.of( + new LayerEntry( + testSourceFilesConfiguration.getResourcesFiles(), + testSourceFilesConfiguration.getResourcesPathOnImage())); + ImmutableList classesLayerEntry = + ImmutableList.of( + new LayerEntry( + testSourceFilesConfiguration.getClassesFiles(), + testSourceFilesConfiguration.getClassesPathOnImage())); + + // Re-initialize cache with the updated metadata. + Cache cache = Cache.init(temporaryCacheDirectory); + + // Verifies that the cached layers are up-to-date. + CacheReader cacheReader = new CacheReader(cache); + Assert.assertEquals( + applicationLayers.get(0).getBlobDescriptor(), + cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntry).getBlobDescriptor()); + Assert.assertEquals( + applicationLayers.get(1).getBlobDescriptor(), + cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntry).getBlobDescriptor()); + Assert.assertEquals( + applicationLayers.get(2).getBlobDescriptor(), + cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntry).getBlobDescriptor()); + + // Verifies that the cache reader gets the same layers as the newest application layers. + Assert.assertEquals( + applicationLayers.get(0).getContentFile(), + cacheReader.getLayerFile(dependenciesLayerEntry)); + Assert.assertEquals( + applicationLayers.get(1).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntry)); + Assert.assertEquals( + applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(classesLayerEntry)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java index 89ac1936eb..8dc223fba9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java @@ -79,16 +79,22 @@ public void setUpMocks() { @Test public void testGenerate() throws IOException, URISyntaxException { Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); + Path testSnapshotDependencies = + Paths.get(Resources.getResource("application/snapshot-dependencies").toURI()); Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); ImmutableList expectedDependenciesFiles = new DirectoryWalker(testDependencies).filterRoot().walk(); + ImmutableList expectedSnapshotDependenciesFiles = + new DirectoryWalker(testSnapshotDependencies).filterRoot().walk(); ImmutableList expectedResourcesFiles = new DirectoryWalker(testResources).filterRoot().walk(); ImmutableList expectedClassesFiles = new DirectoryWalker(testClasses).filterRoot().walk(); Mockito.when(mockSourceFilesConfiguration.getDependenciesFiles()) .thenReturn(expectedDependenciesFiles); + Mockito.when(mockSourceFilesConfiguration.getSnapshotDependenciesFiles()) + .thenReturn(expectedSnapshotDependenciesFiles); Mockito.when(mockSourceFilesConfiguration.getResourcesFiles()) .thenReturn(expectedResourcesFiles); Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(expectedClassesFiles); @@ -107,6 +113,7 @@ public void testGenerate() throws IOException, URISyntaxException { Assert.assertTrue(Files.exists(targetDirectory.resolve("Dockerfile"))); assertSameFiles(targetDirectory.resolve("libs"), testDependencies); + assertSameFiles(targetDirectory.resolve("snapshot-libs"), testSnapshotDependencies); assertSameFiles(targetDirectory.resolve("resources"), testResources); assertSameFiles(targetDirectory.resolve("classes"), testClasses); } diff --git a/jib-core/src/test/resources/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar b/jib-core/src/test/resources/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 73d372b6d0..474ea0c4e2 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -1,6 +1,7 @@ FROM somebaseimage COPY libs /app/libs/ +COPY snapshot-libs /app/libs/ COPY resources /app/resources/ COPY classes /app/classes/ diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c3426cb817..16093e1766 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Added +- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) ### Changed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java index 1f22997656..56397b79b9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java @@ -43,6 +43,7 @@ static GradleSourceFilesConfiguration getForProject( } private final ImmutableList dependenciesFiles; + private final ImmutableList snapshotDependenciesFiles; private final ImmutableList resourcesFiles; private final ImmutableList classesFiles; @@ -55,6 +56,7 @@ private GradleSourceFilesConfiguration(Project project, GradleBuildLogger gradle SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); List dependenciesFiles = new ArrayList<>(); + List snapshotDependenciesFiles = new ArrayList<>(); List resourcesFiles = new ArrayList<>(); List classesFiles = new ArrayList<>(); @@ -92,11 +94,16 @@ private GradleSourceFilesConfiguration(Project project, GradleBuildLogger gradle if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { continue; } - dependenciesFiles.add(dependencyFile.toPath()); + if (dependencyFile.getName().contains("SNAPSHOT")) { + snapshotDependenciesFiles.add(dependencyFile.toPath()); + } else { + dependenciesFiles.add(dependencyFile.toPath()); + } } // Sorts all files by path for consistent ordering. this.dependenciesFiles = ImmutableList.sortedCopyOf(dependenciesFiles); + this.snapshotDependenciesFiles = ImmutableList.sortedCopyOf(snapshotDependenciesFiles); this.resourcesFiles = ImmutableList.sortedCopyOf(resourcesFiles); this.classesFiles = ImmutableList.sortedCopyOf(classesFiles); } @@ -106,6 +113,11 @@ public ImmutableList getDependenciesFiles() { return dependenciesFiles; } + @Override + public ImmutableList getSnapshotDependenciesFiles() { + return snapshotDependenciesFiles; + } + @Override public ImmutableList getResourcesFiles() { return resourcesFiles; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java index 54376a14c5..cd70617596 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java @@ -93,6 +93,11 @@ public void setUp() throws URISyntaxException, IOException { allFiles.add( Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()) .toFile()); + allFiles.add( + Paths.get( + Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") + .toURI()) + .toFile()); FileCollection runtimeFileCollection = new TestFileCollection(allFiles); Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); @@ -117,6 +122,11 @@ public void test_correctFiles() throws URISyntaxException { Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()), Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()), Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); + ImmutableList expectedSnapshotDependenciesFiles = + ImmutableList.of( + Paths.get( + Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") + .toURI())); ImmutableList expectedResourcesFiles = ImmutableList.of( Paths.get(Resources.getResource("application/resources").toURI()).resolve("resourceA"), @@ -130,6 +140,9 @@ public void test_correctFiles() throws URISyntaxException { Assert.assertEquals( expectedDependenciesFiles, testGradleSourceFilesConfiguration.getDependenciesFiles()); + Assert.assertEquals( + expectedSnapshotDependenciesFiles, + testGradleSourceFilesConfiguration.getSnapshotDependenciesFiles()); Assert.assertEquals( expectedResourcesFiles, testGradleSourceFilesConfiguration.getResourcesFiles()); Assert.assertEquals(expectedClassesFiles, testGradleSourceFilesConfiguration.getClassesFiles()); diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index fa707cb743..fbff4fe022 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Added +- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) ### Changed diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 5c4d3214ff..ee93d11df0 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -123,6 +123,20 @@ 2.12.0 test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + test + + + org.apache.maven + maven-compat + 3.5.4 + test + + diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java index 506b19da04..a3d919eaf5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java @@ -44,6 +44,7 @@ static MavenSourceFilesConfiguration getForProject(MavenProject project) throws } private final ImmutableList dependenciesFiles; + private final ImmutableList snapshotDependenciesFiles; private final ImmutableList resourcesFiles; private final ImmutableList classesFiles; @@ -53,12 +54,17 @@ private MavenSourceFilesConfiguration(MavenProject project) throws IOException { Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); List dependenciesFiles = new ArrayList<>(); + List snapshotDependenciesFiles = new ArrayList<>(); List resourcesFiles = new ArrayList<>(); List classesFiles = new ArrayList<>(); // Gets all the dependencies. for (Artifact artifact : project.getArtifacts()) { - dependenciesFiles.add(artifact.getFile().toPath()); + if (artifact.isSnapshot()) { + snapshotDependenciesFiles.add(artifact.getFile().toPath()); + } else { + dependenciesFiles.add(artifact.getFile().toPath()); + } } // Gets the classes files in the 'classes' output directory. It finds the files that are classes @@ -88,6 +94,7 @@ private MavenSourceFilesConfiguration(MavenProject project) throws IOException { // Sort all files by path for consistent ordering. this.dependenciesFiles = ImmutableList.sortedCopyOf(dependenciesFiles); + this.snapshotDependenciesFiles = ImmutableList.sortedCopyOf(snapshotDependenciesFiles); this.resourcesFiles = ImmutableList.sortedCopyOf(resourcesFiles); this.classesFiles = ImmutableList.sortedCopyOf(classesFiles); } @@ -97,6 +104,11 @@ public ImmutableList getDependenciesFiles() { return dependenciesFiles; } + @Override + public ImmutableList getSnapshotDependenciesFiles() { + return snapshotDependenciesFiles; + } + @Override public ImmutableList getResourcesFiles() { return resourcesFiles; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java index b91fb61401..f43e2c8cf3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java @@ -17,19 +17,20 @@ package com.google.cloud.tools.jib.maven; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -40,25 +41,30 @@ @RunWith(MockitoJUnitRunner.class) public class MavenSourceFilesConfigurationTest { + @Rule public TestRepository testRepository = new TestRepository(); + @Mock private MavenProject mockMavenProject; @Mock private Build mockBuild; private MavenSourceFilesConfiguration testMavenSourceFilesConfiguration; @Before - public void setUp() throws IOException, URISyntaxException { + public void setUp() throws IOException, URISyntaxException, ComponentLookupException { Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); Mockito.when(mockBuild.getSourceDirectory()).thenReturn(sourcePath.toString()); Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); + Set artifacts = - new HashSet<>( - Arrays.asList( - makeArtifact(Paths.get("application", "dependencies", "libraryB.jar")), - makeArtifact(Paths.get("application", "dependencies", "libraryA.jar")), - makeArtifact(Paths.get("application", "dependencies", "dependency-1.0.0.jar")))); + ImmutableSet.of( + makeArtifact(Paths.get("application", "dependencies", "libraryB.jar")), + makeArtifact(Paths.get("application", "dependencies", "libraryA.jar")), + // maven reads and populates "Artifacts" with it's own processing, so read some from + // a repository + testRepository.findArtifact("com.test", "dependency", "1.0.0"), + testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); testMavenSourceFilesConfiguration = @@ -68,10 +74,15 @@ public void setUp() throws IOException, URISyntaxException { @Test public void test_correctFiles() throws URISyntaxException { ImmutableList expectedDependenciesFiles = + // on windows, these files may be in a different order, so sort + ImmutableList.sortedCopyOf( + ImmutableList.of( + testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), + Paths.get("application", "dependencies", "libraryA.jar"), + Paths.get("application", "dependencies", "libraryB.jar"))); + ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( - Paths.get("application", "dependencies", "dependency-1.0.0.jar"), - Paths.get("application", "dependencies", "libraryA.jar"), - Paths.get("application", "dependencies", "libraryB.jar")); + testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); ImmutableList expectedResourcesFiles = ImmutableList.of( Paths.get(Resources.getResource("application/output/directory").toURI()), @@ -86,6 +97,9 @@ public void test_correctFiles() throws URISyntaxException { Assert.assertEquals( expectedDependenciesFiles, testMavenSourceFilesConfiguration.getDependenciesFiles()); + Assert.assertEquals( + expectedSnapshotDependenciesFiles, + testMavenSourceFilesConfiguration.getSnapshotDependenciesFiles()); Assert.assertEquals( expectedResourcesFiles, testMavenSourceFilesConfiguration.getResourcesFiles()); Assert.assertEquals(expectedClassesFiles, testMavenSourceFilesConfiguration.getClassesFiles()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java new file mode 100644 index 0000000000..701fa470ed --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java @@ -0,0 +1,69 @@ +package com.google.cloud.tools.jib.maven; + +import com.google.common.io.Resources; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.ArtifactHandler; +import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryFactory; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; +import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; +import org.apache.maven.artifact.resolver.ArtifactResolutionResult; +import org.apache.maven.artifact.resolver.ArtifactResolver; +import org.apache.maven.plugin.testing.MojoRule; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.junit.Assert; +import org.junit.rules.ExternalResource; + +/** A test helper to resolve artifacts from a local repository in test/resources */ +public class TestRepository extends ExternalResource { + + private static final String TEST_M2 = "testM2"; + + private MojoRule testHarness; + private ArtifactRepositoryFactory artifactRepositoryFactory; + private ArtifactHandlerManager artifactHandlerManager; + private ArtifactRepository testLocalRepo; + private ArtifactResolver artifactResolver; + private ArtifactHandler jarHandler; + + @Override + protected void before() + throws ComponentLookupException, URISyntaxException, MalformedURLException { + testHarness = new MojoRule(); + artifactRepositoryFactory = testHarness.lookup(ArtifactRepositoryFactory.class); + artifactHandlerManager = testHarness.lookup(ArtifactHandlerManager.class); + artifactResolver = testHarness.lookup(ArtifactResolver.class); + jarHandler = artifactHandlerManager.getArtifactHandler("jar"); + + testLocalRepo = + artifactRepositoryFactory.createArtifactRepository( + "test", + Resources.getResource(TEST_M2).toURI().toURL().toString(), + new DefaultRepositoryLayout(), + null, + null); + } + + public Artifact findArtifact(String group, String artifact, String version) { + ArtifactResolutionRequest artifactResolutionRequest = new ArtifactResolutionRequest(); + artifactResolutionRequest.setLocalRepository(testLocalRepo); + Artifact artifactToFind = + new DefaultArtifact(group, artifact, version, null, "jar", null, jarHandler); + + artifactResolutionRequest.setArtifact(artifactToFind); + + ArtifactResolutionResult ars = artifactResolver.resolve(artifactResolutionRequest); + + Assert.assertEquals(1, ars.getArtifacts().size()); + return ars.getArtifacts().iterator().next(); + } + + public Path artifactPathOnDisk(String group, String artifact, String version) { + return findArtifact(group, artifact, version).getFile().toPath(); + } +} diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories b/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories new file mode 100644 index 0000000000..d4aa0ba7cb --- /dev/null +++ b/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories @@ -0,0 +1,4 @@ +#NOTE: This is an Aether internal implementation file, its format can be changed without prior notice. +#Wed Jul 11 18:33:40 EDT 2018 +dependency-1.0.0.pom>= +dependency-1.0.0.jar>= diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..333ceb39b26dfc6384b092d18cee5733295b5b12 GIT binary patch literal 770 zcmWIWW@h1HVBp|jkd4;%VE_Uq5CH_7KE;?7qUY=O+4sz8A8%c~i@e^tTIbH3-yCFc#rVO~M^BlM3_Bc!YAZL; zu$0t-)V!3`yyQw0dqGN<#;Em5Gcqu&0Ai5cNJ`y{Qd3JZ^V0Q_a}tY-a|2HMF*ypX zNvpJf(%U(2Pb2&HL%xq&6=n(ev=qoss0r}MCP2FnzB>!p0NB=U#CXy43^H?yh|;Ft?_0= z>YJFKg2J23Z)H?_7wp<=sc6eF?NQkATOU-aA`2{nx#vILbRhU&Osmy3YvbJcU)wIo z>HnO1c2e*6cYRK2yYGfZZM^I4wEDL7w*`+(I-L67m&8QeJih)>ie#k-$64jKEoW?h z*Ey_^wUQUl{-&chVUj;1NBRi`?K#3zo0Quhc^zD2vCe=qU2ng`e9t8+GknijooafT z%OE<3Lp;DW*Su!l9R|O{KRwE1-K?9wE?M`HKfs%jNrV}9qyvK$2m~13I)Z3;5 + + 4.0.0 + com.test + dependency + 1.0.0 + POM was created from install:install-file + diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml new file mode 100644 index 0000000000..d10105b967 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + com.test + dependency + + 1.0.0 + + 1.0.0 + + 20180711223340 + + diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories new file mode 100644 index 0000000000..b12c035277 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories @@ -0,0 +1,4 @@ +#NOTE: This is an Aether internal implementation file, its format can be changed without prior notice. +#Wed Jul 11 18:35:24 EDT 2018 +dependencyX-1.0.0-SNAPSHOT.pom>= +dependencyX-1.0.0-SNAPSHOT.jar>= diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..333ceb39b26dfc6384b092d18cee5733295b5b12 GIT binary patch literal 770 zcmWIWW@h1HVBp|jkd4;%VE_Uq5CH_7KE;?7qUY=O+4sz8A8%c~i@e^tTIbH3-yCFc#rVO~M^BlM3_Bc!YAZL; zu$0t-)V!3`yyQw0dqGN<#;Em5Gcqu&0Ai5cNJ`y{Qd3JZ^V0Q_a}tY-a|2HMF*ypX zNvpJf(%U(2Pb2&HL%xq&6=n(ev=qoss0r}MCP2FnzB>!p0NB=U#CXy43^H?yh|;Ft?_0= z>YJFKg2J23Z)H?_7wp<=sc6eF?NQkATOU-aA`2{nx#vILbRhU&Osmy3YvbJcU)wIo z>HnO1c2e*6cYRK2yYGfZZM^I4wEDL7w*`+(I-L67m&8QeJih)>ie#k-$64jKEoW?h z*Ey_^wUQUl{-&chVUj;1NBRi`?K#3zo0Quhc^zD2vCe=qU2ng`e9t8+GknijooafT z%OE<3Lp;DW*Su!l9R|O{KRwE1-K?9wE?M`HKfs%jNrV}9qyvK$2m~13I)Z3;5 + + 4.0.0 + com.test + dependencyX + 1.0.0-SNAPSHOT + POM was created from install:install-file + diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..87f1ad7dcf --- /dev/null +++ b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,24 @@ + + + com.test + dependencyX + 1.0.0-SNAPSHOT + + + true + + 20180711223524 + + + jar + 1.0.0-SNAPSHOT + 20180711223524 + + + pom + 1.0.0-SNAPSHOT + 20180711223524 + + + + diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml new file mode 100644 index 0000000000..8f606ea131 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.test + dependencyX + + + 1.0.0-SNAPSHOT + + 20180711223524 + + From 5efd9ea1ba302d2d5c02607afe3773ddaf2f5ea8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 12 Jul 2018 18:40:28 -0400 Subject: [PATCH 0005/2020] Add jibBuildTar task to gradle plugin (#590) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../jib/gradle/JibPluginIntegrationTest.java | 24 +++ .../tools/jib/gradle/BuildDockerTask.java | 32 +-- .../cloud/tools/jib/gradle/BuildTarTask.java | 184 ++++++++++++++++++ .../tools/jib/gradle/DockerContextTask.java | 16 +- .../jib/gradle/GradleProjectProperties.java | 61 ++++++ .../cloud/tools/jib/gradle/JibPlugin.java | 7 + .../tools/jib/gradle/BuildDockerTaskTest.java | 68 ------- .../gradle/GradleProjectPropertiesTest.java | 30 +++ 9 files changed, 312 insertions(+), 112 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 16093e1766..7304cf16af 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ### Added - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 617788b5a5..278ba24d1f 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -157,6 +157,30 @@ public void testDockerDaemon_defaultTarget() throws IOException, InterruptedExce defaultTargetTestProject, "default-target-name:default-target-version")); } + @Test + public void testBuildTar_simple() throws IOException, InterruptedException { + String imageReference = "gcr.io/jib-integration-testing/simpleimage:gradle"; + String outputPath = + simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); + BuildResult buildResult = simpleTestProject.build("clean", JibPlugin.BUILD_TAR_TASK_NAME); + + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibBuildTarTask = buildResult.task(":" + JibPlugin.BUILD_TAR_TASK_NAME); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibBuildTarTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibBuildTarTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Built image tarball at ")); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); + + new Command("docker", "load", "--input", outputPath).run(); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", imageReference).run()); + } + @Test public void testDockerContext() throws IOException, InterruptedException { BuildResult buildResult = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index e3e95db1c5..58dcb9415f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -31,7 +31,6 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -97,8 +96,8 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); String mainClass = gradleProjectProperties.getMainClass(jibExtension); - - ImageReference targetImage = getDockerTag(gradleBuildLogger); + ImageReference targetImage = + gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageTask. @@ -152,31 +151,4 @@ BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Returns an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - * - * @param gradleBuildLogger the logger used to notify users of the target image parameter - * @return an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - */ - ImageReference getDockerTag(GradleBuildLogger gradleBuildLogger) - throws InvalidImageReferenceException { - Preconditions.checkNotNull(jibExtension); - if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { - // TODO: Validate that project name and version are valid repository/tag - // TODO: Use HelpfulSuggestions - gradleBuildLogger.lifecycle( - "Tagging image with generated image reference " - + getProject().getName() - + ":" - + getProject().getVersion().toString() - + ". If you'd like to specify a different tag, you can set the jib.to.image " - + "parameter in your build.gradle, or use the --image= commandline flag."); - return ImageReference.of(null, getProject().getName(), getProject().getVersion().toString()); - } else { - return ImageReference.parse(jibExtension.getTargetImage()); - } - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java new file mode 100644 index 0000000000..26debff462 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -0,0 +1,184 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; +import com.google.cloud.tools.jib.frontend.BuildStepsRunner; +import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; + +/** Builds a container image to a tarball. */ +public class BuildTarTask extends DefaultTask { + + /** {@code User-Agent} header suffix to send to the registry. */ + private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; + + private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = + HelpfulSuggestionsProvider.get("Building image tarball failed"); + + @Nullable private JibExtension jibExtension; + + /** + * This will call the property {@code "jib"} so that it is the same name as the extension. This + * way, the user would see error messages for missing configuration with the prefix {@code jib.}. + * + * @return the {@link JibExtension}. + */ + @Nested + @Nullable + public JibExtension getJib() { + return jibExtension; + } + + /** + * The target image can be overridden with the {@code --image} command line option. + * + * @param targetImage the name of the 'to' image. + */ + @Option(option = "image", description = "The image reference for the target image") + public void setTargetImage(String targetImage) { + Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); + } + + /** + * @return the input files to this task are all the output files for all the dependencies of the + * {@code classes} task. + */ + @InputFiles + public FileCollection getInputFiles() { + return GradleProjectProperties.getInputFiles( + Preconditions.checkNotNull(jibExtension).getExtraDirectory(), getProject()); + } + + /** + * The output file to check for task up-to-date. + * + * @return the output path + */ + @OutputFile + public String getOutputFile() { + return getTargetPath(); + } + + /** + * Returns the output directory for the tarball. By default, it is {@code build/jib-image.tar}. + * + * @return the output directory + */ + private String getTargetPath() { + return getProject().getBuildDir().toPath().resolve("jib-image.tar").toString(); + } + + @TaskAction + public void buildTar() throws InvalidImageReferenceException, IOException { + // Asserts required @Input parameters are not null. + Preconditions.checkNotNull(jibExtension); + GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); + jibExtension.handleDeprecatedParameters(gradleBuildLogger); + + RegistryCredentials knownBaseRegistryCredentials = null; + Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); + if (fromAuthorization != null) { + knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); + } + + GradleProjectProperties gradleProjectProperties = + GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); + String mainClass = gradleProjectProperties.getMainClass(jibExtension); + ImageReference targetImage = + gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); + + // Builds the BuildConfiguration. + // TODO: Consolidate with BuildImageTask/BuildDockerTask. + BuildConfiguration.Builder buildConfigurationBuilder = + BuildConfiguration.builder(gradleBuildLogger) + .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) + .setTargetImage(targetImage) + .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) + .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) + .setMainClass(mainClass) + .setJavaArguments(jibExtension.getArgs()) + .setJvmFlags(jibExtension.getJvmFlags()) + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) + .setAllowHttp(jibExtension.getAllowInsecureRegistries()); + if (Files.exists(jibExtension.getExtraDirectory().toPath())) { + try (Stream extraFilesLayerDirectoryFiles = + Files.list(jibExtension.getExtraDirectory().toPath())) { + buildConfigurationBuilder.setExtraFilesLayerConfiguration( + LayerConfiguration.builder() + .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") + .build()); + } + } + CacheConfiguration applicationLayersCacheConfiguration = + CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); + buildConfigurationBuilder.setApplicationLayersCacheConfiguration( + applicationLayersCacheConfiguration); + if (jibExtension.getUseOnlyProjectCache()) { + buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( + applicationLayersCacheConfiguration); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + + // TODO: Instead of disabling logging, have authentication credentials be provided + GradleBuildLogger.disableHttpLogging(); + + RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + + // Uses a directory in the Gradle build cache as the Jib cache. + try { + BuildStepsRunner.forBuildTar( + Paths.get(getTargetPath()), + buildConfiguration, + gradleProjectProperties.getSourceFilesConfiguration()) + .build(HELPFUL_SUGGESTIONS); + + } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { + throw new GradleException(ex.getMessage(), ex.getCause()); + } + } + + BuildTarTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 7a57bc17f3..5d80e7932b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -22,13 +22,9 @@ import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; -import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; @@ -60,15 +56,8 @@ public JibExtension getJib() { */ @InputFiles public FileCollection getInputFiles() { - Task classesTask = getProject().getTasks().getByPath("classes"); - Set classesDependencies = - classesTask.getTaskDependencies().getDependencies(classesTask); - - List dependencyFileCollections = new ArrayList<>(); - for (Task task : classesDependencies) { - dependencyFileCollections.add(task.getOutputs().getFiles()); - } - return getProject().files(dependencyFileCollections); + return GradleProjectProperties.getInputFiles( + Preconditions.checkNotNull(jibExtension).getExtraDirectory(), getProject()); } /** @@ -115,7 +104,6 @@ public void generateDockerContext() { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); String mainClass = gradleProjectProperties.getMainClass(jibExtension); - String targetDir = getTargetDir(); try { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 258c15fc7c..259ac8f3f0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -22,15 +22,23 @@ import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; import com.google.cloud.tools.jib.frontend.ProjectProperties; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ @@ -119,4 +127,57 @@ String getMainClass(JibExtension jibExtension) { throw new GradleException(ex.getMessage(), ex); } } + + /** + * Returns an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + * + * @param jibExtension the plugin configuration parameters to generate the name from + * @param gradleBuildLogger the logger used to notify users of the target image parameter + * @return an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + */ + ImageReference getGeneratedTargetDockerTag( + JibExtension jibExtension, GradleBuildLogger gradleBuildLogger) + throws InvalidImageReferenceException { + Preconditions.checkNotNull(jibExtension); + if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { + // TODO: Validate that project name and version are valid repository/tag + // TODO: Use HelpfulSuggestions + gradleBuildLogger.lifecycle( + "Tagging image with generated image reference " + + project.getName() + + ":" + + project.getVersion().toString() + + ". If you'd like to specify a different tag, you can set the jib.to.image " + + "parameter in your build.gradle, or use the --image= commandline flag."); + return ImageReference.of(null, project.getName(), project.getVersion().toString()); + } else { + return ImageReference.parse(jibExtension.getTargetImage()); + } + } + + /** + * Returns the input files for a task. + * + * @param extraDirectory the image's configured extra directory + * @param project the gradle project + * @return the input files to the task are all the output files for all the dependencies of the + * {@code classes} task + */ + static FileCollection getInputFiles(File extraDirectory, Project project) { + Task classesTask = project.getTasks().getByPath("classes"); + Set classesDependencies = + classesTask.getTaskDependencies().getDependencies(classesTask); + + List dependencyFileCollections = new ArrayList<>(); + for (Task task : classesDependencies) { + dependencyFileCollections.add(task.getOutputs().getFiles()); + } + if (Files.exists(extraDirectory.toPath())) { + return project.files(dependencyFileCollections, extraDirectory); + } else { + return project.files(dependencyFileCollections); + } + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index d98cabf990..336c8e88be 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -30,6 +30,7 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String JIB_EXTENSION_NAME = "jib"; @VisibleForTesting static final String BUILD_IMAGE_TASK_NAME = "jib"; + @VisibleForTesting static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; @@ -55,6 +56,11 @@ public void apply(Project project) { .getTasks() .create(BUILD_DOCKER_TASK_NAME, BuildDockerTask.class) .setJibExtension(jibExtension); + Task buildTarTask = + project + .getTasks() + .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) + .setJibExtension(jibExtension); // Has all tasks depend on the 'classes' task. project.afterEvaluate( @@ -65,6 +71,7 @@ public void apply(Project project) { buildImageTask.dependsOn(classesTask); dockerContextTask.dependsOn(classesTask); buildDockerTask.dependsOn(classesTask); + buildTarTask.dependsOn(classesTask); } catch (UnknownTaskException ex) { throw new GradleException( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java deleted file mode 100644 index 142332ee7e..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildDockerTaskTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class BuildDockerTaskTest { - - @Mock private JibExtension mockJibExtension; - @Mock private GradleBuildLogger mockBuildLogger; - - private BuildDockerTask task; - - @Before - public void setup() { - Project tempProject = ProjectBuilder.builder().withName("project-name").build(); - tempProject.setVersion("project-version"); - task = tempProject.getTasks().create("tempDockerTask", BuildDockerTask.class); - task.setJibExtension(mockJibExtension); - } - - @Test - public void testGetDockerTag_configured() throws InvalidImageReferenceException { - Mockito.when(mockJibExtension.getTargetImage()).thenReturn("a/b:c"); - ImageReference result = task.getDockerTag(mockBuildLogger); - Assert.assertEquals("a/b", result.getRepository()); - Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); - } - - @Test - public void testGetDockerTag_notConfigured() throws InvalidImageReferenceException { - Mockito.when(mockJibExtension.getTargetImage()).thenReturn(null); - ImageReference result = task.getDockerTag(mockBuildLogger); - Assert.assertEquals("project-name", result.getRepository()); - Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockBuildLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the jib.to.image parameter in your " - + "build.gradle, or use the --image= commandline flag."); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index f7c439d556..4e054165ad 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.Collections; @@ -43,6 +45,8 @@ public class GradleProjectPropertiesTest { @Mock private Project mockProject; @Mock private GradleBuildLogger mockGradleBuildLogger; @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; + @Mock private JibExtension mockJibExtension; + @Mock private GradleBuildLogger mockBuildLogger; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -77,4 +81,30 @@ public void testGetMainClassFromJar_multiple() { .thenReturn(ImmutableSet.of(mockJar, mockJar2)); Assert.assertEquals(null, gradleProjectProperties.getMainClassFromJar()); } + + @Test + public void testGetDockerTag_configured() throws InvalidImageReferenceException { + Mockito.when(mockJibExtension.getTargetImage()).thenReturn("a/b:c"); + ImageReference result = + gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockBuildLogger); + Assert.assertEquals("a/b", result.getRepository()); + Assert.assertEquals("c", result.getTag()); + Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); + } + + @Test + public void testGetDockerTag_notConfigured() throws InvalidImageReferenceException { + Mockito.when(mockProject.getName()).thenReturn("project-name"); + Mockito.when(mockProject.getVersion()).thenReturn("project-version"); + Mockito.when(mockJibExtension.getTargetImage()).thenReturn(null); + ImageReference result = + gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockBuildLogger); + Assert.assertEquals("project-name", result.getRepository()); + Assert.assertEquals("project-version", result.getTag()); + Mockito.verify(mockBuildLogger) + .lifecycle( + "Tagging image with generated image reference project-name:project-version. If you'd " + + "like to specify a different tag, you can set the jib.to.image parameter in your " + + "build.gradle, or use the --image= commandline flag."); + } } From ec9edc41371a81d16a021f8e2aa156ee256e8d5a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Jul 2018 11:28:54 -0400 Subject: [PATCH 0006/2020] Add jib:buildTar mojo to maven plugin (#593) --- jib-maven-plugin/CHANGELOG.md | 2 + .../tools/jib/maven/BuildDockerMojo.java | 33 +---- .../cloud/tools/jib/maven/BuildTarMojo.java | 131 ++++++++++++++++++ .../jib/maven/MavenProjectProperties.java | 29 ++++ .../maven/BuildDockerMojoIntegrationTest.java | 1 - .../tools/jib/maven/BuildDockerMojoTest.java | 65 --------- .../maven/BuildTarMojoIntegrationTest.java | 61 ++++++++ .../jib/maven/MavenProjectPropertiesTest.java | 26 ++++ proposals/{ => archives}/build_tarball.md | 0 9 files changed, 253 insertions(+), 95 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java rename proposals/{ => archives}/build_tarball.md (100%) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index fbff4fe022..617c0e628c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ### Added - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index ed35c4dfb2..3a40cdea6b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -30,7 +30,6 @@ import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -64,8 +63,11 @@ public void execute() throws MojoExecutionException { } // Parses 'from' and 'to' into image reference. + MavenProjectProperties mavenProjectProperties = + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); ImageReference baseImage = parseImageReference(getBaseImage(), "from"); - ImageReference targetImage = getDockerTag(mavenBuildLogger); + ImageReference targetImage = + mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); // Checks Maven settings for registry credentials. MavenSettingsServerCredentials mavenSettingsServerCredentials = @@ -73,8 +75,6 @@ public void execute() throws MojoExecutionException { RegistryCredentials knownBaseRegistryCredentials = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); String mainClass = mavenProjectProperties.getMainClass(this); // Builds the BuildConfiguration. @@ -128,29 +128,4 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } - - /** - * Returns an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - * - * @param mavenBuildLogger the logger used to notify users of the target image parameter - * @return an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - */ - ImageReference getDockerTag(MavenBuildLogger mavenBuildLogger) { - if (Strings.isNullOrEmpty(getTargetImage())) { - // TODO: Validate that project name and version are valid repository/tag - // TODO: Use HelpfulSuggestions - mavenBuildLogger.lifecycle( - "Tagging image with generated image reference " - + getProject().getName() - + ":" - + getProject().getVersion() - + ". If you'd like to specify a different tag, you can set the parameter " - + "in your pom.xml, or use the -Dimage= commandline flag."); - return ImageReference.of(null, getProject().getName(), getProject().getVersion()); - } else { - return parseImageReference(getTargetImage(), "to"); - } - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java new file mode 100644 index 0000000000..f4670b022b --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; +import com.google.cloud.tools.jib.frontend.BuildStepsRunner; +import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * Builds a container image and exports to disk at {@code ${project.build.directory}/jib-image.tar}. + */ +@Mojo( + name = BuildTarMojo.GOAL_NAME, + requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) +public class BuildTarMojo extends JibPluginConfiguration { + + @VisibleForTesting static final String GOAL_NAME = "buildTar"; + + /** {@code User-Agent} header suffix to send to the registry. */ + private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; + + private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = + HelpfulSuggestionsProvider.get("Building image tarball failed"); + + @Override + public void execute() throws MojoExecutionException { + MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); + handleDeprecatedParameters(mavenBuildLogger); + + // Parses 'from' and 'to' into image reference. + MavenProjectProperties mavenProjectProperties = + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); + ImageReference baseImage = parseImageReference(getBaseImage(), "from"); + ImageReference targetImage = + mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); + + // Checks Maven settings for registry credentials. + MavenSettingsServerCredentials mavenSettingsServerCredentials = + new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings()); + RegistryCredentials knownBaseRegistryCredentials = + mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + + String mainClass = mavenProjectProperties.getMainClass(this); + + // Builds the BuildConfiguration. + // TODO: Consolidate with BuildImageMojo. + BuildConfiguration.Builder buildConfigurationBuilder = + BuildConfiguration.builder(mavenBuildLogger) + .setBaseImage(baseImage) + .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) + .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) + .setTargetImage(targetImage) + .setMainClass(mainClass) + .setJavaArguments(getArgs()) + .setJvmFlags(getJvmFlags()) + .setEnvironment(getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) + .setAllowHttp(getAllowInsecureRegistries()); + if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { + try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { + buildConfigurationBuilder.setExtraFilesLayerConfiguration( + LayerConfiguration.builder() + .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") + .build()); + + } catch (IOException ex) { + throw new MojoExecutionException( + "Failed to list directory for extra files: " + getExtraDirectory(), ex); + } + } + CacheConfiguration applicationLayersCacheConfiguration = + CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); + buildConfigurationBuilder.setApplicationLayersCacheConfiguration( + applicationLayersCacheConfiguration); + if (getUseOnlyProjectCache()) { + buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( + applicationLayersCacheConfiguration); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + + // TODO: Instead of disabling logging, have authentication credentials be provided + MavenBuildLogger.disableHttpLogging(); + + RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + + try { + BuildStepsRunner.forBuildTar( + Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), + buildConfiguration, + mavenProjectProperties.getSourceFilesConfiguration()) + .build(HELPFUL_SUGGESTIONS); + getLog().info(""); + + } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { + throw new MojoExecutionException(ex.getMessage(), ex.getCause()); + } + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a660124925..45239fc581 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -22,7 +22,9 @@ import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; import com.google.cloud.tools.jib.frontend.ProjectProperties; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -142,4 +144,31 @@ String getMainClass(JibPluginConfiguration jibPluginConfiguration) throws MojoEx throw new MojoExecutionException(ex.getMessage(), ex); } } + + /** + * Returns an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + * + * @param targetImage the configured target image reference (can be empty) + * @param mavenBuildLogger the logger used to notify users of the target image parameter + * @return an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + */ + ImageReference getGeneratedTargetDockerTag( + @Nullable String targetImage, MavenBuildLogger mavenBuildLogger) { + if (Strings.isNullOrEmpty(targetImage)) { + // TODO: Validate that project name and version are valid repository/tag + // TODO: Use HelpfulSuggestions + mavenBuildLogger.lifecycle( + "Tagging image with generated image reference " + + project.getName() + + ":" + + project.getVersion() + + ". If you'd like to specify a different tag, you can set the parameter " + + "in your pom.xml, or use the -Dimage= commandline flag."); + return ImageReference.of(null, project.getName(), project.getVersion()); + } else { + return JibPluginConfiguration.parseImageReference(targetImage, "to"); + } + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index e1d11ea795..44198ae97b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -51,7 +51,6 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe verifier.setAutoclean(false); verifier.executeGoal("package"); - // Builds twice, and checks if the second build took less time. verifier.executeGoal("jib:" + BuildDockerMojo.GOAL_NAME); verifier.verifyErrorFreeLog(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoTest.java deleted file mode 100644 index 93c58b3414..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.image.ImageReference; -import org.apache.maven.project.MavenProject; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -public class BuildDockerMojoTest { - - @Mock private MavenProject mavenProject; - @Mock private MavenBuildLogger mockBuildLogger; - - @InjectMocks private BuildDockerMojo buildDockerMojo; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - Mockito.when(mavenProject.getName()).thenReturn("project-name"); - Mockito.when(mavenProject.getVersion()).thenReturn("project-version"); - buildDockerMojo.setProject(mavenProject); - } - - @Test - public void testGetDockerTag_configured() { - buildDockerMojo.setTargetImage("a/b:c"); - ImageReference result = buildDockerMojo.getDockerTag(mockBuildLogger); - Assert.assertEquals("a/b", result.getRepository()); - Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); - } - - @Test - public void testGetDockerTag_notConfigured() { - buildDockerMojo.setTargetImage(null); - ImageReference result = buildDockerMojo.getDockerTag(mockBuildLogger); - Assert.assertEquals("project-name", result.getRepository()); - Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockBuildLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the parameter in your " - + "pom.xml, or use the -Dimage= commandline flag."); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java new file mode 100644 index 0000000000..0f5c060972 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.Command; +import java.io.IOException; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +public class BuildTarMojoIntegrationTest { + + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + + /** + * Builds and runs jib:buildTar on a project at {@code projectRoot} pushing to {@code + * imageReference}. + */ + @Test + public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setAutoclean(false); + verifier.executeGoal("package"); + + verifier.executeGoal("jib:" + BuildTarMojo.GOAL_NAME); + verifier.verifyErrorFreeLog(); + + new Command( + "docker", + "load", + "--input", + simpleTestProject + .getProjectRoot() + .resolve("target") + .resolve("jib-image.tar") + .toString()) + .run(); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "gcr.io/jib-integration-testing/simpleimage:maven").run()); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 30cd0308eb..8bc97e4fd6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; @@ -36,6 +37,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenBuildLogger mockMavenBuildLogger; @Mock private SourceFilesConfiguration mockSourcesFilesConfiguration; @Mock private Plugin mockJarPlugin; + @Mock private MavenBuildLogger mockBuildLogger; private Xpp3Dom jarPluginConfiguration; private Xpp3Dom archive; @@ -46,6 +48,8 @@ public class MavenProjectPropertiesTest { @Before public void setup() { + Mockito.when(mockMavenProject.getName()).thenReturn("project-name"); + Mockito.when(mockMavenProject.getVersion()).thenReturn("project-version"); mavenProjectProperties = new MavenProjectProperties( mockMavenProject, mockMavenBuildLogger, mockSourcesFilesConfiguration); @@ -110,4 +114,26 @@ public void testGetMainClassFromJar_missingConfiguration() { public void testGetMainClassFromJar_missingPlugin() { Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); } + + @Test + public void testGetDockerTag_configured() { + ImageReference result = + mavenProjectProperties.getGeneratedTargetDockerTag("a/b:c", mockBuildLogger); + Assert.assertEquals("a/b", result.getRepository()); + Assert.assertEquals("c", result.getTag()); + Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); + } + + @Test + public void testGetDockerTag_notConfigured() { + ImageReference result = + mavenProjectProperties.getGeneratedTargetDockerTag(null, mockBuildLogger); + Assert.assertEquals("project-name", result.getRepository()); + Assert.assertEquals("project-version", result.getTag()); + Mockito.verify(mockBuildLogger) + .lifecycle( + "Tagging image with generated image reference project-name:project-version. If you'd " + + "like to specify a different tag, you can set the parameter in your " + + "pom.xml, or use the -Dimage= commandline flag."); + } } diff --git a/proposals/build_tarball.md b/proposals/archives/build_tarball.md similarity index 100% rename from proposals/build_tarball.md rename to proposals/archives/build_tarball.md From d455edee4c0b9995fee1ab0d92abb9ad60dc52ca Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 13 Jul 2018 09:19:04 -0700 Subject: [PATCH 0007/2020] Remove unthrown exception (#602) --- .../tools/jib/maven/MavenSourceFilesConfigurationTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java index f43e2c8cf3..cecf83e1f2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java @@ -27,7 +27,6 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; -import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -49,7 +48,7 @@ public class MavenSourceFilesConfigurationTest { private MavenSourceFilesConfiguration testMavenSourceFilesConfiguration; @Before - public void setUp() throws IOException, URISyntaxException, ComponentLookupException { + public void setUp() throws IOException, URISyntaxException { Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); From f436cad635c34c1cbaaa4eba1539551e4907a34f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Jul 2018 12:50:53 -0400 Subject: [PATCH 0008/2020] Fix changelogs for 0.9.6 (#603) --- jib-gradle-plugin/CHANGELOG.md | 4 ++++ jib-maven-plugin/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7304cf16af..541ff3765e 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.9.6 + +### Fixed + - Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 617c0e628c..e62c65a58a 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,6 +12,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.9.6 + +### Fixed + - Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 From 78c0dcbd86f2171909618dbf3c8500a443e88b4a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 13 Jul 2018 14:38:33 -0400 Subject: [PATCH 0009/2020] Links to Jib as a library for Java issue in README. (#610) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 234cc0a27e..0606246b98 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Jib builds Docker and OCI images for your Java applications and is available as [Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin).\ [Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin). -*Jib as a container-building library for Java is work-in-progress. Watch for updates.* +*Jib as a container-building library for Java is work-in-progress. [Watch for updates.](https://github.com/GoogleContainerTools/jib/issues/337)* ## Goals From db524dd2c6fef76ee5af561c81f97b7f37e14505 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 13 Jul 2018 15:10:35 -0400 Subject: [PATCH 0010/2020] Adds link to blog post in README. (#611) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0606246b98..a29cfd3d0a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). ## How Jib Works -Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. +Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html). See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). From cfdc014c66321965c50ceb4af98c4fd6e4cedb76 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Jul 2018 17:49:54 -0400 Subject: [PATCH 0011/2020] Add error message for credentials not being sent over HTTP (#613) --- .../tools/jib/frontend/BuildStepsRunner.java | 5 +++ .../jib/frontend/HelpfulSuggestions.java | 14 +++++--- .../RegistryCredentialsNotSentException.java | 36 +++++++++++++++++++ .../jib/registry/RegistryEndpointCaller.java | 28 ++++++++++++--- .../jib/frontend/BuildStepsRunnerTest.java | 20 +++++++++++ .../jib/frontend/HelpfulSuggestionsTest.java | 3 ++ .../registry/RegistryEndpointCallerTest.java | 35 ++++++++++++++++++ 7 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 466a2389a4..01a6e337f8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -232,6 +233,10 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut buildConfiguration, helpfulSuggestions); + } else if (exceptionDuringBuildSteps instanceof RegistryCredentialsNotSentException) { + throw new BuildStepsExecutionException( + helpfulSuggestions.forCredentialsNotSent(), exceptionDuringBuildSteps); + } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { handleRegistryUnauthorizedException( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java index 602f4dc38d..61274949b6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java @@ -96,6 +96,10 @@ public String forCredentialsNotCorrect(String registry) { return suggest("make sure your credentials for '" + registry + "' are set up correctly"); } + public String forCredentialsNotSent() { + return suggest("use a registry that supports HTTPS so credentials can be sent safely"); + } + public String forDockerContextInsecureRecursiveDelete(String directory) { return suggest("clear " + directory + " manually before creating the Docker context"); } @@ -108,6 +112,11 @@ public String forDockerNotInstalled() { return suggest("make sure Docker is installed and you have correct privileges to run it"); } + public String forInsecureRegistry() { + return suggest( + "use a registry that supports HTTPS or set the configuration parameter 'allowInsecureRegistries'"); + } + /** * @param parameter the parameter name (e.g. 'to.image' or {@literal }) * @param buildConfigFilename the name of the build config (build.gradle or pom.xml) @@ -145,9 +154,4 @@ private String forNoCredentialHelpersDefined( + "' or " + authConfiguration); } - - public String forInsecureRegistry() { - return suggest( - "use a registry that supports HTTPS or set the configuration parameter 'allowInsecureRegistries'"); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java new file mode 100644 index 0000000000..11fa8623c1 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +/** Thrown when registry request was unauthorized because credentials weren't sent. */ +public class RegistryCredentialsNotSentException extends RegistryException { + + /** + * Identifies the image registry and repository that denied access. + * + * @param registry the image registry + * @param repository the image repository + */ + RegistryCredentialsNotSentException(String registry, String repository) { + super( + "Required credentials for " + + registry + + "/" + + repository + + " were not sent because the connection was over HTTP"); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 2bd54ec75e..0d73d325fa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -52,7 +52,8 @@ class RegistryEndpointCaller { private static final String DEFAULT_PROTOCOL = "https"; /** Maintains the state of a request. This is used to retry requests with different parameters. */ - private static class RequestState { + @VisibleForTesting + static class RequestState { @Nullable private final Authorization authorization; private final URL url; @@ -61,7 +62,8 @@ private static class RequestState { * @param authorization authentication credentials * @param url the endpoint URL to call */ - private RequestState(@Nullable Authorization authorization, URL url) { + @VisibleForTesting + RequestState(@Nullable Authorization authorization, URL url) { this.authorization = authorization; this.url = url; } @@ -148,8 +150,9 @@ T call() throws IOException, RegistryException { * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ + @VisibleForTesting @Nullable - private T call(RequestState requestState) throws IOException, RegistryException { + T call(RequestState requestState) throws IOException, RegistryException { boolean isHttpProtocol = "http".equals(requestState.url.getProtocol()); if (!allowHttp && isHttpProtocol) { throw new InsecureRegistryException(requestState.url); @@ -193,13 +196,28 @@ private T call(RequestState requestState) throws IOException, RegistryException throw registryErrorExceptionBuilder.build(); - } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED - || httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { throw new RegistryUnauthorizedException( registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), httpResponseException); + } else if (httpResponseException.getStatusCode() + == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { + if (isHttpProtocol) { + // Using HTTP, so credentials weren't sent. + throw new RegistryCredentialsNotSentException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName()); + + } else { + // Using HTTPS, so credentials are missing. + throw new RegistryUnauthorizedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), + httpResponseException); + } + } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT || httpResponseException.getStatusCode() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index cd1800ddef..946c14074a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.registry.InsecureRegistryException; +import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -64,6 +65,7 @@ public class BuildStepsRunnerTest { @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; @Mock private BuildLogger mockBuildLogger; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; + @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @Mock private ExecutionException mockExecutionException; @Mock private BuildConfiguration mockBuildConfiguration; @@ -215,6 +217,24 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr } } + @Test + public void testBuildImage_executionException_registryCredentialsNotSentException() + throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, + CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + Mockito.when(mockExecutionException.getCause()) + .thenReturn(mockRegistryCredentialsNotSentException); + Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + + try { + testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage()); + Assert.assertEquals(mockRegistryCredentialsNotSentException, ex.getCause()); + } + } + @Test public void testBuildImage_executionException_registryUnauthorizedException_other() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java index 3531af8308..df8ad35397 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java @@ -68,5 +68,8 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should add a parameter configuration parameter to your buildFile or set the parameter via the commandline (e.g. 'command').", TEST_HELPFUL_SUGGESTIONS.forToNotConfigured("parameter", "buildFile", "command")); Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); + Assert.assertEquals( + "messagePrefix, perhaps you should use a registry that supports HTTPS so credentials can be sent safely", + TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index d1601ccfcc..e1c6f22c04 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.RegistryEndpointCaller.RequestState; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; import java.io.IOException; @@ -143,6 +144,40 @@ public void testCall_unauthorized() throws IOException, RegistryException { verifyThrowsRegistryUnauthorizedException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); } + @Test + public void testCall_credentialsNotSent() throws IOException, RegistryException { + // Mocks a response for temporary redirect to a new location. + Mockito.when(mockHttpResponse.getStatusCode()) + .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + Mockito.when(mockHttpResponse.getHeaders()) + .thenReturn(new HttpHeaders().setLocation("http://location")); + + HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(httpResponseException) + .thenReturn(mockResponse); + + RegistryEndpointCaller testRegistryEndpointCallerInsecure = + new RegistryEndpointCaller<>( + "userAgent", + "apiRouteBase", + new TestRegistryEndpointProvider(), + Authorizations.withBasicToken("token"), + new RegistryEndpointRequestProperties("serverUrl", "imageName"), + true, + mockConnectionFactory); + try { + testRegistryEndpointCallerInsecure.call( + new RequestState(Authorizations.withBasicToken("token"), new URL("http://location"))); + Assert.fail("Call should have failed"); + + } catch (RegistryCredentialsNotSentException ex) { + Assert.assertEquals( + "Required credentials for serverUrl/imageName were not sent because the connection was over HTTP", + ex.getMessage()); + } + } + @Test public void testCall_forbidden() throws IOException, RegistryException { verifyThrowsRegistryUnauthorizedException(HttpStatusCodes.STATUS_CODE_FORBIDDEN); From f065c30376ee09f443e07d805e93a8a80bcc0c35 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Sat, 14 Jul 2018 19:43:03 -0700 Subject: [PATCH 0012/2020] Apply Maven SettingsDecrypter for encrypted server details (#609) --- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 3 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../jib/maven/JibPluginConfiguration.java | 4 + .../maven/MavenSettingsServerCredentials.java | 75 ++++++++++- .../MavenSettingsServerCredentialsTest.java | 126 +++++++++++++++++- 6 files changed, 202 insertions(+), 12 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 3a40cdea6b..0954b7b7a2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -71,7 +71,8 @@ public void execute() throws MojoExecutionException { // Checks Maven settings for registry credentials. MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings()); + new MavenSettingsServerCredentials( + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); RegistryCredentials knownBaseRegistryCredentials = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 3159bc05fb..ac3d6aec63 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -87,7 +87,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Checks Maven settings for registry credentials. MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings()); + new MavenSettingsServerCredentials( + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); RegistryCredentials knownBaseRegistryCredentials = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); RegistryCredentials knownTargetRegistryCredentials = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index f4670b022b..e942fafc21 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -69,7 +69,8 @@ public void execute() throws MojoExecutionException { // Checks Maven settings for registry credentials. MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings()); + new MavenSettingsServerCredentials( + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); RegistryCredentials knownBaseRegistryCredentials = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 4fb8108931..3d0ad68e17 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -30,9 +30,11 @@ import javax.annotation.Nullable; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.crypto.SettingsDecrypter; /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ abstract class JibPluginConfiguration extends AbstractMojo { @@ -164,6 +166,8 @@ void handleDeprecatedParameters(BuildLogger logger) { @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) private String extraDirectory; + @Nullable @Component protected SettingsDecrypter settingsDecrypter; + MavenProject getProject() { return Preconditions.checkNotNull(project); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 1e2d4fd397..0501bdfbb6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -19,9 +19,17 @@ import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nullable; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; /** * Retrieves credentials for servers defined in Maven + * password encryption. Such passwords appear between unescaped braces. + */ + @VisibleForTesting + static boolean isEncrypted(String password) { + Matcher matcher = ENCRYPTED_STRING_PATTERN.matcher(password); + return matcher.matches() || matcher.find(); + } + private final Settings settings; + @Nullable private final SettingsDecrypter settingsDecrypter; + private final MavenBuildLogger mavenBuildLogger; - MavenSettingsServerCredentials(Settings settings) { + /** + * Create new instance. + * + * @param settings the Maven settings object + * @param settingsDecrypter the Maven decrypter component + * @param mavenBuildLogger the Maven build log + */ + MavenSettingsServerCredentials( + Settings settings, + @Nullable SettingsDecrypter settingsDecrypter, + MavenBuildLogger mavenBuildLogger) { this.settings = settings; + this.settingsDecrypter = settingsDecrypter; + this.mavenBuildLogger = mavenBuildLogger; } /** @@ -42,21 +80,48 @@ class MavenSettingsServerCredentials { * * @param registry the registry * @return the credentials for the registry + * @throws MojoExecutionException if the credentials could not be retrieved */ @Nullable - RegistryCredentials retrieve(@Nullable String registry) { + RegistryCredentials retrieve(@Nullable String registry) throws MojoExecutionException { if (registry == null) { return null; } - Server registryServerSettings = settings.getServer(registry); - if (registryServerSettings == null) { + Server registryServer = settings.getServer(registry); + if (registryServer == null) { return null; } + if (settingsDecrypter != null) { + // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return + // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers + // can be empty. We handle the results as follows: + // - if there are any ERROR or FATAL problems reported, then decryption failed + // - if no decrypted servers returned then treat as if no decryption was required + SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(registryServer); + SettingsDecryptionResult result = settingsDecrypter.decrypt(request); + // un-encrypted passwords are passed through, so a problem indicates a real issue + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new MojoExecutionException( + "Unable to decrypt password for " + registry + ": " + problem); + } + } + if (result.getServer() != null) { + registryServer = result.getServer(); + } + } else if (isEncrypted(registryServer.getPassword())) { + mavenBuildLogger.warn( + "Server password for registry " + + registry + + " appears to be encrypted, but there is no decrypter available"); + } + return new RegistryCredentials( CREDENTIAL_SOURCE, Authorizations.withBasicCredentials( - registryServerSettings.getUsername(), registryServerSettings.getPassword())); + registryServer.getUsername(), registryServer.getPassword())); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 4fa507cde0..8b03444ee4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -19,8 +19,13 @@ import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import java.util.Collections; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -35,16 +40,18 @@ public class MavenSettingsServerCredentialsTest { @Mock private Settings mockSettings; @Mock private Server mockServer1; + @Mock private MavenBuildLogger mockLogger; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; @Before public void setUp() { - testMavenSettingsServerCredentials = new MavenSettingsServerCredentials(mockSettings); + testMavenSettingsServerCredentials = + new MavenSettingsServerCredentials(mockSettings, null, mockLogger); } @Test - public void testRetrieve_found() { + public void testRetrieve_found() throws MojoExecutionException { Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); @@ -63,10 +70,11 @@ public void testRetrieve_found() { Assert.assertEquals( Authorizations.withBasicCredentials("server1 username", "server1 password").toString(), retrievedServer1Authorization.toString()); + Mockito.verifyZeroInteractions(mockLogger); } @Test - public void testRetrieve_notFound() { + public void testRetrieve_notFound() throws MojoExecutionException { RegistryCredentials registryCredentials = testMavenSettingsServerCredentials.retrieve("serverUnknown"); @@ -74,9 +82,119 @@ public void testRetrieve_notFound() { } @Test - public void testRetrieve_withNullServer() { + public void testRetrieve_withNullServer() throws MojoExecutionException { RegistryCredentials registryCredentials = testMavenSettingsServerCredentials.retrieve(null); Assert.assertNull(registryCredentials); } + + @Test + public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionException { + Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); + Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); + Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); + + RegistryCredentials registryCredentials = + testMavenSettingsServerCredentials.retrieve("server1"); + + Assert.assertNotNull(registryCredentials); + Assert.assertEquals( + MavenSettingsServerCredentials.CREDENTIAL_SOURCE, + registryCredentials.getCredentialSource()); + + Authorization retrievedServer1Authorization = registryCredentials.getAuthorization(); + Assert.assertNotNull(retrievedServer1Authorization); + Assert.assertEquals( + Authorizations.withBasicCredentials("server1 username", "{COQLCE6DU6GtcS5P=}").toString(), + retrievedServer1Authorization.toString()); + Mockito.verify(mockLogger) + .warn( + "Server password for registry server1 appears to be encrypted, " + + "but there is no decrypter available"); + } + + @Test + public void testRetrieve_withDecrypter_success() throws MojoExecutionException { + SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class); + Mockito.when(mockResult.getProblems()).thenReturn(Collections.emptyList()); + Mockito.when(mockResult.getServer()).thenReturn(mockServer1); + + // don't actually perform encryption/decryption + SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); + Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); + testMavenSettingsServerCredentials = + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger); + + // essentially the same as testRetrieve_found() + Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); + Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); + Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); + + RegistryCredentials registryCredentials = + testMavenSettingsServerCredentials.retrieve("server1"); + + Assert.assertNotNull(registryCredentials); + Assert.assertEquals( + MavenSettingsServerCredentials.CREDENTIAL_SOURCE, + registryCredentials.getCredentialSource()); + + Authorization retrievedServer1Authorization = registryCredentials.getAuthorization(); + Assert.assertNotNull(retrievedServer1Authorization); + Assert.assertEquals( + Authorizations.withBasicCredentials("server1 username", "server1 password").toString(), + retrievedServer1Authorization.toString()); + + Mockito.verify(mockDecrypter).decrypt(Mockito.any()); + Mockito.verify(mockResult).getProblems(); + Mockito.verify(mockResult, Mockito.atLeastOnce()).getServer(); + } + + @Test + public void testRetrieve_withDecrypter_failure() { + + SettingsProblem mockProblem = Mockito.mock(SettingsProblem.class); + Mockito.when(mockProblem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR); + // Maven's SettingsProblem has a more structured toString, but irrelevant here + Mockito.when(mockProblem.toString()).thenReturn("MockProblemText"); + + SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class); + Mockito.when(mockResult.getProblems()).thenReturn(Collections.singletonList(mockProblem)); + + // return an result with problems + SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); + Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); + testMavenSettingsServerCredentials = + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger); + + // essentially the same as testRetrieve_found() + Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); + + try { + testMavenSettingsServerCredentials.retrieve("server1"); + Assert.fail("decryption should have failed"); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + ex.getMessage(), "Unable to decrypt password for server1: MockProblemText"); + Mockito.verify(mockDecrypter).decrypt(Mockito.any()); + Mockito.verify(mockResult).getProblems(); + Mockito.verifyNoMoreInteractions(mockResult); // getServer() should never be called + } + } + + @Test + public void testIsEncrypted_plaintext() { + Assert.assertFalse(MavenSettingsServerCredentials.isEncrypted("plain text")); + } + + @Test + public void testIsEncrypted_encryptedPayload() { + String examples[] = { + "{COQLCE6DU6GtcS5P=}", + "expires on 2009-04-11 {COQLCE6DU6GtcS5P=}", // with note + "{jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+\\{EF1iFQyJQ=}" // with escaped brace + }; + for (String payload : examples) { + Assert.assertTrue(MavenSettingsServerCredentials.isEncrypted(payload)); + } + } } From 4ec77da2d86d714ab3eae7c57a2c5a8d3d4bad99 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Sat, 14 Jul 2018 20:14:39 -0700 Subject: [PATCH 0013/2020] Use registry for missing service in 'bearer' auth challenge (#614) --- .../AuthenticationMethodRetriever.java | 2 +- .../jib/registry/RegistryAuthenticator.java | 17 +++++---- ...BuildAndCacheApplicationLayerStepTest.java | 2 +- .../registry/RegistryAuthenticatorTest.java | 36 ++++++++++--------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index b18f655bb2..20da894e47 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -96,7 +96,7 @@ public RegistryAuthenticator handleHttpResponseException( // Parses the header to retrieve the components. try { return RegistryAuthenticator.fromAuthenticationMethod( - authenticationMethod, registryEndpointRequestProperties.getImageName()); + authenticationMethod, registryEndpointRequestProperties); } catch (RegistryAuthenticationFailedException ex) { throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index b038bf5cdf..cdce382ec0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -108,6 +108,7 @@ public static Initializer initializer(String serverUrl, String repository) { * Instantiates from parsing a {@code WWW-Authenticate} header. * * @param authenticationMethod the {@code WWW-Authenticate} header value + * @param registryEndpointRequestProperties the registry request properties * @param repository the repository/image name * @return a new {@link RegistryAuthenticator} for authenticating with the registry service * @throws RegistryAuthenticationFailedException if authentication fails @@ -116,7 +117,9 @@ public static Initializer initializer(String serverUrl, String repository) { */ @Nullable static RegistryAuthenticator fromAuthenticationMethod( - String authenticationMethod, String repository) throws RegistryAuthenticationFailedException { + String authenticationMethod, + RegistryEndpointRequestProperties registryEndpointRequestProperties) + throws RegistryAuthenticationFailedException { // If the authentication method starts with 'basic ' (case insensitive), no registry // authentication is needed. if (authenticationMethod.matches("^(?i)(basic) .*")) { @@ -137,12 +140,14 @@ static RegistryAuthenticator fromAuthenticationMethod( Pattern servicePattern = Pattern.compile("service=\"(.*?)\""); Matcher serviceMatcher = servicePattern.matcher(authenticationMethod); - if (!serviceMatcher.find()) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "service"); - } - String service = serviceMatcher.group(1); + // use the provided registry location when missing service (e.g., for OpenShift) + String service = + serviceMatcher.find() + ? serviceMatcher.group(1) + : registryEndpointRequestProperties.getServerUrl(); - return new RegistryAuthenticator(realm, service, repository); + return new RegistryAuthenticator( + realm, service, registryEndpointRequestProperties.getImageName()); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index eacaf0659b..0c92492ad2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -59,7 +59,7 @@ public class BuildAndCacheApplicationLayerStepTest { private Path temporaryCacheDirectory; @Before - public void setUp() throws IOException, URISyntaxException { + public void setUp() throws IOException { Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestBuildLogger()); temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 536922357d..e843a4488a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -23,6 +23,8 @@ /** Tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorTest { + private final RegistryEndpointRequestProperties registryEndpointRequestProperties = + new RegistryEndpointRequestProperties("someserver", "someimage"); @Test public void testFromAuthenticationMethod_bearer() @@ -30,7 +32,7 @@ public void testFromAuthenticationMethod_bearer() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage"); + registryEndpointRequestProperties); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl("scope")); @@ -38,7 +40,7 @@ public void testFromAuthenticationMethod_bearer() registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage"); + registryEndpointRequestProperties); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl("scope")); @@ -49,24 +51,25 @@ public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFa Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + registryEndpointRequestProperties)); Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + registryEndpointRequestProperties)); Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - "someimage")); + registryEndpointRequestProperties)); } @Test public void testFromAuthenticationMethod_noBearer() { try { RegistryAuthenticator.fromAuthenticationMethod( - "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", "someimage"); + "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties); Assert.fail("Authentication method without 'Bearer ' or 'Basic ' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -79,7 +82,8 @@ public void testFromAuthenticationMethod_noBearer() { @Test public void testFromAuthenticationMethod_noRealm() { try { - RegistryAuthenticator.fromAuthenticationMethod("Bearer scope=\"somescope\"", "someimage"); + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer scope=\"somescope\"", registryEndpointRequestProperties); Assert.fail("Authentication method without 'realm' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -90,16 +94,14 @@ public void testFromAuthenticationMethod_noRealm() { } @Test - public void testFromAuthenticationMethod_noService() { - try { - RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\"", "someimage"); - Assert.fail("Authentication method without 'service' should fail"); + public void testFromAuthenticationMethod_noService() + throws MalformedURLException, RegistryAuthenticationFailedException { + RegistryAuthenticator registryAuthenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties); - } catch (RegistryAuthenticationFailedException ex) { - Assert.assertEquals( - "Failed to authenticate with the registry because: 'service' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer realm=\"https://somerealm\"", - ex.getMessage()); - } + Assert.assertEquals( + new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), + registryAuthenticator.getAuthenticationUrl("scope")); } } From 8316b2065bdc6a0eedfb7865ab544aff827c9b3a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Sat, 14 Jul 2018 23:47:46 -0400 Subject: [PATCH 0014/2020] Adds twitter badge for easy sharing. (#617) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a29cfd3d0a..f62e67169f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) [![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.6-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) +[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Google%20container%20tool%20for%20dockerizing%20Java%20applications&url=https://github.com/GoogleContainerTools/jib&via=JibContainerBuilder&hashtags=java,docker,kubernetes,maven,gradle,microservices,jib) # Jib From e4c3f392a53e290bd012a6ff46eddc71ae725eb3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 16 Jul 2018 09:48:02 -0400 Subject: [PATCH 0015/2020] Adds ways to get involved. (#616) --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f62e67169f..032898c666 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,16 @@ See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a simila See the [Frequently Asked Questions (FAQ) wiki page](/../../wiki/Frequently-Asked-Questions-(FAQ)). -## Community +## Get involved with the community +We welcome contributions! Here's how you can contribute: + +* [Browse issues](https://github.com/GoogleContainerTools/jib/issues) or [file an issue](https://github.com/GoogleContainerTools/jib/issues/new) * Chat with us on [gitter](https://gitter.im/google/jib) -* [jib-users mailing list](https://groups.google.com/forum/#!forum/jib-users) +* Join the [jib-users mailing list](https://groups.google.com/forum/#!forum/jib-users) +* Contribute: + * Try to fix [good first issues](https://github.com/GoogleContainerTools/jib/labels/good%20first%20issue) + * Help out on [issues that need help](https://github.com/GoogleContainerTools/jib/labels/help%20wanted) + * Join in on [discussion issues](https://github.com/GoogleContainerTools/jib/labels/discuss) + +*Make sure to follow the [Code of Conduct](https://github.com/GoogleContainerTools/jib/blob/master/CODE_OF_CONDUCT.md) when contributing so we can foster an open and welcoming community.* From 5dc98cf1626444a7450d0d74cbdcdd987824ccfc Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 16 Jul 2018 12:20:58 -0400 Subject: [PATCH 0016/2020] Changes Upcoming Features section to be more sustainable. (#621) --- jib-gradle-plugin/README.md | 5 +---- jib-maven-plugin/README.md | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 927de64e29..084f9d9236 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -12,10 +12,7 @@ For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). ## Upcoming Features -These features are not currently supported but will be added in later releases. - -* Support for WAR format -* Run and debug the built container +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. ## Quickstart diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index da303e0063..f54ad291bb 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -12,10 +12,7 @@ For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin) ## Upcoming Features -These features are not currently supported but will be added in later releases. - -* Support for WAR format -* Run and debug the built container +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. ## Quickstart From 097d21fd30f29e2c1c0ff965cfafe4df1f489e1d Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Tue, 17 Jul 2018 10:33:22 +1200 Subject: [PATCH 0017/2020] Add instructions for self-signed certificates (#618) --- docs/self_sign_cert.md | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/self_sign_cert.md diff --git a/docs/self_sign_cert.md b/docs/self_sign_cert.md new file mode 100644 index 0000000000..b2581b3958 --- /dev/null +++ b/docs/self_sign_cert.md @@ -0,0 +1,53 @@ +# Accessing private docker registry with self-signed certificate + +Currently, `jib` does not support docker registries with self-signed `https` certificate. + +Jib uses the JVM's list of approved CA Certificates to validate SSL certificates. The following instructions describe how to add a registry's self-signed certificate to the JVM's approved CAs. + +The certificate will be trusted at the JRE level, affecting all Java applications running on it. You will also need to re-import the certificate when you use a different JRE or upgrade it. + +## Using KeyStore Explorer + +The easiest way to import the self-signed certificate into JVM is using the [KeyStore Explorer](http://keystore-explorer.org/). + +KeyStore Explorer is an open source GUI replacement for the Java command-line utilities keytool and jarsigner. KeyStore Explorer presents their functionality, and more, via an intuitive graphical user interface. + +### Installation + +Download and install KeyStore Explorer from [official website](http://keystore-explorer.org/downloads.html) + +### Indentify java runtime being used by build tool + +#### Maven + +Run `mvn --version` and take note on java runtime: + +```shell +$ mvn --version +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T06:33:14+12:00) +Maven home: /usr/local/Cellar/maven/3.5.4/libexec +Java version: 1.8.0_172, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre +Default locale: en_NZ, platform encoding: UTF-8 +OS name: "mac os x", version: "10.13.6", arch: "x86_64", family: "mac" +``` + +On this example the `java runtime` is `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre`. + +### Import certificate + +* Launch `KeyStore Explorer` + +#### 1. Select java runtime + +* Open `Preferences` +* Go to `Authorithy Certificates` +* Replace `CA Certificates KeyStore` with the `java runtime` from the previous step. Include `lib/security/cacerts` on the path. + +#### 2. Import certificate + +* Hit `Open CA Certificates KeyStore` on the main window +* On the tool bar, click on `Import Trusted Certificate` +* Select the certifcate file on disk +* Give it a name, or use suggested name, hit `OK` +* Hit ok on the success window +* Last step, hit `Save` From 4d1b5deb79067064e81de10a76315d29cb0f85a1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Jul 2018 19:04:05 -0400 Subject: [PATCH 0018/2020] Fix writing multi-byte characters to tar archive (#627) --- jib-core/build.gradle | 1 + .../cloud/tools/jib/tar/TarStreamBuilder.java | 10 +++--- .../tools/jib/tar/TarStreamBuilderTest.java | 33 +++++++++++++++++++ jib-gradle-plugin/build.gradle | 1 + 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 09d5bc6942..fa437194d0 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -11,6 +11,7 @@ group 'com.google.cloud.tools' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 +compileJava.options.encoding = 'UTF-8' repositories { mavenCentral() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 318f9f9253..f08034c693 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -53,7 +53,7 @@ private interface TarArchiveOutputStreamConsumer { */ private void writeEntriesAsTarArchive(OutputStream tarByteStream) throws IOException { try (TarArchiveOutputStream tarArchiveOutputStream = - new TarArchiveOutputStream(tarByteStream)) { + new TarArchiveOutputStream(tarByteStream, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); for (Map.Entry entry : @@ -92,11 +92,9 @@ public void addEntry(TarArchiveEntry entry) { */ public void addEntry(String contents, String name) { TarArchiveEntry entry = new TarArchiveEntry(name); - entry.setSize(contents.length()); - archiveMap.put( - entry, - tarArchiveOutputStream -> - tarArchiveOutputStream.write(contents.getBytes(StandardCharsets.UTF_8))); + byte[] contentsBytes = contents.getBytes(StandardCharsets.UTF_8); + entry.setSize(contentsBytes.length); + archiveMap.put(entry, tarArchiveOutputStream -> tarArchiveOutputStream.write(contentsBytes)); } /** @return a new {@link Blob} that can stream the uncompressed tarball archive BLOB. */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index ddc83e59da..1d6806ab72 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -95,6 +95,39 @@ public void testToBlob_stringsAndTarArchiveEntriesWithCompression() throws IOExc verifyBlobWithCompression(); } + @Test + public void testToBlob_multiByte() throws IOException { + testTarStreamBuilder.addEntry("日本語", "test"); + testTarStreamBuilder.addEntry("asdf", "crepecake"); + Blob blob = testTarStreamBuilder.toBlob(); + + // Writes the BLOB and captures the output. + ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); + OutputStream compressorStream = new GZIPOutputStream(tarByteOutputStream); + blob.writeTo(compressorStream); + + // Rearrange the output into input for verification. + ByteArrayInputStream byteArrayInputStream = + new ByteArrayInputStream(tarByteOutputStream.toByteArray()); + InputStream tarByteInputStream = new GZIPInputStream(byteArrayInputStream); + TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(tarByteInputStream); + + // Verify multi-byte characters are written/read correctly + TarArchiveEntry headerFile = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("test", headerFile.getName()); + String fileString = + CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals("日本語", fileString); + + headerFile = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("crepecake", headerFile.getName()); + fileString = + CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals("asdf", fileString); + + Assert.assertNull(tarArchiveInputStream.getNextTarEntry()); + } + /** Creates a TarStreamBuilder using TarArchiveEntries. */ private void setUpWithTarEntries() { // Prepares a test TarStreamBuilder. diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index d94cbe65c9..1d365b8e65 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -20,6 +20,7 @@ group 'com.google.cloud.tools' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 +compileJava.options.encoding = 'UTF-8' repositories { // Use jcenter for gradle plugin portal releases. From 044770b85287378b240c9daee75c17a8f99d8170 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 17 Jul 2018 00:28:06 -0400 Subject: [PATCH 0019/2020] Adds link to OCI. (#629) --- README.md | 2 +- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 032898c666..0ab2c5eeff 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## What is Jib? -Jib builds Docker and OCI images for your Java applications and is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin). +Jib builds Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications and is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin). [Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin).\ [Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin). diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 084f9d9236..54ba2063b9 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -5,7 +5,7 @@ # Jib - Containerize your Gradle Java project -Jib is [Gradle](https://gradle.org/) plugin for building Docker and OCI images for your Java applications. +Jib is [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. For information about the project, see the [Jib project README](../README.md). For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index f54ad291bb..5eb03e438e 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -5,7 +5,7 @@ # Jib - Containerize your Maven project -Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and OCI images for your Java applications. +Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. For information about the project, see the [Jib project README](../README.md). For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). From 034db7eeb8141efe4e07b1b63d86fe36081fc2e6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 17 Jul 2018 10:41:46 -0400 Subject: [PATCH 0020/2020] Add creationTime parameter to BuildConfiguration (#628) --- .../tools/jib/builder/BuildConfiguration.java | 21 ++++++++++++++++ .../jib/builder/steps/BuildImageStep.java | 1 + .../google/cloud/tools/jib/image/Image.java | 25 +++++++++++++++++++ ...ContainerConfigurationFormatException.java | 4 +++ .../json/ContainerConfigurationTemplate.java | 19 ++++++++------ .../jib/image/json/ImageToJsonTranslator.java | 3 +++ .../jib/image/json/JsonToImageTranslator.java | 11 ++++++++ .../jib/builder/BuildConfigurationTest.java | 5 ++++ .../jib/builder/steps/BuildImageStepTest.java | 2 ++ .../cloud/tools/jib/image/ImageTest.java | 3 +++ .../ContainerConfigurationTemplateTest.java | 5 ++-- .../image/json/ImageToJsonTranslatorTest.java | 5 ++-- .../image/json/JsonToImageTranslatorTest.java | 2 ++ .../test/resources/json/containerconfig.json | 2 +- .../json/translated_ocimanifest.json | 2 +- .../json/translated_v22manifest.json | 2 +- 16 files changed, 95 insertions(+), 17 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java index 4b63826137..500e123a77 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java @@ -27,6 +27,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,6 +40,7 @@ public class BuildConfiguration { public static class Builder { // All the parameters below are set to their default values. + private Instant creationTime = Instant.EPOCH; @Nullable private ImageReference baseImageReference; @Nullable private String baseImageCredentialHelperName; @Nullable private RegistryCredentials knownBaseRegistryCredentials; @@ -184,6 +186,17 @@ public Builder setExtraFilesLayerConfiguration( return this; } + /** + * Sets the image creation time. + * + * @param creationTime the creation time + * @return this + */ + public Builder setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + /** @return the corresponding build configuration */ public BuildConfiguration build() { // Validates the parameters. @@ -211,6 +224,7 @@ public BuildConfiguration build() { } return new BuildConfiguration( buildLogger, + creationTime, baseImageReference, baseImageCredentialHelperName, knownBaseRegistryCredentials, @@ -270,6 +284,7 @@ public static Builder builder(BuildLogger buildLogger) { } private final BuildLogger buildLogger; + private final Instant creationTime; private final ImageReference baseImageReference; @Nullable private final String baseImageCredentialHelperName; @Nullable private final RegistryCredentials knownBaseRegistryCredentials; @@ -290,6 +305,7 @@ public static Builder builder(BuildLogger buildLogger) { /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( BuildLogger buildLogger, + Instant creationTime, ImageReference baseImageReference, @Nullable String baseImageCredentialHelperName, @Nullable RegistryCredentials knownBaseRegistryCredentials, @@ -307,6 +323,7 @@ private BuildConfiguration( boolean allowHttp, @Nullable LayerConfiguration extraFilesLayerConfiguration) { this.buildLogger = buildLogger; + this.creationTime = creationTime; this.baseImageReference = baseImageReference; this.baseImageCredentialHelperName = baseImageCredentialHelperName; this.knownBaseRegistryCredentials = knownBaseRegistryCredentials; @@ -329,6 +346,10 @@ public BuildLogger getBuildLogger() { return buildLogger; } + public Instant getCreationTime() { + return creationTime; + } + public ImageReference getBaseImageReference() { return baseImageReference; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index ddddd6181a..94a48e2108 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -99,6 +99,7 @@ private Image afterCachedLayersSteps() buildAndCacheApplicationLayerSteps) { imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); } + imageBuilder.setCreated(buildConfiguration.getCreationTime()); imageBuilder.setEnvironment(buildConfiguration.getEnvironment()); imageBuilder.setEntrypoint(entrypoint); imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index b2735dd523..b9d4c56384 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -18,8 +18,10 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.common.collect.ImmutableList; +import java.time.Instant; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; /** Represents an image. */ public class Image { @@ -30,10 +32,22 @@ public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); private final ImmutableList.Builder environmentBuilder = ImmutableList.builder(); + @Nullable private Instant created; private ImmutableList entrypoint = ImmutableList.of(); private ImmutableList javaArguments = ImmutableList.of(); private ImmutableList exposedPorts = ImmutableList.of(); + /** + * Sets the image creation time. + * + * @param created the creation time + * @return this + */ + public Builder setCreated(Instant created) { + this.created = created; + return this; + } + /** * Sets the environment with a map from environment variable names to values. * @@ -117,6 +131,7 @@ public Builder addLayer(T layer) throws LayerPropertyNotFoundException { public Image build() { return new Image<>( + created, imageLayersBuilder.build(), environmentBuilder.build(), ImmutableList.copyOf(entrypoint), @@ -129,6 +144,9 @@ public static Builder builder() { return new Builder<>(); } + /** The image creation time. */ + @Nullable private final Instant created; + /** The layers of the image, in the order in which they are applied. */ private final ImageLayers layers; @@ -145,11 +163,13 @@ public static Builder builder() { private final ImmutableList exposedPorts; private Image( + @Nullable Instant created, ImageLayers layers, ImmutableList environment, ImmutableList entrypoint, ImmutableList javaArguments, ImmutableList exposedPorts) { + this.created = created; this.layers = layers; this.environmentBuilder = environment; this.entrypoint = entrypoint; @@ -157,6 +177,11 @@ private Image( this.exposedPorts = exposedPorts; } + @Nullable + public Instant getCreated() { + return created; + } + public ImmutableList getEnvironment() { return environmentBuilder; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java index 2b7c4df956..5b7d20caef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java @@ -23,4 +23,8 @@ public class BadContainerConfigurationFormatException extends Exception { BadContainerConfigurationFormatException(String message) { super(message); } + + BadContainerConfigurationFormatException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 32c612b2c2..2ff134c05c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -57,14 +57,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class ContainerConfigurationTemplate implements JsonTemplate { - /** - * A combined date and time at which the image was created. Constant to maintain reproducibility - * and avoid Docker's weird "292 years old" bug. - * - * @see https://github.com/GoogleContainerTools/jib/issues/341 - */ - private String created = "1970-01-01T00:00:00Z"; + /** ISO-8601 formatted combined date and time at which the image was created. */ + @Nullable private String created; /** The CPU architecture to run the binaries in this container. */ private String architecture = "amd64"; @@ -111,6 +105,10 @@ private static class RootFilesystemObjectTemplate implements JsonTemplate { private final List diff_ids = new ArrayList<>(); } + public void setCreated(@Nullable String created) { + this.created = created; + } + public void setContainerEnvironment(List environment) { config.Env = environment; } @@ -135,6 +133,11 @@ List getDiffIds() { return rootfs.diff_ids; } + @Nullable + String getCreated() { + return created; + } + @Nullable List getContainerEnvironment() { return config.Env; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index eb79141f62..ad0145d11e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -87,6 +87,9 @@ public Blob getContainerConfigurationBlob() { template.addLayerDiffId(layer.getDiffId()); } + // Sets the creation time. Instant#toString() returns an ISO-8601 formatted string. + template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); + // Adds the environment variables. template.setContainerEnvironment(image.getEnvironment()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index da12d73c21..890ce1d0c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -29,6 +29,8 @@ import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -113,6 +115,15 @@ public static Image toImage( imageBuilder.addLayer(new ReferenceLayer(noDiffIdLayer.getBlobDescriptor(), diffId)); } + if (containerConfigurationTemplate.getCreated() != null) { + try { + imageBuilder.setCreated(Instant.parse(containerConfigurationTemplate.getCreated())); + } catch (DateTimeParseException ex) { + throw new BadContainerConfigurationFormatException( + "Invalid image creation time: " + containerConfigurationTemplate.getCreated(), ex); + } + } + if (containerConfigurationTemplate.getContainerEntrypoint() != null) { imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java index cd463a845a..abad5c1082 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.nio.file.Paths; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -40,6 +41,7 @@ public class BuildConfigurationTest { @Test public void testBuilder() { + Instant expectedCreationTime = Instant.ofEpochSecond(10000); String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; String expectedBaseImageTag = "baseimagetag"; @@ -68,6 +70,7 @@ public void testBuilder() { BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setCreationTime(expectedCreationTime) .setBaseImage( ImageReference.of( expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) @@ -90,6 +93,7 @@ public void testBuilder() { .setExtraFilesLayerConfiguration(expectedExtraFilesLayerConfiguration); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + Assert.assertEquals(expectedCreationTime, buildConfiguration.getCreationTime()); Assert.assertEquals(expectedBaseImageServerUrl, buildConfiguration.getBaseImageRegistry()); Assert.assertEquals(expectedBaseImageName, buildConfiguration.getBaseImageRepository()); Assert.assertEquals(expectedBaseImageTag, buildConfiguration.getBaseImageTag()); @@ -141,6 +145,7 @@ public void testBuilder_default() { .setMainClass(expectedMainClass) .build(); + Assert.assertEquals(buildConfiguration.getCreationTime(), Instant.EPOCH); Assert.assertNull(buildConfiguration.getBaseImageCredentialHelperName()); Assert.assertNull(buildConfiguration.getKnownBaseRegistryCredentials()); Assert.assertNull(buildConfiguration.getTargetImageCredentialHelperName()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 3ae274f153..ea865b518c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -29,6 +29,7 @@ import com.google.common.util.concurrent.MoreExecutors; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.junit.Assert; @@ -63,6 +64,7 @@ public void setUp() throws DigestException { null); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); + Mockito.when(mockBuildConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); Mockito.when(mockBuildConfiguration.getEnvironment()).thenReturn(ImmutableMap.of()); Mockito.when(mockBuildConfiguration.getJavaArguments()).thenReturn(ImmutableList.of()); Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index e97e69742a..c78f47febe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; +import java.time.Instant; import java.util.Arrays; import org.junit.Assert; import org.junit.Before; @@ -49,6 +50,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Image image = Image.builder() + .setCreated(Instant.ofEpochSecond(10000)) .setEnvironmentVariable("crepecake", "is great") .setEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) @@ -60,6 +62,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Assert.assertEquals( mockDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest()); + Assert.assertEquals(Instant.ofEpochSecond(10000), image.getCreated()); Assert.assertEquals(expectedEnvironment, image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 018f5a39ab..fd383447d4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -45,6 +45,7 @@ public void testToJson() throws IOException, URISyntaxException, DigestException // Creates the JSON object to serialize. ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); + containerConfigJson.setCreated("1970-01-01T00:00:20Z"); containerConfigJson.setContainerEnvironment(Arrays.asList("VAR1=VAL1", "VAR2=VAL2")); containerConfigJson.setContainerEntrypoint(Arrays.asList("some", "entrypoint", "command")); containerConfigJson.setContainerCmd(Arrays.asList("arg1", "arg2")); @@ -77,15 +78,13 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti ContainerConfigurationTemplate containerConfigJson = JsonTemplateMapper.readJsonFromFile(jsonFile, ContainerConfigurationTemplate.class); + Assert.assertEquals("1970-01-01T00:00:20Z", containerConfigJson.getCreated()); Assert.assertEquals( Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), containerConfigJson.getContainerEnvironment()); - Assert.assertEquals( Arrays.asList("some", "entrypoint", "command"), containerConfigJson.getContainerEntrypoint()); - Assert.assertEquals(Arrays.asList("arg1", "arg2"), containerConfigJson.getContainerCmd()); - Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index c227f2ce31..77125884c7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -38,6 +38,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.Arrays; import java.util.Map; import org.junit.Assert; @@ -54,13 +55,11 @@ public class ImageToJsonTranslatorTest { public void setUp() throws DigestException, LayerPropertyNotFoundException { Image.Builder testImageBuilder = Image.builder(); + testImageBuilder.setCreated(Instant.ofEpochSecond(20)); testImageBuilder.setEnvironmentVariable("VAR1", "VAL1"); testImageBuilder.setEnvironmentVariable("VAR2", "VAL2"); - testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); - testImageBuilder.setJavaArguments(Arrays.asList("arg1", "arg2")); - testImageBuilder.setExposedPorts( ImmutableList.of( new Port(1000, Protocol.TCP), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index b731480611..5953ad0018 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -33,6 +33,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -141,6 +142,7 @@ private void testToImage_buildable( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), layers.get(0).getDiffId()); + Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated()); Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), image.getEnvironment()); Assert.assertEquals( diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index 6c83f82c47..be521e6341 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:00Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}}},"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}}},"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/json/translated_ocimanifest.json index b1b1d8c803..07e5cbde08 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:71050b2d503c7463cfe574c0b37b6575aed1ace518c0c9174ddb9cd91b57ff4a","size":353},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:1bc0730d8135d29caa0e0c5b502164195afcab6494e1b024e419dc9d591d778d","size":353},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index 8d35a1fd6d..153f896ef7 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:71050b2d503c7463cfe574c0b37b6575aed1ace518c0c9174ddb9cd91b57ff4a","size":353},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:1bc0730d8135d29caa0e0c5b502164195afcab6494e1b024e419dc9d591d778d","size":353},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file From 0b22e7acd7b3e7c2ed56e2e6d9449f302d23d94f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 17 Jul 2018 10:55:54 -0400 Subject: [PATCH 0021/2020] Add multi-byte fix to changelogs (#635) --- jib-gradle-plugin/CHANGELOG.md | 4 +++- jib-maven-plugin/CHANGELOG.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 541ff3765e..a158c61885 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -4,14 +4,16 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Added -- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) ### Changed ### Fixed +- Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) + ## 0.9.6 ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index e62c65a58a..7d21c8224d 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -4,14 +4,16 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Added -- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) ### Changed ### Fixed +- Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) + ## 0.9.6 ### Fixed From f3a725bdbbfc61d57f664c973ef4664702ca5bfe Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 17 Jul 2018 11:51:50 -0400 Subject: [PATCH 0022/2020] Check other potential alises if Docker Hub (#605) * Try all aliases for retrieving Docker confg creds * Update CHANGELOG --- .../jib/registry/RegistryAliasGroup.java | 53 +++++++++++++++++++ .../DockerConfigCredentialRetriever.java | 12 +++++ .../jib/registry/RegistryAliasGroupTest.java | 47 ++++++++++++++++ .../DockerConfigCredentialRetrieverTest.java | 17 ++++++ .../src/test/resources/json/dockerconfig.json | 16 +++++- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 7 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java new file mode 100644 index 0000000000..58c988414d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Provides known aliases for a given registry. */ +public class RegistryAliasGroup { + + private static final ImmutableList> REGISTRY_ALIAS_GROUPS = + ImmutableList.of( + // Docker Hub alias group + ImmutableSet.of("registry.hub.docker.com", "index.docker.io")); + + /** + * Returns the list of registry aliases for the given {@code registry}, including {@code registry} + * as the first element. + * + * @param registry the registry for which the alias group is requested + * @return non-empty list of registries where {@code registry} is the first element + */ + public static List getAliasesGroup(String registry) { + for (ImmutableSet aliasGroup : REGISTRY_ALIAS_GROUPS) { + if (aliasGroup.contains(registry)) { + // Found a group. Move the requested "registry" to the front before returning it. + Stream self = Stream.of(registry); + Stream withoutSelf = aliasGroup.stream().filter(alias -> !registry.equals(alias)); + return Stream.concat(self, withoutSelf).collect(Collectors.toList()); + } + } + + return Collections.singletonList(registry); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 4670214931..53be488636 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -86,6 +87,17 @@ public Authorization retrieve() throws IOException { return null; } + for (String registry : RegistryAliasGroup.getAliasesGroup(registry)) { + Authorization authorization = retrieve(dockerConfigTemplate, registry); + if (authorization != null) { + return authorization; + } + } + return null; + } + + @Nullable + private Authorization retrieve(DockerConfigTemplate dockerConfigTemplate, String registry) { // First, tries to find defined auth. String auth = dockerConfigTemplate.getAuthFor(registry); if (auth != null) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java new file mode 100644 index 0000000000..90dc98e2d8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RegistryAliasGroup}. */ +public class RegistryAliasGroupTest { + + @Test + public void testGetAliasesGroup_noKnownAliases() { + List singleton = RegistryAliasGroup.getAliasesGroup("something.gcr.io"); + Assert.assertEquals(1, singleton.size()); + Assert.assertEquals("something.gcr.io", singleton.get(0)); + } + + @Test + public void testGetAliasesGroup_registryHubDockerCom() { + Assert.assertEquals( + Arrays.asList("registry.hub.docker.com", "index.docker.io"), + RegistryAliasGroup.getAliasesGroup("registry.hub.docker.com")); + } + + @Test + public void testGetAliasesGroup_indexDockerIo() { + Assert.assertEquals( + Arrays.asList("index.docker.io", "registry.hub.docker.com"), + RegistryAliasGroup.getAliasesGroup("index.docker.io")); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 256e3384e0..b295157617 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -118,4 +118,21 @@ public void testRetrieve_none() throws IOException { Assert.assertNull(dockerConfigCredentialRetriever.retrieve()); } + + @Test + public void testRetrieve_credentialFromAlias() throws IOException { + Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix(Mockito.anyString())) + .thenReturn(Mockito.mock(DockerCredentialHelper.class)); + Mockito.when( + mockDockerCredentialHelperFactory.withCredentialHelperSuffix( + "index.docker.io credential helper")) + .thenReturn(mockDockerCredentialHelper); + + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + new DockerConfigCredentialRetriever( + "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); + + Authorization authorization = dockerConfigCredentialRetriever.retrieve(); + Assert.assertEquals(mockAuthorization, authorization); + } } diff --git a/jib-core/src/test/resources/json/dockerconfig.json b/jib-core/src/test/resources/json/dockerconfig.json index a4cbe7a126..cc8c1d9093 100644 --- a/jib-core/src/test/resources/json/dockerconfig.json +++ b/jib-core/src/test/resources/json/dockerconfig.json @@ -1 +1,15 @@ -{"auths":{"some other registry":{"auth":"some other auth"},"some registry":{"auth":"some auth","password":"ignored"},"https://registry":{"auth":"token"},"just registry":{},"https://with.protocol":{}},"credsStore":"some credential store","credHelpers":{"another registry":"another credential helper","some registry":"some credential helper"}} \ No newline at end of file +{ + "auths":{ + "some other registry":{"auth":"some other auth"}, + "some registry":{"auth":"some auth","password":"ignored"}, + "https://registry":{"auth":"token"}, + "just registry":{}, + "https://with.protocol":{} + }, + "credsStore":"some credential store", + "credHelpers":{ + "another registry":"another credential helper", + "some registry":"some credential helper", + "index.docker.io":"index.docker.io credential helper" + } +} \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index a158c61885..9b24c56728 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7d21c8224d..6db12229a2 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ### Changed From 57982820a3e75b265c3b787bcd057eb0ec23cbec Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 17 Jul 2018 13:02:53 -0400 Subject: [PATCH 0023/2020] Add useCurrentTimestamp to gradle plugin (#638) --- jib-gradle-plugin/CHANGELOG.md | 3 +- .../jib/gradle/JibPluginIntegrationTest.java | 53 ++++++++++++++++++- .../resources/projects/simple/build.gradle | 1 + .../tools/jib/gradle/BuildDockerTask.java | 7 +++ .../tools/jib/gradle/BuildImageTask.java | 7 +++ .../cloud/tools/jib/gradle/BuildTarTask.java | 7 +++ .../tools/jib/gradle/ContainerParameters.java | 11 ++++ .../cloud/tools/jib/gradle/JibExtension.java | 6 +++ 8 files changed, 93 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 9b24c56728..8e28b460f2 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,13 +7,14 @@ All notable changes to this project will be documented in this file. - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) -- For Docker Hub, also tries registry aliases when getting a credential from the Docker config +- `container.useCurrentTimestamp` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) ### Changed ### Fixed - Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) ## 0.9.6 diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 278ba24d1f..f9df14cbd8 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; import java.nio.file.Files; +import java.time.Instant; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -97,6 +98,16 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String public void testBuild_empty() throws IOException, InterruptedException { Assert.assertEquals( "", buildAndRun(emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/emptyimage:gradle") + .run() + .trim()); } @Test @@ -112,9 +123,11 @@ public void testBuild_simple() throws IOException, InterruptedException { "No classes files were found - did you compile your project?")); } + Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", buildAndRun(simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); + assertSimpleCreationTimeIsAfter(beforeBuild); } @Test @@ -139,14 +152,26 @@ public void testDockerDaemon_empty() throws IOException, InterruptedException { "", buildToDockerDaemonAndRun( emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/emptyimage:gradle") + .run() + .trim()); } @Test public void testDockerDaemon_simple() throws IOException, InterruptedException { + Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", buildToDockerDaemonAndRun( simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); + assertSimpleCreationTimeIsAfter(beforeBuild); } @Test @@ -162,6 +187,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { String imageReference = "gcr.io/jib-integration-testing/simpleimage:gradle"; String outputPath = simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); + Instant beforeBuild = Instant.now(); BuildResult buildResult = simpleTestProject.build("clean", JibPlugin.BUILD_TAR_TASK_NAME); BuildTask classesTask = buildResult.task(":classes"); @@ -179,6 +205,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageReference).run()); + assertSimpleCreationTimeIsAfter(beforeBuild); } @Test @@ -246,7 +273,7 @@ public void testDockerContext() throws IOException, InterruptedException { Assert.assertEquals(TaskOutcome.SUCCESS, reexecutedJibDockerContextTask.getOutcome()); } catch (UnexpectedBuildFailure ex) { - // THis might happen on systems without SecureDirectoryStream, so we just ignore it. + // This might happen on systems without SecureDirectoryStream, so we just ignore it. // See com.google.common.io.MoreFiles#deleteDirectoryContents. Assert.assertThat( ex.getMessage(), @@ -254,4 +281,28 @@ public void testDockerContext() throws IOException, InterruptedException { "Export Docker context failed because cannot clear directory")); } } + + /** + * Asserts that the creation time of the simple test project is set. If the time parsed from the + * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the + * assertion will fail. + * + * @param before the specified time to compare the resulting image's creation time to + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertSimpleCreationTimeIsAfter(Instant before) + throws IOException, InterruptedException { + String inspect = + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/simpleimage:gradle") + .run() + .trim(); + Instant parsed = Instant.parse(inspect); + Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); + } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 24ea16fd7b..3280e0da87 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -20,6 +20,7 @@ jib { credHelper = 'gcr' } container { + useCurrentTimestamp = true args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 58dcb9415f..100e6df31b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -129,6 +130,12 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 90aa14d58b..19e22a9c64 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -137,6 +138,12 @@ public void buildImage() throws InvalidImageReferenceException, IOException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 26debff462..af5f49db9e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -34,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -157,6 +158,12 @@ public void buildTar() throws InvalidImageReferenceException, IOException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 8b9aa39461..cca369fdd9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -31,12 +31,23 @@ */ public class ContainerParameters { + private boolean useCurrentTimestamp = false; private List jvmFlags = Collections.emptyList(); @Nullable private String mainClass; private List args = Collections.emptyList(); private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); + @Input + @Optional + public boolean getUseCurrentTimestamp() { + return useCurrentTimestamp; + } + + public void setUseCurrentTimestamp(boolean useCurrentTimestamp) { + this.useCurrentTimestamp = useCurrentTimestamp; + } + @Input @Optional public List getJvmFlags() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 7ba35097f7..75c4b3783b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -253,6 +253,12 @@ List getExposedPorts() { return container.getPorts(); } + @Internal + @Optional + boolean getUseCurrentTimestamp() { + return container.getUseCurrentTimestamp(); + } + @Input @Optional boolean getUseOnlyProjectCache() { From 255a565d72cd0f1dd28c153e29a0b47de2dd5b95 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 17 Jul 2018 17:20:00 -0400 Subject: [PATCH 0024/2020] Allow authentication over HTTP using system property (#641) --- .../google/cloud/tools/jib/frontend/HelpfulSuggestions.java | 3 ++- .../cloud/tools/jib/registry/RegistryAuthenticator.java | 1 - .../cloud/tools/jib/registry/RegistryEndpointCaller.java | 2 +- .../cloud/tools/jib/frontend/HelpfulSuggestionsTest.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + .../com/google/cloud/tools/jib/gradle/BuildDockerTask.java | 5 +++++ .../com/google/cloud/tools/jib/gradle/BuildImageTask.java | 5 +++++ .../java/com/google/cloud/tools/jib/gradle/BuildTarTask.java | 5 +++++ jib-maven-plugin/CHANGELOG.md | 1 + .../com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 5 +++++ .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 5 +++++ .../java/com/google/cloud/tools/jib/maven/BuildTarMojo.java | 5 +++++ 12 files changed, 36 insertions(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java index 61274949b6..f3785fa2da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java @@ -97,7 +97,8 @@ public String forCredentialsNotCorrect(String registry) { } public String forCredentialsNotSent() { - return suggest("use a registry that supports HTTPS so credentials can be sent safely"); + return suggest( + "use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true"); } public String forDockerContextInsecureRecursiveDelete(String directory) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index cdce382ec0..1fabdb6acb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -109,7 +109,6 @@ public static Initializer initializer(String serverUrl, String repository) { * * @param authenticationMethod the {@code WWW-Authenticate} header value * @param registryEndpointRequestProperties the registry request properties - * @param repository the repository/image name * @return a new {@link RegistryAuthenticator} for authenticating with the registry service * @throws RegistryAuthenticationFailedException if authentication fails * @see ")); } + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + gradleBuildLogger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } RegistryCredentials knownBaseRegistryCredentials = null; RegistryCredentials knownTargetRegistryCredentials = null; Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index af5f49db9e..35fab736eb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -116,6 +116,11 @@ public void buildTar() throws InvalidImageReferenceException, IOException { GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleBuildLogger); + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + gradleBuildLogger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } RegistryCredentials knownBaseRegistryCredentials = null; Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); if (fromAuthorization != null) { diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 6db12229a2..896bb8b1cb 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) - For Docker Hub, also tries registry aliases when getting a credential from the Docker config ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 0954b7b7a2..3a3a9ff811 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -70,6 +70,11 @@ public void execute() throws MojoExecutionException { mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); // Checks Maven settings for registry credentials. + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + mavenBuildLogger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index ac3d6aec63..2d0ccac5a3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -86,6 +86,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { ImageReference targetImage = parseImageReference(getTargetImage(), "to"); // Checks Maven settings for registry credentials. + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + mavenBuildLogger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index e942fafc21..e7e51550d8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -68,6 +68,11 @@ public void execute() throws MojoExecutionException { mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); // Checks Maven settings for registry credentials. + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + mavenBuildLogger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); From 07e148d4474fa498dcff90261101c7a6372f4afb Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 17 Jul 2018 17:46:48 -0400 Subject: [PATCH 0025/2020] Add useCurrentTimestamp to maven plugin (#639) --- jib-maven-plugin/CHANGELOG.md | 3 ++- .../tools/jib/maven/BuildDockerMojo.java | 7 ++++++ .../cloud/tools/jib/maven/BuildImageMojo.java | 7 ++++++ .../cloud/tools/jib/maven/BuildTarMojo.java | 7 ++++++ .../jib/maven/JibPluginConfiguration.java | 6 +++++ .../maven/BuildDockerMojoIntegrationTest.java | 23 ++++++++++++++++++ .../maven/BuildImageMojoIntegrationTest.java | 24 +++++++++++++++++++ .../maven/BuildTarMojoIntegrationTest.java | 14 +++++++++++ .../test/resources/projects/simple/pom.xml | 1 + 9 files changed, 91 insertions(+), 1 deletion(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 896bb8b1cb..730b97d149 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,14 +7,15 @@ All notable changes to this project will be documented in this file. - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- `` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) - Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) -- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ### Changed ### Fixed - Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) ## 0.9.6 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 3a3a9ff811..e41d7dca77 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.plugin.MojoExecutionException; @@ -117,6 +118,12 @@ public void execute() throws MojoExecutionException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 2d0ccac5a3..5c028bc191 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -139,6 +140,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index e7e51550d8..ebe232ce01 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -33,6 +33,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.plugin.MojoExecutionException; @@ -115,6 +116,12 @@ public void execute() throws MojoExecutionException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + buildConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 3d0ad68e17..28b16c082c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -67,6 +67,8 @@ public void set(String image) { /** Configuration for {@code container} parameter. */ public static class ContainerParameters { + @Parameter private boolean useCurrentTimestamp = false; + @Parameter private List jvmFlags = Collections.emptyList(); @Nullable @Parameter private String mainClass; @@ -191,6 +193,10 @@ String getTargetImageCredentialHelperName() { return Preconditions.checkNotNull(to).credHelper; } + boolean getUseCurrentTimestamp() { + return container.useCurrentTimestamp; + } + List getJvmFlags() { return container.jvmFlags; } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 44198ae97b..97f211d86a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; @@ -68,11 +69,23 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + Instant before = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", buildToDockerDaemonAndRun( simpleTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/simpleimage:maven")); + Instant buildTime = + Instant.parse( + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/simpleimage:maven") + .run() + .trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } @Test @@ -81,6 +94,16 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi "", buildToDockerDaemonAndRun( emptyTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/emptyimage:maven")); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/emptyimage:maven") + .run() + .trim()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 600566f9c1..5940437df3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; @@ -101,11 +102,24 @@ public void testExecute_simple() throws VerificationException, IOException, Inte + "clean jib:build\" instead of \"mvn clean compile jib:build\"?)")); } + Instant before = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", buildAndRun( simpleTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/simpleimage:maven")); + + Instant buildTime = + Instant.parse( + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/simpleimage:maven") + .run() + .trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } @Test @@ -114,6 +128,16 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi "", buildAndRun( emptyTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/emptyimage:maven")); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/emptyimage:maven") + .run() + .trim()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 0f5c060972..e79af53601 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.time.Instant; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.junit.Assert; @@ -37,6 +38,7 @@ public class BuildTarMojoIntegrationTest { */ @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setAutoclean(false); verifier.executeGoal("package"); @@ -57,5 +59,17 @@ public void testExecute_simple() throws VerificationException, IOException, Inte Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", "gcr.io/jib-integration-testing/simpleimage:maven").run()); + + Instant buildTime = + Instant.parse( + new Command( + "docker", + "inspect", + "-f", + "{{.Created}}", + "gcr.io/jib-integration-testing/simpleimage:maven") + .run() + .trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } } diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index 893473e3f8..012d72ecf6 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -42,6 +42,7 @@ gcr.io/jib-integration-testing/simpleimage:maven + true An argument. 1000/tcp From e360c0cf7303b1d79b2d20b767aad6fbef2cafd5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 17 Jul 2018 18:14:06 -0400 Subject: [PATCH 0026/2020] Additionally allow suffix matching for cred lookup (#642) --- .../json/DockerConfigTemplate.java | 46 +++++++++++++++---- .../DockerConfigCredentialRetrieverTest.java | 26 +++++++++++ .../json/DockerConfigTemplateTest.java | 16 +++++++ .../json/dockerconfig_extra_matches.json | 17 +++++++ .../json/dockerconfig_index_docker_io_v1.json | 5 ++ 5 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 jib-core/src/test/resources/json/dockerconfig_extra_matches.json create mode 100644 jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index b05e53e065..8d1c27b6ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -19,8 +19,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; import javax.annotation.Nullable; /** @@ -77,21 +81,45 @@ private static class AuthTemplate implements JsonTemplate { private final Map credHelpers = new HashMap<>(); /** + * Returns the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if + * none exists. The order of lookup preference: + * + *
    + *
  1. Exact registry name + *
  2. https:// + registry name + *
  3. registry name + arbitrary suffix + *
  4. https:// + registry name + arbitrary suffix + *
+ * * @param registry the registry to get the authorization for * @return the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if * none exists */ @Nullable public String getAuthFor(String registry) { - AuthTemplate registryAuth = auths.get(registry); - if (registryAuth == null) { - // The registry could be prefixed with the HTTPS protocol. - registryAuth = auths.get("https://" + registry); - } - if (registryAuth != null) { - return registryAuth.auth; - } - return null; + Predicate exactMatch = registry::equals; + Predicate withHttps = ("https://" + registry)::equals; + Predicate startsWith = name -> name.startsWith(registry); + Predicate startsWithAndWithHttps = name -> name.startsWith("https://" + registry); + AuthTemplate authTemplate = + getAuthTemplate(Arrays.asList(exactMatch, withHttps, startsWith, startsWithAndWithHttps)); + + return authTemplate != null ? authTemplate.auth : null; + } + + /** Returns the first {@link AuthTemplate} matching the given predicates (short-circuiting). */ + private AuthTemplate getAuthTemplate(List> registryMatches) { + return registryMatches + .stream() + .map(this::getAuthTemplate) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** Returns {@link AuthTemplate} matching the given predicate. */ + private AuthTemplate getAuthTemplate(Predicate registryMatch) { + return auths.keySet().stream().filter(registryMatch).map(auths::get).findFirst().orElse(null); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index b295157617..59015be929 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -135,4 +135,30 @@ public void testRetrieve_credentialFromAlias() throws IOException { Authorization authorization = dockerConfigCredentialRetriever.retrieve(); Assert.assertEquals(mockAuthorization, authorization); } + + @Test + public void testRetrieve_suffixMatching() throws IOException, URISyntaxException { + Path dockerConfigFile = + Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); + + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + new DockerConfigCredentialRetriever( + "index.docker.io", dockerConfigFile, mockDockerCredentialHelperFactory); + + Authorization authorization = dockerConfigCredentialRetriever.retrieve(); + Assert.assertEquals("token for index.docker.io/v1/", authorization.getToken()); + } + + @Test + public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntaxException { + Path dockerConfigFile = + Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); + + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + new DockerConfigCredentialRetriever( + "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); + + Authorization authorization = dockerConfigCredentialRetriever.retrieve(); + Assert.assertEquals("token for index.docker.io/v1/", authorization.getToken()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java index 3a7ce059bb..6dcf67113d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java @@ -57,4 +57,20 @@ public void test_fromJson() throws URISyntaxException, IOException { dockerConfigTemplate.getCredentialHelperFor("another registry")); Assert.assertEquals(null, dockerConfigTemplate.getCredentialHelperFor("unknonwn registry")); } + + @Test + public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertEquals("my-registry: exact match", dockerConfig.getAuthFor("my-registry")); + Assert.assertEquals("cool-registry: with https", dockerConfig.getAuthFor("cool-registry")); + Assert.assertEquals( + "awesome-registry: starting with name", dockerConfig.getAuthFor("awesome-registry")); + Assert.assertEquals( + "dull-registry: starting with name and with https", + dockerConfig.getAuthFor("dull-registry")); + } } diff --git a/jib-core/src/test/resources/json/dockerconfig_extra_matches.json b/jib-core/src/test/resources/json/dockerconfig_extra_matches.json new file mode 100644 index 0000000000..2b81d7cf31 --- /dev/null +++ b/jib-core/src/test/resources/json/dockerconfig_extra_matches.json @@ -0,0 +1,17 @@ +{ + "auths":{ + "https://my-registry/v1/":{"auth":"my-registry: starting with name and with https"}, + "my-registry/v4/":{"auth":"my-registry: starting with name"}, + "https://my-registry":{"auth":"my-registry: with https"}, + "my-registry":{"auth":"my-registry: exact match"}, + + "https://cool-registry":{"auth":"cool-registry: with https"}, + "cool-registry/v8/":{"auth":"cool-registry: starting with registry"}, + "https://cool-registry/v1/":{"auth":"cool-registry: starting with name and with https"}, + + "https://awesome-registry/v9/":{"auth":"awesome-registry: starting with name and with https"}, + "awesome-registry/v9/":{"auth":"awesome-registry: starting with name"}, + + "https://dull-registry/v3/":{"auth":"dull-registry: starting with name and with https"} + } +} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json b/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json new file mode 100644 index 0000000000..29329ce6be --- /dev/null +++ b/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json @@ -0,0 +1,5 @@ +{ + "auths":{ + "index.docker.io/v1/":{"auth":"token for index.docker.io/v1/"} + } +} \ No newline at end of file From fc35488120e9e628f3579625a38c97594787dc35 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 18 Jul 2018 11:43:53 -0400 Subject: [PATCH 0027/2020] Adds note about reading CONTRIBUTING. (#649) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0ab2c5eeff..ebf385d55b 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ We welcome contributions! Here's how you can contribute: * Chat with us on [gitter](https://gitter.im/google/jib) * Join the [jib-users mailing list](https://groups.google.com/forum/#!forum/jib-users) * Contribute: + * *Read the [contributing guide](https://github.com/GoogleContainerTools/jib/blob/master/CONTRIBUTING.md) before starting work on an issue* * Try to fix [good first issues](https://github.com/GoogleContainerTools/jib/labels/good%20first%20issue) * Help out on [issues that need help](https://github.com/GoogleContainerTools/jib/labels/help%20wanted) * Join in on [discussion issues](https://github.com/GoogleContainerTools/jib/labels/discuss) From 5c94263776db5360eeff2c4467f0bebcd6520796 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 18 Jul 2018 12:21:35 -0400 Subject: [PATCH 0028/2020] Look up cred helper additionally with possible suffixes (#650) --- .../json/DockerConfigTemplate.java | 77 +++++++++++-------- .../json/DockerConfigTemplateTest.java | 67 ++++++++++++++++ .../src/test/resources/json/dockerconfig.json | 17 +++- 3 files changed, 126 insertions(+), 35 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index 8d1c27b6ff..5ca753ce94 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -72,6 +72,24 @@ private static class AuthTemplate implements JsonTemplate { @Nullable private String auth; } + /** + * Returns the first value matching the given key predicates (short-circuiting in the order of + * predicates). + */ + private static T findFirstInMapByKey(Map map, List> keyMatches) { + return keyMatches + .stream() + .map(keyMatch -> findFirstInMapByKey(map, keyMatch)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** Returns the first value matching the given key predicate. */ + private static T findFirstInMapByKey(Map map, Predicate keyMatch) { + return map.keySet().stream().filter(keyMatch).map(map::get).findFirst().orElse(null); + } + /** Maps from registry to its {@link AuthTemplate}. */ private final Map auths = new HashMap<>(); @@ -87,8 +105,8 @@ private static class AuthTemplate implements JsonTemplate { *
    *
  1. Exact registry name *
  2. https:// + registry name - *
  3. registry name + arbitrary suffix - *
  4. https:// + registry name + arbitrary suffix + *
  5. registry name + / + arbitrary suffix + *
  6. https:// + registry name + / arbitrary suffix *
* * @param registry the registry to get the authorization for @@ -97,48 +115,41 @@ private static class AuthTemplate implements JsonTemplate { */ @Nullable public String getAuthFor(String registry) { - Predicate exactMatch = registry::equals; - Predicate withHttps = ("https://" + registry)::equals; - Predicate startsWith = name -> name.startsWith(registry); - Predicate startsWithAndWithHttps = name -> name.startsWith("https://" + registry); - AuthTemplate authTemplate = - getAuthTemplate(Arrays.asList(exactMatch, withHttps, startsWith, startsWithAndWithHttps)); - + AuthTemplate authTemplate = findFirstInMapByKey(auths, getRegistryMatchersFor(registry)); return authTemplate != null ? authTemplate.auth : null; } - /** Returns the first {@link AuthTemplate} matching the given predicates (short-circuiting). */ - private AuthTemplate getAuthTemplate(List> registryMatches) { - return registryMatches - .stream() - .map(this::getAuthTemplate) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - /** Returns {@link AuthTemplate} matching the given predicate. */ - private AuthTemplate getAuthTemplate(Predicate registryMatch) { - return auths.keySet().stream().filter(registryMatch).map(auths::get).findFirst().orElse(null); - } - /** + * Returns {@code credsStore} or {@code credHelpers} for the given {@code registry}. If there + * exists a matching registry entry in {@code auths}, returns {@code credStore}; otherwise, a + * matching entry in {@code credHelpers} is returned based on the following lookup order: + * + *
    + *
  1. Exact registry name + *
  2. https:// + registry name + *
  3. registry name + / + arbitrary suffix + *
  4. https:// + registry name + / + arbitrary suffix + *
+ * * @param registry the registry to get the credential helpers for * @return {@code credsStore} if {@code registry} is present in {@code auths}; otherwise, searches * {@code credHelpers}; otherwise, {@code null} if not found */ @Nullable public String getCredentialHelperFor(String registry) { - if (credsStore != null) { - // The registry could be prefixed with the HTTPS protocol. - if (auths.containsKey(registry) || auths.containsKey("https://" + registry)) { - return credsStore; - } - } - if (credHelpers.containsKey(registry)) { - return credHelpers.get(registry); + List> registryMatchers = getRegistryMatchersFor(registry); + if (credsStore != null && findFirstInMapByKey(auths, registryMatchers) != null) { + return credsStore; } - return null; + return findFirstInMapByKey(credHelpers, registryMatchers); + } + + private List> getRegistryMatchersFor(String registry) { + Predicate exactMatch = registry::equals; + Predicate withHttps = ("https://" + registry)::equals; + Predicate withSuffix = name -> name.startsWith(registry + "/"); + Predicate WithHttpsAndSuffix = name -> name.startsWith("https://" + registry + "/"); + return Arrays.asList(exactMatch, withHttps, withSuffix, WithHttpsAndSuffix); } @VisibleForTesting diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java index 6dcf67113d..e627751bc2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java @@ -73,4 +73,71 @@ public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, I "dull-registry: starting with name and with https", dockerConfig.getAuthFor("dull-registry")); } + + @Test + public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertNull(dockerConfig.getAuthFor("example")); + } + + @Test + public void testGetCredentialHelperFor() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertEquals( + "some credential store", dockerConfig.getCredentialHelperFor("just registry")); + } + + @Test + public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertEquals( + "some credential store", dockerConfig.getCredentialHelperFor("with.protocol")); + } + + @Test + public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertEquals( + "some credential store", dockerConfig.getCredentialHelperFor("with.suffix")); + } + + @Test + public void testGetCredentialHelperFor_withProtocolAndSuffix() + throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertEquals( + "some credential store", dockerConfig.getCredentialHelperFor("with.protocol.and.suffix")); + } + + @Test + public void testGetCredentialHelperFor_correctSuffixMatching() + throws URISyntaxException, IOException { + Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + + DockerConfigTemplate dockerConfig = + JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + + Assert.assertNull(dockerConfig.getCredentialHelperFor("example")); + Assert.assertNull(dockerConfig.getCredentialHelperFor("another.example")); + } } diff --git a/jib-core/src/test/resources/json/dockerconfig.json b/jib-core/src/test/resources/json/dockerconfig.json index cc8c1d9093..ce7eed37a7 100644 --- a/jib-core/src/test/resources/json/dockerconfig.json +++ b/jib-core/src/test/resources/json/dockerconfig.json @@ -3,13 +3,26 @@ "some other registry":{"auth":"some other auth"}, "some registry":{"auth":"some auth","password":"ignored"}, "https://registry":{"auth":"token"}, + "just registry":{}, - "https://with.protocol":{} + "https://with.protocol":{}, + "with.suffix/suffix/":{}, + "https://with.protocol.and.suffix/v10/":{}, + + "example.com":{"auth":"should not match example"} }, "credsStore":"some credential store", "credHelpers":{ "another registry":"another credential helper", "some registry":"some credential helper", - "index.docker.io":"index.docker.io credential helper" + "index.docker.io":"index.docker.io credential helper", + + "just.registry.in.helpers":"credHelper for just.registry.in.helpers", + "https://with.protocol.in.helpers":"credHelper for https://with.protocol.in.helpers", + "with.suffix.in.helpers/v2/":"credHelper for with.suffix.in.helpers/v2/", + "https://with.protocol.and.suffix.in.helpers/suffix": + "credHelper for https://with.protocol.and.suffix.in.helpers/suffix", + + "another.example.com.in.helpers":"should not match example" } } \ No newline at end of file From 2dd7a4e5b126f7657b4d37260be3c903ab231d2d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 18 Jul 2018 16:29:16 -0400 Subject: [PATCH 0029/2020] Document alternative trust-store option for self-signed certificates (#652) --- docs/self_sign_cert-kse-import.png | Bin 0 -> 188746 bytes docs/self_sign_cert.md | 118 ++++++++++++++++++++++------- 2 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 docs/self_sign_cert-kse-import.png diff --git a/docs/self_sign_cert-kse-import.png b/docs/self_sign_cert-kse-import.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fce5e0fe32a2b63ca6b02e2cc703b669fc7039 GIT binary patch literal 188746 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 literal 0 HcmV?d00001 diff --git a/docs/self_sign_cert.md b/docs/self_sign_cert.md index b2581b3958..fadea5b3cd 100644 --- a/docs/self_sign_cert.md +++ b/docs/self_sign_cert.md @@ -1,26 +1,17 @@ -# Accessing private docker registry with self-signed certificate +# Accessing a private docker registry with self-signed certificate -Currently, `jib` does not support docker registries with self-signed `https` certificate. +Jib relies on the Java Runtime Environment's list of approved _Certification Authority Certificates_ for validating SSL certificates, and will hence fail when connecting to a docker registry that uses a self-signed `https` certificate. This document describes two approaches for handling registries with self-signed certificates. Both approaches configure the JRE's list of approved CA Certificates. -Jib uses the JVM's list of approved CA Certificates to validate SSL certificates. The following instructions describe how to add a registry's self-signed certificate to the JVM's approved CAs. +These CA Certificates are managed through a _keystore_ file. The easiest way to manipulate keystores is using the [KeyStore Explorer](http://keystore-explorer.org/), an open source GUI replacement for the Java command-line `keytool` and `jarsigner` utilities. Download and install KeyStore Explorer from the [official website](http://keystore-explorer.org/downloads.html). -The certificate will be trusted at the JRE level, affecting all Java applications running on it. You will also need to re-import the certificate when you use a different JRE or upgrade it. -## Using KeyStore Explorer +## Step 1. Identify Java runtime used by build tool -The easiest way to import the self-signed certificate into JVM is using the [KeyStore Explorer](http://keystore-explorer.org/). +We must first identify the location of your build-tool's JRE's list of CA Certificates. -KeyStore Explorer is an open source GUI replacement for the Java command-line utilities keytool and jarsigner. KeyStore Explorer presents their functionality, and more, via an intuitive graphical user interface. +### Maven -### Installation - -Download and install KeyStore Explorer from [official website](http://keystore-explorer.org/downloads.html) - -### Indentify java runtime being used by build tool - -#### Maven - -Run `mvn --version` and take note on java runtime: +Run `mvn --version` and take note of the Java runtime location: ```shell $ mvn --version @@ -31,23 +22,92 @@ Default locale: en_NZ, platform encoding: UTF-8 OS name: "mac os x", version: "10.13.6", arch: "x86_64", family: "mac" ``` -On this example the `java runtime` is `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre`. +In this example the Java runtime location is `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre`. -### Import certificate +### Gradle -* Launch `KeyStore Explorer` +Create an init script with the following: -#### 1. Select java runtime +``` +println org.gradle.internal.jvm.Jvm.current().getJavaHome() +``` + +And run `gradle -I /path/to/script` to output the executing JRE location. -* Open `Preferences` -* Go to `Authorithy Certificates` -* Replace `CA Certificates KeyStore` with the `java runtime` from the previous step. Include `lib/security/cacerts` on the path. +```shell +$ gradle -I /tmp/printjrelocation +/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home -#### 2. Import certificate +> Task :help + +Welcome to Gradle 4.6. +[...] +``` -* Hit `Open CA Certificates KeyStore` on the main window -* On the tool bar, click on `Import Trusted Certificate` +### JRE vs JDK Distributions + +The Maven and Gradle examples above report two different directories, where the Maven example reported a `.../jre` subdirectory. Java Development Kits usually include a standalone Java Runtime Environment inside the `jre/` directory. If present, use the `jre/` directory as the runtime location. + +## 2. Load JRE CA Certificates + +Having identified your Java runtime location: + +* Launch `KeyStore Explorer` +* Select _Open an existing KeyStore_ +* Navigate to the Java runtime location identified previously, and then continue to open the file at `jre/lib/security/cacerts`. If there is no `jre/` directory then this is a JRE distribution and should navigate and instead open the file at `lib/security/cacerts`. + * In the example above, this file would be `/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/security/cacerts`. +* You will likely be prompted for a password. The default password for the `cacerts` file is `changeit`. + +## 3. Import Self-Signed Certificate + +If you have the self-signed certificate in a file then: + +* Select _Tools > Import Trusted Certificate_ * Select the certifcate file on disk -* Give it a name, or use suggested name, hit `OK` -* Hit ok on the success window -* Last step, hit `Save` +* Give it a name, or use suggested name, and click _OK_ +* Click _OK_ on the success window + +Otherwise use _Examine > Examine SSL_ to connect to your service and click the _Import_ button to import its SSL certificate. Then click _OK_. + +![Importing certificate with KeyStore Explorer](self_sign_cert-kse-import.png) + +## 4. Save the CA Certificates + +Now we save the updated keystore. We can either save to a new keystore and configure our build's JVM to use this new keystore as a _trusted keystore_, or modify the JRE's list of CA Certificates. + +#### Option 1: Create a New Trusted Keystore + +This option creates a _new_ list of CA Certificates and configures your build tool to use ths new list as the JRE's list of approved CA certificates, called the _trust store_. + +Within _KeyStore Explorer_, select _File > Save As..._ and save the new keystore file as a _JKS_ file within your project location. You will be prompted for a password; we use `password` in the examples below. + +##### Maven + +The following snippet shows how to configure Maven to use this new keystore file: + +```shell +$ ./mvnw -Djavax.net.ssl.trustStore=path/to/keystore.jks \ + -Djavax.net.ssl.trustStorePassword=password \ + -Dimage=:/ jib:build +``` + +You may choose to configure your registry credentials with [the `~/.m2/settings.xml` mechanism](https://github.com/GoogleContainerTools/jib/blob/master/jib-maven-plugin/README.md#using-maven-settings). + +##### Gradle + +The following snippet shows how to configure Gradle to use this new keystore file: + +```shell +$ ./gradlew jib \ + -Djavax.net.ssl.trustStore=path/to/keystore.jks \ + -Djavax.net.ssl.trustStorePassword=password +``` + +You may choose to explicitly configure your registry credentials [in your build.gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials). + +#### Option 2: Modify the JRE `cacerts` + +The other approach modifies the JRE's list of CA Certificates to include the registry's self-signed certificate. The certificate will be trusted at the JRE level, affecting all Java applications running on it. You must re-import the certificate when you update to a new JRE. + +Basically you instruct KeyStore Explorer to save your modified `cacerts` and replace what was previously configured with the JRE. Depending on your operating system and permissions, you may need to save to a new file and then replace the original `lib/security/cacerts` file with administrative privileges. + From d200f6fbf646785bc220c281838f96aff220de2e Mon Sep 17 00:00:00 2001 From: Duke Nguyen Date: Thu, 19 Jul 2018 09:52:15 +0700 Subject: [PATCH 0030/2020] Fix NullPointerException when jib-maven-plugin is configured with environment variables (#653) * Fix NullPointerException when jib-maven-plugin is configured with environment variables * Test the BuildConfiguration.Builder can accept the empty environment map * Fix review comments --- .../tools/jib/builder/BuildConfiguration.java | 5 +- .../jib/builder/BuildConfigurationTest.java | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java index 500e123a77..de4181d7ba 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java @@ -27,10 +27,12 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.annotation.Nullable; import javax.lang.model.SourceVersion; @@ -120,7 +122,8 @@ public Builder setJvmFlags(@Nullable List jvmFlags) { public Builder setEnvironment(@Nullable Map environmentMap) { if (environmentMap != null) { Preconditions.checkArgument( - !environmentMap.containsKey(null) && !environmentMap.containsValue(null)); + !Iterables.any(environmentMap.keySet(), Objects::isNull) + && !Iterables.any(environmentMap.values(), Objects::isNull)); this.environmentMap = ImmutableMap.copyOf(environmentMap); } return this; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java index abad5c1082..34d06b826c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java @@ -31,8 +31,11 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -200,6 +203,65 @@ public void testBuilder_missingValues() { } } + @Test + @SuppressWarnings("JdkObsolete") + public void testBuilder_nullValues() { + // Java arguments element should not be null. + try { + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setJavaArguments(Arrays.asList("first", null)); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertNull(ex.getMessage()); + } + + // JVM flags element should not be null. + try { + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setJvmFlags(Arrays.asList("first", null)); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertNull(ex.getMessage()); + } + + // Exposed ports element should not be null. + try { + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setExposedPorts(Arrays.asList(new Port(1000, Protocol.TCP), null)); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertNull(ex.getMessage()); + } + + // Environment keys element should not be null. + Map nullKeyMap = new HashMap<>(); + nullKeyMap.put(null, "value"); + + try { + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(nullKeyMap); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertNull(ex.getMessage()); + } + + // Environment values element should not be null. + Map nullValueMap = new HashMap<>(); + nullValueMap.put("key", null); + try { + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(nullValueMap); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertNull(ex.getMessage()); + } + + // Can accept empty environment. + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(ImmutableMap.of()); + + // Environment map can accept TreeMap and Hashtable. + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(new TreeMap<>()); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(new Hashtable<>()); + } + @Test public void testValidJavaClassRegex() { Assert.assertTrue(BuildConfiguration.isValidJavaClass("my.Class")); From 99b588f5a2cc54d85b6c2a037820b03b30f09a6b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 11:02:00 -0400 Subject: [PATCH 0031/2020] Fix maven CHANGELOG newline inconsistency (#660) --- jib-maven-plugin/CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 730b97d149..be80d3ebb0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -39,30 +39,38 @@ All notable changes to this project will be documented in this file. ## 0.9.3 ### Fixed + - Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol) ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) ## 0.9.2 ### Changed + - Minor improvements and issue fixes ## 0.9.1 + ### Added + - `` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`) ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) - Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) ### Changed + - Fetches credentials from inferred credential helper before Docker config ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) - Container creation date set to timestamp 0 ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) - Does not authenticate base image pull unless necessary - reduces build time by about 500ms ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) - `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) ### Fixed + - Using Azure Container Registry now works - define credentials in Maven settings ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) - Supports `access_token` as alias to `token` in registry authentication ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) ## 0.9.0 + ### Added + - Better feedback for build failures ([#197](https://github.com/google/jib/pull/197)) - Warns if specified `mainClass` is not a valid Java class ([#206](https://github.com/google/jib/issues/206)) - Warns if build may not be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) @@ -72,45 +80,62 @@ All notable changes to this project will be documented in this file. - `args` parameter to define default main args ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) ### Changed + - Removed `enableReproducibleBuilds` parameter - application layers will always be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) - Changed configuration schema to be more like configuration for `jib-gradle-plugin` - NOT compatible with prior versions of `jib-maven-plugin` ([#212](https://github.com/GoogleContainerTools/jib/issues/212)) - `jib:dockercontext` has been changed to `jib:exportDockerContext` ([#350](https://github.com/GoogleContainerTools/jib/issues/350)) ### Fixed + - Directories in resources are added to classes layer ([#318](https://github.com/GoogleContainerTools/jib/issues/318)) ## 0.1.7 + ### Fixed + - Using base images that lack entrypoints ([#284](https://github.com/GoogleContainerTools/jib/pull/284) ## 0.1.6 + ### Changed + - Base image layers are now cached on a user-level rather than a project level - disable with `useOnlyProjectCache` configuration ([#29](https://github.com/google/jib/issues/29)) ### Fixed + - `jib:dockercontext` not building a `Dockerfile` ([#171](https://github.com/google/jib/pull/171)) - Failure to parse Docker config with `HttpHeaders` field ([#175](https://github.com/google/jib/pull/175)) ## 0.1.5 + ### Added + - Export a Docker context (including a Dockerfile) with `jib:dockercontext` ([#49](https://github.com/google/jib/issues/49)) ## 0.1.4 + ### Fixed + - Null tag validation generating NullPointerException ([#125](https://github.com/google/jib/issues/125)) - Build failure on project with no dependencies ([#126](https://github.com/google/jib/issues/126)) ## 0.1.3 + ### Added + - Build and push OCI container image ([#96](https://github.com/google/jib/issues/96)) ## 0.1.2 + ### Added + - Use credentials from Docker config if none can be found otherwise ([#101](https://github.com/google/jib/issues/101)) - Reproducible image building ([#7](https://github.com/google/jib/issues/7)) ## 0.1.1 + ### Added + - Simple example `helloworld` project under `examples/` ([#62](https://github.com/google/jib/pull/62)) - Better error messages when pushing an image manifest ([#63](https://github.com/google/jib/pull/63)) - Validates target image configuration ([#63](https://github.com/google/jib/pull/63)) @@ -118,9 +143,11 @@ All notable changes to this project will be documented in this file. - Configure registry credentials with Maven settings ([#81](https://github.com/google/jib/pull/81)) ### Changed + - Removed configuration `credentialHelperName` ([#68](https://github.com/google/jib/pull/68)) ### Fixed + - Build failure on Windows ([#74](https://github.com/google/jib/issues/74)) - Infers common credential helper names (for GCR and ECR) ([#64](https://github.com/google/jib/pull/64)) - Cannot use private base image ([#68](https://github.com/google/jib/pull/68)) From c683f4c7d63fe7382e8873443e9210182a334ed0 Mon Sep 17 00:00:00 2001 From: Dviejo Date: Thu, 19 Jul 2018 17:56:32 +0200 Subject: [PATCH 0032/2020] Changed gradle documentation export docker context from --jib.dockerDir to --targetDir (#661) In #499, the option from gradle to export the docker context was changed, but it wasn't reflected in the docs. --- jib-gradle-plugin/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 54ba2063b9..6d8794fb2c 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -127,10 +127,10 @@ Jib can also export a Docker context so that you can build with Docker, if neede gradle jibExportDockerContext ``` -The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `---jib.dockerDir` parameter: +The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `---targetDir` parameter: ```shell -gradle jibExportDockerContext --jib.dockerDir=my/docker/context/ +gradle jibExportDockerContext --targetDir=my/docker/context/ ``` You can then build your image with Docker: From ff77455ea6f709727246b5129f978561205c476a Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 19 Jul 2018 12:44:52 -0400 Subject: [PATCH 0033/2020] Add instructions for building & testing/debugging locally (#655) --- CONTRIBUTING.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++--- build.sh | 14 ++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 build.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee0674066c..260d5d29f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,16 @@ You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. +## Building Jib + +Jib comes as 3 components: + + - `jib-core`: a library + - `jib-maven-plugin`: a Maven plugin that uses `jib-core` + - `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` + +To build, use the provided `build.sh` which builds and tests each of the components into your local `~/.m2/repository`. Note that this script does not run integration tests. + ## Code Reviews 1. Set your git user.email property to the address used for step 1. E.g. @@ -25,11 +35,79 @@ again. use your corporate email address here, not your personal address. 2. Fork the repository into your own Github account. 3. Please include unit tests (and integration tests if applicable) for all new code. -4. Make sure all existing tests pass. +4. Make sure all existing tests pass (but see the note below about integration tests). * In `jib-core`, run `./gradlew clean goJF build integrationTest` - * In `jib-gradle-plugin`, run `./gradlew clean goJF build` * - * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify` * + * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` + * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -Pintegration-tests` 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! -\* *Note that you will not be able to run the integration tests for `jib-gradle-plugin` or `jib-maven-plugin` because those push to our integration-testing GCP project. If you would like to run integration tests, change all uses of the `jib-integration-testing` project to your own GCP project and run `./gradlew integrationTest` for `jib-gradle-plugin` and `./mvnw verify -Pintegration-tests` for `jib-maven-plugin`.* +**Note** that you will not be able to run the integration tests for `jib-gradle-plugin` or `jib-maven-plugin` because those push to our integration-testing GCP project. If you would like to run integration tests, change all uses of the `jib-integration-testing` project to your own GCP project and run `./gradlew integrationTest` for `jib-gradle-plugin` and `./mvnw verify -Pintegration-tests` for `jib-maven-plugin`. + +# Development Tips + +## Debugging the Jib Maven Plugin (`jib-maven-plugin`) + +### Build and use a local snapshot + +To use a local build of the `jib-maven-plugin`: + + 1. Build and install `jib-maven-plugin` into your local `~/.m2/repository` + with `(cd jib-maven-plugin && ./mvnw install)`; this also builds `jib-core`. + Alternatively, use the provided `build.sh` which performs an `install`. + 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` + version of the `com.google.cloud.tools.jib` plugin. + +If developing from within Eclipse with M2Eclipse (the Maven tooling for Eclipse): + + 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` + version of the `com.google.cloud.tools.jib` plugin. + 2. Create and launch a _Maven Build_ launch configuration for the + test project, and ensure the _Resolve Workspace artifacts_ is checked. + +### Attaching a debugger + +Run `mvnDebug jib:build` and attach to port 8000. + +If developing with Eclipse and M2Eclipse (the Maven tooling for Eclipse), just launch the _Maven Build_ with _Debug_. + +## Debugging the Jib Gradle Plugin (`jib-gradle-plugin`) + +### Build and use a local snapshot + +To use a local build of the `jib-gradle-plugin`: + + 1. Build and install `jib-gradle-plugin` into your local `~/.m2/repository` + with `(cd jib-gradle-plugin && ./gradlew build install)`; this also builds `jib-core`. + Alternatively, use the provided `build.sh` which performs an `install`. + 1. Modify your test project's `build.gradle` to look like the following: + ```groovy + buildscript { + repositories { + mavenLocal() // resolve in ~/.m2/repository + mavenCentral() + } + dependencies { + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.7-SNAPSHOT' + } + } + + plugins { + // id 'com.google.cloud.tools.jib' version '0.9.6' + } + + // Applies the java plugin after Jib to make sure it works in this order. + apply plugin: 'com.google.cloud.tools.jib' // must explicitly apply local + apply plugin: 'java' + ``` + +### Attaching a debugger + +Attach a debugger to a Gradle instance by running Gradle as follows: + +```shell +./gradlew jib \ + --no-daemon \ + -Dorg.gradle.jvmargs='-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5005,suspend=y' +``` + diff --git a/build.sh b/build.sh new file mode 100644 index 0000000000..c8f5938f5b --- /dev/null +++ b/build.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Build Jib + +# doBuild: Run a command in a directory +# $1 = directory +# $2... = build command +doBuild() { + (directory="$1"; shift; echo ">>> $directory: $*"; cd "$directory" && eval '"$@"') +} + +set -e # exit on error +doBuild jib-core ./gradlew googleJavaFormat build +doBuild jib-gradle-plugin ./gradlew googleJavaFormat build install +doBuild jib-maven-plugin ./mvnw fmt:format install -U From 5310e5872cdd08c8a3cc09f02fbcbd191b4524f6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 15:01:38 -0400 Subject: [PATCH 0034/2020] Move FAQ to docs/ folder (#666) --- README.md | 2 +- docs/faq.md | 181 ++++++++++++++++++++++++++++++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 docs/faq.md diff --git a/README.md b/README.md index ebf385d55b..d70e1b5e85 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a simila ## Frequently Asked Questions (FAQ) -See the [Frequently Asked Questions (FAQ) wiki page](/../../wiki/Frequently-Asked-Questions-(FAQ)). +See the [Frequently Asked Questions (FAQ) page](docs/faq.md). ## Get involved with the community diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..e42a7c367b --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,181 @@ +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/wiki/faq)](https://github.com/igrigorik/ga-beacon) + +## Frequently Asked Questions (FAQ) + +If a question you have is not answered below, please [submit an issue](/../../issues/new). + +[But, I'm not a Java developer.](#but-im-not-a-java-developer)\ +[What image format does Jib use?](#what-image-format-does-jib-use)\ +[Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint)\ +[But I just want to set some JVM flags when running the image?](#but-i-just-want-to-set-some-jvm-flags-when-running-the-image)\ +[Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ +[I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ +[Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ +[Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ +[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ +[How do I enable debugging?](#how-do-i-enable-debugging)\ +[Why is my image created 48 years ago?](#why-is-my-image-created-48-years-ago)\ +[I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ +[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp) + +### But, I'm not a Java developer. + +See [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). The tool can build images for languages such as Python, NodeJS, Java, Scala, Groovy, C, Go, Rust, and D. + +### What image format does Jib use? + +Jib currently builds into the [Docker V2.2](https://docs.docker.com/registry/spec/manifest-v2-2/) image format or [OCI image format](https://github.com/opencontainers/image-spec). + +#### Maven + +See [Extended Usage](../jib-maven-plugin#extended-usage) for the `` configuration. + +#### Gradle + +See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.format` configuration. + +### Can I define a custom entrypoint? + +The plugin attaches a default entrypoint that will run your application automatically. + +When running the image, you can override this default entrypoint with your own custom command. + +See [`docker run --entrypoint` reference](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) for running the image with Docker and overriding the entrypoint command. + +See [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. + +### But I just want to set some JVM flags when running the image? + +When running the image, you can pass in additional JVM flags via the [`JAVA_TOOL_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html). + +See [`docker run -e` reference](https://docs.docker.com/engine/reference/run/#env-environment-variables) for running the image with Docker and setting environment variables. + +See [Define Environment Variables for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and setting environment variables. + +### Where is the application in the container filesystem? + +Jib packages your Java application into the following paths on the image: + +* `/app/libs/` contains all the dependency artifacts +* `/app/resources/` contains all the resource files +* `/app/classes/` contains all the classes files + +### I need to RUN commands like `apt-get`. + +Running commands like `apt-get` slows down the container build process. We **do not recommend or support** running commands as part of the build. + +However, if you need to run commands, you can build a custom image and configure Jib to use it as the base image. + +
+Base image configuration examples +

+ +#### Maven + +In [`jib-maven-plugin`](../jib-maven-plugin), you can then use this custom base image by adding the following configuration: + +```xml + + + custom-base-image + + +``` + +#### Gradle + +In [`jib-gradle-plugin`](../jib-gradle-plugin), you can then use this custom base image by adding the following configuration: + +```groovy +jib.from.image = 'custom-base-image' +``` +

+
+ +### Can I ADD a custom directory to the image? + +We currently support adding a custom directory with an **incubating** feature. This feature may change in later versions. If your application needs to use custom files, place them into the `src/main/jib` folder. Files placed here will be added to the filesystem of the container. For example, `src/main/jib/foo/bar` would add `/foo/bar` into the container filesystem. + +### Can I build to a local Docker daemon? + +See [`jib:dockerBuild` for Maven](../jib-maven-plugin#build-to-docker-daemon) and [`jibDockerBuild` for Gradle](../jib-gradle-plugin#build-to-docker-daemon). + +You can also [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. + +You can also [run a local Docker registry](https://docs.docker.com/registry/deploying/) and point Jib to push to the local registry. + +### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). + +When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). + +```shell +kubectl create secret docker-registry registry-json-key \ + --docker-server= \ + --docker-username= \ + --docker-password= \ + --docker-email= + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"registry-json-key"}]}' +``` + +For example, if you are using GCR, the commands would look like (see [Advanced Authentication Methods](https://cloud.google.com/container-registry/docs/advanced-authentication)): + +```shell +kubectl create secret docker-registry gcr-json-key \ + --docker-server=https://gcr.io \ + --docker-username=_json_key \ + --docker-password="$(cat keyfile.json)" \ + --docker-email=any@valid.com + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"gcr-json-key"}]}' +``` + +See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). + +### How do I enable debugging? + +*TODO: Provide solution.* + +### Why is my image created 48 years ago? + +For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use a real creation time, set [work in progress](https://github.com/GoogleContainerTools/jib/issues/413). + +### I would like to run my application with a javaagent. + +See [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image). + +*TODO: Provide more comprehensive solution.* + +### How can I tag my image with a timestamp? + +#### Maven + +To tag the image with a simple timestamp, add the following to your `pom.xml`: + +```xml + + yyyyMMdd-HHmmssSSS + +``` + +Then in the `jib-maven-plugin` configuration, set the `tag` to: + +```xml + + + my-image-name:${maven.build.timestamp} + + +``` + +You can then use the same timestamp to reference the image in other plugins. + +#### Gradle + +To tag the image with a timestamp, simply set the timestamp as the tag for `to.image` in your `jib` configuration. For example: + +```groovy +jib.to.image = 'gcr.io/my-gcp-project/my-app:' + System.nanoTime() +``` diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 6d8794fb2c..7376189c1e 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -291,7 +291,7 @@ See the [Jib project README](/../../#how-jib-works). ## Frequently Asked Questions (FAQ) -See the [Jib project README](/../../#frequently-asked-questions-faq). +See the [Jib project FAQ](../docs/faq.md). ## Community diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 5eb03e438e..9367722e88 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -320,7 +320,7 @@ See the [Jib project README](/../../#how-jib-works). ## Frequently Asked Questions (FAQ) -See the [Jib project README](/../../#frequently-asked-questions-faq). +See the [Jib project FAQ](../docs/faq.md). ## Community From 68edcc6ea010dff2857e1e698512e2c13cf369ef Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 19 Jul 2018 15:23:25 -0400 Subject: [PATCH 0035/2020] Enable jib.httpTimeout system property for HTTP connection and read timeout (#656) * Enable jib.httpTimeout system property * Set timeout only when property is defined * Opt for simple code * Update CHANGELOG files --- .../cloud/tools/jib/http/Connection.java | 12 ++- .../google/cloud/tools/jib/http/Request.java | 24 +++++- .../jib/registry/RegistryAuthenticator.java | 3 +- .../jib/registry/RegistryEndpointCaller.java | 1 + .../cloud/tools/jib/http/ConnectionTest.java | 70 ++++++++++++------ .../cloud/tools/jib/http/MockConnection.java | 47 ++++++++++++ .../cloud/tools/jib/http/RequestTest.java | 38 ++++++++++ .../registry/RegistryEndpointCallerTest.java | 74 +++++++++++++++++++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 10 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 9df7dd6dd6..7c0702b4e3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -18,6 +18,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.apache.ApacheHttpTransport; @@ -117,11 +118,16 @@ public Response put(Request request) throws IOException { * @throws IOException if building the HTTP request fails. */ public Response send(String httpMethod, Request request) throws IOException { - httpResponse = + HttpRequest httpRequest = requestFactory .buildRequest(httpMethod, url, request.getHttpContent()) - .setHeaders(request.getHeaders()) - .execute(); + .setHeaders(request.getHeaders()); + if (request.getHttpTimeout() != null) { + httpRequest.setConnectTimeout(request.getHttpTimeout()); + httpRequest.setReadTimeout(request.getHttpTimeout()); + } + + httpResponse = httpRequest.execute(); return new Response(httpResponse); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java index b3fae5cf59..6c00d8ace3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java @@ -27,12 +27,16 @@ public class Request { private final HttpHeaders headers; /** The HTTP request body. */ - @Nullable private BlobHttpContent body; + @Nullable private final BlobHttpContent body; + + /** HTTP connection and read timeout. */ + @Nullable private final Integer httpTimeout; public static class Builder { private final HttpHeaders headers = new HttpHeaders().setAccept("*/*"); @Nullable private BlobHttpContent body; + @Nullable private Integer httpTimeout; public Request build() { return new Request(this); @@ -73,6 +77,18 @@ public Builder setUserAgent(String userAgent) { return this; } + /** + * Sets the HTTP connection and read timeout in milliseconds. {@code null} uses the default + * timeout and {@code 0} an infinite timeout. + * + * @param httpTimeout timeout in milliseconds + * @return this + */ + public Builder setHttpTimeout(@Nullable Integer httpTimeout) { + this.httpTimeout = httpTimeout; + return this; + } + /** * Sets the body and its corresponding {@code Content-Type} header. * @@ -92,6 +108,7 @@ public static Builder builder() { private Request(Builder builder) { this.headers = builder.headers; this.body = builder.body; + this.httpTimeout = builder.httpTimeout; } HttpHeaders getHeaders() { @@ -102,4 +119,9 @@ HttpHeaders getHeaders() { BlobHttpContent getHttpContent() { return body; } + + @Nullable + Integer getHttpTimeout() { + return httpTimeout; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 1fabdb6acb..eb1c5072c2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -239,7 +239,8 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa URL authenticationUrl = getAuthenticationUrl(scope); try (Connection connection = new Connection(authenticationUrl)) { - Request.Builder requestBuilder = Request.builder(); + Request.Builder requestBuilder = + Request.builder().setHttpTimeout(Integer.getInteger("jib.httpTimeout")); if (authorization != null) { requestBuilder.setAuthorization(authorization); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index f59e042fd1..2848c664bf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -162,6 +162,7 @@ T call(RequestState requestState) throws IOException, RegistryException { Request.Builder requestBuilder = Request.builder() .setUserAgent(userAgent) + .setHttpTimeout(Integer.getInteger("jib.httpTimeout")) .setAccept(registryEndpointProvider.getAccept()) .setBody(registryEndpointProvider.getContent()); // Only sends authorization if using HTTPS. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index e6c5045d5d..a1a2152e5a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -28,7 +28,6 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -55,47 +54,76 @@ public class ConnectionTest { @InjectMocks private final Connection testConnection = new Connection(fakeUrl.toURL()); - @Before - public void setUpMocksAndFakes() throws IOException { - fakeRequest = - Request.builder() - .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) - .setUserAgent("fake user agent") - .setBody(new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type")) - .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) - .build(); - - Mockito.when( - mockHttpRequestFactory.buildRequest( - Mockito.any(String.class), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) - .thenReturn(mockHttpRequest); - - Mockito.when(mockHttpRequest.setHeaders(Mockito.any(HttpHeaders.class))) - .thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); - } - @Test public void testGet() throws IOException { + setUpMocksAndFakes(null); testSend(HttpMethods.GET, Connection::get); } @Test public void testPost() throws IOException { + setUpMocksAndFakes(null); testSend(HttpMethods.POST, Connection::post); } @Test public void testPut() throws IOException { + setUpMocksAndFakes(null); testSend(HttpMethods.PUT, Connection::put); } + @Test + public void testHttpTimeout_doNotSetByDefault() throws IOException { + setUpMocksAndFakes(null); + try (Connection connection = testConnection) { + connection.send(HttpMethods.GET, fakeRequest); + } + + Mockito.verify(mockHttpRequest, Mockito.never()).setConnectTimeout(Mockito.anyInt()); + Mockito.verify(mockHttpRequest, Mockito.never()).setReadTimeout(Mockito.anyInt()); + } + + @Test + public void testHttpTimeout() throws IOException { + setUpMocksAndFakes(5982); + try (Connection connection = testConnection) { + connection.send(HttpMethods.GET, fakeRequest); + } + + Mockito.verify(mockHttpRequest).setConnectTimeout(5982); + Mockito.verify(mockHttpRequest).setReadTimeout(5982); + } + @FunctionalInterface private interface SendFunction { Response send(Connection connection, Request request) throws IOException; } + private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { + fakeRequest = + Request.builder() + .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) + .setUserAgent("fake user agent") + .setBody(new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type")) + .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) + .setHttpTimeout(httpTimeout) + .build(); + + Mockito.when( + mockHttpRequestFactory.buildRequest( + Mockito.any(String.class), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) + .thenReturn(mockHttpRequest); + + Mockito.when(mockHttpRequest.setHeaders(Mockito.any(HttpHeaders.class))) + .thenReturn(mockHttpRequest); + if (httpTimeout != null) { + Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + } + Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); + } + private void testSend(String httpMethod, SendFunction sendFunction) throws IOException { try (Connection connection = testConnection) { sendFunction.send(connection, fakeRequest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java new file mode 100644 index 0000000000..4e0e2611aa --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.GenericUrl; +import java.io.IOException; +import java.util.function.BiFunction; + +/** + * Mock {@link Connection} used for testing. Normally, you would use {@link + * org.mockito.Mockito#mock}; this class is intended to examine the {@link Request) object by + * calling its non-public package-protected methods. + */ +public class MockConnection extends Connection { + + private BiFunction responseSupplier; + private Integer httpTimeout; + + public MockConnection(BiFunction responseSupplier) { + super(new GenericUrl("ftp://non-exisiting.example.url.ever").toURL()); + this.responseSupplier = responseSupplier; + } + + @Override + public Response send(String httpMethod, Request request) throws IOException { + httpTimeout = request.getHttpTimeout(); + return responseSupplier.apply(httpMethod, request); + } + + public Integer getRequestedHttpTimeout() { + return httpTimeout; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java new file mode 100644 index 0000000000..d142022d23 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Request}. */ +public class RequestTest { + + @Test + public void testGetHttpTimeout() { + Request request = Request.builder().build(); + + Assert.assertNull(request.getHttpTimeout()); + } + + @Test + public void testSetHttpTimeout() { + Request request = Request.builder().setHttpTimeout(3000).build(); + + Assert.assertEquals(Integer.valueOf(3000), request.getHttpTimeout()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index e1c6f22c04..9a0b8747dd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,10 +20,12 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.MockConnection; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryEndpointCaller.RequestState; @@ -40,6 +42,7 @@ import org.apache.http.NoHttpResponseException; import org.apache.http.conn.HttpHostConnectException; import org.hamcrest.CoreMatchers; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -113,6 +116,11 @@ public void setUp() throws IOException { Mockito.when(mockHttpResponse.getHeaders()).thenReturn(new HttpHeaders()); } + @After + public void tearDown() { + System.clearProperty("jib.httpTimeout"); + } + @Test public void testCall_httpsPeerUnverified() throws IOException, RegistryException { verifyRetriesWithHttp(SSLPeerUnverifiedException.class); @@ -253,6 +261,72 @@ public void testCall_disallowInsecure() throws IOException, RegistryException { } } + @Test + public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { + MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); + Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); + + Assert.assertNull(System.getProperty("jib.httpTimeout")); + testRegistryEndpointCallerSecure.call(); + + // We fall back to the default timeout: + // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 + Assert.assertNull(mockConnection.getRequestedHttpTimeout()); + } + + @Test + public void testHttpTimeout_stringValue() throws IOException, RegistryException { + MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); + Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); + + System.setProperty("jib.httpTimeout", "random string"); + testRegistryEndpointCallerSecure.call(); + + Assert.assertNull(mockConnection.getRequestedHttpTimeout()); + } + + @Test + public void testHttpTimeout_negativeValue() throws IOException, RegistryException { + MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); + Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); + + System.setProperty("jib.httpTimeout", "-1"); + testRegistryEndpointCallerSecure.call(); + + // We let the negative value pass through: + // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 + Assert.assertEquals(Integer.valueOf(-1), mockConnection.getRequestedHttpTimeout()); + } + + @Test + public void testHttpTimeout_0accepted() throws IOException, RegistryException { + System.setProperty("jib.httpTimeout", "0"); + + MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); + Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + + Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); + testRegistryEndpointCallerSecure.call(); + + Assert.assertEquals(Integer.valueOf(0), mockConnection.getRequestedHttpTimeout()); + } + + @Test + public void testHttpTimeout() throws IOException, RegistryException { + System.setProperty("jib.httpTimeout", "7593"); + + MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); + Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + + Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); + testRegistryEndpointCallerSecure.call(); + + Assert.assertEquals(Integer.valueOf(7593), mockConnection.getRequestedHttpTimeout()); + } + /** Verifies a request is retried with HTTP protocol if {@code exceptionClass} is thrown. */ private void verifyRetriesWithHttp(Class exceptionClass) throws IOException, RegistryException { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index eca7d25e39..1fc7c0dcbd 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) - `container.useCurrentTimestamp` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) - Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index be80d3ebb0..9e2c20df46 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) - `` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) - Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) ### Changed From 05aa0da8dcc87bda1ca0e669bf50a69c507f2d5b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 16:02:40 -0400 Subject: [PATCH 0036/2020] Change "jib.dockerDir" in maven plugin and "targetDir" in gradle plugin to "jibTargetDir" for consistency (#663) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../google/cloud/tools/jib/gradle/DockerContextTask.java | 6 +++--- jib-maven-plugin/CHANGELOG.md | 2 ++ .../com/google/cloud/tools/jib/maven/DockerContextMojo.java | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1fc7c0dcbd..aabe65ed58 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Docker context export command-line option `--targetDir` to `--jibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) + ### Fixed - Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 5d80e7932b..97b14522b8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -85,11 +85,11 @@ public String getTargetDir() { } /** - * The output directory can be overriden with the {@code --targetDir} command line option. + * The output directory can be overriden with the {@code --jibTargetDir} command line option. * * @param targetDir the output directory. */ - @Option(option = "targetDir", description = "Directory to output the Docker context to") + @Option(option = "jibTargetDir", description = "Directory to output the Docker context to") public void setTargetDir(String targetDir) { this.targetDir = targetDir; } @@ -134,7 +134,7 @@ public void generateDockerContext() { } catch (IOException ex) { throw new GradleException( HelpfulSuggestionsProvider.get("Export Docker context failed") - .suggest("check if the command-line option `--jib.dockerDir` is set correctly"), + .suggest("check if the command-line option `--jibTargetDir` is set correctly"), ex); } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9e2c20df46..ac80a57d57 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Docker context export parameter `-Djib.dockerDir` to `-DjibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) + ### Fixed - Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 855923e4f6..5735839029 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -40,7 +40,7 @@ public class DockerContextMojo extends JibPluginConfiguration { @Nullable @Parameter( - property = "jib.dockerDir", + property = "jibTargetDir", defaultValue = "${project.build.directory}/jib-docker-context", required = true) private String targetDir; From 283b33a19fa91f9ca6cbb60de2b3fbc954bb5584 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 19 Jul 2018 16:21:51 -0400 Subject: [PATCH 0037/2020] Error on invalid jib.httpTimeout values at plugin front-ends (#667) --- .../jib/frontend/SystemPropertyValidator.java | 45 ++++++++++++++ .../frontend/SystemPropertyValidatorTest.java | 58 +++++++++++++++++++ .../tools/jib/gradle/BuildDockerTask.java | 2 + .../tools/jib/gradle/BuildImageTask.java | 2 + .../cloud/tools/jib/gradle/BuildTarTask.java | 2 + .../tools/jib/gradle/DockerContextTask.java | 2 + .../tools/jib/maven/BuildDockerMojo.java | 2 + .../cloud/tools/jib/maven/BuildImageMojo.java | 2 + .../cloud/tools/jib/maven/BuildTarMojo.java | 2 + .../tools/jib/maven/DockerContextMojo.java | 2 + 10 files changed, 119 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java new file mode 100644 index 0000000000..e83cb628cd --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import java.util.function.Function; + +/** Validator for system properties. */ +public class SystemPropertyValidator { + + /** + * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) + * values. + * + * @param exceptionFactory factory to create an exception with the given description + * @param the exception type to throw if invalid values + * @throws T if invalid values + */ + public static void checkHttpTimeoutProperty( + Function exceptionFactory) throws T { + String value = System.getProperty("jib.httpTimeout"); + try { + if (value != null && Integer.parseInt(value) < 0) { + throw exceptionFactory.apply("jib.httpTimeout cannot be negative: " + value); + } + } catch (NumberFormatException ex) { + throw exceptionFactory.apply("jib.httpTimeout must be an integer: " + value); + } + } + + private SystemPropertyValidator() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java new file mode 100644 index 0000000000..ef7feb0c30 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link SystemPropertyValidator}. */ +public class SystemPropertyValidatorTest { + + @After + public void tearDown() { + System.clearProperty("jib.httpTimeout"); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_ok() throws Exception { + Assert.assertNull(System.getProperty("jib.httpTimeout")); + SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_stringValue() { + System.setProperty("jib.httpTimeout", "random string"); + try { + SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); + Assert.fail("Should error with a non-integer timeout"); + } catch (Exception ex) { + Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); + } + } + + @Test + public void testCheckHttpTimeoutSystemProperty_negativeValue() { + System.setProperty("jib.httpTimeout", "-80"); + try { + SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); + Assert.fail("Should error with a negative timeout"); + } catch (Exception ex) { + Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); + } + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 7415aed902..0b6241d29a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -87,6 +88,7 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { Preconditions.checkNotNull(jibExtension); GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Boolean.getBoolean("sendCredentialsOverHttp")) { gradleBuildLogger.warn( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 497f2da9de..4e77100196 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -83,6 +84,7 @@ public void buildImage() throws InvalidImageReferenceException, IOException { Preconditions.checkNotNull(jibExtension); GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 35fab736eb..64086da31a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -115,6 +116,7 @@ public void buildTar() throws InvalidImageReferenceException, IOException { Preconditions.checkNotNull(jibExtension); GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Boolean.getBoolean("sendCredentialsOverHttp")) { gradleBuildLogger.warn( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 97b14522b8..2e10cb4521 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; @@ -100,6 +101,7 @@ public void generateDockerContext() { GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index e41d7dca77..f34f8c9528 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; @@ -58,6 +59,7 @@ public class BuildDockerMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException { MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); if (!new DockerClient().isDockerInstalled()) { throw new MojoExecutionException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 5c028bc191..62a840f291 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -61,6 +62,7 @@ public class BuildImageMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException, MojoFailureException { MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); // Validates 'format'. if (Arrays.stream(ImageFormat.values()).noneMatch(value -> value.name().equals(getFormat()))) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index ebe232ce01..4787261e18 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; @@ -60,6 +61,7 @@ public class BuildTarMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException { MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 5735839029..9911661fee 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -49,6 +50,7 @@ public class DockerContextMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException, MojoFailureException { MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); + SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); Preconditions.checkNotNull(targetDir); From c07f4f21d2d70b8aeb80a1e63f087fc350945d2f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 17:28:16 -0400 Subject: [PATCH 0038/2020] Maven release v0.9.7 (#669) * preparing release 0.9.7 * 0.9.8-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index ee93d11df0..8c34fb34ca 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.7-SNAPSHOT + 0.9.8-SNAPSHOT maven-plugin Jib From 59b2df9154864c8a98010dfef0d3e255e2fb7808 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 19 Jul 2018 19:34:09 -0400 Subject: [PATCH 0039/2020] Uses matched registry alias for credential helper. (#671) --- ...DockerCredentialHelperIntegrationTest.java | 6 +- .../RetrieveRegistryCredentialsStep.java | 4 +- .../registry/credentials/DockerConfig.java | 144 ++++++++++++++++++ .../DockerConfigCredentialRetriever.java | 31 ++-- .../credentials/DockerCredentialHelper.java | 11 ++ .../DockerCredentialHelperFactory.java | 10 +- .../json/DockerConfigTemplate.java | 80 ++-------- .../RetrieveRegistryCredentialsStepTest.java | 14 +- .../DockerConfigCredentialRetrieverTest.java | 17 +-- ...emplateTest.java => DockerConfigTest.java} | 78 +++++----- 10 files changed, 257 insertions(+), 138 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java rename jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/{json/DockerConfigTemplateTest.java => DockerConfigTest.java} (55%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index d2093a8269..265caa9a88 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -39,7 +39,7 @@ public void testRetrieveGCR() .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); DockerCredentialHelper dockerCredentialHelper = - new DockerCredentialHelperFactory("myregistry").withCredentialHelperSuffix("gcr"); + new DockerCredentialHelperFactory().newDockerCredentialHelper("myregistry", "gcr"); Authorization authorization = dockerCredentialHelper.retrieve(); @@ -52,7 +52,7 @@ public void testRetrieve_nonexistentCredentialHelper() throws IOException, NonexistentServerUrlDockerCredentialHelperException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory("").withCredentialHelperSuffix("fake-cloud-provider"); + new DockerCredentialHelperFactory().newDockerCredentialHelper("", "fake-cloud-provider"); fakeDockerCredentialHelper.retrieve(); @@ -69,7 +69,7 @@ public void testRetrieve_nonexistentServerUrl() throws IOException, NonexistentDockerCredentialHelperException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory("fake.server.url").withCredentialHelperSuffix("gcr"); + new DockerCredentialHelperFactory().newDockerCredentialHelper("fake.server.url", "gcr"); fakeDockerCredentialHelper.retrieve(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 4dcccec735..efb23ca520 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -109,7 +109,7 @@ private RetrieveRegistryCredentialsStep( registry, credentialHelperSuffix, knownRegistryCredentials, - new DockerCredentialHelperFactory(registry), + new DockerCredentialHelperFactory(), new DockerConfigCredentialRetriever(registry)); } @@ -195,7 +195,7 @@ Authorization retrieveFromCredentialHelper(String credentialHelperSuffix) try { Authorization authorization = dockerCredentialHelperFactory - .withCredentialHelperSuffix(credentialHelperSuffix) + .newDockerCredentialHelper(registry, credentialHelperSuffix) .retrieve(); logGotCredentialsFrom("docker-credential-" + credentialHelperSuffix); return authorization; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java new file mode 100644 index 0000000000..4f3a5dcfd7 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -0,0 +1,144 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; +import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import javax.annotation.Nullable; + +/** Handles getting useful information from a {@link DockerConfigTemplate}. */ +class DockerConfig { + + /** + * Returns the first entry matching the given key predicates (short-circuiting in the order of + * predicates). + */ + @Nullable + private static Map.Entry findFirstInMapByKey( + Map map, List> keyMatches) { + return keyMatches + .stream() + .map(keyMatch -> findFirstInMapByKey(map, keyMatch)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + /** Returns the first entry matching the given key predicate. */ + @Nullable + private static Map.Entry findFirstInMapByKey(Map map, Predicate keyMatch) { + return map.entrySet() + .stream() + .filter(entry -> keyMatch.test(entry.getKey())) + .findFirst() + .orElse(null); + } + + private final DockerConfigTemplate dockerConfigTemplate; + + DockerConfig(DockerConfigTemplate dockerConfigTemplate) { + this.dockerConfigTemplate = dockerConfigTemplate; + } + + /** + * Returns the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if + * none exists. The order of lookup preference: + * + *
    + *
  1. Exact registry name + *
  2. https:// + registry name + *
  3. registry name + / + arbitrary suffix + *
  4. https:// + registry name + / arbitrary suffix + *
+ * + * @param registry the registry to get the authorization for + * @return the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if + * none exists + */ + @Nullable + String getAuthFor(String registry) { + Map.Entry authEntry = + findFirstInMapByKey(dockerConfigTemplate.getAuths(), getRegistryMatchersFor(registry)); + return authEntry != null ? authEntry.getValue().getAuth() : null; + } + + @VisibleForTesting + @Nullable + DockerCredentialHelper getCredentialHelperFor(String registry) { + return getCredentialHelperFor(new DockerCredentialHelperFactory(), registry); + } + + /** + * Determines a {@link DockerCredentialHelper} to use for {@code registry}. + * + *

If there exists a matching registry entry (or its aliases) in {@code auths} for {@code + * registry}, the credential helper is {@code credStore}; otherwise, if there exists a matching + * registry entry (or its aliases) in {@code credHelpers}, the corresponding credential helper + * suffix is used. + * + *

See {@link #getRegistryMatchersFor} for the alias lookup order. + * + * @param registry the registry to get the credential helpers for + * @return the {@link DockerCredentialHelper} or {@code null} if none is found for the given + * registry + */ + @Nullable + DockerCredentialHelper getCredentialHelperFor( + DockerCredentialHelperFactory dockerCredentialHelperFactory, String registry) { + List> registryMatchers = getRegistryMatchersFor(registry); + Map.Entry firstMatchInAuths = + findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); + if (dockerConfigTemplate.getCredsStore() != null && firstMatchInAuths != null) { + return dockerCredentialHelperFactory.newDockerCredentialHelper( + firstMatchInAuths.getKey(), dockerConfigTemplate.getCredsStore()); + } + Map.Entry firstMatchInCredHelpers = + findFirstInMapByKey(dockerConfigTemplate.getCredHelpers(), registryMatchers); + if (firstMatchInCredHelpers == null) { + return null; + } + return dockerCredentialHelperFactory.newDockerCredentialHelper( + firstMatchInCredHelpers.getKey(), firstMatchInCredHelpers.getValue()); + } + + /** + * Registry alias matches in the following order: + * + *

    + *
  1. Exact registry name + *
  2. https:// + registry name + *
  3. registry name + / + arbitrary suffix + *
  4. https:// + registry name + / + arbitrary suffix + *
+ * + * @param registry the registry to get matchers for + * @return the list of predicates to match possible aliases + */ + private List> getRegistryMatchersFor(String registry) { + Predicate exactMatch = registry::equals; + Predicate withHttps = ("https://" + registry)::equals; + Predicate withSuffix = name -> name.startsWith(registry + "/"); + Predicate withHttpsAndSuffix = name -> name.startsWith("https://" + registry + "/"); + return Arrays.asList(exactMatch, withHttps, withSuffix, withHttpsAndSuffix); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 53be488636..940bf18c51 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -63,7 +63,7 @@ public DockerConfigCredentialRetriever(String registry) { DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; - this.dockerCredentialHelperFactory = new DockerCredentialHelperFactory(registry); + this.dockerCredentialHelperFactory = new DockerCredentialHelperFactory(); } @VisibleForTesting @@ -77,6 +77,8 @@ public DockerConfigCredentialRetriever(String registry) { } /** + * Retrieves credentials for a registry. Tries all possible known aliases. + * * @return {@link Authorization} found for {@code registry}, or {@code null} if not found * @throws IOException if failed to parse the config JSON */ @@ -87,8 +89,10 @@ public Authorization retrieve() throws IOException { return null; } - for (String registry : RegistryAliasGroup.getAliasesGroup(registry)) { - Authorization authorization = retrieve(dockerConfigTemplate, registry); + DockerConfig dockerConfig = new DockerConfig(dockerConfigTemplate); + + for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { + Authorization authorization = retrieve(dockerConfig, registryAlias); if (authorization != null) { return authorization; } @@ -96,21 +100,28 @@ public Authorization retrieve() throws IOException { return null; } + /** + * Retrieves credentials for a registry alias from a {@link DockerConfig}. + * + * @param dockerConfig the {@link DockerConfig} to retrieve from + * @param registryAlias the registry alias to use + * @return the retrieved credentials, or {@code null} if none are found + */ @Nullable - private Authorization retrieve(DockerConfigTemplate dockerConfigTemplate, String registry) { + private Authorization retrieve(DockerConfig dockerConfig, String registryAlias) { // First, tries to find defined auth. - String auth = dockerConfigTemplate.getAuthFor(registry); + String auth = dockerConfig.getAuthFor(registryAlias); if (auth != null) { return Authorizations.withBasicToken(auth); } // Then, tries to use a defined credHelpers credential helper. - String credentialHelperSuffix = dockerConfigTemplate.getCredentialHelperFor(registry); - if (credentialHelperSuffix != null) { + DockerCredentialHelper dockerCredentialHelper = + dockerConfig.getCredentialHelperFor(dockerCredentialHelperFactory, registryAlias); + if (dockerCredentialHelper != null) { try { - return dockerCredentialHelperFactory - .withCredentialHelperSuffix(credentialHelperSuffix) - .retrieve(); + // Tries with the given registry alias (NOT the original registry). + return dockerCredentialHelper.retrieve(); } catch (IOException | NonexistentServerUrlDockerCredentialHelperException diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 88ea117257..72117e72e8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStreamReader; @@ -131,4 +132,14 @@ public Authorization retrieve() throw ex; } } + + @VisibleForTesting + String getServerUrl() { + return serverUrl; + } + + @VisibleForTesting + String getCredentialHelperSuffix() { + return credentialHelperSuffix; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java index 7cef1f5bc1..6f901480d2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java @@ -19,17 +19,15 @@ /** Factory class for constructing {@link DockerCredentialHelper}. */ public class DockerCredentialHelperFactory { - private final String registry; - - public DockerCredentialHelperFactory(String registry) { - this.registry = registry; - } + public DockerCredentialHelperFactory() {} /** + * @param registry the {@code ServerURL} stored by the credential helper * @param credentialHelperSuffix the suffix of the docker-credential-[suffix] command to be run. * @return a {@link DockerCredentialHelper} retrieved from the command. */ - public DockerCredentialHelper withCredentialHelperSuffix(String credentialHelperSuffix) { + public DockerCredentialHelper newDockerCredentialHelper( + String registry, String credentialHelperSuffix) { return new DockerCredentialHelper(registry, credentialHelperSuffix); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index 5ca753ce94..e3d6d1ecca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -19,12 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.function.Predicate; import javax.annotation.Nullable; /** @@ -67,27 +63,14 @@ public class DockerConfigTemplate implements JsonTemplate { /** Template for an {@code auth} defined for a registry under {@code auths}. */ @JsonIgnoreProperties(ignoreUnknown = true) - private static class AuthTemplate implements JsonTemplate { + public static class AuthTemplate implements JsonTemplate { @Nullable private String auth; - } - - /** - * Returns the first value matching the given key predicates (short-circuiting in the order of - * predicates). - */ - private static T findFirstInMapByKey(Map map, List> keyMatches) { - return keyMatches - .stream() - .map(keyMatch -> findFirstInMapByKey(map, keyMatch)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - /** Returns the first value matching the given key predicate. */ - private static T findFirstInMapByKey(Map map, Predicate keyMatch) { - return map.keySet().stream().filter(keyMatch).map(map::get).findFirst().orElse(null); + @Nullable + public String getAuth() { + return auth; + } } /** Maps from registry to its {@link AuthTemplate}. */ @@ -98,58 +81,17 @@ private static T findFirstInMapByKey(Map map, Predicate keyMatch /** Maps from registry to credential helper name. */ private final Map credHelpers = new HashMap<>(); - /** - * Returns the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if - * none exists. The order of lookup preference: - * - *
    - *
  1. Exact registry name - *
  2. https:// + registry name - *
  3. registry name + / + arbitrary suffix - *
  4. https:// + registry name + / arbitrary suffix - *
- * - * @param registry the registry to get the authorization for - * @return the base64-encoded {@code Basic} authorization for {@code registry}, or {@code null} if - * none exists - */ - @Nullable - public String getAuthFor(String registry) { - AuthTemplate authTemplate = findFirstInMapByKey(auths, getRegistryMatchersFor(registry)); - return authTemplate != null ? authTemplate.auth : null; + public Map getAuths() { + return auths; } - /** - * Returns {@code credsStore} or {@code credHelpers} for the given {@code registry}. If there - * exists a matching registry entry in {@code auths}, returns {@code credStore}; otherwise, a - * matching entry in {@code credHelpers} is returned based on the following lookup order: - * - *
    - *
  1. Exact registry name - *
  2. https:// + registry name - *
  3. registry name + / + arbitrary suffix - *
  4. https:// + registry name + / + arbitrary suffix - *
- * - * @param registry the registry to get the credential helpers for - * @return {@code credsStore} if {@code registry} is present in {@code auths}; otherwise, searches - * {@code credHelpers}; otherwise, {@code null} if not found - */ @Nullable - public String getCredentialHelperFor(String registry) { - List> registryMatchers = getRegistryMatchersFor(registry); - if (credsStore != null && findFirstInMapByKey(auths, registryMatchers) != null) { - return credsStore; - } - return findFirstInMapByKey(credHelpers, registryMatchers); + public String getCredsStore() { + return credsStore; } - private List> getRegistryMatchersFor(String registry) { - Predicate exactMatch = registry::equals; - Predicate withHttps = ("https://" + registry)::equals; - Predicate withSuffix = name -> name.startsWith(registry + "/"); - Predicate WithHttpsAndSuffix = name -> name.startsWith("https://" + registry + "/"); - return Arrays.asList(exactMatch, withHttps, withSuffix, WithHttpsAndSuffix); + public Map getCredHelpers() { + return credHelpers; } @VisibleForTesting diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index ffda43775f..bd2801ebdb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -85,8 +85,8 @@ public void setUpMocks() public void testCall_useCredentialHelper() throws IOException, NonexistentDockerCredentialHelperException { Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix( - "someOtherCredentialHelper")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "someRegistry", "someOtherCredentialHelper")) .thenReturn(mockDockerCredentialHelper); Assert.assertEquals( @@ -117,7 +117,8 @@ public void testCall_useDockerConfig() throws IOException, NonexistentDockerCredentialHelperException { // Credential helper does not have credentials. Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("someCredentialHelper")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "someRegistry", "someCredentialHelper")) .thenReturn(mockNonexistentServerUrlDockerCredentialHelper); Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(mockAuthorization); @@ -134,9 +135,12 @@ public void testCall_useDockerConfig() @Test public void testCall_inferCommonCredentialHelpers() throws IOException, NonexistentDockerCredentialHelperException { - Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix("gcr")) + Mockito.when( + mockDockerCredentialHelperFactory.newDockerCredentialHelper("something.gcr.io", "gcr")) .thenReturn(mockDockerCredentialHelper); - Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix("ecr-login")) + Mockito.when( + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "something.amazonaws.com", "ecr-login")) .thenReturn(mockNonexistentDockerCredentialHelper); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 59015be929..7f3ed392a5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -36,7 +36,6 @@ public class DockerConfigCredentialRetrieverTest { @Mock private Authorization mockAuthorization; @Mock private DockerCredentialHelper mockDockerCredentialHelper; - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; private Path dockerConfigFile; @@ -71,7 +70,8 @@ public void testRetrieve_hasAuth() throws IOException { @Test public void testRetrieve_useCredsStore() throws IOException { Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("some credential store")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "just registry", "some credential store")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = @@ -85,7 +85,8 @@ public void testRetrieve_useCredsStore() throws IOException { @Test public void testRetrieve_useCredsStore_withProtocol() throws IOException { Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix("some credential store")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "https://with.protocol", "some credential store")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = @@ -99,8 +100,8 @@ public void testRetrieve_useCredsStore_withProtocol() throws IOException { @Test public void testRetrieve_useCredHelper() throws IOException { Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix( - "another credential helper")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "another registry", "another credential helper")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = @@ -121,11 +122,9 @@ public void testRetrieve_none() throws IOException { @Test public void testRetrieve_credentialFromAlias() throws IOException { - Mockito.when(mockDockerCredentialHelperFactory.withCredentialHelperSuffix(Mockito.anyString())) - .thenReturn(Mockito.mock(DockerCredentialHelper.class)); Mockito.when( - mockDockerCredentialHelperFactory.withCredentialHelperSuffix( - "index.docker.io credential helper")) + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "index.docker.io", "index.docker.io credential helper")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java similarity index 55% rename from jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index e627751bc2..ce6f1e0052 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -14,9 +14,10 @@ * the License. */ -package com.google.cloud.tools.jib.registry.credentials.json; +package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -25,8 +26,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link DockerConfigTemplate}. */ -public class DockerConfigTemplateTest { +/** Tests for {@link DockerConfig}. */ +public class DockerConfigTest { @Test public void test_fromJson() throws URISyntaxException, IOException { @@ -34,36 +35,39 @@ public void test_fromJson() throws URISyntaxException, IOException { Path jsonFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); // Deserializes into a docker config JSON object. - DockerConfigTemplate dockerConfigTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(jsonFile, DockerConfigTemplate.class)); - Assert.assertEquals("some auth", dockerConfigTemplate.getAuthFor("some registry")); - Assert.assertEquals("some other auth", dockerConfigTemplate.getAuthFor("some other registry")); - Assert.assertEquals("token", dockerConfigTemplate.getAuthFor("registry")); - Assert.assertEquals("token", dockerConfigTemplate.getAuthFor("https://registry")); - Assert.assertEquals(null, dockerConfigTemplate.getAuthFor("just registry")); + Assert.assertEquals("some auth", dockerConfig.getAuthFor("some registry")); + Assert.assertEquals("some other auth", dockerConfig.getAuthFor("some other registry")); + Assert.assertEquals("token", dockerConfig.getAuthFor("registry")); + Assert.assertEquals("token", dockerConfig.getAuthFor("https://registry")); + Assert.assertNull(dockerConfig.getAuthFor("just registry")); Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("some registry")); + "some credential store", + dockerConfig.getCredentialHelperFor("some registry").getCredentialHelperSuffix()); Assert.assertEquals( "some credential store", - dockerConfigTemplate.getCredentialHelperFor("some other registry")); + dockerConfig.getCredentialHelperFor("some other registry").getCredentialHelperSuffix()); Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("just registry")); + "some credential store", + dockerConfig.getCredentialHelperFor("just registry").getCredentialHelperSuffix()); Assert.assertEquals( - "some credential store", dockerConfigTemplate.getCredentialHelperFor("with.protocol")); + "some credential store", + dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelperSuffix()); Assert.assertEquals( "another credential helper", - dockerConfigTemplate.getCredentialHelperFor("another registry")); - Assert.assertEquals(null, dockerConfigTemplate.getCredentialHelperFor("unknonwn registry")); + dockerConfig.getCredentialHelperFor("another registry").getCredentialHelperSuffix()); + Assert.assertNull(dockerConfig.getCredentialHelperFor("unknonwn registry")); } @Test public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals("my-registry: exact match", dockerConfig.getAuthFor("my-registry")); Assert.assertEquals("cool-registry: with https", dockerConfig.getAuthFor("cool-registry")); @@ -78,8 +82,8 @@ public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, I public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertNull(dockerConfig.getAuthFor("example")); } @@ -88,33 +92,36 @@ public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IO public void testGetCredentialHelperFor() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", dockerConfig.getCredentialHelperFor("just registry")); + "some credential store", + dockerConfig.getCredentialHelperFor("just registry").getCredentialHelperSuffix()); } @Test public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", dockerConfig.getCredentialHelperFor("with.protocol")); + "some credential store", + dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelperSuffix()); } @Test public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", dockerConfig.getCredentialHelperFor("with.suffix")); + "some credential store", + dockerConfig.getCredentialHelperFor("with.suffix").getCredentialHelperSuffix()); } @Test @@ -122,11 +129,14 @@ public void testGetCredentialHelperFor_withProtocolAndSuffix() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", dockerConfig.getCredentialHelperFor("with.protocol.and.suffix")); + "some credential store", + dockerConfig + .getCredentialHelperFor("with.protocol.and.suffix") + .getCredentialHelperSuffix()); } @Test @@ -134,8 +144,8 @@ public void testGetCredentialHelperFor_correctSuffixMatching() throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - DockerConfigTemplate dockerConfig = - JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class); + DockerConfig dockerConfig = + new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertNull(dockerConfig.getCredentialHelperFor("example")); Assert.assertNull(dockerConfig.getCredentialHelperFor("another.example")); From 336243858f037a346c383077ce51671cad3f5674 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 19 Jul 2018 19:54:12 -0400 Subject: [PATCH 0040/2020] Gradle release v0.9.7 (#672) * [Gradle Release Plugin] - pre tag commit: 'v0.9.7-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.9.8-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 4afad4b820..45d430072d 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.7-SNAPSHOT +version = 0.9.8-SNAPSHOT From 31c9b3da61d57f879cb4d2d617ca7e5762fa61b5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 20:11:38 -0400 Subject: [PATCH 0041/2020] Maven release v0.9.7 (#673) * preparing release 0.9.7 * 0.9.8-SNAPSHOT From 851e5ba53c9430883891f1ee568137f7189ab721 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 19 Jul 2018 20:35:37 -0400 Subject: [PATCH 0042/2020] Update documentation for 0.9.7 (#664) --- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 29 +++++++++++++++++++++++++---- jib-maven-plugin/CHANGELOG.md | 9 +++++++++ jib-maven-plugin/README.md | 29 +++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index aabe65ed58..7961d04df2 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.7 + +### Added + - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) - `container.useCurrentTimestamp` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 7376189c1e..a535f40fc5 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -109,6 +109,20 @@ eval $(minikube docker-env) gradle jibDockerBuild ``` +#### Build an image tarball + +You can build and save your image to disk as a tarball with: + +```shell +gradle jibBuildTar +``` + +This builds and saves your image to `build/jib-image.tar`, which you can load into docker with: + +```shell +docker load --input build/jib-image.tar +``` + ### Run `jib` with each build You can also have `jib` run with each build by attaching it to the `build` task: @@ -127,10 +141,10 @@ Jib can also export a Docker context so that you can build with Docker, if neede gradle jibExportDockerContext ``` -The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `---targetDir` parameter: +The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `--jibTargetDir` parameter: ```shell -gradle jibExportDockerContext --targetDir=my/docker/context/ +gradle jibExportDockerContext --jibTargetDir=my/docker/context/ ``` You can then build your image with Docker: @@ -148,8 +162,8 @@ Field | Type | Default | Description `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. -`useProjectOnlyCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Maven projects. -`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS. Leaving this parameter set to false is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. +`allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS or whose certificates cannot be verified. Leaving this parameter set to `false` is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network.
`from` is a closure with the following properties: @@ -183,6 +197,13 @@ Property | Type | Default | Description `args` | `List` | *None* | Default main method arguments to run your application with. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). +`useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. + +You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): + +```shell +gradle jib -Djib.httpTimeout=3000 +``` *\* Uses the main class defined in the `jar` task or tries to find a valid main class.* diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ac80a57d57..7338eaf4b9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.7 + +### Added + - Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) - `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) - `` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) @@ -19,6 +27,7 @@ All notable changes to this project will be documented in this file. - Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) - For Docker Hub, also tries registry aliases when getting a credential from the Docker config ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) +- Decrypting credentials from Maven settings ([#592](https://github.com/GoogleContainerTools/jib/issues/592)) ## 0.9.6 diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 9367722e88..185a03651f 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -134,6 +134,20 @@ eval $(minikube docker-env) mvn compile jib:dockerBuild ``` +#### Build an image tarball + +You can build and save your image to disk as a tarball with: + +```shell +mvn compile jib:buildTar +``` + +This builds and saves your image to `target/jib-image.tar`, which you can load into docker with: + +```shell +docker load --input target/jib-image.tar +``` + ### Bind to a lifecycle You can also bind `jib:build` to a Maven lifecycle, such as `package`, by adding the following execution to your `jib-maven-plugin` definition: @@ -168,10 +182,10 @@ Jib can also export a Docker context so that you can build with Docker, if neede mvn compile jib:exportDockerContext ``` -The Docker context will be created at `target/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `jib.dockerDir` parameter: +The Docker context will be created at `target/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `jibTargetDir` parameter: ```shell -mvn compile jib:exportDockerContext -Djib.dockerDir=my/docker/context/ +mvn compile jib:exportDockerContext -DjibTargetDir=my/docker/context/ ``` You can then build your image with Docker: @@ -190,7 +204,7 @@ Field | Type | Default | Description `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. `useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Gradle projects. -`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS. Leaving this parameter set to false is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS or whose certificates cannot be verified. Leaving this parameter set to `false` is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. `from` is an object with the following properties: @@ -213,8 +227,15 @@ Property | Type | Default | Description `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `mainClass` | string | *Inferred\** | The main class to launch the application from. `args` | list | *None* | Default main method arguments to run your application with. -`ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). +`useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. + +You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): + +```shell +mvn compile jib:build -Djib.httpTimeout=3000 +``` *\* Uses `mainClass` from `maven-jar-plugin` or tries to find a valid main class.* From 518f046206198eeb8fcc8ffb379169ed50cedbbd Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 20 Jul 2018 11:45:06 -0400 Subject: [PATCH 0043/2020] Update FAQ with new features, how to run image, and how to parameterize (#679) --- docs/faq.md | 110 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 14 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index e42a7c367b..8c8191da02 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,9 +5,10 @@ If a question you have is not answered below, please [submit an issue](/../../issues/new). [But, I'm not a Java developer.](#but-im-not-a-java-developer)\ +[How do I run the image I built?](#how-do-i-run-the-image-i-built)\ +[How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint)\ -[But I just want to set some JVM flags when running the image?](#but-i-just-want-to-set-some-jvm-flags-when-running-the-image)\ [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ @@ -22,6 +23,78 @@ If a question you have is not answered below, please [submit an issue](/../../is See [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). The tool can build images for languages such as Python, NodeJS, Java, Scala, Groovy, C, Go, Rust, and D. +### How do I run the image I built? + +If you built your image directly to the Docker daemon using `jib:dockerBuild` (Maven) or `jibDockerBuild` (Gradle), you simply need to use `docker run `. + +If you built your image to a registry using `jib:build` (Maven) or `jib` (Gradle), you will need to pull the image using `docker pull ` before using `docker run`. + +To run your image on Kubernetes, you can use kubectl: + +```shell +kubectl run jib-deployment --image= +``` + +For more information, see [steps 4-6 of the Kubernetes Engine deployment tutorial](https://cloud.google.com/kubernetes-engine/docs/tutorials/hello-app#step_4_create_a_container_cluster). + +### How do I set parameters for my image at runtime? + +#### JVM Flags + +For the default `distroless/java` base image, you can use the `JAVA_TOOL_OPTIONS` environment variable (note that other JRE images may require using other environment variables): + +Using Docker: `docker run -e "JAVA_TOOL_OPTIONS=" ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + env: + - name: JAVA_TOOL_OPTIONS + value: +``` + +#### Other Environment Variables + +Using Docker: `docker run -e "NAME=VALUE" ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + env: + - name: NAME + value: VALUE +``` + +#### Arguments to Main + +Using Docker: `docker run ` + +Using Kubernetes: +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: + image: + args: + - + - + - +``` + +For more information, see the [`JAVA_TOOL_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html), the [`docker run -e` reference](https://docs.docker.com/engine/reference/run/#env-environment-variables), and [defining environment variables for a container in Kubernetes](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/). + ### What image format does Jib use? Jib currently builds into the [Docker V2.2](https://docs.docker.com/registry/spec/manifest-v2-2/) image format or [OCI image format](https://github.com/opencontainers/image-spec). @@ -44,14 +117,6 @@ See [`docker run --entrypoint` reference](https://docs.docker.com/engine/referen See [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. -### But I just want to set some JVM flags when running the image? - -When running the image, you can pass in additional JVM flags via the [`JAVA_TOOL_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html). - -See [`docker run -e` reference](https://docs.docker.com/engine/reference/run/#env-environment-variables) for running the image with Docker and setting environment variables. - -See [Define Environment Variables for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and setting environment variables. - ### Where is the application in the container filesystem? Jib packages your Java application into the following paths on the image: @@ -98,11 +163,12 @@ We currently support adding a custom directory with an **incubating** feature. T ### Can I build to a local Docker daemon? -See [`jib:dockerBuild` for Maven](../jib-maven-plugin#build-to-docker-daemon) and [`jibDockerBuild` for Gradle](../jib-gradle-plugin#build-to-docker-daemon). +There are several ways of doing this: -You can also [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. - -You can also [run a local Docker registry](https://docs.docker.com/registry/deploying/) and point Jib to push to the local registry. +- Use [`jib:dockerBuild` for Maven](../jib-maven-plugin#build-to-docker-daemon) or [`jibDockerBuild` for Gradle](../jib-gradle-plugin#build-to-docker-daemon) to build directly to your local Docker daemon. +- Use [`jib:buildTar` for Maven](../jib-maven-plugin#build-an-image-tarball) or [`jibBuildTar` for Gradle](../jib-gradle-plugin#build-an-image-tarball) to build the image to a tarball, then use `docker load --input` to load the image into Docker (the tarball built with these commands will be located in `target/jib-image.tar` for Maven and `build/jib-image.tar` for Gradle by default). +- [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. +- Alternatively, instead of using a Docker daemon, you can run a local container registry, such as [Docker registry](https://docs.docker.com/registry/deploying/) or other repository managers, and point Jib to push to the local registry. ### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). @@ -140,7 +206,23 @@ See more at [Using Google Container Registry (GCR) with Minikube](https://ryanes ### Why is my image created 48 years ago? -For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use a real creation time, set [work in progress](https://github.com/GoogleContainerTools/jib/issues/413). +For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. + +#### Maven + +```xml + + + true + + +``` + +#### Gradle + +```groovy +jib.container.useCurrentTimestamp = true +``` ### I would like to run my application with a javaagent. From fe8cf8b3cab5197b5c0c5f575de2e981f44accd4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 20 Jul 2018 13:46:33 -0400 Subject: [PATCH 0044/2020] Fix regex bug (#680) * Fix regex bug * [-]+ --> -+ --- .../java/com/google/cloud/tools/jib/image/ImageReference.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index 17ffc2fa65..cef5a181ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -54,8 +54,7 @@ public class ImageReference { * *

A separator is either an underscore, a dot, two underscores, or any number of dashes. */ - private static final String REPOSITORY_COMPONENT_REGEX = - "[a-z\\d]+(?:(?:[_.]|__|[-]*)[a-z\\d]+)*"; + private static final String REPOSITORY_COMPONENT_REGEX = "[a-z\\d]+(?:(?:[_.]|__|-+)[a-z\\d]+)*"; /** Matches all repetitions of {@code REPOSITORY_COMPONENT_REGEX} separated by a backslash. */ private static final String REPOSITORY_REGEX = From dea7a2ae244629a5f97ee1fec7be5df49bf07156 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 20 Jul 2018 21:16:42 -0400 Subject: [PATCH 0045/2020] Creates PULL_REQUEST_TEMPLATE.md (#688) --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..15be00ac90 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ + From a2a16d29098adf8f65f52917b5cd122831d9beca Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 23 Jul 2018 10:08:06 -0400 Subject: [PATCH 0046/2020] Prevent sending multiple requests in a connection (#687) --- .../cloud/tools/jib/http/Connection.java | 6 +- .../registry/credentials/DockerConfig.java | 2 +- .../cloud/tools/jib/http/ConnectionTest.java | 3 +- .../cloud/tools/jib/http/TestWebServer.java | 65 +++++++++++++++++++ .../jib/http/WithServerConnectionTest.java | 55 ++++++++++++++++ 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 7c0702b4e3..4382f3a68e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -23,6 +23,7 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.apache.ApacheHttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.common.base.Preconditions; import java.io.Closeable; import java.io.IOException; import java.net.URL; @@ -30,7 +31,8 @@ import org.apache.http.NoHttpResponseException; /** - * Sends an HTTP {@link Request} and stores the {@link Response}. + * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than + * one request. * *

Example usage: * @@ -118,6 +120,8 @@ public Response put(Request request) throws IOException { * @throws IOException if building the HTTP request fails. */ public Response send(String httpMethod, Request request) throws IOException { + Preconditions.checkState(httpResponse == null, "Connection can send only one request"); + HttpRequest httpRequest = requestFactory .buildRequest(httpMethod, url, request.getHttpContent()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index 4f3a5dcfd7..c45a4afa1b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -106,7 +106,7 @@ DockerCredentialHelper getCredentialHelperFor(String registry) { DockerCredentialHelper getCredentialHelperFor( DockerCredentialHelperFactory dockerCredentialHelperFactory, String registry) { List> registryMatchers = getRegistryMatchersFor(registry); - Map.Entry firstMatchInAuths = + Map.Entry firstMatchInAuths = findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); if (dockerConfigTemplate.getCredsStore() != null && firstMatchInAuths != null) { return dockerCredentialHelperFactory.newDockerCredentialHelper( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index a1a2152e5a..21350533a7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -42,7 +42,6 @@ public class ConnectionTest { @Mock private HttpRequestFactory mockHttpRequestFactory; @Mock private HttpRequest mockHttpRequest; - @Mock private HttpResponse mockHttpResponse; private final ArgumentCaptor httpHeadersArgumentCaptor = ArgumentCaptor.forClass(HttpHeaders.class); @@ -51,6 +50,7 @@ public class ConnectionTest { private final GenericUrl fakeUrl = new GenericUrl("http://crepecake/fake/url"); private Request fakeRequest; + private HttpResponse mockHttpResponse; @InjectMocks private final Connection testConnection = new Connection(fakeUrl.toURL()); @@ -121,6 +121,7 @@ private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); } + mockHttpResponse = Mockito.mock(HttpResponse.class); Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java new file mode 100644 index 0000000000..b913f55afd --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; + +/** Simple local web server for testing. */ +class TestWebServer implements Closeable { + + private final ServerSocket serverSocket; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final Semaphore threadStarted = new Semaphore(0); + + TestWebServer() throws IOException, InterruptedException { + serverSocket = new ServerSocket(0); + executorService.submit(this::serve200); + threadStarted.acquire(); + } + + String getEndpoint() { + String host = serverSocket.getInetAddress().getHostAddress(); + return "http://" + host + ":" + serverSocket.getLocalPort(); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + executorService.shutdown(); + } + + private Void serve200() throws IOException { + threadStarted.release(); + try (Socket socket = serverSocket.accept()) { + String response = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); + socket.getOutputStream().flush(); + + InputStream in = socket.getInputStream(); + for (int ch = in.read(); ch != -1; ch = in.read()) ; + } + return null; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java new file mode 100644 index 0000000000..8697a7b76a --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URL; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Connection} using an actual local server. */ +public class WithServerConnectionTest { + + @Test + public void testGet() throws IOException, InterruptedException { + try (TestWebServer server = new TestWebServer(); + Connection connection = new Connection(new URL(server.getEndpoint()))) { + Response response = connection.send("GET", new Request.Builder().build()); + + Assert.assertEquals(200, response.getStatusCode()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.getBody().writeTo(out); + Assert.assertEquals("Hello World!", out.toString("UTF-8")); + } + } + + @Test + public void testErrorOnSecondSend() throws IOException, InterruptedException { + try (TestWebServer server = new TestWebServer(); + Connection connection = new Connection(new URL(server.getEndpoint()))) { + connection.send("GET", new Request.Builder().build()); + try { + connection.send("GET", new Request.Builder().build()); + Assert.fail("Should fail on the second send"); + } catch (IllegalStateException ex) { + Assert.assertEquals("Connection can send only one request", ex.getMessage()); + } + } + } +} From c2c481fbeb8ae99f96e55b96419a7d6ba9b04392 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 23 Jul 2018 10:27:25 -0400 Subject: [PATCH 0047/2020] CHANGELOG for slow regex parsing fix (#690) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7961d04df2..096f846de8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) + ## 0.9.7 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7338eaf4b9..bfe5e9db41 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) + ## 0.9.7 ### Added From d36ec2a81cc1b134f9b603dc9d969da57553dde1 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 23 Jul 2018 12:11:12 -0400 Subject: [PATCH 0048/2020] Improve logging for pulling base image (#685) --- .../cloud/tools/jib/builder/steps/PullBaseImageStep.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 14a3811bd9..598c53d620 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -109,6 +109,13 @@ public BaseImageWithAuthorization call() return new BaseImageWithAuthorization(pullBaseImage(null), null); } catch (RegistryUnauthorizedException ex) { + buildConfiguration + .getBuildLogger() + .lifecycle( + "The base image requires auth. Trying again for " + + buildConfiguration.getBaseImageReference() + + "..."); + // If failed, then, retrieve base registry credentials and try with retrieved credentials. // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to // registry.credentials.RegistryCredentialsRetriever to avoid this direct executor hack. From 42606c9e72fcaebc4c05cb8334bad9597e219967 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 23 Jul 2018 14:23:36 -0400 Subject: [PATCH 0049/2020] Rename fields and methods / update Javadocs (#695) --- .../builder/BuildStepsIntegrationTest.java | 4 +-- .../registry/BlobCheckerIntegrationTest.java | 8 +++-- .../registry/BlobPullerIntegrationTest.java | 6 ++-- .../registry/BlobPusherIntegrationTest.java | 2 +- .../ManifestPullerIntegrationTest.java | 6 ++-- .../ManifestPusherIntegrationTest.java | 6 ++-- .../tools/jib/builder/BuildConfiguration.java | 23 ++++++++------- .../builder/steps/AuthenticatePushStep.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 2 +- .../jib/builder/steps/PullBaseImageStep.java | 4 +-- .../tools/jib/builder/steps/PushBlobStep.java | 2 +- .../jib/builder/steps/PushImageStep.java | 2 +- .../jib/registry/RegistryAuthenticator.java | 8 ++--- .../tools/jib/registry/RegistryClient.java | 29 ++++++++++--------- .../jib/registry/RegistryEndpointCaller.java | 17 +++++------ .../jib/builder/BuildConfigurationTest.java | 6 ++-- .../jib/registry/RegistryClientTest.java | 9 ++++-- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 2 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- 23 files changed, 81 insertions(+), 67 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 0320e4d851..f85160a109 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -76,7 +76,7 @@ public void testSteps_forBuildToDockerRegistry() .setJavaArguments(Collections.singletonList("An argument.")) .setExposedPorts( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .build()); long lastTime = System.nanoTime(); @@ -112,7 +112,7 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) .setMainClass("HelloWorld") .setJavaArguments(Collections.singletonList("An argument.")) - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .build()) .run(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 9f810a4342..639911b22d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -32,7 +32,9 @@ public class BlobCheckerIntegrationTest { @Test public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory("localhost:5000", "busybox") + .setAllowInsecureRegistries(true) + .newRegistryClient(); V22ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V22ManifestTemplate.class); DescriptorDigest blobDigest = manifestTemplate.getLayers().get(0).getDigest(); @@ -43,7 +45,9 @@ public void testCheck_exists() throws IOException, RegistryException { @Test public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory("localhost:5000", "busybox") + .setAllowInsecureRegistries(true) + .newRegistryClient(); DescriptorDigest fakeBlobDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 370244b7e5..ab8b9b139d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -42,7 +42,9 @@ public class BlobPullerIntegrationTest { public void testPull() throws IOException, RegistryException { // Pulls the busybox image. RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory("localhost:5000", "busybox") + .setAllowInsecureRegistries(true) + .newRegistryClient(); V21ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V21ManifestTemplate.class); @@ -65,7 +67,7 @@ public void testPull_unknownBlob() throws RegistryException, IOException, Digest try { RegistryClient registryClient = RegistryClient.factory("localhost:5000", "busybox") - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class)); Assert.fail("Trying to pull nonexistent blob should have errored"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 2f571eb68b..c1feab67d2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -40,7 +40,7 @@ public void testPush() throws DigestException, IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory("localhost:5000", "testimage") - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob)); } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 40a1e7dd15..3dca13bf2f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -33,7 +33,9 @@ public class ManifestPullerIntegrationTest { @Test public void testPull_v21() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory("localhost:5000", "busybox") + .setAllowInsecureRegistries(true) + .newRegistryClient(); V21ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V21ManifestTemplate.class); @@ -57,7 +59,7 @@ public void testPull_unknownManifest() throws RegistryException, IOException { try { RegistryClient registryClient = RegistryClient.factory("localhost:5000", "busybox") - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); Assert.fail("Trying to pull nonexistent image should have errored"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 2834e7cd0a..8a922f4c1a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -41,7 +41,9 @@ public void testPush_missingBlobs() throws IOException, RegistryException { ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory("localhost:5000", "busybox").setAllowHttp(true).newRegistryClient(); + RegistryClient.factory("localhost:5000", "busybox") + .setAllowInsecureRegistries(true) + .newRegistryClient(); try { registryClient.pushManifest((V22ManifestTemplate) manifestTemplate, "latest"); Assert.fail("Pushing manifest without its BLOBs should fail"); @@ -74,7 +76,7 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pushes the BLOBs. RegistryClient registryClient = RegistryClient.factory("localhost:5000", "testimage") - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob)); Assert.assertFalse( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java index de4181d7ba..890b50db2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java @@ -57,7 +57,7 @@ public static class Builder { private Class targetFormat = V22ManifestTemplate.class; @Nullable private CacheConfiguration applicationLayersCacheConfiguration; @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; - private boolean allowHttp = false; + private boolean allowInsecureRegistries = false; @Nullable private LayerConfiguration extraFilesLayerConfiguration; private BuildLogger buildLogger; @@ -169,11 +169,11 @@ public Builder setBaseImageLayersCacheConfiguration( /** * Sets whether or not to allow communication over HTTP (as opposed to HTTPS). * - * @param allowHttp if {@code true}, insecure connections will be allowed + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed * @return this */ - public Builder setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; + public Builder setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; return this; } @@ -242,7 +242,7 @@ public BuildConfiguration build() { targetFormat, applicationLayersCacheConfiguration, baseImageLayersCacheConfiguration, - allowHttp, + allowInsecureRegistries, extraFilesLayerConfiguration); case 1: @@ -302,7 +302,7 @@ public static Builder builder(BuildLogger buildLogger) { private final Class targetFormat; @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; - private final boolean allowHttp; + private final boolean allowInsecureRegistries; @Nullable private final LayerConfiguration extraFilesLayerConfiguration; /** Instantiate with {@link Builder#build}. */ @@ -323,7 +323,7 @@ private BuildConfiguration( Class targetFormat, @Nullable CacheConfiguration applicationLayersCacheConfiguration, @Nullable CacheConfiguration baseImageLayersCacheConfiguration, - boolean allowHttp, + boolean allowInsecureRegistries, @Nullable LayerConfiguration extraFilesLayerConfiguration) { this.buildLogger = buildLogger; this.creationTime = creationTime; @@ -341,7 +341,7 @@ private BuildConfiguration( this.targetFormat = targetFormat; this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; - this.allowHttp = allowHttp; + this.allowInsecureRegistries = allowInsecureRegistries; this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; } @@ -450,12 +450,13 @@ public CacheConfiguration getBaseImageLayersCacheConfiguration() { } /** - * Gets whether or not to allow communication over HTTP (as opposed to HTTPS). + * Gets whether or not to allow insecure registries (ignoring certificate validation failure or + * communicating over HTTP if all else fail). * * @return {@code true} if insecure connections will be allowed; {@code false} otherwise */ - public boolean getAllowHttp() { - return allowHttp; + public boolean getAllowInsecureRegistries() { + return allowInsecureRegistries; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 56d8cb8ec1..cb701726d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -81,7 +81,7 @@ public Authorization call() RegistryAuthenticator.initializer( buildConfiguration.getTargetImageRegistry(), buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { return registryCredentials; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 71df43c8f2..a9904b5d8a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -73,7 +73,7 @@ public CachedLayer call() throws IOException, RegistryException { RegistryClient.factory( buildConfiguration.getBaseImageRegistry(), buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(pullAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 598c53d620..7f03a0f7ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -137,7 +137,7 @@ public BaseImageWithAuthorization call() RegistryAuthenticator.initializer( buildConfiguration.getBaseImageRegistry(), buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { buildConfiguration @@ -176,7 +176,7 @@ private Image pullBaseImage(@Nullable Authorization registryCredentials) RegistryClient.factory( buildConfiguration.getBaseImageRegistry(), buildConfiguration.getBaseImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(registryCredentials) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 928d997467..cad4ee2c60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -72,7 +72,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc RegistryClient.factory( buildConfiguration.getTargetImageRegistry(), buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); registryClient.setTimer(timer); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 6e15f6c49a..82c65a42ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -120,7 +120,7 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx RegistryClient.factory( buildConfiguration.getTargetImageRegistry(), buildConfiguration.getTargetImageRepository()) - .setAllowHttp(buildConfiguration.getAllowHttp()) + .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index eb1c5072c2..eaba7330ea 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -46,7 +46,7 @@ public static class Initializer { private final String serverUrl; private final String repository; - private boolean allowHttp = false; + private boolean allowInsecureRegistries = false; /** * Instantiates a new initializer for {@link RegistryAuthenticator}. @@ -59,8 +59,8 @@ private Initializer(String serverUrl, String repository) { this.repository = repository; } - public Initializer setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; + public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; return this; } @@ -78,7 +78,7 @@ public RegistryAuthenticator initialize() throws RegistryAuthenticationFailedException, IOException, RegistryException { try { return RegistryClient.factory(serverUrl, repository) - .setAllowHttp(allowHttp) + .setAllowInsecureRegistries(allowInsecureRegistries) .newRegistryClient() .getRegistryAuthenticator(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b54485d2c4..b89ff1bc3a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -68,7 +68,7 @@ public static class Factory { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private boolean allowHttp = false; + private boolean allowInsecureRegistries = false; @Nullable private Authorization authorization; private Factory(RegistryEndpointRequestProperties registryEndpointRequestProperties) { @@ -76,15 +76,14 @@ private Factory(RegistryEndpointRequestProperties registryEndpointRequestPropert } /** - * Sets whether or not {@code HTTP} should be allowed (credentials should not be sent when set - * to {@code true}). Defaults to {@code false}. + * Sets whether or not to allow insecure registries (ignoring certificate validation failure or + * communicating over HTTP if all else fail). * - * @param allowHttp if {@code true}, allows {@code HTTP} connections; otherwise, only {@code - * HTTPS} connections are allowed + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed * @return this */ - public Factory setAllowHttp(boolean allowHttp) { - this.allowHttp = allowHttp; + public Factory setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; return this; } @@ -106,7 +105,10 @@ public Factory setAuthorization(@Nullable Authorization authorization) { */ public RegistryClient newRegistryClient() { return new RegistryClient( - authorization, registryEndpointRequestProperties, allowHttp, makeUserAgent()); + authorization, + registryEndpointRequestProperties, + allowInsecureRegistries, + makeUserAgent()); } } @@ -159,7 +161,7 @@ static String makeUserAgent() { @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowHttp; + private final boolean allowInsecureRegistries; private final String userAgent; /** @@ -167,17 +169,16 @@ static String makeUserAgent() { * * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests - * @param allowHttp if {@code true}, allows redirects and fallbacks to HTTP; otherwise, only - * allows HTTPS + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp, + boolean allowInsecureRegistries, String userAgent) { this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowHttp = allowHttp; + this.allowInsecureRegistries = allowInsecureRegistries; this.userAgent = userAgent; } @@ -334,7 +335,7 @@ private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointP registryEndpointProvider, authorization, registryEndpointRequestProperties, - allowHttp) + allowInsecureRegistries) .call(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 2848c664bf..ca6bea19f7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -76,7 +76,7 @@ static class RequestState { private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowHttp; + private final boolean allowInsecureRegistries; /** * Constructs with parameters for making the request. @@ -86,8 +86,7 @@ static class RequestState { * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request - * @param allowHttp if {@code true}, allows redirects and fallbacks to HTTP; otherwise, only - * allows HTTPS + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( @@ -96,7 +95,7 @@ static class RequestState { RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp) + boolean allowInsecureRegistries) throws MalformedURLException { this( userAgent, @@ -104,7 +103,7 @@ static class RequestState { registryEndpointProvider, authorization, registryEndpointRequestProperties, - allowHttp, + allowInsecureRegistries, Connection::new); } @@ -115,7 +114,7 @@ static class RequestState { RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowHttp, + boolean allowInsecureRegistries, Function connectionFactory) throws MalformedURLException { this.initialRequestState = @@ -125,7 +124,7 @@ static class RequestState { this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowHttp = allowHttp; + this.allowInsecureRegistries = allowInsecureRegistries; this.connectionFactory = connectionFactory; } @@ -154,7 +153,7 @@ T call() throws IOException, RegistryException { @Nullable T call(RequestState requestState) throws IOException, RegistryException { boolean isHttpProtocol = "http".equals(requestState.url.getProtocol()); - if (!allowHttp && isHttpProtocol) { + if (!allowInsecureRegistries && isHttpProtocol) { throw new InsecureRegistryException(requestState.url); } @@ -237,7 +236,7 @@ T call(RequestState requestState) throws IOException, RegistryException { } catch (HttpHostConnectException | SSLPeerUnverifiedException ex) { // Tries to call with HTTP protocol if HTTPS failed to connect. - // Note that this will not succeed if 'allowHttp' is false. + // Note that this will not succeed if 'allowInsecureRegistries' is false. if ("https".equals(requestState.url.getProtocol())) { GenericUrl httpUrl = new GenericUrl(requestState.url); httpUrl.setScheme("http"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java index 34d06b826c..c7a4af6e45 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java @@ -92,7 +92,7 @@ public void testBuilder() { .setTargetFormat(OCIManifestTemplate.class) .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .setExtraFilesLayerConfiguration(expectedExtraFilesLayerConfiguration); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); @@ -121,7 +121,7 @@ public void testBuilder() { Assert.assertEquals( expectedBaseImageLayersCacheConfiguration, buildConfiguration.getBaseImageLayersCacheConfiguration()); - Assert.assertTrue(buildConfiguration.getAllowHttp()); + Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals( expectedExtraFilesLayerConfiguration, buildConfiguration.getExtraFilesLayerConfiguration()); } @@ -160,7 +160,7 @@ public void testBuilder_default() { Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); - Assert.assertFalse(buildConfiguration.getAllowHttp()); + Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertNull(buildConfiguration.getExtraFilesLayerConfiguration()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 6dd4392329..fe6d0f63c0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -63,13 +63,13 @@ public void testGetUserAgent() { Assert.assertTrue( testRegistryClientFactory - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient() .getUserAgent() .startsWith("jib ")); Assert.assertTrue( testRegistryClientFactory - .setAllowHttp(true) + .setAllowInsecureRegistries(true) .newRegistryClient() .getUserAgent() .endsWith(" some user agent suffix")); @@ -79,6 +79,9 @@ public void testGetUserAgent() { public void testGetApiRouteBase() { Assert.assertEquals( "some.server.url/v2/", - testRegistryClientFactory.setAllowHttp(true).newRegistryClient().getApiRouteBase()); + testRegistryClientFactory + .setAllowInsecureRegistries(true) + .newRegistryClient() + .getApiRouteBase()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 0b6241d29a..c067a20134 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -119,7 +119,7 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { .setJavaArguments(jibExtension.getArgs()) .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setAllowHttp(jibExtension.getAllowInsecureRegistries()); + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); if (Files.exists(jibExtension.getExtraDirectory().toPath())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(jibExtension.getExtraDirectory().toPath())) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 4e77100196..4c958ce22e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -127,7 +127,7 @@ public void buildImage() throws InvalidImageReferenceException, IOException { .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) .setTargetFormat(jibExtension.getFormat()) - .setAllowHttp(jibExtension.getAllowInsecureRegistries()); + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); if (Files.exists(jibExtension.getExtraDirectory().toPath())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(jibExtension.getExtraDirectory().toPath())) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 64086da31a..f9b1c6f8ae 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -147,7 +147,7 @@ public void buildTar() throws InvalidImageReferenceException, IOException { .setJavaArguments(jibExtension.getArgs()) .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setAllowHttp(jibExtension.getAllowInsecureRegistries()); + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); if (Files.exists(jibExtension.getExtraDirectory().toPath())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(jibExtension.getExtraDirectory().toPath())) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index f34f8c9528..956c8fa243 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -99,7 +99,7 @@ public void execute() throws MojoExecutionException { .setJvmFlags(getJvmFlags()) .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) - .setAllowHttp(getAllowInsecureRegistries()); + .setAllowInsecureRegistries(getAllowInsecureRegistries()); if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { buildConfigurationBuilder.setExtraFilesLayerConfiguration( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 62a840f291..68b46468d2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -121,7 +121,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) - .setAllowHttp(getAllowInsecureRegistries()); + .setAllowInsecureRegistries(getAllowInsecureRegistries()); if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { buildConfigurationBuilder.setExtraFilesLayerConfiguration( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 4787261e18..9ea8915265 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -97,7 +97,7 @@ public void execute() throws MojoExecutionException { .setJvmFlags(getJvmFlags()) .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) - .setAllowHttp(getAllowInsecureRegistries()); + .setAllowInsecureRegistries(getAllowInsecureRegistries()); if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { buildConfigurationBuilder.setExtraFilesLayerConfiguration( From 4e8126dcee7f7e01cc4160bba13fccd2425730c6 Mon Sep 17 00:00:00 2001 From: HelloWood Date: Tue, 24 Jul 2018 04:44:52 +0800 Subject: [PATCH 0050/2020] Add SpringBoot application w/ Jib demo draft (#645) --- examples/README.md | 8 ++ examples/spring-boot-draft/README.md | 127 ++++++++++++++++++ examples/spring-boot-draft/build.gradle | 61 +++++++++ examples/spring-boot-draft/gradle.properties | 2 + examples/spring-boot-draft/pom.xml | 86 ++++++++++++ examples/spring-boot-draft/settings.gradle | 1 + examples/spring-boot-draft/settings.xml | 14 ++ .../java/com/example/jib/JibApplication.java | 20 +++ .../src/main/resources/application.properties | 0 .../com/example/jib/JibApplicationTests.java | 16 +++ 10 files changed, 335 insertions(+) create mode 100644 examples/spring-boot-draft/README.md create mode 100644 examples/spring-boot-draft/build.gradle create mode 100644 examples/spring-boot-draft/gradle.properties create mode 100644 examples/spring-boot-draft/pom.xml create mode 100644 examples/spring-boot-draft/settings.gradle create mode 100644 examples/spring-boot-draft/settings.xml create mode 100644 examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java create mode 100644 examples/spring-boot-draft/src/main/resources/application.properties create mode 100644 examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java diff --git a/examples/README.md b/examples/README.md index efd97f50cf..9e2c821e35 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1 +1,9 @@ [![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples)](https://github.com/igrigorik/ga-beacon) + +### Simple Example + +You can find simple usage for Java in [helloworld](helloworld). + + diff --git a/examples/spring-boot-draft/README.md b/examples/spring-boot-draft/README.md new file mode 100644 index 0000000000..ff8143c788 --- /dev/null +++ b/examples/spring-boot-draft/README.md @@ -0,0 +1,127 @@ +SpringBoot Demo for using Jib + +## Tips: this is just a draft + +Please contribute if you have good suggestion/contribute if you have suggested edits . + +### Quickstart + +1. Login docker registry with password `hellojib`: + +``` +docker login -u hellojib +``` + +2. Modify configuration: + +Modify `credHelper` to match your platform . + +3. Build the image: + +Run `gradle jib` or `mvn compile jib:build` under project dictionary . + +4. Run container: + +Run `docker run --name hellojib -p 8080:8080 hellojib/hellojib:jib` . + +5. Access application: + +Run `curl localhost:8080` and it will return `"Hello Jib"` . + +### Custom base image and push registry + +You will need to add custom configuration (examples below). See the configuration reference for [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#extended-usage) or [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage) for more details: + +- Gradle + +build.gradle + +```gradle +jib { + from { + image = 'registry.hub.docker.com/openjdk:8-jdk-alpine' + credHelper = 'osxkeychain' + } + to { + image = 'CUSTOM_REGISTRY_URL/YOUR_USER_NAME//hellojib:jib' + credHelper = 'osxkeychain' + // Or use auth like below if you are not login + // Defined in gradle.properties + //auth { + // username = dockerUsername + // password = dockerPassword + //} + } + container { + jvmFlags = ['-Djava.security.egd=file:/dev/./urandom', '-Duser.timezone=GMT+08', '-Xdebug'] + mainClass = 'com.example.jib.JibApplication' + args = ['some args'] + ports = ['8080'] + } +} +``` + +gradle.properties + +```gradle +dockerUsername=YOUR_USERNAME +dockerPassword=YOUR_PASSWORD +``` + +- Maven + +pom.xml + +```xml + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + registry.hub.docker.com/openjdk:8-jdk-alpine + osxkeychain + + + CUSTOM_REGISTRY_URL/YOUR_USER_NAME/hellojib:jib + osxkeychain + + + + -Djava.security.egd=file:/dev/./urandom + -Xdebug + -Duser.timezone=GMT+08 + + com.example.jib.JibApplication + + some args + + + 8080 + + + + + + +``` + +settings.xml(`${MAVEN_HOME}/conf/settings.xml`)(You should config this is you are not login) + +```xml + + + + + CUSTOM_REGISTRY_URL + YOUR_USERNAME + YOUR_PASSWORD + + + +``` \ No newline at end of file diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle new file mode 100644 index 0000000000..d075adc31d --- /dev/null +++ b/examples/spring-boot-draft/build.gradle @@ -0,0 +1,61 @@ +buildscript { + ext { + springBootVersion = '2.0.3.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} +plugins { + id 'com.google.cloud.tools.jib' version '0.9.7' +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + + +group = 'com.example' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile('org.springframework.boot:spring-boot-starter-web') + testCompile('org.springframework.boot:spring-boot-starter-test') +} + + +jib.to.image = 'registry.hub.docker.com/hellojib/hellojib:jib' + +// If you want custom base image and push registry, use below configuration replace above + +//jib { +// from { +// image = 'registry.hub.docker.com/openjdk:8-jdk-alpine' +// credHelper = 'osxkeychain' +// } +// to { +// image = 'registry.hub.docker.com/hellojib/hellojib:jib' +// credHelper = 'osxkeychain' +// // Or use auth like below if you are not login +// // Defined in gradle.properties +// //auth { +// // username = dockerUsername +// // password = dockerPassword +// //} +// } +// container { +// jvmFlags = ['-Djava.security.egd=file:/dev/./urandom', '-Duser.timezone=GMT+08', '-Xdebug'] +// mainClass = 'com.example.jib.JibApplication' +// args = ['some args'] +// ports = ['8080'] +// } +//} \ No newline at end of file diff --git a/examples/spring-boot-draft/gradle.properties b/examples/spring-boot-draft/gradle.properties new file mode 100644 index 0000000000..9b39b2fdda --- /dev/null +++ b/examples/spring-boot-draft/gradle.properties @@ -0,0 +1,2 @@ +dockerUsername=hellojib +dockerPassword=hellojib \ No newline at end of file diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml new file mode 100644 index 0000000000..8df7de450f --- /dev/null +++ b/examples/spring-boot-draft/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + com.example + spring-boot + 0.0.1-SNAPSHOT + jar + + SpringBoot + Demo project for Jib with Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.3.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + 0.9.7 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + registry.hub.docker.com/hellojib/hellojib:jib + osxkeychain + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/spring-boot-draft/settings.gradle b/examples/spring-boot-draft/settings.gradle new file mode 100644 index 0000000000..19b5204418 --- /dev/null +++ b/examples/spring-boot-draft/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-boot' diff --git a/examples/spring-boot-draft/settings.xml b/examples/spring-boot-draft/settings.xml new file mode 100644 index 0000000000..5213b98fb0 --- /dev/null +++ b/examples/spring-boot-draft/settings.xml @@ -0,0 +1,14 @@ + + + + + + registry.hub.docker.com/hellojib/hellojib:jib + hellojib + {hXGhaewM6RYHG6jQDZjHyA3WVprcYjPQpto1ST8OJwI=} + + + \ No newline at end of file diff --git a/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java b/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java new file mode 100644 index 0000000000..57194b937c --- /dev/null +++ b/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java @@ -0,0 +1,20 @@ +package com.example.jib; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@RestController +public class JibApplication { + + public static void main(String[] args) { + SpringApplication.run(JibApplication.class, args); + } + + @RequestMapping("/") + public String helloJib() { + return "Hello Jib"; + } +} diff --git a/examples/spring-boot-draft/src/main/resources/application.properties b/examples/spring-boot-draft/src/main/resources/application.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java b/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java new file mode 100644 index 0000000000..3867c1d687 --- /dev/null +++ b/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java @@ -0,0 +1,16 @@ +package com.example.jib; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class JibApplicationTests { + + @Test + public void contextLoads() { + } + +} From daa7cabd770f2d59be2522c842b4a3d9ed1b220d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 24 Jul 2018 10:38:13 -0400 Subject: [PATCH 0051/2020] Update versions to 0.9.7 (#700) --- CONTRIBUTING.md | 4 ++-- README.md | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- jib-gradle-plugin/README.md | 4 ++-- jib-maven-plugin/README.md | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 260d5d29f7..9e2bf03d66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,12 +88,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.7-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.8-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.6' + // id 'com.google.cloud.tools.jib' version '0.9.7' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/README.md b/README.md index d70e1b5e85..8af6910613 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) [![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.6-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.7-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Google%20container%20tool%20for%20dockerizing%20Java%20applications&url=https://github.com/GoogleContainerTools/jib&via=JibContainerBuilder&hashtags=java,docker,kubernetes,maven,gradle,microservices,jib) diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 08aad9b5f8..1df17961a4 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.6' + id 'com.google.cloud.tools.jib' version '0.9.7' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 3d76167bab..9f1c9319c4 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -6,7 +6,7 @@ 1 - 0.9.6 + 0.9.7 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index a535f40fc5..29cd01ede1 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,6 +1,6 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) [![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-gradle-plugin)](https://github.com/igrigorik/ga-beacon) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.6-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.7-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Gradle Java project @@ -24,7 +24,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.6' + id 'com.google.cloud.tools.jib' version '0.9.7' } ``` diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 185a03651f..3f77b7e9c0 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -19,7 +19,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.6:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.7:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -27,7 +27,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.6:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.7:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -45,7 +45,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.6 + 0.9.7 myimage From 61e96f3e463230b81deee810b99e3e0555ce5da3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 24 Jul 2018 11:16:34 -0400 Subject: [PATCH 0052/2020] Simplify RegistryEndpointCaller code (#696) --- .../jib/registry/RegistryEndpointCaller.java | 56 ++++++------------- .../registry/RegistryEndpointCallerTest.java | 4 +- 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index ca6bea19f7..cfa6aaaa61 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -51,30 +51,13 @@ class RegistryEndpointCaller { private static final String DEFAULT_PROTOCOL = "https"; - /** Maintains the state of a request. This is used to retry requests with different parameters. */ - @VisibleForTesting - static class RequestState { - - @Nullable private final Authorization authorization; - private final URL url; - - /** - * @param authorization authentication credentials - * @param url the endpoint URL to call - */ - @VisibleForTesting - RequestState(@Nullable Authorization authorization, URL url) { - this.authorization = authorization; - this.url = url; - } - } - /** Makes a {@link Connection} to the specified {@link URL}. */ private final Function connectionFactory; - private final RequestState initialRequestState; + private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; + @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -117,12 +100,11 @@ static class RequestState { boolean allowInsecureRegistries, Function connectionFactory) throws MalformedURLException { - this.initialRequestState = - new RequestState( - authorization, - registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase)); + this.initialRequestUrl = + registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; + this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; this.connectionFactory = connectionFactory; @@ -137,27 +119,26 @@ static class RequestState { */ @Nullable T call() throws IOException, RegistryException { - return call(initialRequestState); + return call(initialRequestUrl); } /** - * Calls the registry endpoint with a certain {@link RequestState}. + * Calls the registry endpoint with a certain {@link URL}. * - * @param requestState the state of the request - determines how to make the request and how to - * process the response + * @param url the endpoint URL to call * @return an object representing the response, or {@code null} * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ @VisibleForTesting @Nullable - T call(RequestState requestState) throws IOException, RegistryException { - boolean isHttpProtocol = "http".equals(requestState.url.getProtocol()); + T call(URL url) throws IOException, RegistryException { + boolean isHttpProtocol = "http".equals(url.getProtocol()); if (!allowInsecureRegistries && isHttpProtocol) { - throw new InsecureRegistryException(requestState.url); + throw new InsecureRegistryException(url); } - try (Connection connection = connectionFactory.apply(requestState.url)) { + try (Connection connection = connectionFactory.apply(url)) { Request.Builder requestBuilder = Request.builder() .setUserAgent(userAgent) @@ -166,7 +147,7 @@ T call(RequestState requestState) throws IOException, RegistryException { .setBody(registryEndpointProvider.getContent()); // Only sends authorization if using HTTPS. if (!isHttpProtocol || Boolean.getBoolean("sendCredentialsOverHttp")) { - requestBuilder.setAuthorization(requestState.authorization); + requestBuilder.setAuthorization(authorization); } Response response = connection.send(registryEndpointProvider.getHttpMethod(), requestBuilder.build()); @@ -224,9 +205,8 @@ T call(RequestState requestState) throws IOException, RegistryException { == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY || httpResponseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { // 'Location' header can be relative or absolute. - URL redirectLocation = - new URL(requestState.url, httpResponseException.getHeaders().getLocation()); - return call(new RequestState(requestState.authorization, redirectLocation)); + URL redirectLocation = new URL(url, httpResponseException.getHeaders().getLocation()); + return call(redirectLocation); } else { // Unknown @@ -237,10 +217,10 @@ T call(RequestState requestState) throws IOException, RegistryException { } catch (HttpHostConnectException | SSLPeerUnverifiedException ex) { // Tries to call with HTTP protocol if HTTPS failed to connect. // Note that this will not succeed if 'allowInsecureRegistries' is false. - if ("https".equals(requestState.url.getProtocol())) { - GenericUrl httpUrl = new GenericUrl(requestState.url); + if ("https".equals(url.getProtocol())) { + GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); - return call(new RequestState(requestState.authorization, httpUrl.toURL())); + return call(httpUrl.toURL()); } throw ex; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 9a0b8747dd..4f4881600c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.http.MockConnection; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.RegistryEndpointCaller.RequestState; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; import java.io.IOException; @@ -175,8 +174,7 @@ public void testCall_credentialsNotSent() throws IOException, RegistryException true, mockConnectionFactory); try { - testRegistryEndpointCallerInsecure.call( - new RequestState(Authorizations.withBasicToken("token"), new URL("http://location"))); + testRegistryEndpointCallerInsecure.call(new URL("http://location")); Assert.fail("Call should have failed"); } catch (RegistryCredentialsNotSentException ex) { From 3478390a3eea94652cf47fa6bfb922d0545243df Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 24 Jul 2018 11:32:05 -0400 Subject: [PATCH 0053/2020] Add kokoro build orb to README (#497) * Add build orbs * Move analytics badge to bottom of page * Remove tweet link --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8af6910613..be8fe06942 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) +[![Maven Central](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/com/google/cloud/tools/jib-maven-plugin/maven-metadata.xml.svg?colorB=007ec6)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) [![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.7-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-ubuntu-master-orb.svg) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-windows-master-orb.svg) +![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-macos-master-orb.svg) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) -[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Google%20container%20tool%20for%20dockerizing%20Java%20applications&url=https://github.com/GoogleContainerTools/jib&via=JibContainerBuilder&hashtags=java,docker,kubernetes,maven,gradle,microservices,jib) # Jib @@ -60,3 +61,5 @@ We welcome contributions! Here's how you can contribute: * Join in on [discussion issues](https://github.com/GoogleContainerTools/jib/labels/discuss) *Make sure to follow the [Code of Conduct](https://github.com/GoogleContainerTools/jib/blob/master/CODE_OF_CONDUCT.md) when contributing so we can foster an open and welcoming community.* + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) From 8a2d4d1fd340de11974ad677dec280f5f87e06db Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 24 Jul 2018 11:48:58 -0400 Subject: [PATCH 0054/2020] Simplify TarStreamBuilder (#689) --- .../google/cloud/tools/jib/blob/Blobs.java | 8 ++- .../jib/docker/ImageToTarballTranslator.java | 12 ++-- .../jib/image/ReproducibleLayerBuilder.java | 2 +- .../cloud/tools/jib/tar/TarStreamBuilder.java | 52 ++++++------------ .../tools/jib/tar/TarStreamBuilderTest.java | 55 ++++++++++--------- 5 files changed, 59 insertions(+), 70 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 81edbd3459..c41268f40d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -33,6 +33,12 @@ public static Blob from(Path file) { return new FileBlob(file); } + /** + * Creates a {@link StringBlob} with UTF-8 encoding. + * + * @param content the string to create the blob from + * @return the {@link StringBlob} + */ public static Blob from(String content) { return new StringBlob(content); } @@ -42,7 +48,7 @@ public static Blob from(BlobWriter writer) { } /** - * Writes the BLOB to a string. + * Writes the BLOB to a string with UTF-8 decoding. * * @param blob the BLOB to write * @return the BLOB contents as a string diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java index 0936a9ae5d..2de8a737f1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java @@ -57,20 +57,22 @@ public Blob toTarballBlob(ImageReference imageReference) throws IOException { Path layerContentFile = layer.getContentFile(); String layerName = layerContentFile.getFileName().toString(); - tarStreamBuilder.addEntry(new TarArchiveEntry(layerContentFile.toFile(), layerName)); + tarStreamBuilder.addTarArchiveEntry( + new TarArchiveEntry(layerContentFile.toFile(), layerName)); manifestTemplate.addLayerFile(layerName); } // Adds the container configuration to the tarball. Blob containerConfigurationBlob = new ImageToJsonTranslator(image).getContainerConfigurationBlob(); - tarStreamBuilder.addEntry( - Blobs.writeToString(containerConfigurationBlob), CONTAINER_CONFIGURATION_JSON_FILE_NAME); + tarStreamBuilder.addByteEntry( + Blobs.writeToByteArray(containerConfigurationBlob), CONTAINER_CONFIGURATION_JSON_FILE_NAME); // Adds the manifest to tarball. manifestTemplate.setRepoTags(imageReference.toStringWithTag()); - tarStreamBuilder.addEntry( - Blobs.writeToString(JsonTemplateMapper.toBlob(manifestTemplate)), MANIFEST_JSON_FILE_NAME); + tarStreamBuilder.addByteEntry( + Blobs.writeToByteArray(JsonTemplateMapper.toBlob(manifestTemplate)), + MANIFEST_JSON_FILE_NAME); return tarStreamBuilder.toBlob(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 34de3fefca..dd0fc73469 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -121,7 +121,7 @@ public UnwrittenLayer build() throws IOException { entry.setUserName(""); entry.setGroupName(""); - tarStreamBuilder.addEntry(entry); + tarStreamBuilder.addTarArchiveEntry(entry); } return new UnwrittenLayer(tarStreamBuilder.toBlob()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index f08034c693..3c52f6e41c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -18,13 +18,9 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.common.io.ByteStreams; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.LinkedHashMap; import java.util.Map; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; @@ -33,17 +29,11 @@ /** Builds a tarball archive. */ public class TarStreamBuilder { - @FunctionalInterface - private interface TarArchiveOutputStreamConsumer { - void accept(TarArchiveOutputStream tarArchiveOutputStream) throws IOException; - } - /** - * Maps from {@link TarArchiveEntry} to function that outputs the entry onto a {@link - * TarArchiveOutputStream}. The order of the entries is the order they belong in the tarball. + * Maps from {@link TarArchiveEntry} to a {@link Blob}. The order of the entries is the order they + * belong in the tarball. */ - private final LinkedHashMap archiveMap = - new LinkedHashMap<>(); + private final LinkedHashMap archiveMap = new LinkedHashMap<>(); /** * Writes each entry in the filesystem to the tarball archive stream. @@ -56,45 +46,35 @@ private void writeEntriesAsTarArchive(OutputStream tarByteStream) throws IOExcep new TarArchiveOutputStream(tarByteStream, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - for (Map.Entry entry : - archiveMap.entrySet()) { + for (Map.Entry entry : archiveMap.entrySet()) { tarArchiveOutputStream.putArchiveEntry(entry.getKey()); - entry.getValue().accept(tarArchiveOutputStream); + entry.getValue().writeTo(tarArchiveOutputStream); tarArchiveOutputStream.closeArchiveEntry(); } } } /** - * Adds an entry to the archive. + * Adds a {@link TarArchiveEntry} to the archive. * - * @param entry the entry to add. + * @param entry the {@link TarArchiveEntry} */ - public void addEntry(TarArchiveEntry entry) { + public void addTarArchiveEntry(TarArchiveEntry entry) { archiveMap.put( - entry, - tarArchiveOutputStream -> { - // Note that this will skip files that don't exist. - if (entry.isFile()) { - try (InputStream contentStream = - new BufferedInputStream(Files.newInputStream(entry.getFile().toPath()))) { - ByteStreams.copy(contentStream, tarArchiveOutputStream); - } - } - }); + entry, entry.isFile() ? Blobs.from(entry.getFile().toPath()) : Blobs.from(ignored -> {})); } /** - * Adds a blob to the archive. + * Adds a blob to the archive. Note that this should be used with raw bytes and not file contents; + * for adding files to the archive, use {@code TarStreamBuilder#addTarArchiveEntry()}. * - * @param contents the blob contents to add to the tarball. - * @param name the name of the entry (i.e. filename). + * @param contents the bytes to add to the tarball + * @param name the name of the entry (i.e. filename) */ - public void addEntry(String contents, String name) { + public void addByteEntry(byte[] contents, String name) { TarArchiveEntry entry = new TarArchiveEntry(name); - byte[] contentsBytes = contents.getBytes(StandardCharsets.UTF_8); - entry.setSize(contentsBytes.length); - archiveMap.put(entry, tarArchiveOutputStream -> tarArchiveOutputStream.write(contentsBytes)); + entry.setSize(contents.length); + archiveMap.put(entry, Blobs.from(outputStream -> outputStream.write(contents))); } /** @return a new {@link Blob} that can stream the uncompressed tarball archive BLOB. */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index 1d6806ab72..a5cceb87ef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.tar; import com.google.cloud.tools.jib.blob.Blob; +import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; @@ -44,8 +45,8 @@ public class TarStreamBuilderTest { private Path fileA; private Path fileB; private Path directoryA; - private String fileAContents; - private String fileBContents; + private byte[] fileAContents; + private byte[] fileBContents; private TarStreamBuilder testTarStreamBuilder = new TarStreamBuilder(); @Before @@ -55,8 +56,8 @@ public void setup() throws URISyntaxException, IOException { fileB = Paths.get(Resources.getResource("fileB").toURI()); directoryA = Paths.get(Resources.getResource("directoryA").toURI()); - fileAContents = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); - fileBContents = new String(Files.readAllBytes(fileB), StandardCharsets.UTF_8); + fileAContents = Files.readAllBytes(fileA); + fileBContents = Files.readAllBytes(fileB); } @Test @@ -97,8 +98,8 @@ public void testToBlob_stringsAndTarArchiveEntriesWithCompression() throws IOExc @Test public void testToBlob_multiByte() throws IOException { - testTarStreamBuilder.addEntry("日本語", "test"); - testTarStreamBuilder.addEntry("asdf", "crepecake"); + testTarStreamBuilder.addByteEntry("日本語".getBytes(StandardCharsets.UTF_8), "test"); + testTarStreamBuilder.addByteEntry("asdf".getBytes(StandardCharsets.UTF_8), "crepecake"); Blob blob = testTarStreamBuilder.toBlob(); // Writes the BLOB and captures the output. @@ -131,11 +132,12 @@ public void testToBlob_multiByte() throws IOException { /** Creates a TarStreamBuilder using TarArchiveEntries. */ private void setUpWithTarEntries() { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addTarArchiveEntry( new TarArchiveEntry(fileA.toFile(), "some/path/to/resourceFileA")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); + testTarStreamBuilder.addTarArchiveEntry( + new TarArchiveEntry(directoryA.toFile(), "some/path/to")); + testTarStreamBuilder.addTarArchiveEntry( new TarArchiveEntry( fileA.toFile(), "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890")); @@ -144,10 +146,11 @@ private void setUpWithTarEntries() { /** Creates a TarStreamBuilder using Strings. */ private void setUpWithStrings() { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry(fileAContents, "some/path/to/resourceFileA"); - testTarStreamBuilder.addEntry(fileBContents, "crepecake"); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addByteEntry(fileAContents, "some/path/to/resourceFileA"); + testTarStreamBuilder.addByteEntry(fileBContents, "crepecake"); + testTarStreamBuilder.addTarArchiveEntry( + new TarArchiveEntry(directoryA.toFile(), "some/path/to")); + testTarStreamBuilder.addByteEntry( fileAContents, "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890"); } @@ -155,10 +158,11 @@ private void setUpWithStrings() { /** Creates a TarStreamBuilder using Strings and TarArchiveEntries. */ private void setUpWithStringsAndTarEntries() { // Prepares a test TarStreamBuilder. - testTarStreamBuilder.addEntry(fileAContents, "some/path/to/resourceFileA"); - testTarStreamBuilder.addEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); - testTarStreamBuilder.addEntry(new TarArchiveEntry(directoryA.toFile(), "some/path/to")); - testTarStreamBuilder.addEntry( + testTarStreamBuilder.addByteEntry(fileAContents, "some/path/to/resourceFileA"); + testTarStreamBuilder.addTarArchiveEntry(new TarArchiveEntry(fileB.toFile(), "crepecake")); + testTarStreamBuilder.addTarArchiveEntry( + new TarArchiveEntry(directoryA.toFile(), "some/path/to")); + testTarStreamBuilder.addByteEntry( fileAContents, "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890"); } @@ -203,16 +207,14 @@ private void verifyTarArchive(TarArchiveInputStream tarArchiveInputStream) throw // Verifies fileA was archived correctly. TarArchiveEntry headerFileA = tarArchiveInputStream.getNextTarEntry(); Assert.assertEquals("some/path/to/resourceFileA", headerFileA.getName()); - String fileAString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileAContents, fileAString); + byte[] fileAString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileAContents, fileAString); // Verifies fileB was archived correctly. TarArchiveEntry headerFileB = tarArchiveInputStream.getNextTarEntry(); Assert.assertEquals("crepecake", headerFileB.getName()); - String fileBString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileBContents, fileBString); + byte[] fileBString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileBContents, fileBString); // Verifies directoryA was archived correctly. TarArchiveEntry headerDirectoryA = tarArchiveInputStream.getNextTarEntry(); @@ -223,9 +225,8 @@ private void verifyTarArchive(TarArchiveInputStream tarArchiveInputStream) throw Assert.assertEquals( "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890", headerFileALong.getName()); - String fileALongString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals(fileAContents, fileALongString); + byte[] fileALongString = ByteStreams.toByteArray(tarArchiveInputStream); + Assert.assertArrayEquals(fileAContents, fileALongString); Assert.assertNull(tarArchiveInputStream.getNextTarEntry()); } From 237bf4220f239f6450d96bf31cd22379849f70d9 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 24 Jul 2018 13:09:18 -0400 Subject: [PATCH 0055/2020] System property 'jibSerialize' causes build steps to be executed serially (#682) -DjibSerialize=true for the win --- .../cloud/tools/jib/builder/steps/StepsRunner.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 12a226b411..c400b43019 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Nullable; @@ -44,8 +45,7 @@ */ public class StepsRunner { - private final ListeningExecutorService listeningExecutorService = - MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; private final SourceFilesConfiguration sourceFilesConfiguration; private final Cache baseLayersCache; @@ -76,6 +76,12 @@ public StepsRunner( this.sourceFilesConfiguration = sourceFilesConfiguration; this.baseLayersCache = baseLayersCache; this.applicationLayersCache = applicationLayersCache; + + ExecutorService executorService = + Boolean.getBoolean("jibSerialized") + ? MoreExecutors.newDirectExecutorService() + : Executors.newCachedThreadPool(); + listeningExecutorService = MoreExecutors.listeningDecorator(executorService); } public StepsRunner runRetrieveTargetRegistryCredentialsStep() { From 0f8dd028304e81584d482940a391fe27814c9829 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 24 Jul 2018 14:30:19 -0400 Subject: [PATCH 0056/2020] Log RegistryErrorExceptions (#697) --- .../cloud/tools/jib/frontend/BuildStepsRunner.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 01a6e337f8..79aa0a1d49 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -31,8 +31,10 @@ import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; +import com.google.cloud.tools.jib.registry.RegistryErrorException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.Path; @@ -255,6 +257,14 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut throw new BuildStepsExecutionException( helpfulSuggestions.forInsecureRegistry(), exceptionDuringBuildSteps); + } else if (exceptionDuringBuildSteps instanceof RegistryErrorException) { + // RegistryErrorExceptions have good messages + RegistryErrorException registryErrorException = + (RegistryErrorException) exceptionDuringBuildSteps; + String message = + Verify.verifyNotNull(registryErrorException.getMessage()); // keep null-away happy + throw new BuildStepsExecutionException(message, exceptionDuringBuildSteps); + } else { throw new BuildStepsExecutionException( helpfulSuggestions.none(), executionException.getCause()); From 7486d56662165d22a92ed2860479152c6569465f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 24 Jul 2018 19:00:15 -0400 Subject: [PATCH 0057/2020] Make Error Prone happy (#707) --- .../com/google/cloud/tools/jib/http/TestWebServer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index b913f55afd..37d1e971de 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.Semaphore; /** Simple local web server for testing. */ @@ -35,7 +36,7 @@ class TestWebServer implements Closeable { TestWebServer() throws IOException, InterruptedException { serverSocket = new ServerSocket(0); - executorService.submit(this::serve200); + ignoreReturn(executorService.submit(this::serve200)); threadStarted.acquire(); } @@ -62,4 +63,8 @@ private Void serve200() throws IOException { } return null; } + + private void ignoreReturn(Future future) { + // do nothing; to make Error Prone happy + } } From 0cef3ba03f0c8045c61c153560bff4c4036ae590 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 24 Jul 2018 23:44:12 -0400 Subject: [PATCH 0058/2020] Send correct error message (#706) --- .../cloud/tools/jib/blob/StringBlob.java | 2 +- .../jib/registry/RegistryEndpointCaller.java | 19 +++--- .../registry/RegistryEndpointCallerTest.java | 68 ++++++++----------- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java index b883b6a6ab..e0c4386c8c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java @@ -22,7 +22,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; -/** A {@link Blob} that holds a {@link String}. */ +/** A {@link Blob} that holds a {@link String}. Encodes in UTF-8 when writing in bytes. */ class StringBlob implements Blob { private final String content; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index cfa6aaaa61..33c1d5f28c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -137,6 +137,8 @@ T call(URL url) throws IOException, RegistryException { if (!allowInsecureRegistries && isHttpProtocol) { throw new InsecureRegistryException(url); } + // Only sends authorization if using HTTPS or explicitly forcing over HTTP. + boolean sendCredentials = !isHttpProtocol || Boolean.getBoolean("sendCredentialsOverHttp"); try (Connection connection = connectionFactory.apply(url)) { Request.Builder requestBuilder = @@ -145,8 +147,7 @@ T call(URL url) throws IOException, RegistryException { .setHttpTimeout(Integer.getInteger("jib.httpTimeout")) .setAccept(registryEndpointProvider.getAccept()) .setBody(registryEndpointProvider.getContent()); - // Only sends authorization if using HTTPS. - if (!isHttpProtocol || Boolean.getBoolean("sendCredentialsOverHttp")) { + if (sendCredentials) { requestBuilder.setAuthorization(authorization); } Response response = @@ -185,18 +186,16 @@ T call(URL url) throws IOException, RegistryException { } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { - if (isHttpProtocol) { - // Using HTTP, so credentials weren't sent. - throw new RegistryCredentialsNotSentException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName()); - - } else { - // Using HTTPS, so credentials are missing. + if (sendCredentials) { + // Credentials are either missing or wrong. throw new RegistryUnauthorizedException( registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), httpResponseException); + } else { + throw new RegistryCredentialsNotSentException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName()); } } else if (httpResponseException.getStatusCode() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 4f4881600c..ebcf0be4b4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,7 +20,6 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -100,24 +99,18 @@ public String getActionDescription() { @Before public void setUp() throws IOException { - testRegistryEndpointCallerSecure = - new RegistryEndpointCaller<>( - "userAgent", - "apiRouteBase", - new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), - new RegistryEndpointRequestProperties("serverUrl", "imageName"), - false, - mockConnectionFactory); + testRegistryEndpointCallerSecure = createRegistryEndpointCaller(false); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); Mockito.when(mockHttpResponse.parseAsString()).thenReturn(""); Mockito.when(mockHttpResponse.getHeaders()).thenReturn(new HttpHeaders()); + Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); } @After public void tearDown() { System.clearProperty("jib.httpTimeout"); + System.clearProperty("sendCredentialsOverHttp"); } @Test @@ -153,26 +146,15 @@ public void testCall_unauthorized() throws IOException, RegistryException { @Test public void testCall_credentialsNotSent() throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. Mockito.when(mockHttpResponse.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponse.getHeaders()) - .thenReturn(new HttpHeaders().setLocation("http://location")); HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException) - .thenReturn(mockResponse); + .thenThrow(httpResponseException); RegistryEndpointCaller testRegistryEndpointCallerInsecure = - new RegistryEndpointCaller<>( - "userAgent", - "apiRouteBase", - new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), - new RegistryEndpointRequestProperties("serverUrl", "imageName"), - true, - mockConnectionFactory); + createRegistryEndpointCaller(true); try { testRegistryEndpointCallerInsecure.call(new URL("http://location")); Assert.fail("Call should have failed"); @@ -184,6 +166,18 @@ public void testCall_credentialsNotSent() throws IOException, RegistryException } } + @Test + public void testCall_sendingCredentialsForced() throws IOException, RegistryException { + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenReturn(mockResponse); + + RegistryEndpointCaller testRegistryEndpointCallerInsecure = + createRegistryEndpointCaller(true); + System.setProperty("sendCredentialsOverHttp", "true"); + Assert.assertEquals( + "body", testRegistryEndpointCallerInsecure.call(new URL("http://location"))); + } + @Test public void testCall_forbidden() throws IOException, RegistryException { verifyThrowsRegistryUnauthorizedException(HttpStatusCodes.STATUS_CODE_FORBIDDEN); @@ -263,7 +257,6 @@ public void testCall_disallowInsecure() throws IOException, RegistryException { public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); Assert.assertNull(System.getProperty("jib.httpTimeout")); testRegistryEndpointCallerSecure.call(); @@ -277,7 +270,6 @@ public void testHttpTimeout_propertyNotSet() throws IOException, RegistryExcepti public void testHttpTimeout_stringValue() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); System.setProperty("jib.httpTimeout", "random string"); testRegistryEndpointCallerSecure.call(); @@ -289,7 +281,6 @@ public void testHttpTimeout_stringValue() throws IOException, RegistryException public void testHttpTimeout_negativeValue() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); System.setProperty("jib.httpTimeout", "-1"); testRegistryEndpointCallerSecure.call(); @@ -306,7 +297,6 @@ public void testHttpTimeout_0accepted() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); testRegistryEndpointCallerSecure.call(); Assert.assertEquals(Integer.valueOf(0), mockConnection.getRequestedHttpTimeout()); @@ -319,7 +309,6 @@ public void testHttpTimeout() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Mockito.mock(Blob.class)); testRegistryEndpointCallerSecure.call(); Assert.assertEquals(Integer.valueOf(7593), mockConnection.getRequestedHttpTimeout()); @@ -332,17 +321,9 @@ private void verifyRetriesWithHttp(Class exceptionClass) Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(exceptionClass)) .thenReturn(mockResponse); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); RegistryEndpointCaller testRegistryEndpointCallerInsecure = - new RegistryEndpointCaller<>( - "userAgent", - "apiRouteBase", - new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), - new RegistryEndpointRequestProperties("serverUrl", "imageName"), - true, - mockConnectionFactory); + createRegistryEndpointCaller(true); Assert.assertEquals("body", testRegistryEndpointCallerInsecure.call()); // Checks that the URL protocol was first HTTPS, then HTTP. @@ -420,7 +401,6 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(httpResponseException) .thenReturn(mockResponse); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); Assert.assertEquals("body", testRegistryEndpointCallerSecure.call()); @@ -431,4 +411,16 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) new URL("https://apiRouteBase/api"), urlArgumentCaptor.getAllValues().get(0)); Assert.assertEquals(new URL("https://newlocation"), urlArgumentCaptor.getAllValues().get(1)); } + + private RegistryEndpointCaller createRegistryEndpointCaller(boolean allowInsecure) + throws MalformedURLException { + return new RegistryEndpointCaller<>( + "userAgent", + "apiRouteBase", + new TestRegistryEndpointProvider(), + Authorizations.withBasicToken("token"), + new RegistryEndpointRequestProperties("serverUrl", "imageName"), + allowInsecure, + mockConnectionFactory); + } } From 6bf0148fcf31c5da00e726be3e0e75c34677e06e Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 25 Jul 2018 08:47:12 -0700 Subject: [PATCH 0059/2020] Removes SourceFilesConfiguration in favor of list of LayerConfiguration in BuildConfiguration. (#516) * Removes SourceFilesConfiguration in favor of list of LayerConfiguration in BuildConfiguration. * Refactors entrypoint building, renames JavaEntrypointConstructor. * Simplifies DockerContextGenerator and labels layers to avoid indexing. * Exports extra files layer in Docker context. * Do not add empty layers to image. --- .../builder/BuildStepsIntegrationTest.java | 74 ++++-- .../tools/jib/builder/BuildConfiguration.java | 153 ++++++------ .../cloud/tools/jib/builder/BuildSteps.java | 48 +--- .../tools/jib/builder/EntrypointBuilder.java | 56 ----- .../jib/builder/SourceFilesConfiguration.java | 72 ------ .../BuildAndCacheApplicationLayerStep.java | 74 ++---- .../jib/builder/steps/BuildImageStep.java | 7 +- .../tools/jib/builder/steps/StepsRunner.java | 18 +- .../jib/configuration/LayerConfiguration.java | 23 +- .../jib/docker/DockerContextGenerator.java | 133 ++++++++--- .../tools/jib/frontend/BuildStepsRunner.java | 60 ++--- .../frontend/JavaEntrypointConstructor.java | 64 +++++ .../tools/jib/frontend/MainClassFinder.java | 7 +- .../tools/jib/frontend/ProjectProperties.java | 16 +- .../google/cloud/tools/jib/image/Image.java | 70 ++++-- .../json/ContainerConfigurationTemplate.java | 8 +- .../jib/image/json/ImageToJsonTranslator.java | 11 +- .../credentials/DockerCredentialHelper.java | 4 +- .../jib/builder/BuildConfigurationTest.java | 55 ++--- .../builder/TestSourceFilesConfiguration.java | 131 ----------- ...BuildAndCacheApplicationLayerStepTest.java | 220 +++++++++--------- .../jib/builder/steps/BuildImageStepTest.java | 4 +- .../docker/DockerContextGeneratorTest.java | 70 +++--- .../jib/frontend/BuildStepsRunnerTest.java | 15 +- .../JavaEntrypointConstructorTest.java} | 32 ++- .../jib/frontend/MainClassFinderTest.java | 50 ++-- jib-core/src/test/resources/sampleDockerfile | 1 + jib-gradle-plugin/CHANGELOG.md | 6 +- .../jib/gradle/JibPluginIntegrationTest.java | 2 +- .../tools/jib/gradle/BuildDockerTask.java | 33 +-- .../tools/jib/gradle/BuildImageTask.java | 33 +-- .../cloud/tools/jib/gradle/BuildTarTask.java | 34 +-- .../tools/jib/gradle/DockerContextTask.java | 11 +- .../jib/gradle/GradleLayerConfigurations.java | 203 ++++++++++++++++ .../jib/gradle/GradleProjectProperties.java | 43 +++- .../GradleSourceFilesConfiguration.java | 145 ------------ ...ava => GradleLayerConfigurationsTest.java} | 64 +++-- .../gradle/GradleProjectPropertiesTest.java | 9 +- .../tools/jib/maven/BuildDockerMojo.java | 32 +-- .../cloud/tools/jib/maven/BuildImageMojo.java | 32 +-- .../cloud/tools/jib/maven/BuildTarMojo.java | 31 +-- .../tools/jib/maven/DockerContextMojo.java | 13 +- .../jib/maven/JibPluginConfiguration.java | 3 +- .../jib/maven/MavenLayerConfigurations.java | 194 +++++++++++++++ .../jib/maven/MavenProjectProperties.java | 47 +++- .../maven/MavenSourceFilesConfiguration.java | 136 ----------- .../DockerContextMojoIntegrationTest.java | 2 +- ...java => MavenLayerConfigurationsTest.java} | 55 +++-- .../jib/maven/MavenProjectPropertiesTest.java | 15 +- 49 files changed, 1307 insertions(+), 1312 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java rename jib-core/src/test/java/com/google/cloud/tools/jib/{builder/EntrypointBuilderTest.java => frontend/JavaEntrypointConstructorTest.java} (60%) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java rename jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/{GradleSourceFilesConfigurationTest.java => GradleLayerConfigurationsTest.java} (72%) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java rename jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/{MavenSourceFilesConfigurationTest.java => MavenLayerConfigurationsTest.java} (66%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index f85160a109..d73112c53f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -21,16 +21,23 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -42,25 +49,36 @@ /** Integration tests for {@link BuildSteps}. */ public class BuildStepsIntegrationTest { + /** Lists the files in the {@code resourcePath} resources directory. */ + private static ImmutableList getResourceFilesList(String resourcePath) + throws URISyntaxException, IOException { + try (Stream fileStream = + Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { + return fileStream.collect(ImmutableList.toImmutableList()); + } + } + @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); private static final TestBuildLogger logger = new TestBuildLogger(); - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); - - @Rule public TemporaryFolder temporaryTarOutput = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private SourceFilesConfiguration sourceFilesConfiguration; + private ImmutableList fakeLayerConfigurations; @Before public void setUp() throws IOException, URISyntaxException { - sourceFilesConfiguration = - TestSourceFilesConfiguration.builder() - .withClasses() - .withDependencies() - .withSnapshotDependencies() - .withResources() - .build(); + fakeLayerConfigurations = + ImmutableList.of( + LayerConfiguration.builder() + .addEntry(getResourceFilesList("application/dependencies"), "/app/libs/") + .build(), + LayerConfiguration.builder() + .addEntry(getResourceFilesList("application/resources"), "/app/resources/") + .build(), + LayerConfiguration.builder() + .addEntry(getResourceFilesList("application/classes"), "/app/classes/") + .build()); } @Test @@ -72,11 +90,14 @@ public void testSteps_forBuildToDockerRegistry() BuildConfiguration.builder(logger) .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setMainClass("HelloWorld") .setJavaArguments(Collections.singletonList("An argument.")) .setExposedPorts( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) .setAllowInsecureRegistries(true) + .setLayerConfigurations(fakeLayerConfigurations) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + Collections.emptyList(), "HelloWorld")) .build()); long lastTime = System.nanoTime(); @@ -110,9 +131,12 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() BuildConfiguration.builder(logger) .setBaseImage(ImageReference.parse("openjdk:8-jre-alpine")) .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setMainClass("HelloWorld") .setJavaArguments(Collections.singletonList("An argument.")) .setAllowInsecureRegistries(true) + .setLayerConfigurations(fakeLayerConfigurations) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + Collections.emptyList(), "HelloWorld")) .build()) .run(); @@ -130,16 +154,18 @@ public void testSteps_forBuildToDockerDaemon() BuildConfiguration.builder(logger) .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) .setTargetImage(ImageReference.of(null, "testdocker", null)) - .setMainClass("HelloWorld") .setJavaArguments(Collections.singletonList("An argument.")) .setExposedPorts( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) + .setLayerConfigurations(fakeLayerConfigurations) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + Collections.emptyList(), "HelloWorld")) .build(); - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToDockerDaemon( buildConfiguration, - sourceFilesConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) .run(); @@ -164,16 +190,18 @@ public void testSteps_forBuildToTarball() BuildConfiguration.builder(logger) .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) .setTargetImage(ImageReference.of(null, "testtar", null)) - .setMainClass("HelloWorld") .setJavaArguments(Collections.singletonList("An argument.")) + .setLayerConfigurations(fakeLayerConfigurations) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + Collections.emptyList(), "HelloWorld")) .build(); - Path outputPath = temporaryTarOutput.newFolder().toPath().resolve("test.tar"); - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); + Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToTar( outputPath, buildConfiguration, - sourceFilesConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) .run(); @@ -183,10 +211,8 @@ public void testSteps_forBuildToTarball() } private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws IOException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + Path cacheDirectory = temporaryFolder.newFolder().toPath(); return BuildSteps.forBuildToDockerRegistry( - buildConfiguration, - sourceFilesConfiguration, - new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)); + buildConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java index 890b50db2f..4d0412c971 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java @@ -49,16 +49,16 @@ public static class Builder { @Nullable private ImageReference targetImageReference; @Nullable private String targetImageCredentialHelperName; @Nullable private RegistryCredentials knownTargetRegistryCredentials; - @Nullable private String mainClass; - private ImmutableList javaArguments = ImmutableList.of(); - private ImmutableList jvmFlags = ImmutableList.of(); - private ImmutableMap environmentMap = ImmutableMap.of(); - private ImmutableList exposedPorts = ImmutableList.of(); + // TODO: Should rename to not be java-specific. + @Nullable private ImmutableList javaArguments; + @Nullable private ImmutableMap environmentMap; + @Nullable private ImmutableList exposedPorts; private Class targetFormat = V22ManifestTemplate.class; @Nullable private CacheConfiguration applicationLayersCacheConfiguration; @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; private boolean allowInsecureRegistries = false; - @Nullable private LayerConfiguration extraFilesLayerConfiguration; + private ImmutableList layerConfigurations = ImmutableList.of(); + @Nullable private ImmutableList entrypoint; private BuildLogger buildLogger; @@ -98,11 +98,6 @@ public Builder setKnownTargetRegistryCredentials( return this; } - public Builder setMainClass(@Nullable String mainClass) { - this.mainClass = mainClass; - return this; - } - public Builder setJavaArguments(@Nullable List javaArguments) { if (javaArguments != null) { Preconditions.checkArgument(!javaArguments.contains(null)); @@ -111,16 +106,10 @@ public Builder setJavaArguments(@Nullable List javaArguments) { return this; } - public Builder setJvmFlags(@Nullable List jvmFlags) { - if (jvmFlags != null) { - Preconditions.checkArgument(!jvmFlags.contains(null)); - this.jvmFlags = ImmutableList.copyOf(jvmFlags); - } - return this; - } - public Builder setEnvironment(@Nullable Map environmentMap) { - if (environmentMap != null) { + if (environmentMap == null) { + this.environmentMap = null; + } else { Preconditions.checkArgument( !Iterables.any(environmentMap.keySet(), Objects::isNull) && !Iterables.any(environmentMap.values(), Objects::isNull)); @@ -130,7 +119,9 @@ public Builder setEnvironment(@Nullable Map environmentMap) { } public Builder setExposedPorts(@Nullable List exposedPorts) { - if (exposedPorts != null) { + if (exposedPorts == null) { + this.exposedPorts = null; + } else { Preconditions.checkArgument(!exposedPorts.contains(null)); this.exposedPorts = ImmutableList.copyOf(exposedPorts); } @@ -178,14 +169,13 @@ public Builder setAllowInsecureRegistries(boolean allowInsecureRegistries) { } /** - * Sets the {@link LayerConfiguration} for an extra layer. + * Sets the layers to build. * - * @param extraFilesLayerConfiguration the layer configuration for the extra layer + * @param layerConfigurations the configurations for the layers * @return this */ - public Builder setExtraFilesLayerConfiguration( - @Nullable LayerConfiguration extraFilesLayerConfiguration) { - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; + public Builder setLayerConfigurations(List layerConfigurations) { + this.layerConfigurations = ImmutableList.copyOf(layerConfigurations); return this; } @@ -200,6 +190,22 @@ public Builder setCreationTime(Instant creationTime) { return this; } + /** + * Sets the container entrypoint. + * + * @param entrypoint the tokenized command to run when the container starts + * @return this + */ + public Builder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint == null) { + this.entrypoint = null; + } else { + Preconditions.checkArgument(!entrypoint.contains(null)); + this.entrypoint = ImmutableList.copyOf(entrypoint); + } + return this; + } + /** @return the corresponding build configuration */ public BuildConfiguration build() { // Validates the parameters. @@ -210,13 +216,10 @@ public BuildConfiguration build() { if (targetImageReference == null) { errorMessages.add("target image is required but not set"); } - if (mainClass == null) { - errorMessages.add("main class is required but not set"); - } switch (errorMessages.size()) { case 0: // No errors - if (baseImageReference == null || targetImageReference == null || mainClass == null) { + if (baseImageReference == null || targetImageReference == null) { throw new IllegalStateException("Required fields should not be null"); } if (baseImageReference.usesDefaultTag()) { @@ -234,16 +237,15 @@ public BuildConfiguration build() { targetImageReference, targetImageCredentialHelperName, knownTargetRegistryCredentials, - mainClass, javaArguments, - jvmFlags, environmentMap, exposedPorts, targetFormat, applicationLayersCacheConfiguration, baseImageLayersCacheConfiguration, allowInsecureRegistries, - extraFilesLayerConfiguration); + layerConfigurations, + entrypoint); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -252,19 +254,8 @@ public BuildConfiguration build() { throw new IllegalStateException(errorMessages.get(0) + " and " + errorMessages.get(1)); default: - // Appends the descriptions in correct grammar. - StringBuilder errorMessage = new StringBuilder(errorMessages.get(0)); - for (int errorMessageIndex = 1; - errorMessageIndex < errorMessages.size(); - errorMessageIndex++) { - if (errorMessageIndex == errorMessages.size() - 1) { - errorMessage.append(", and "); - } else { - errorMessage.append(", "); - } - errorMessage.append(errorMessages.get(errorMessageIndex)); - } - throw new IllegalStateException(errorMessage.toString()); + // Should never reach here. + throw new IllegalStateException(); } } } @@ -294,16 +285,15 @@ public static Builder builder(BuildLogger buildLogger) { private final ImageReference targetImageReference; @Nullable private final String targetImageCredentialHelperName; @Nullable private final RegistryCredentials knownTargetRegistryCredentials; - private final String mainClass; - private final ImmutableList javaArguments; - private final ImmutableList jvmFlags; - private final ImmutableMap environmentMap; - private final ImmutableList exposedPorts; + @Nullable private final ImmutableList javaArguments; + @Nullable private final ImmutableMap environmentMap; + @Nullable private final ImmutableList exposedPorts; private final Class targetFormat; @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; private final boolean allowInsecureRegistries; - @Nullable private final LayerConfiguration extraFilesLayerConfiguration; + private final ImmutableList layerConfigurations; + @Nullable private final ImmutableList entrypoint; /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( @@ -315,16 +305,15 @@ private BuildConfiguration( ImageReference targetImageReference, @Nullable String targetImageCredentialHelperName, @Nullable RegistryCredentials knownTargetRegistryCredentials, - String mainClass, - ImmutableList javaArguments, - ImmutableList jvmFlags, - ImmutableMap environmentMap, - ImmutableList exposedPorts, + @Nullable ImmutableList javaArguments, + @Nullable ImmutableMap environmentMap, + @Nullable ImmutableList exposedPorts, Class targetFormat, @Nullable CacheConfiguration applicationLayersCacheConfiguration, @Nullable CacheConfiguration baseImageLayersCacheConfiguration, boolean allowInsecureRegistries, - @Nullable LayerConfiguration extraFilesLayerConfiguration) { + ImmutableList layerConfigurations, + @Nullable ImmutableList entrypoint) { this.buildLogger = buildLogger; this.creationTime = creationTime; this.baseImageReference = baseImageReference; @@ -333,16 +322,15 @@ private BuildConfiguration( this.targetImageReference = targetImageReference; this.targetImageCredentialHelperName = targetImageCredentialHelperName; this.knownTargetRegistryCredentials = knownTargetRegistryCredentials; - this.mainClass = mainClass; this.javaArguments = javaArguments; - this.jvmFlags = jvmFlags; this.environmentMap = environmentMap; this.exposedPorts = exposedPorts; this.targetFormat = targetFormat; this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; this.allowInsecureRegistries = allowInsecureRegistries; - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; + this.layerConfigurations = layerConfigurations; + this.entrypoint = entrypoint; } public BuildLogger getBuildLogger() { @@ -405,22 +393,32 @@ public RegistryCredentials getKnownTargetRegistryCredentials() { return knownTargetRegistryCredentials; } - public String getMainClass() { - return mainClass; - } - + /** + * Gets the arguments to pass to the entrypoint. + * + * @return the list of arguments, or {@code null} if not set + */ + @Nullable public ImmutableList getJavaArguments() { return javaArguments; } - public ImmutableList getJvmFlags() { - return jvmFlags; - } - + /** + * Gets the map from environment variable names to values for the container. + * + * @return the map of environment variables, or {@code null} if not set + */ + @Nullable public ImmutableMap getEnvironment() { return environmentMap; } + /** + * Gets the ports to expose on the container. + * + * @return the list of exposed ports, or {@code null} if not set + */ + @Nullable public ImmutableList getExposedPorts() { return exposedPorts; } @@ -460,12 +458,21 @@ public boolean getAllowInsecureRegistries() { } /** - * Gets the {@link LayerConfiguration} for an extra layer. + * Gets the configurations for building the layers. + * + * @return the list of layer configurations + */ + public ImmutableList getLayerConfigurations() { + return layerConfigurations; + } + + /** + * Gets the container entrypoint. * - * @return the layer configuration + * @return the list of entrypoint tokens, or {@code null} if not set */ @Nullable - public LayerConfiguration getExtraFilesLayerConfiguration() { - return extraFilesLayerConfiguration; + public ImmutableList getEntrypoint() { + return entrypoint; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index c100eca3da..f74032c76b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.ExecutionException; @@ -63,18 +62,14 @@ private interface StepsRunnerConsumer { * All the steps to build an image to a Docker registry. * * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache * @return a new {@link BuildSteps} for building to a registry */ public static BuildSteps forBuildToDockerRegistry( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Caches.Initializer cachesInitializer) { + BuildConfiguration buildConfiguration, Caches.Initializer cachesInitializer) { return new BuildSteps( DESCRIPTION_FOR_DOCKER_REGISTRY, buildConfiguration, - sourceFilesConfiguration, cachesInitializer, String.format( STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, @@ -90,7 +85,7 @@ public static BuildSteps forBuildToDockerRegistry( .runPullAndCacheBaseImageLayersStep() .runPushBaseImageLayersStep() .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) + .runBuildImageStep() .runPushContainerConfigurationStep() .runPushApplicationLayersStep() .runFinalizingPushStep() @@ -102,18 +97,14 @@ public static BuildSteps forBuildToDockerRegistry( * All the steps to build to Docker daemon * * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache * @return a new {@link BuildSteps} for building to a Docker daemon */ public static BuildSteps forBuildToDockerDaemon( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Caches.Initializer cachesInitializer) { + BuildConfiguration buildConfiguration, Caches.Initializer cachesInitializer) { return new BuildSteps( DESCRIPTION_FOR_DOCKER_DAEMON, buildConfiguration, - sourceFilesConfiguration, cachesInitializer, String.format( STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, buildConfiguration.getTargetImageReference()), @@ -124,7 +115,7 @@ public static BuildSteps forBuildToDockerDaemon( .runPullBaseImageStep() .runPullAndCacheBaseImageLayersStep() .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) + .runBuildImageStep() .runFinalizingBuildStep() .runLoadDockerStep() .waitOnLoadDockerStep()); @@ -135,19 +126,16 @@ public static BuildSteps forBuildToDockerDaemon( * * @param outputPath the path to output the tarball to * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache * @return a new {@link BuildSteps} for building a tarball */ public static BuildSteps forBuildToTar( Path outputPath, BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, Caches.Initializer cachesInitializer) { return new BuildSteps( DESCRIPTION_FOR_TARBALL, buildConfiguration, - sourceFilesConfiguration, cachesInitializer, String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), @@ -156,24 +144,14 @@ public static BuildSteps forBuildToTar( .runPullBaseImageStep() .runPullAndCacheBaseImageLayersStep() .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep(getEntrypoint(buildConfiguration, sourceFilesConfiguration)) + .runBuildImageStep() .runFinalizingBuildStep() .runWriteTarFileStep(outputPath) .waitOnWriteTarFileStep()); } - /** Creates the container entrypoint for a given configuration. */ - private static ImmutableList getEntrypoint( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) { - return EntrypointBuilder.makeEntrypoint( - sourceFilesConfiguration, - buildConfiguration.getJvmFlags(), - buildConfiguration.getMainClass()); - } - private final String description; private final BuildConfiguration buildConfiguration; - private final SourceFilesConfiguration sourceFilesConfiguration; private final Caches.Initializer cachesInitializer; private final String startupMessage; private final String successMessage; @@ -188,14 +166,12 @@ private static ImmutableList getEntrypoint( private BuildSteps( String description, BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, Caches.Initializer cachesInitializer, String startupMessage, String successMessage, StepsRunnerConsumer stepsRunnerConsumer) { this.description = description; this.buildConfiguration = buildConfiguration; - this.sourceFilesConfiguration = sourceFilesConfiguration; this.cachesInitializer = cachesInitializer; this.startupMessage = startupMessage; this.successMessage = successMessage; @@ -206,10 +182,6 @@ public BuildConfiguration getBuildConfiguration() { return buildConfiguration; } - public SourceFilesConfiguration getSourceFilesConfiguration() { - return sourceFilesConfiguration; - } - public String getStartupMessage() { return startupMessage; } @@ -229,11 +201,7 @@ public void run() Cache applicationLayersCache = caches.getApplicationCache(); StepsRunner stepsRunner = - new StepsRunner( - buildConfiguration, - sourceFilesConfiguration, - baseImageLayersCache, - applicationLayersCache); + new StepsRunner(buildConfiguration, baseImageLayersCache, applicationLayersCache); stepsRunnerConsumer.accept(stepsRunner); // Writes the cached layers to the cache metadata. @@ -246,8 +214,6 @@ public void run() buildConfiguration.getBuildLogger().lifecycle(""); buildConfiguration .getBuildLogger() - .lifecycle( - "Container entrypoint set to " - + getEntrypoint(buildConfiguration, sourceFilesConfiguration)); + .lifecycle("Container entrypoint set to " + buildConfiguration.getEntrypoint()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java deleted file mode 100644 index 2c1c06ffea..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/EntrypointBuilder.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import java.util.List; - -/** Builds an image entrypoint for the Java application. */ -public class EntrypointBuilder { - - /** - * Builds the container entrypoint. - * - *

The entrypoint is {@code java [jvm flags] -cp [classpaths] [main class]}. - * - * @param sourceFilesConfiguration configuration defining where files are copied onto the image - * @param jvmFlags the JVM flags to start the container with - * @param mainClass the name of the main class to run on startup - * @return a list of the entrypoint tokens - */ - public static ImmutableList makeEntrypoint( - SourceFilesConfiguration sourceFilesConfiguration, List jvmFlags, String mainClass) { - ImmutableList classPaths = - ImmutableList.of( - sourceFilesConfiguration.getDependenciesPathOnImage() + "*", - sourceFilesConfiguration.getResourcesPathOnImage(), - sourceFilesConfiguration.getClassesPathOnImage()); - - String classPathsString = String.join(":", classPaths); - - ImmutableList.Builder entrypointBuilder = - ImmutableList.builderWithExpectedSize(4 + jvmFlags.size()); - entrypointBuilder.add("java"); - entrypointBuilder.addAll(jvmFlags); - entrypointBuilder.add("-cp"); - entrypointBuilder.add(classPathsString); - entrypointBuilder.add(mainClass); - return entrypointBuilder.build(); - } - - private EntrypointBuilder() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java deleted file mode 100644 index 695227c751..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/SourceFilesConfiguration.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; - -/** - * Immutable configuration that defines where the source files for each of the application layers - * are. - */ -public interface SourceFilesConfiguration { - - String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; - String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; - String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; - - /** - * @return the source files for the dependencies layer. These files should be in a deterministic - * order. - */ - ImmutableList getDependenciesFiles(); - - /** - * @return the source files for snapshot dependencies. These files should be in a deterministic - * order - */ - ImmutableList getSnapshotDependenciesFiles(); - - /** - * @return the source files for the resources layer. These files should be in a deterministic - * order. - */ - ImmutableList getResourcesFiles(); - - /** - * @return the source files for the classes layer. These files should be in a deterministic order. - */ - ImmutableList getClassesFiles(); - - /** - * @return the Unix-style path where the dependencies source files are placed in the container - * filesystem. Must end with slash. This includes both regular and snapshot dependencies. - */ - String getDependenciesPathOnImage(); - - /** - * @return the Unix-style path where the resources source files are placed in the container - * filesystem. Must end with slash. - */ - String getResourcesPathOnImage(); - - /** - * @return the Unix-style path where the classes source files are placed in the container - * filesystem. Must end with slash. - */ - String getClassesPathOnImage(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 0536ea01f6..d6ad5a23de 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; @@ -47,71 +46,28 @@ class BuildAndCacheApplicationLayerStep static ImmutableList makeList( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, Cache cache) { try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { - ImmutableList.Builder buildLayerStepsBuilder = - ImmutableList.builder() - .add( - new BuildAndCacheApplicationLayerStep( - "dependencies", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getDependenciesFiles(), - sourceFilesConfiguration.getDependenciesPathOnImage()) - .build(), - cache)) - .add( - new BuildAndCacheApplicationLayerStep( - "resources", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getResourcesFiles(), - sourceFilesConfiguration.getResourcesPathOnImage()) - .build(), - cache)) - .add( - new BuildAndCacheApplicationLayerStep( - "classes", - listeningExecutorService, - buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getClassesFiles(), - sourceFilesConfiguration.getClassesPathOnImage()) - .build(), - cache)); - - // Adds a snapshot dependencies layer, if snapshot files present. - if (!sourceFilesConfiguration.getSnapshotDependenciesFiles().isEmpty()) { - buildLayerStepsBuilder.add( + ImmutableList.Builder buildAndCacheApplicationLayerSteps = + ImmutableList.builderWithExpectedSize(buildConfiguration.getLayerConfigurations().size()); + for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { + // Skips the layer if empty. + if (layerConfiguration + .getLayerEntries() + .stream() + .allMatch(layerEntry -> layerEntry.getSourceFiles().isEmpty())) { + continue; + } + + buildAndCacheApplicationLayerSteps.add( new BuildAndCacheApplicationLayerStep( - "snapshot-dependencies", + layerConfiguration.getLabel(), listeningExecutorService, buildConfiguration, - LayerConfiguration.builder() - .addEntry( - sourceFilesConfiguration.getSnapshotDependenciesFiles(), - sourceFilesConfiguration.getDependenciesPathOnImage()) - .build(), + layerConfiguration, cache)); } - // Adds the extra layer to be built, if configured. - if (buildConfiguration.getExtraFilesLayerConfiguration() != null) { - buildLayerStepsBuilder.add( - new BuildAndCacheApplicationLayerStep( - "extra files", - listeningExecutorService, - buildConfiguration, - buildConfiguration.getExtraFilesLayerConfiguration(), - cache)); - } - - return buildLayerStepsBuilder.build(); + return buildAndCacheApplicationLayerSteps.build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 94a48e2108..4e7273bd79 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -41,7 +41,6 @@ class BuildImageStep private final BuildConfiguration buildConfiguration; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; - private final ImmutableList entrypoint; private final ListeningExecutorService listeningExecutorService; private final ListenableFuture>> listenableFuture; @@ -50,13 +49,11 @@ class BuildImageStep ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps, - ImmutableList entrypoint) { + ImmutableList buildAndCacheApplicationLayerSteps) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - this.entrypoint = entrypoint; listenableFuture = Futures.whenAllSucceed(pullAndCacheBaseImageLayersStep.getFuture()) @@ -101,7 +98,7 @@ private Image afterCachedLayersSteps() } imageBuilder.setCreated(buildConfiguration.getCreationTime()); imageBuilder.setEnvironment(buildConfiguration.getEnvironment()); - imageBuilder.setEntrypoint(entrypoint); + imageBuilder.setEntrypoint(buildConfiguration.getEntrypoint()); imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments()); imageBuilder.setExposedPorts(buildConfiguration.getExposedPorts()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index c400b43019..99b8091a45 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; @@ -47,7 +46,6 @@ public class StepsRunner { private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; - private final SourceFilesConfiguration sourceFilesConfiguration; private final Cache baseLayersCache; private final Cache applicationLayersCache; @@ -68,12 +66,8 @@ public class StepsRunner { @Nullable private WriteTarFileStep writeTarFileStep; public StepsRunner( - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration, - Cache baseLayersCache, - Cache applicationLayersCache) { + BuildConfiguration buildConfiguration, Cache baseLayersCache, Cache applicationLayersCache) { this.buildConfiguration = buildConfiguration; - this.sourceFilesConfiguration = sourceFilesConfiguration; this.baseLayersCache = baseLayersCache; this.applicationLayersCache = applicationLayersCache; @@ -128,21 +122,17 @@ public StepsRunner runPushBaseImageLayersStep() { public StepsRunner runBuildAndCacheApplicationLayerSteps() { buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - listeningExecutorService, - buildConfiguration, - sourceFilesConfiguration, - applicationLayersCache); + listeningExecutorService, buildConfiguration, applicationLayersCache); return this; } - public StepsRunner runBuildImageStep(ImmutableList entrypoint) { + public StepsRunner runBuildImageStep() { buildImageStep = new BuildImageStep( listeningExecutorService, buildConfiguration, Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), - entrypoint); + Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps)); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index e420381c39..b87774e6a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -29,9 +29,21 @@ public class LayerConfiguration { public static class Builder { private final ImmutableList.Builder layerEntries = ImmutableList.builder(); + private String label = ""; private Builder() {} + /** + * Sets a label for this layer. + * + * @param label the label + * @return this + */ + public Builder setLabel(String label) { + this.label = label; + return this; + } + /** * Adds an entry to the layer. * @@ -56,7 +68,7 @@ public Builder addEntry(List sourceFiles, String destinationOnImage) { * @return the built {@link LayerConfiguration} */ public LayerConfiguration build() { - return new LayerConfiguration(layerEntries.build()); + return new LayerConfiguration(label, layerEntries.build()); } } @@ -65,16 +77,23 @@ public static Builder builder() { } private final ImmutableList layerEntries; + private final String label; /** * Constructs a new layer configuration. * + * @param label an optional label for the layer * @param layerEntries the list of {@link LayerEntry}s */ - private LayerConfiguration(ImmutableList layerEntries) { + private LayerConfiguration(String label, ImmutableList layerEntries) { + this.label = label; this.layerEntries = layerEntries; } + public String getLabel() { + return label; + } + public ImmutableList getLayerEntries() { return layerEntries; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java index 83275229f2..2840a007c7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java @@ -18,11 +18,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.builder.EntrypointBuilder; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.io.MoreFiles; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -36,17 +37,67 @@ /** * Generates a Docker context that mimics how Jib builds the image. * - *

The image consists of a base image layer and three application layers under the directories: + *

The image consists of a base image layer and 5 application layers under the directories: * *

    - *
  • libs/ (dependency jars) - *
  • resources/ - *
  • classes + *
  • {@code libs/} (dependency jars) + *
  • {@code snapshot-libs/} (snapshot dependency jars) + *
  • {@code resources/} (resource files) + *
  • {@code classes/} ({@code .class} files) + *
  • {@code root/} (extra files) *
+ * + * Empty application layers are omitted. */ public class DockerContextGenerator { - private final SourceFilesConfiguration sourceFilesConfiguration; + private static final String DEPENDENCIES_LAYER_DIRECTORY = "libs"; + private static final String SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY = "snapshot-libs"; + private static final String RESOURCES_LAYER_DIRECTORY = "resources"; + private static final String CLASSES_LAYER_DIRECTORY = "classes"; + private static final String EXTRA_FILES_LAYER_DIRECTORY = "root"; + + /** Represents a Dockerfile {@code COPY} directive. */ + private static class CopyDirective { + + /** The source files to put into the context. */ + private final ImmutableList sourceFiles; + + /** The directory in the context to put the source files for the layer */ + private final String directoryInContext; + + /** The extraction path in the image. */ + private final String extractionPath; + + private CopyDirective( + ImmutableList sourceFiles, String directoryInContext, String extractionPath) { + this.sourceFiles = sourceFiles; + this.directoryInContext = directoryInContext; + this.extractionPath = extractionPath; + } + } + + /** + * Adds a copy directive for the {@code layerEntry} if it's not empty. + * + * @param listBuilder the {@link ImmutableList.Builder} to add to + * @param layerEntry the layer entry + * @param directoryInContext the directory in the context to put the source files for the layer + */ + private static void addIfNotEmpty( + ImmutableList.Builder listBuilder, + LayerEntry layerEntry, + String directoryInContext) { + if (layerEntry.getSourceFiles().isEmpty()) { + return; + } + + listBuilder.add( + new CopyDirective( + layerEntry.getSourceFiles(), directoryInContext, layerEntry.getExtractionPath())); + } + + private final ImmutableList copyDirectives; @Nullable private String baseImage; private List jvmFlags = Collections.emptyList(); @@ -54,8 +105,23 @@ public class DockerContextGenerator { private List javaArguments = Collections.emptyList(); private List exposedPorts = Collections.emptyList(); - public DockerContextGenerator(SourceFilesConfiguration sourceFilesConfiguration) { - this.sourceFilesConfiguration = sourceFilesConfiguration; + // TODO: Just take the LayerConfigurations. + public DockerContextGenerator( + LayerEntry dependenciesLayerEntry, + LayerEntry snapshotDependenciesLayerEntry, + LayerEntry resourcesLayerEntry, + LayerEntry classesLayerEntry, + LayerEntry extraFilesLayerEntry) { + ImmutableList.Builder copyDirectivesBuilder = ImmutableList.builder(); + addIfNotEmpty(copyDirectivesBuilder, dependenciesLayerEntry, DEPENDENCIES_LAYER_DIRECTORY); + addIfNotEmpty( + copyDirectivesBuilder, + snapshotDependenciesLayerEntry, + SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY); + addIfNotEmpty(copyDirectivesBuilder, resourcesLayerEntry, RESOURCES_LAYER_DIRECTORY); + addIfNotEmpty(copyDirectivesBuilder, classesLayerEntry, CLASSES_LAYER_DIRECTORY); + addIfNotEmpty(copyDirectivesBuilder, extraFilesLayerEntry, EXTRA_FILES_LAYER_DIRECTORY); + copyDirectives = copyDirectivesBuilder.build(); } /** @@ -134,22 +200,14 @@ public void generate(Path targetDirectory) throws IOException { Files.createDirectory(targetDirectory); - // Creates the directories. - Path dependenciesDir = targetDirectory.resolve("libs"); - Path snapshotDependenciesDir = targetDirectory.resolve("snapshot-libs"); - Path resourcesDIr = targetDirectory.resolve("resources"); - Path classesDir = targetDirectory.resolve("classes"); - Files.createDirectory(dependenciesDir); - Files.createDirectories(snapshotDependenciesDir); - Files.createDirectory(resourcesDIr); - Files.createDirectory(classesDir); - - // Copies dependencies. - FileOperations.copy(sourceFilesConfiguration.getDependenciesFiles(), dependenciesDir); - FileOperations.copy( - sourceFilesConfiguration.getSnapshotDependenciesFiles(), snapshotDependenciesDir); - FileOperations.copy(sourceFilesConfiguration.getResourcesFiles(), resourcesDIr); - FileOperations.copy(sourceFilesConfiguration.getClassesFiles(), classesDir); + for (CopyDirective copyDirective : copyDirectives) { + // Creates the directories. + Path directoryInContext = targetDirectory.resolve(copyDirective.directoryInContext); + Files.createDirectory(directoryInContext); + + // Copies dependencies. + FileOperations.copy(copyDirective.sourceFiles, directoryInContext); + } // Creates the Dockerfile. Files.write( @@ -163,8 +221,10 @@ public void generate(Path targetDirectory) throws IOException { * FROM [base image] * * COPY libs [path/to/dependencies] + * COPY snapshot-libs [path/to/dependencies] * COPY resources [path/to/resources] * COPY classes [path/to/classes] + * COPY root [path/to/classes] * * EXPOSE [port] * [More EXPOSE instructions, if necessary] @@ -178,18 +238,15 @@ public void generate(Path targetDirectory) throws IOException { String makeDockerfile() throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper(); StringBuilder dockerfile = new StringBuilder(); - dockerfile - .append("FROM ") - .append(Preconditions.checkNotNull(baseImage)) - .append("\n\nCOPY libs ") - .append(sourceFilesConfiguration.getDependenciesPathOnImage()) - .append("\nCOPY snapshot-libs ") - .append(sourceFilesConfiguration.getDependenciesPathOnImage()) - .append("\nCOPY resources ") - .append(sourceFilesConfiguration.getResourcesPathOnImage()) - .append("\nCOPY classes ") - .append(sourceFilesConfiguration.getClassesPathOnImage()) - .append("\n"); + dockerfile.append("FROM ").append(Preconditions.checkNotNull(baseImage)).append("\n"); + for (CopyDirective copyDirective : copyDirectives) { + dockerfile + .append("\nCOPY ") + .append(copyDirective.directoryInContext) + .append(" ") + .append(copyDirective.extractionPath); + } + dockerfile.append("\n"); for (String port : exposedPorts) { dockerfile.append("\nEXPOSE ").append(port); } @@ -197,7 +254,7 @@ String makeDockerfile() throws JsonProcessingException { .append("\nENTRYPOINT ") .append( objectMapper.writeValueAsString( - EntrypointBuilder.makeEntrypoint(sourceFilesConfiguration, jvmFlags, mainClass))) + JavaEntrypointConstructor.makeDefaultEntrypoint(jvmFlags, mainClass))) .append("\nCMD ") .append(objectMapper.writeValueAsString(javaArguments)); return dockerfile.toString(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 79aa0a1d49..b45f888f91 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -21,13 +21,14 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.cache.Caches.Initializer; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; @@ -48,32 +49,28 @@ public class BuildStepsRunner { * Creates a runner to build an image. Creates a directory for the cache, if needed. * * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @return a {@link BuildStepsRunner} for building to a registry * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildImage( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) + public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfiguration) throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToDockerRegistry( - buildConfiguration, sourceFilesConfiguration, getCacheInitializer(buildConfiguration))); + buildConfiguration, getCacheInitializer(buildConfiguration))); } /** * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. * * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @return a {@link BuildStepsRunner} for building to a Docker daemon * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildToDockerDaemon( - BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration) + public static BuildStepsRunner forBuildToDockerDaemon(BuildConfiguration buildConfiguration) throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToDockerDaemon( - buildConfiguration, sourceFilesConfiguration, getCacheInitializer(buildConfiguration))); + buildConfiguration, getCacheInitializer(buildConfiguration))); } /** @@ -81,21 +78,14 @@ public static BuildStepsRunner forBuildToDockerDaemon( * * @param outputPath the path to output the tarball to * @param buildConfiguration the configuration parameters for the build - * @param sourceFilesConfiguration the source/destination file configuration for the image * @return a {@link BuildStepsRunner} for building a tarball * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildTar( - Path outputPath, - BuildConfiguration buildConfiguration, - SourceFilesConfiguration sourceFilesConfiguration) + public static BuildStepsRunner forBuildTar(Path outputPath, BuildConfiguration buildConfiguration) throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToTar( - outputPath, - buildConfiguration, - sourceFilesConfiguration, - getCacheInitializer(buildConfiguration))); + outputPath, buildConfiguration, getCacheInitializer(buildConfiguration))); } // TODO: Move this up to somewhere where defaults for cache location are provided and ownership is @@ -168,6 +158,13 @@ private static void handleRegistryUnauthorizedException( } } + private static String capitalizeFirstLetter(String string) { + if (string.length() == 0) { + return string; + } + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + private final BuildSteps buildSteps; @VisibleForTesting @@ -192,23 +189,16 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut // Logs the different source files used. buildLogger.info("Containerizing application with the following files:"); - buildLogger.info("\tClasses:"); - buildSteps - .getSourceFilesConfiguration() - .getClassesFiles() - .forEach(classesFile -> buildLogger.info("\t\t" + classesFile)); - - buildLogger.info("\tResources:"); - buildSteps - .getSourceFilesConfiguration() - .getResourcesFiles() - .forEach(resourceFile -> buildLogger.info("\t\t" + resourceFile)); - - buildLogger.info("\tDependencies:"); - buildSteps - .getSourceFilesConfiguration() - .getDependenciesFiles() - .forEach(dependencyFile -> buildLogger.info("\t\t" + dependencyFile)); + for (LayerConfiguration layerConfiguration : + buildSteps.getBuildConfiguration().getLayerConfigurations()) { + buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getLabel()) + ":"); + + for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { + for (Path sourceFile : layerEntry.getSourceFiles()) { + buildLogger.info("\t\t" + sourceFile); + } + } + } buildSteps.run(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java new file mode 100644 index 0000000000..d18fcf09eb --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Constructs an image entrypoint for the Java application. */ +public class JavaEntrypointConstructor { + + public static final String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; + public static final String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; + public static final String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; + + public static List makeDefaultEntrypoint(List jvmFlags, String mainClass) { + return makeEntrypoint( + Arrays.asList( + DEFAULT_DEPENDENCIES_PATH_ON_IMAGE + "*", + DEFAULT_RESOURCES_PATH_ON_IMAGE, + DEFAULT_CLASSES_PATH_ON_IMAGE), + jvmFlags, + mainClass); + } + + /** + * Constructs the container entrypoint. + * + *

The entrypoint is {@code java [jvm flags] -cp [classpaths] [main class]}. + * + * @param classpathElements paths to add to the classpath (will be separated by {@code :} + * @param jvmFlags the JVM flags to start the container with + * @param mainClass the name of the main class to run on startup + * @return a list of the entrypoint tokens + */ + public static List makeEntrypoint( + List classpathElements, List jvmFlags, String mainClass) { + String classpathString = String.join(":", classpathElements); + + List entrypointBuilder = new ArrayList<>(4 + jvmFlags.size()); + entrypointBuilder.add("java"); + entrypointBuilder.addAll(jvmFlags); + entrypointBuilder.add("-cp"); + entrypointBuilder.add(classpathString); + entrypointBuilder.add(mainClass); + return entrypointBuilder; + } + + private JavaEntrypointConstructor() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 42babaa9da..009ae52e68 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Modifier; @@ -74,9 +75,11 @@ public static String resolveMainClass( try { // Adds each file in the classes output directory to the classes files list. - Set visitedRoots = new HashSet<>(); + ImmutableList classesFiles = + projectProperties.getClassesLayerEntry().getSourceFiles(); List mainClasses = new ArrayList<>(); - for (Path classPath : projectProperties.getSourceFilesConfiguration().getClassesFiles()) { + Set visitedRoots = new HashSet<>(); + for (Path classPath : classesFiles) { Path root = classPath.getParent(); if (visitedRoots.contains(root)) { continue; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java index 1ddfa8460c..6afd732de6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java @@ -17,7 +17,9 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; import java.nio.file.Path; import javax.annotation.Nullable; @@ -31,7 +33,7 @@ public interface ProjectProperties { String getPluginName(); - SourceFilesConfiguration getSourceFilesConfiguration(); + ImmutableList getLayerConfigurations(); Path getCacheDirectory(); @@ -41,6 +43,16 @@ public interface ProjectProperties { @Nullable String getMainClassFromJar(); + LayerEntry getDependenciesLayerEntry(); + + LayerEntry getSnapshotDependenciesLayerEntry(); + + LayerEntry getResourcesLayerEntry(); + + LayerEntry getClassesLayerEntry(); + + LayerEntry getExtraFilesLayerEntry(); + /** * @param prefix the prefix message for the {@link HelpfulSuggestions}. * @return a {@link HelpfulSuggestions} instance for main class inference failure. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index b9d4c56384..58704e2b7a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -30,12 +30,12 @@ public class Image { public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); - private final ImmutableList.Builder environmentBuilder = ImmutableList.builder(); + private ImmutableList.Builder environmentBuilder = ImmutableList.builder(); @Nullable private Instant created; - private ImmutableList entrypoint = ImmutableList.of(); - private ImmutableList javaArguments = ImmutableList.of(); - private ImmutableList exposedPorts = ImmutableList.of(); + @Nullable private ImmutableList entrypoint; + @Nullable private ImmutableList javaArguments; + @Nullable private ImmutableList exposedPorts; /** * Sets the image creation time. @@ -54,9 +54,13 @@ public Builder setCreated(Instant created) { * @param environment the map of environment variables * @return this */ - public Builder setEnvironment(Map environment) { - for (Map.Entry environmentVariable : environment.entrySet()) { - setEnvironmentVariable(environmentVariable.getKey(), environmentVariable.getValue()); + public Builder setEnvironment(@Nullable Map environment) { + if (environment == null) { + this.environmentBuilder = ImmutableList.builder(); + } else { + for (Map.Entry environmentVariable : environment.entrySet()) { + setEnvironmentVariable(environmentVariable.getKey(), environmentVariable.getValue()); + } } return this; } @@ -90,8 +94,12 @@ public Builder addEnvironmentVariableDefinition(String environmentVariableDef * @param entrypoint the list of entrypoint tokens * @return this */ - public Builder setEntrypoint(List entrypoint) { - this.entrypoint = ImmutableList.copyOf(entrypoint); + public Builder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint == null) { + this.entrypoint = null; + } else { + this.entrypoint = ImmutableList.copyOf(entrypoint); + } return this; } @@ -101,8 +109,12 @@ public Builder setEntrypoint(List entrypoint) { * @param javaArguments the list of main args to add * @return this */ - public Builder setJavaArguments(List javaArguments) { - this.javaArguments = ImmutableList.copyOf(javaArguments); + public Builder setJavaArguments(@Nullable List javaArguments) { + if (javaArguments == null) { + this.javaArguments = null; + } else { + this.javaArguments = ImmutableList.copyOf(javaArguments); + } return this; } @@ -112,8 +124,12 @@ public Builder setJavaArguments(List javaArguments) { * @param exposedPorts the list of exposed ports to add * @return this */ - public Builder setExposedPorts(ImmutableList exposedPorts) { - this.exposedPorts = exposedPorts; + public Builder setExposedPorts(@Nullable ImmutableList exposedPorts) { + if (exposedPorts == null) { + this.exposedPorts = null; + } else { + this.exposedPorts = exposedPorts; + } return this; } @@ -134,8 +150,8 @@ public Image build() { created, imageLayersBuilder.build(), environmentBuilder.build(), - ImmutableList.copyOf(entrypoint), - ImmutableList.copyOf(javaArguments), + entrypoint, + javaArguments, exposedPorts); } } @@ -151,27 +167,27 @@ public static Builder builder() { private final ImageLayers layers; /** Environment variable definitions for running the image, in the format {@code NAME=VALUE}. */ - private final ImmutableList environmentBuilder; + @Nullable private final ImmutableList environment; /** Initial command to run when running the image. */ - private final ImmutableList entrypoint; + @Nullable private final ImmutableList entrypoint; /** Arguments to pass into main when running the image. */ - private final ImmutableList javaArguments; + @Nullable private final ImmutableList javaArguments; /** Ports that the container listens on. */ - private final ImmutableList exposedPorts; + @Nullable private final ImmutableList exposedPorts; private Image( @Nullable Instant created, ImageLayers layers, - ImmutableList environment, - ImmutableList entrypoint, - ImmutableList javaArguments, - ImmutableList exposedPorts) { + @Nullable ImmutableList environment, + @Nullable ImmutableList entrypoint, + @Nullable ImmutableList javaArguments, + @Nullable ImmutableList exposedPorts) { this.created = created; this.layers = layers; - this.environmentBuilder = environment; + this.environment = environment; this.entrypoint = entrypoint; this.javaArguments = javaArguments; this.exposedPorts = exposedPorts; @@ -182,18 +198,22 @@ public Instant getCreated() { return created; } + @Nullable public ImmutableList getEnvironment() { - return environmentBuilder; + return environment; } + @Nullable public ImmutableList getEntrypoint() { return entrypoint; } + @Nullable public ImmutableList getJavaArguments() { return javaArguments; } + @Nullable public ImmutableList getExposedPorts() { return exposedPorts; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 2ff134c05c..9d8f0f2b4f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -109,19 +109,19 @@ public void setCreated(@Nullable String created) { this.created = created; } - public void setContainerEnvironment(List environment) { + public void setContainerEnvironment(@Nullable List environment) { config.Env = environment; } - public void setContainerEntrypoint(List command) { + public void setContainerEntrypoint(@Nullable List command) { config.Entrypoint = command; } - public void setContainerCmd(List cmd) { + public void setContainerCmd(@Nullable List cmd) { config.Cmd = cmd; } - public void setContainerExposedPorts(Map> exposedPorts) { + public void setContainerExposedPorts(@Nullable Map> exposedPorts) { config.ExposedPorts = exposedPorts; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index ad0145d11e..5731005163 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; /** * Translates an {@link Image} into a manifest or container configuration JSON BLOB. @@ -48,12 +49,16 @@ public class ImageToJsonTranslator { * Converts a list of {@link Port}s to the corresponding container config format for exposed ports * (e.g. {@code Port(1000, Protocol.TCP)} -> {@code {"1000/tcp":{}}}). * - * @param exposedPorts the list of {@link Port}s to translate + * @param exposedPorts the list of {@link Port}s to translate, or {@code null} * @return a sorted map with the string representation of the ports as keys and empty maps as - * values + * values, or {@code null} if {@code exposedPorts} is {@code null} */ @VisibleForTesting - static Map> portListToMap(List exposedPorts) { + @Nullable + static Map> portListToMap(@Nullable List exposedPorts) { + if (exposedPorts == null) { + return null; + } ImmutableSortedMap.Builder> result = new ImmutableSortedMap.Builder<>(String::compareTo); for (Port port : exposedPorts) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 72117e72e8..e292867814 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStreamReader; @@ -104,7 +105,8 @@ public Authorization retrieve() try { DockerCredentialsTemplate dockerCredentials = JsonTemplateMapper.readJson(output, DockerCredentialsTemplate.class); - if (dockerCredentials.Username == null || dockerCredentials.Secret == null) { + if (Strings.isNullOrEmpty(dockerCredentials.Username) + || Strings.isNullOrEmpty(dockerCredentials.Secret)) { throw new NonexistentServerUrlDockerCredentialHelperException( credentialHelper, serverUrl, output); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java index c7a4af6e45..686bc265ef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java @@ -57,9 +57,7 @@ public void testBuilder() { String expectedTargetImageCredentialHelperName = "anotherCredentialHelper"; RegistryCredentials expectedKnownTargetRegistryCredentials = Mockito.mock(RegistryCredentials.class); - String expectedMainClass = "mainclass"; List expectedJavaArguments = Arrays.asList("arg1", "arg2"); - List expectedJvmFlags = Arrays.asList("some", "jvm", "flags"); Map expectedEnvironment = ImmutableMap.of("key", "value"); ImmutableList expectedExposedPorts = ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP)); @@ -68,8 +66,10 @@ public void testBuilder() { CacheConfiguration.forPath(Paths.get("application/layers")); CacheConfiguration expectedBaseImageLayersCacheConfiguration = CacheConfiguration.forPath(Paths.get("base/image/layers")); - LayerConfiguration expectedExtraFilesLayerConfiguration = - LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build(); + List expectedLayerConfigurations = + Collections.singletonList( + LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build()); + List expectedEntrypoint = Arrays.asList("some", "entrypoint"); BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) @@ -84,16 +84,15 @@ public void testBuilder() { expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .setTargetImageCredentialHelperName(expectedTargetImageCredentialHelperName) .setKnownTargetRegistryCredentials(expectedKnownTargetRegistryCredentials) - .setMainClass(expectedMainClass) .setJavaArguments(expectedJavaArguments) - .setJvmFlags(expectedJvmFlags) .setEnvironment(expectedEnvironment) .setExposedPorts(expectedExposedPorts) .setTargetFormat(OCIManifestTemplate.class) .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) .setAllowInsecureRegistries(true) - .setExtraFilesLayerConfiguration(expectedExtraFilesLayerConfiguration); + .setLayerConfigurations(expectedLayerConfigurations) + .setEntrypoint(expectedEntrypoint); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertEquals(expectedCreationTime, buildConfiguration.getCreationTime()); @@ -109,9 +108,7 @@ public void testBuilder() { Assert.assertEquals( expectedTargetImageCredentialHelperName, buildConfiguration.getTargetImageCredentialHelperName()); - Assert.assertEquals(expectedMainClass, buildConfiguration.getMainClass()); Assert.assertEquals(expectedJavaArguments, buildConfiguration.getJavaArguments()); - Assert.assertEquals(expectedJvmFlags, buildConfiguration.getJvmFlags()); Assert.assertEquals(expectedEnvironment, buildConfiguration.getEnvironment()); Assert.assertEquals(expectedExposedPorts, buildConfiguration.getExposedPorts()); Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); @@ -122,8 +119,8 @@ public void testBuilder() { expectedBaseImageLayersCacheConfiguration, buildConfiguration.getBaseImageLayersCacheConfiguration()); Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); - Assert.assertEquals( - expectedExtraFilesLayerConfiguration, buildConfiguration.getExtraFilesLayerConfiguration()); + Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); + Assert.assertEquals(expectedEntrypoint, buildConfiguration.getEntrypoint()); } @Test @@ -135,7 +132,6 @@ public void testBuilder_default() { String expectedTargetServerUrl = "someotherserver"; String expectedTargetImageName = "targetimage"; String expectedTargetTag = "targettag"; - String expectedMainClass = "mainclass"; BuildConfiguration buildConfiguration = BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) @@ -145,7 +141,6 @@ public void testBuilder_default() { .setTargetImage( ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) - .setMainClass(expectedMainClass) .build(); Assert.assertEquals(buildConfiguration.getCreationTime(), Instant.EPOCH); @@ -153,32 +148,20 @@ public void testBuilder_default() { Assert.assertNull(buildConfiguration.getKnownBaseRegistryCredentials()); Assert.assertNull(buildConfiguration.getTargetImageCredentialHelperName()); Assert.assertNull(buildConfiguration.getKnownTargetRegistryCredentials()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getJavaArguments()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getJvmFlags()); - Assert.assertEquals(Collections.emptyMap(), buildConfiguration.getEnvironment()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getExposedPorts()); + Assert.assertNull(buildConfiguration.getJavaArguments()); + Assert.assertNull(buildConfiguration.getEnvironment()); + Assert.assertNull(buildConfiguration.getExposedPorts()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); - Assert.assertNull(buildConfiguration.getExtraFilesLayerConfiguration()); + Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); + Assert.assertNull(buildConfiguration.getEntrypoint()); } @Test public void testBuilder_missingValues() { - // Main class is missing - try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage(Mockito.mock(ImageReference.class)) - .setTargetImage(Mockito.mock(ImageReference.class)) - .build(); - Assert.fail("Build configuration should not be built with missing values"); - - } catch (IllegalStateException ex) { - Assert.assertEquals("main class is required but not set", ex.getMessage()); - } - - // Main class and target image are missing + // Target image is missing try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) .setBaseImage(Mockito.mock(ImageReference.class)) @@ -186,9 +169,7 @@ public void testBuilder_missingValues() { Assert.fail("Build configuration should not be built with missing values"); } catch (IllegalStateException ex) { - Assert.assertEquals( - "target image is required but not set and main class is required but not set", - ex.getMessage()); + Assert.assertEquals("target image is required but not set", ex.getMessage()); } // All required fields missing @@ -198,7 +179,7 @@ public void testBuilder_missingValues() { } catch (IllegalStateException ex) { Assert.assertEquals( - "base image is required but not set, target image is required but not set, and main class is required but not set", + "base image is required but not set and target image is required but not set", ex.getMessage()); } } @@ -215,10 +196,10 @@ public void testBuilder_nullValues() { Assert.assertNull(ex.getMessage()); } - // JVM flags element should not be null. + // Entrypoint element should not be null. try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setJvmFlags(Arrays.asList("first", null)); + .setEntrypoint(Arrays.asList("first", null)); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java deleted file mode 100644 index 33396e3295..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestSourceFilesConfiguration.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; - -/** Implementation of {@link SourceFilesConfiguration} that uses test resources. */ -public class TestSourceFilesConfiguration implements SourceFilesConfiguration { - - public static class Builder { - private ImmutableList dependenciesSourceFiles = ImmutableList.of(); - private ImmutableList snapshotDependenciesSourceFiles = ImmutableList.of(); - private ImmutableList resourcesSourceFiles = ImmutableList.of(); - private ImmutableList classesSourceFiles = ImmutableList.of(); - - public Builder withDependencies() throws IOException, URISyntaxException { - dependenciesSourceFiles = getFilesList("application/dependencies"); - return this; - } - - public Builder withSnapshotDependencies() throws IOException, URISyntaxException { - snapshotDependenciesSourceFiles = getFilesList("application/snapshot-dependencies"); - return this; - } - - public Builder withResources() throws IOException, URISyntaxException { - resourcesSourceFiles = getFilesList("application/resources"); - return this; - } - - public Builder withClasses() throws IOException, URISyntaxException { - classesSourceFiles = getFilesList("application/classes"); - return this; - } - - public TestSourceFilesConfiguration build() { - return new TestSourceFilesConfiguration( - dependenciesSourceFiles, - snapshotDependenciesSourceFiles, - resourcesSourceFiles, - classesSourceFiles); - } - - /** Lists the files in the {@code resourcePath} resources directory. */ - private ImmutableList getFilesList(String resourcePath) - throws URISyntaxException, IOException { - try (Stream fileStream = - Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { - return fileStream.collect(ImmutableList.toImmutableList()); - } - } - } - - public static Builder builder() { - return new Builder(); - } - - private static final String EXTRACTION_PATH = "/some/extraction/path/"; - - private final ImmutableList dependenciesSourceFiles; - private final ImmutableList snapshotDependenciesSourceFiles; - private final ImmutableList resourcesSourceFiles; - private final ImmutableList classesSourceFiles; - - private TestSourceFilesConfiguration( - ImmutableList dependenciesSourceFiles, - ImmutableList snapshotDependenciesSourceFiles, - ImmutableList resourcesSourceFiles, - ImmutableList classesSourceFiles) { - this.dependenciesSourceFiles = dependenciesSourceFiles; - this.snapshotDependenciesSourceFiles = snapshotDependenciesSourceFiles; - this.resourcesSourceFiles = resourcesSourceFiles; - this.classesSourceFiles = classesSourceFiles; - } - - @Override - public ImmutableList getDependenciesFiles() { - return dependenciesSourceFiles; - } - - @Override - public ImmutableList getSnapshotDependenciesFiles() { - return snapshotDependenciesSourceFiles; - } - - @Override - public ImmutableList getResourcesFiles() { - return resourcesSourceFiles; - } - - @Override - public ImmutableList getClassesFiles() { - return classesSourceFiles; - } - - @Override - public String getDependenciesPathOnImage() { - return EXTRACTION_PATH + "libs/"; - } - - @Override - public String getResourcesPathOnImage() { - return EXTRACTION_PATH + "resources/"; - } - - @Override - public String getClassesPathOnImage() { - return EXTRACTION_PATH + "classes/"; - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 0c92492ad2..b2eadaf04c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -18,9 +18,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.BuildConfiguration; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.builder.TestBuildLogger; -import com.google.cloud.tools.jib.builder.TestSourceFilesConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; @@ -34,9 +32,11 @@ import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -51,34 +51,64 @@ @RunWith(MockitoJUnitRunner.class) public class BuildAndCacheApplicationLayerStepTest { + // TODO: Consolidate with BuildStepsIntegrationTest. + private static final String EXTRACTION_PATH = "/some/extraction/path/"; + private static final String EXTRA_FILES_LAYER_EXTRACTION_PATH = "/extra"; + /** Lists the files in the {@code resourcePath} resources directory. */ + private static ImmutableList getFilesList(String resourcePath) + throws URISyntaxException, IOException { + try (Stream fileStream = + Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { + return fileStream.collect(ImmutableList.toImmutableList()); + } + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; private Path temporaryCacheDirectory; + private LayerConfiguration fakeDependenciesLayerConfiguration; + private LayerConfiguration fakeSnapshotDependenciesLayerConfiguration; + private LayerConfiguration fakeResourcesLayerConfiguration; + private LayerConfiguration fakeClassesLayerConfiguration; + private LayerConfiguration fakeExtraFilesLayerConfiguration; + private LayerConfiguration emptyLayerConfiguration; + @Before - public void setUp() throws IOException { + public void setUp() throws IOException, URISyntaxException { + fakeDependenciesLayerConfiguration = + LayerConfiguration.builder() + .addEntry(getFilesList("application/dependencies"), EXTRACTION_PATH + "libs/") + .build(); + fakeSnapshotDependenciesLayerConfiguration = + LayerConfiguration.builder() + .addEntry(getFilesList("application/snapshot-dependencies"), EXTRACTION_PATH + "libs/") + .build(); + fakeResourcesLayerConfiguration = + LayerConfiguration.builder() + .addEntry(getFilesList("application/resources"), EXTRACTION_PATH + "resources/") + .build(); + fakeClassesLayerConfiguration = + LayerConfiguration.builder() + .addEntry(getFilesList("application/classes"), EXTRACTION_PATH + "classes/") + .build(); + fakeExtraFilesLayerConfiguration = + LayerConfiguration.builder() + .addEntry( + ImmutableList.of( + Paths.get(Resources.getResource("fileA").toURI()), + Paths.get(Resources.getResource("fileB").toURI())), + EXTRA_FILES_LAYER_EXTRACTION_PATH) + .build(); + emptyLayerConfiguration = LayerConfiguration.builder().build(); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestBuildLogger()); temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); } - private ImmutableList configureExtraFilesLayer() throws URISyntaxException { - ImmutableList extraFilesLayerSourceFiles = - ImmutableList.of( - Paths.get(Resources.getResource("fileA").toURI()), - Paths.get(Resources.getResource("fileB").toURI())); - Mockito.when(mockBuildConfiguration.getExtraFilesLayerConfiguration()) - .thenReturn( - LayerConfiguration.builder() - .addEntry(extraFilesLayerSourceFiles, EXTRA_FILES_LAYER_EXTRACTION_PATH) - .build()); - return extraFilesLayerSourceFiles; - } - - private ImageLayers configureAvailableLayers( - SourceFilesConfiguration testSourceFilesConfiguration) + private ImageLayers buildFakeLayersToCache() throws CacheMetadataCorruptedException, IOException, ExecutionException { ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); ImageLayers applicationLayers; @@ -86,10 +116,7 @@ private ImageLayers configureAvailableLayers( try (Cache cache = Cache.init(temporaryCacheDirectory)) { ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - MoreExecutors.newDirectExecutorService(), - mockBuildConfiguration, - testSourceFilesConfiguration, - cache); + MoreExecutors.newDirectExecutorService(), mockBuildConfiguration, cache); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { @@ -105,117 +132,94 @@ private ImageLayers configureAvailableLayers( @Test public void testRun() throws LayerPropertyNotFoundException, IOException, CacheMetadataCorruptedException, - URISyntaxException, ExecutionException { - - TestSourceFilesConfiguration testSourceFilesConfiguration = - TestSourceFilesConfiguration.builder() - .withDependencies() - .withSnapshotDependencies() - .withClasses() - .withResources() - .build(); - - // Adds an extra file layer. - ImmutableList extraFilesLayerSourceFiles = configureExtraFilesLayer(); - - // Populate the cache - ImageLayers applicationLayers = - configureAvailableLayers(testSourceFilesConfiguration); + ExecutionException { + ImmutableList fakeLayerConfigurations = + ImmutableList.of( + fakeDependenciesLayerConfiguration, + fakeSnapshotDependenciesLayerConfiguration, + fakeResourcesLayerConfiguration, + fakeClassesLayerConfiguration, + fakeExtraFilesLayerConfiguration); + Mockito.when(mockBuildConfiguration.getLayerConfigurations()) + .thenReturn(fakeLayerConfigurations); + + // Populates the cache. + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(5, applicationLayers.size()); // Re-initialize cache with the updated metadata. Cache cache = Cache.init(temporaryCacheDirectory); - ImmutableList dependenciesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getDependenciesFiles(), - testSourceFilesConfiguration.getDependenciesPathOnImage())); - ImmutableList snapshotDependenciesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getSnapshotDependenciesFiles(), - testSourceFilesConfiguration.getDependenciesPathOnImage())); - ImmutableList resourcesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getResourcesFiles(), - testSourceFilesConfiguration.getResourcesPathOnImage())); - ImmutableList classesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getClassesFiles(), - testSourceFilesConfiguration.getClassesPathOnImage())); - ImmutableList extraFilesLayerEntry = - ImmutableList.of( - new LayerEntry(extraFilesLayerSourceFiles, EXTRA_FILES_LAYER_EXTRACTION_PATH)); + ImmutableList dependenciesLayerEntries = + fakeLayerConfigurations.get(0).getLayerEntries(); + ImmutableList snapshotDependenciesLayerEntries = + fakeLayerConfigurations.get(1).getLayerEntries(); + ImmutableList resourcesLayerEntries = + fakeLayerConfigurations.get(2).getLayerEntries(); + ImmutableList classesLayerEntries = + fakeLayerConfigurations.get(3).getLayerEntries(); + ImmutableList extraFilesLayerEntries = + fakeLayerConfigurations.get(4).getLayerEntries(); // Verifies that the cached layers are up-to-date. CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntries).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntry).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(snapshotDependenciesLayerEntries) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(2).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntries).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(3).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(snapshotDependenciesLayerEntry) - .getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntries).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(4).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(extraFilesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(extraFilesLayerEntries).getBlobDescriptor()); // Verifies that the cache reader gets the same layers as the newest application layers. Assert.assertEquals( applicationLayers.get(0).getContentFile(), - cacheReader.getLayerFile(dependenciesLayerEntry)); + cacheReader.getLayerFile(dependenciesLayerEntries)); Assert.assertEquals( - applicationLayers.get(1).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntry)); + applicationLayers.get(1).getContentFile(), + cacheReader.getLayerFile(snapshotDependenciesLayerEntries)); Assert.assertEquals( - applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(classesLayerEntry)); + applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntries)); Assert.assertEquals( - applicationLayers.get(3).getContentFile(), - cacheReader.getLayerFile(snapshotDependenciesLayerEntry)); + applicationLayers.get(3).getContentFile(), cacheReader.getLayerFile(classesLayerEntries)); Assert.assertEquals( - applicationLayers.get(4).getContentFile(), cacheReader.getLayerFile(extraFilesLayerEntry)); + applicationLayers.get(4).getContentFile(), + cacheReader.getLayerFile(extraFilesLayerEntries)); } @Test public void testRun_emptyLayersIgnored() - throws IOException, URISyntaxException, CacheMetadataCorruptedException, ExecutionException { - - TestSourceFilesConfiguration testSourceFilesConfiguration = - TestSourceFilesConfiguration.builder() - .withDependencies() - .withClasses() - .withResources() - .build(); - - // Populate the cache - ImageLayers applicationLayers = - configureAvailableLayers(testSourceFilesConfiguration); + throws IOException, CacheMetadataCorruptedException, ExecutionException { + ImmutableList fakeLayerConfigurations = + ImmutableList.of( + fakeDependenciesLayerConfiguration, + emptyLayerConfiguration, + fakeResourcesLayerConfiguration, + fakeClassesLayerConfiguration, + emptyLayerConfiguration); + Mockito.when(mockBuildConfiguration.getLayerConfigurations()) + .thenReturn(fakeLayerConfigurations); + + // Populates the cache. + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(3, applicationLayers.size()); - ImmutableList dependenciesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getDependenciesFiles(), - testSourceFilesConfiguration.getDependenciesPathOnImage())); - ImmutableList resourcesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getResourcesFiles(), - testSourceFilesConfiguration.getResourcesPathOnImage())); - ImmutableList classesLayerEntry = - ImmutableList.of( - new LayerEntry( - testSourceFilesConfiguration.getClassesFiles(), - testSourceFilesConfiguration.getClassesPathOnImage())); + ImmutableList dependenciesLayerEntries = + fakeLayerConfigurations.get(0).getLayerEntries(); + ImmutableList resourcesLayerEntries = + fakeLayerConfigurations.get(2).getLayerEntries(); + ImmutableList classesLayerEntries = + fakeLayerConfigurations.get(3).getLayerEntries(); // Re-initialize cache with the updated metadata. Cache cache = Cache.init(temporaryCacheDirectory); @@ -224,21 +228,23 @@ public void testRun_emptyLayersIgnored() CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntries).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntries).getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(2).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntry).getBlobDescriptor()); + cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntries).getBlobDescriptor()); // Verifies that the cache reader gets the same layers as the newest application layers. Assert.assertEquals( applicationLayers.get(0).getContentFile(), - cacheReader.getLayerFile(dependenciesLayerEntry)); + cacheReader.getLayerFile(fakeLayerConfigurations.get(0).getLayerEntries())); Assert.assertEquals( - applicationLayers.get(1).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntry)); + applicationLayers.get(1).getContentFile(), + cacheReader.getLayerFile(fakeLayerConfigurations.get(2).getLayerEntries())); Assert.assertEquals( - applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(classesLayerEntry)); + applicationLayers.get(2).getContentFile(), + cacheReader.getLayerFile(fakeLayerConfigurations.get(3).getLayerEntries())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index ea865b518c..e9f1000a38 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -68,6 +68,7 @@ public void setUp() throws DigestException { Mockito.when(mockBuildConfiguration.getEnvironment()).thenReturn(ImmutableMap.of()); Mockito.when(mockBuildConfiguration.getJavaArguments()).thenReturn(ImmutableList.of()); Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); + Mockito.when(mockBuildConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) .thenReturn( @@ -92,8 +93,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup ImmutableList.of( mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep), - ImmutableList.of()); + mockBuildAndCacheApplicationLayerStep)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java index 8dc223fba9..15ba372345 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; @@ -30,20 +30,22 @@ import java.util.Arrays; import java.util.Deque; import java.util.List; +import java.util.stream.Stream; import org.junit.Assert; -import org.junit.Before; 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.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link DockerContextGenerator}. */ @RunWith(MockitoJUnitRunner.class) public class DockerContextGeneratorTest { + private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; + private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; + private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; + private static void assertSameFiles(Path directory1, Path directory2) throws IOException { Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); @@ -58,24 +60,14 @@ private static void assertSameFiles(Path directory1, Path directory2) throws IOE Assert.assertEquals(0, directory1Paths.size()); } - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; - - @Before - public void setUpMocks() { - String expectedDependenciesPath = "/app/libs/"; - String expectedResourcesPath = "/app/resources/"; - String expectedClassesPath = "/app/classes/"; - - Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) - .thenReturn(expectedDependenciesPath); - Mockito.when(mockSourceFilesConfiguration.getResourcesPathOnImage()) - .thenReturn(expectedResourcesPath); - Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) - .thenReturn(expectedClassesPath); + private static ImmutableList listFilesInDirectory(Path directory) throws IOException { + try (Stream files = Files.list(directory)) { + return files.collect(ImmutableList.toImmutableList()); + } } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test public void testGenerate() throws IOException, URISyntaxException { Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); @@ -83,21 +75,14 @@ public void testGenerate() throws IOException, URISyntaxException { Paths.get(Resources.getResource("application/snapshot-dependencies").toURI()); Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); + Path testExtraFiles = Paths.get(Resources.getResource("layer").toURI()); - ImmutableList expectedDependenciesFiles = - new DirectoryWalker(testDependencies).filterRoot().walk(); + ImmutableList expectedDependenciesFiles = listFilesInDirectory(testDependencies); ImmutableList expectedSnapshotDependenciesFiles = - new DirectoryWalker(testSnapshotDependencies).filterRoot().walk(); - ImmutableList expectedResourcesFiles = - new DirectoryWalker(testResources).filterRoot().walk(); - ImmutableList expectedClassesFiles = new DirectoryWalker(testClasses).filterRoot().walk(); - Mockito.when(mockSourceFilesConfiguration.getDependenciesFiles()) - .thenReturn(expectedDependenciesFiles); - Mockito.when(mockSourceFilesConfiguration.getSnapshotDependenciesFiles()) - .thenReturn(expectedSnapshotDependenciesFiles); - Mockito.when(mockSourceFilesConfiguration.getResourcesFiles()) - .thenReturn(expectedResourcesFiles); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(expectedClassesFiles); + listFilesInDirectory(testSnapshotDependencies); + ImmutableList expectedResourcesFiles = listFilesInDirectory(testResources); + ImmutableList expectedClassesFiles = listFilesInDirectory(testClasses); + ImmutableList expectedExtraFiles = listFilesInDirectory(testExtraFiles); Path targetDirectory = temporaryFolder.newFolder().toPath(); @@ -107,7 +92,12 @@ public void testGenerate() throws IOException, URISyntaxException { */ Files.delete(targetDirectory); - new DockerContextGenerator(mockSourceFilesConfiguration) + new DockerContextGenerator( + new LayerEntry(expectedDependenciesFiles, EXPECTED_DEPENDENCIES_PATH), + new LayerEntry(expectedSnapshotDependenciesFiles, EXPECTED_DEPENDENCIES_PATH), + new LayerEntry(expectedResourcesFiles, EXPECTED_RESOURCES_PATH), + new LayerEntry(expectedClassesFiles, EXPECTED_CLASSES_PATH), + new LayerEntry(expectedExtraFiles, "/")) .setBaseImage("somebaseimage") .generate(targetDirectory); @@ -116,6 +106,7 @@ public void testGenerate() throws IOException, URISyntaxException { assertSameFiles(targetDirectory.resolve("snapshot-libs"), testSnapshotDependencies); assertSameFiles(targetDirectory.resolve("resources"), testResources); assertSameFiles(targetDirectory.resolve("classes"), testClasses); + assertSameFiles(targetDirectory.resolve("root"), testExtraFiles); } @Test @@ -127,7 +118,12 @@ public void testMakeDockerfile() throws IOException { List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); String dockerfile = - new DockerContextGenerator(mockSourceFilesConfiguration) + new DockerContextGenerator( + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH), + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH), + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_RESOURCES_PATH), + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_CLASSES_PATH), + new LayerEntry(ImmutableList.of(Paths.get("ignored")), "/")) .setBaseImage(expectedBaseImage) .setJvmFlags(expectedJvmFlags) .setMainClass(expectedMainClass) @@ -138,8 +134,6 @@ public void testMakeDockerfile() throws IOException { // Need to split/rejoin the string here to avoid cross-platform troubles List sampleDockerfile = Resources.readLines(Resources.getResource("sampleDockerfile"), StandardCharsets.UTF_8); - Assert.assertArrayEquals( - String.join("\n", sampleDockerfile).getBytes(StandardCharsets.UTF_8), - dockerfile.getBytes(StandardCharsets.UTF_8)); + Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index 946c14074a..f80fbc7ac9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -21,11 +21,11 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; @@ -62,7 +62,6 @@ public class BuildStepsRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildSteps mockBuildSteps; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; @Mock private BuildLogger mockBuildLogger; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @@ -78,12 +77,12 @@ public void setUpMocks() { Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockBuildSteps.getSourceFilesConfiguration()) - .thenReturn(mockSourceFilesConfiguration); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); - Mockito.when(mockSourceFilesConfiguration.getResourcesFiles()).thenReturn(ImmutableList.of()); - Mockito.when(mockSourceFilesConfiguration.getDependenciesFiles()) - .thenReturn(ImmutableList.of()); + Mockito.when(mockBuildConfiguration.getLayerConfigurations()) + .thenReturn( + ImmutableList.of( + LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build(), + LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build(), + LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build())); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java similarity index 60% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index f6da756da0..3159645b9b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/EntrypointBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -14,35 +14,29 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib.frontend; import java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; -/** Tests for {@link EntrypointBuilder}. */ -public class EntrypointBuilderTest { +/** Tests for {@link JavaEntrypointConstructor}. */ +public class JavaEntrypointConstructorTest { @Test public void testMakeEntrypoint() { - String expectedDependenciesPath = "/app/libs/"; + String expectedDependenciesPath = "/app/libs/*"; String expectedResourcesPath = "/app/resources/"; String expectedClassesPath = "/app/classes/"; List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag"); String expectedMainClass = "SomeMainClass"; - SourceFilesConfiguration mockSourceFilesConfiguration = - Mockito.mock(SourceFilesConfiguration.class); - - Mockito.when(mockSourceFilesConfiguration.getDependenciesPathOnImage()) - .thenReturn(expectedDependenciesPath); - Mockito.when(mockSourceFilesConfiguration.getResourcesPathOnImage()) - .thenReturn(expectedResourcesPath); - Mockito.when(mockSourceFilesConfiguration.getClassesPathOnImage()) - .thenReturn(expectedClassesPath); - + List entrypoint = + JavaEntrypointConstructor.makeEntrypoint( + Arrays.asList(expectedDependenciesPath, expectedResourcesPath, expectedClassesPath), + expectedJvmFlags, + expectedMainClass); Assert.assertEquals( Arrays.asList( "java", @@ -51,7 +45,11 @@ public void testMakeEntrypoint() { "-cp", "/app/libs/*:/app/resources/:/app/classes/", "SomeMainClass"), - EntrypointBuilder.makeEntrypoint( - mockSourceFilesConfiguration, expectedJvmFlags, expectedMainClass)); + entrypoint); + + // Checks that this is also the default entrypoint. + Assert.assertEquals( + JavaEntrypointConstructor.makeDefaultEntrypoint(expectedJvmFlags, expectedMainClass), + entrypoint); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 3b2d5cc7b6..29cde64bbf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; @@ -39,7 +39,6 @@ public class MainClassFinderTest { @Mock private BuildLogger mockBuildLogger; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; @Mock private ProjectProperties mockProjectProperties; @Mock private HelpfulSuggestions mockHelpfulSuggestions; @@ -49,8 +48,6 @@ public class MainClassFinderTest { public void setup() { Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); - Mockito.when(mockProjectProperties.getSourceFilesConfiguration()) - .thenReturn(mockSourceFilesConfiguration); Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) .thenReturn(mockHelpfulSuggestions); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); @@ -136,7 +133,8 @@ public void testResolveMainClass() throws MainClassInferenceException { @Test public void testResolveMainClass_notValid() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(fakeClassesPath); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(fakeClassesPath, "ignored")); Assert.assertEquals( "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -146,14 +144,18 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { public void testResolveMainClass_multipleInferredWithBackup() throws MainClassInferenceException, URISyntaxException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()) + Mockito.when(mockProjectProperties.getClassesLayerEntry()) .thenReturn( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class").toURI()))); + new LayerEntry( + ImmutableList.of( + Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/HelloWorld.class") + .toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/NotMain.class") + .toURI())), + "ignored")); Assert.assertEquals( "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -162,14 +164,18 @@ public void testResolveMainClass_multipleInferredWithBackup() @Test public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()) + Mockito.when(mockProjectProperties.getClassesLayerEntry()) .thenReturn( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class").toURI()))); + new LayerEntry( + ImmutableList.of( + Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/HelloWorld.class") + .toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/NotMain.class") + .toURI())), + "ignored")); try { MainClassFinder.resolveMainClass(null, mockProjectProperties); Assert.fail(); @@ -183,7 +189,8 @@ public void testResolveMainClass_multipleInferredWithoutBackup() throws URISynta @Test public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); Assert.assertEquals( "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -191,7 +198,8 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere @Test public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(ImmutableList.of()); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); try { MainClassFinder.resolveMainClass(null, mockProjectProperties); Assert.fail(); diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 474ea0c4e2..5dde515f99 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -4,6 +4,7 @@ COPY libs /app/libs/ COPY snapshot-libs /app/libs/ COPY resources /app/resources/ COPY classes /app/classes/ +COPY root / EXPOSE 1000/tcp EXPOSE 2000-2010/udp diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 096f846de8..009e3327ab 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,10 +5,11 @@ All notable changes to this project will be documented in this file. ### Added -### Changed +- Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -### Fixed +### Changed +- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) - Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) ## 0.9.7 @@ -20,6 +21,7 @@ All notable changes to this project will be documented in this file. - `container.useCurrentTimestamp` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) - Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) - HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) +- Docker context export command-line option `--targetDir` to `--jibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index f9df14cbd8..75380d0085 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -246,7 +246,7 @@ public void testDockerContext() throws IOException, InterruptedException { + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageName).run()); + "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); // Checks that generating the Docker context again is skipped. BuildTask upToDateJibDockerContextTask = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index c067a20134..24831b5535 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -19,12 +19,12 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; @@ -32,12 +32,7 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -79,7 +74,7 @@ public void setTargetImage(String targetImage) { } @TaskAction - public void buildDocker() throws InvalidImageReferenceException, IOException { + public void buildDocker() throws InvalidImageReferenceException { if (!new DockerClient().isDockerInstalled()) { throw new GradleException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); } @@ -102,7 +97,8 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { } GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); + GradleProjectProperties.getForProject( + getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); @@ -115,20 +111,13 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { .setTargetImage(targetImage) .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setMainClass(mainClass) .setJavaArguments(jibExtension.getArgs()) - .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); - if (Files.exists(jibExtension.getExtraDirectory().toPath())) { - try (Stream extraFilesLayerDirectoryFiles = - Files.list(jibExtension.getExtraDirectory().toPath())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - } - } + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -152,9 +141,7 @@ public void buildDocker() throws InvalidImageReferenceException, IOException { // Uses a directory in the Gradle build cache as the Jib cache. try { - BuildStepsRunner.forBuildToDockerDaemon( - buildConfiguration, gradleProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { throw new GradleException(ex.getMessage(), ex.getCause()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 4c958ce22e..1a4f114b83 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; @@ -32,12 +32,7 @@ import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -79,7 +74,7 @@ public void setTargetImage(String targetImage) { } @TaskAction - public void buildImage() throws InvalidImageReferenceException, IOException { + public void buildImage() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); @@ -110,7 +105,8 @@ public void buildImage() throws InvalidImageReferenceException, IOException { } GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); + GradleProjectProperties.getForProject( + getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); // Builds the BuildConfiguration. @@ -122,21 +118,14 @@ public void buildImage() throws InvalidImageReferenceException, IOException { .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) .setTargetImageCredentialHelperName(jibExtension.getTo().getCredHelper()) .setKnownTargetRegistryCredentials(knownTargetRegistryCredentials) - .setMainClass(mainClass) .setJavaArguments(jibExtension.getArgs()) - .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) .setTargetFormat(jibExtension.getFormat()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); - if (Files.exists(jibExtension.getExtraDirectory().toPath())) { - try (Stream extraFilesLayerDirectoryFiles = - Files.list(jibExtension.getExtraDirectory().toPath())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - } - } + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -159,9 +148,7 @@ public void buildImage() throws InvalidImageReferenceException, IOException { RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); try { - BuildStepsRunner.forBuildImage( - buildConfiguration, gradleProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { throw new GradleException(ex.getMessage(), ex.getCause()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index f9b1c6f8ae..7cac24b230 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; @@ -31,13 +31,8 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -111,7 +106,7 @@ private String getTargetPath() { } @TaskAction - public void buildTar() throws InvalidImageReferenceException, IOException { + public void buildTar() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); @@ -130,7 +125,8 @@ public void buildTar() throws InvalidImageReferenceException, IOException { } GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); + GradleProjectProperties.getForProject( + getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); @@ -143,20 +139,13 @@ public void buildTar() throws InvalidImageReferenceException, IOException { .setTargetImage(targetImage) .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setMainClass(mainClass) .setJavaArguments(jibExtension.getArgs()) - .setJvmFlags(jibExtension.getJvmFlags()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()); - if (Files.exists(jibExtension.getExtraDirectory().toPath())) { - try (Stream extraFilesLayerDirectoryFiles = - Files.list(jibExtension.getExtraDirectory().toPath())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - } - } + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -180,10 +169,7 @@ public void buildTar() throws InvalidImageReferenceException, IOException { // Uses a directory in the Gradle build cache as the Jib cache. try { - BuildStepsRunner.forBuildTar( - Paths.get(getTargetPath()), - buildConfiguration, - gradleProjectProperties.getSourceFilesConfiguration()) + BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) .build(HELPFUL_SUGGESTIONS); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 2e10cb4521..a5effd5953 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -104,7 +104,8 @@ public void generateDockerContext() { SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject(getProject(), gradleBuildLogger); + GradleProjectProperties.getForProject( + getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); String targetDir = getTargetDir(); @@ -113,8 +114,12 @@ public void generateDockerContext() { // here. ExposedPortsParser.parse(jibExtension.getExposedPorts()); - // TODO: Add support for extra files layer. - new DockerContextGenerator(gradleProjectProperties.getSourceFilesConfiguration()) + new DockerContextGenerator( + gradleProjectProperties.getDependenciesLayerEntry(), + gradleProjectProperties.getSnapshotDependenciesLayerEntry(), + gradleProjectProperties.getResourcesLayerEntry(), + gradleProjectProperties.getClassesLayerEntry(), + gradleProjectProperties.getExtraFilesLayerEntry()) .setBaseImage(jibExtension.getBaseImage()) .setJvmFlags(jibExtension.getJvmFlags()) .setMainClass(mainClass) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java new file mode 100644 index 0000000000..641f285426 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -0,0 +1,203 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; + +/** Builds {@link LayerConfiguration}s based on inputs from a {@link Project}. */ +class GradleLayerConfigurations { + + /** Name of the `main` {@link SourceSet} to use as source files. */ + private static final String MAIN_SOURCE_SET_NAME = "main"; + + private static final String DEPENDENCIES_LAYER_LABEL = "dependencies"; + private static final String SNAPSHOT_DEPENDENCIES_LAYER_LABEL = "snapshot dependencies"; + private static final String RESOURCES_LAYER_LABEL = "resources"; + private static final String CLASSES_LAYER_LABEL = "classes"; + private static final String EXTRA_FILES_LAYER_LABEL = "extra files"; + + /** + * Resolves the source files configuration for a Gradle {@link Project}. + * + * @param project the Gradle {@link Project} + * @param gradleBuildLogger the build logger for providing feedback about the resolution + * @param extraDirectory path to the directory for the extra files layer + * @return a {@link GradleLayerConfigurations} for building the layers for the Gradle {@link + * Project} + * @throws IOException if an I/O exception occurred during resolution + */ + static GradleLayerConfigurations getForProject( + Project project, GradleBuildLogger gradleBuildLogger, Path extraDirectory) + throws IOException { + JavaPluginConvention javaPluginConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + + SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); + + List dependenciesFiles = new ArrayList<>(); + List snapshotDependenciesFiles = new ArrayList<>(); + List resourcesFiles = new ArrayList<>(); + List classesFiles = new ArrayList<>(); + List extraFiles = new ArrayList<>(); + + // Adds each file in each classes output directory to the classes files list. + FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); + for (File classesOutputDirectory : classesOutputDirectories) { + if (Files.notExists(classesOutputDirectory.toPath())) { + // Warns that output directory was not found. + gradleBuildLogger.warn( + "Could not find build output directory '" + classesOutputDirectory + "'"); + continue; + } + try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { + classFileStream.forEach(classesFiles::add); + } + } + if (classesFiles.isEmpty()) { + gradleBuildLogger.warn("No classes files were found - did you compile your project?"); + } + + // Adds each file in the resources output directory to the resources files list. + Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); + if (Files.exists(resourcesOutputDirectory)) { + try (Stream resourceFileStream = Files.list(resourcesOutputDirectory)) { + resourceFileStream.forEach(resourcesFiles::add); + } + } + + // Adds all other files to the dependencies files list. + FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); + // Removes the classes output directories. + allFiles = allFiles.minus(classesOutputDirectories); + for (File dependencyFile : allFiles) { + // Removes the resources output directory. + if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { + continue; + } + if (dependencyFile.getName().contains("SNAPSHOT")) { + snapshotDependenciesFiles.add(dependencyFile.toPath()); + } else { + dependenciesFiles.add(dependencyFile.toPath()); + } + } + + // Adds all the extra files. + if (Files.exists(extraDirectory)) { + try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { + extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); + } + } + + // Sorts all files by path for consistent ordering. + Collections.sort(dependenciesFiles); + Collections.sort(snapshotDependenciesFiles); + Collections.sort(resourcesFiles); + Collections.sort(classesFiles); + Collections.sort(extraFiles); + + return new GradleLayerConfigurations( + LayerConfiguration.builder() + .addEntry( + dependenciesFiles, JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .setLabel(DEPENDENCIES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry( + snapshotDependenciesFiles, + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .setLabel(SNAPSHOT_DEPENDENCIES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(resourcesFiles, JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE) + .setLabel(RESOURCES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(classesFiles, JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE) + .setLabel(CLASSES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(extraFiles, "/") + .setLabel(EXTRA_FILES_LAYER_LABEL) + .build()); + } + + private final LayerConfiguration dependenciesLayerConfiguration; + private final LayerConfiguration snapshotDependenciesLayerConfiguration; + private final LayerConfiguration resourcesLayerConfiguration; + private final LayerConfiguration classesLayerConfiguration; + private final LayerConfiguration extraFilesLayerConfiguration; + + // TODO: Consolidate with MavenLayerConfigurations. + /** Instantiate with {@link #getForProject}. */ + private GradleLayerConfigurations( + LayerConfiguration dependenciesLayerConfiguration, + LayerConfiguration snapshotDependenciesLayerConfiguration, + LayerConfiguration resourcesLayerConfiguration, + LayerConfiguration classesLayerConfiguration, + LayerConfiguration extraFilesLayerConfiguration) { + this.dependenciesLayerConfiguration = dependenciesLayerConfiguration; + this.snapshotDependenciesLayerConfiguration = snapshotDependenciesLayerConfiguration; + this.resourcesLayerConfiguration = resourcesLayerConfiguration; + this.classesLayerConfiguration = classesLayerConfiguration; + this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; + } + + ImmutableList getLayerConfigurations() { + return ImmutableList.of( + dependenciesLayerConfiguration, + snapshotDependenciesLayerConfiguration, + resourcesLayerConfiguration, + classesLayerConfiguration, + extraFilesLayerConfiguration); + } + + LayerEntry getDependenciesLayerEntry() { + return dependenciesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getSnapshotDependenciesLayerEntry() { + return snapshotDependenciesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getResourcesLayerEntry() { + return resourcesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getClassesLayerEntry() { + return classesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getExtraFilesLayerEntry() { + return extraFilesLayerConfiguration.getLayerEntries().get(0); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 259ac8f3f0..f93b2ea204 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -17,16 +17,18 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -49,12 +51,12 @@ class GradleProjectProperties implements ProjectProperties { /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, GradleBuildLogger gradleBuildLogger) { + Project project, GradleBuildLogger gradleBuildLogger, Path extraDirectory) { try { return new GradleProjectProperties( project, gradleBuildLogger, - GradleSourceFilesConfiguration.getForProject(project, gradleBuildLogger)); + GradleLayerConfigurations.getForProject(project, gradleBuildLogger, extraDirectory)); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); @@ -63,21 +65,46 @@ static GradleProjectProperties getForProject( private final Project project; private final GradleBuildLogger gradleBuildLogger; - private final SourceFilesConfiguration sourceFilesConfiguration; + private final GradleLayerConfigurations gradleLayerConfigurations; @VisibleForTesting GradleProjectProperties( Project project, GradleBuildLogger gradleBuildLogger, - SourceFilesConfiguration sourceFilesConfiguration) { + GradleLayerConfigurations gradleLayerConfigurations) { this.project = project; this.gradleBuildLogger = gradleBuildLogger; - this.sourceFilesConfiguration = sourceFilesConfiguration; + this.gradleLayerConfigurations = gradleLayerConfigurations; } @Override - public SourceFilesConfiguration getSourceFilesConfiguration() { - return sourceFilesConfiguration; + public ImmutableList getLayerConfigurations() { + return gradleLayerConfigurations.getLayerConfigurations(); + } + + @Override + public LayerEntry getDependenciesLayerEntry() { + return gradleLayerConfigurations.getDependenciesLayerEntry(); + } + + @Override + public LayerEntry getSnapshotDependenciesLayerEntry() { + return gradleLayerConfigurations.getSnapshotDependenciesLayerEntry(); + } + + @Override + public LayerEntry getResourcesLayerEntry() { + return gradleLayerConfigurations.getResourcesLayerEntry(); + } + + @Override + public LayerEntry getClassesLayerEntry() { + return gradleLayerConfigurations.getClassesLayerEntry(); + } + + @Override + public LayerEntry getExtraFilesLayerEntry() { + return gradleLayerConfigurations.getExtraFilesLayerEntry(); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java deleted file mode 100644 index 56397b79b9..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfiguration.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2018 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.common.collect.ImmutableList; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.SourceSet; - -/** {@link SourceFilesConfiguration} implementation based on inputs from a {@link Project}. */ -class GradleSourceFilesConfiguration implements SourceFilesConfiguration { - - /** Name of the `main` {@link SourceSet} to use as source files. */ - private static final String MAIN_SOURCE_SET_NAME = "main"; - - /** Resolves the source files configuration for a Gradle {@link Project}. */ - static GradleSourceFilesConfiguration getForProject( - Project project, GradleBuildLogger gradleBuildLogger) throws IOException { - return new GradleSourceFilesConfiguration(project, gradleBuildLogger); - } - - private final ImmutableList dependenciesFiles; - private final ImmutableList snapshotDependenciesFiles; - private final ImmutableList resourcesFiles; - private final ImmutableList classesFiles; - - /** Instantiate with {@link #getForProject}. */ - private GradleSourceFilesConfiguration(Project project, GradleBuildLogger gradleBuildLogger) - throws IOException { - JavaPluginConvention javaPluginConvention = - project.getConvention().getPlugin(JavaPluginConvention.class); - - SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); - - List dependenciesFiles = new ArrayList<>(); - List snapshotDependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); - List classesFiles = new ArrayList<>(); - - // Adds each file in each classes output directory to the classes files list. - FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); - for (File classesOutputDirectory : classesOutputDirectories) { - if (Files.notExists(classesOutputDirectory.toPath())) { - // Warns that output directory was not found. - gradleBuildLogger.warn( - "Could not find build output directory '" + classesOutputDirectory + "'"); - continue; - } - try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { - classFileStream.forEach(classesFiles::add); - } - } - if (classesFiles.isEmpty()) { - gradleBuildLogger.warn("No classes files were found - did you compile your project?"); - } - - // Adds each file in the resources output directory to the resources files list. - Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); - if (Files.exists(resourcesOutputDirectory)) { - try (Stream resourceFileStream = Files.list(resourcesOutputDirectory)) { - resourceFileStream.forEach(resourcesFiles::add); - } - } - - // Adds all other files to the dependencies files list. - FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); - // Removes the classes output directories. - allFiles = allFiles.minus(classesOutputDirectories); - for (File dependencyFile : allFiles) { - // Removes the resources output directory. - if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { - continue; - } - if (dependencyFile.getName().contains("SNAPSHOT")) { - snapshotDependenciesFiles.add(dependencyFile.toPath()); - } else { - dependenciesFiles.add(dependencyFile.toPath()); - } - } - - // Sorts all files by path for consistent ordering. - this.dependenciesFiles = ImmutableList.sortedCopyOf(dependenciesFiles); - this.snapshotDependenciesFiles = ImmutableList.sortedCopyOf(snapshotDependenciesFiles); - this.resourcesFiles = ImmutableList.sortedCopyOf(resourcesFiles); - this.classesFiles = ImmutableList.sortedCopyOf(classesFiles); - } - - @Override - public ImmutableList getDependenciesFiles() { - return dependenciesFiles; - } - - @Override - public ImmutableList getSnapshotDependenciesFiles() { - return snapshotDependenciesFiles; - } - - @Override - public ImmutableList getResourcesFiles() { - return resourcesFiles; - } - - @Override - public ImmutableList getClassesFiles() { - return classesFiles; - } - - @Override - public String getDependenciesPathOnImage() { - return DEFAULT_DEPENDENCIES_PATH_ON_IMAGE; - } - - @Override - public String getResourcesPathOnImage() { - return DEFAULT_RESOURCES_PATH_ON_IMAGE; - } - - @Override - public String getClassesPathOnImage() { - return DEFAULT_CLASSES_PATH_ON_IMAGE; - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java similarity index 72% rename from jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index cd70617596..56d372eff3 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleSourceFilesConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -42,9 +42,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Test for {@link GradleSourceFilesConfiguration}. */ +/** Test for {@link GradleLayerConfigurations}. */ @RunWith(MockitoJUnitRunner.class) -public class GradleSourceFilesConfigurationTest { +public class GradleLayerConfigurationsTest { /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ private static class TestFileCollection extends AbstractFileCollection { @@ -74,7 +74,7 @@ public Set getFiles() { @Mock private SourceSetOutput mockMainSourceSetOutput; @Mock private GradleBuildLogger mockGradleBuildLogger; - private GradleSourceFilesConfiguration testGradleSourceFilesConfiguration; + private GradleLayerConfigurations testGradleLayerConfigurations; @Before public void setUp() throws URISyntaxException, IOException { @@ -110,8 +110,9 @@ public void setUp() throws URISyntaxException, IOException { Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - testGradleSourceFilesConfiguration = - GradleSourceFilesConfiguration.getForProject(mockProject, mockGradleBuildLogger); + testGradleLayerConfigurations = + GradleLayerConfigurations.getForProject( + mockProject, mockGradleBuildLogger, Paths.get("nonexistent/path")); } @Test @@ -137,15 +138,23 @@ public void test_correctFiles() throws URISyntaxException { Paths.get(Resources.getResource("application/classes").toURI()) .resolve("HelloWorld.class"), Paths.get(Resources.getResource("application/classes").toURI()).resolve("some.class")); + ImmutableList expectedExtraFiles = ImmutableList.of(); Assert.assertEquals( - expectedDependenciesFiles, testGradleSourceFilesConfiguration.getDependenciesFiles()); + expectedDependenciesFiles, + testGradleLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( expectedSnapshotDependenciesFiles, - testGradleSourceFilesConfiguration.getSnapshotDependenciesFiles()); + testGradleLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedResourcesFiles, testGradleSourceFilesConfiguration.getResourcesFiles()); - Assert.assertEquals(expectedClassesFiles, testGradleSourceFilesConfiguration.getClassesFiles()); + expectedResourcesFiles, + testGradleLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + Assert.assertEquals( + expectedClassesFiles, + testGradleLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + Assert.assertEquals( + expectedExtraFiles, + testGradleLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); } @Test @@ -154,8 +163,9 @@ public void test_noClassesFiles() throws IOException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - testGradleSourceFilesConfiguration = - GradleSourceFilesConfiguration.getForProject(mockProject, mockGradleBuildLogger); + testGradleLayerConfigurations = + GradleLayerConfigurations.getForProject( + mockProject, mockGradleBuildLogger, Paths.get("nonexistent/path")); Mockito.verify(mockGradleBuildLogger) .warn("Could not find build output directory '" + nonexistentFile + "'"); @@ -163,13 +173,39 @@ public void test_noClassesFiles() throws IOException { .warn("No classes files were found - did you compile your project?"); } + @Test + public void test_extraFiles() throws URISyntaxException, IOException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + testGradleLayerConfigurations = + GradleLayerConfigurations.getForProject( + mockProject, mockGradleBuildLogger, extraFilesDirectory); + + ImmutableList expectedExtraFiles = + ImmutableList.of( + Paths.get(Resources.getResource("layer/a").toURI()), + Paths.get(Resources.getResource("layer/c").toURI()), + Paths.get(Resources.getResource("layer/foo").toURI())); + + Assert.assertEquals( + expectedExtraFiles, + testGradleLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); + } + @Test public void test_correctPathsOnImage() { Assert.assertEquals( - "/app/libs/", testGradleSourceFilesConfiguration.getDependenciesPathOnImage()); + "/app/libs/", + testGradleLayerConfigurations.getDependenciesLayerEntry().getExtractionPath()); + Assert.assertEquals( + "/app/libs/", + testGradleLayerConfigurations.getSnapshotDependenciesLayerEntry().getExtractionPath()); + Assert.assertEquals( + "/app/resources/", + testGradleLayerConfigurations.getResourcesLayerEntry().getExtractionPath()); Assert.assertEquals( - "/app/resources/", testGradleSourceFilesConfiguration.getResourcesPathOnImage()); + "/app/classes/", testGradleLayerConfigurations.getClassesLayerEntry().getExtractionPath()); Assert.assertEquals( - "/app/classes/", testGradleSourceFilesConfiguration.getClassesPathOnImage()); + "/", testGradleLayerConfigurations.getExtraFilesLayerEntry().getExtractionPath()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 4e054165ad..696a46f5d7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; @@ -44,9 +43,9 @@ public class GradleProjectPropertiesTest { @Mock private Jar mockJar2; @Mock private Project mockProject; @Mock private GradleBuildLogger mockGradleBuildLogger; - @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; @Mock private JibExtension mockJibExtension; @Mock private GradleBuildLogger mockBuildLogger; + @Mock private GradleLayerConfigurations mockGradleLayerConfigurations; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -58,7 +57,7 @@ public void setup() { gradleProjectProperties = new GradleProjectProperties( - mockProject, mockGradleBuildLogger, mockSourceFilesConfiguration); + mockProject, mockGradleBuildLogger, mockGradleLayerConfigurations); } @Test @@ -71,7 +70,7 @@ public void testGetMainClassFromJar_success() { @Test public void testGetMainClassFromJar_missing() { Mockito.when(mockProject.getTasksByName("jar", false)).thenReturn(Collections.emptySet()); - Assert.assertEquals(null, gradleProjectProperties.getMainClassFromJar()); + Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); } @Test @@ -79,7 +78,7 @@ public void testGetMainClassFromJar_multiple() { manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); Mockito.when(mockProject.getTasksByName("jar", false)) .thenReturn(ImmutableSet.of(mockJar, mockJar2)); - Assert.assertEquals(null, gradleProjectProperties.getMainClassFromJar()); + Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 956c8fa243..af7ae1f47e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -19,24 +19,19 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -67,7 +62,7 @@ public void execute() throws MojoExecutionException { // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); ImageReference baseImage = parseImageReference(getBaseImage(), "from"); ImageReference targetImage = mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); @@ -94,24 +89,13 @@ public void execute() throws MojoExecutionException { .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) .setTargetImage(targetImage) - .setMainClass(mainClass) .setJavaArguments(getArgs()) - .setJvmFlags(getJvmFlags()) .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) - .setAllowInsecureRegistries(getAllowInsecureRegistries()); - if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - - } catch (IOException ex) { - throw new MojoExecutionException( - "Failed to list directory for extra files: " + getExtraDirectory(), ex); - } - } + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -134,9 +118,7 @@ public void execute() throws MojoExecutionException { RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); try { - BuildStepsRunner.forBuildToDockerDaemon( - buildConfiguration, mavenProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); getLog().info(""); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 68b46468d2..4d88722635 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; @@ -32,13 +32,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; import java.util.Arrays; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -103,7 +98,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { mavenSettingsServerCredentials.retrieve(targetImage.getRegistry()); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); String mainClass = mavenProjectProperties.getMainClass(this); // Builds the BuildConfiguration. @@ -115,25 +110,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setTargetImage(targetImage) .setTargetImageCredentialHelperName(getTargetImageCredentialHelperName()) .setKnownTargetRegistryCredentials(knownTargetRegistryCredentials) - .setMainClass(mainClass) .setJavaArguments(getArgs()) - .setJvmFlags(getJvmFlags()) .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()); - if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - - } catch (IOException ex) { - throw new MojoExecutionException( - "Failed to list directory for extra files: " + getExtraDirectory(), ex); - } - } + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -156,9 +140,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); try { - BuildStepsRunner.forBuildImage( - buildConfiguration, mavenProjectProperties.getSourceFilesConfiguration()) - .build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); getLog().info(""); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 9ea8915265..b5c59fe153 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -19,24 +19,19 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -65,7 +60,7 @@ public void execute() throws MojoExecutionException { // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); ImageReference baseImage = parseImageReference(getBaseImage(), "from"); ImageReference targetImage = mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); @@ -92,24 +87,13 @@ public void execute() throws MojoExecutionException { .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) .setTargetImage(targetImage) - .setMainClass(mainClass) .setJavaArguments(getArgs()) - .setJvmFlags(getJvmFlags()) .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) - .setAllowInsecureRegistries(getAllowInsecureRegistries()); - if (getExtraDirectory() != null && Files.exists(getExtraDirectory())) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(getExtraDirectory())) { - buildConfigurationBuilder.setExtraFilesLayerConfiguration( - LayerConfiguration.builder() - .addEntry(extraFilesLayerDirectoryFiles.collect(Collectors.toList()), "/") - .build()); - - } catch (IOException ex) { - throw new MojoExecutionException( - "Failed to list directory for extra files: " + getExtraDirectory(), ex); - } - } + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -134,8 +118,7 @@ public void execute() throws MojoExecutionException { try { BuildStepsRunner.forBuildTar( Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), - buildConfiguration, - mavenProjectProperties.getSourceFilesConfiguration()) + buildConfiguration) .build(HELPFUL_SUGGESTIONS); getLog().info(""); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 9911661fee..bff24b268a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -26,7 +26,6 @@ import java.nio.file.Paths; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -47,7 +46,7 @@ public class DockerContextMojo extends JibPluginConfiguration { private String targetDir; @Override - public void execute() throws MojoExecutionException, MojoFailureException { + public void execute() throws MojoExecutionException { MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); @@ -55,7 +54,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Preconditions.checkNotNull(targetDir); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger); + MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); String mainClass = mavenProjectProperties.getMainClass(this); try { @@ -63,8 +62,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { // here. ExposedPortsParser.parse(getExposedPorts()); - // TODO: Add support for extra files layer. - new DockerContextGenerator(mavenProjectProperties.getSourceFilesConfiguration()) + new DockerContextGenerator( + mavenProjectProperties.getDependenciesLayerEntry(), + mavenProjectProperties.getSnapshotDependenciesLayerEntry(), + mavenProjectProperties.getResourcesLayerEntry(), + mavenProjectProperties.getClassesLayerEntry(), + mavenProjectProperties.getExtraFilesLayerEntry()) .setBaseImage(getBaseImage()) .setJvmFlags(getJvmFlags()) .setMainClass(mainClass) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 28b16c082c..59dfa41ed1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -231,10 +231,9 @@ boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } - @Nullable Path getExtraDirectory() { // TODO: Should inform user about nonexistent directory if using custom directory. - return Paths.get(extraDirectory); + return Paths.get(Preconditions.checkNotNull(extraDirectory)); } @VisibleForTesting diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java new file mode 100644 index 0000000000..78f3e25411 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -0,0 +1,194 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.project.MavenProject; + +/** Builds {@link LayerConfiguration}s based on inputs from a {@link MavenProject}. */ +class MavenLayerConfigurations { + + private static final String DEPENDENCIES_LAYER_LABEL = "dependencies"; + private static final String SNAPSHOT_DEPENDENCIES_LAYER_LABEL = "snapshot dependencies"; + private static final String RESOURCES_LAYER_LABEL = "resources"; + private static final String CLASSES_LAYER_LABEL = "classes"; + private static final String EXTRA_FILES_LAYER_LABEL = "extra files"; + + /** + * Resolves the source files configuration for a {@link MavenProject}. + * + * @param project the {@link MavenProject} + * @param extraDirectory path to the directory for the extra files layer + * @return a new {@link MavenLayerConfigurations} for the project + * @throws IOException if collecting the project files fails + */ + static MavenLayerConfigurations getForProject(MavenProject project, Path extraDirectory) + throws IOException { + Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory()); + Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); + + List dependenciesFiles = new ArrayList<>(); + List snapshotDependenciesFiles = new ArrayList<>(); + List resourcesFiles = new ArrayList<>(); + List classesFiles = new ArrayList<>(); + List extraFiles = new ArrayList<>(); + + // Gets all the dependencies. + for (Artifact artifact : project.getArtifacts()) { + if (artifact.isSnapshot()) { + snapshotDependenciesFiles.add(artifact.getFile().toPath()); + } else { + dependenciesFiles.add(artifact.getFile().toPath()); + } + } + + // Gets the classes files in the 'classes' output directory. It finds the files that are classes + // files by matching them against the .java source files. All other files are deemed resources. + try (Stream classFileStream = Files.list(classesOutputDirectory)) { + classFileStream.forEach( + classFile -> { + /* + * Adds classFile to classesFiles if it is a .class file or is a directory that also + * exists in the classes source directory; otherwise, adds file to resourcesFiles. + */ + if (Files.isDirectory(classFile) + && Files.exists( + classesSourceDirectory.resolve(classesOutputDirectory.relativize(classFile)))) { + classesFiles.add(classFile); + return; + } + + if (FileSystems.getDefault().getPathMatcher("glob:**.class").matches(classFile)) { + classesFiles.add(classFile); + return; + } + + resourcesFiles.add(classFile); + }); + } + + // Adds all the extra files. + if (Files.exists(extraDirectory)) { + try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { + extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); + + } catch (IOException ex) { + throw new IOException("Failed to list directory for extra files: " + extraDirectory, ex); + } + } + + // Sort all files by path for consistent ordering. + Collections.sort(dependenciesFiles); + Collections.sort(snapshotDependenciesFiles); + Collections.sort(resourcesFiles); + Collections.sort(classesFiles); + Collections.sort(extraFiles); + + return new MavenLayerConfigurations( + LayerConfiguration.builder() + .addEntry( + dependenciesFiles, JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .setLabel(DEPENDENCIES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry( + snapshotDependenciesFiles, + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .setLabel(SNAPSHOT_DEPENDENCIES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(resourcesFiles, JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE) + .setLabel(RESOURCES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(classesFiles, JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE) + .setLabel(CLASSES_LAYER_LABEL) + .build(), + LayerConfiguration.builder() + .addEntry(extraFiles, "/") + .setLabel(EXTRA_FILES_LAYER_LABEL) + .build()); + } + + private final LayerConfiguration dependenciesLayerConfiguration; + private final LayerConfiguration snapshotDependenciesLayerConfiguration; + private final LayerConfiguration resourcesLayerConfiguration; + private final LayerConfiguration classesLayerConfiguration; + private final LayerConfiguration extraFilesLayerConfiguration; + + /** Instantiate with {@link #getForProject}. */ + private MavenLayerConfigurations( + LayerConfiguration dependenciesLayerConfiguration, + LayerConfiguration snapshotDependenciesLayerConfiguration, + LayerConfiguration resourcesLayerConfiguration, + LayerConfiguration classesLayerConfiguration, + LayerConfiguration extraFilesLayerConfiguration) { + this.dependenciesLayerConfiguration = dependenciesLayerConfiguration; + this.snapshotDependenciesLayerConfiguration = snapshotDependenciesLayerConfiguration; + this.resourcesLayerConfiguration = resourcesLayerConfiguration; + this.classesLayerConfiguration = classesLayerConfiguration; + this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; + } + + /** + * Gets the list of {@link LayerConfiguration}s to use to build the container image. + * + * @return the list of {@link LayerConfiguration}s + */ + ImmutableList getLayerConfigurations() { + return ImmutableList.of( + dependenciesLayerConfiguration, + snapshotDependenciesLayerConfiguration, + resourcesLayerConfiguration, + classesLayerConfiguration, + extraFilesLayerConfiguration); + } + + LayerEntry getDependenciesLayerEntry() { + return dependenciesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getSnapshotDependenciesLayerEntry() { + return snapshotDependenciesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getResourcesLayerEntry() { + return resourcesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getClassesLayerEntry() { + return classesLayerConfiguration.getLayerEntries().get(0); + } + + LayerEntry getExtraFilesLayerEntry() { + return extraFilesLayerConfiguration.getLayerEntries().get(0); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 45239fc581..7b2c175cec 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -17,14 +17,16 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -43,14 +45,18 @@ class MavenProjectProperties implements ProjectProperties { /** * @param project the {@link MavenProject} for the plugin. * @param mavenBuildLogger the logger used for printing status messages. + * @param extraDirectory path to the directory for the extra files layer * @return a MavenProjectProperties from the given project and logger. * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, MavenBuildLogger mavenBuildLogger) throws MojoExecutionException { + MavenProject project, MavenBuildLogger mavenBuildLogger, Path extraDirectory) + throws MojoExecutionException { try { return new MavenProjectProperties( - project, mavenBuildLogger, MavenSourceFilesConfiguration.getForProject(project)); + project, + mavenBuildLogger, + MavenLayerConfigurations.getForProject(project, extraDirectory)); } catch (IOException ex) { throw new MojoExecutionException( "Obtaining project build output files failed; make sure you have compiled your project " @@ -62,21 +68,46 @@ static MavenProjectProperties getForProject( private final MavenProject project; private final MavenBuildLogger mavenBuildLogger; - private final SourceFilesConfiguration sourceFilesConfiguration; + private final MavenLayerConfigurations mavenLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, MavenBuildLogger mavenBuildLogger, - SourceFilesConfiguration sourceFilesConfiguration) { + MavenLayerConfigurations mavenLayerConfigurations) { this.project = project; this.mavenBuildLogger = mavenBuildLogger; - this.sourceFilesConfiguration = sourceFilesConfiguration; + this.mavenLayerConfigurations = mavenLayerConfigurations; } @Override - public SourceFilesConfiguration getSourceFilesConfiguration() { - return sourceFilesConfiguration; + public ImmutableList getLayerConfigurations() { + return mavenLayerConfigurations.getLayerConfigurations(); + } + + @Override + public LayerEntry getDependenciesLayerEntry() { + return mavenLayerConfigurations.getDependenciesLayerEntry(); + } + + @Override + public LayerEntry getSnapshotDependenciesLayerEntry() { + return mavenLayerConfigurations.getSnapshotDependenciesLayerEntry(); + } + + @Override + public LayerEntry getResourcesLayerEntry() { + return mavenLayerConfigurations.getResourcesLayerEntry(); + } + + @Override + public LayerEntry getClassesLayerEntry() { + return mavenLayerConfigurations.getClassesLayerEntry(); + } + + @Override + public LayerEntry getExtraFilesLayerEntry() { + return mavenLayerConfigurations.getExtraFilesLayerEntry(); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java deleted file mode 100644 index a3d919eaf5..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfiguration.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import org.apache.maven.artifact.Artifact; -import org.apache.maven.project.MavenProject; - -/** {@link SourceFilesConfiguration} implementation based on inputs from a {@link MavenProject}. */ -class MavenSourceFilesConfiguration implements SourceFilesConfiguration { - - /** - * Resolves the source files configuration for a Maven {@link MavenProject}. - * - * @param project the {@link MavenProject} - * @return a new {@link MavenSourceFilesConfiguration} for the project - * @throws IOException if collecting the project files fails - */ - static MavenSourceFilesConfiguration getForProject(MavenProject project) throws IOException { - return new MavenSourceFilesConfiguration(project); - } - - private final ImmutableList dependenciesFiles; - private final ImmutableList snapshotDependenciesFiles; - private final ImmutableList resourcesFiles; - private final ImmutableList classesFiles; - - /** Instantiate with {@link #getForProject}. */ - private MavenSourceFilesConfiguration(MavenProject project) throws IOException { - Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory()); - Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); - - List dependenciesFiles = new ArrayList<>(); - List snapshotDependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); - List classesFiles = new ArrayList<>(); - - // Gets all the dependencies. - for (Artifact artifact : project.getArtifacts()) { - if (artifact.isSnapshot()) { - snapshotDependenciesFiles.add(artifact.getFile().toPath()); - } else { - dependenciesFiles.add(artifact.getFile().toPath()); - } - } - - // Gets the classes files in the 'classes' output directory. It finds the files that are classes - // files by matching them against the .java source files. All other files are deemed resources. - try (Stream classFileStream = Files.list(classesOutputDirectory)) { - classFileStream.forEach( - classFile -> { - /* - * Adds classFile to classesFiles if it is a .class file or is a directory that also - * exists in the classes source directory; otherwise, adds file to resourcesFiles. - */ - if (Files.isDirectory(classFile) - && Files.exists( - classesSourceDirectory.resolve(classesOutputDirectory.relativize(classFile)))) { - classesFiles.add(classFile); - return; - } - - if (FileSystems.getDefault().getPathMatcher("glob:**.class").matches(classFile)) { - classesFiles.add(classFile); - return; - } - - resourcesFiles.add(classFile); - }); - } - - // Sort all files by path for consistent ordering. - this.dependenciesFiles = ImmutableList.sortedCopyOf(dependenciesFiles); - this.snapshotDependenciesFiles = ImmutableList.sortedCopyOf(snapshotDependenciesFiles); - this.resourcesFiles = ImmutableList.sortedCopyOf(resourcesFiles); - this.classesFiles = ImmutableList.sortedCopyOf(classesFiles); - } - - @Override - public ImmutableList getDependenciesFiles() { - return dependenciesFiles; - } - - @Override - public ImmutableList getSnapshotDependenciesFiles() { - return snapshotDependenciesFiles; - } - - @Override - public ImmutableList getResourcesFiles() { - return resourcesFiles; - } - - @Override - public ImmutableList getClassesFiles() { - return classesFiles; - } - - @Override - public String getDependenciesPathOnImage() { - return DEFAULT_DEPENDENCIES_PATH_ON_IMAGE; - } - - @Override - public String getResourcesPathOnImage() { - return DEFAULT_RESOURCES_PATH_ON_IMAGE; - } - - @Override - public String getClassesPathOnImage() { - return DEFAULT_CLASSES_PATH_ON_IMAGE; - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 8929ea48b9..66b6a7b190 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -62,6 +62,6 @@ public void testExecute() throws VerificationException, IOException, Interrupted + " \"2003/udp\": {}")); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageName).run()); + "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java similarity index 66% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java rename to jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index cecf83e1f2..e9fb8177e7 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSourceFilesConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -26,6 +26,7 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.junit.Assert; import org.junit.Before; @@ -36,19 +37,19 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link MavenSourceFilesConfiguration}. */ +/** Tests for {@link MavenLayerConfigurations}. */ @RunWith(MockitoJUnitRunner.class) -public class MavenSourceFilesConfigurationTest { +public class MavenLayerConfigurationsTest { @Rule public TestRepository testRepository = new TestRepository(); @Mock private MavenProject mockMavenProject; @Mock private Build mockBuild; - private MavenSourceFilesConfiguration testMavenSourceFilesConfiguration; + private MavenLayerConfigurations testMavenLayerConfigurations; @Before - public void setUp() throws IOException, URISyntaxException { + public void setUp() throws IOException, URISyntaxException, MojoExecutionException { Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); @@ -66,8 +67,8 @@ public void setUp() throws IOException, URISyntaxException { testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); - testMavenSourceFilesConfiguration = - MavenSourceFilesConfiguration.getForProject(mockMavenProject); + testMavenLayerConfigurations = + MavenLayerConfigurations.getForProject(mockMavenProject, Paths.get("nonexistent/path")); } @Test @@ -95,22 +96,50 @@ public void test_correctFiles() throws URISyntaxException { Paths.get(Resources.getResource("application/output/some.class").toURI())); Assert.assertEquals( - expectedDependenciesFiles, testMavenSourceFilesConfiguration.getDependenciesFiles()); + expectedDependenciesFiles, + testMavenLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( expectedSnapshotDependenciesFiles, - testMavenSourceFilesConfiguration.getSnapshotDependenciesFiles()); + testMavenLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedResourcesFiles, testMavenSourceFilesConfiguration.getResourcesFiles()); - Assert.assertEquals(expectedClassesFiles, testMavenSourceFilesConfiguration.getClassesFiles()); + expectedResourcesFiles, + testMavenLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + Assert.assertEquals( + expectedClassesFiles, testMavenLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + } + + @Test + public void test_extraFiles() throws URISyntaxException, IOException, MojoExecutionException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + testMavenLayerConfigurations = + MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory); + + ImmutableList expectedExtraFiles = + ImmutableList.of( + Paths.get(Resources.getResource("layer/a").toURI()), + Paths.get(Resources.getResource("layer/c").toURI()), + Paths.get(Resources.getResource("layer/foo").toURI())); + + Assert.assertEquals( + expectedExtraFiles, + testMavenLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); } @Test public void test_correctPathsOnImage() { Assert.assertEquals( - "/app/libs/", testMavenSourceFilesConfiguration.getDependenciesPathOnImage()); + "/app/libs/", testMavenLayerConfigurations.getDependenciesLayerEntry().getExtractionPath()); + Assert.assertEquals( + "/app/libs/", + testMavenLayerConfigurations.getSnapshotDependenciesLayerEntry().getExtractionPath()); + Assert.assertEquals( + "/app/resources/", + testMavenLayerConfigurations.getResourcesLayerEntry().getExtractionPath()); + Assert.assertEquals( + "/app/classes/", testMavenLayerConfigurations.getClassesLayerEntry().getExtractionPath()); Assert.assertEquals( - "/app/resources/", testMavenSourceFilesConfiguration.getResourcesPathOnImage()); - Assert.assertEquals("/app/classes/", testMavenSourceFilesConfiguration.getClassesPathOnImage()); + "/", testMavenLayerConfigurations.getExtraFilesLayerEntry().getExtractionPath()); } private Artifact makeArtifact(Path path) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 8bc97e4fd6..d77341aea9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; @@ -35,7 +34,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; @Mock private MavenBuildLogger mockMavenBuildLogger; - @Mock private SourceFilesConfiguration mockSourcesFilesConfiguration; + @Mock private MavenLayerConfigurations mockMavenLayerConfigurations; @Mock private Plugin mockJarPlugin; @Mock private MavenBuildLogger mockBuildLogger; @@ -52,7 +51,7 @@ public void setup() { Mockito.when(mockMavenProject.getVersion()).thenReturn("project-version"); mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockMavenBuildLogger, mockSourcesFilesConfiguration); + mockMavenProject, mockMavenBuildLogger, mockMavenLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); @@ -80,7 +79,7 @@ public void testGetMainClassFromJar_missingMainClass() { jarPluginConfiguration.addChild(archive); archive.addChild(manifest); - Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); + Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @Test @@ -90,7 +89,7 @@ public void testGetMainClassFromJar_missingManifest() { Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); jarPluginConfiguration.addChild(archive); - Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); + Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @Test @@ -99,7 +98,7 @@ public void testGetMainClassFromJar_missingArchive() { .thenReturn(mockJarPlugin); Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); - Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); + Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @Test @@ -107,12 +106,12 @@ public void testGetMainClassFromJar_missingConfiguration() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) .thenReturn(mockJarPlugin); - Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); + Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @Test public void testGetMainClassFromJar_missingPlugin() { - Assert.assertEquals(null, mavenProjectProperties.getMainClassFromJar()); + Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @Test From 9ebea0e0f33665bab8e8810ec4905c5356cadfa4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 25 Jul 2018 12:57:34 -0400 Subject: [PATCH 0060/2020] Move BuildConfiguration to container package (#715) --- .../cloud/tools/jib/builder/BuildStepsIntegrationTest.java | 1 + .../java/com/google/cloud/tools/jib/builder/BuildSteps.java | 1 + .../cloud/tools/jib/builder/steps/AuthenticatePushStep.java | 2 +- .../builder/steps/BuildAndCacheApplicationLayerStep.java | 2 +- .../cloud/tools/jib/builder/steps/BuildImageStep.java | 2 +- .../cloud/tools/jib/builder/steps/FinalizingStep.java | 2 +- .../cloud/tools/jib/builder/steps/LoadDockerStep.java | 2 +- .../jib/builder/steps/PullAndCacheBaseImageLayerStep.java | 2 +- .../jib/builder/steps/PullAndCacheBaseImageLayersStep.java | 2 +- .../cloud/tools/jib/builder/steps/PullBaseImageStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PushBlobStep.java | 2 +- .../jib/builder/steps/PushContainerConfigurationStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PushImageStep.java | 2 +- .../cloud/tools/jib/builder/steps/PushLayersStep.java | 2 +- .../jib/builder/steps/RetrieveRegistryCredentialsStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/StepsRunner.java | 2 +- .../cloud/tools/jib/builder/steps/WriteTarFileStep.java | 2 +- .../jib/{builder => configuration}/BuildConfiguration.java | 6 ++---- .../google/cloud/tools/jib/frontend/BuildStepsRunner.java | 2 +- .../google/cloud/tools/jib/frontend/MainClassFinder.java | 2 +- .../steps/BuildAndCacheApplicationLayerStepTest.java | 2 +- .../cloud/tools/jib/builder/steps/BuildImageStepTest.java | 2 +- .../builder/steps/RetrieveRegistryCredentialsStepTest.java | 2 +- .../{builder => configuration}/BuildConfigurationTest.java | 6 ++---- .../cloud/tools/jib/frontend/BuildStepsRunnerTest.java | 2 +- .../com/google/cloud/tools/jib/gradle/BuildDockerTask.java | 2 +- .../com/google/cloud/tools/jib/gradle/BuildImageTask.java | 2 +- .../com/google/cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 2 +- .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../java/com/google/cloud/tools/jib/maven/BuildTarMojo.java | 2 +- 31 files changed, 33 insertions(+), 35 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{builder => configuration}/BuildConfiguration.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{builder => configuration}/BuildConfigurationTest.java (98%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index d73112c53f..f824bb4f69 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index f74032c76b..f5e414f483 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.ExecutionException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index cb701726d0..2503d6d432 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index d6ad5a23de..e70a9e60b1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -18,12 +18,12 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CacheWriter; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 4e7273bd79..b2030bd982 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index e5022f676c..17eb0ce79a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 8d4f724a9c..17c5f888be 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.image.Image; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index a9904b5d8a..387738b1d1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -18,11 +18,11 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CacheWriter; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.registry.RegistryClient; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 26eef859b2..c5c926a19e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -19,9 +19,9 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 7f03a0f7ab..b528f6d85a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -20,8 +20,8 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index cad4ee2c60..cc3de6423e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.Futures; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 12529f2f5d..5d3079c630 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -21,8 +21,8 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 82c65a42ef..d818367e9d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 76f139bd4a..f890e9bd06 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index efb23ca520..55102168b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 99b8091a45..ff959b9b31 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,10 +18,10 @@ import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 6520f13a99..237efc2c60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 4d0412c971..3c824072f8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -14,11 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index b45f888f91..2cfbe0573a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -18,7 +18,6 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; @@ -26,6 +25,7 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.cache.Caches.Initializer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 009ae52e68..49c61b02b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index b2eadaf04c..ba7c0c493a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -17,12 +17,12 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.TestBuildLogger; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index e9f1000a38..1ee4181982 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -17,10 +17,10 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index bd2801ebdb..36e84fc516 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 686bc265ef..3af886e7d6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -14,11 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index f80fbc7ac9..9fb9c6306b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -18,12 +18,12 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.registry.InsecureRegistryException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 24831b5535..eda72fb2a1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 1a4f114b83..4da2dce50f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 7cac24b230..2be9a5ff8a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index af7ae1f47e..7648812449 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 4d88722635..f878961c78 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index b5c59fe153..908e321e8b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; From 387da877f5cd34ff86305fc84f662ff7dc44195e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 25 Jul 2018 18:53:31 -0400 Subject: [PATCH 0061/2020] Fix gradle warnings about @Input annotations on java.io.File (#717) --- .../resources/projects/simple/build.gradle | 1 + .../main/{jib => custom-extra-dir}/bar/cat | 0 .../src/main/{jib => custom-extra-dir}/foo | 0 .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 2 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 ++-- .../tools/jib/gradle/DockerContextTask.java | 4 ++-- .../cloud/tools/jib/gradle/JibExtension.java | 20 ++++++++++++++----- 8 files changed, 22 insertions(+), 11 deletions(-) rename jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/{jib => custom-extra-dir}/bar/cat (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/{jib => custom-extra-dir}/foo (100%) diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 3280e0da87..b72d4cf2f7 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -24,6 +24,7 @@ jib { args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] } + extraDirectory = file('src/main/custom-extra-dir') // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/bar/cat b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/bar/cat similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/bar/cat rename to jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/bar/cat diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/foo b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/foo similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/jib/foo rename to jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/foo diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index eda72fb2a1..14690d5f24 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -98,7 +98,7 @@ public void buildDocker() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); + getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 4da2dce50f..8ad605c99b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -106,7 +106,7 @@ public void buildImage() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); + getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); // Builds the BuildConfiguration. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 2be9a5ff8a..7eccbf213a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -83,7 +83,7 @@ public void setTargetImage(String targetImage) { @InputFiles public FileCollection getInputFiles() { return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectory(), getProject()); + Preconditions.checkNotNull(jibExtension).getExtraDirectoryPath().toFile(), getProject()); } /** @@ -126,7 +126,7 @@ public void buildTar() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); + getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index a5effd5953..24ed9907b9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -58,7 +58,7 @@ public JibExtension getJib() { @InputFiles public FileCollection getInputFiles() { return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectory(), getProject()); + Preconditions.checkNotNull(jibExtension).getExtraDirectoryPath().toFile(), getProject()); } /** @@ -105,7 +105,7 @@ public void generateDockerContext() { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectory().toPath()); + getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); String targetDir = getTargetDir(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 75c4b3783b..11a98ea5bb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -77,7 +77,9 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { private final ContainerParameters container; private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; - private final Property extraDirectory; + private final Property extraDirectory; + + private final Path projectDir; // TODO: Deprecated parameters; remove these 4 private final ListProperty jvmFlags; @@ -86,6 +88,7 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { private final Property format; public JibExtension(Project project) { + projectDir = project.getProjectDir().toPath(); ObjectFactory objectFactory = project.getObjects(); from = objectFactory.newInstance(ImageConfiguration.class); @@ -99,7 +102,7 @@ public JibExtension(Project project) { useOnlyProjectCache = objectFactory.property(Boolean.class); allowInsecureRegistries = objectFactory.property(Boolean.class); - extraDirectory = objectFactory.property(File.class); + extraDirectory = objectFactory.property(Path.class); // Sets defaults. from.setImage(DEFAULT_FROM_IMAGE); @@ -107,7 +110,7 @@ public JibExtension(Project project) { args.set(Collections.emptyList()); useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); - extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath()).toFile()); + extraDirectory.set(resolveDefaultExtraDirectory(projectDir)); } /** @@ -186,7 +189,7 @@ public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { } public void setExtraDirectory(File extraDirectory) { - this.extraDirectory.set(extraDirectory); + this.extraDirectory.set(extraDirectory.toPath()); } @Internal @@ -272,7 +275,14 @@ boolean getAllowInsecureRegistries() { } @Input - File getExtraDirectory() { + String getExtraDirectory() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + return extraDirectory.get().toString(); + } + + @Internal + Path getExtraDirectoryPath() { // TODO: Should inform user about nonexistent directory if using custom directory. return extraDirectory.get(); } From 48ef58bf11a0d2893d08450627221b6fda9c0fae Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 25 Jul 2018 19:14:18 -0400 Subject: [PATCH 0062/2020] Support insecure TLS connection (#708) * Support insecure TLS connection * ConnectionFactory --- .../cloud/tools/jib/http/Connection.java | 45 ++++++++++++--- .../cloud/tools/jib/registry/BlobPusher.java | 3 +- .../registry/InsecureRegistryException.java | 5 +- .../jib/registry/RegistryAuthenticator.java | 4 +- .../jib/registry/RegistryEndpointCaller.java | 16 ++++-- .../cloud/tools/jib/http/ConnectionTest.java | 16 +++--- .../cloud/tools/jib/http/MockConnection.java | 6 +- .../cloud/tools/jib/http/TestWebServer.java | 40 ++++++++++++- .../jib/http/WithServerConnectionTest.java | 53 +++++++++++++++--- .../registry/RegistryEndpointCallerTest.java | 37 +++++++----- .../src/test/resources/TestWebServer-keystore | Bin 0 -> 2252 bytes 11 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 jib-core/src/test/resources/TestWebServer-keystore diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 4382f3a68e..8beae8bbcf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -21,14 +21,16 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.Closeable; import java.io.IOException; import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.function.Function; import javax.annotation.Nullable; -import org.apache.http.NoHttpResponseException; /** * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than @@ -46,14 +48,37 @@ public class Connection implements Closeable { /** - * Do not use {@link NetHttpTransport}. It does not process response errors properly. A new {@link - * ApacheHttpTransport} needs to be created for each connection because otherwise HTTP connection - * persistence causes the connection to throw {@link NoHttpResponseException}. + * Returns a factory for {@link Connection}. * - * @see https://github.com/google/google-http-java-client/issues/39 + * @return {@link Connection} factory, a function that generates a {@link Connection} to a URL */ - private HttpRequestFactory requestFactory = new ApacheHttpTransport().createRequestFactory(); + public static Function getConnectionFactory() { + /* + * Do not use {@link NetHttpTransport}. It does not process response errors properly. A new + * {@link ApacheHttpTransport} needs to be created for each connection because otherwise HTTP + * connection persistence causes the connection to throw {@link NoHttpResponseException}. + * + * @see https://github.com/google/google-http-java-client/issues/39 + */ + HttpTransport transport = new ApacheHttpTransport(); + return url -> new Connection(url, transport); + } + + /** + * Returns a factory for {@link Connection} that does not verify TLS peer verification. + * + * @throws GeneralSecurityException if unable to turn off TLS peer verification + * @return {@link Connection} factory, a function that generates a {@link Connection} to a URL + */ + public static Function getInsecureConnectionFactory() + throws GeneralSecurityException { + // Do not use {@link NetHttpTransport}. See {@link getConnectionFactory} for details. + HttpTransport transport = new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); + return url -> new Connection(url, transport); + } + + private HttpRequestFactory requestFactory; @Nullable private HttpResponse httpResponse; @@ -65,8 +90,10 @@ public class Connection implements Closeable { * * @param url the url to send the request to */ - public Connection(URL url) { + @VisibleForTesting + Connection(URL url, HttpTransport transport) { this.url = new GenericUrl(url); + requestFactory = transport.createRequestFactory(); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 8dc6bce671..847a6a62ad 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -18,7 +18,6 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; @@ -70,7 +69,7 @@ public List getAccept() { @Override public URL handleResponse(Response response) throws RegistryErrorException { switch (response.getStatusCode()) { - case HttpStatusCodes.STATUS_CODE_CREATED: + case HttpURLConnection.HTTP_CREATED: // The BLOB exists in the registry. return null; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java index e76fa42f8c..2459c3f839 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java @@ -24,6 +24,9 @@ public class InsecureRegistryException extends RegistryException { InsecureRegistryException(URL insecureUrl) { - super("Only secure connections are allowed, but tried to reach URL " + insecureUrl); + super( + "Failed to verify the server at " + + insecureUrl + + " because only secure connections are allowed."); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index eaba7330ea..86486f8a4f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -86,7 +86,7 @@ public RegistryAuthenticator initialize() throw new RegistryAuthenticationFailedException(ex); } catch (InsecureRegistryException ex) { - // HTTP is not allowed, so just return null. + // Cannot skip certificate validation or use HTTP, so just return null. return null; } } @@ -238,7 +238,7 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa try { URL authenticationUrl = getAuthenticationUrl(scope); - try (Connection connection = new Connection(authenticationUrl)) { + try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { Request.Builder requestBuilder = Request.builder().setHttpTimeout(Integer.getInteger("jib.httpTimeout")); if (authorization != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 33c1d5f28c..101c6d07f5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -51,9 +51,6 @@ class RegistryEndpointCaller { private static final String DEFAULT_PROTOCOL = "https"; - /** Makes a {@link Connection} to the specified {@link URL}. */ - private final Function connectionFactory; - private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -61,6 +58,12 @@ class RegistryEndpointCaller { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; + /** Makes a {@link Connection} to the specified {@link URL}. */ + private final Function connectionFactory; + + /** Makes an insecure {@link Connection} to the specified {@link URL}. */ + @Nullable private Function insecureConnectionFactory; + /** * Constructs with parameters for making the request. * @@ -87,7 +90,8 @@ class RegistryEndpointCaller { authorization, registryEndpointRequestProperties, allowInsecureRegistries, - Connection::new); + Connection.getConnectionFactory(), + null /* might never be used, so create lazily to delay throwing potential GeneralSecurityException */); } @VisibleForTesting @@ -98,7 +102,8 @@ class RegistryEndpointCaller { @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, - Function connectionFactory) + Function connectionFactory, + @Nullable Function insecureConnectionFactory) throws MalformedURLException { this.initialRequestUrl = registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); @@ -108,6 +113,7 @@ class RegistryEndpointCaller { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; this.connectionFactory = connectionFactory; + this.insecureConnectionFactory = insecureConnectionFactory; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index 21350533a7..5eb6160cf6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -40,6 +40,12 @@ @RunWith(MockitoJUnitRunner.class) public class ConnectionTest { + @FunctionalInterface + private static interface SendFunction { + + Response send(Connection connection, Request request) throws IOException; + } + @Mock private HttpRequestFactory mockHttpRequestFactory; @Mock private HttpRequest mockHttpRequest; @@ -52,7 +58,9 @@ public class ConnectionTest { private Request fakeRequest; private HttpResponse mockHttpResponse; - @InjectMocks private final Connection testConnection = new Connection(fakeUrl.toURL()); + @InjectMocks + private final Connection testConnection = + Connection.getConnectionFactory().apply(fakeUrl.toURL()); @Test public void testGet() throws IOException { @@ -94,12 +102,6 @@ public void testHttpTimeout() throws IOException { Mockito.verify(mockHttpRequest).setReadTimeout(5982); } - @FunctionalInterface - private interface SendFunction { - - Response send(Connection connection, Request request) throws IOException; - } - private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { fakeRequest = Request.builder() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java index 4e0e2611aa..3b6665168c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.apache.ApacheHttpTransport; import java.io.IOException; import java.util.function.BiFunction; @@ -27,11 +28,12 @@ */ public class MockConnection extends Connection { - private BiFunction responseSupplier; + private final BiFunction responseSupplier; private Integer httpTimeout; public MockConnection(BiFunction responseSupplier) { - super(new GenericUrl("ftp://non-exisiting.example.url.ever").toURL()); + super( + new GenericUrl("ftp://non-exisiting.example.url.ever").toURL(), new ApacheHttpTransport()); this.responseSupplier = responseSupplier; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 37d1e971de..ed590ff010 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -16,33 +16,45 @@ package com.google.cloud.tools.jib.http; +import com.google.common.io.Resources; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; /** Simple local web server for testing. */ class TestWebServer implements Closeable { + private final boolean https; private final ServerSocket serverSocket; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final Semaphore threadStarted = new Semaphore(0); - TestWebServer() throws IOException, InterruptedException { - serverSocket = new ServerSocket(0); + TestWebServer(boolean https) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this.https = https; + serverSocket = createServerSocket(https); ignoreReturn(executorService.submit(this::serve200)); threadStarted.acquire(); } String getEndpoint() { String host = serverSocket.getInetAddress().getHostAddress(); - return "http://" + host + ":" + serverSocket.getLocalPort(); + return (https ? "https" : "http") + "://" + host + ":" + serverSocket.getLocalPort(); } @Override @@ -51,6 +63,28 @@ public void close() throws IOException { executorService.shutdown(); } + private ServerSocket createServerSocket(boolean https) + throws IOException, GeneralSecurityException, URISyntaxException { + if (https) { + KeyStore keyStore = KeyStore.getInstance("JKS"); + // generated with: keytool -genkey -keyalg RSA -keystore ./TestWebServer-keystore + Path keyStoreFile = Paths.get(Resources.getResource("TestWebServer-keystore").toURI()); + try (InputStream in = Files.newInputStream(keyStoreFile)) { + keyStore.load(in, "password".toCharArray()); + } + + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, "password".toCharArray()); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + return sslContext.getServerSocketFactory().createServerSocket(0); + } else { + return new ServerSocket(0); + } + } + private Void serve200() throws IOException { threadStarted.release(); try (Socket socket = serverSocket.accept()) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index 8697a7b76a..cbfd61ab98 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -18,7 +18,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLPeerUnverifiedException; import org.junit.Assert; import org.junit.Test; @@ -26,23 +30,27 @@ public class WithServerConnectionTest { @Test - public void testGet() throws IOException, InterruptedException { - try (TestWebServer server = new TestWebServer(); - Connection connection = new Connection(new URL(server.getEndpoint()))) { + public void testGet() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(false); + Connection connection = + Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { Response response = connection.send("GET", new Request.Builder().build()); Assert.assertEquals(200, response.getStatusCode()); ByteArrayOutputStream out = new ByteArrayOutputStream(); response.getBody().writeTo(out); - Assert.assertEquals("Hello World!", out.toString("UTF-8")); + Assert.assertEquals("Hello World!", new String(out.toByteArray(), StandardCharsets.UTF_8)); } } @Test - public void testErrorOnSecondSend() throws IOException, InterruptedException { - try (TestWebServer server = new TestWebServer(); - Connection connection = new Connection(new URL(server.getEndpoint()))) { + public void testErrorOnSecondSend() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(false); + Connection connection = + Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { connection.send("GET", new Request.Builder().build()); try { connection.send("GET", new Request.Builder().build()); @@ -52,4 +60,35 @@ public void testErrorOnSecondSend() throws IOException, InterruptedException { } } } + + @Test + public void testSecureConnectionOnInsecureHttpsServer() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(true); + Connection connection = + Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { + try { + connection.send("GET", new Request.Builder().build()); + Assert.fail("Should fail if cannot verify peer"); + } catch (SSLPeerUnverifiedException ex) { + Assert.assertNotNull(ex.getMessage()); + } + } + } + + @Test + public void testInsecureConnection() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(true); + Connection connection = + Connection.getInsecureConnectionFactory().apply(new URL(server.getEndpoint()))) { + Response response = connection.send("GET", new Request.Builder().build()); + + Assert.assertEquals(200, response.getStatusCode()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.getBody().writeTo(out); + Assert.assertEquals("Hello World!", new String(out.toByteArray(), StandardCharsets.UTF_8)); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index ebcf0be4b4..0da6552a32 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.security.GeneralSecurityException; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -114,12 +115,14 @@ public void tearDown() { } @Test - public void testCall_httpsPeerUnverified() throws IOException, RegistryException { + public void testCall_httpsPeerUnverified() + throws IOException, RegistryException, GeneralSecurityException { verifyRetriesWithHttp(SSLPeerUnverifiedException.class); } @Test - public void testCall_retryWithHttp() throws IOException, RegistryException { + public void testCall_retryWithHttp() + throws IOException, RegistryException, GeneralSecurityException { verifyRetriesWithHttp(HttpHostConnectException.class); } @@ -217,17 +220,20 @@ public void testCall_unknown() throws IOException, RegistryException { } @Test - public void testCall_temporaryRedirect() throws IOException, RegistryException { + public void testCall_temporaryRedirect() + throws IOException, RegistryException, GeneralSecurityException { verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); } @Test - public void testCall_movedPermanently() throws IOException, RegistryException { + public void testCall_movedPermanently() + throws IOException, RegistryException, GeneralSecurityException { verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY); } @Test - public void testCall_permanentRedirect() throws IOException, RegistryException { + public void testCall_permanentRedirect() + throws IOException, RegistryException, GeneralSecurityException { verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); } @@ -254,7 +260,8 @@ public void testCall_disallowInsecure() throws IOException, RegistryException { } @Test - public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { + public void testHttpTimeout_propertyNotSet() + throws IOException, RegistryException, GeneralSecurityException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -267,7 +274,8 @@ public void testHttpTimeout_propertyNotSet() throws IOException, RegistryExcepti } @Test - public void testHttpTimeout_stringValue() throws IOException, RegistryException { + public void testHttpTimeout_stringValue() + throws IOException, RegistryException, GeneralSecurityException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -278,7 +286,8 @@ public void testHttpTimeout_stringValue() throws IOException, RegistryException } @Test - public void testHttpTimeout_negativeValue() throws IOException, RegistryException { + public void testHttpTimeout_negativeValue() + throws IOException, RegistryException, GeneralSecurityException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -291,7 +300,8 @@ public void testHttpTimeout_negativeValue() throws IOException, RegistryExceptio } @Test - public void testHttpTimeout_0accepted() throws IOException, RegistryException { + public void testHttpTimeout_0accepted() + throws IOException, RegistryException, GeneralSecurityException { System.setProperty("jib.httpTimeout", "0"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); @@ -303,7 +313,7 @@ public void testHttpTimeout_0accepted() throws IOException, RegistryException { } @Test - public void testHttpTimeout() throws IOException, RegistryException { + public void testHttpTimeout() throws IOException, RegistryException, GeneralSecurityException { System.setProperty("jib.httpTimeout", "7593"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); @@ -316,7 +326,7 @@ public void testHttpTimeout() throws IOException, RegistryException { /** Verifies a request is retried with HTTP protocol if {@code exceptionClass} is thrown. */ private void verifyRetriesWithHttp(Class exceptionClass) - throws IOException, RegistryException { + throws IOException, RegistryException, GeneralSecurityException { // Has mockConnection.send throw first, then succeed. Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(exceptionClass)) @@ -390,7 +400,7 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) * Location} header. */ private void verifyRetriesWithNewLocation(int httpStatusCode) - throws IOException, RegistryException { + throws IOException, RegistryException, GeneralSecurityException { // Mocks a response for temporary redirect to a new location. Mockito.when(mockHttpResponse.getStatusCode()).thenReturn(httpStatusCode); Mockito.when(mockHttpResponse.getHeaders()) @@ -421,6 +431,7 @@ private RegistryEndpointCaller createRegistryEndpointCaller(boolean allo Authorizations.withBasicToken("token"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), allowInsecure, - mockConnectionFactory); + mockConnectionFactory, + null); } } diff --git a/jib-core/src/test/resources/TestWebServer-keystore b/jib-core/src/test/resources/TestWebServer-keystore new file mode 100644 index 0000000000000000000000000000000000000000..a485abe8d385a9ccaf3317ca85a4596f9101c3ae GIT binary patch literal 2252 zcmc(g`8U*y8^`A}Ge%>KZN!%%Th=b~u};VqX0nZaEm>ylW+uw6vJ4VkYbi^Fk`xjV zLiXKNvQx50_M{9~_j|tQ-2dSFob&$S_5SHO@8>zsbFg}_3V}eNM*{v4lW(v$IrxZ3 zeU~veArK}28wqD%*$^x;2mlU9LpcF}2?CFVFWnEg<;v4Xm7#B(v)jG#eaz~Um=3D} zk%NPz;{-X$>MW?#AjK1hSL(Fsl8XCmxMTkeGTEd?+7bblf1JG5kdY&YO`h4ATf#># z{Hj@FQuO#COH}6n;{qG5PYe!RG+W`iZE!5aAjOBO$EPh~pG&Of!ek-r$m+Q@1CKa6 zexSH3XBKQq7%52`#jVW*tE`v7r%HW|;#U={9AYj`DZGm(X^V%U!>$+cGHbCX=pt(* z(ajZjk@*T%Qiua-j7ykhNKP?4oTl1{e)}prDVwBP&!Er{odI#oMlmD#)G-T0!&l$E(Cpt+PPc?g#R zYTX2es=19vBL=&wl%Cr}v3{U@F=GEyN8wMSHm&%AXHcXF1^E?>n4meZEcxQxl#j|(z4Dx@j*NH1X?nWzqs!O3fA7()72|lm~Cyo&HDdOJs zr(leQt&QEaZoJHML!EOv5m-ABJ9MC3=lZcQPLW7yN=BJJF{D=aSL>?Wy-oz4FD0m4 z==u7#PV|R^P`5(V`IaCszhzDyzbfaSW+oPUHEiy2`be;<-n%ndj|yb+^g#t(pNGi3 zF2$Sp%U`qtZ|toZxSO!@5_GK7{OL>`!!irUt0%kZmw^rG#Cfm!OM;QQfgeJ-zMQb_ zPML4LXE^4@Ysyn7+i810FLo$vH8N0%l4U(hv6M`ntaN`I@(wq=1BF1R3j!}1v~ zaRR!GxI7Pk!gI-2clA}eehP2wu6cFb6Ynd0^Tp{Srl3M3No}Chx5>Nhn#*YgFO4vY zzQmw}&P!|_{E9^>J-?ya;r8lsLvgXDrII!cuV^Qmb*CV@<)Y&nJ0{epWX^b7xk5$$ z;A8fK_IH?eyN%5*kM)}tDUS8x`?eZFfi2=48jJ`hK@V7ZnsU_ME^bwYeQb1AUa#Pm zy#;a1^%&QsAhXdIeuupNV-jEUg{SaAguH{ioKLD4s1z(|`->VU{X;FnA-p8rA?6CP zT0wMuUDkU+G#BdMw<6N;sNWt722P;*l0j}P*Kb5&P)42so)mW> zb1K#Me=ZDzI`833@h4Ngg@_(lL0&Y7!>Y(D%FBZ|5VZe|AXfgU{=xr8OOb%czk@k? z8?Z=#9fFMnSfG&r0C`IGsBX$JTLxCOzmYX=7h?#flg3`7#Dd~ak>SD|zQxX4*PMAD zdTpyvyCOEAFaQGN3u8}V1&+onzy{}mbL)~ug*=}r->M1-HrD9iFL>+d!TIkHkGAV5 zL;xavFqOf>Pt*FK-%N~N04Ix8lS(h-a(@;fP*2F!BcOw=kgEY6yhWo{)U zI7m$S>QuCM(A?iG+qMK*z`nn7s4XStu20<=C^@Z1yuN2;;Xh&kw1R(Vmj#U3LnToL zXolyxJwJgxYm7qTl1OxYMeoxmRJj#ClX!hF(-Smz&hBlbSo_Yiwj)n5pU;~s3NS3c zTz{v_ia**~z|nc*RmmsrwzpER<@D0o%pz8LXKz< Date: Thu, 26 Jul 2018 11:38:56 -0400 Subject: [PATCH 0063/2020] Propagate base image's environment variable config to built image (#716) --- .../jib/builder/steps/BuildImageStep.java | 13 +++++-- .../jib/builder/steps/PullBaseImageStep.java | 4 ++- .../tools/jib/builder/steps/StepsRunner.java | 1 + .../google/cloud/tools/jib/image/Image.java | 34 ++++++------------- .../jib/image/json/ImageToJsonTranslator.java | 25 +++++++++++++- .../jib/image/json/JsonToImageTranslator.java | 25 +++++++++++--- .../jib/builder/steps/BuildImageStepTest.java | 32 +++++++++++++++-- .../cloud/tools/jib/image/ImageTest.java | 7 ++-- .../image/json/ImageToJsonTranslatorTest.java | 7 ++++ .../image/json/JsonToImageTranslatorTest.java | 28 ++++++++++++++- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 2 ++ 12 files changed, 139 insertions(+), 40 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index b2030bd982..9ebd5a4589 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -39,6 +39,7 @@ class BuildImageStep private static final String DESCRIPTION = "Building container configuration"; private final BuildConfiguration buildConfiguration; + private final PullBaseImageStep pullBaseImageStep; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; @@ -48,15 +49,18 @@ class BuildImageStep BuildImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + PullBaseImageStep pullBaseImageStep, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.pullBaseImageStep = pullBaseImageStep; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; listenableFuture = - Futures.whenAllSucceed(pullAndCacheBaseImageLayersStep.getFuture()) + Futures.whenAllSucceed( + pullBaseImageStep.getFuture(), pullAndCacheBaseImageLayersStep.getFuture()) .call(this, listeningExecutorService); } @@ -96,8 +100,13 @@ private Image afterCachedLayersSteps() buildAndCacheApplicationLayerSteps) { imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); } + + // Start with environment from base image and overlay build configuration + imageBuilder.addEnvironment( + NonBlockingSteps.get(pullBaseImageStep).getBaseImage().getEnvironment()); + imageBuilder.addEnvironment(buildConfiguration.getEnvironment()); + imageBuilder.setCreated(buildConfiguration.getCreationTime()); - imageBuilder.setEnvironment(buildConfiguration.getEnvironment()); imageBuilder.setEntrypoint(buildConfiguration.getEntrypoint()); imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments()); imageBuilder.setExposedPorts(buildConfiguration.getExposedPorts()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index b528f6d85a..3d758a82f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -40,6 +40,7 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; +import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -62,7 +63,8 @@ static class BaseImageWithAuthorization { private final Image baseImage; private final @Nullable Authorization baseImageAuthorization; - private BaseImageWithAuthorization( + @VisibleForTesting + BaseImageWithAuthorization( Image baseImage, @Nullable Authorization baseImageAuthorization) { this.baseImage = baseImage; this.baseImageAuthorization = baseImageAuthorization; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index ff959b9b31..936ed8619f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -131,6 +131,7 @@ public StepsRunner runBuildImageStep() { new BuildImageStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(pullBaseImageStep), Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps)); return this; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 58704e2b7a..60d3c2d7f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.time.Instant; import java.util.List; import java.util.Map; @@ -30,7 +31,7 @@ public class Image { public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); - private ImmutableList.Builder environmentBuilder = ImmutableList.builder(); + private ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @@ -49,18 +50,14 @@ public Builder setCreated(Instant created) { } /** - * Sets the environment with a map from environment variable names to values. + * Adds a map of environment variables to the current map. * * @param environment the map of environment variables * @return this */ - public Builder setEnvironment(@Nullable Map environment) { - if (environment == null) { - this.environmentBuilder = ImmutableList.builder(); - } else { - for (Map.Entry environmentVariable : environment.entrySet()) { - setEnvironmentVariable(environmentVariable.getKey(), environmentVariable.getValue()); - } + public Builder addEnvironment(@Nullable Map environment) { + if (environment != null) { + this.environmentBuilder.putAll(environment); } return this; } @@ -73,18 +70,7 @@ public Builder setEnvironment(@Nullable Map environment) { * @return this */ public Builder setEnvironmentVariable(String name, String value) { - environmentBuilder.add(name + "=" + value); - return this; - } - - /** - * Adds an environment variable definition in the format {@code NAME=VALUE}. - * - * @param environmentVariableDefinition the definition to add - * @return this - */ - public Builder addEnvironmentVariableDefinition(String environmentVariableDefinition) { - environmentBuilder.add(environmentVariableDefinition); + environmentBuilder.put(name, value); return this; } @@ -167,7 +153,7 @@ public static Builder builder() { private final ImageLayers layers; /** Environment variable definitions for running the image, in the format {@code NAME=VALUE}. */ - @Nullable private final ImmutableList environment; + @Nullable private final ImmutableMap environment; /** Initial command to run when running the image. */ @Nullable private final ImmutableList entrypoint; @@ -181,7 +167,7 @@ public static Builder builder() { private Image( @Nullable Instant created, ImageLayers layers, - @Nullable ImmutableList environment, + @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @Nullable ImmutableList javaArguments, @Nullable ImmutableList exposedPorts) { @@ -199,7 +185,7 @@ public Instant getCreated() { } @Nullable - public ImmutableList getEnvironment() { + public ImmutableMap getEnvironment() { return environment; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 5731005163..8e07d870a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -24,6 +24,8 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import java.lang.reflect.InvocationTargetException; import java.util.Collections; @@ -67,6 +69,27 @@ public class ImageToJsonTranslator { return result.build(); } + /** + * Converts the map of environment variables to a list with items in the format "NAME=VALUE". + * + * @return the list + */ + @VisibleForTesting + @Nullable + static ImmutableList environmentMapToList(@Nullable Map environment) { + if (environment == null) { + return null; + } + Preconditions.checkArgument( + environment.keySet().stream().noneMatch(key -> key.contains("=")), + "Illegal environment variable: name cannot contain '='"); + return environment + .entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(ImmutableList.toImmutableList()); + } + private final Image image; /** @@ -96,7 +119,7 @@ public Blob getContainerConfigurationBlob() { template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); // Adds the environment variables. - template.setContainerEnvironment(image.getEnvironment()); + template.setContainerEnvironment(environmentMapToList(image.getEnvironment())); // Sets the entrypoint. template.setContainerEntrypoint(image.getEntrypoint()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 890ce1d0c3..a6ac326c1f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -47,7 +47,17 @@ public class JsonToImageTranslator { * *

Example matches: 100, 1000/tcp, 2000/udp */ - private static final Pattern portPattern = Pattern.compile("(\\d+)(?:/(tcp|udp))?"); + private static final Pattern PORT_PATTERN = + Pattern.compile("(?\\d+)(?:/(?tcp|udp))?"); + + /** + * Pattern used for parsing environment variables in the format {@code NAME=VALUE}. {@code NAME} + * should not contain an '='. + * + *

Example matches: NAME=VALUE, A12345=$$$$$ + */ + @VisibleForTesting + static final Pattern ENVIRONMENT_PATTERN = Pattern.compile("(?[^=]+)=(?.*)"); /** * Translates {@link V21ManifestTemplate} to {@link Image}. @@ -139,7 +149,12 @@ public static Image toImage( if (containerConfigurationTemplate.getContainerEnvironment() != null) { for (String environmentVariable : containerConfigurationTemplate.getContainerEnvironment()) { - imageBuilder.addEnvironmentVariableDefinition(environmentVariable); + Matcher matcher = ENVIRONMENT_PATTERN.matcher(environmentVariable); + if (!matcher.matches()) { + throw new BadContainerConfigurationFormatException( + "Invalid environment variable definition: " + environmentVariable); + } + imageBuilder.setEnvironmentVariable(matcher.group("name"), matcher.group("value")); } } @@ -162,14 +177,14 @@ static ImmutableList portMapToList(@Nullable Map> portMa ImmutableList.Builder ports = new ImmutableList.Builder<>(); for (Map.Entry> entry : portMap.entrySet()) { String port = entry.getKey(); - Matcher matcher = portPattern.matcher(port); + Matcher matcher = PORT_PATTERN.matcher(port); if (!matcher.matches()) { throw new BadContainerConfigurationFormatException( "Invalid port configuration: '" + port + "'."); } - int portNumber = Integer.parseInt(matcher.group(1)); - String protocol = matcher.group(2); + int portNumber = Integer.parseInt(matcher.group("portNum")); + String protocol = matcher.group("protocol"); ports.add(new Port(portNumber, Protocol.getFromString(protocol))); } return ports.build(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 1ee4181982..188989484b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Futures; @@ -46,6 +47,7 @@ public class BuildImageStepTest { @Mock private BuildConfiguration mockBuildConfiguration; @Mock private BuildLogger mockBuildLogger; + @Mock private PullBaseImageStep mockPullBaseImageStep; @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStep; @@ -70,6 +72,10 @@ public void setUp() throws DigestException { Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); Mockito.when(mockBuildConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Image baseImage = + Image.builder().addEnvironment(ImmutableMap.of("NAME", "VALUE")).build(); + Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) + .thenReturn(Futures.immediateFuture(testCachedLayer)); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) .thenReturn( Futures.immediateFuture( @@ -77,8 +83,10 @@ public void setUp() throws DigestException { mockPullAndCacheBaseImageLayerStep, mockPullAndCacheBaseImageLayerStep, mockPullAndCacheBaseImageLayerStep))); - Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + Mockito.when(mockPullBaseImageStep.getFuture()) + .thenReturn( + Futures.immediateFuture( + new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null))); Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture()) .thenReturn(Futures.immediateFuture(testCachedLayer)); } @@ -89,6 +97,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( mockBuildAndCacheApplicationLayerStep, @@ -98,4 +107,23 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); } + + @Test + public void test_propagateBaseImageConfiguration() + throws ExecutionException, InterruptedException { + Mockito.when(mockBuildConfiguration.getEnvironment()) + .thenReturn(ImmutableMap.of("BASE", "IMAGE")); + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + Assert.assertEquals(ImmutableMap.of("NAME", "VALUE", "BASE", "IMAGE"), image.getEnvironment()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index c78f47febe..f21fe3396b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.time.Instant; import java.util.Arrays; import org.junit.Assert; @@ -45,9 +46,6 @@ public void setUp() throws LayerPropertyNotFoundException { @Test public void test_smokeTest() throws LayerPropertyNotFoundException { - ImmutableList expectedEnvironment = - ImmutableList.of("crepecake=is great", "VARIABLE=VALUE"); - Image image = Image.builder() .setCreated(Instant.ofEpochSecond(10000)) @@ -63,7 +61,8 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Assert.assertEquals( mockDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest()); Assert.assertEquals(Instant.ofEpochSecond(10000), image.getCreated()); - Assert.assertEquals(expectedEnvironment, image.getEnvironment()); + Assert.assertEquals( + ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments()); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 77125884c7..8e6b3e2451 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -111,6 +111,13 @@ public void testPortListToMap() { Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input)); } + @Test + public void testEnvironmentMapToList() { + ImmutableMap input = ImmutableMap.of("NAME1", "VALUE1", "NAME2", "VALUE2"); + ImmutableList expected = ImmutableList.of("NAME1=VALUE1", "NAME2=VALUE2"); + Assert.assertEquals(expected, ImageToJsonTranslator.environmentMapToList(input)); + } + /** Tests translation of image to {@link BuildableManifestTemplate}. */ private void testGetManifest( Class manifestTemplateClass, String translatedJsonFilename) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 5953ad0018..267d3915f7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; import org.junit.Assert; import org.junit.Test; @@ -110,6 +111,31 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException } } + @Test + public void testJsonToImageTranslatorRegex() { + assertGoodEnvironmentPattern("NAME=VALUE", "NAME", "VALUE"); + assertGoodEnvironmentPattern("A1203921=www=ww", "A1203921", "www=ww"); + assertGoodEnvironmentPattern("&*%(&#$(*@(%&@$*$(=", "&*%(&#$(*@(%&@$*$(", ""); + assertGoodEnvironmentPattern("m_a_8943=100", "m_a_8943", "100"); + assertGoodEnvironmentPattern("A_B_C_D=*****", "A_B_C_D", "*****"); + + assertBadEnvironmentPattern("================="); + assertBadEnvironmentPattern("A_B_C"); + } + + private void assertGoodEnvironmentPattern( + String input, String expectedName, String expectedValue) { + Matcher matcher = JsonToImageTranslator.ENVIRONMENT_PATTERN.matcher(input); + Assert.assertTrue(matcher.matches()); + Assert.assertEquals(expectedName, matcher.group("name")); + Assert.assertEquals(expectedValue, matcher.group("value")); + } + + private void assertBadEnvironmentPattern(String input) { + Matcher matcher = JsonToImageTranslator.ENVIRONMENT_PATTERN.matcher(input); + Assert.assertFalse(matcher.matches()); + } + private void testToImage_buildable( String jsonFilename, Class manifestTemplateClass) throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, @@ -144,7 +170,7 @@ private void testToImage_buildable( layers.get(0).getDiffId()); Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated()); Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); - Assert.assertEquals(Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), image.getEnvironment()); + Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); Assert.assertEquals( ImmutableList.of( new Port(1000, Protocol.TCP), diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 009e3327ab..3fd325eba8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) - Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) ## 0.9.7 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index bfe5e9db41..3095735226 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) + ### Fixed - Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) From 48927a3fa163cb344c77ace5aa8e7fc2347cd7b2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 26 Jul 2018 17:14:49 -0400 Subject: [PATCH 0064/2020] More detailed Javadocs (#723) --- .../cloud/tools/jib/async/NonBlockingSteps.java | 15 +++++++++++++-- .../cloud/tools/jib/tar/TarStreamBuilder.java | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java index 2fab895a7a..0258dcd120 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java @@ -17,12 +17,23 @@ package com.google.cloud.tools.jib.async; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.ExecutionException; -/** Static utility for ensuring {@link ListenableFuture#get} does not block. */ +/** + * Static utility for checking at runtime that the caller attempts to get a result only from a + * completed {@link AsyncStep} by otherwise throwing a runtime exception. + */ public class NonBlockingSteps { + /** + * Gets the completed computation result of {@code asyncStep}. + * + * @param the type of the computation result of {@code asyncStep} + * @param asyncStep completed {@link AsyncStep} + * @return the completed computation result + * @throws ExecutionException if the {@code Future} failed with an exception + * @throws IllegalStateException if {@code asyncStep} has not been completed + */ public static T get(AsyncStep asyncStep) throws ExecutionException { return Futures.getDone(asyncStep.getFuture()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 3c52f6e41c..fc4b357623 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -66,7 +66,7 @@ public void addTarArchiveEntry(TarArchiveEntry entry) { /** * Adds a blob to the archive. Note that this should be used with raw bytes and not file contents; - * for adding files to the archive, use {@code TarStreamBuilder#addTarArchiveEntry()}. + * for adding files to the archive, use {@link #addTarArchiveEntry}. * * @param contents the bytes to add to the tarball * @param name the name of the entry (i.e. filename) From 5f1ac8ed9dbfe67c4a5c5d32b65964cced5ec833 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 27 Jul 2018 11:22:14 -0400 Subject: [PATCH 0065/2020] Add 'auth' configuration to maven plugin (#722) --- jib-maven-plugin/CHANGELOG.md | 3 + .../tools/jib/maven/BuildDockerMojo.java | 7 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 13 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 7 +- .../jib/maven/JibPluginConfiguration.java | 142 +++++++++++++----- .../jib/maven/JibPluginConfigurationTest.java | 29 ++++ 6 files changed, 161 insertions(+), 40 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 3095735226..8c733b903c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. ### Added +- `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) + ### Changed - Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 7648812449..4a3f5d14c1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; @@ -76,8 +77,12 @@ public void execute() throws MojoExecutionException { MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = - mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + fromAuthorization != null + ? new RegistryCredentials( + "jib-maven-plugin configuration", fromAuthorization) + : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); String mainClass = mavenProjectProperties.getMainClass(this); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index f878961c78..ea24f9652f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -89,13 +90,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } + MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = - mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + fromAuthorization != null + ? new RegistryCredentials( + "jib-maven-plugin configuration", fromAuthorization) + : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + Authorization toAuthorization = getTargetImageAuth(); RegistryCredentials knownTargetRegistryCredentials = - mavenSettingsServerCredentials.retrieve(targetImage.getRegistry()); + toAuthorization != null + ? new RegistryCredentials("jib-maven-plugin configuration", toAuthorization) + : mavenSettingsServerCredentials.retrieve(targetImage.getRegistry()); MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 908e321e8b..708035876d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; @@ -74,8 +75,12 @@ public void execute() throws MojoExecutionException { MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = - mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + fromAuthorization != null + ? new RegistryCredentials( + "jib-maven-plugin configuration", fromAuthorization) + : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); String mainClass = mavenProjectProperties.getMainClass(this); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 59dfa41ed1..9711976842 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; @@ -39,6 +41,37 @@ /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ abstract class JibPluginConfiguration extends AbstractMojo { + /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ + public static class AuthConfiguration { + + @Nullable @Parameter private String username; + + @Nullable @Parameter private String password; + + @VisibleForTesting + void setUsername(String username) { + this.username = username; + } + + @VisibleForTesting + void setPassword(String password) { + this.password = password; + } + + /** + * Converts the {@link AuthConfiguration} to an {@link Authorization}. + * + * @return the {@link Authorization} + */ + @Nullable + private Authorization getAuthorization() { + if (username == null || password == null) { + return null; + } + return Authorizations.withBasicCredentials(username, password); + } + } + /** * Configuration for {@code from} parameter, where image by default is {@code * gcr.io/distroless/java}. @@ -50,6 +83,8 @@ public static class FromConfiguration { private String image = "gcr.io/distroless/java"; @Nullable @Parameter private String credHelper; + + @Parameter private AuthConfiguration auth = new AuthConfiguration(); } /** Configuration for {@code to} parameter, where image is required. */ @@ -59,6 +94,8 @@ public static class ToConfiguration { @Nullable @Parameter private String credHelper; + @Parameter private AuthConfiguration auth = new AuthConfiguration(); + public void set(String image) { this.image = image; } @@ -96,51 +133,36 @@ static ImageReference parseImageReference(String image, String type) { } /** - * Warns about deprecated parameters in use. + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. * - * @param logger The logger used to print the warnings + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. */ - void handleDeprecatedParameters(BuildLogger logger) { - StringBuilder deprecatedParams = new StringBuilder(); - if (!jvmFlags.isEmpty()) { - deprecatedParams.append(" -> \n"); - if (container.jvmFlags.isEmpty()) { - container.jvmFlags = jvmFlags; - } - } - if (!Strings.isNullOrEmpty(mainClass)) { - deprecatedParams.append(" -> \n"); - if (Strings.isNullOrEmpty(container.mainClass)) { - container.mainClass = mainClass; - } - } - if (!args.isEmpty()) { - deprecatedParams.append(" -> \n"); - if (container.args.isEmpty()) { - container.args = args; - } - } - if (!Strings.isNullOrEmpty(format)) { - deprecatedParams.append(" -> \n"); - container.format = format; - } - - if (deprecatedParams.length() > 0) { - logger.warn( - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your pom.xml to avoid issues in the future:\n" - + deprecatedParams); + @VisibleForTesting + @Nullable + static Authorization getImageAuth( + String usernameProperty, String passwordProperty, AuthConfiguration auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (commandlineUsername != null && commandlinePassword != null) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); } + return auth.getAuthorization(); } - @Nullable - @Parameter(defaultValue = "${project}", readonly = true) - private MavenProject project; - @Nullable @Parameter(defaultValue = "${session}", readonly = true) MavenSession session; + @Nullable + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + @Parameter private FromConfiguration from = new FromConfiguration(); @Parameter(property = "image") @@ -170,6 +192,44 @@ void handleDeprecatedParameters(BuildLogger logger) { @Nullable @Component protected SettingsDecrypter settingsDecrypter; + /** + * Warns about deprecated parameters in use. + * + * @param logger The logger used to print the warnings + */ + void handleDeprecatedParameters(BuildLogger logger) { + StringBuilder deprecatedParams = new StringBuilder(); + if (!jvmFlags.isEmpty()) { + deprecatedParams.append(" -> \n"); + if (container.jvmFlags.isEmpty()) { + container.jvmFlags = jvmFlags; + } + } + if (!Strings.isNullOrEmpty(mainClass)) { + deprecatedParams.append(" -> \n"); + if (Strings.isNullOrEmpty(container.mainClass)) { + container.mainClass = mainClass; + } + } + if (!args.isEmpty()) { + deprecatedParams.append(" -> \n"); + if (container.args.isEmpty()) { + container.args = args; + } + } + if (!Strings.isNullOrEmpty(format)) { + deprecatedParams.append(" -> \n"); + container.format = format; + } + + if (deprecatedParams.length() > 0) { + logger.warn( + "There are deprecated parameters used in the build configuration. Please make the " + + "following changes to your pom.xml to avoid issues in the future:\n" + + deprecatedParams); + } + } + MavenProject getProject() { return Preconditions.checkNotNull(project); } @@ -183,6 +243,11 @@ String getBaseImageCredentialHelperName() { return Preconditions.checkNotNull(from).credHelper; } + @Nullable + Authorization getBaseImageAuth() { + return getImageAuth("jib.from.auth.username", "jib.from.auth.password", from.auth); + } + @Nullable String getTargetImage() { return to.image; @@ -193,6 +258,11 @@ String getTargetImageCredentialHelperName() { return Preconditions.checkNotNull(to).credHelper; } + @Nullable + Authorization getTargetImageAuth() { + return getImageAuth("jib.to.auth.username", "jib.to.auth.password", to.auth); + } + boolean getUseCurrentTimestamp() { return container.useCurrentTimestamp; } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 67b3c9dabf..0f92d9d293 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -17,6 +17,9 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; import java.nio.file.Paths; import java.util.Arrays; import org.junit.Assert; @@ -66,4 +69,30 @@ public void execute() {} Assert.assertEquals("OCI", testPluginConfiguration.getFormat()); Assert.assertEquals(Paths.get("some/path"), testPluginConfiguration.getExtraDirectory()); } + + @Test + public void testGetImageAuth() { + AuthConfiguration auth = new AuthConfiguration(); + auth.setUsername("vwxyz"); + auth.setPassword("98765"); + + System.setProperty("jib.test.auth.user", "abcde"); + System.setProperty("jib.test.auth.pass", "12345"); + Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); + Authorization actual = + JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + + System.clearProperty("jib.test.auth.user"); + System.clearProperty("jib.test.auth.pass"); + expected = Authorizations.withBasicCredentials("vwxyz", "98765"); + actual = JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + + auth = new AuthConfiguration(); + actual = JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNull(actual); + } } From 02104b3b52c17fd989b5e7cc306271c04167b292 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 27 Jul 2018 13:12:38 -0400 Subject: [PATCH 0066/2020] Enhance build.sh to allow faster iterations for test (#726) --- build.sh | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) mode change 100644 => 100755 build.sh diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index c8f5938f5b..70485ca565 --- a/build.sh +++ b/build.sh @@ -1,14 +1,80 @@ #!/bin/sh # Build Jib +quickMode=false +mavenOptions="" +gradleOptions="" + +usage() +{ + eval 1>&2 + echo "Simple builder for Jib for jib-core, jib-maven-plugin, and jib-gradle-plugin" + echo "use: $0 [-qe] [clean | core | maven | gradle | all]" + echo " -q quick mode: skip tests, formatting" + echo " -e show error information (mvn: -e, gradle: --stacktrace)" + exit 1 +} + # doBuild: Run a command in a directory # $1 = directory # $2... = build command doBuild() { - (directory="$1"; shift; echo ">>> $directory: $*"; cd "$directory" && eval '"$@"') + (directory="$1"; shift; echo ">>> (cd $directory; $*)"; cd "$directory" && eval '"$@"') } +while getopts qe c; do + case $c in + q) quickMode=true;; + e) mavenOptions="$mavenOptions -e" + gradleOptions="$gradleOptions --stacktrace" + ;; + \?) usage;; + esac +done +shift `expr $OPTIND - 1` + +if [ $# -eq 0 ]; then + set -- core gradle maven +fi + set -e # exit on error -doBuild jib-core ./gradlew googleJavaFormat build -doBuild jib-gradle-plugin ./gradlew googleJavaFormat build install -doBuild jib-maven-plugin ./mvnw fmt:format install -U +for target in "$@"; do + case "$target" in + clean) + doBuild jib-core ./gradlew $gradleOptions clean + doBuild jib-gradle-plugin ./gradlew $gradleOptions clean + doBuild jib-maven-plugin ./mvnw $mavenOptions clean + ;; + + core) + if [ "$quickMode" = false ]; then + doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build + else + doBuild jib-core ./gradlew $gradleOptions build \ + --exclude-task test --exclude-task check + fi + ;; + + maven) + if [ "$quickMode" = false ]; then + doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build + doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U + else + # jib-maven-plugin pulls in jib-core directly + doBuild jib-maven-plugin ./mvnw $mavenOptions -Dcheckstyle.skip -Dfmt.skip -DskipTests install -U + fi + ;; + + gradle) + if [ "$quickMode" = false ]; then + doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build + doBuild jib-gradle-plugin ./gradlew $gradleOptions googleJavaFormat build install + else + # jib-gradle-plugin pulls in jib-core directly + doBuild jib-gradle-plugin ./gradlew $gradleOptions build install \ + --exclude-task test --exclude-task check + fi + ;; + + esac +done From 51b20c451130efde721fb0055337756e779caa23 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 27 Jul 2018 14:38:11 -0400 Subject: [PATCH 0067/2020] Do not overwrite existing layer TAR (#729) --- .../cloud/tools/jib/cache/CacheWriter.java | 16 +++- .../tools/jib/cache/CacheWriterTest.java | 94 ++++++++++++------- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java index 21aa29a2e7..69b55f06ae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java @@ -27,9 +27,9 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.zip.GZIPInputStream; @@ -76,11 +76,17 @@ public CachedLayerWithMetadata writeLayer(ReproducibleLayerBuilder reproducibleL compressorStream.close(); BlobDescriptor compressedBlobDescriptor = compressedDigestOutputStream.toBlobDescriptor(); - // Renames the temporary layer file to the correct filename. If the file already exists, we - // skip renaming and use the existing file. This happens if a new layer happens to have the - // same content as a previously-cached layer. + // Renames the temporary layer file to the correct filename. Path layerFile = getLayerFile(compressedBlobDescriptor.getDigest()); - Files.move(tempLayerFile, layerFile, StandardCopyOption.REPLACE_EXISTING); + try { + Files.move(tempLayerFile, layerFile); + } catch (FileAlreadyExistsException ignored) { + // If the file already exists, we skip renaming and use the existing file. This happens if a + // new layer happens to have the same content as a previously-cached layer. + // + // Do not attempt to remove the try-catch block with the idea of checking file existence + // before moving; there can be concurrent file moves. + } CachedLayer cachedLayer = new CachedLayer(layerFile, compressedBlobDescriptor, diffId); LayerMetadata layerMetadata = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java index 2de09e2fd1..3f70700e7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java @@ -25,19 +25,18 @@ import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.cloud.tools.jib.image.UnwrittenLayer; import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; +import com.google.common.io.ByteStreams; import com.google.common.io.CountingOutputStream; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.junit.Assert; @@ -50,9 +49,14 @@ /** Tests for {@link CacheWriter}. */ public class CacheWriterTest { - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); + private static boolean isTarGz(Path path) { + return path.toString().endsWith(".tar.gz"); + } + + @Rule public final TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); private Cache testCache; + private CacheWriter cacheWriter; private Path resourceBlob; @@ -74,17 +78,14 @@ public void setUp() throws CacheMetadataCorruptedException, IOException, URISynt Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); testCache = Cache.init(cacheDirectory); + // Writes resourceBlob as a layer to the cache. + cacheWriter = new CacheWriter(testCache); resourceBlob = Paths.get(Resources.getResource("blobA").toURI()); } @Test public void testWriteLayer_unwritten() throws IOException { - ExpectedLayer expectedLayer = getExpectedLayer(); - - // Writes resourceBlob as a layer to the cache. - CacheWriter cacheWriter = new CacheWriter(testCache); - UnwrittenLayer unwrittenLayer = new UnwrittenLayer(Blobs.from(resourceBlob)); ReproducibleLayerBuilder mockReproducibleLayerBuilder = @@ -94,34 +95,64 @@ public void testWriteLayer_unwritten() throws IOException { .thenReturn( ImmutableList.of( new LayerEntry( - ImmutableList.of(Paths.get("some", "source", "file")), - "/some/extraction/path"))); + ImmutableList.of(Paths.get("some/source/file")), "/some/extraction/path"))); CachedLayerWithMetadata cachedLayerWithMetadata = cacheWriter.writeLayer(mockReproducibleLayerBuilder); testCache.addCachedLayersWithMetadataToMetadata( Collections.singletonList(cachedLayerWithMetadata)); - CachedLayerWithMetadata layerInMetadata = testCache.getUpdatedMetadata().getLayers().get(0); - Assert.assertNotNull(layerInMetadata.getMetadata()); - Assert.assertEquals(1, layerInMetadata.getMetadata().getEntries().size()); + LayerMetadata layerMetadata = testCache.getUpdatedMetadata().getLayers().get(0).getMetadata(); + Assert.assertNotNull(layerMetadata); + Assert.assertEquals(1, layerMetadata.getEntries().size()); Assert.assertEquals( - Collections.singletonList(Paths.get("some", "source", "file").toString()), - layerInMetadata.getMetadata().getEntries().get(0).getSourceFilesStrings()); + Collections.singletonList(Paths.get("some/source/file").toString()), + layerMetadata.getEntries().get(0).getSourceFilesStrings()); Assert.assertEquals( - "/some/extraction/path", - layerInMetadata.getMetadata().getEntries().get(0).getExtractionPath()); + "/some/extraction/path", layerMetadata.getEntries().get(0).getExtractionPath()); + + verifyCachedLayerIsExpected(getExpectedLayer(), cachedLayerWithMetadata); + } + + // Windows file overwrite issue: https://github.com/GoogleContainerTools/jib/issues/719 + @Test + public void testWriteLayer_doesNotOverwriteExistingTarGz() + throws IOException, InterruptedException { + // Writes resourceBlob as a layer to the cache. + UnwrittenLayer unwrittenLayer = new UnwrittenLayer(Blobs.from(resourceBlob)); + + ReproducibleLayerBuilder layerBuilder = Mockito.mock(ReproducibleLayerBuilder.class); + Mockito.when(layerBuilder.build()).thenReturn(unwrittenLayer); + LayerEntry layerEntry = + new LayerEntry(ImmutableList.of(Paths.get("some/source/file")), "/some/extraction/path"); + Mockito.when(layerBuilder.getLayerEntries()).thenReturn(ImmutableList.of(layerEntry)); - verifyCachedLayerIsExpected(expectedLayer, cachedLayerWithMetadata); + cacheWriter.writeLayer(layerBuilder); + Assert.assertEquals(1, getTarGzCountInCache()); + long tarGzModifiedTime = getTarGzModifiedTimeInCache(); + + Thread.sleep(1000); // to have different modified time + cacheWriter.writeLayer(layerBuilder); + Assert.assertEquals(1, getTarGzCountInCache()); + Assert.assertEquals(tarGzModifiedTime, getTarGzModifiedTimeInCache()); + } + + private long getTarGzCountInCache() throws IOException { + try (Stream stream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { + return stream.filter(CacheWriterTest::isTarGz).count(); + } + } + + private long getTarGzModifiedTimeInCache() throws IOException { + try (Stream stream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { + return stream.filter(CacheWriterTest::isTarGz).findFirst().get().toFile().lastModified(); + } } @Test public void testGetLayerOutputStream() throws IOException { ExpectedLayer expectedLayer = getExpectedLayer(); - // Writes resourceBlob as a layer to the cache. - CacheWriter cacheWriter = new CacheWriter(testCache); - CountingOutputStream layerOutputStream = cacheWriter.getLayerOutputStream(expectedLayer.blobDescriptor.getDigest()); expectedLayer.blob.writeTo(layerOutputStream); @@ -142,9 +173,6 @@ public void testGetLayerOutputStream() throws IOException { * file */ private ExpectedLayer getExpectedLayer() throws IOException { - String expectedBlobAString = - new String(Files.readAllBytes(resourceBlob), StandardCharsets.UTF_8); - // Gets the expected content descriptor, diff ID, and compressed BLOB. ByteArrayOutputStream compressedBlobOutputStream = new ByteArrayOutputStream(); CountingDigestOutputStream compressedDigestOutputStream = @@ -152,7 +180,8 @@ private ExpectedLayer getExpectedLayer() throws IOException { CountingDigestOutputStream uncompressedDigestOutputStream; try (GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream)) { uncompressedDigestOutputStream = new CountingDigestOutputStream(compressorStream); - uncompressedDigestOutputStream.write(expectedBlobAString.getBytes(StandardCharsets.UTF_8)); + byte[] expectedBlobABytes = Files.readAllBytes(resourceBlob); + uncompressedDigestOutputStream.write(expectedBlobABytes); } BlobDescriptor expectedBlobADescriptor = compressedDigestOutputStream.toBlobDescriptor(); @@ -171,15 +200,10 @@ private void verifyCachedLayerIsExpected(ExpectedLayer expectedLayer, CachedLaye // Reads the cached layer back. Path compressedBlobFile = cachedLayer.getContentFile(); - try (InputStreamReader fileReader = - new InputStreamReader( - new GZIPInputStream(Files.newInputStream(compressedBlobFile)), - StandardCharsets.UTF_8)) { - String decompressedString = CharStreams.toString(fileReader); - - String expectedBlobAString = - new String(Files.readAllBytes(resourceBlob), StandardCharsets.UTF_8); - Assert.assertEquals(expectedBlobAString, decompressedString); + try (GZIPInputStream in = new GZIPInputStream(Files.newInputStream(compressedBlobFile))) { + byte[] decompressedBytes = ByteStreams.toByteArray(in); + byte[] expectedBlobABytes = Files.readAllBytes(resourceBlob); + Assert.assertArrayEquals(expectedBlobABytes, decompressedBytes); Assert.assertEquals( expectedLayer.blobDescriptor.getSize(), cachedLayer.getBlobDescriptor().getSize()); Assert.assertEquals( From aab67f9ea4929bdcd26aea6dd6c3d6483bfdf721 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 30 Jul 2018 13:22:41 -0400 Subject: [PATCH 0068/2020] Skips execution if packaging is 'pom'. (#735) --- .../com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 5 +++++ .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 6 ++++++ .../java/com/google/cloud/tools/jib/maven/BuildTarMojo.java | 5 +++++ .../com/google/cloud/tools/jib/maven/DockerContextMojo.java | 5 +++++ 4 files changed, 21 insertions(+) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 4a3f5d14c1..d808050741 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -53,6 +53,11 @@ public class BuildDockerMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if ("pom".equals(getProject().getPackaging())) { + getLog().info("Skipping containerization because packaging is 'pom'..."); + return; + } + MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index ea24f9652f..e3508f7340 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -56,6 +56,12 @@ public class BuildImageMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { + // TODO: Consolidate all of these checks. + if ("pom".equals(getProject().getPackaging())) { + getLog().info("Skipping containerization because packaging is 'pom'..."); + return; + } + MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 708035876d..e9712f2379 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -55,6 +55,11 @@ public class BuildTarMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if ("pom".equals(getProject().getPackaging())) { + getLog().info("Skipping containerization because packaging is 'pom'..."); + return; + } + MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index bff24b268a..cebf59dea8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -47,6 +47,11 @@ public class DockerContextMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if ("pom".equals(getProject().getPackaging())) { + getLog().info("Skipping containerization because packaging is 'pom'..."); + return; + } + MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); handleDeprecatedParameters(mavenBuildLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); From a516426b2072bd7a96747eb50322f25e856edc51 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 30 Jul 2018 15:40:45 -0400 Subject: [PATCH 0069/2020] Split BuildConfiguration into multiple classes (#732) --- .../builder/BuildStepsIntegrationTest.java | 80 +++--- .../cloud/tools/jib/builder/BuildSteps.java | 12 +- .../jib/builder/steps/BuildImageStep.java | 15 +- .../jib/configuration/BuildConfiguration.java | 265 +++++------------- .../configuration/ContainerConfiguration.java | 181 ++++++++++++ .../jib/configuration/ImageConfiguration.java | 106 +++++++ .../jib/builder/steps/BuildImageStepTest.java | 16 +- .../configuration/BuildConfigurationTest.java | 118 +++++--- .../tools/jib/gradle/BuildDockerTask.java | 42 ++- .../tools/jib/gradle/BuildImageTask.java | 49 ++-- .../cloud/tools/jib/gradle/BuildTarTask.java | 42 ++- ...onfiguration.java => ImageParameters.java} | 6 +- .../cloud/tools/jib/gradle/JibExtension.java | 16 +- .../tools/jib/maven/BuildDockerMojo.java | 42 ++- .../cloud/tools/jib/maven/BuildImageMojo.java | 49 ++-- .../cloud/tools/jib/maven/BuildTarMojo.java | 42 ++- 16 files changed, 683 insertions(+), 398 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{ImageConfiguration.java => ImageParameters.java} (93%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index f824bb4f69..43409a919a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; @@ -88,18 +90,9 @@ public void testSteps_forBuildToDockerRegistry() CacheDirectoryNotOwnedException, CacheDirectoryCreationException { BuildSteps buildImageSteps = getBuildSteps( - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) - .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setJavaArguments(Collections.singletonList("An argument.")) - .setExposedPorts( - ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) - .setAllowInsecureRegistries(true) - .setLayerConfigurations(fakeLayerConfigurations) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - Collections.emptyList(), "HelloWorld")) - .build()); + getBuildConfiguration( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("localhost:5000", "testimage", "testtag"))); long lastTime = System.nanoTime(); buildImageSteps.run(); @@ -129,16 +122,9 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() CacheDirectoryCreationException, CacheMetadataCorruptedException, CacheDirectoryNotOwnedException { getBuildSteps( - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.parse("openjdk:8-jre-alpine")) - .setTargetImage(ImageReference.of("localhost:5000", "testimage", "testtag")) - .setJavaArguments(Collections.singletonList("An argument.")) - .setAllowInsecureRegistries(true) - .setLayerConfigurations(fakeLayerConfigurations) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - Collections.emptyList(), "HelloWorld")) - .build()) + getBuildConfiguration( + ImageReference.parse("openjdk:8-jre-alpine"), + ImageReference.of("localhost:5000", "testimage", "testtag"))) .run(); String imageReference = "localhost:5000/testimage:testtag"; @@ -152,18 +138,9 @@ public void testSteps_forBuildToDockerDaemon() throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { BuildConfiguration buildConfiguration = - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) - .setTargetImage(ImageReference.of(null, "testdocker", null)) - .setJavaArguments(Collections.singletonList("An argument.")) - .setExposedPorts( - ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) - .setLayerConfigurations(fakeLayerConfigurations) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - Collections.emptyList(), "HelloWorld")) - .build(); - + getBuildConfiguration( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of(null, "testdocker", null)); Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToDockerDaemon( buildConfiguration, @@ -188,16 +165,9 @@ public void testSteps_forBuildToTarball() throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { BuildConfiguration buildConfiguration = - BuildConfiguration.builder(logger) - .setBaseImage(ImageReference.of("gcr.io", "distroless/java", "latest")) - .setTargetImage(ImageReference.of(null, "testtar", null)) - .setJavaArguments(Collections.singletonList("An argument.")) - .setLayerConfigurations(fakeLayerConfigurations) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - Collections.emptyList(), "HelloWorld")) - .build(); - + getBuildConfiguration( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of(null, "testtar", null)); Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToTar( @@ -216,4 +186,26 @@ private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws I return BuildSteps.forBuildToDockerRegistry( buildConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)); } + + private BuildConfiguration getBuildConfiguration( + ImageReference baseImage, ImageReference targetImage) { + ImageConfiguration baseImageConfiguration = ImageConfiguration.builder(baseImage).build(); + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); + ContainerConfiguration containerConfiguration = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + Collections.emptyList(), "HelloWorld")) + .setProgramArguments(Collections.singletonList("An argument.")) + .setExposedPorts( + ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) + .build(); + return BuildConfiguration.builder(logger) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfiguration) + .setAllowInsecureRegistries(true) + .setLayerConfigurations(fakeLayerConfigurations) + .build(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index f5e414f483..144d8a9256 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -212,9 +212,13 @@ public void run() } } - buildConfiguration.getBuildLogger().lifecycle(""); - buildConfiguration - .getBuildLogger() - .lifecycle("Container entrypoint set to " + buildConfiguration.getEntrypoint()); + if (buildConfiguration.getContainerConfiguration() != null) { + buildConfiguration.getBuildLogger().lifecycle(""); + buildConfiguration + .getBuildLogger() + .lifecycle( + "Container entrypoint set to " + + buildConfiguration.getContainerConfiguration().getEntrypoint()); + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 9ebd5a4589..ebecdb31e1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -104,12 +105,16 @@ private Image afterCachedLayersSteps() // Start with environment from base image and overlay build configuration imageBuilder.addEnvironment( NonBlockingSteps.get(pullBaseImageStep).getBaseImage().getEnvironment()); - imageBuilder.addEnvironment(buildConfiguration.getEnvironment()); - imageBuilder.setCreated(buildConfiguration.getCreationTime()); - imageBuilder.setEntrypoint(buildConfiguration.getEntrypoint()); - imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments()); - imageBuilder.setExposedPorts(buildConfiguration.getExposedPorts()); + ContainerConfiguration containerConfiguration = + buildConfiguration.getContainerConfiguration(); + if (containerConfiguration != null) { + imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); + imageBuilder.setCreated(containerConfiguration.getCreationTime()); + imageBuilder.setEntrypoint(containerConfiguration.getEntrypoint()); + imageBuilder.setJavaArguments(containerConfiguration.getProgramArguments()); + imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); + } // Gets the container configuration content descriptor. return imageBuilder.build(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 3c824072f8..4e3f72e89d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -21,16 +21,10 @@ import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Objects; import javax.annotation.Nullable; import javax.lang.model.SourceVersion; @@ -40,23 +34,14 @@ public class BuildConfiguration { public static class Builder { // All the parameters below are set to their default values. - private Instant creationTime = Instant.EPOCH; - @Nullable private ImageReference baseImageReference; - @Nullable private String baseImageCredentialHelperName; - @Nullable private RegistryCredentials knownBaseRegistryCredentials; - @Nullable private ImageReference targetImageReference; - @Nullable private String targetImageCredentialHelperName; - @Nullable private RegistryCredentials knownTargetRegistryCredentials; - // TODO: Should rename to not be java-specific. - @Nullable private ImmutableList javaArguments; - @Nullable private ImmutableMap environmentMap; - @Nullable private ImmutableList exposedPorts; - private Class targetFormat = V22ManifestTemplate.class; + @Nullable private ImageConfiguration baseImageConfiguration; + @Nullable private ImageConfiguration targetImageConfiguration; + @Nullable private ContainerConfiguration containerConfiguration; @Nullable private CacheConfiguration applicationLayersCacheConfiguration; @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; private boolean allowInsecureRegistries = false; private ImmutableList layerConfigurations = ImmutableList.of(); - @Nullable private ImmutableList entrypoint; + private Class targetFormat = V22ManifestTemplate.class; private BuildLogger buildLogger; @@ -64,70 +49,36 @@ private Builder(BuildLogger buildLogger) { this.buildLogger = buildLogger; } - public Builder setBaseImage(@Nullable ImageReference imageReference) { - baseImageReference = imageReference; - return this; - } - - public Builder setTargetImage(@Nullable ImageReference imageReference) { - targetImageReference = imageReference; - return this; - } - - public Builder setBaseImageCredentialHelperName(@Nullable String credentialHelperName) { - baseImageCredentialHelperName = credentialHelperName; - return this; - } - - public Builder setTargetImageCredentialHelperName(@Nullable String credentialHelperName) { - targetImageCredentialHelperName = credentialHelperName; - return this; - } - - public Builder setKnownBaseRegistryCredentials( - @Nullable RegistryCredentials knownRegistryCrendentials) { - knownBaseRegistryCredentials = knownRegistryCrendentials; - return this; - } - - public Builder setKnownTargetRegistryCredentials( - @Nullable RegistryCredentials knownRegistryCrendentials) { - knownTargetRegistryCredentials = knownRegistryCrendentials; - return this; - } - - public Builder setJavaArguments(@Nullable List javaArguments) { - if (javaArguments != null) { - Preconditions.checkArgument(!javaArguments.contains(null)); - this.javaArguments = ImmutableList.copyOf(javaArguments); - } - return this; - } - - public Builder setEnvironment(@Nullable Map environmentMap) { - if (environmentMap == null) { - this.environmentMap = null; - } else { - Preconditions.checkArgument( - !Iterables.any(environmentMap.keySet(), Objects::isNull) - && !Iterables.any(environmentMap.values(), Objects::isNull)); - this.environmentMap = ImmutableMap.copyOf(environmentMap); - } + /** + * Sets the base image configuration. + * + * @param imageConfiguration the {@link ImageConfiguration} describing the base image + * @return this + */ + public Builder setBaseImageConfiguration(ImageConfiguration imageConfiguration) { + this.baseImageConfiguration = imageConfiguration; return this; } - public Builder setExposedPorts(@Nullable List exposedPorts) { - if (exposedPorts == null) { - this.exposedPorts = null; - } else { - Preconditions.checkArgument(!exposedPorts.contains(null)); - this.exposedPorts = ImmutableList.copyOf(exposedPorts); - } + /** + * Sets the target image configuration. + * + * @param imageConfiguration the {@link ImageConfiguration} describing the target image + * @return this + */ + public Builder setTargetImageConfiguration(ImageConfiguration imageConfiguration) { + this.targetImageConfiguration = imageConfiguration; return this; } - public Builder setTargetFormat(Class targetFormat) { - this.targetFormat = targetFormat; + /** + * Sets configuration parameters for the container. + * + * @param containerConfiguration the {@link ContainerConfiguration} + * @return this + */ + public Builder setContainerConfiguration(ContainerConfiguration containerConfiguration) { + this.containerConfiguration = containerConfiguration; return this; } @@ -155,6 +106,17 @@ public Builder setBaseImageLayersCacheConfiguration( return this; } + /** + * Sets the target format of the container image. + * + * @param targetFormat the target format + * @return this + */ + public Builder setTargetFormat(Class targetFormat) { + this.targetFormat = targetFormat; + return this; + } + /** * Sets whether or not to allow communication over HTTP (as opposed to HTTPS). * @@ -178,72 +140,42 @@ public Builder setLayerConfigurations(List layerConfiguratio } /** - * Sets the image creation time. - * - * @param creationTime the creation time - * @return this - */ - public Builder setCreationTime(Instant creationTime) { - this.creationTime = creationTime; - return this; - } - - /** - * Sets the container entrypoint. + * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. * - * @param entrypoint the tokenized command to run when the container starts - * @return this + * @return the corresponding build configuration */ - public Builder setEntrypoint(@Nullable List entrypoint) { - if (entrypoint == null) { - this.entrypoint = null; - } else { - Preconditions.checkArgument(!entrypoint.contains(null)); - this.entrypoint = ImmutableList.copyOf(entrypoint); - } - return this; - } - - /** @return the corresponding build configuration */ public BuildConfiguration build() { // Validates the parameters. List errorMessages = new ArrayList<>(); - if (baseImageReference == null) { - errorMessages.add("base image is required but not set"); + if (baseImageConfiguration == null) { + errorMessages.add("base image configuration is required but not set"); } - if (targetImageReference == null) { - errorMessages.add("target image is required but not set"); + if (targetImageConfiguration == null) { + errorMessages.add("target image configuration is required but not set"); } switch (errorMessages.size()) { case 0: // No errors - if (baseImageReference == null || targetImageReference == null) { + if (baseImageConfiguration == null || targetImageConfiguration == null) { throw new IllegalStateException("Required fields should not be null"); } - if (baseImageReference.usesDefaultTag()) { + if (baseImageConfiguration.getImage().usesDefaultTag()) { buildLogger.warn( "Base image '" - + baseImageReference + + baseImageConfiguration.getImage() + "' does not use a specific image digest - build may not be reproducible"); } + return new BuildConfiguration( buildLogger, - creationTime, - baseImageReference, - baseImageCredentialHelperName, - knownBaseRegistryCredentials, - targetImageReference, - targetImageCredentialHelperName, - knownTargetRegistryCredentials, - javaArguments, - environmentMap, - exposedPorts, - targetFormat, + baseImageConfiguration, + targetImageConfiguration, + containerConfiguration, applicationLayersCacheConfiguration, baseImageLayersCacheConfiguration, + targetFormat, allowInsecureRegistries, - layerConfigurations, - entrypoint); + layerConfigurations); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -276,69 +208,49 @@ public static Builder builder(BuildLogger buildLogger) { } private final BuildLogger buildLogger; - private final Instant creationTime; private final ImageReference baseImageReference; @Nullable private final String baseImageCredentialHelperName; @Nullable private final RegistryCredentials knownBaseRegistryCredentials; private final ImageReference targetImageReference; @Nullable private final String targetImageCredentialHelperName; @Nullable private final RegistryCredentials knownTargetRegistryCredentials; - @Nullable private final ImmutableList javaArguments; - @Nullable private final ImmutableMap environmentMap; - @Nullable private final ImmutableList exposedPorts; - private final Class targetFormat; + @Nullable private final ContainerConfiguration containerConfiguration; @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; + private Class targetFormat; private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; - @Nullable private final ImmutableList entrypoint; /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( BuildLogger buildLogger, - Instant creationTime, - ImageReference baseImageReference, - @Nullable String baseImageCredentialHelperName, - @Nullable RegistryCredentials knownBaseRegistryCredentials, - ImageReference targetImageReference, - @Nullable String targetImageCredentialHelperName, - @Nullable RegistryCredentials knownTargetRegistryCredentials, - @Nullable ImmutableList javaArguments, - @Nullable ImmutableMap environmentMap, - @Nullable ImmutableList exposedPorts, - Class targetFormat, + ImageConfiguration baseImageConfiguration, + ImageConfiguration targetImageConfiguration, + @Nullable ContainerConfiguration containerConfiguration, @Nullable CacheConfiguration applicationLayersCacheConfiguration, @Nullable CacheConfiguration baseImageLayersCacheConfiguration, + Class targetFormat, boolean allowInsecureRegistries, - ImmutableList layerConfigurations, - @Nullable ImmutableList entrypoint) { + ImmutableList layerConfigurations) { this.buildLogger = buildLogger; - this.creationTime = creationTime; - this.baseImageReference = baseImageReference; - this.baseImageCredentialHelperName = baseImageCredentialHelperName; - this.knownBaseRegistryCredentials = knownBaseRegistryCredentials; - this.targetImageReference = targetImageReference; - this.targetImageCredentialHelperName = targetImageCredentialHelperName; - this.knownTargetRegistryCredentials = knownTargetRegistryCredentials; - this.javaArguments = javaArguments; - this.environmentMap = environmentMap; - this.exposedPorts = exposedPorts; - this.targetFormat = targetFormat; + this.baseImageReference = baseImageConfiguration.getImage(); + this.baseImageCredentialHelperName = baseImageConfiguration.getCredentialHelper(); + this.knownBaseRegistryCredentials = baseImageConfiguration.getKnownRegistryCredentials(); + this.targetImageReference = targetImageConfiguration.getImage(); + this.targetImageCredentialHelperName = targetImageConfiguration.getCredentialHelper(); + this.knownTargetRegistryCredentials = targetImageConfiguration.getKnownRegistryCredentials(); + this.containerConfiguration = containerConfiguration; this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; + this.targetFormat = targetFormat; this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; - this.entrypoint = entrypoint; } public BuildLogger getBuildLogger() { return buildLogger; } - public Instant getCreationTime() { - return creationTime; - } - public ImageReference getBaseImageReference() { return baseImageReference; } @@ -391,34 +303,9 @@ public RegistryCredentials getKnownTargetRegistryCredentials() { return knownTargetRegistryCredentials; } - /** - * Gets the arguments to pass to the entrypoint. - * - * @return the list of arguments, or {@code null} if not set - */ - @Nullable - public ImmutableList getJavaArguments() { - return javaArguments; - } - - /** - * Gets the map from environment variable names to values for the container. - * - * @return the map of environment variables, or {@code null} if not set - */ - @Nullable - public ImmutableMap getEnvironment() { - return environmentMap; - } - - /** - * Gets the ports to expose on the container. - * - * @return the list of exposed ports, or {@code null} if not set - */ @Nullable - public ImmutableList getExposedPorts() { - return exposedPorts; + public ContainerConfiguration getContainerConfiguration() { + return containerConfiguration; } public Class getTargetFormat() { @@ -463,14 +350,4 @@ public boolean getAllowInsecureRegistries() { public ImmutableList getLayerConfigurations() { return layerConfigurations; } - - /** - * Gets the container entrypoint. - * - * @return the list of entrypoint tokens, or {@code null} if not set - */ - @Nullable - public ImmutableList getEntrypoint() { - return entrypoint; - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java new file mode 100644 index 0000000000..d8d750c209 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -0,0 +1,181 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; + +/** Immutable configuration options for the container. */ +public class ContainerConfiguration { + + /** Builder for instantiating a {@link ContainerConfiguration}. */ + public static class Builder { + + private Instant creationTime = Instant.EPOCH; + @Nullable private ImmutableList entrypoint; + @Nullable private ImmutableList programArguments; + @Nullable private ImmutableMap environmentMap; + @Nullable private ImmutableList exposedPorts; + + /** + * Sets the image creation time. + * + * @param creationTime the creation time + * @return this + */ + public Builder setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + + /** + * Sets the commandline arguments for main. + * + * @param programArguments the list of arguments + * @return this + */ + public Builder setProgramArguments(@Nullable List programArguments) { + if (programArguments == null) { + this.programArguments = null; + } else { + Preconditions.checkArgument(!programArguments.contains(null)); + this.programArguments = ImmutableList.copyOf(programArguments); + } + return this; + } + + /** + * Sets the container's environment variables, mapping variable name to value. + * + * @param environmentMap the map + * @return this + */ + public Builder setEnvironment(@Nullable Map environmentMap) { + if (environmentMap == null) { + this.environmentMap = null; + } else { + Preconditions.checkArgument(!Iterables.any(environmentMap.keySet(), Objects::isNull)); + Preconditions.checkArgument(!Iterables.any(environmentMap.values(), Objects::isNull)); + this.environmentMap = ImmutableMap.copyOf(environmentMap); + } + return this; + } + + /** + * Sets the container's exposed ports. + * + * @param exposedPorts the list of ports + * @return this + */ + public Builder setExposedPorts(@Nullable List exposedPorts) { + if (exposedPorts == null) { + this.exposedPorts = null; + } else { + Preconditions.checkArgument(!exposedPorts.contains(null)); + this.exposedPorts = ImmutableList.copyOf(exposedPorts); + } + return this; + } + + /** + * Sets the container entrypoint. + * + * @param entrypoint the tokenized command to run when the container starts + * @return this + */ + public Builder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint == null) { + this.entrypoint = null; + } else { + Preconditions.checkArgument(!entrypoint.contains(null)); + this.entrypoint = ImmutableList.copyOf(entrypoint); + } + return this; + } + + /** + * Builds the {@link ContainerConfiguration}. + * + * @return the corresponding {@link ContainerConfiguration} + */ + public ContainerConfiguration build() { + return new ContainerConfiguration( + creationTime, entrypoint, programArguments, environmentMap, exposedPorts); + } + + private Builder() {} + } + + /** + * Constructs a builder for a {@link ContainerConfiguration}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + private final Instant creationTime; + @Nullable private final ImmutableList entrypoint; + @Nullable private final ImmutableList programArguments; + @Nullable private final ImmutableMap environmentMap; + @Nullable private final ImmutableList exposedPorts; + + private ContainerConfiguration( + Instant creationTime, + @Nullable ImmutableList entrypoint, + @Nullable ImmutableList programArguments, + @Nullable ImmutableMap environmentMap, + @Nullable ImmutableList exposedPorts) { + this.creationTime = creationTime; + this.entrypoint = entrypoint; + this.programArguments = programArguments; + this.environmentMap = environmentMap; + this.exposedPorts = exposedPorts; + } + + public Instant getCreationTime() { + return creationTime; + } + + @Nullable + public ImmutableList getEntrypoint() { + return entrypoint; + } + + @Nullable + public ImmutableList getProgramArguments() { + return programArguments; + } + + @Nullable + public ImmutableMap getEnvironmentMap() { + return environmentMap; + } + + @Nullable + public ImmutableList getExposedPorts() { + return exposedPorts; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java new file mode 100644 index 0000000000..e4ae598c15 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -0,0 +1,106 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import javax.annotation.Nullable; + +/** Immutable configuration options for an image reference with credentials. */ +public class ImageConfiguration { + + /** Builder for instantiating an {@link ImageConfiguration}. */ + public static class Builder { + + private ImageReference imageReference; + @Nullable private String credentialHelper; + @Nullable private RegistryCredentials knownRegistryCredentials; + + /** + * Sets the credential helper name used for authenticating with the image's registry. + * + * @param credentialHelper the credential helper's suffix. + * @return this + */ + public Builder setCredentialHelper(@Nullable String credentialHelper) { + this.credentialHelper = credentialHelper; + return this; + } + + /** + * Sets known credentials used for authenticating with the image's registry. + * + * @param knownRegistryCredentials the credentials. + * @return this + */ + public Builder setKnownRegistryCredentials( + @Nullable RegistryCredentials knownRegistryCredentials) { + this.knownRegistryCredentials = knownRegistryCredentials; + return this; + } + + /** + * Builds the {@link ImageConfiguration}. + * + * @return the corresponding {@link ImageConfiguration} + */ + public ImageConfiguration build() { + return new ImageConfiguration(imageReference, credentialHelper, knownRegistryCredentials); + } + + private Builder(ImageReference imageReference) { + this.imageReference = imageReference; + } + } + + /** + * Constructs a builder for an {@link ImageConfiguration}. + * + * @param imageReference the image reference, which is a required field + * @return the builder + */ + public static Builder builder(ImageReference imageReference) { + return new Builder(imageReference); + } + + private final ImageReference image; + @Nullable private final String credentialHelper; + @Nullable private final RegistryCredentials knownRegistryCredentials; + + private ImageConfiguration( + ImageReference image, + @Nullable String credentialHelper, + @Nullable RegistryCredentials knownRegistryCredentials) { + this.image = image; + this.credentialHelper = credentialHelper; + this.knownRegistryCredentials = knownRegistryCredentials; + } + + public ImageReference getImage() { + return image; + } + + @Nullable + public String getCredentialHelper() { + return credentialHelper; + } + + @Nullable + public RegistryCredentials getKnownRegistryCredentials() { + return knownRegistryCredentials; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 188989484b..2b2a2b5164 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -46,6 +47,7 @@ public class BuildImageStepTest { @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private ContainerConfiguration mockContainerConfiguration; @Mock private BuildLogger mockBuildLogger; @Mock private PullBaseImageStep mockPullBaseImageStep; @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; @@ -66,11 +68,13 @@ public void setUp() throws DigestException { null); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockBuildConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); - Mockito.when(mockBuildConfiguration.getEnvironment()).thenReturn(ImmutableMap.of()); - Mockito.when(mockBuildConfiguration.getJavaArguments()).thenReturn(ImmutableList.of()); - Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); - Mockito.when(mockBuildConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Mockito.when(mockBuildConfiguration.getContainerConfiguration()) + .thenReturn(mockContainerConfiguration); + Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); + Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); Image baseImage = Image.builder().addEnvironment(ImmutableMap.of("NAME", "VALUE")).build(); @@ -111,7 +115,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup @Test public void test_propagateBaseImageConfiguration() throws ExecutionException, InterruptedException { - Mockito.when(mockBuildConfiguration.getEnvironment()) + Mockito.when(mockContainerConfiguration.getEnvironmentMap()) .thenReturn(ImmutableMap.of("BASE", "IMAGE")); BuildImageStep buildImageStep = new BuildImageStep( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 3af886e7d6..55d28c41ee 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -42,7 +42,6 @@ public class BuildConfigurationTest { @Test public void testBuilder() { - Instant expectedCreationTime = Instant.ofEpochSecond(10000); String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; String expectedBaseImageTag = "baseimagetag"; @@ -55,6 +54,8 @@ public void testBuilder() { String expectedTargetImageCredentialHelperName = "anotherCredentialHelper"; RegistryCredentials expectedKnownTargetRegistryCredentials = Mockito.mock(RegistryCredentials.class); + Instant expectedCreationTime = Instant.ofEpochSecond(10000); + List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedJavaArguments = Arrays.asList("arg1", "arg2"); Map expectedEnvironment = ImmutableMap.of("key", "value"); ImmutableList expectedExposedPorts = @@ -67,33 +68,44 @@ public void testBuilder() { List expectedLayerConfigurations = Collections.singletonList( LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build()); - List expectedEntrypoint = Arrays.asList("some", "entrypoint"); - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setCreationTime(expectedCreationTime) - .setBaseImage( + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder( ImageReference.of( expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) - .setBaseImageCredentialHelperName(expectedBaseImageCredentialHelperName) - .setKnownBaseRegistryCredentials(expectedKnownBaseRegistryCredentials) - .setTargetImage( + .setCredentialHelper(expectedBaseImageCredentialHelperName) + .setKnownRegistryCredentials(expectedKnownBaseRegistryCredentials) + .build(); + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder( ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) - .setTargetImageCredentialHelperName(expectedTargetImageCredentialHelperName) - .setKnownTargetRegistryCredentials(expectedKnownTargetRegistryCredentials) - .setJavaArguments(expectedJavaArguments) + .setCredentialHelper(expectedTargetImageCredentialHelperName) + .setKnownRegistryCredentials(expectedKnownTargetRegistryCredentials) + .build(); + ContainerConfiguration containerConfiguration = + ContainerConfiguration.builder() + .setCreationTime(expectedCreationTime) + .setEntrypoint(expectedEntrypoint) + .setProgramArguments(expectedJavaArguments) .setEnvironment(expectedEnvironment) .setExposedPorts(expectedExposedPorts) - .setTargetFormat(OCIManifestTemplate.class) + .build(); + BuildConfiguration.Builder buildConfigurationBuilder = + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfiguration) .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) + .setTargetFormat(OCIManifestTemplate.class) .setAllowInsecureRegistries(true) - .setLayerConfigurations(expectedLayerConfigurations) - .setEntrypoint(expectedEntrypoint); + .setLayerConfigurations(expectedLayerConfigurations); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - Assert.assertEquals(expectedCreationTime, buildConfiguration.getCreationTime()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + expectedCreationTime, buildConfiguration.getContainerConfiguration().getCreationTime()); Assert.assertEquals(expectedBaseImageServerUrl, buildConfiguration.getBaseImageRegistry()); Assert.assertEquals(expectedBaseImageName, buildConfiguration.getBaseImageRepository()); Assert.assertEquals(expectedBaseImageTag, buildConfiguration.getBaseImageTag()); @@ -106,9 +118,13 @@ public void testBuilder() { Assert.assertEquals( expectedTargetImageCredentialHelperName, buildConfiguration.getTargetImageCredentialHelperName()); - Assert.assertEquals(expectedJavaArguments, buildConfiguration.getJavaArguments()); - Assert.assertEquals(expectedEnvironment, buildConfiguration.getEnvironment()); - Assert.assertEquals(expectedExposedPorts, buildConfiguration.getExposedPorts()); + Assert.assertEquals( + expectedJavaArguments, + buildConfiguration.getContainerConfiguration().getProgramArguments()); + Assert.assertEquals( + expectedEnvironment, buildConfiguration.getContainerConfiguration().getEnvironmentMap()); + Assert.assertEquals( + expectedExposedPorts, buildConfiguration.getContainerConfiguration().getExposedPorts()); Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); Assert.assertEquals( expectedApplicationLayersCacheConfiguration, @@ -118,7 +134,8 @@ public void testBuilder() { buildConfiguration.getBaseImageLayersCacheConfiguration()); Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); - Assert.assertEquals(expectedEntrypoint, buildConfiguration.getEntrypoint()); + Assert.assertEquals( + expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); } @Test @@ -131,30 +148,33 @@ public void testBuilder_default() { String expectedTargetImageName = "targetimage"; String expectedTargetTag = "targettag"; - BuildConfiguration buildConfiguration = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage( + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder( ImageReference.of( expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) - .setTargetImage( + .build(); + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder( ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .build(); + BuildConfiguration buildConfiguration = + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .build(); - Assert.assertEquals(buildConfiguration.getCreationTime(), Instant.EPOCH); Assert.assertNull(buildConfiguration.getBaseImageCredentialHelperName()); Assert.assertNull(buildConfiguration.getKnownBaseRegistryCredentials()); Assert.assertNull(buildConfiguration.getTargetImageCredentialHelperName()); Assert.assertNull(buildConfiguration.getKnownTargetRegistryCredentials()); - Assert.assertNull(buildConfiguration.getJavaArguments()); - Assert.assertNull(buildConfiguration.getEnvironment()); - Assert.assertNull(buildConfiguration.getExposedPorts()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals(buildConfiguration.getTargetFormat(), V22ManifestTemplate.class); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); - Assert.assertNull(buildConfiguration.getEntrypoint()); } @Test @@ -162,12 +182,13 @@ public void testBuilder_missingValues() { // Target image is missing try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setBaseImage(Mockito.mock(ImageReference.class)) + .setBaseImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .build(); Assert.fail("Build configuration should not be built with missing values"); } catch (IllegalStateException ex) { - Assert.assertEquals("target image is required but not set", ex.getMessage()); + Assert.assertEquals("target image configuration is required but not set", ex.getMessage()); } // All required fields missing @@ -177,7 +198,7 @@ public void testBuilder_missingValues() { } catch (IllegalStateException ex) { Assert.assertEquals( - "base image is required but not set and target image is required but not set", + "base image configuration is required but not set and target image configuration is required but not set", ex.getMessage()); } } @@ -188,7 +209,10 @@ public void testBuilder_nullValues() { // Java arguments element should not be null. try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setJavaArguments(Arrays.asList("first", null)); + .setContainerConfiguration( + ContainerConfiguration.builder() + .setProgramArguments(Arrays.asList("first", null)) + .build()); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); @@ -197,7 +221,8 @@ public void testBuilder_nullValues() { // Entrypoint element should not be null. try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setEntrypoint(Arrays.asList("first", null)); + .setContainerConfiguration( + ContainerConfiguration.builder().setEntrypoint(Arrays.asList("first", null)).build()); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); @@ -206,7 +231,10 @@ public void testBuilder_nullValues() { // Exposed ports element should not be null. try { BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) - .setExposedPorts(Arrays.asList(new Port(1000, Protocol.TCP), null)); + .setContainerConfiguration( + ContainerConfiguration.builder() + .setExposedPorts(Arrays.asList(new Port(1000, Protocol.TCP), null)) + .build()); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); @@ -217,7 +245,9 @@ public void testBuilder_nullValues() { nullKeyMap.put(null, "value"); try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(nullKeyMap); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setContainerConfiguration( + ContainerConfiguration.builder().setEnvironment(nullKeyMap).build()); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); @@ -227,18 +257,26 @@ public void testBuilder_nullValues() { Map nullValueMap = new HashMap<>(); nullValueMap.put("key", null); try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(nullValueMap); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setContainerConfiguration( + ContainerConfiguration.builder().setEnvironment(nullValueMap).build()); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { Assert.assertNull(ex.getMessage()); } // Can accept empty environment. - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(ImmutableMap.of()); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setContainerConfiguration( + ContainerConfiguration.builder().setEnvironment(ImmutableMap.of()).build()); // Environment map can accept TreeMap and Hashtable. - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(new TreeMap<>()); - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).setEnvironment(new Hashtable<>()); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setContainerConfiguration( + ContainerConfiguration.builder().setEnvironment(new TreeMap<>()).build()); + BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + .setContainerConfiguration( + ContainerConfiguration.builder().setEnvironment(new Hashtable<>()).build()); } @Test diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 14690d5f24..adba0a3672 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; @@ -105,19 +107,34 @@ public void buildDocker() throws InvalidImageReferenceException { // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageTask. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) + .setCredentialHelper(jibExtension.getFrom().getCredHelper()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)) + .setProgramArguments(jibExtension.getArgs()) + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(gradleBuildLogger) - .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) - .setTargetImage(targetImage) - .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setJavaArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)); + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -126,11 +143,6 @@ public void buildDocker() throws InvalidImageReferenceException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 8ad605c99b..999d68fbbc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -110,22 +112,40 @@ public void buildImage() throws InvalidImageReferenceException { String mainClass = gradleProjectProperties.getMainClass(jibExtension); // Builds the BuildConfiguration. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) + .setCredentialHelper(jibExtension.getFrom().getCredHelper()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder(ImageReference.parse(jibExtension.getTargetImage())) + .setCredentialHelper(jibExtension.getTo().getCredHelper()) + .setKnownRegistryCredentials(knownTargetRegistryCredentials) + .build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)) + .setProgramArguments(jibExtension.getArgs()) + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(gradleBuildLogger) - .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) - .setTargetImage(ImageReference.parse(jibExtension.getTargetImage())) - .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setTargetImageCredentialHelperName(jibExtension.getTo().getCredHelper()) - .setKnownTargetRegistryCredentials(knownTargetRegistryCredentials) - .setJavaArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setTargetFormat(jibExtension.getFormat()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)); + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); + CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -134,11 +154,6 @@ public void buildImage() throws InvalidImageReferenceException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 7eccbf213a..6943704065 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -133,19 +135,34 @@ public void buildTar() throws InvalidImageReferenceException { // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageTask/BuildDockerTask. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) + .setCredentialHelper(jibExtension.getFrom().getCredHelper()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)) + .setProgramArguments(jibExtension.getArgs()) + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); + if (jibExtension.getUseCurrentTimestamp()) { + gradleBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(gradleBuildLogger) - .setBaseImage(ImageReference.parse(jibExtension.getBaseImage())) - .setTargetImage(targetImage) - .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setJavaArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)); + .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -154,11 +171,6 @@ public void buildTar() throws InvalidImageReferenceException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java similarity index 93% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java index 4170306afc..255df4b230 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java @@ -34,7 +34,7 @@ *

{@code image} (required) is the image reference and {@code credHelper} (optional) is the name * (after {@code docker-credential} of the credential helper for accessing the {@code image}. */ -public class ImageConfiguration { +public class ImageParameters { private AuthConfiguration auth; @@ -42,7 +42,7 @@ public class ImageConfiguration { @Nullable private String credHelper; @Inject - public ImageConfiguration(ObjectFactory objectFactory) { + public ImageParameters(ObjectFactory objectFactory) { auth = objectFactory.newInstance(AuthConfiguration.class); } @@ -78,7 +78,7 @@ public void auth(Action action) { action.execute(auth); } - /** Converts the {@link ImageConfiguration} to an {@link Authorization}. */ + /** Converts the {@link ImageParameters} to an {@link Authorization}. */ @Internal @Nullable Authorization getImageAuthorization() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 11a98ea5bb..75d7f82027 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -72,8 +72,8 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { return projectDirectory.resolve("src").resolve("main").resolve("jib"); } - private final ImageConfiguration from; - private final ImageConfiguration to; + private final ImageParameters from; + private final ImageParameters to; private final ContainerParameters container; private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; @@ -91,8 +91,8 @@ public JibExtension(Project project) { projectDir = project.getProjectDir().toPath(); ObjectFactory objectFactory = project.getObjects(); - from = objectFactory.newInstance(ImageConfiguration.class); - to = objectFactory.newInstance(ImageConfiguration.class); + from = objectFactory.newInstance(ImageParameters.class); + to = objectFactory.newInstance(ImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); jvmFlags = objectFactory.listProperty(String.class); @@ -152,11 +152,11 @@ void handleDeprecatedParameters(BuildLogger logger) { } } - public void from(Action action) { + public void from(Action action) { action.execute(from); } - public void to(Action action) { + public void to(Action action) { action.execute(to); } @@ -205,13 +205,13 @@ String getTargetImage() { @Nested @Optional - public ImageConfiguration getFrom() { + public ImageParameters getFrom() { return from; } @Nested @Optional - public ImageConfiguration getTo() { + public ImageParameters getTo() { return to; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index d808050741..f15c62000d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; @@ -93,19 +95,34 @@ public void execute() throws MojoExecutionException { // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageMojo. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(baseImage) + .setCredentialHelper(getBaseImageCredentialHelperName()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) + .setProgramArguments(getArgs()) + .setEnvironment(getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(mavenBuildLogger) - .setBaseImage(baseImage) - .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setTargetImage(targetImage) - .setJavaArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -114,11 +131,6 @@ public void execute() throws MojoExecutionException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index e3508f7340..12e0f76c10 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -117,22 +119,40 @@ public void execute() throws MojoExecutionException, MojoFailureException { String mainClass = mavenProjectProperties.getMainClass(this); // Builds the BuildConfiguration. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(baseImage) + .setCredentialHelper(getBaseImageCredentialHelperName()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = + ImageConfiguration.builder(targetImage) + .setCredentialHelper(getTargetImageCredentialHelperName()) + .setKnownRegistryCredentials(knownTargetRegistryCredentials) + .build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) + .setProgramArguments(getArgs()) + .setEnvironment(getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(mavenBuildLogger) - .setBaseImage(baseImage) - .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setTargetImage(targetImage) - .setTargetImageCredentialHelperName(getTargetImageCredentialHelperName()) - .setKnownTargetRegistryCredentials(knownTargetRegistryCredentials) - .setJavaArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); + CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -141,11 +161,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index e9712f2379..2b4ac0267e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -91,19 +93,34 @@ public void execute() throws MojoExecutionException { // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageMojo. + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(baseImage) + .setCredentialHelper(getBaseImageCredentialHelperName()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .build(); + + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); + + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) + .setProgramArguments(getArgs()) + .setEnvironment(getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); + if (getUseCurrentTimestamp()) { + mavenBuildLogger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(mavenBuildLogger) - .setBaseImage(baseImage) - .setBaseImageCredentialHelperName(getBaseImageCredentialHelperName()) - .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) - .setTargetImage(targetImage) - .setJavaArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())) + .setBaseImageConfiguration(baseImageConfiguration) + .setTargetImageConfiguration(targetImageConfiguration) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)); + .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( @@ -112,11 +129,6 @@ public void execute() throws MojoExecutionException { buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( applicationLayersCacheConfiguration); } - if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - buildConfigurationBuilder.setCreationTime(Instant.now()); - } BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); From 5cf24f2e0963cd99a011835cbc728b02f03fd966 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 30 Jul 2018 16:21:10 -0400 Subject: [PATCH 0070/2020] Updates and fixes CHANGELOG entries. (#740) --- jib-gradle-plugin/CHANGELOG.md | 7 +++++-- jib-maven-plugin/CHANGELOG.md | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3fd325eba8..c4f9b23e0c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,10 +9,13 @@ All notable changes to this project will be documented in this file. ### Changed -- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) - Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +### Fixed + +- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) + ## 0.9.7 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 8c733b903c..02fbd536c2 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,14 +7,17 @@ All notable changes to this project will be documented in this file. - `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) - Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) ### Changed - Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +- Skips execution if packaging is `pom` ([#735](https://github.com/GoogleContainerTools/jib/pull/735)) ### Fixed - Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) ## 0.9.7 From 47e83d789e4bdaea87467f6f171996b8deec7592 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 30 Jul 2018 16:39:28 -0400 Subject: [PATCH 0071/2020] Remove unnecessary jib-core rebuilding (#742) --- build.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.sh b/build.sh index 70485ca565..4c5c8534a7 100755 --- a/build.sh +++ b/build.sh @@ -57,7 +57,6 @@ for target in "$@"; do maven) if [ "$quickMode" = false ]; then - doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U else # jib-maven-plugin pulls in jib-core directly @@ -67,7 +66,6 @@ for target in "$@"; do gradle) if [ "$quickMode" = false ]; then - doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build doBuild jib-gradle-plugin ./gradlew $gradleOptions googleJavaFormat build install else # jib-gradle-plugin pulls in jib-core directly From d84b88f53d995261202a4173e7b89b289a553eb6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 30 Jul 2018 17:43:57 -0400 Subject: [PATCH 0072/2020] Move image-related getters in BuildConfiguration to ImageConfiguration (#743) --- .../cloud/tools/jib/builder/BuildSteps.java | 10 +-- .../builder/steps/AuthenticatePushStep.java | 8 ++- .../jib/builder/steps/LoadDockerStep.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 4 +- .../jib/builder/steps/PullBaseImageStep.java | 17 +++-- .../tools/jib/builder/steps/PushBlobStep.java | 4 +- .../jib/builder/steps/PushImageStep.java | 7 +- .../RetrieveRegistryCredentialsStep.java | 12 ++-- .../jib/builder/steps/WriteTarFileStep.java | 2 +- .../jib/configuration/BuildConfiguration.java | 70 +++---------------- .../jib/configuration/ImageConfiguration.java | 12 ++++ .../tools/jib/frontend/BuildStepsRunner.java | 18 ++--- .../RetrieveRegistryCredentialsStepTest.java | 10 +-- .../configuration/BuildConfigurationTest.java | 34 +++++---- .../jib/frontend/BuildStepsRunnerTest.java | 13 ++-- 15 files changed, 103 insertions(+), 120 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 144d8a9256..851eadc67c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -74,10 +74,10 @@ public static BuildSteps forBuildToDockerRegistry( cachesInitializer, String.format( STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageReference()), + buildConfiguration.getTargetImageConfiguration().getImage()), String.format( SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageReference()), + buildConfiguration.getTargetImageConfiguration().getImage()), stepsRunner -> stepsRunner .runRetrieveTargetRegistryCredentialsStep() @@ -108,9 +108,11 @@ public static BuildSteps forBuildToDockerDaemon( buildConfiguration, cachesInitializer, String.format( - STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, buildConfiguration.getTargetImageReference()), + STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, + buildConfiguration.getTargetImageConfiguration().getImage()), String.format( - SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, buildConfiguration.getTargetImageReference()), + SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, + buildConfiguration.getTargetImageConfiguration().getImage()), stepsRunner -> stepsRunner .runPullBaseImageStep() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 2503d6d432..57f18c439c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -73,14 +73,16 @@ public Authorization call() try (Timer ignored = new Timer( buildConfiguration.getBuildLogger(), - String.format(DESCRIPTION, buildConfiguration.getTargetImageRegistry()))) { + String.format( + DESCRIPTION, + buildConfiguration.getTargetImageConfiguration().getImageRegistry()))) { Authorization registryCredentials = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) + buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 17c5f888be..0b7a989f1a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -91,7 +91,7 @@ private Void afterPushBaseImageLayerFuturesFuture() new DockerClient() .load( new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageReference())); + .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage())); return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 387738b1d1..b456b3817f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -71,8 +71,8 @@ public CachedLayer call() throws IOException, RegistryException { new Timer(buildConfiguration.getBuildLogger(), String.format(DESCRIPTION, layerDigest))) { RegistryClient registryClient = RegistryClient.factory( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) + buildConfiguration.getBaseImageConfiguration().getImageRegistry(), + buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(pullAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 3d758a82f4..e454e1b308 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -103,7 +103,10 @@ public BaseImageWithAuthorization call() RegistryAuthenticationFailedException { buildConfiguration .getBuildLogger() - .lifecycle("Getting base image " + buildConfiguration.getBaseImageReference() + "..."); + .lifecycle( + "Getting base image " + + buildConfiguration.getBaseImageConfiguration().getImage() + + "..."); try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { // First, try with no credentials. @@ -115,7 +118,7 @@ public BaseImageWithAuthorization call() .getBuildLogger() .lifecycle( "The base image requires auth. Trying again for " - + buildConfiguration.getBaseImageReference() + + buildConfiguration.getBaseImageConfiguration().getImage() + "..."); // If failed, then, retrieve base registry credentials and try with retrieved credentials. @@ -137,8 +140,8 @@ public BaseImageWithAuthorization call() // See https://docs.docker.com/registry/spec/auth/token RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) + buildConfiguration.getBaseImageConfiguration().getImageRegistry(), + buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { @@ -176,14 +179,14 @@ private Image pullBaseImage(@Nullable Authorization registryCredentials) LayerCountMismatchException, BadContainerConfigurationFormatException { RegistryClient registryClient = RegistryClient.factory( - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageRepository()) + buildConfiguration.getBaseImageConfiguration().getImageRegistry(), + buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(registryCredentials) .newRegistryClient(); ManifestTemplate manifestTemplate = - registryClient.pullManifest(buildConfiguration.getBaseImageTag()); + registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); // TODO: Make schema version be enum. switch (manifestTemplate.getSchemaVersion()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index cc3de6423e..0bd1099e1b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -70,8 +70,8 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = RegistryClient.factory( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) + buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index d818367e9d..15760d9d98 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -118,8 +118,8 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { RegistryClient registryClient = RegistryClient.factory( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository()) + buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); @@ -134,7 +134,8 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx buildConfiguration.getTargetFormat(), NonBlockingSteps.get( NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)))); - registryClient.pushManifest(manifestTemplate, buildConfiguration.getTargetImageTag()); + registryClient.pushManifest( + manifestTemplate, buildConfiguration.getTargetImageConfiguration().getImageTag()); } return null; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 55102168b8..5592bb2cce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -52,9 +52,9 @@ static RetrieveRegistryCredentialsStep forBaseImage( return new RetrieveRegistryCredentialsStep( listeningExecutorService, buildConfiguration.getBuildLogger(), - buildConfiguration.getBaseImageRegistry(), - buildConfiguration.getBaseImageCredentialHelperName(), - buildConfiguration.getKnownBaseRegistryCredentials()); + buildConfiguration.getBaseImageConfiguration().getImageRegistry(), + buildConfiguration.getBaseImageConfiguration().getCredentialHelper(), + buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials()); } /** Retrieves credentials for the target image. */ @@ -63,9 +63,9 @@ static RetrieveRegistryCredentialsStep forTargetImage( return new RetrieveRegistryCredentialsStep( listeningExecutorService, buildConfiguration.getBuildLogger(), - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageCredentialHelperName(), - buildConfiguration.getKnownTargetRegistryCredentials()); + buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + buildConfiguration.getTargetImageConfiguration().getCredentialHelper(), + buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials()); } private final BuildLogger buildLogger; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 237efc2c60..6181c7bc5c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -95,7 +95,7 @@ private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, I Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outputPath))) { new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageReference()) + .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage()) .writeTo(outputStream); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 4e3f72e89d..c27cfdbebf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -17,10 +17,8 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -208,12 +206,8 @@ public static Builder builder(BuildLogger buildLogger) { } private final BuildLogger buildLogger; - private final ImageReference baseImageReference; - @Nullable private final String baseImageCredentialHelperName; - @Nullable private final RegistryCredentials knownBaseRegistryCredentials; - private final ImageReference targetImageReference; - @Nullable private final String targetImageCredentialHelperName; - @Nullable private final RegistryCredentials knownTargetRegistryCredentials; + private final ImageConfiguration baseImageConfiguration; + private final ImageConfiguration targetImageConfiguration; @Nullable private final ContainerConfiguration containerConfiguration; @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; @@ -233,12 +227,8 @@ private BuildConfiguration( boolean allowInsecureRegistries, ImmutableList layerConfigurations) { this.buildLogger = buildLogger; - this.baseImageReference = baseImageConfiguration.getImage(); - this.baseImageCredentialHelperName = baseImageConfiguration.getCredentialHelper(); - this.knownBaseRegistryCredentials = baseImageConfiguration.getKnownRegistryCredentials(); - this.targetImageReference = targetImageConfiguration.getImage(); - this.targetImageCredentialHelperName = targetImageConfiguration.getCredentialHelper(); - this.knownTargetRegistryCredentials = targetImageConfiguration.getKnownRegistryCredentials(); + this.baseImageConfiguration = baseImageConfiguration; + this.targetImageConfiguration = targetImageConfiguration; this.containerConfiguration = containerConfiguration; this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; @@ -251,56 +241,12 @@ public BuildLogger getBuildLogger() { return buildLogger; } - public ImageReference getBaseImageReference() { - return baseImageReference; + public ImageConfiguration getBaseImageConfiguration() { + return baseImageConfiguration; } - public String getBaseImageRegistry() { - return baseImageReference.getRegistry(); - } - - public String getBaseImageRepository() { - return baseImageReference.getRepository(); - } - - public String getBaseImageTag() { - return baseImageReference.getTag(); - } - - @Nullable - public String getBaseImageCredentialHelperName() { - return baseImageCredentialHelperName; - } - - @Nullable - public RegistryCredentials getKnownBaseRegistryCredentials() { - return knownBaseRegistryCredentials; - } - - public ImageReference getTargetImageReference() { - return targetImageReference; - } - - public String getTargetImageRegistry() { - return targetImageReference.getRegistry(); - } - - public String getTargetImageRepository() { - return targetImageReference.getRepository(); - } - - public String getTargetImageTag() { - return targetImageReference.getTag(); - } - - @Nullable - public String getTargetImageCredentialHelperName() { - return targetImageCredentialHelperName; - } - - @Nullable - public RegistryCredentials getKnownTargetRegistryCredentials() { - return knownTargetRegistryCredentials; + public ImageConfiguration getTargetImageConfiguration() { + return targetImageConfiguration; } @Nullable diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index e4ae598c15..532e30fbe8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -94,6 +94,18 @@ public ImageReference getImage() { return image; } + public String getImageRegistry() { + return image.getRegistry(); + } + + public String getImageRepository() { + return image.getRepository(); + } + + public String getImageTag() { + return image.getTag(); + } + @Nullable public String getCredentialHelper() { return credentialHelper; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 2cfbe0573a..71ccc88365 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -125,17 +125,19 @@ private static void handleRegistryUnauthorizedException( boolean isRegistryForBase = registryUnauthorizedException .getRegistry() - .equals(buildConfiguration.getBaseImageRegistry()); + .equals(buildConfiguration.getBaseImageConfiguration().getImageRegistry()); boolean isRegistryForTarget = registryUnauthorizedException .getRegistry() - .equals(buildConfiguration.getTargetImageRegistry()); + .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()); boolean areBaseImageCredentialsConfigured = - buildConfiguration.getBaseImageCredentialHelperName() != null - || buildConfiguration.getKnownBaseRegistryCredentials() != null; + buildConfiguration.getBaseImageConfiguration().getCredentialHelper() != null + || buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials() + != null; boolean areTargetImageCredentialsConfigured = - buildConfiguration.getTargetImageCredentialHelperName() != null - || buildConfiguration.getKnownTargetRegistryCredentials() != null; + buildConfiguration.getTargetImageConfiguration().getCredentialHelper() != null + || buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials() + != null; if (isRegistryForBase && !areBaseImageCredentialsConfigured) { throw new BuildStepsExecutionException( @@ -233,8 +235,8 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { handleRegistryUnauthorizedException( new RegistryUnauthorizedException( - buildConfiguration.getTargetImageRegistry(), - buildConfiguration.getTargetImageRepository(), + buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + buildConfiguration.getTargetImageConfiguration().getImageRepository(), (HttpResponseException) exceptionDuringBuildSteps.getCause()), buildConfiguration, helpfulSuggestions); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 36e84fc516..1683047111 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; @@ -43,7 +43,7 @@ public class RetrieveRegistryCredentialsStepTest { private static final String FAKE_TARGET_REGISTRY = "someRegistry"; @Mock private ListeningExecutorService mockListeningExecutorService; - @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private ImageConfiguration mockImageConfiguration; @Mock private BuildLogger mockBuildLogger; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @@ -149,8 +149,8 @@ public void testCall_inferCommonCredentialHelpers() Mockito.verify(mockBuildLogger).info("Using docker-credential-gcr for something.gcr.io"); Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); - Assert.assertEquals( - null, makeRetrieveRegistryCredentialsStep("something.amazonaws.com", null, null).call()); + Assert.assertNull( + makeRetrieveRegistryCredentialsStep("something.amazonaws.com", null, null).call()); Mockito.verify(mockBuildLogger).warn("warning"); } @@ -159,7 +159,7 @@ private RetrieveRegistryCredentialsStep makeRetrieveRegistryCredentialsStep( String registry, @Nullable String credentialHelperSuffix, @Nullable RegistryCredentials knownRegistryCredentials) { - Mockito.when(mockBuildConfiguration.getTargetImageRegistry()).thenReturn(FAKE_TARGET_REGISTRY); + Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn(FAKE_TARGET_REGISTRY); return new RetrieveRegistryCredentialsStep( mockListeningExecutorService, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 55d28c41ee..88c598e779 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -106,18 +106,27 @@ public void testBuilder() { Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( expectedCreationTime, buildConfiguration.getContainerConfiguration().getCreationTime()); - Assert.assertEquals(expectedBaseImageServerUrl, buildConfiguration.getBaseImageRegistry()); - Assert.assertEquals(expectedBaseImageName, buildConfiguration.getBaseImageRepository()); - Assert.assertEquals(expectedBaseImageTag, buildConfiguration.getBaseImageTag()); + Assert.assertEquals( + expectedBaseImageServerUrl, + buildConfiguration.getBaseImageConfiguration().getImageRegistry()); + Assert.assertEquals( + expectedBaseImageName, buildConfiguration.getBaseImageConfiguration().getImageRepository()); + Assert.assertEquals( + expectedBaseImageTag, buildConfiguration.getBaseImageConfiguration().getImageTag()); Assert.assertEquals( expectedBaseImageCredentialHelperName, - buildConfiguration.getBaseImageCredentialHelperName()); - Assert.assertEquals(expectedTargetServerUrl, buildConfiguration.getTargetImageRegistry()); - Assert.assertEquals(expectedTargetImageName, buildConfiguration.getTargetImageRepository()); - Assert.assertEquals(expectedTargetTag, buildConfiguration.getTargetImageTag()); + buildConfiguration.getBaseImageConfiguration().getCredentialHelper()); + Assert.assertEquals( + expectedTargetServerUrl, + buildConfiguration.getTargetImageConfiguration().getImageRegistry()); + Assert.assertEquals( + expectedTargetImageName, + buildConfiguration.getTargetImageConfiguration().getImageRepository()); + Assert.assertEquals( + expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); Assert.assertEquals( expectedTargetImageCredentialHelperName, - buildConfiguration.getTargetImageCredentialHelperName()); + buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); Assert.assertEquals( expectedJavaArguments, buildConfiguration.getContainerConfiguration().getProgramArguments()); @@ -164,10 +173,11 @@ public void testBuilder_default() { .setTargetImageConfiguration(targetImageConfiguration) .build(); - Assert.assertNull(buildConfiguration.getBaseImageCredentialHelperName()); - Assert.assertNull(buildConfiguration.getKnownBaseRegistryCredentials()); - Assert.assertNull(buildConfiguration.getTargetImageCredentialHelperName()); - Assert.assertNull(buildConfiguration.getKnownTargetRegistryCredentials()); + Assert.assertNull(buildConfiguration.getBaseImageConfiguration().getCredentialHelper()); + Assert.assertNull(buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials()); + Assert.assertNull(buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); + Assert.assertNull( + buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index 9fb9c6306b..fa7c4a93f0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; @@ -68,6 +69,7 @@ public class BuildStepsRunnerTest { @Mock private HttpResponseException mockHttpResponseException; @Mock private ExecutionException mockExecutionException; @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private ImageConfiguration mockImageConfiguration; private BuildStepsRunner testBuildImageStepsRunner; @@ -76,6 +78,10 @@ public void setUpMocks() { testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps); Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); + Mockito.when(mockBuildConfiguration.getBaseImageConfiguration()) + .thenReturn(mockImageConfiguration); + Mockito.when(mockBuildConfiguration.getTargetImageConfiguration()) + .thenReturn(mockImageConfiguration); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( @@ -202,7 +208,7 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - Mockito.when(mockBuildConfiguration.getBaseImageRegistry()).thenReturn("someregistry"); + Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn("someregistry"); try { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); @@ -246,9 +252,8 @@ public void testBuildImage_executionException_registryUnauthorizedException_othe Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - Mockito.when(mockBuildConfiguration.getBaseImageRegistry()).thenReturn("someregistry"); - Mockito.when(mockBuildConfiguration.getBaseImageCredentialHelperName()) - .thenReturn("some-credential-helper"); + Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn("someregistry"); + Mockito.when(mockImageConfiguration.getCredentialHelper()).thenReturn("some-credential-helper"); try { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); From 537763973a3181ec042c7a0132a801c7673aa0b9 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 30 Jul 2018 18:01:35 -0400 Subject: [PATCH 0073/2020] Factors MainClassFinder out from MainClassResolver. (#737) --- .../jib/configuration/BuildConfiguration.java | 15 -- .../tools/jib/frontend/MainClassFinder.java | 218 +++++++++++------- .../frontend/MainClassInferenceException.java | 1 + .../tools/jib/frontend/MainClassResolver.java | 150 ++++++++++++ .../configuration/BuildConfigurationTest.java | 12 - .../jib/frontend/MainClassFinderTest.java | 189 +++++---------- .../jib/frontend/MainClassResolverTest.java | 154 +++++++++++++ .../jib/gradle/GradleProjectProperties.java | 4 +- .../jib/maven/MavenProjectProperties.java | 4 +- 9 files changed, 503 insertions(+), 244 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index c27cfdbebf..12282292b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -19,12 +19,10 @@ import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; -import javax.lang.model.SourceVersion; /** Immutable configuration options for the builder process. */ public class BuildConfiguration { @@ -188,19 +186,6 @@ public BuildConfiguration build() { } } - /** - * @param className the class name to check - * @return {@code true} if {@code className} is a valid Java class name; {@code false} otherwise - */ - public static boolean isValidJavaClass(String className) { - for (String part : Splitter.on('.').split(className)) { - if (!SourceVersion.isIdentifier(part)) { - return false; - } - } - return true; - } - public static Builder builder(BuildLogger buildLogger) { return new Builder(buildLogger); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 49c61b02b8..f861a4f621 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -17,9 +17,7 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -38,101 +36,159 @@ import javassist.NotFoundException; import javax.annotation.Nullable; -/** Infers the main class in an application. */ public class MainClassFinder { + /** The result of a call to {@link #find}. */ + public static class Result { + + /** The type of error. */ + public enum ErrorType { + + // An IOException occurred. + IO_EXCEPTION, + + // Did not find any main class. + MAIN_CLASS_NOT_FOUND, + + // Found multiple main classes. + MULTIPLE_MAIN_CLASSES + } + + private static Result success(String foundMainClass) { + return new Result(true, Collections.singletonList(foundMainClass), null, null); + } + + private static Result mainClassNotFound() { + return new Result(false, Collections.emptyList(), ErrorType.MAIN_CLASS_NOT_FOUND, null); + } + + private static Result multipleMainClasses(List foundMainClasses) { + return new Result(false, foundMainClasses, ErrorType.MULTIPLE_MAIN_CLASSES, null); + } + + private static Result ioException(IOException ioException) { + return new Result(false, Collections.emptyList(), ErrorType.IO_EXCEPTION, ioException); + } + + private final boolean isSuccess; + private final List foundMainClasses; + @Nullable private final ErrorType errorType; + @Nullable private final Throwable errorCause; + + private Result( + boolean isSuccess, + List foundMainClasses, + @Nullable ErrorType errorType, + @Nullable Throwable errorCause) { + this.isSuccess = isSuccess; + this.foundMainClasses = foundMainClasses; + this.errorType = errorType; + this.errorCause = errorCause; + } + + /** + * Gets whether or not this result is a success. + * + * @return {@code true} if successful; {@code false} if not + */ + public boolean isSuccess() { + return isSuccess; + } + + /** + * Gets the found main class. Only call if {@link #isSuccess} is {@code true}. + * + * @return the found main class + */ + public String getFoundMainClass() { + Preconditions.checkArgument(isSuccess); + Preconditions.checkArgument(foundMainClasses.size() == 1); + return foundMainClasses.get(0); + } + + /** + * Gets the type of error. Call only if {@link #isSuccess} is {@code false}. + * + * @return the type of error, or {@code null} if successful + */ + public ErrorType getErrorType() { + return Preconditions.checkNotNull(errorType); + } + + /** + * Gets the cause of the error. Call only if {@link #getErrorType} is {@link + * ErrorType#IO_EXCEPTION}. + * + * @return the cause of the error, or {@code null} if not available + */ + public Throwable getErrorCause() { + return Preconditions.checkNotNull(errorCause); + } + + /** + * Gets the found main classes. + * + * @return the found main classes + */ + public List getFoundMainClasses() { + return foundMainClasses; + } + } + + private final ImmutableList classesFiles; + private final BuildLogger buildLogger; + /** - * If {@code mainClass} is {@code null}, tries to infer main class in this order: + * Finds a class with {@code psvm} in {@code classesFiles}. * - *

- * - *

Warns if main class is not valid, or throws an error if no valid main class is not found. + * @param classesFiles the classes files to check + * @param buildLogger used for displaying status messages. + */ + public MainClassFinder(ImmutableList classesFiles, BuildLogger buildLogger) { + this.classesFiles = classesFiles; + this.buildLogger = buildLogger; + } + + /** + * Tries to find a class with {@code psvm} in {@link #classesFiles}. * - * @param mainClass the explicitly configured main class ({@code null} if not configured). - * @param projectProperties properties containing plugin information and help messages. - * @return the name of the main class to be used for the container entrypoint. - * @throws MainClassInferenceException if no valid main class is configured or discovered. + * @return the {@link Result} of the main class finding attempt */ - public static String resolveMainClass( - @Nullable String mainClass, ProjectProperties projectProperties) - throws MainClassInferenceException { - BuildLogger logger = projectProperties.getLogger(); - if (mainClass == null) { - logger.info( - "Searching for main class... Add a 'mainClass' configuration to '" - + projectProperties.getPluginName() - + "' to improve build speed."); - mainClass = projectProperties.getMainClassFromJar(); - if (mainClass == null || !BuildConfiguration.isValidJavaClass(mainClass)) { - logger.debug( - "Could not find a valid main class specified in " - + projectProperties.getJarPluginName() - + "; attempting to infer main class."); - - try { - // Adds each file in the classes output directory to the classes files list. - ImmutableList classesFiles = - projectProperties.getClassesLayerEntry().getSourceFiles(); - List mainClasses = new ArrayList<>(); - Set visitedRoots = new HashSet<>(); - for (Path classPath : classesFiles) { - Path root = classPath.getParent(); - if (visitedRoots.contains(root)) { - continue; - } - visitedRoots.add(root); - mainClasses.addAll(findMainClasses(root, logger)); - } - - if (mainClasses.size() == 1) { - // Valid class found; use inferred main class - mainClass = mainClasses.get(0); - } else if (mainClasses.size() == 0 && mainClass == null) { - // No main class found anywhere - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Main class was not found") - .forMainClassNotFound(projectProperties.getPluginName())); - } else if (mainClasses.size() > 1 && mainClass == null) { - // More than one main class found with no jar plugin to fall back on; error - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: " + String.join(", ", mainClasses)) - .forMainClassNotFound(projectProperties.getPluginName())); - } - } catch (IOException ex) { - throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Failed to get main class") - .forMainClassNotFound(projectProperties.getPluginName()), - ex); - } + public Result find() { + try { + List mainClasses = new ArrayList<>(); + Set roots = new HashSet<>(); + for (Path classPath : classesFiles) { + roots.add(classPath.getParent()); + } + for (Path root : roots) { + mainClasses.addAll(findMainClasses(root)); } - } - Preconditions.checkNotNull(mainClass); - if (!BuildConfiguration.isValidJavaClass(mainClass)) { - logger.warn("'mainClass' is not a valid Java class : " + mainClass); - } - return mainClass; + if (mainClasses.size() == 1) { + // Valid class found. + return Result.success(mainClasses.get(0)); + } + if (mainClasses.size() == 0) { + // No main class found anywhere. + return Result.mainClassNotFound(); + } + // More than one main class found. + return Result.multipleMainClasses(mainClasses); + + } catch (IOException ex) { + return Result.ioException(ex); + } } /** * Finds the classes with {@code public static void main(String[] args)} in {@code rootDirectory}. * * @param rootDirectory directory containing the {@code .class} files. - * @param buildLogger used for displaying status messages. * @return a list of class names containing a main method. * @throws IOException if searching the root directory fails. */ - @VisibleForTesting - static List findMainClasses(Path rootDirectory, BuildLogger buildLogger) - throws IOException { + private List findMainClasses(Path rootDirectory) throws IOException { // Makes sure rootDirectory is valid. if (!Files.exists(rootDirectory) || !Files.isDirectory(rootDirectory)) { return Collections.emptyList(); @@ -179,6 +235,4 @@ static List findMainClasses(Path rootDirectory, BuildLogger buildLogger) throw new RuntimeException(ex); } } - - private MainClassFinder() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java index d07566cd1c..75bff54c26 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java @@ -18,6 +18,7 @@ /** Thrown when main class inference fails. */ public class MainClassInferenceException extends Exception { + MainClassInferenceException(String message) { super(message); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java new file mode 100644 index 0000000000..eaa01c73df --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java @@ -0,0 +1,150 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Verify; +import javax.annotation.Nullable; +import javax.lang.model.SourceVersion; + +/** Infers the main class in an application. */ +public class MainClassResolver { + + /** + * If {@code mainClass} is {@code null}, tries to infer main class in this order: + * + *

    + *
  • 1. Looks in a {@code jar} plugin provided by {@code projectProperties} ({@code + * maven-jar-plugin} for maven or {@code jar} task for gradle). + *
  • 2. Searches for a class defined with a main method. + *
+ * + *

Warns if main class provided by {@code projectProperties} is not valid, or throws an error + * if no valid main class is found. + * + * @param mainClass the explicitly configured main class ({@code null} if not configured). + * @param projectProperties properties containing plugin information and help messages. + * @return the name of the main class to be used for the container entrypoint. + * @throws MainClassInferenceException if no valid main class is configured or discovered. + */ + public static String resolveMainClass( + @Nullable String mainClass, ProjectProperties projectProperties) + throws MainClassInferenceException { + // If mainClass is null, try to find via projectProperties. + if (mainClass == null) { + mainClass = getMainClassFromJar(projectProperties); + } + + // If mainClass is still null, try to search in class files. + if (mainClass == null) { + mainClass = findMainClassInClassFiles(projectProperties); + + } else if (!isValidJavaClass(mainClass)) { + // If mainClass found in projectProperties is not valid, try to search in class files, but + // don't error if not found in class files. + try { + mainClass = findMainClassInClassFiles(projectProperties); + + } catch (MainClassInferenceException ignored) { + // Fallback to using the mainClass found in projectProperties. + } + } + + Preconditions.checkNotNull(mainClass); + if (!isValidJavaClass(mainClass)) { + projectProperties.getLogger().warn("'mainClass' is not a valid Java class : " + mainClass); + } + + return mainClass; + } + + /** + * @param className the class name to check + * @return {@code true} if {@code className} is a valid Java class name; {@code false} otherwise + */ + @VisibleForTesting + static boolean isValidJavaClass(String className) { + for (String part : Splitter.on('.').split(className)) { + if (!SourceVersion.isIdentifier(part)) { + return false; + } + } + return true; + } + + @Nullable + private static String getMainClassFromJar(ProjectProperties projectProperties) { + projectProperties + .getLogger() + .info( + "Searching for main class... Add a 'mainClass' configuration to '" + + projectProperties.getPluginName() + + "' to improve build speed."); + return projectProperties.getMainClassFromJar(); + } + + private static String findMainClassInClassFiles(ProjectProperties projectProperties) + throws MainClassInferenceException { + projectProperties + .getLogger() + .debug( + "Could not find a valid main class specified in " + + projectProperties.getJarPluginName() + + "; attempting to infer main class."); + + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder( + projectProperties.getClassesLayerEntry().getSourceFiles(), + projectProperties.getLogger()) + .find(); + + if (mainClassFinderResult.isSuccess()) { + return mainClassFinderResult.getFoundMainClass(); + } + + Verify.verify(mainClassFinderResult.getErrorType() != null); + switch (mainClassFinderResult.getErrorType()) { + case MAIN_CLASS_NOT_FOUND: + throw new MainClassInferenceException( + projectProperties + .getMainClassHelpfulSuggestions("Main class was not found") + .forMainClassNotFound(projectProperties.getPluginName())); + + case MULTIPLE_MAIN_CLASSES: + throw new MainClassInferenceException( + projectProperties + .getMainClassHelpfulSuggestions( + "Multiple valid main classes were found: " + + String.join(", ", mainClassFinderResult.getFoundMainClasses())) + .forMainClassNotFound(projectProperties.getPluginName())); + + case IO_EXCEPTION: + throw new MainClassInferenceException( + projectProperties + .getMainClassHelpfulSuggestions("Failed to get main class") + .forMainClassNotFound(projectProperties.getPluginName()), + mainClassFinderResult.getErrorCause()); + + default: + throw new IllegalStateException("Cannot reach here"); + } + } + + private MainClassResolver() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 88c598e779..c82c04fc2f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -288,16 +288,4 @@ public void testBuilder_nullValues() { .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(new Hashtable<>()).build()); } - - @Test - public void testValidJavaClassRegex() { - Assert.assertTrue(BuildConfiguration.isValidJavaClass("my.Class")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("my.java_Class$valid")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("multiple.package.items")); - Assert.assertTrue(BuildConfiguration.isValidJavaClass("is123.valid")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("${start-class}")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("123not.Valid")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("{class}")); - Assert.assertFalse(BuildConfiguration.isValidJavaClass("not valid")); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 29cde64bbf..4b03259c76 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -17,195 +17,122 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.builder.BuildLogger; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; +import org.hamcrest.CoreMatchers; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Test for MainClassFinder. */ +/** Tests for {@link MainClassFinder}. */ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { @Mock private BuildLogger mockBuildLogger; - @Mock private ProjectProperties mockProjectProperties; - @Mock private HelpfulSuggestions mockHelpfulSuggestions; - - private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); - - @Before - public void setup() { - Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); - Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); - Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) - .thenReturn(mockHelpfulSuggestions); - Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); - } @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("HelloWorld")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); } @Test public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("multi.layered.HelloWorld")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("multi.layered.HelloWorld")); } @Test public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertTrue(mainClasses.isEmpty()); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertFalse(mainClassFinderResult.isSuccess()); + Assert.assertEquals( + MainClassFinder.Result.ErrorType.MAIN_CLASS_NOT_FOUND, + mainClassFinderResult.getErrorType()); } @Test public void testFindMainClass_multiple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(2, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("multi.layered.HelloMoon")); - Assert.assertTrue(mainClasses.contains("HelloWorld")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertFalse(mainClassFinderResult.isSuccess()); + Assert.assertEquals( + MainClassFinder.Result.ErrorType.MULTIPLE_MAIN_CLASSES, + mainClassFinderResult.getErrorType()); + Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); + Assert.assertTrue( + mainClassFinderResult.getFoundMainClasses().contains("multi.layered.HelloMoon")); + Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("HelloWorld")); } @Test public void testFindMainClass_extension() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("main.MainClass")); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); - List mainClasses = MainClassFinder.findMainClasses(rootDirectory, mockBuildLogger); - Assert.assertEquals(1, mainClasses.size()); - Assert.assertTrue(mainClasses.contains("HelloWorld$InnerClass")); - } - - @Test - public void testResolveMainClass() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("some.main.class"); - Assert.assertEquals( - "some.main.class", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Assert.assertEquals( - "configured", MainClassFinder.resolveMainClass("configured", mockProjectProperties)); - } - - @Test - public void testResolveMainClass_notValid() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) - .thenReturn(new LayerEntry(fakeClassesPath, "ignored")); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_multipleInferredWithBackup() - throws MainClassInferenceException, URISyntaxException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) - .thenReturn( - new LayerEntry( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class") - .toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class") - .toURI())), - "ignored")); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) - .thenReturn( - new LayerEntry( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class") - .toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class") - .toURI())), - "ignored")); - try { - MainClassFinder.resolveMainClass(null, mockProjectProperties); - Assert.fail(); - } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: HelloWorld, multi.layered.HelloMoon"); - } - } - - @Test - public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { - Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); - Assert.assertEquals( - "${start-class}", MainClassFinder.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); - } - - @Test - public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockProjectProperties.getClassesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); - try { - MainClassFinder.resolveMainClass(null, mockProjectProperties); - Assert.fail(); - } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions("Main class was not found"); - } + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) + .find(); + Assert.assertTrue(mainClassFinderResult.isSuccess()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), + CoreMatchers.containsString("HelloWorld$InnerClass")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java new file mode 100644 index 0000000000..838f723f9b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Test for {@link MainClassResolver}. */ +@RunWith(MockitoJUnitRunner.class) +public class MainClassResolverTest { + + @Mock private BuildLogger mockBuildLogger; + @Mock private ProjectProperties mockProjectProperties; + @Mock private HelpfulSuggestions mockHelpfulSuggestions; + + private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); + + @Before + public void setup() { + Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); + Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); + Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) + .thenReturn(mockHelpfulSuggestions); + Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); + } + + @Test + public void testResolveMainClass() throws MainClassInferenceException { + Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("some.main.class"); + Assert.assertEquals( + "some.main.class", MainClassResolver.resolveMainClass(null, mockProjectProperties)); + Assert.assertEquals( + "configured", MainClassResolver.resolveMainClass("configured", mockProjectProperties)); + } + + @Test + public void testResolveMainClass_notValid() throws MainClassInferenceException { + Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(fakeClassesPath, "ignored")); + Assert.assertEquals( + "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); + Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + } + + @Test + public void testResolveMainClass_multipleInferredWithBackup() + throws MainClassInferenceException, URISyntaxException { + Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn( + new LayerEntry( + ImmutableList.of( + Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/HelloWorld.class") + .toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/NotMain.class") + .toURI())), + "ignored")); + Assert.assertEquals( + "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); + Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + } + + @Test + public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { + Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn( + new LayerEntry( + ImmutableList.of( + Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/HelloWorld.class") + .toURI()), + Paths.get( + Resources.getResource("class-finder-tests/multiple/NotMain.class") + .toURI())), + "ignored")); + try { + MainClassResolver.resolveMainClass(null, mockProjectProperties); + Assert.fail(); + + } catch (MainClassInferenceException ex) { + Mockito.verify(mockProjectProperties) + .getMainClassHelpfulSuggestions( + "Multiple valid main classes were found: HelloWorld, multi.layered.HelloMoon"); + } + } + + @Test + public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { + Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); + Assert.assertEquals( + "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); + Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + } + + @Test + public void testResolveMainClass_noneInferredWithoutBackup() { + Mockito.when(mockProjectProperties.getClassesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); + try { + MainClassResolver.resolveMainClass(null, mockProjectProperties); + Assert.fail(); + } catch (MainClassInferenceException ex) { + Mockito.verify(mockProjectProperties) + .getMainClassHelpfulSuggestions("Main class was not found"); + } + } + + @Test + public void testValidJavaClassRegex() { + Assert.assertTrue(MainClassResolver.isValidJavaClass("my.Class")); + Assert.assertTrue(MainClassResolver.isValidJavaClass("my.java_Class$valid")); + Assert.assertTrue(MainClassResolver.isValidJavaClass("multiple.package.items")); + Assert.assertTrue(MainClassResolver.isValidJavaClass("is123.valid")); + Assert.assertFalse(MainClassResolver.isValidJavaClass("${start-class}")); + Assert.assertFalse(MainClassResolver.isValidJavaClass("123not.Valid")); + Assert.assertFalse(MainClassResolver.isValidJavaClass("{class}")); + Assert.assertFalse(MainClassResolver.isValidJavaClass("not valid")); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index f93b2ea204..bb5638f43f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; +import com.google.cloud.tools.jib.frontend.MainClassResolver; import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -149,7 +149,7 @@ public String getJarPluginName() { */ String getMainClass(JibExtension jibExtension) { try { - return MainClassFinder.resolveMainClass(jibExtension.getMainClass(), this); + return MainClassResolver.resolveMainClass(jibExtension.getMainClass(), this); } catch (MainClassInferenceException ex) { throw new GradleException(ex.getMessage(), ex); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 7b2c175cec..499cd99dbc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; +import com.google.cloud.tools.jib.frontend.MainClassResolver; import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; @@ -170,7 +170,7 @@ public String getJarPluginName() { */ String getMainClass(JibPluginConfiguration jibPluginConfiguration) throws MojoExecutionException { try { - return MainClassFinder.resolveMainClass(jibPluginConfiguration.getMainClass(), this); + return MainClassResolver.resolveMainClass(jibPluginConfiguration.getMainClass(), this); } catch (MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } From f23eb71ac1c130927271756e2303472279f6ea89 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 31 Jul 2018 11:21:47 -0400 Subject: [PATCH 0074/2020] BlobPusher must use `from` with `mount` (#731) --- .../registry/BlobPusherIntegrationTest.java | 2 +- .../ManifestPusherIntegrationTest.java | 4 ++-- .../tools/jib/builder/steps/PushBlobStep.java | 14 ++++++++++- .../cloud/tools/jib/registry/BlobPusher.java | 19 ++++++++++----- .../tools/jib/registry/RegistryClient.java | 14 +++++++---- .../tools/jib/registry/BlobPusherTest.java | 24 ++++++++++++++++--- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index c1feab67d2..a0321800d8 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -42,6 +42,6 @@ public void testPush() throws DigestException, IOException, RegistryException { RegistryClient.factory("localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob)); + Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null)); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 8a922f4c1a..7b2a225e38 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -78,10 +78,10 @@ public void testPush() throws DigestException, IOException, RegistryException { RegistryClient.factory("localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob)); + Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null)); Assert.assertFalse( registryClient.pushBlob( - testContainerConfigurationBlobDigest, testContainerConfigurationBlob)); + testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null)); // Pushes the manifest. registryClient.pushManifest(expectedManifestTemplate, "latest"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 0bd1099e1b..993bccee74 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -77,6 +77,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc .newRegistryClient(); registryClient.setTimer(timer); + // check if the BLOB is available if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { buildConfiguration .getBuildLogger() @@ -84,7 +85,18 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc return blobDescriptor; } - registryClient.pushBlob(blobDescriptor.getDigest(), blob); + // If base and target images are in the same registry, then use mount/from to try mounting the + // BLOB from the base image repository to the target image repository and possibly avoid + // having to push the BLOB. See + // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. + boolean sameRegistry = + buildConfiguration + .getBaseImageConfiguration() + .getImageRegistry() + .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()); + String mountFrom = + sameRegistry ? buildConfiguration.getBaseImageConfiguration().getImageRepository() : null; + registryClient.pushBlob(blobDescriptor.getDigest(), blob, mountFrom); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 847a6a62ad..515b450b99 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -46,6 +46,7 @@ class BlobPusher { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final DescriptorDigest blobDigest; private final Blob blob; + @Nullable private final String sourceRepository; /** Initializes the BLOB upload. */ private class Initializer implements RegistryEndpointProvider { @@ -84,11 +85,15 @@ public URL handleResponse(Response response) throws RegistryErrorException { @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { - return new URL( - apiRouteBase - + registryEndpointRequestProperties.getImageName() - + "/blobs/uploads/?mount=" - + blobDigest); + StringBuilder url = + new StringBuilder(apiRouteBase) + .append(registryEndpointRequestProperties.getImageName()) + .append("/blobs/uploads/"); + if (sourceRepository != null) { + url.append("?mount=").append(blobDigest).append("&from=").append(sourceRepository); + } + + return new URL(url.toString()); } @Override @@ -190,10 +195,12 @@ private Committer(URL location) { BlobPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, - Blob blob) { + Blob blob, + @Nullable String sourceRepository) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; this.blob = blob; + this.sourceRepository = sourceRepository; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b89ff1bc3a..6339bfd008 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -267,25 +267,29 @@ public Void pullBlob(DescriptorDigest blobDigest, OutputStream destinationOutput return callRegistryEndpoint(blobPuller); } - // TODO: Add mount with 'from' parameter /** - * Pushes the BLOB, or skips if the BLOB already exists on the registry. + * Pushes the BLOB. If the {@code sourceRepository} is provided then the remote registry may skip + * if the BLOB already exists on the registry. * * @param blobDigest the digest of the BLOB, used for existence-check * @param blob the BLOB to push + * @param sourceRepository if pushing to the same registry then the source image, or {@code null} + * otherwise; used to optimize the BLOB push * @return {@code true} if the BLOB already exists on the registry and pushing was skipped; false * if the BLOB was pushed * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public boolean pushBlob(DescriptorDigest blobDigest, Blob blob) + public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String sourceRepository) throws IOException, RegistryException { - BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob); + BlobPusher blobPusher = + new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); try (Timer t = parentTimer.subTimer("pushBlob")) { try (Timer t2 = t.subTimer("pushBlob POST " + blobDigest)) { - // POST /v2//blobs/uploads/?mount={blob.digest} + // POST /v2//blobs/uploads/ OR + // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} URL patchLocation = callRegistryEndpoint(blobPusher.initializer()); if (patchLocation == null) { // The BLOB exists already. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index 49db99edf5..fed49e88f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -57,7 +57,8 @@ public void setUpFakes() throws DigestException { new BlobPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeDescriptorDigest, - mockBlob); + mockBlob, + null); } @Test @@ -119,9 +120,26 @@ public void testInitializer_handleResponse_unrecognized() throws IOException, Re } @Test - public void testInitializer_getApiRoute() throws MalformedURLException { + public void testInitializer_getApiRoute_nullSource() throws MalformedURLException { Assert.assertEquals( - new URL("http://someApiBase/someImageName/blobs/uploads/?mount=" + fakeDescriptorDigest), + new URL("http://someApiBase/someImageName/blobs/uploads/"), + testBlobPusher.initializer().getApiRoute("http://someApiBase/")); + } + + @Test + public void testInitializer_getApiRoute_sameSource() throws MalformedURLException { + testBlobPusher = + new BlobPusher( + new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), + fakeDescriptorDigest, + mockBlob, + "sourceImageName"); + + Assert.assertEquals( + new URL( + "http://someApiBase/someImageName/blobs/uploads/?mount=" + + fakeDescriptorDigest + + "&from=sourceImageName"), testBlobPusher.initializer().getApiRoute("http://someApiBase/")); } From b23dcdcfdf08fd760bc3c429fa70d062f4c08118 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 31 Jul 2018 12:31:45 -0400 Subject: [PATCH 0075/2020] Makes sure there are no duplicate cache entries. (#739) --- .../cloud/tools/jib/cache/CachedLayer.java | 17 ++++ .../cloud/tools/jib/image/ImageLayers.java | 39 +++----- .../cloud/tools/jib/cache/CacheTest.java | 97 +++++++++++++++++++ .../tools/jib/image/ImageLayersTest.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 5 +- jib-maven-plugin/CHANGELOG.md | 5 +- 6 files changed, 134 insertions(+), 31 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index 063655dfeb..e1cf18184f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -67,4 +67,21 @@ public BlobDescriptor getBlobDescriptor() { public DescriptorDigest getDiffId() { return diffId; } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof CachedLayer)) { + return false; + } + CachedLayer otherLayer = (CachedLayer) other; + return getBlobDescriptor().getDigest().equals(otherLayer.getBlobDescriptor().getDigest()); + } + + @Override + public int hashCode() { + return getBlobDescriptor().getDigest().hashCode(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java index a52dfd7280..8f6326d3ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java @@ -19,34 +19,34 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.util.Iterator; +import java.util.LinkedHashSet; import javax.annotation.Nullable; -/** Holds the layers for an image. Makes sure that each layer is only added once. */ +/** Holds the layers for an image. Makes sure that there are no duplicate layers. */ public class ImageLayers implements Iterable { public static class Builder { - private final ImmutableList.Builder layersBuilder = ImmutableList.builder(); + private final LinkedHashSet layers = new LinkedHashSet<>(); private final ImmutableSet.Builder layerDigestsBuilder = ImmutableSet.builder(); - /** The last layer added. */ - @Nullable private T lastLayer; - /** - * Adds a layer. + * Adds a layer. Removes any prior occurrences of the same layer. + * + *

Note that only subclasses of {@link Layer} that implement {@code equals/hashCode} will be + * guaranteed to not be duplicated. * * @param layer the layer to add * @return this * @throws LayerPropertyNotFoundException if adding the layer fails */ public Builder add(T layer) throws LayerPropertyNotFoundException { - // Doesn't add the layer if the last layer is the same. - if (!isSameAsLastLayer(layer)) { - layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); - layersBuilder.add(layer); - lastLayer = layer; - } + layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); + + // Remove necessary to move layer to the end of the LinkedHashSet. + layers.remove(layer); + layers.add(layer); return this; } @@ -68,20 +68,7 @@ public Builder addAll(ImageLayers layers) } public ImageLayers build() { - return new ImageLayers<>(layersBuilder.build(), layerDigestsBuilder.build()); - } - - /** - * @param layer the layer to compare - * @return {@code true} if {@code layer} is the same as the last layer in {@link #layers} - * @throws LayerPropertyNotFoundException if getting the last layer's blob descriptor fails - */ - private boolean isSameAsLastLayer(T layer) throws LayerPropertyNotFoundException { - return lastLayer != null - && layer - .getBlobDescriptor() - .getDigest() - .equals(lastLayer.getBlobDescriptor().getDigest()); + return new ImageLayers<>(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 975e8d386a..c00cc3c54b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -16,11 +16,23 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.NotDirectoryException; import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.security.DigestException; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -68,4 +80,89 @@ public void testInit_withMetadata() Assert.assertArrayEquals( Files.readAllBytes(resourceMetadataJsonPath), Files.readAllBytes(testMetadataJsonPath)); } + + @Test + public void test_saveMetadata_noDuplicates() + throws IOException, CacheMetadataCorruptedException, DigestException, URISyntaxException { + Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); + + Path resourceMetadataJsonPath = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); + Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); + + DescriptorDigest descriptorDigest1 = + DescriptorDigest.fromHash( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); + DescriptorDigest descriptorDigest2 = + DescriptorDigest.fromHash( + "6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); + + LayerEntry layerEntry1 = + new LayerEntry( + ImmutableList.of(Paths.get("some", "file"), Paths.get("some", "other", "file")), + "extractionPath1"); + LayerEntry layerEntry2 = + new LayerEntry( + ImmutableList.of(Paths.get("another", "file"), Paths.get("yet", "another", "file")), + "extractionPath2"); + + LayerMetadata layerMetadata1 = + LayerMetadata.from( + ImmutableList.of(layerEntry1, layerEntry2), FileTime.from(Instant.now())); + LayerMetadata layerMetadata2 = + LayerMetadata.from(ImmutableList.of(layerEntry2), FileTime.from(Instant.EPOCH)); + + DescriptorDigest mockDiffId = + DescriptorDigest.fromHash( + "91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"); + + // Layers ABA. + List cachedLayersWithMetadata = + Arrays.asList( + new CachedLayerWithMetadata( + new CachedLayer( + Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest1), mockDiffId), + layerMetadata1), + new CachedLayerWithMetadata( + new CachedLayer( + Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest2), mockDiffId), + layerMetadata2), + new CachedLayerWithMetadata( + new CachedLayer( + Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest1), mockDiffId), + layerMetadata2)); + + // Saves the new layers to the cache metadata. + try (Cache cache = Cache.init(cacheDirectory)) { + cache.addCachedLayersWithMetadataToMetadata(cachedLayersWithMetadata); + } + + // Reload the cache and check that all digests are unique. + try (Cache cache = Cache.init(cacheDirectory)) { + Set encounteredDigests = new HashSet<>(); + for (CachedLayerWithMetadata layer : cache.getMetadata().getLayers()) { + DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); + Assert.assertFalse(encounteredDigests.contains(layerDigest)); + encounteredDigests.add(layerDigest); + } + + // The layer metadata for layer with digest descriptorDigest1 should be layerMetadata2. + CachedLayerWithMetadata descriptorDigest1Layer = + cache.getMetadata().getLayers().get(descriptorDigest1); + Assert.assertNotNull(descriptorDigest1Layer); + LayerMetadata layerMetadata = descriptorDigest1Layer.getMetadata(); + Assert.assertNotNull(layerMetadata); + Assert.assertEquals(1, layerMetadata.getEntries().size()); + Assert.assertEquals(FileTime.from(Instant.EPOCH), layerMetadata.getLastModifiedTime()); + Assert.assertEquals( + layerEntry2 + .getSourceFiles() + .stream() + .map(Path::toString) + .collect(ImmutableList.toImmutableList()), + layerMetadata.getEntries().get(0).getSourceFilesStrings()); + Assert.assertEquals( + layerEntry2.getExtractionPath(), layerMetadata.getEntries().get(0).getExtractionPath()); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index 94fac9b447..7d66fce37e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -76,7 +76,7 @@ public void testAddLayer_success() throws LayerPropertyNotFoundException { @Test public void testAddLayer_sameAsLastLayer() throws LayerPropertyNotFoundException { List expectedLayers = - Arrays.asList(mockCachedLayer, mockReferenceLayer, mockDigestOnlyLayer, mockUnwrittenLayer); + Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockUnwrittenLayer, mockCachedLayer); ImageLayers imageLayers = ImageLayers.builder() diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c4f9b23e0c..cd7ff40d7f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,8 +13,9 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) -- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) ## 0.9.7 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 02fbd536c2..61a4ee7d7f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -16,8 +16,9 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) -- Only builds non-empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) ## 0.9.7 From 25f44969cb7f01c89abfda0a8246132abc321e4e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 31 Jul 2018 12:50:06 -0400 Subject: [PATCH 0076/2020] Minor fixes (#747) --- .../google/cloud/tools/jib/filesystem/DirectoryWalker.java | 6 +----- .../credentials/DockerConfigCredentialRetriever.java | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java index ebb29b210e..c40244c480 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java @@ -90,11 +90,7 @@ public ImmutableList walk(PathConsumer pathConsumer) throws IOException { */ public ImmutableList walk() throws IOException { try (Stream fileStream = Files.walk(rootDir)) { - Stream filteredFileStream = fileStream; - if (pathFilter != null) { - filteredFileStream = fileStream.filter(pathFilter); - } - return filteredFileStream.sorted().collect(ImmutableList.toImmutableList()); + return fileStream.filter(pathFilter).sorted().collect(ImmutableList.toImmutableList()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 940bf18c51..648b95ff33 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -120,7 +120,7 @@ private Authorization retrieve(DockerConfig dockerConfig, String registryAlias) dockerConfig.getCredentialHelperFor(dockerCredentialHelperFactory, registryAlias); if (dockerCredentialHelper != null) { try { - // Tries with the given registry alias (NOT the original registry). + // Tries with the given registry alias (may be the original registry). return dockerCredentialHelper.retrieve(); } catch (IOException From 709759b0a358036d880a0716e55a4c1cbd40ab8d Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 31 Jul 2018 15:57:15 -0400 Subject: [PATCH 0077/2020] Disables second run time check for empty integration test project. (#749) --- .../maven/BuildImageMojoIntegrationTest.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 5940437df3..3785e20ff4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -47,7 +47,7 @@ public class BuildImageMojoIntegrationTest { * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. */ - private static String buildAndRun(Path projectRoot, String imageReference) + private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setAutoclean(false); @@ -59,19 +59,21 @@ private static String buildAndRun(Path projectRoot, String imageReference) long timeOne = System.nanoTime() - lastTime; lastTime = System.nanoTime(); - verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); - long timeTwo = System.nanoTime() - lastTime; + if (runTwice) { + verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); + long timeTwo = System.nanoTime() - lastTime; + + Assert.assertTrue( + "First build time (" + + timeOne + + ") is not greater than second build time (" + + timeTwo + + ")", + timeOne > timeTwo); + } verifier.verifyErrorFreeLog(); - Assert.assertTrue( - "First build time (" - + timeOne - + ") is not greater than second build time (" - + timeTwo - + ")", - timeOne > timeTwo); - new Command("docker", "pull", imageReference).run(); Assert.assertThat( new Command("docker", "inspect", imageReference).run(), @@ -107,7 +109,8 @@ public void testExecute_simple() throws VerificationException, IOException, Inte "Hello, world. An argument.\nfoo\ncat\n", buildAndRun( simpleTestProject.getProjectRoot(), - "gcr.io/jib-integration-testing/simpleimage:maven")); + "gcr.io/jib-integration-testing/simpleimage:maven", + true)); Instant buildTime = Instant.parse( @@ -127,7 +130,9 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi Assert.assertEquals( "", buildAndRun( - emptyTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/emptyimage:maven")); + emptyTestProject.getProjectRoot(), + "gcr.io/jib-integration-testing/emptyimage:maven", + false)); Assert.assertEquals( "1970-01-01T00:00:00Z", new Command( From b5d8c26988c832e59b4b1132b0f41303dd7026d1 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 31 Jul 2018 16:49:25 -0400 Subject: [PATCH 0078/2020] jibSerialized -> jibSerialize (#752) --- .../com/google/cloud/tools/jib/builder/steps/StepsRunner.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 936ed8619f..435a51d98d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -72,7 +72,7 @@ public StepsRunner( this.applicationLayersCache = applicationLayersCache; ExecutorService executorService = - Boolean.getBoolean("jibSerialized") + Boolean.getBoolean("jibSerialize") ? MoreExecutors.newDirectExecutorService() : Executors.newCachedThreadPool(); listeningExecutorService = MoreExecutors.listeningDecorator(executorService); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index cd7ff40d7f..3959dbdc58 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true` ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 61a4ee7d7f..2c8f43e8a9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) - Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) - Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true` ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) ### Changed From a46e33b5aee2f1a39ba74d791b61e6b6e9b518ae Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 31 Jul 2018 19:07:18 -0400 Subject: [PATCH 0079/2020] Log detailed message (#757) --- .../jib/builder/steps/RetrieveRegistryCredentialsStep.java | 3 +++ .../jib/builder/steps/RetrieveRegistryCredentialsStepTest.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 5592bb2cce..938a2c8a34 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -156,6 +156,9 @@ public Authorization call() throws IOException, NonexistentDockerCredentialHelpe // Warns the user that the specified (or inferred) credential helper is not on the // system. buildLogger.warn(ex.getMessage()); + if (ex.getCause() != null && ex.getCause().getMessage() != null) { + buildLogger.info(" Caused by: " + ex.getCause().getMessage()); + } } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 1683047111..61f7e2ad43 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -149,9 +149,12 @@ public void testCall_inferCommonCredentialHelpers() Mockito.verify(mockBuildLogger).info("Using docker-credential-gcr for something.gcr.io"); Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); + Mockito.when(mockNonexistentDockerCredentialHelperException.getCause()) + .thenReturn(new IOException("the root cause")); Assert.assertNull( makeRetrieveRegistryCredentialsStep("something.amazonaws.com", null, null).call()); Mockito.verify(mockBuildLogger).warn("warning"); + Mockito.verify(mockBuildLogger).info(" Caused by: the root cause"); } /** Creates a fake {@link RetrieveRegistryCredentialsStep} for {@code registry}. */ From 8386bb27ac7e0ca6507a5bd35fd171f1747c6b6f Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 31 Jul 2018 23:14:11 -0400 Subject: [PATCH 0080/2020] Remove unnecessary exceptions (#754) --- .../jib/frontend/MainClassFinderTest.java | 17 +++++----- .../registry/RegistryEndpointCallerTest.java | 34 +++++++------------ .../maven/MavenLayerConfigurationsTest.java | 5 ++- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 4b03259c76..758fa73be6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; -import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; @@ -37,7 +36,7 @@ public class MainClassFinderTest { @Mock private BuildLogger mockBuildLogger; @Test - public void testFindMainClass_simple() throws URISyntaxException, IOException { + public void testFindMainClass_simple() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) @@ -48,7 +47,7 @@ public void testFindMainClass_simple() throws URISyntaxException, IOException { } @Test - public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { + public void testFindMainClass_subdirectories() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = @@ -61,7 +60,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce } @Test - public void testFindMainClass_noClass() throws URISyntaxException, IOException { + public void testFindMainClass_noClass() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) @@ -73,7 +72,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { } @Test - public void testFindMainClass_multiple() throws URISyntaxException, IOException { + public void testFindMainClass_multiple() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) @@ -89,7 +88,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException } @Test - public void testFindMainClass_extension() throws URISyntaxException, IOException { + public void testFindMainClass_extension() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) @@ -100,7 +99,7 @@ public void testFindMainClass_extension() throws URISyntaxException, IOException } @Test - public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { + public void testFindMainClass_importedMethods() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = @@ -112,7 +111,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc } @Test - public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { + public void testFindMainClass_externalClasses() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = @@ -124,7 +123,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc } @Test - public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { + public void testFindMainClass_innerClasses() throws URISyntaxException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 0da6552a32..9edbfed2ee 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.security.GeneralSecurityException; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -115,14 +114,12 @@ public void tearDown() { } @Test - public void testCall_httpsPeerUnverified() - throws IOException, RegistryException, GeneralSecurityException { + public void testCall_httpsPeerUnverified() throws IOException, RegistryException { verifyRetriesWithHttp(SSLPeerUnverifiedException.class); } @Test - public void testCall_retryWithHttp() - throws IOException, RegistryException, GeneralSecurityException { + public void testCall_retryWithHttp() throws IOException, RegistryException { verifyRetriesWithHttp(HttpHostConnectException.class); } @@ -220,20 +217,17 @@ public void testCall_unknown() throws IOException, RegistryException { } @Test - public void testCall_temporaryRedirect() - throws IOException, RegistryException, GeneralSecurityException { + public void testCall_temporaryRedirect() throws IOException, RegistryException { verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); } @Test - public void testCall_movedPermanently() - throws IOException, RegistryException, GeneralSecurityException { + public void testCall_movedPermanently() throws IOException, RegistryException { verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY); } @Test - public void testCall_permanentRedirect() - throws IOException, RegistryException, GeneralSecurityException { + public void testCall_permanentRedirect() throws IOException, RegistryException { verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); } @@ -260,8 +254,7 @@ public void testCall_disallowInsecure() throws IOException, RegistryException { } @Test - public void testHttpTimeout_propertyNotSet() - throws IOException, RegistryException, GeneralSecurityException { + public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -274,8 +267,7 @@ public void testHttpTimeout_propertyNotSet() } @Test - public void testHttpTimeout_stringValue() - throws IOException, RegistryException, GeneralSecurityException { + public void testHttpTimeout_stringValue() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -286,8 +278,7 @@ public void testHttpTimeout_stringValue() } @Test - public void testHttpTimeout_negativeValue() - throws IOException, RegistryException, GeneralSecurityException { + public void testHttpTimeout_negativeValue() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -300,8 +291,7 @@ public void testHttpTimeout_negativeValue() } @Test - public void testHttpTimeout_0accepted() - throws IOException, RegistryException, GeneralSecurityException { + public void testHttpTimeout_0accepted() throws IOException, RegistryException { System.setProperty("jib.httpTimeout", "0"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); @@ -313,7 +303,7 @@ public void testHttpTimeout_0accepted() } @Test - public void testHttpTimeout() throws IOException, RegistryException, GeneralSecurityException { + public void testHttpTimeout() throws IOException, RegistryException { System.setProperty("jib.httpTimeout", "7593"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); @@ -326,7 +316,7 @@ public void testHttpTimeout() throws IOException, RegistryException, GeneralSecu /** Verifies a request is retried with HTTP protocol if {@code exceptionClass} is thrown. */ private void verifyRetriesWithHttp(Class exceptionClass) - throws IOException, RegistryException, GeneralSecurityException { + throws IOException, RegistryException { // Has mockConnection.send throw first, then succeed. Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(exceptionClass)) @@ -400,7 +390,7 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) * Location} header. */ private void verifyRetriesWithNewLocation(int httpStatusCode) - throws IOException, RegistryException, GeneralSecurityException { + throws IOException, RegistryException { // Mocks a response for temporary redirect to a new location. Mockito.when(mockHttpResponse.getStatusCode()).thenReturn(httpStatusCode); Mockito.when(mockHttpResponse.getHeaders()) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index e9fb8177e7..572f3561c5 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -26,7 +26,6 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; -import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.junit.Assert; import org.junit.Before; @@ -49,7 +48,7 @@ public class MavenLayerConfigurationsTest { private MavenLayerConfigurations testMavenLayerConfigurations; @Before - public void setUp() throws IOException, URISyntaxException, MojoExecutionException { + public void setUp() throws IOException, URISyntaxException { Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); @@ -109,7 +108,7 @@ public void test_correctFiles() throws URISyntaxException { } @Test - public void test_extraFiles() throws URISyntaxException, IOException, MojoExecutionException { + public void test_extraFiles() throws URISyntaxException, IOException { Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); testMavenLayerConfigurations = From b83d5a446b7e747410ab59a85a18cffe57b44fbb Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 1 Aug 2018 11:25:26 -0400 Subject: [PATCH 0081/2020] Inform user of fallback to HTTP (#748) --- .../cloud/tools/jib/EmptyBuildLogger.java | 35 +++++++++++++++++++ ...icationMethodRetrieverIntegrationTest.java | 8 +++-- .../registry/BlobCheckerIntegrationTest.java | 8 +++-- .../registry/BlobPullerIntegrationTest.java | 6 ++-- .../registry/BlobPusherIntegrationTest.java | 4 ++- .../ManifestPullerIntegrationTest.java | 8 +++-- .../ManifestPusherIntegrationTest.java | 8 +++-- .../RegistryAuthenticatorIntegrationTest.java | 7 +++- .../tools/jib/{builder => }/BuildLogger.java | 2 +- .../com/google/cloud/tools/jib/Timer.java | 1 - .../builder/steps/AuthenticatePushStep.java | 1 + .../steps/PullAndCacheBaseImageLayerStep.java | 1 + .../jib/builder/steps/PullBaseImageStep.java | 2 ++ .../tools/jib/builder/steps/PushBlobStep.java | 1 + .../jib/builder/steps/PushImageStep.java | 1 + .../RetrieveRegistryCredentialsStep.java | 2 +- .../jib/configuration/BuildConfiguration.java | 2 +- .../tools/jib/frontend/BuildStepsRunner.java | 2 +- .../tools/jib/frontend/MainClassFinder.java | 2 +- .../tools/jib/frontend/ProjectProperties.java | 2 +- .../jib/registry/RegistryAuthenticator.java | 14 +++++--- .../tools/jib/registry/RegistryClient.java | 21 ++++++++--- .../jib/registry/RegistryEndpointCaller.java | 9 +++++ .../tools/jib/builder/TestBuildLogger.java | 1 + .../jib/builder/steps/BuildImageStepTest.java | 2 +- .../RetrieveRegistryCredentialsStepTest.java | 2 +- .../configuration/BuildConfigurationTest.java | 2 +- .../jib/frontend/BuildStepsRunnerTest.java | 2 +- .../jib/frontend/ExposedPortsParserTest.java | 2 +- .../jib/frontend/MainClassFinderTest.java | 2 +- .../jib/frontend/MainClassResolverTest.java | 2 +- .../jib/registry/RegistryClientTest.java | 5 ++- .../registry/RegistryEndpointCallerTest.java | 9 +++++ .../tools/jib/gradle/GradleBuildLogger.java | 2 +- .../jib/gradle/GradleProjectProperties.java | 2 +- .../cloud/tools/jib/gradle/JibExtension.java | 2 +- .../tools/jib/gradle/JibExtensionTest.java | 2 +- .../jib/maven/JibPluginConfiguration.java | 2 +- .../tools/jib/maven/MavenBuildLogger.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../jib/maven/JibPluginConfigurationTest.java | 2 +- 41 files changed, 146 insertions(+), 46 deletions(-) create mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java rename jib-core/src/main/java/com/google/cloud/tools/jib/{builder => }/BuildLogger.java (95%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java new file mode 100644 index 0000000000..fc423a1d5f --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib; + +public class EmptyBuildLogger implements BuildLogger { + + @Override + public void error(CharSequence message) {} + + @Override + public void lifecycle(CharSequence message) {} + + @Override + public void warn(CharSequence message) {} + + @Override + public void info(CharSequence message) {} + + @Override + public void debug(CharSequence message) {} +} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 30ba8796af..f8c16ba01d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -24,17 +25,20 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + @Test public void testGetRegistryAuthenticator() throws RegistryAuthenticationFailedException, IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("registry.hub.docker.com", "library/busybox").newRegistryClient(); + RegistryClient.factory(BUILD_LOGGER, "registry.hub.docker.com", "library/busybox") + .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(); RegistryClient authorizedRegistryClient = - RegistryClient.factory("registry.hub.docker.com", "library/busybox") + RegistryClient.factory(BUILD_LOGGER, "registry.hub.docker.com", "library/busybox") .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 639911b22d..54ad3f4bcf 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -28,11 +29,12 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private static final EmptyBuildLogger buildLogger = new EmptyBuildLogger(); @Test public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(buildLogger, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = @@ -45,13 +47,13 @@ public void testCheck_exists() throws IOException, RegistryException { @Test public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(buildLogger, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); DescriptorDigest fakeBlobDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Assert.assertEquals(null, registryClient.checkBlob(fakeBlobDigest)); + Assert.assertNull(registryClient.checkBlob(fakeBlobDigest)); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index ab8b9b139d..957b5762ab 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -35,6 +36,7 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -42,7 +44,7 @@ public class BlobPullerIntegrationTest { public void testPull() throws IOException, RegistryException { // Pulls the busybox image. RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -66,7 +68,7 @@ public void testPull_unknownBlob() throws RegistryException, IOException, Digest try { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index a0321800d8..08359bf184 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -29,6 +30,7 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); @Test public void testPush() throws DigestException, IOException, RegistryException { @@ -39,7 +41,7 @@ public void testPush() throws DigestException, IOException, RegistryException { "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "testimage") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 3dca13bf2f..9315501965 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -29,11 +30,12 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); @Test public void testPull_v21() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -46,7 +48,7 @@ public void testPull_v21() throws IOException, RegistryException { @Test public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -58,7 +60,7 @@ public void testPull_v22() throws IOException, RegistryException { public void testPull_unknownManifest() throws RegistryException, IOException { try { RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 7b2a225e38..973514f252 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -33,15 +34,16 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); @Test public void testPush_missingBlobs() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory("gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory("localhost:5000", "busybox") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); try { @@ -75,7 +77,7 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory("localhost:5000", "testimage") + RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index dd482adc0f..7c73944747 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.EmptyBuildLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -26,6 +27,8 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { + private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + @Test public void testAuthenticate() throws RegistryAuthenticationFailedException, InvalidImageReferenceException, IOException, @@ -33,7 +36,9 @@ public void testAuthenticate() ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) + BUILD_LOGGER, + dockerHubImageReference.getRegistry(), + dockerHubImageReference.getRepository()) .initialize(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java b/jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java index 368e0a37e4..ff61e8f2c1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildLogger.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib; public interface BuildLogger { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java index 64ed458ff2..4b348cb5cb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib; -import com.google.cloud.tools.jib.builder.BuildLogger; import java.io.Closeable; import javax.annotation.Nullable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 57f18c439c..eb07ac890f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -81,6 +81,7 @@ public Authorization call() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( + buildConfiguration.getBuildLogger(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index b456b3817f..8d68f8141d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -71,6 +71,7 @@ public CachedLayer call() throws IOException, RegistryException { new Timer(buildConfiguration.getBuildLogger(), String.format(DESCRIPTION, layerDigest))) { RegistryClient registryClient = RegistryClient.factory( + buildConfiguration.getBuildLogger(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index e454e1b308..821c9a65a6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -140,6 +140,7 @@ public BaseImageWithAuthorization call() // See https://docs.docker.com/registry/spec/auth/token RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( + buildConfiguration.getBuildLogger(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) @@ -179,6 +180,7 @@ private Image pullBaseImage(@Nullable Authorization registryCredentials) LayerCountMismatchException, BadContainerConfigurationFormatException { RegistryClient registryClient = RegistryClient.factory( + buildConfiguration.getBuildLogger(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 993bccee74..0bf10ab9b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -70,6 +70,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = RegistryClient.factory( + buildConfiguration.getBuildLogger(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 15760d9d98..92d7bf0266 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -118,6 +118,7 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { RegistryClient registryClient = RegistryClient.factory( + buildConfiguration.getBuildLogger(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 938a2c8a34..8ae1fc244f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 12282292b4..ab8af72e78 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 71ccc88365..47e56a8c67 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -18,7 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index f861a4f621..095a1cf84f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java index 6afd732de6..42cb4bf117 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 86486f8a4f..3caee97f3d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; @@ -44,6 +45,7 @@ public class RegistryAuthenticator { /** Initializer for {@link RegistryAuthenticator}. */ public static class Initializer { + private final BuildLogger buildLogger; private final String serverUrl; private final String repository; private boolean allowInsecureRegistries = false; @@ -51,10 +53,12 @@ public static class Initializer { /** * Instantiates a new initializer for {@link RegistryAuthenticator}. * + * @param buildLogger the build logger used for printing messages * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) */ - private Initializer(String serverUrl, String repository) { + private Initializer(BuildLogger buildLogger, String serverUrl, String repository) { + this.buildLogger = buildLogger; this.serverUrl = serverUrl; this.repository = repository; } @@ -77,7 +81,7 @@ public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { public RegistryAuthenticator initialize() throws RegistryAuthenticationFailedException, IOException, RegistryException { try { - return RegistryClient.factory(serverUrl, repository) + return RegistryClient.factory(buildLogger, serverUrl, repository) .setAllowInsecureRegistries(allowInsecureRegistries) .newRegistryClient() .getRegistryAuthenticator(); @@ -95,12 +99,14 @@ public RegistryAuthenticator initialize() /** * Gets a new initializer for {@link RegistryAuthenticator}. * + * @param buildLogger the build logger used for printing messages * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) * @return the new {@link Initializer} */ - public static Initializer initializer(String serverUrl, String repository) { - return new Initializer(serverUrl, repository); + public static Initializer initializer( + BuildLogger buildLogger, String serverUrl, String repository) { + return new Initializer(buildLogger, serverUrl, repository); } // TODO: Replace with a WWW-Authenticate header parser. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 6339bfd008..e83b2dcca3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -66,12 +66,16 @@ public RegistryClient setTimer(Timer parentTimer) { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { + private final BuildLogger buildLogger; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; @Nullable private Authorization authorization; - private Factory(RegistryEndpointRequestProperties registryEndpointRequestProperties) { + private Factory( + BuildLogger buildLogger, + RegistryEndpointRequestProperties registryEndpointRequestProperties) { + this.buildLogger = buildLogger; this.registryEndpointRequestProperties = registryEndpointRequestProperties; } @@ -105,6 +109,7 @@ public Factory setAuthorization(@Nullable Authorization authorization) { */ public RegistryClient newRegistryClient() { return new RegistryClient( + buildLogger, authorization, registryEndpointRequestProperties, allowInsecureRegistries, @@ -115,12 +120,15 @@ public RegistryClient newRegistryClient() { @Nullable private static String userAgentSuffix; /** + * Creates a new {@link Factory} for building a {@link RegistryClient}. + * + * @param buildLogger the build logger used for printing messages * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) * @return the new {@link Factory} */ - public static Factory factory(String serverUrl, String imageName) { - return new Factory(new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory(BuildLogger buildLogger, String serverUrl, String imageName) { + return new Factory(buildLogger, new RegistryEndpointRequestProperties(serverUrl, imageName)); } // TODO: Inject via a RegistryClient.Factory. @@ -159,6 +167,7 @@ static String makeUserAgent() { return userAgentBuilder.toString(); } + private final BuildLogger buildLogger; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -167,15 +176,18 @@ static String makeUserAgent() { /** * Instantiate with {@link #factory}. * + * @param buildLogger the build logger used for printing messages * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( + BuildLogger buildLogger, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, String userAgent) { + this.buildLogger = buildLogger; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; @@ -334,6 +346,7 @@ String getUserAgent() { private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { return new RegistryEndpointCaller<>( + buildLogger, userAgent, getApiRouteBase(), registryEndpointProvider, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 101c6d07f5..e23f21184a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -19,6 +19,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; @@ -51,6 +52,7 @@ class RegistryEndpointCaller { private static final String DEFAULT_PROTOCOL = "https"; + private final BuildLogger logger; private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -67,6 +69,7 @@ class RegistryEndpointCaller { /** * Constructs with parameters for making the request. * + * @param logger the build logger used for printing messages * @param userAgent {@code User-Agent} header to send with the request * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint @@ -76,6 +79,7 @@ class RegistryEndpointCaller { * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( + BuildLogger logger, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -84,6 +88,7 @@ class RegistryEndpointCaller { boolean allowInsecureRegistries) throws MalformedURLException { this( + logger, userAgent, apiRouteBase, registryEndpointProvider, @@ -96,6 +101,7 @@ class RegistryEndpointCaller { @VisibleForTesting RegistryEndpointCaller( + BuildLogger logger, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -105,6 +111,7 @@ class RegistryEndpointCaller { Function connectionFactory, @Nullable Function insecureConnectionFactory) throws MalformedURLException { + this.logger = logger; this.initialRequestUrl = registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; @@ -225,6 +232,8 @@ T call(URL url) throws IOException, RegistryException { if ("https".equals(url.getProtocol())) { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); + logger.warn( + "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl); return call(httpUrl.toURL()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java index 45720a5df1..1d500b8c45 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder; +import com.google.cloud.tools.jib.BuildLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 2b2a2b5164..ba4d65b247 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildLogger; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 61f7e2ad43..fc987d52ab 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index c82c04fc2f..eb9425937e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index fa7c4a93f0..f72b2509cb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -18,7 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index eb31b03b26..864b7149ed 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 758fa73be6..ea69697649 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.net.URISyntaxException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java index 838f723f9b..a1c5b25efa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index fe6d0f63c0..c3c4dc4c42 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; @@ -30,13 +31,15 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { + @Mock private BuildLogger buildLogger; @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; @Before public void setUp() { - testRegistryClientFactory = RegistryClient.factory("some.server.url", "some image name"); + testRegistryClientFactory = + RegistryClient.factory(buildLogger, "some.server.url", "some image name"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 9edbfed2ee..4853b2a34f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,6 +20,7 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -90,6 +91,7 @@ public String getActionDescription() { } } + @Mock private BuildLogger mockBuildLogger; @Mock private Connection mockConnection; @Mock private Response mockResponse; @Mock private Function mockConnectionFactory; @@ -331,6 +333,12 @@ private void verifyRetriesWithHttp(Class exceptionClass) Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlArgumentCaptor.capture()); Assert.assertEquals("https", urlArgumentCaptor.getAllValues().get(0).getProtocol()); Assert.assertEquals("http", urlArgumentCaptor.getAllValues().get(1).getProtocol()); + Mockito.verify(mockBuildLogger) + .warn( + "Failed to connect to " + + urlArgumentCaptor.getAllValues().get(0) + + " over HTTPS. Attempting again with HTTP: " + + urlArgumentCaptor.getAllValues().get(1)); } /** @@ -415,6 +423,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) private RegistryEndpointCaller createRegistryEndpointCaller(boolean allowInsecure) throws MalformedURLException { return new RegistryEndpointCaller<>( + mockBuildLogger, "userAgent", "apiRouteBase", new TestRegistryEndpointProvider(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java index 75c365ae2e..77e5406c07 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index bb5638f43f..4a593b29da 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 75d7f82027..f62bbada70 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.common.base.Preconditions; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 66cf5785c3..cf5d2b217e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 9711976842..87398f10a0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java index 409b0cb15d..ef05b4c38a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import org.apache.maven.plugin.logging.Log; /** Implementation of {@link BuildLogger} for Maven plugins. */ diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 499cd99dbc..8465991043 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 0f92d9d293..459c22896e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.builder.BuildLogger; +import com.google.cloud.tools.jib.BuildLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; From 5b9e6d36bed27bbba64f1c77f3b37c8a7b52c23c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 1 Aug 2018 11:45:51 -0400 Subject: [PATCH 0082/2020] Fix incorrect target authentication error message when base and target registry are the same (#761) --- .../tools/jib/frontend/BuildStepsRunner.java | 22 +++++---- .../jib/frontend/BuildStepsRunnerTest.java | 48 ++++++++++++++++--- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index 47e56a8c67..fbf7944a7e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -122,14 +122,20 @@ private static void handleRegistryUnauthorizedException( registryUnauthorizedException); } else { - boolean isRegistryForBase = + boolean isImageForBase = registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getBaseImageConfiguration().getImageRegistry()); - boolean isRegistryForTarget = + .getRegistry() + .equals(buildConfiguration.getBaseImageConfiguration().getImageRegistry()) + && registryUnauthorizedException + .getRepository() + .equals(buildConfiguration.getBaseImageConfiguration().getImageRepository()); + boolean isImageForTarget = registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()); + .getRegistry() + .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()) + && registryUnauthorizedException + .getRepository() + .equals(buildConfiguration.getTargetImageConfiguration().getImageRepository()); boolean areBaseImageCredentialsConfigured = buildConfiguration.getBaseImageConfiguration().getCredentialHelper() != null || buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials() @@ -139,13 +145,13 @@ private static void handleRegistryUnauthorizedException( || buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials() != null; - if (isRegistryForBase && !areBaseImageCredentialsConfigured) { + if (isImageForBase && !areBaseImageCredentialsConfigured) { throw new BuildStepsExecutionException( helpfulSuggestions.forNoCredentialHelpersDefinedForBaseImage( registryUnauthorizedException.getRegistry()), registryUnauthorizedException); } - if (isRegistryForTarget && !areTargetImageCredentialsConfigured) { + if (isImageForTarget && !areTargetImageCredentialsConfigured) { throw new BuildStepsExecutionException( helpfulSuggestions.forNoCredentialHelpersDefinedForTargetImage( registryUnauthorizedException.getRegistry()), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index f72b2509cb..62ba142baa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -69,7 +69,8 @@ public class BuildStepsRunnerTest { @Mock private HttpResponseException mockHttpResponseException; @Mock private ExecutionException mockExecutionException; @Mock private BuildConfiguration mockBuildConfiguration; - @Mock private ImageConfiguration mockImageConfiguration; + @Mock private ImageConfiguration mockFromImageConfiguration; + @Mock private ImageConfiguration mockToImageConfiguration; private BuildStepsRunner testBuildImageStepsRunner; @@ -79,9 +80,9 @@ public void setUpMocks() { Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); Mockito.when(mockBuildConfiguration.getBaseImageConfiguration()) - .thenReturn(mockImageConfiguration); + .thenReturn(mockFromImageConfiguration); Mockito.when(mockBuildConfiguration.getTargetImageConfiguration()) - .thenReturn(mockImageConfiguration); + .thenReturn(mockToImageConfiguration); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( @@ -203,12 +204,14 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); + Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn("someregistry"); + Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("someregistry"); + Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("somerepository"); try { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); @@ -222,6 +225,36 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr } } + @Test + public void testBuildImage_executionException_registryUnauthorizedException_sameRegistry() + throws CacheDirectoryNotOwnedException, InterruptedException, ExecutionException, + CacheMetadataCorruptedException, CacheDirectoryCreationException, IOException { + Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) + .thenReturn(mockHttpResponseException); + Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("toRegistry"); + Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("toRepository"); + Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown + + Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); + Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + + Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("toRegistry"); + Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("fromRepository"); + Mockito.when(mockToImageConfiguration.getImageRegistry()).thenReturn("toRegistry"); + Mockito.when(mockToImageConfiguration.getImageRepository()).thenReturn("toRepository"); + + try { + testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals( + TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForTargetImage("toRegistry"), + ex.getMessage()); + Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); + } + } + @Test public void testBuildImage_executionException_registryCredentialsNotSentException() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, @@ -247,13 +280,16 @@ public void testBuildImage_executionException_registryUnauthorizedException_othe Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); + Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn("someregistry"); - Mockito.when(mockImageConfiguration.getCredentialHelper()).thenReturn("some-credential-helper"); + Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("someregistry"); + Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("somerepository"); + Mockito.when(mockFromImageConfiguration.getCredentialHelper()) + .thenReturn("some-credential-helper"); try { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3959dbdc58..959e0d056e 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) - Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) - Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) ## 0.9.7 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 2c8f43e8a9..b412c02b34 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. - Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) - Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) - Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) ## 0.9.7 From 373300b82fbe67aa293bdf35436d938b4d3771ae Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 1 Aug 2018 12:03:20 -0400 Subject: [PATCH 0083/2020] Rename BuildLogger to JibLogger (#760) --- ...tyBuildLogger.java => EmptyJibLogger.java} | 2 +- .../builder/BuildStepsIntegrationTest.java | 2 +- ...icationMethodRetrieverIntegrationTest.java | 4 +-- .../registry/BlobCheckerIntegrationTest.java | 4 +-- .../registry/BlobPullerIntegrationTest.java | 4 +-- .../registry/BlobPusherIntegrationTest.java | 4 +-- .../ManifestPullerIntegrationTest.java | 4 +-- .../ManifestPusherIntegrationTest.java | 4 +-- .../RegistryAuthenticatorIntegrationTest.java | 4 +-- .../jib/{BuildLogger.java => JibLogger.java} | 2 +- .../com/google/cloud/tools/jib/Timer.java | 6 ++--- .../RetrieveRegistryCredentialsStep.java | 8 +++--- .../jib/configuration/BuildConfiguration.java | 14 +++++----- .../tools/jib/frontend/BuildStepsRunner.java | 4 +-- .../tools/jib/frontend/MainClassFinder.java | 6 ++--- .../tools/jib/frontend/ProjectProperties.java | 4 +-- .../jib/registry/RegistryAuthenticator.java | 8 +++--- .../tools/jib/registry/RegistryClient.java | 14 +++++----- .../jib/registry/RegistryEndpointCaller.java | 8 +++--- ...estBuildLogger.java => TestJibLogger.java} | 8 +++--- ...BuildAndCacheApplicationLayerStepTest.java | 4 +-- .../jib/builder/steps/BuildImageStepTest.java | 4 +-- .../RetrieveRegistryCredentialsStepTest.java | 4 +-- .../configuration/BuildConfigurationTest.java | 26 +++++++++---------- .../jib/frontend/BuildStepsRunnerTest.java | 4 +-- .../jib/frontend/ExposedPortsParserTest.java | 4 +-- .../jib/frontend/MainClassFinderTest.java | 4 +-- .../jib/frontend/MainClassResolverTest.java | 4 +-- .../jib/registry/RegistryClientTest.java | 4 +-- .../registry/RegistryEndpointCallerTest.java | 4 +-- .../tools/jib/gradle/BuildDockerTask.java | 16 ++++++------ .../tools/jib/gradle/BuildImageTask.java | 14 +++++----- .../cloud/tools/jib/gradle/BuildTarTask.java | 16 ++++++------ .../tools/jib/gradle/DockerContextTask.java | 8 +++--- ...eBuildLogger.java => GradleJibLogger.java} | 8 +++--- .../jib/gradle/GradleLayerConfigurations.java | 9 +++---- .../jib/gradle/GradleProjectProperties.java | 24 ++++++++--------- .../cloud/tools/jib/gradle/JibExtension.java | 4 +-- .../gradle/GradleLayerConfigurationsTest.java | 12 ++++----- .../gradle/GradleProjectPropertiesTest.java | 13 +++++----- .../tools/jib/gradle/JibExtensionTest.java | 4 +-- .../tools/jib/maven/BuildDockerMojo.java | 18 ++++++------- .../cloud/tools/jib/maven/BuildImageMojo.java | 16 ++++++------ .../cloud/tools/jib/maven/BuildTarMojo.java | 18 ++++++------- .../tools/jib/maven/DockerContextMojo.java | 8 +++--- .../jib/maven/JibPluginConfiguration.java | 4 +-- ...enBuildLogger.java => MavenJibLogger.java} | 8 +++--- .../jib/maven/MavenProjectProperties.java | 26 +++++++++---------- .../maven/MavenSettingsServerCredentials.java | 10 +++---- .../jib/maven/JibPluginConfigurationTest.java | 4 +-- .../jib/maven/MavenProjectPropertiesTest.java | 13 +++++----- .../MavenSettingsServerCredentialsTest.java | 2 +- 52 files changed, 213 insertions(+), 218 deletions(-) rename jib-core/src/integration-test/java/com/google/cloud/tools/jib/{EmptyBuildLogger.java => EmptyJibLogger.java} (94%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{BuildLogger.java => JibLogger.java} (96%) rename jib-core/src/test/java/com/google/cloud/tools/jib/builder/{TestBuildLogger.java => TestJibLogger.java} (86%) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{GradleBuildLogger.java => GradleJibLogger.java} (93%) rename jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/{MavenBuildLogger.java => MavenJibLogger.java} (89%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java similarity index 94% rename from jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java rename to jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java index fc423a1d5f..ed8148c97d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyBuildLogger.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib; -public class EmptyBuildLogger implements BuildLogger { +public class EmptyJibLogger implements JibLogger { @Override public void error(CharSequence message) {} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 43409a919a..c477d1e6aa 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -63,7 +63,7 @@ private static ImmutableList getResourceFilesList(String resourcePath) @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final TestBuildLogger logger = new TestBuildLogger(); + private static final TestJibLogger logger = new TestJibLogger(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index f8c16ba01d..80033d5fcd 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -25,7 +25,7 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test public void testGetRegistryAuthenticator() diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 54ad3f4bcf..bb27fd4858 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -29,7 +29,7 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyBuildLogger buildLogger = new EmptyBuildLogger(); + private static final EmptyJibLogger buildLogger = new EmptyJibLogger(); @Test public void testCheck_exists() throws IOException, RegistryException { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 957b5762ab..2e33057ba2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -36,7 +36,7 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 08359bf184..e481689b23 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -30,7 +30,7 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test public void testPush() throws DigestException, IOException, RegistryException { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 9315501965..d61fdb7382 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -30,7 +30,7 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test public void testPull_v21() throws IOException, RegistryException { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 973514f252..5b52ec768f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -18,7 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -34,7 +34,7 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test public void testPush_missingBlobs() throws IOException, RegistryException { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 7c73944747..c8ca3b37a8 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyBuildLogger; +import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -27,7 +27,7 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { - private static final EmptyBuildLogger BUILD_LOGGER = new EmptyBuildLogger(); + private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test public void testAuthenticate() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java similarity index 96% rename from jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java index ff61e8f2c1..1629c57d20 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/BuildLogger.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib; -public interface BuildLogger { +public interface JibLogger { void error(CharSequence message); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java index 4b348cb5cb..f9aeac0a70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java @@ -25,17 +25,17 @@ */ public class Timer implements Closeable { - private final BuildLogger buildLogger; + private final JibLogger buildLogger; private final int depth; @Nullable private String label; private long startTime = System.nanoTime(); - public Timer(BuildLogger buildLogger, String label) { + public Timer(JibLogger buildLogger, String label) { this(buildLogger, label, 0); } - private Timer(BuildLogger buildLogger, String label, int depth) { + private Timer(JibLogger buildLogger, String label, int depth) { this.buildLogger = buildLogger; this.label = label; this.depth = depth; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 8ae1fc244f..e8d39454a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -68,7 +68,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials()); } - private final BuildLogger buildLogger; + private final JibLogger buildLogger; private final String registry; @Nullable private final String credentialHelperSuffix; @Nullable private final RegistryCredentials knownRegistryCredentials; @@ -80,7 +80,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( @VisibleForTesting RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, - BuildLogger buildLogger, + JibLogger buildLogger, String registry, @Nullable String credentialHelperSuffix, @Nullable RegistryCredentials knownRegistryCredentials, @@ -99,7 +99,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( /** Instantiate with {@link #forBaseImage} or {@link #forTargetImage}. */ private RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, - BuildLogger buildLogger, + JibLogger buildLogger, String registry, @Nullable String credentialHelperSuffix, @Nullable RegistryCredentials knownRegistryCredentials) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index ab8af72e78..f5ad1f866f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; @@ -39,9 +39,9 @@ public static class Builder { private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = V22ManifestTemplate.class; - private BuildLogger buildLogger; + private JibLogger buildLogger; - private Builder(BuildLogger buildLogger) { + private Builder(JibLogger buildLogger) { this.buildLogger = buildLogger; } @@ -186,11 +186,11 @@ public BuildConfiguration build() { } } - public static Builder builder(BuildLogger buildLogger) { + public static Builder builder(JibLogger buildLogger) { return new Builder(buildLogger); } - private final BuildLogger buildLogger; + private final JibLogger buildLogger; private final ImageConfiguration baseImageConfiguration; private final ImageConfiguration targetImageConfiguration; @Nullable private final ContainerConfiguration containerConfiguration; @@ -202,7 +202,7 @@ public static Builder builder(BuildLogger buildLogger) { /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( - BuildLogger buildLogger, + JibLogger buildLogger, ImageConfiguration baseImageConfiguration, ImageConfiguration targetImageConfiguration, @Nullable ContainerConfiguration containerConfiguration, @@ -222,7 +222,7 @@ private BuildConfiguration( this.layerConfigurations = layerConfigurations; } - public BuildLogger getBuildLogger() { + public JibLogger getBuildLogger() { return buildLogger; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java index fbf7944a7e..0b0845db78 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java @@ -18,7 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; @@ -189,7 +189,7 @@ private static String capitalizeFirstLetter(String string) { public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { try { // TODO: This logging should be injected via another logging class. - BuildLogger buildLogger = buildSteps.getBuildConfiguration().getBuildLogger(); + JibLogger buildLogger = buildSteps.getBuildConfiguration().getBuildLogger(); buildLogger.lifecycle(""); buildLogger.lifecycle(buildSteps.getStartupMessage()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 095a1cf84f..e6e6e3fc28 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -136,7 +136,7 @@ public List getFoundMainClasses() { } private final ImmutableList classesFiles; - private final BuildLogger buildLogger; + private final JibLogger buildLogger; /** * Finds a class with {@code psvm} in {@code classesFiles}. @@ -144,7 +144,7 @@ public List getFoundMainClasses() { * @param classesFiles the classes files to check * @param buildLogger used for displaying status messages. */ - public MainClassFinder(ImmutableList classesFiles, BuildLogger buildLogger) { + public MainClassFinder(ImmutableList classesFiles, JibLogger buildLogger) { this.classesFiles = classesFiles; this.buildLogger = buildLogger; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java index 42cb4bf117..097d4aa89d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; @@ -29,7 +29,7 @@ public interface ProjectProperties { /** Directory name for the cache. The directory will be relative to the build output directory. */ String CACHE_DIRECTORY_NAME = "jib-cache"; - BuildLogger getLogger(); + JibLogger getLogger(); String getPluginName(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 3caee97f3d..49d0a4232e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; @@ -45,7 +45,7 @@ public class RegistryAuthenticator { /** Initializer for {@link RegistryAuthenticator}. */ public static class Initializer { - private final BuildLogger buildLogger; + private final JibLogger buildLogger; private final String serverUrl; private final String repository; private boolean allowInsecureRegistries = false; @@ -57,7 +57,7 @@ public static class Initializer { * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) */ - private Initializer(BuildLogger buildLogger, String serverUrl, String repository) { + private Initializer(JibLogger buildLogger, String serverUrl, String repository) { this.buildLogger = buildLogger; this.serverUrl = serverUrl; this.repository = repository; @@ -105,7 +105,7 @@ public RegistryAuthenticator initialize() * @return the new {@link Initializer} */ public static Initializer initializer( - BuildLogger buildLogger, String serverUrl, String repository) { + JibLogger buildLogger, String serverUrl, String repository) { return new Initializer(buildLogger, serverUrl, repository); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index e83b2dcca3..c8e698dae1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; @@ -40,7 +40,7 @@ public class RegistryClient { // TODO: Remove private Timer parentTimer = new Timer( - new BuildLogger() { + new JibLogger() { @Override public void debug(CharSequence message) {} @@ -66,14 +66,14 @@ public RegistryClient setTimer(Timer parentTimer) { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { - private final BuildLogger buildLogger; + private final JibLogger buildLogger; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; @Nullable private Authorization authorization; private Factory( - BuildLogger buildLogger, + JibLogger buildLogger, RegistryEndpointRequestProperties registryEndpointRequestProperties) { this.buildLogger = buildLogger; this.registryEndpointRequestProperties = registryEndpointRequestProperties; @@ -127,7 +127,7 @@ public RegistryClient newRegistryClient() { * @param imageName the image/repository name (also known as, namespace) * @return the new {@link Factory} */ - public static Factory factory(BuildLogger buildLogger, String serverUrl, String imageName) { + public static Factory factory(JibLogger buildLogger, String serverUrl, String imageName) { return new Factory(buildLogger, new RegistryEndpointRequestProperties(serverUrl, imageName)); } @@ -167,7 +167,7 @@ static String makeUserAgent() { return userAgentBuilder.toString(); } - private final BuildLogger buildLogger; + private final JibLogger buildLogger; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -182,7 +182,7 @@ static String makeUserAgent() { * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( - BuildLogger buildLogger, + JibLogger buildLogger, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index e23f21184a..6a1daa23c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -19,7 +19,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; @@ -52,7 +52,7 @@ class RegistryEndpointCaller { private static final String DEFAULT_PROTOCOL = "https"; - private final BuildLogger logger; + private final JibLogger logger; private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -79,7 +79,7 @@ class RegistryEndpointCaller { * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( - BuildLogger logger, + JibLogger logger, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -101,7 +101,7 @@ class RegistryEndpointCaller { @VisibleForTesting RegistryEndpointCaller( - BuildLogger logger, + JibLogger logger, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java similarity index 86% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java index 1d500b8c45..d40d9e92c2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestBuildLogger.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java @@ -16,14 +16,14 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Implementation of {@link BuildLogger} for testing purposes. */ -public class TestBuildLogger implements BuildLogger { +/** Implementation of {@link JibLogger} for testing purposes. */ +public class TestJibLogger implements JibLogger { - private static final Logger LOGGER = LoggerFactory.getLogger(TestBuildLogger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TestJibLogger.class); @Override public void debug(CharSequence message) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index ba7c0c493a..943c218951 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TestBuildLogger; +import com.google.cloud.tools.jib.builder.TestJibLogger; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; @@ -104,7 +104,7 @@ public void setUp() throws IOException, URISyntaxException { EXTRA_FILES_LAYER_EXTRACTION_PATH) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestBuildLogger()); + Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestJibLogger()); temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index ba4d65b247..c8360db412 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; @@ -48,7 +48,7 @@ public class BuildImageStepTest { @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Mock private PullBaseImageStep mockPullBaseImageStep; @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index fc987d52ab..6447f037bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; @@ -44,7 +44,7 @@ public class RetrieveRegistryCredentialsStepTest { @Mock private ListeningExecutorService mockListeningExecutorService; @Mock private ImageConfiguration mockImageConfiguration; - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @Mock private DockerCredentialHelper mockDockerCredentialHelper; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index eb9425937e..a94abdf7df 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -92,7 +92,7 @@ public void testBuilder() { .setExposedPorts(expectedExposedPorts) .build(); BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfiguration) @@ -168,7 +168,7 @@ public void testBuilder_default() { expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .build(); BuildConfiguration buildConfiguration = - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .build(); @@ -191,7 +191,7 @@ public void testBuilder_default() { public void testBuilder_missingValues() { // Target image is missing try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .build(); @@ -203,7 +203,7 @@ public void testBuilder_missingValues() { // All required fields missing try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)).build(); + BuildConfiguration.builder(Mockito.mock(JibLogger.class)).build(); Assert.fail("Build configuration should not be built with missing values"); } catch (IllegalStateException ex) { @@ -218,7 +218,7 @@ public void testBuilder_missingValues() { public void testBuilder_nullValues() { // Java arguments element should not be null. try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder() .setProgramArguments(Arrays.asList("first", null)) @@ -230,7 +230,7 @@ public void testBuilder_nullValues() { // Entrypoint element should not be null. try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEntrypoint(Arrays.asList("first", null)).build()); Assert.fail("The IllegalArgumentException should be thrown."); @@ -240,7 +240,7 @@ public void testBuilder_nullValues() { // Exposed ports element should not be null. try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder() .setExposedPorts(Arrays.asList(new Port(1000, Protocol.TCP), null)) @@ -255,7 +255,7 @@ public void testBuilder_nullValues() { nullKeyMap.put(null, "value"); try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(nullKeyMap).build()); Assert.fail("The IllegalArgumentException should be thrown."); @@ -267,7 +267,7 @@ public void testBuilder_nullValues() { Map nullValueMap = new HashMap<>(); nullValueMap.put("key", null); try { - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(nullValueMap).build()); Assert.fail("The IllegalArgumentException should be thrown."); @@ -276,15 +276,15 @@ public void testBuilder_nullValues() { } // Can accept empty environment. - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(ImmutableMap.of()).build()); // Environment map can accept TreeMap and Hashtable. - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(new TreeMap<>()).build()); - BuildConfiguration.builder(Mockito.mock(BuildLogger.class)) + BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setContainerConfiguration( ContainerConfiguration.builder().setEnvironment(new Hashtable<>()).build()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java index 62ba142baa..3c6b219771 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java @@ -18,7 +18,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; @@ -63,7 +63,7 @@ public class BuildStepsRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildSteps mockBuildSteps; - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index 864b7149ed..c339020e9c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; @@ -33,7 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class ExposedPortsParserTest { - @Mock private BuildLogger mockLogger; + @Mock private JibLogger mockLogger; @Test public void testParse() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index ea69697649..0279d9875d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.net.URISyntaxException; @@ -33,7 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Test public void testFindMainClass_simple() throws URISyntaxException { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java index a1c5b25efa..6f5795a465 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; @@ -36,7 +36,7 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassResolverTest { - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Mock private ProjectProperties mockProjectProperties; @Mock private HelpfulSuggestions mockHelpfulSuggestions; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index c3c4dc4c42..8d564318c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; @@ -31,7 +31,7 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { - @Mock private BuildLogger buildLogger; + @Mock private JibLogger buildLogger; @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 4853b2a34f..134789e54c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,7 +20,7 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -91,7 +91,7 @@ public String getActionDescription() { } } - @Mock private BuildLogger mockBuildLogger; + @Mock private JibLogger mockBuildLogger; @Mock private Connection mockConnection; @Mock private Response mockResponse; @Mock private Function mockConnectionFactory; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index adba0a3672..bb54c77241 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -83,12 +83,12 @@ public void buildDocker() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); + GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + jibExtension.handleDeprecatedParameters(gradleJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } @@ -100,10 +100,10 @@ public void buildDocker() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = - gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); + gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageTask. @@ -123,13 +123,13 @@ public void buildDocker() throws InvalidImageReferenceException { .setProgramArguments(jibExtension.getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleBuildLogger) + BuildConfiguration.builder(gradleJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -147,7 +147,7 @@ public void buildDocker() throws InvalidImageReferenceException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - GradleBuildLogger.disableHttpLogging(); + GradleJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 999d68fbbc..bca11ed13b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -79,8 +79,8 @@ public void setTargetImage(String targetImage) { public void buildImage() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); + GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + jibExtension.handleDeprecatedParameters(gradleJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { @@ -91,7 +91,7 @@ public void buildImage() throws InvalidImageReferenceException { } if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } @@ -108,7 +108,7 @@ public void buildImage() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); // Builds the BuildConfiguration. @@ -132,13 +132,13 @@ public void buildImage() throws InvalidImageReferenceException { .setProgramArguments(jibExtension.getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleBuildLogger) + BuildConfiguration.builder(gradleJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -158,7 +158,7 @@ public void buildImage() throws InvalidImageReferenceException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - GradleBuildLogger.disableHttpLogging(); + GradleJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 6943704065..3257894a57 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -111,12 +111,12 @@ private String getTargetPath() { public void buildTar() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); + GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + jibExtension.handleDeprecatedParameters(gradleJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } @@ -128,10 +128,10 @@ public void buildTar() throws InvalidImageReferenceException { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); ImageReference targetImage = - gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleBuildLogger); + gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); // Builds the BuildConfiguration. // TODO: Consolidate with BuildImageTask/BuildDockerTask. @@ -151,13 +151,13 @@ public void buildTar() throws InvalidImageReferenceException { .setProgramArguments(jibExtension.getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); if (jibExtension.getUseCurrentTimestamp()) { - gradleBuildLogger.warn( + gradleJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleBuildLogger) + BuildConfiguration.builder(gradleJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -175,7 +175,7 @@ public void buildTar() throws InvalidImageReferenceException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - GradleBuildLogger.disableHttpLogging(); + GradleJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 24ed9907b9..a86d8816ff 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -99,13 +99,13 @@ public void setTargetDir(String targetDir) { public void generateDockerContext() { Preconditions.checkNotNull(jibExtension); - GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleBuildLogger); + GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + jibExtension.handleDeprecatedParameters(gradleJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleBuildLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); String mainClass = gradleProjectProperties.getMainClass(jibExtension); String targetDir = getTargetDir(); @@ -127,7 +127,7 @@ public void generateDockerContext() { .setExposedPorts(jibExtension.getExposedPorts()) .generate(Paths.get(targetDir)); - gradleBuildLogger.lifecycle("Created Docker context at " + targetDir); + gradleJibLogger.lifecycle("Created Docker context at " + targetDir); } catch (InsecureRecursiveDeleteException ex) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java similarity index 93% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java index 77e5406c07..416f8fd3dc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleBuildLogger.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; @@ -27,10 +27,10 @@ import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; import org.slf4j.LoggerFactory; -/** Implementation of {@link BuildLogger} for Gradle plugins. */ +/** Implementation of {@link JibLogger} for Gradle plugins. */ // We don't care about the return values of the logging futures. @SuppressWarnings("FutureReturnValueIgnored") -class GradleBuildLogger implements BuildLogger { +class GradleJibLogger implements JibLogger { /** This executor keeps all log messages in order. */ private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -58,7 +58,7 @@ static void disableHttpLogging() { private final Logger logger; - GradleBuildLogger(Logger logger) { + GradleJibLogger(Logger logger) { this.logger = logger; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 641f285426..8d609089a0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -50,15 +50,14 @@ class GradleLayerConfigurations { * Resolves the source files configuration for a Gradle {@link Project}. * * @param project the Gradle {@link Project} - * @param gradleBuildLogger the build logger for providing feedback about the resolution + * @param gradleJibLogger the build logger for providing feedback about the resolution * @param extraDirectory path to the directory for the extra files layer * @return a {@link GradleLayerConfigurations} for building the layers for the Gradle {@link * Project} * @throws IOException if an I/O exception occurred during resolution */ static GradleLayerConfigurations getForProject( - Project project, GradleBuildLogger gradleBuildLogger, Path extraDirectory) - throws IOException { + Project project, GradleJibLogger gradleJibLogger, Path extraDirectory) throws IOException { JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); @@ -75,7 +74,7 @@ static GradleLayerConfigurations getForProject( for (File classesOutputDirectory : classesOutputDirectories) { if (Files.notExists(classesOutputDirectory.toPath())) { // Warns that output directory was not found. - gradleBuildLogger.warn( + gradleJibLogger.warn( "Could not find build output directory '" + classesOutputDirectory + "'"); continue; } @@ -84,7 +83,7 @@ static GradleLayerConfigurations getForProject( } } if (classesFiles.isEmpty()) { - gradleBuildLogger.warn("No classes files were found - did you compile your project?"); + gradleJibLogger.warn("No classes files were found - did you compile your project?"); } // Adds each file in the resources output directory to the resources files list. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 4a593b29da..02c94bbe4a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; @@ -51,12 +51,12 @@ class GradleProjectProperties implements ProjectProperties { /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, GradleBuildLogger gradleBuildLogger, Path extraDirectory) { + Project project, GradleJibLogger gradleJibLogger, Path extraDirectory) { try { return new GradleProjectProperties( project, - gradleBuildLogger, - GradleLayerConfigurations.getForProject(project, gradleBuildLogger, extraDirectory)); + gradleJibLogger, + GradleLayerConfigurations.getForProject(project, gradleJibLogger, extraDirectory)); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); @@ -64,16 +64,16 @@ static GradleProjectProperties getForProject( } private final Project project; - private final GradleBuildLogger gradleBuildLogger; + private final GradleJibLogger gradleJibLogger; private final GradleLayerConfigurations gradleLayerConfigurations; @VisibleForTesting GradleProjectProperties( Project project, - GradleBuildLogger gradleBuildLogger, + GradleJibLogger gradleJibLogger, GradleLayerConfigurations gradleLayerConfigurations) { this.project = project; - this.gradleBuildLogger = gradleBuildLogger; + this.gradleJibLogger = gradleJibLogger; this.gradleLayerConfigurations = gradleLayerConfigurations; } @@ -113,8 +113,8 @@ public HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix) { } @Override - public BuildLogger getLogger() { - return gradleBuildLogger; + public JibLogger getLogger() { + return gradleJibLogger; } @Override @@ -160,18 +160,18 @@ String getMainClass(JibExtension jibExtension) { * {@code project-name:project-version} if target image is not configured * * @param jibExtension the plugin configuration parameters to generate the name from - * @param gradleBuildLogger the logger used to notify users of the target image parameter + * @param gradleJibLogger the logger used to notify users of the target image parameter * @return an {@link ImageReference} parsed from the configured target image, or one of the form * {@code project-name:project-version} if target image is not configured */ ImageReference getGeneratedTargetDockerTag( - JibExtension jibExtension, GradleBuildLogger gradleBuildLogger) + JibExtension jibExtension, GradleJibLogger gradleJibLogger) throws InvalidImageReferenceException { Preconditions.checkNotNull(jibExtension); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { // TODO: Validate that project name and version are valid repository/tag // TODO: Use HelpfulSuggestions - gradleBuildLogger.lifecycle( + gradleJibLogger.lifecycle( "Tagging image with generated image reference " + project.getName() + ":" diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index f62bbada70..276a6177c3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.common.base.Preconditions; @@ -118,7 +118,7 @@ public JibExtension(Project project) { * * @param logger The logger used to print the warnings */ - void handleDeprecatedParameters(BuildLogger logger) { + void handleDeprecatedParameters(JibLogger logger) { StringBuilder deprecatedParams = new StringBuilder(); if (!jvmFlags.get().isEmpty()) { deprecatedParams.append(" jvmFlags -> container.jvmFlags\n"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 56d372eff3..2adb007eac 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -72,7 +72,7 @@ public Set getFiles() { @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private GradleBuildLogger mockGradleBuildLogger; + @Mock private GradleJibLogger mockGradleJibLogger; private GradleLayerConfigurations testGradleLayerConfigurations; @@ -112,7 +112,7 @@ public void setUp() throws URISyntaxException, IOException { testGradleLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleBuildLogger, Paths.get("nonexistent/path")); + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); } @Test @@ -165,11 +165,11 @@ public void test_noClassesFiles() throws IOException { testGradleLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleBuildLogger, Paths.get("nonexistent/path")); + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); - Mockito.verify(mockGradleBuildLogger) + Mockito.verify(mockGradleJibLogger) .warn("Could not find build output directory '" + nonexistentFile + "'"); - Mockito.verify(mockGradleBuildLogger) + Mockito.verify(mockGradleJibLogger) .warn("No classes files were found - did you compile your project?"); } @@ -179,7 +179,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { testGradleLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleBuildLogger, extraFilesDirectory); + mockProject, mockGradleJibLogger, extraFilesDirectory); ImmutableList expectedExtraFiles = ImmutableList.of( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 696a46f5d7..2bac31a16a 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -42,9 +42,8 @@ public class GradleProjectPropertiesTest { @Mock private Jar mockJar; @Mock private Jar mockJar2; @Mock private Project mockProject; - @Mock private GradleBuildLogger mockGradleBuildLogger; + @Mock private GradleJibLogger mockGradleJibLogger; @Mock private JibExtension mockJibExtension; - @Mock private GradleBuildLogger mockBuildLogger; @Mock private GradleLayerConfigurations mockGradleLayerConfigurations; private Manifest manifest; @@ -57,7 +56,7 @@ public void setup() { gradleProjectProperties = new GradleProjectProperties( - mockProject, mockGradleBuildLogger, mockGradleLayerConfigurations); + mockProject, mockGradleJibLogger, mockGradleLayerConfigurations); } @Test @@ -85,10 +84,10 @@ public void testGetMainClassFromJar_multiple() { public void testGetDockerTag_configured() throws InvalidImageReferenceException { Mockito.when(mockJibExtension.getTargetImage()).thenReturn("a/b:c"); ImageReference result = - gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockBuildLogger); + gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockGradleJibLogger); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); + Mockito.verify(mockGradleJibLogger, Mockito.never()).lifecycle(Mockito.any()); } @Test @@ -97,10 +96,10 @@ public void testGetDockerTag_notConfigured() throws InvalidImageReferenceExcepti Mockito.when(mockProject.getVersion()).thenReturn("project-version"); Mockito.when(mockJibExtension.getTargetImage()).thenReturn(null); ImageReference result = - gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockBuildLogger); + gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockGradleJibLogger); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockBuildLogger) + Mockito.verify(mockGradleJibLogger) .lifecycle( "Tagging image with generated image reference project-name:project-version. If you'd " + "like to specify a different tag, you can set the jib.to.image parameter in your " diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index cf5d2b217e..64a8419f7e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -36,7 +36,7 @@ @RunWith(MockitoJUnitRunner.class) public class JibExtensionTest { - @Mock private BuildLogger mockLogger; + @Mock private JibLogger mockLogger; private JibExtension testJibExtension; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index f15c62000d..1f0db91d93 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -60,8 +60,8 @@ public void execute() throws MojoExecutionException { return; } - MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); - handleDeprecatedParameters(mavenBuildLogger); + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + handleDeprecatedParameters(mavenJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); if (!new DockerClient().isDockerInstalled()) { @@ -70,20 +70,20 @@ public void execute() throws MojoExecutionException { // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); + MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); ImageReference baseImage = parseImageReference(getBaseImage(), "from"); ImageReference targetImage = - mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); + mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); // Checks Maven settings for registry credentials. if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null @@ -111,13 +111,13 @@ public void execute() throws MojoExecutionException { .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenBuildLogger) + BuildConfiguration.builder(mavenJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -135,7 +135,7 @@ public void execute() throws MojoExecutionException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - MavenBuildLogger.disableHttpLogging(); + MavenJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 12e0f76c10..b93fe667cf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -64,8 +64,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); - handleDeprecatedParameters(mavenBuildLogger); + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + handleDeprecatedParameters(mavenJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); // Validates 'format'. @@ -94,14 +94,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Checks Maven settings for registry credentials. if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null @@ -115,7 +115,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { : mavenSettingsServerCredentials.retrieve(targetImage.getRegistry()); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); + MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); String mainClass = mavenProjectProperties.getMainClass(this); // Builds the BuildConfiguration. @@ -139,13 +139,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenBuildLogger) + BuildConfiguration.builder(mavenJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -165,7 +165,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - MavenBuildLogger.disableHttpLogging(); + MavenJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 2b4ac0267e..543196ecfa 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -62,26 +62,26 @@ public void execute() throws MojoExecutionException { return; } - MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); - handleDeprecatedParameters(mavenBuildLogger); + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + handleDeprecatedParameters(mavenJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); + MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); ImageReference baseImage = parseImageReference(getBaseImage(), "from"); ImageReference targetImage = - mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenBuildLogger); + mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); // Checks Maven settings for registry credentials. if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger); + Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); Authorization fromAuthorization = getBaseImageAuth(); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null @@ -109,13 +109,13 @@ public void execute() throws MojoExecutionException { .setEnvironment(getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); if (getUseCurrentTimestamp()) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenBuildLogger) + BuildConfiguration.builder(mavenJibLogger) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfigurationBuilder.build()) @@ -133,7 +133,7 @@ public void execute() throws MojoExecutionException { BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); // TODO: Instead of disabling logging, have authentication credentials be provided - MavenBuildLogger.disableHttpLogging(); + MavenJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index cebf59dea8..d202171d34 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -52,14 +52,14 @@ public void execute() throws MojoExecutionException { return; } - MavenBuildLogger mavenBuildLogger = new MavenBuildLogger(getLog()); - handleDeprecatedParameters(mavenBuildLogger); + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + handleDeprecatedParameters(mavenJibLogger); SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); Preconditions.checkNotNull(targetDir); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenBuildLogger, getExtraDirectory()); + MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); String mainClass = mavenProjectProperties.getMainClass(this); try { @@ -80,7 +80,7 @@ public void execute() throws MojoExecutionException { .setExposedPorts(getExposedPorts()) .generate(Paths.get(targetDir)); - mavenBuildLogger.lifecycle("Created Docker context at " + targetDir); + mavenJibLogger.lifecycle("Created Docker context at " + targetDir); } catch (InsecureRecursiveDeleteException ex) { throw new MojoExecutionException( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 87398f10a0..9d5a644393 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; @@ -197,7 +197,7 @@ static Authorization getImageAuth( * * @param logger The logger used to print the warnings */ - void handleDeprecatedParameters(BuildLogger logger) { + void handleDeprecatedParameters(JibLogger logger) { StringBuilder deprecatedParams = new StringBuilder(); if (!jvmFlags.isEmpty()) { deprecatedParams.append(" -> \n"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java similarity index 89% rename from jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java rename to jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java index ef05b4c38a..8cce9108ad 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenBuildLogger.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import org.apache.maven.plugin.logging.Log; -/** Implementation of {@link BuildLogger} for Maven plugins. */ -class MavenBuildLogger implements BuildLogger { +/** Implementation of {@link JibLogger} for Maven plugins. */ +class MavenJibLogger implements JibLogger { /** Disables annoying Apache HTTP client logging. */ static void disableHttpLogging() { @@ -31,7 +31,7 @@ static void disableHttpLogging() { private final Log log; - MavenBuildLogger(Log log) { + MavenJibLogger(Log log) { this.log = log; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 8465991043..777cffed60 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.MainClassInferenceException; @@ -44,19 +44,17 @@ class MavenProjectProperties implements ProjectProperties { /** * @param project the {@link MavenProject} for the plugin. - * @param mavenBuildLogger the logger used for printing status messages. + * @param mavenJibLogger the logger used for printing status messages. * @param extraDirectory path to the directory for the extra files layer * @return a MavenProjectProperties from the given project and logger. * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, MavenBuildLogger mavenBuildLogger, Path extraDirectory) + MavenProject project, MavenJibLogger mavenJibLogger, Path extraDirectory) throws MojoExecutionException { try { return new MavenProjectProperties( - project, - mavenBuildLogger, - MavenLayerConfigurations.getForProject(project, extraDirectory)); + project, mavenJibLogger, MavenLayerConfigurations.getForProject(project, extraDirectory)); } catch (IOException ex) { throw new MojoExecutionException( "Obtaining project build output files failed; make sure you have compiled your project " @@ -67,16 +65,16 @@ static MavenProjectProperties getForProject( } private final MavenProject project; - private final MavenBuildLogger mavenBuildLogger; + private final MavenJibLogger mavenJibLogger; private final MavenLayerConfigurations mavenLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, - MavenBuildLogger mavenBuildLogger, + MavenJibLogger mavenJibLogger, MavenLayerConfigurations mavenLayerConfigurations) { this.project = project; - this.mavenBuildLogger = mavenBuildLogger; + this.mavenJibLogger = mavenJibLogger; this.mavenLayerConfigurations = mavenLayerConfigurations; } @@ -116,8 +114,8 @@ public HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix) { } @Override - public BuildLogger getLogger() { - return mavenBuildLogger; + public JibLogger getLogger() { + return mavenJibLogger; } @Override @@ -181,16 +179,16 @@ String getMainClass(JibPluginConfiguration jibPluginConfiguration) throws MojoEx * {@code project-name:project-version} if target image is not configured * * @param targetImage the configured target image reference (can be empty) - * @param mavenBuildLogger the logger used to notify users of the target image parameter + * @param mavenJibLogger the logger used to notify users of the target image parameter * @return an {@link ImageReference} parsed from the configured target image, or one of the form * {@code project-name:project-version} if target image is not configured */ ImageReference getGeneratedTargetDockerTag( - @Nullable String targetImage, MavenBuildLogger mavenBuildLogger) { + @Nullable String targetImage, MavenJibLogger mavenJibLogger) { if (Strings.isNullOrEmpty(targetImage)) { // TODO: Validate that project name and version are valid repository/tag // TODO: Use HelpfulSuggestions - mavenBuildLogger.lifecycle( + mavenJibLogger.lifecycle( "Tagging image with generated image reference " + project.getName() + ":" diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 0501bdfbb6..61e31e8226 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -57,22 +57,22 @@ static boolean isEncrypted(String password) { private final Settings settings; @Nullable private final SettingsDecrypter settingsDecrypter; - private final MavenBuildLogger mavenBuildLogger; + private final MavenJibLogger mavenJibLogger; /** * Create new instance. * * @param settings the Maven settings object * @param settingsDecrypter the Maven decrypter component - * @param mavenBuildLogger the Maven build log + * @param mavenJibLogger the Maven build log */ MavenSettingsServerCredentials( Settings settings, @Nullable SettingsDecrypter settingsDecrypter, - MavenBuildLogger mavenBuildLogger) { + MavenJibLogger mavenJibLogger) { this.settings = settings; this.settingsDecrypter = settingsDecrypter; - this.mavenBuildLogger = mavenBuildLogger; + this.mavenJibLogger = mavenJibLogger; } /** @@ -113,7 +113,7 @@ RegistryCredentials retrieve(@Nullable String registry) throws MojoExecutionExce registryServer = result.getServer(); } } else if (isEncrypted(registryServer.getPassword())) { - mavenBuildLogger.warn( + mavenJibLogger.warn( "Server password for registry " + registry + " appears to be encrypted, but there is no decrypter available"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 459c22896e..8cb03efea1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.BuildLogger; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; @@ -33,7 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class JibPluginConfigurationTest { - @Mock private BuildLogger mockLogger; + @Mock private JibLogger mockLogger; @Test public void testHandleDeprecatedParameters() { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index d77341aea9..74af806cd4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -33,10 +33,9 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; - @Mock private MavenBuildLogger mockMavenBuildLogger; + @Mock private MavenJibLogger mockMavenJibLogger; @Mock private MavenLayerConfigurations mockMavenLayerConfigurations; @Mock private Plugin mockJarPlugin; - @Mock private MavenBuildLogger mockBuildLogger; private Xpp3Dom jarPluginConfiguration; private Xpp3Dom archive; @@ -51,7 +50,7 @@ public void setup() { Mockito.when(mockMavenProject.getVersion()).thenReturn("project-version"); mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockMavenBuildLogger, mockMavenLayerConfigurations); + mockMavenProject, mockMavenJibLogger, mockMavenLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); @@ -117,19 +116,19 @@ public void testGetMainClassFromJar_missingPlugin() { @Test public void testGetDockerTag_configured() { ImageReference result = - mavenProjectProperties.getGeneratedTargetDockerTag("a/b:c", mockBuildLogger); + mavenProjectProperties.getGeneratedTargetDockerTag("a/b:c", mockMavenJibLogger); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockBuildLogger, Mockito.never()).lifecycle(Mockito.any()); + Mockito.verify(mockMavenJibLogger, Mockito.never()).lifecycle(Mockito.any()); } @Test public void testGetDockerTag_notConfigured() { ImageReference result = - mavenProjectProperties.getGeneratedTargetDockerTag(null, mockBuildLogger); + mavenProjectProperties.getGeneratedTargetDockerTag(null, mockMavenJibLogger); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockBuildLogger) + Mockito.verify(mockMavenJibLogger) .lifecycle( "Tagging image with generated image reference project-name:project-version. If you'd " + "like to specify a different tag, you can set the parameter in your " diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 8b03444ee4..a3d2c3d3c8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -40,7 +40,7 @@ public class MavenSettingsServerCredentialsTest { @Mock private Settings mockSettings; @Mock private Server mockServer1; - @Mock private MavenBuildLogger mockLogger; + @Mock private MavenJibLogger mockLogger; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; From df08f662c4755b91b091b3c9a137d93f27f2d373 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 1 Aug 2018 14:11:29 -0400 Subject: [PATCH 0084/2020] Has integration test check actual build time. (#753) * Makes sure first run has different layer. --- .../maven/BuildImageMojoIntegrationTest.java | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 3785e20ff4..8c4f08061a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,9 +18,14 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; @@ -51,17 +56,17 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setAutoclean(false); - verifier.executeGoal("package"); + verifier.addCliOption("-X"); + verifier.executeGoals(Arrays.asList("clean", "compile")); // Builds twice, and checks if the second build took less time. - long lastTime = System.nanoTime(); verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); - long timeOne = System.nanoTime() - lastTime; - lastTime = System.nanoTime(); + float timeOne = getBuildTimeFromVerifierLog(verifier); if (runTwice) { + verifier.resetStreams(); verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); - long timeTwo = System.nanoTime() - lastTime; + float timeTwo = getBuildTimeFromVerifierLog(verifier); Assert.assertTrue( "First build time (" @@ -87,6 +92,22 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole return new Command("docker", "run", imageReference).run(); } + private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOException { + Pattern pattern = Pattern.compile("Building and pushing image : (?`to` is an object with the following properties: @@ -218,6 +219,14 @@ Property | Type | Default | Description --- | --- | --- | --- `image` | string | *Required* | The image reference for the target image. This can also be specified via the `-Dimage` command line option. `credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). + +`auth` is an object with the following properties (see [Using Specific Credentials](#using-specific-credentials)): + +Property | Type +--- | --- +`username` | `String` +`password` | `String` `container` is an object with the following properties: @@ -310,6 +319,44 @@ Configure credential helpers to use by specifying them as a `credHelper` for the ``` +#### Using Specific Credentials + +You can specify credentials directly in the `` parameter for the `from` and/or `to` images. In the example below, `to` credentials are retrieved from the `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables. + +```xml + + ... + + aws_account_id.dkr.ecr.region.amazonaws.com/my-base-image + + my_username + my_password + + + + gcr.io/my-gcp-project/my-app + + ${env.REGISTRY_USERNAME} + ${env.REGISTRY_PASSWORD} + + + ... + +``` + +Alternatively, you can specify credentials via commandline using the following system properties. + +Property | Description +--- | --- +`-Djib.from.auth.username` | Username for base image registry. +`-Djib.from.auth.password` | Password for base image registry. +`-Djib.to.auth.username` | Username for target image registry. +`-Djib.to.auth.password` | Password for target image registry. + +e.g. `mvn compile jib:build -Djib.to.auth.username=user -Djib.to.auth.password=pass` + +**Note:** This method of authentication should be used only as a last resort, as it is insecure to make your password visible in plain text. + #### Using Maven Settings Registry credentials can be added to your [Maven settings](https://maven.apache.org/settings.html). These credentials will be used if credentials could not be found in any specified Docker credential helpers. From dfe34a092225d6b29e8eecb0600b0fd0f643d2a3 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 2 Aug 2018 12:13:55 -0400 Subject: [PATCH 0095/2020] Move dependencies to end of classpath (#780) --- .../cloud/tools/jib/frontend/JavaEntrypointConstructor.java | 6 +++--- .../cloud/tools/jib/docker/DockerContextGeneratorTest.java | 2 +- .../tools/jib/frontend/JavaEntrypointConstructorTest.java | 6 +++--- jib-core/src/test/resources/sampleDockerfile | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index d18fcf09eb..dc4d677e30 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -23,16 +23,16 @@ /** Constructs an image entrypoint for the Java application. */ public class JavaEntrypointConstructor { - public static final String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; public static final String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; public static final String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; + public static final String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; public static List makeDefaultEntrypoint(List jvmFlags, String mainClass) { return makeEntrypoint( Arrays.asList( - DEFAULT_DEPENDENCIES_PATH_ON_IMAGE + "*", DEFAULT_RESOURCES_PATH_ON_IMAGE, - DEFAULT_CLASSES_PATH_ON_IMAGE), + DEFAULT_CLASSES_PATH_ON_IMAGE, + DEFAULT_DEPENDENCIES_PATH_ON_IMAGE + "*"), jvmFlags, mainClass); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java index 15ba372345..78e822c5d4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java @@ -42,9 +42,9 @@ @RunWith(MockitoJUnitRunner.class) public class DockerContextGeneratorTest { - private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; + private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; private static void assertSameFiles(Path directory1, Path directory2) throws IOException { Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index 3159645b9b..5ea9a8d8bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -26,15 +26,15 @@ public class JavaEntrypointConstructorTest { @Test public void testMakeEntrypoint() { - String expectedDependenciesPath = "/app/libs/*"; String expectedResourcesPath = "/app/resources/"; String expectedClassesPath = "/app/classes/"; + String expectedDependenciesPath = "/app/libs/*"; List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag"); String expectedMainClass = "SomeMainClass"; List entrypoint = JavaEntrypointConstructor.makeEntrypoint( - Arrays.asList(expectedDependenciesPath, expectedResourcesPath, expectedClassesPath), + Arrays.asList(expectedResourcesPath, expectedClassesPath, expectedDependenciesPath), expectedJvmFlags, expectedMainClass); Assert.assertEquals( @@ -43,7 +43,7 @@ public void testMakeEntrypoint() { "-flag", "anotherFlag", "-cp", - "/app/libs/*:/app/resources/:/app/classes/", + "/app/resources/:/app/classes/:/app/libs/*", "SomeMainClass"), entrypoint); diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 5dde515f99..d15e47f901 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -8,5 +8,5 @@ COPY root / EXPOSE 1000/tcp EXPOSE 2000-2010/udp -ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/libs/*:/app/resources/:/app/classes/","SomeMainClass"] +ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources/:/app/classes/:/app/libs/*","SomeMainClass"] CMD ["arg1","arg2"] From 881014ad1feb8910939565925c7e57002beac79c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 2 Aug 2018 13:14:51 -0400 Subject: [PATCH 0096/2020] Update Jib plugin versions and add CHANGELOG entries (#775) --- CONTRIBUTING.md | 4 ++-- README.md | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 4 ++-- examples/multi-module/build.gradle | 2 +- examples/spring-boot-draft/build.gradle | 4 ++-- examples/spring-boot-draft/pom.xml | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 4 ++-- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 11 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e2bf03d66..0d64015151 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,12 +88,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.8-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.9-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.7' + // id 'com.google.cloud.tools.jib' version '0.9.8' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/README.md b/README.md index ad2ad7024b..9aac545592 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) [![Maven Central](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/com/google/cloud/tools/jib-maven-plugin/maven-metadata.xml.svg?colorB=007ec6)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.7-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.8-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-ubuntu-master-orb.svg) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-windows-master-orb.svg) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-macos-master-orb.svg) diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 1df17961a4..fd6f63a265 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.7' + id 'com.google.cloud.tools.jib' version '0.9.8' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 9f1c9319c4..62bae49723 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -6,7 +6,7 @@ 1 - 0.9.7 + 0.9.8 @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 060afd17af..d3198e82b5 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.7' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.8' } } diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index d075adc31d..d23605b239 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.7' + id 'com.google.cloud.tools.jib' version '0.9.8' } apply plugin: 'java' @@ -58,4 +58,4 @@ jib.to.image = 'registry.hub.docker.com/hellojib/hellojib:jib' // args = ['some args'] // ports = ['8080'] // } -//} \ No newline at end of file +//} diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 8df7de450f..f8d81cf022 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.9.7 + 0.9.8 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 2da3829a5b..57ddc7ca79 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.8 + +### Added + - Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) - Disable parallel operation by setting the `jibSerialize` system property to `true` ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 2a07f579a3..915a08f88a 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,5 +1,5 @@ ![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.7-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.8-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Gradle Java project @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.7' + id 'com.google.cloud.tools.jib' version '0.9.8' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 6a53f0a6fd..d5fb3eda30 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.8 + +### Added + - `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) - Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) - Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index cd43e5d1a5..1dccae432b 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.7:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.8:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.7:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.8:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.7 + 0.9.8 myimage From ee482377c1a1149209b777567f2fe0b8227cc902 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 2 Aug 2018 13:29:51 -0400 Subject: [PATCH 0097/2020] Add warning when auth configuration is incomplete (#773) --- ...Configuration.java => AuthParameters.java} | 2 +- .../tools/jib/gradle/BuildDockerTask.java | 43 +++++++- .../tools/jib/gradle/BuildImageTask.java | 47 ++++++++- .../cloud/tools/jib/gradle/BuildTarTask.java | 43 +++++++- .../tools/jib/gradle/ImageParameters.java | 22 +---- .../tools/jib/gradle/BuildImageTaskTest.java | 73 ++++++++++++++ .../tools/jib/maven/BuildDockerMojo.java | 83 +++++++++++++++- .../cloud/tools/jib/maven/BuildImageMojo.java | 91 ++++++++++++++++- .../cloud/tools/jib/maven/BuildTarMojo.java | 83 +++++++++++++++- .../jib/maven/JibPluginConfiguration.java | 52 +++------- .../tools/jib/maven/BuildImageMojoTest.java | 98 +++++++++++++++++++ .../jib/maven/JibPluginConfigurationTest.java | 29 ------ 12 files changed, 569 insertions(+), 97 deletions(-) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{AuthConfiguration.java => AuthParameters.java} (97%) create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java similarity index 97% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index 65d6f48fc5..27eca3eadd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -24,7 +24,7 @@ * A bean that configures authorization credentials to be used for a registry. This is configurable * with Groovy closures and can be validated when used as a task input. */ -public class AuthConfiguration { +public class AuthParameters { @Nullable private String username; @Nullable private String password; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index bb54c77241..2860140742 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -29,11 +30,13 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.time.Instant; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -93,7 +96,8 @@ public void buildDocker() throws InvalidImageReferenceException { + "this on a public network!"); } RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); + Authorization fromAuthorization = + getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); if (fromAuthorization != null) { knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); } @@ -164,4 +168,41 @@ BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } + + /** + * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. + * + *

TODO: Consolidate with other tasks. + * + * @param logger the {@link JibLogger} used to print warnings + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param auth the auth configuration to get the {@link Authorization} from + * @return the {@link Authorization}, or null if the username and password aren't both configured + */ + @Nullable + private static Authorization getImageAuthorization( + JibLogger logger, String imageProperty, AuthParameters auth) { + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "jib." + + imageProperty + + ".auth.username is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "jib." + + imageProperty + + ".auth.password is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index bca11ed13b..6aebe77389 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -28,10 +29,12 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.time.Instant; @@ -97,11 +100,13 @@ public void buildImage() throws InvalidImageReferenceException { } RegistryCredentials knownBaseRegistryCredentials = null; RegistryCredentials knownTargetRegistryCredentials = null; - Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); + Authorization fromAuthorization = + getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); if (fromAuthorization != null) { knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); } - Authorization toAuthorization = jibExtension.getTo().getImageAuthorization(); + Authorization toAuthorization = + getImageAuthorization(gradleJibLogger, "to", jibExtension.getFrom().getAuth()); if (toAuthorization != null) { knownTargetRegistryCredentials = new RegistryCredentials("jib.to.auth", toAuthorization); } @@ -174,4 +179,42 @@ BuildImageTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } + + /** + * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. + * + *

TODO: Consolidate with other tasks. + * + * @param logger the {@link JibLogger} used to print warnings + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param auth the auth configuration to get the {@link Authorization} from + * @return the {@link Authorization}, or null if the username and password aren't both configured + */ + @VisibleForTesting + @Nullable + static Authorization getImageAuthorization( + JibLogger logger, String imageProperty, AuthParameters auth) { + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "jib." + + imageProperty + + ".auth.username is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "jib." + + imageProperty + + ".auth.password is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 3257894a57..f02553a739 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -28,11 +29,13 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.nio.file.Paths; import java.time.Instant; import javax.annotation.Nullable; @@ -121,7 +124,8 @@ public void buildTar() throws InvalidImageReferenceException { + "this on a public network!"); } RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = jibExtension.getFrom().getImageAuthorization(); + Authorization fromAuthorization = + getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); if (fromAuthorization != null) { knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); } @@ -193,4 +197,41 @@ BuildTarTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } + + /** + * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. + * + *

TODO: Consolidate with other tasks. + * + * @param logger the {@link JibLogger} used to print warnings + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param auth the auth configuration to get the {@link Authorization} from + * @return the {@link Authorization}, or null if the username and password aren't both configured + */ + @Nullable + private static Authorization getImageAuthorization( + JibLogger logger, String imageProperty, AuthParameters auth) { + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "jib." + + imageProperty + + ".auth.username is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "jib." + + imageProperty + + ".auth.password is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java index 255df4b230..017c809668 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java @@ -16,14 +16,11 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import javax.annotation.Nullable; import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.model.ObjectFactory; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; @@ -36,14 +33,14 @@ */ public class ImageParameters { - private AuthConfiguration auth; + private AuthParameters auth; @Nullable private String image; @Nullable private String credHelper; @Inject public ImageParameters(ObjectFactory objectFactory) { - auth = objectFactory.newInstance(AuthConfiguration.class); + auth = objectFactory.newInstance(AuthParameters.class); } @Input @@ -70,22 +67,11 @@ public void setCredHelper(String credHelper) { @Nested @Optional - AuthConfiguration getAuth() { + AuthParameters getAuth() { return auth; } - public void auth(Action action) { + public void auth(Action action) { action.execute(auth); } - - /** Converts the {@link ImageParameters} to an {@link Authorization}. */ - @Internal - @Nullable - Authorization getImageAuthorization() { - if (auth.getUsername() == null || auth.getPassword() == null) { - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java new file mode 100644 index 0000000000..7b78d81cdf --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Test for {@link BuildImageTask}. + * + *

TODO: This only tests the {@link BuildImageTask#getImageAuthorization(JibLogger, String, + * AuthParameters)} method, which is copy-pasted between the 3 build tasks. When we refactor, we'll + * need to move this test. + */ +@RunWith(MockitoJUnitRunner.class) +public class BuildImageTaskTest { + + @Mock private JibLogger mockLogger; + + @Test + public void testGetImageAuthorization() { + + // Auth set + AuthParameters auth = new AuthParameters(); + auth.setUsername("vwxyz"); + auth.setPassword("98765"); + Authorization expected = Authorizations.withBasicCredentials("vwxyz", "98765"); + Authorization actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); + + // Auth completely missing + auth = new AuthParameters(); + actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + Assert.assertNull(actual); + + // Password missing + auth = new AuthParameters(); + auth.setUsername("vwxyz"); + actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + Assert.assertNull(actual); + Mockito.verify(mockLogger).warn("jib.to.auth.password is null; ignoring jib.to.auth section."); + + // Username missing + auth = new AuthParameters(); + auth.setPassword("98765"); + actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + Assert.assertNull(actual); + Mockito.verify(mockLogger).warn("jib.to.auth.username is null; ignoring jib.to.auth section."); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 1f0db91d93..fef8b60a58 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -29,12 +30,15 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.time.Instant; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -84,7 +88,13 @@ public void execute() throws MojoExecutionException { MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = getBaseImageAuth(); + Authorization fromAuthorization = + getImageAuth( + mavenJibLogger, + "from", + "jib.from.auth.username", + "jib.from.auth.password", + getBaseImageAuth()); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null ? new RegistryCredentials( @@ -147,4 +157,75 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } + + /** + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. + * + *

TODO: Consolidate with the other mojos. + * + * @param logger the {@link JibLogger} used to print warnings messages + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. + */ + @Nullable + private static Authorization getImageAuth( + JibLogger logger, + String imageProperty, + String usernameProperty, + String passwordProperty, + AuthConfiguration auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (!Strings.isNullOrEmpty(commandlineUsername) + && !Strings.isNullOrEmpty(commandlinePassword)) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + } + + // Warn if a system property is missing + if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + logger.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods."); + } + if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + logger.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods."); + } + + // Check auth configuration next; warn if they aren't both set + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index b93fe667cf..35c1e608b7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -28,6 +29,7 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -37,6 +39,7 @@ import com.google.common.base.Strings; import java.time.Instant; import java.util.Arrays; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -102,13 +105,25 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = getBaseImageAuth(); + Authorization fromAuthorization = + getImageAuth( + mavenJibLogger, + "from", + "jib.from.auth.username", + "jib.from.auth.password", + getBaseImageAuth()); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null ? new RegistryCredentials( "jib-maven-plugin configuration", fromAuthorization) : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); - Authorization toAuthorization = getTargetImageAuth(); + Authorization toAuthorization = + getImageAuth( + mavenJibLogger, + "to", + "jib.to.auth.username", + "jib.to.auth.password", + getTargetImageAuth()); RegistryCredentials knownTargetRegistryCredentials = toAuthorization != null ? new RegistryCredentials("jib-maven-plugin configuration", toAuthorization) @@ -177,4 +192,76 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } + + /** + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. + * + *

TODO: Consolidate with the other mojos. + * + * @param logger the {@link JibLogger} used to print warnings messages + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. + */ + @VisibleForTesting + @Nullable + static Authorization getImageAuth( + JibLogger logger, + String imageProperty, + String usernameProperty, + String passwordProperty, + AuthConfiguration auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (!Strings.isNullOrEmpty(commandlineUsername) + && !Strings.isNullOrEmpty(commandlinePassword)) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + } + + // Warn if a system property is missing + if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + logger.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods."); + } + if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + logger.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods."); + } + + // Check auth configuration next; warn if they aren't both set + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 543196ecfa..b46ad45702 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -28,13 +29,16 @@ import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.nio.file.Paths; import java.time.Instant; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -82,7 +86,13 @@ public void execute() throws MojoExecutionException { MavenSettingsServerCredentials mavenSettingsServerCredentials = new MavenSettingsServerCredentials( Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = getBaseImageAuth(); + Authorization fromAuthorization = + getImageAuth( + mavenJibLogger, + "from", + "jib.from.auth.username", + "jib.from.auth.password", + getBaseImageAuth()); RegistryCredentials knownBaseRegistryCredentials = fromAuthorization != null ? new RegistryCredentials( @@ -148,4 +158,75 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } + + /** + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. + * + *

TODO: Consolidate with the other mojos. + * + * @param logger the {@link JibLogger} used to print warnings messages + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. + */ + @Nullable + private static Authorization getImageAuth( + JibLogger logger, + String imageProperty, + String usernameProperty, + String passwordProperty, + AuthConfiguration auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (!Strings.isNullOrEmpty(commandlineUsername) + && !Strings.isNullOrEmpty(commandlinePassword)) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + } + + // Warn if a system property is missing + if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + logger.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods."); + } + if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + logger.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods."); + } + + // Check auth configuration next; warn if they aren't both set + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 9d5a644393..04a627563e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; @@ -58,17 +56,14 @@ void setPassword(String password) { this.password = password; } - /** - * Converts the {@link AuthConfiguration} to an {@link Authorization}. - * - * @return the {@link Authorization} - */ @Nullable - private Authorization getAuthorization() { - if (username == null || password == null) { - return null; - } - return Authorizations.withBasicCredentials(username, password); + String getUsername() { + return username; + } + + @Nullable + String getPassword() { + return password; } } @@ -132,29 +127,6 @@ static ImageReference parseImageReference(String image, String type) { } } - /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then - * tries build configuration, otherwise returns null. - * - * @param usernameProperty the name of the username system property - * @param passwordProperty the name of the password system property - * @param auth the configured credentials - * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. - */ - @VisibleForTesting - @Nullable - static Authorization getImageAuth( - String usernameProperty, String passwordProperty, AuthConfiguration auth) { - // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); - if (commandlineUsername != null && commandlinePassword != null) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); - } - return auth.getAuthorization(); - } - @Nullable @Parameter(defaultValue = "${session}", readonly = true) MavenSession session; @@ -243,9 +215,8 @@ String getBaseImageCredentialHelperName() { return Preconditions.checkNotNull(from).credHelper; } - @Nullable - Authorization getBaseImageAuth() { - return getImageAuth("jib.from.auth.username", "jib.from.auth.password", from.auth); + AuthConfiguration getBaseImageAuth() { + return from.auth; } @Nullable @@ -258,9 +229,8 @@ String getTargetImageCredentialHelperName() { return Preconditions.checkNotNull(to).credHelper; } - @Nullable - Authorization getTargetImageAuth() { - return getImageAuth("jib.to.auth.username", "jib.to.auth.password", to.auth); + AuthConfiguration getTargetImageAuth() { + return to.auth; } boolean getUseCurrentTimestamp() { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java new file mode 100644 index 0000000000..ecd7e323b4 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Test for {@link BuildImageMojo}. + * + *

TODO: This only tests the {@link BuildImageMojo#getImageAuth(JibLogger, String, String, + * String, AuthConfiguration)} method, which is copy-pasted between the 3 build mojos. When we + * refactor, we'll need to move this test. + */ +@RunWith(MockitoJUnitRunner.class) +public class BuildImageMojoTest { + + @Mock private JibLogger mockLogger; + + @Test + public void testGetImageAuth() { + AuthConfiguration auth = new AuthConfiguration(); + auth.setUsername("vwxyz"); + auth.setPassword("98765"); + + // System properties set + System.setProperty("jib.test.auth.user", "abcde"); + System.setProperty("jib.test.auth.pass", "12345"); + Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); + Authorization actual = + BuildImageMojo.getImageAuth( + mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + + // Auth set in configuration + System.clearProperty("jib.test.auth.user"); + System.clearProperty("jib.test.auth.pass"); + expected = Authorizations.withBasicCredentials("vwxyz", "98765"); + actual = + BuildImageMojo.getImageAuth( + mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); + + // Auth completely missing + auth = new AuthConfiguration(); + actual = + BuildImageMojo.getImageAuth( + mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNull(actual); + + // Password missing + auth = new AuthConfiguration(); + auth.setUsername("vwxyz"); + actual = + BuildImageMojo.getImageAuth( + mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNull(actual); + Mockito.verify(mockLogger) + .warn( + " is missing from maven configuration; ignoring section."); + + // Username missing + auth = new AuthConfiguration(); + auth.setPassword("98765"); + actual = + BuildImageMojo.getImageAuth( + mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); + Assert.assertNull(actual); + Mockito.verify(mockLogger) + .warn( + " is missing from maven configuration; ignoring section."); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 8cb03efea1..71de6176d8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -17,9 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; import java.nio.file.Paths; import java.util.Arrays; import org.junit.Assert; @@ -69,30 +66,4 @@ public void execute() {} Assert.assertEquals("OCI", testPluginConfiguration.getFormat()); Assert.assertEquals(Paths.get("some/path"), testPluginConfiguration.getExtraDirectory()); } - - @Test - public void testGetImageAuth() { - AuthConfiguration auth = new AuthConfiguration(); - auth.setUsername("vwxyz"); - auth.setPassword("98765"); - - System.setProperty("jib.test.auth.user", "abcde"); - System.setProperty("jib.test.auth.pass", "12345"); - Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); - Authorization actual = - JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); - - System.clearProperty("jib.test.auth.user"); - System.clearProperty("jib.test.auth.pass"); - expected = Authorizations.withBasicCredentials("vwxyz", "98765"); - actual = JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); - - auth = new AuthConfiguration(); - actual = JibPluginConfiguration.getImageAuth("jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNull(actual); - } } From f9d073693ec43c0f6aa5b67e54704e6b1bc6562e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 2 Aug 2018 13:39:20 -0400 Subject: [PATCH 0098/2020] Update changelog for classpath changes (#782) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 57ddc7ca79..1638e4f333 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) + ### Fixed ## 0.9.8 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index d5fb3eda30..ca9147b957 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) + ### Fixed ## 0.9.8 From 61c4602085472606e80c22835dfcd873c88f3325 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 2 Aug 2018 14:08:46 -0400 Subject: [PATCH 0099/2020] Update allowInsecureRegistries documentation to match 0.9.8 (#779) * Update allowInsecureRegistries to match 0.9.8 * Add alternative option for self-signed registries --- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 915a08f88a..21f479d9da 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -162,7 +162,7 @@ Field | Type | Default | Description `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. `useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. -`allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS or whose certificates cannot be verified. Leaving this parameter set to `false` is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `from` is a closure with the following properties: diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 1dccae432b..2833170932 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -203,7 +203,7 @@ Field | Type | Default | Description `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. `useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Gradle projects. -`allowInsecureRegistries` | boolean | `false` | If set to true, Jib uses HTTP as a fallback for registries that do not support HTTPS or whose certificates cannot be verified. Leaving this parameter set to `false` is strongly recommended, since communication with insecure registries is unencrypted and visible to others on the network. +`allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `from` is an object with the following properties: From be01e2adef73289f593222f88283c6b12b777cb2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 2 Aug 2018 14:28:52 -0400 Subject: [PATCH 0100/2020] Fall back to HTTP on right condition when HttpHostConnectException (#771) * Fall back conditionally if HttpHostConnectException * Log when trying insecure HTTPS --- .../jib/registry/RegistryEndpointCaller.java | 28 +++- .../registry/RegistryEndpointCallerTest.java | 125 ++++++++++++++---- 2 files changed, 119 insertions(+), 34 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index da1c719da1..7a35a64ce2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -151,6 +151,14 @@ private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, Reg } catch (SSLPeerUnverifiedException ex) { return handleUnverifiableServerException(url); + + } catch (HttpHostConnectException ex) { + if (allowInsecureRegistries && isHttpsProtocol(url) && url.getPort() == -1) { + // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS + // port 443) and we could not connect to 443. It's worth trying port 80. + return fallBackToHttp(url); + } + throw ex; } } @@ -161,18 +169,24 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } try { + logger.warn( + "Cannot verify server at " + url + ". Attempting again with no TLS verification."); return call(url, getInsecureConnectionFactory()); - } catch (SSLPeerUnverifiedException | HttpHostConnectException ex) { - // Try HTTP as a last resort. - GenericUrl httpUrl = new GenericUrl(url); - httpUrl.setScheme("http"); - logger.warn( - "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl); - return call(httpUrl.toURL(), connectionFactory); + } catch (SSLPeerUnverifiedException ex) { + return fallBackToHttp(url); } } + @Nullable + private T fallBackToHttp(URL url) throws IOException, RegistryException { + GenericUrl httpUrl = new GenericUrl(url); + httpUrl.setScheme("http"); + logger.warn( + "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl); + return call(httpUrl.toURL(), connectionFactory); + } + private Function getInsecureConnectionFactory() throws RegistryException { try { if (insecureConnectionFactory == null) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 57665d62fb..a6a4685c2a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -113,7 +113,7 @@ private static HttpResponse mockRedirectHttpResponse(String redirectLocation) th @Before public void setUp() throws IOException { - secureEndpointCaller = createRegistryEndpointCaller(false); + secureEndpointCaller = createRegistryEndpointCaller(false, -1); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); Mockito.when(mockInsecureConnectionFactory.apply(Mockito.any())) @@ -149,15 +149,22 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenReturn(mockResponse); // OK with non-verifying connection - RegistryEndpointCaller insecureCaller = createRegistryEndpointCaller(true); + RegistryEndpointCaller insecureCaller = createRegistryEndpointCaller(true, -1); Assert.assertEquals("body", insecureCaller.call()); ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(1)).apply(urlCaptor.capture()); + Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); - Mockito.verify(mockInsecureConnectionFactory, Mockito.times(1)).apply(urlCaptor.capture()); + Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); + + Mockito.verify(mockBuildLogger) + .warn( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); } @Test @@ -168,7 +175,7 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true); + RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); Assert.assertEquals("body", insecureEndpointCaller.call()); ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); @@ -176,27 +183,28 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); - Mockito.verify(mockInsecureConnectionFactory, Mockito.times(1)).apply(urlCaptor.capture()); + Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(2)); + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); + + Mockito.verify(mockBuildLogger) + .warn( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); Mockito.verify(mockBuildLogger) .warn( - "Failed to connect to " - + urlCaptor.getAllValues().get(0) - + " over HTTPS. Attempting again with HTTP: " - + urlCaptor.getAllValues().get(1)); + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); } @Test - public void testCall_insecureCallerOnHttpServerByHttpHostConnectException() + public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() throws IOException, RegistryException { Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)) // server is not HTTPS - .thenReturn(mockResponse); - Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(HttpHostConnectException.class)); // server is not HTTPS + .thenThrow(Mockito.mock(HttpHostConnectException.class)) // server is not listening on 443 + .thenReturn(mockResponse); // respond when connected through 80 - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true); + RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); Assert.assertEquals("body", insecureEndpointCaller.call()); ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); @@ -204,15 +212,56 @@ public void testCall_insecureCallerOnHttpServerByHttpHostConnectException() Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); - Mockito.verify(mockInsecureConnectionFactory, Mockito.times(1)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(2)); + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); Mockito.verify(mockBuildLogger) .warn( - "Failed to connect to " - + urlCaptor.getAllValues().get(0) - + " over HTTPS. Attempting again with HTTP: " - + urlCaptor.getAllValues().get(1)); + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); + } + + @Test + public void testCall_secureCallerOnNonListeningServerAndNoPortSpecified() + throws IOException, RegistryException { + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(Mockito.mock(HttpHostConnectException.class)); // server is not listening on 443 + + try { + secureEndpointCaller.call(); + Assert.fail("Should not fall back to HTTP if not allowInsecureRegistries"); + } catch (HttpHostConnectException ex) { + Assert.assertNull(ex.getMessage()); + } + + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); + Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); + + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); + } + + @Test + public void testCall_insecureCallerOnNonListeningServerAndPortSpecified() + throws IOException, RegistryException { + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(Mockito.mock(HttpHostConnectException.class)); // server is not listening on 5000 + + RegistryEndpointCaller insecureEndpointCaller = + createRegistryEndpointCaller(true, 5000); + try { + insecureEndpointCaller.call(); + Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); + } catch (HttpHostConnectException ex) { + Assert.assertNull(ex.getMessage()); + } + + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); + Assert.assertEquals(new URL("https://apiRouteBase:5000/api"), urlCaptor.getAllValues().get(0)); + + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); } @Test @@ -249,7 +298,7 @@ public void testCall_credentialsNotSentOverHttp() throws IOException, RegistryEx Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true); + RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); try { insecureEndpointCaller.call(); Assert.fail("Call should have failed"); @@ -273,8 +322,27 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS System.setProperty("sendCredentialsOverHttp", "true"); - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true); + RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); Assert.assertEquals("body", insecureEndpointCaller.call()); + + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + Mockito.verify(mockConnectionFactory, Mockito.times(3)).apply(urlCaptor.capture()); + Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new URL("http://newlocation"), urlCaptor.getAllValues().get(2)); + + Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); + Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(3)); + + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); + + Mockito.verify(mockBuildLogger) + .warn( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); + Mockito.verify(mockBuildLogger) + .warn( + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); } @Test @@ -482,14 +550,17 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) Assert.assertEquals( new URL("https://apiRouteBase/api"), urlArgumentCaptor.getAllValues().get(0)); Assert.assertEquals(new URL("https://newlocation"), urlArgumentCaptor.getAllValues().get(1)); + + Mockito.verifyNoMoreInteractions(mockConnectionFactory); + Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); } - private RegistryEndpointCaller createRegistryEndpointCaller(boolean allowInsecure) - throws MalformedURLException { + private RegistryEndpointCaller createRegistryEndpointCaller( + boolean allowInsecure, int port) throws MalformedURLException { return new RegistryEndpointCaller<>( mockBuildLogger, "userAgent", - "apiRouteBase", + (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), Authorizations.withBasicToken("token"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), From 74d0ecfd87c26ba168e4d52f596cafa8c64ec0c8 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 2 Aug 2018 14:42:17 -0400 Subject: [PATCH 0101/2020] Configures integration testing project with env var and uses different images for each test. (#781) --- CONTRIBUTING.md | 2 +- .../jib/IntegrationTestingConfiguration.java | 35 ++++++ .../jib/gradle/JibPluginIntegrationTest.java | 118 +++++++++--------- .../resources/projects/empty/build.gradle | 2 +- .../resources/projects/simple/build.gradle | 2 +- .../maven/BuildDockerMojoIntegrationTest.java | 31 ++--- .../maven/BuildImageMojoIntegrationTest.java | 46 +++---- .../maven/BuildTarMojoIntegrationTest.java | 15 +-- .../DockerContextMojoIntegrationTest.java | 2 +- .../src/test/resources/projects/empty/pom.xml | 2 +- .../test/resources/projects/simple/pom.xml | 2 +- 11 files changed, 132 insertions(+), 125 deletions(-) create mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d64015151..8e5e0fb75a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ To build, use the provided `build.sh` which builds and tests each of the compone 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! -**Note** that you will not be able to run the integration tests for `jib-gradle-plugin` or `jib-maven-plugin` because those push to our integration-testing GCP project. If you would like to run integration tests, change all uses of the `jib-integration-testing` project to your own GCP project and run `./gradlew integrationTest` for `jib-gradle-plugin` and `./mvnw verify -Pintegration-tests` for `jib-maven-plugin`. +**Note** that in order to run integration tests, you will need to set the environment variable `JIB_INTEGRATION_TESTING_PROJECT` to the GCP project you would like to use for testing. You will also need Docker installed with the daemon running. Otherwise, feel free to skip integration tests. # Development Tips diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java new file mode 100644 index 0000000000..64b92854ff --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib; + +import com.google.common.base.Strings; +import org.junit.Assert; + +/** Configuration for integration tests. */ +public class IntegrationTestingConfiguration { + + public static String getGCPProject() { + String projectId = System.getenv("JIB_INTEGRATION_TESTING_PROJECT"); + if (Strings.isNullOrEmpty(projectId)) { + Assert.fail( + "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the GCP project to use for integration testing."); + } + return projectId; + } + + private IntegrationTestingConfiguration() {} +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 75380d0085..60531bd6db 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; import java.io.IOException; import java.nio.file.Files; import java.time.Instant; @@ -41,7 +42,9 @@ public class JibPluginIntegrationTest { private static String buildAndRun(TestProject testProject, String imageReference) throws IOException, InterruptedException { - BuildResult buildResult = testProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME); + BuildResult buildResult = + testProject.build( + "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); BuildTask classesTask = buildResult.task(":classes"); BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); @@ -69,7 +72,9 @@ private static String buildAndRun(TestProject testProject, String imageReference private static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) throws IOException, InterruptedException { - BuildResult buildResult = testProject.build("clean", JibPlugin.BUILD_DOCKER_TASK_NAME); + BuildResult buildResult = + testProject.build( + "clean", JibPlugin.BUILD_DOCKER_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); BuildTask classesTask = buildResult.task(":classes"); BuildTask jibBuildDockerTask = buildResult.task(":" + JibPlugin.BUILD_DOCKER_TASK_NAME); @@ -94,28 +99,51 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String return new Command("docker", "run", imageReference).run(); } + /** + * Asserts that the creation time of the simple test project is set. If the time parsed from the + * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the + * assertion will fail. + * + * @param before the specified time to compare the resulting image's creation time to + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertSimpleCreationTimeIsAfter(Instant before, String imageReference) + throws IOException, InterruptedException { + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); + Instant parsed = Instant.parse(inspect); + Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); + } + @Test public void testBuild_empty() throws IOException, InterruptedException { - Assert.assertEquals( - "", buildAndRun(emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/emptyimage:gradle" + + System.nanoTime(); + Assert.assertEquals("", buildAndRun(emptyTestProject, targetImage)); Assert.assertEquals( "1970-01-01T00:00:00Z", - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/emptyimage:gradle") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test public void testBuild_simple() throws IOException, InterruptedException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/simpleimage:gradle" + + System.nanoTime(); + // Test empty output error try { - simpleTestProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes"); + simpleTestProject.build( + "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes", "-D_TARGET_IMAGE=" + targetImage); Assert.fail(); + } catch (UnexpectedBuildFailure ex) { Assert.assertThat( ex.getMessage(), @@ -125,9 +153,8 @@ public void testBuild_simple() throws IOException, InterruptedException { Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - buildAndRun(simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); - assertSimpleCreationTimeIsAfter(beforeBuild); + "Hello, world. An argument.\nfoo\ncat\n", buildAndRun(simpleTestProject, targetImage)); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @Test @@ -148,30 +175,21 @@ public void testBuild_defaultTarget() { @Test public void testDockerDaemon_empty() throws IOException, InterruptedException { - Assert.assertEquals( - "", - buildToDockerDaemonAndRun( - emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); + String targetImage = "emptyimage:gradle" + System.nanoTime(); + Assert.assertEquals("", buildToDockerDaemonAndRun(emptyTestProject, targetImage)); Assert.assertEquals( "1970-01-01T00:00:00Z", - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/emptyimage:gradle") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test public void testDockerDaemon_simple() throws IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", - buildToDockerDaemonAndRun( - simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); - assertSimpleCreationTimeIsAfter(beforeBuild); + buildToDockerDaemonAndRun(simpleTestProject, targetImage)); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @Test @@ -184,11 +202,14 @@ public void testDockerDaemon_defaultTarget() throws IOException, InterruptedExce @Test public void testBuildTar_simple() throws IOException, InterruptedException { - String imageReference = "gcr.io/jib-integration-testing/simpleimage:gradle"; + String targetImage = "simpleimage:gradle" + System.nanoTime(); + String outputPath = simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); Instant beforeBuild = Instant.now(); - BuildResult buildResult = simpleTestProject.build("clean", JibPlugin.BUILD_TAR_TASK_NAME); + BuildResult buildResult = + simpleTestProject.build( + "clean", JibPlugin.BUILD_TAR_TASK_NAME, "-D_TARGET_IMAGE=" + targetImage); BuildTask classesTask = buildResult.task(":classes"); BuildTask jibBuildTarTask = buildResult.task(":" + JibPlugin.BUILD_TAR_TASK_NAME); @@ -203,9 +224,8 @@ public void testBuildTar_simple() throws IOException, InterruptedException { new Command("docker", "load", "--input", outputPath).run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - new Command("docker", "run", imageReference).run()); - assertSimpleCreationTimeIsAfter(beforeBuild); + "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @Test @@ -223,7 +243,7 @@ public void testDockerContext() throws IOException, InterruptedException { Assert.assertThat( buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); - String imageName = "jib-gradle-plugin/integration-test"; + String imageName = "jib-gradle-plugin/integration-test" + System.nanoTime(); new Command( "docker", "build", @@ -281,28 +301,4 @@ public void testDockerContext() throws IOException, InterruptedException { "Export Docker context failed because cannot clear directory")); } } - - /** - * Asserts that the creation time of the simple test project is set. If the time parsed from the - * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the - * assertion will fail. - * - * @param before the specified time to compare the resulting image's creation time to - * @throws IOException if the {@code docker inspect} command fails to run - * @throws InterruptedException if the {@code docker inspect} command is interrupted - */ - private static void assertSimpleCreationTimeIsAfter(Instant before) - throws IOException, InterruptedException { - String inspect = - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/simpleimage:gradle") - .run() - .trim(); - Instant parsed = Instant.parse(inspect); - Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); - } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index 966d53be7e..0fb84889b4 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -14,7 +14,7 @@ repositories { jib { to { - image = 'gcr.io/jib-integration-testing/emptyimage:gradle' + image = System.getProperty("_TARGET_IMAGE") credHelper = 'gcr' } container { diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index b72d4cf2f7..7301397b6f 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -16,7 +16,7 @@ dependencies { jib { to { - image = 'gcr.io/jib-integration-testing/simpleimage:gradle' + image = System.getProperty("_TARGET_IMAGE") credHelper = 'gcr' } container { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 97f211d86a..906e29e653 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -49,6 +49,7 @@ public class BuildDockerMojoIntegrationTest { private static String buildToDockerDaemonAndRun(Path projectRoot, String imageReference) throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); verifier.executeGoal("package"); @@ -69,41 +70,27 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + Instant before = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", - buildToDockerDaemonAndRun( - simpleTestProject.getProjectRoot(), - "gcr.io/jib-integration-testing/simpleimage:maven")); + buildToDockerDaemonAndRun(simpleTestProject.getProjectRoot(), targetImage)); Instant buildTime = Instant.parse( - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/simpleimage:maven") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException { + String targetImage = "emptyimage:maven" + System.nanoTime(); + Assert.assertEquals( - "", - buildToDockerDaemonAndRun( - emptyTestProject.getProjectRoot(), "gcr.io/jib-integration-testing/emptyimage:maven")); + "", buildToDockerDaemonAndRun(emptyTestProject.getProjectRoot(), targetImage)); Assert.assertEquals( "1970-01-01T00:00:00Z", - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/emptyimage:maven") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 8c4f08061a..a47e9cd74d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -55,6 +56,7 @@ public class BuildImageMojoIntegrationTest { private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); verifier.addCliOption("-X"); verifier.executeGoals(Arrays.asList("clean", "compile")); @@ -110,12 +112,20 @@ private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOExc @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/simpleimage:maven" + + System.nanoTime(); + // Test empty output error try { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); verifier.executeGoals(Arrays.asList("clean", "jib:" + BuildImageMojo.GOAL_NAME)); Assert.fail(); + } catch (VerificationException ex) { Assert.assertThat( ex.getMessage(), @@ -142,42 +152,26 @@ public void testExecute_simple() throws VerificationException, IOException, Inte Assert.assertEquals( "Hello, " + before + ". An argument.\nfoo\ncat\n", - buildAndRun( - simpleTestProject.getProjectRoot(), - "gcr.io/jib-integration-testing/simpleimage:maven", - true)); + buildAndRun(simpleTestProject.getProjectRoot(), targetImage, true)); Instant buildTime = Instant.parse( - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/simpleimage:maven") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException { - Assert.assertEquals( - "", - buildAndRun( - emptyTestProject.getProjectRoot(), - "gcr.io/jib-integration-testing/emptyimage:maven", - false)); + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/emptyimage:maven" + + System.nanoTime(); + + Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); Assert.assertEquals( "1970-01-01T00:00:00Z", - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/emptyimage:maven") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index e79af53601..c979fd6195 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -38,8 +38,11 @@ public class BuildTarMojoIntegrationTest { */ @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); verifier.executeGoal("package"); @@ -57,19 +60,11 @@ public void testExecute_simple() throws VerificationException, IOException, Inte .toString()) .run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - new Command("docker", "run", "gcr.io/jib-integration-testing/simpleimage:maven").run()); + "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); Instant buildTime = Instant.parse( - new Command( - "docker", - "inspect", - "-f", - "{{.Created}}", - "gcr.io/jib-integration-testing/simpleimage:maven") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 66b6a7b190..99f275ae54 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -49,7 +49,7 @@ public void testExecute() throws VerificationException, IOException, Interrupted simpleTestProject.getProjectRoot().resolve("target").resolve("jib-docker-context"); Assert.assertTrue(Files.exists(dockerContextDirectory)); - String imageName = "jib/integration-test"; + String imageName = "jib/integration-test" + System.nanoTime(); new Command("docker", "build", "-t", imageName, dockerContextDirectory.toString()).run(); Assert.assertThat( new Command("docker", "inspect", imageName).run(), diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index f5446efe97..6a7b746b27 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -29,7 +29,7 @@ ${jib-maven-plugin.version} - gcr.io/jib-integration-testing/emptyimage:maven + ${_TARGET_IMAGE} diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index 012d72ecf6..eb5331900a 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -39,7 +39,7 @@ ${jib-maven-plugin.version} - gcr.io/jib-integration-testing/simpleimage:maven + ${_TARGET_IMAGE} true From d7cb3b15a99f163a7618b7ad7424da708258d6f7 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 2 Aug 2018 15:09:47 -0400 Subject: [PATCH 0102/2020] Sets the JIB_INTEGRATION_TESTING_PROJECT for Kokoro. (#784) --- kokoro/continuous.bat | 3 +++ kokoro/continuous.sh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/kokoro/continuous.bat b/kokoro/continuous.bat index 35bb79d0ae..64f7499bf1 100755 --- a/kokoro/continuous.bat +++ b/kokoro/continuous.bat @@ -9,6 +9,9 @@ cd github/jib REM Stops any left-over containers. REM FOR /f "tokens=*" %%i IN ('docker ps -aq') DO docker rm -vf %%i +REM Sets the integration testing project. +set JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing + REM TODO: Enable integration tests once docker works (b/73345382). cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -X && ^ diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 8e609f0570..33d2fd5b76 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -19,6 +19,9 @@ docker-credential-gcr configure-docker docker stop $(docker ps --all --quiet) || true docker kill $(docker ps --all --quiet) || true +# Sets the integration testing project. +export JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing + (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd github/jib/jib-maven-plugin; ./mvnw clean install -P integration-tests -B -U -X) (cd github/jib/jib-gradle-plugin; ./gradlew clean build integrationTest --info --stacktrace) From 35c2e5d76864e9b6907ceb11ce93f2dc9557e6c0 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 2 Aug 2018 15:33:29 -0400 Subject: [PATCH 0103/2020] Moves plugin-specific common code to .plugins.common package. (#785) --- .../cloud/tools/jib/frontend/ExposedPortsParser.java | 2 +- .../common}/BuildStepsExecutionException.java | 2 +- .../{frontend => plugins/common}/BuildStepsRunner.java | 2 +- .../{frontend => plugins/common}/HelpfulSuggestions.java | 2 +- .../common}/MainClassInferenceException.java | 2 +- .../{frontend => plugins/common}/MainClassResolver.java | 3 ++- .../{frontend => plugins/common}/ProjectProperties.java | 2 +- .../common}/SystemPropertyValidator.java | 2 +- .../common}/BuildStepsRunnerTest.java | 2 +- .../common}/HelpfulSuggestionsTest.java | 2 +- .../common}/MainClassResolverTest.java | 2 +- .../common}/SystemPropertyValidatorTest.java | 2 +- .../google/cloud/tools/jib/gradle/BuildDockerTask.java | 8 ++++---- .../com/google/cloud/tools/jib/gradle/BuildImageTask.java | 8 ++++---- .../com/google/cloud/tools/jib/gradle/BuildTarTask.java | 8 ++++---- .../google/cloud/tools/jib/gradle/DockerContextTask.java | 2 +- .../cloud/tools/jib/gradle/GradleProjectProperties.java | 8 ++++---- .../tools/jib/gradle/HelpfulSuggestionsProvider.java | 2 +- .../com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 8 ++++---- .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 8 ++++---- .../com/google/cloud/tools/jib/maven/BuildTarMojo.java | 8 ++++---- .../google/cloud/tools/jib/maven/DockerContextMojo.java | 2 +- .../cloud/tools/jib/maven/HelpfulSuggestionsProvider.java | 2 +- .../cloud/tools/jib/maven/MavenProjectProperties.java | 8 ++++---- 24 files changed, 49 insertions(+), 48 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/BuildStepsExecutionException.java (94%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/BuildStepsRunner.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/HelpfulSuggestions.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/MainClassInferenceException.java (94%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/MainClassResolver.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/ProjectProperties.java (97%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => plugins/common}/SystemPropertyValidator.java (96%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend => plugins/common}/BuildStepsRunnerTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend => plugins/common}/HelpfulSuggestionsTest.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend => plugins/common}/MainClassResolverTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend => plugins/common}/SystemPropertyValidatorTest.java (97%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java index e2eb8c0613..be451081fc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java @@ -24,7 +24,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Utility for parsing exposed ports from plugin configuration */ +/** Utility for parsing exposed ports from text representations. */ public class ExposedPortsParser { /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsExecutionException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java similarity index 94% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsExecutionException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java index 8bdfa2e332..b74bf9aa58 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsExecutionException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.builder.BuildSteps; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 0b0845db78..81e649353a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildStepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index f3785fa2da..6988db9269 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import java.nio.file.Path; import java.util.function.Function; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java similarity index 94% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java index 75bff54c26..d54de94966 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassInferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; /** Thrown when main class inference fails. */ public class MainClassInferenceException extends Exception { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index eaa01c73df..3e6c66466d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassResolver.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java similarity index 97% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 097d4aa89d..4df88a242d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ProjectProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java similarity index 96% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java index e83cb628cd..0f9bd0f7d4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import java.util.function.Function; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 3c6b219771..e574a4eb5e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index e803cd447d..3d76e69c97 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import java.nio.file.Paths; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 6f5795a465..242593a07a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassResolverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.LayerEntry; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java similarity index 97% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java index ef7feb0c30..3e98bcdda0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/SystemPropertyValidatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.plugins.common; import org.junit.After; import org.junit.Assert; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2860140742..5ab53357da 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -23,16 +23,16 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 6aebe77389..84d842101f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -22,16 +22,16 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index f02553a739..2024aeeaff 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -22,16 +22,16 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index a86d8816ff..8e1665f72b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 02c94bbe4a..32e32be4d5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -18,13 +18,13 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.frontend.MainClassInferenceException; -import com.google.cloud.tools.jib.frontend.MainClassResolver; -import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.MainClassResolver; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java index 9bd8d5893a..57fedd4e51 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; /** Provider for Maven-specific {@link HelpfulSuggestions}. */ class HelpfulSuggestionsProvider { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index fef8b60a58..6255953bcb 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -23,15 +23,15 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 35c1e608b7..4e6574d505 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -22,16 +22,16 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index b46ad45702..ee550c3df7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -22,15 +22,15 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.BuildStepsExecutionException; -import com.google.cloud.tools.jib.frontend.BuildStepsRunner; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index d202171d34..e08574fee5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java index 9831f30731..a0f9124314 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import java.util.function.Function; /** Provider for Maven-specific {@link HelpfulSuggestions}. */ diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 777cffed60..2651ea406d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -18,12 +18,12 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.frontend.HelpfulSuggestions; -import com.google.cloud.tools.jib.frontend.MainClassInferenceException; -import com.google.cloud.tools.jib.frontend.MainClassResolver; -import com.google.cloud.tools.jib.frontend.ProjectProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.MainClassResolver; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; From 806d790b9ba4b831c5716a63b9db6f64e8c9201c Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 2 Aug 2018 19:56:37 -0400 Subject: [PATCH 0104/2020] Use maven-metadata shields.io badge for gradle (#787) * Use maven-metadata shields.io badge for gradle --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aac545592..17e6c45007 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) [![Maven Central](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/com/google/cloud/tools/jib-maven-plugin/maven-metadata.xml.svg?colorB=007ec6)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.8-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-ubuntu-master-orb.svg) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-windows-master-orb.svg) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-macos-master-orb.svg) From 8fbdb2e1b1018565388c3036540125b852ddc142 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 2 Aug 2018 20:15:22 -0400 Subject: [PATCH 0105/2020] Use dynamic gradle plugin version in sub-project (#788) --- jib-gradle-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 21f479d9da..c9693d5988 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,5 +1,5 @@ ![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) -[![Gradle Plugin Portal](https://img.shields.io/badge/gradle%20plugin-v0.9.8-blue.svg)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) +[![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) # Jib - Containerize your Gradle Java project From 8577e8d2441178cc4984a34cdefe1a4002b9134a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 3 Aug 2018 13:22:13 -0400 Subject: [PATCH 0106/2020] Reduce duplicate code in gradle plugin (#793) --- .../tools/jib/gradle/BuildDockerTask.java | 125 ++----------- .../tools/jib/gradle/BuildImageTask.java | 129 ++----------- .../cloud/tools/jib/gradle/BuildTarTask.java | 125 ++----------- .../gradle/PluginConfigurationProcessor.java | 172 ++++++++++++++++++ ... => PluginConfigurationProcessorTest.java} | 19 +- 5 files changed, 221 insertions(+), 349 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java rename jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/{BuildImageTaskTest.java => PluginConfigurationProcessorTest.java} (77%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 5ab53357da..2ecd16454d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,28 +16,16 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import java.time.Instant; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -48,9 +36,6 @@ /** Builds a container image and exports to the default Docker daemon. */ public class BuildDockerTask extends DefaultTask { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Build to Docker daemon failed"); @@ -87,74 +72,27 @@ public void buildDocker() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); - - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = - getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); - } - GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); + ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); - // Builds the BuildConfiguration. - // TODO: Consolidate with BuildImageTask. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) - .setCredentialHelper(jibExtension.getFrom().getCredHelper()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + gradleJibLogger, jibExtension, gradleProjectProperties); + + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); - - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)) - .setProgramArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); - if (jibExtension.getUseCurrentTimestamp()) { - gradleJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - GradleJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); - // Uses a directory in the Gradle build cache as the Jib cache. try { BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); @@ -168,41 +106,4 @@ BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. - * - *

TODO: Consolidate with other tasks. - * - * @param logger the {@link JibLogger} used to print warnings - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param auth the auth configuration to get the {@link Authorization} from - * @return the {@link Authorization}, or null if the username and password aren't both configured - */ - @Nullable - private static Authorization getImageAuthorization( - JibLogger logger, String imageProperty, AuthParameters auth) { - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "jib." - + imageProperty - + ".auth.username is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "jib." - + imageProperty - + ".auth.password is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 84d842101f..75af11265b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,28 +16,18 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.time.Instant; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -48,9 +38,6 @@ /** Builds a container image. */ public class BuildImageTask extends DefaultTask { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Build image failed"); @@ -83,8 +70,9 @@ public void buildImage() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + GradleProjectProperties gradleProjectProperties = + GradleProjectProperties.getForProject( + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { throw new GradleException( @@ -92,80 +80,33 @@ public void buildImage() throws InvalidImageReferenceException { .forToNotConfigured( "'jib.to.image'", "build.gradle", "gradle jib --image ")); } - - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - RegistryCredentials knownBaseRegistryCredentials = null; RegistryCredentials knownTargetRegistryCredentials = null; - Authorization fromAuthorization = - getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); - } Authorization toAuthorization = - getImageAuthorization(gradleJibLogger, "to", jibExtension.getFrom().getAuth()); + PluginConfigurationProcessor.getImageAuthorization( + gradleJibLogger, "to", jibExtension.getFrom().getAuth()); if (toAuthorization != null) { knownTargetRegistryCredentials = new RegistryCredentials("jib.to.auth", toAuthorization); } - - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); - - // Builds the BuildConfiguration. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) - .setCredentialHelper(jibExtension.getFrom().getCredHelper()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) - .build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(ImageReference.parse(jibExtension.getTargetImage())) .setCredentialHelper(jibExtension.getTo().getCredHelper()) .setKnownRegistryCredentials(knownTargetRegistryCredentials) .build(); - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)) - .setProgramArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); - if (jibExtension.getUseCurrentTimestamp()) { - gradleJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + gradleJibLogger, jibExtension, gradleProjectProperties); - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .setTargetFormat(jibExtension.getFormat()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); - - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - GradleJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + .build(); try { BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); @@ -179,42 +120,4 @@ BuildImageTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. - * - *

TODO: Consolidate with other tasks. - * - * @param logger the {@link JibLogger} used to print warnings - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param auth the auth configuration to get the {@link Authorization} from - * @return the {@link Authorization}, or null if the username and password aren't both configured - */ - @VisibleForTesting - @Nullable - static Authorization getImageAuthorization( - JibLogger logger, String imageProperty, AuthParameters auth) { - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "jib." - + imageProperty - + ".auth.username is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "jib." - + imageProperty - + ".auth.password is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 2024aeeaff..c32b0cfabc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,28 +16,16 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.nio.file.Paths; -import java.time.Instant; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -51,9 +39,6 @@ /** Builds a container image to a tarball. */ public class BuildTarTask extends DefaultTask { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Building image tarball failed"); @@ -115,74 +100,27 @@ public void buildTar() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); - - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - gradleJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = - getImageAuthorization(gradleJibLogger, "from", jibExtension.getFrom().getAuth()); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); - } - GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); + ImageReference targetImage = gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); - // Builds the BuildConfiguration. - // TODO: Consolidate with BuildImageTask/BuildDockerTask. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) - .setCredentialHelper(jibExtension.getFrom().getCredHelper()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + gradleJibLogger, jibExtension, gradleProjectProperties); + + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); - - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)) - .setProgramArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); - if (jibExtension.getUseCurrentTimestamp()) { - gradleJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(gradleJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(gradleProjectProperties.getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(gradleProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - GradleJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); - // Uses a directory in the Gradle build cache as the Jib cache. try { BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) @@ -197,41 +135,4 @@ BuildTarTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. - * - *

TODO: Consolidate with other tasks. - * - * @param logger the {@link JibLogger} used to print warnings - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param auth the auth configuration to get the {@link Authorization} from - * @return the {@link Authorization}, or null if the username and password aren't both configured - */ - @Nullable - private static Authorization getImageAuthorization( - JibLogger logger, String imageProperty, AuthParameters auth) { - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "jib." - + imageProperty - + ".auth.username is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "jib." - + imageProperty - + ".auth.password is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java new file mode 100644 index 0000000000..a5e37dcbf9 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.base.Strings; +import java.time.Instant; +import javax.annotation.Nullable; +import org.gradle.api.GradleException; + +/** Configures and provides builders for the image building tasks. */ +class PluginConfigurationProcessor { + + /** {@code User-Agent} header suffix to send to the registry. */ + private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; + + /** + * Sets up {@link BuildConfiguration} that is common among the image building tasks. This includes + * setting up the base image reference/authorization, container configuration, cache + * configuration, and layer configuration. + * + * @param logger the logger used to display messages. + * @param jibExtension the {@link JibExtension} providing the configuration data + * @param projectProperties used for providing additional information + * @return a new {@link PluginConfigurationProcessor} containing pre-configured builders + * @throws InvalidImageReferenceException if parsing the base image configuration fails + */ + static PluginConfigurationProcessor processCommonConfiguration( + JibLogger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) + throws InvalidImageReferenceException { + jibExtension.handleDeprecatedParameters(logger); + SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + + // TODO: Instead of disabling logging, have authentication credentials be provided + GradleJibLogger.disableHttpLogging(); + RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + logger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } + RegistryCredentials knownBaseRegistryCredentials = null; + Authorization fromAuthorization = + getImageAuthorization(logger, "from", jibExtension.getFrom().getAuth()); + if (fromAuthorization != null) { + knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); + } + + ImageConfiguration.Builder baseImageConfigurationBuilder = + ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) + .setCredentialHelper(jibExtension.getFrom().getCredHelper()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials); + + String mainClass = projectProperties.getMainClass(jibExtension); + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getJvmFlags(), mainClass)) + .setProgramArguments(jibExtension.getArgs()) + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); + if (jibExtension.getUseCurrentTimestamp()) { + logger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + + BuildConfiguration.Builder buildConfigurationBuilder = + BuildConfiguration.builder(logger) + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) + .setLayerConfigurations(projectProperties.getLayerConfigurations()); + CacheConfiguration applicationLayersCacheConfiguration = + CacheConfiguration.forPath(projectProperties.getCacheDirectory()); + buildConfigurationBuilder.setApplicationLayersCacheConfiguration( + applicationLayersCacheConfiguration); + if (jibExtension.getUseOnlyProjectCache()) { + buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( + applicationLayersCacheConfiguration); + } + + return new PluginConfigurationProcessor( + buildConfigurationBuilder, baseImageConfigurationBuilder, containerConfigurationBuilder); + } + + /** + * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. + * + * @param logger the {@link JibLogger} used to print warnings + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param auth the auth configuration to get the {@link Authorization} from + * @return the {@link Authorization}, or null if the username and password aren't both configured + */ + @Nullable + static Authorization getImageAuthorization( + JibLogger logger, String imageProperty, AuthParameters auth) { + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "jib." + + imageProperty + + ".auth.username is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "jib." + + imageProperty + + ".auth.password is null; ignoring jib." + + imageProperty + + ".auth section."); + return null; + } + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } + + private final BuildConfiguration.Builder buildConfigurationBuilder; + private final ImageConfiguration.Builder baseImageConfigurationBuilder; + private final ContainerConfiguration.Builder containerConfigurationBuilder; + + private PluginConfigurationProcessor( + BuildConfiguration.Builder buildConfigurationBuilder, + ImageConfiguration.Builder baseImageConfigurationBuilder, + ContainerConfiguration.Builder containerConfigurationBuilder) { + this.buildConfigurationBuilder = buildConfigurationBuilder; + this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; + this.containerConfigurationBuilder = containerConfigurationBuilder; + } + + BuildConfiguration.Builder getBuildConfigurationBuilder() { + return buildConfigurationBuilder; + } + + ImageConfiguration.Builder getBaseImageConfigurationBuilder() { + return baseImageConfigurationBuilder; + } + + ContainerConfiguration.Builder getContainerConfigurationBuilder() { + return containerConfigurationBuilder; + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java similarity index 77% rename from jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 7b78d81cdf..d7b3705244 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/BuildImageTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -26,15 +26,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** - * Test for {@link BuildImageTask}. - * - *

TODO: This only tests the {@link BuildImageTask#getImageAuthorization(JibLogger, String, - * AuthParameters)} method, which is copy-pasted between the 3 build tasks. When we refactor, we'll - * need to move this test. - */ +/** Test for {@link PluginConfigurationProcessor}. */ @RunWith(MockitoJUnitRunner.class) -public class BuildImageTaskTest { +public class PluginConfigurationProcessorTest { @Mock private JibLogger mockLogger; @@ -46,27 +40,28 @@ public void testGetImageAuthorization() { auth.setUsername("vwxyz"); auth.setPassword("98765"); Authorization expected = Authorizations.withBasicCredentials("vwxyz", "98765"); - Authorization actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + Authorization actual = + PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); Assert.assertNotNull(actual); Assert.assertEquals(expected.toString(), actual.toString()); Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); // Auth completely missing auth = new AuthParameters(); - actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); Assert.assertNull(actual); // Password missing auth = new AuthParameters(); auth.setUsername("vwxyz"); - actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); Assert.assertNull(actual); Mockito.verify(mockLogger).warn("jib.to.auth.password is null; ignoring jib.to.auth section."); // Username missing auth = new AuthParameters(); auth.setPassword("98765"); - actual = BuildImageTask.getImageAuthorization(mockLogger, "to", auth); + actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); Assert.assertNull(actual); Mockito.verify(mockLogger).warn("jib.to.auth.username is null; ignoring jib.to.auth section."); } From 62279dba035fe491e9203203eeceeda79f24d10f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 3 Aug 2018 13:46:58 -0400 Subject: [PATCH 0107/2020] Reduce duplicate code in maven plugin (#796) --- .../tools/jib/maven/BuildDockerMojo.java | 176 +------------ .../cloud/tools/jib/maven/BuildImageMojo.java | 186 ++----------- .../cloud/tools/jib/maven/BuildTarMojo.java | 174 +----------- .../jib/maven/JibPluginConfiguration.java | 23 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../maven/PluginConfigurationProcessor.java | 248 ++++++++++++++++++ ... => PluginConfigurationProcessorTest.java} | 20 +- 7 files changed, 312 insertions(+), 517 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java rename jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/{BuildImageMojoTest.java => PluginConfigurationProcessorTest.java} (86%) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 6255953bcb..0419acd18a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,29 +16,15 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import java.time.Instant; -import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -51,9 +37,6 @@ public class BuildDockerMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Build to Docker daemon failed"); @@ -64,91 +47,31 @@ public void execute() throws MojoExecutionException { return; } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); - handleDeprecatedParameters(mavenJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); - if (!new DockerClient().isDockerInstalled()) { throw new MojoExecutionException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); } - // Parses 'from' and 'to' into image reference. + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - ImageReference baseImage = parseImageReference(getBaseImage(), "from"); + ImageReference targetImage = mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); - // Checks Maven settings for registry credentials. - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = - getImageAuth( - mavenJibLogger, - "from", - "jib.from.auth.username", - "jib.from.auth.password", - getBaseImageAuth()); - RegistryCredentials knownBaseRegistryCredentials = - fromAuthorization != null - ? new RegistryCredentials( - "jib-maven-plugin configuration", fromAuthorization) - : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); - - String mainClass = mavenProjectProperties.getMainClass(this); - - // Builds the BuildConfiguration. - // TODO: Consolidate with BuildImageMojo. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(baseImage) - .setCredentialHelper(getBaseImageCredentialHelperName()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + mavenJibLogger, this, mavenProjectProperties); + + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); - - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) - .setProgramArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); - if (getUseCurrentTimestamp()) { - mavenJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - MavenJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); - try { BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); getLog().info(""); @@ -157,75 +80,4 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } - - /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then - * tries build configuration, otherwise returns null. - * - *

TODO: Consolidate with the other mojos. - * - * @param logger the {@link JibLogger} used to print warnings messages - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param usernameProperty the name of the username system property - * @param passwordProperty the name of the password system property - * @param auth the configured credentials - * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. - */ - @Nullable - private static Authorization getImageAuth( - JibLogger logger, - String imageProperty, - String usernameProperty, - String passwordProperty, - AuthConfiguration auth) { - // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); - if (!Strings.isNullOrEmpty(commandlineUsername) - && !Strings.isNullOrEmpty(commandlinePassword)) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); - } - - // Warn if a system property is missing - if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - logger.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods."); - } - if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - logger.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods."); - } - - // Check auth configuration next; warn if they aren't both set - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 4e6574d505..06906ceff8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,30 +16,19 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.time.Instant; import java.util.Arrays; -import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -53,24 +42,16 @@ public class BuildImageMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "build"; - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Build image failed"); @Override public void execute() throws MojoExecutionException, MojoFailureException { - // TODO: Consolidate all of these checks. if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); return; } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); - handleDeprecatedParameters(mavenJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); - // Validates 'format'. if (Arrays.stream(ImageFormat.values()).noneMatch(value -> value.name().equals(getFormat()))) { throw new MojoFailureException( @@ -83,9 +64,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { + "'."); } - // Parses 'from' into image reference. - ImageReference baseImage = parseImageReference(getBaseImage(), "from"); - // Parses 'to' into image reference. if (Strings.isNullOrEmpty(getTargetImage())) { throw new MojoFailureException( @@ -93,32 +71,19 @@ public void execute() throws MojoExecutionException, MojoFailureException { .forToNotConfigured( "", "pom.xml", "mvn compile jib:build -Dimage=")); } - ImageReference targetImage = parseImageReference(getTargetImage(), "to"); - // Checks Maven settings for registry credentials. - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } + MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + MavenProjectProperties mavenProjectProperties = + MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = - getImageAuth( - mavenJibLogger, - "from", - "jib.from.auth.username", - "jib.from.auth.password", - getBaseImageAuth()); - RegistryCredentials knownBaseRegistryCredentials = - fromAuthorization != null - ? new RegistryCredentials( - "jib-maven-plugin configuration", fromAuthorization) - : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + mavenJibLogger, this, mavenProjectProperties); + + ImageReference targetImage = + PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); Authorization toAuthorization = - getImageAuth( + PluginConfigurationProcessor.getImageAuth( mavenJibLogger, "to", "jib.to.auth.username", @@ -127,62 +92,25 @@ public void execute() throws MojoExecutionException, MojoFailureException { RegistryCredentials knownTargetRegistryCredentials = toAuthorization != null ? new RegistryCredentials("jib-maven-plugin configuration", toAuthorization) - : mavenSettingsServerCredentials.retrieve(targetImage.getRegistry()); - - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - String mainClass = mavenProjectProperties.getMainClass(this); - - // Builds the BuildConfiguration. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(baseImage) - .setCredentialHelper(getBaseImageCredentialHelperName()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) - .build(); - + : pluginConfigurationProcessor + .getMavenSettingsServerCredentials() + .retrieve(targetImage.getRegistry()); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) .setCredentialHelper(getTargetImageCredentialHelperName()) .setKnownRegistryCredentials(knownTargetRegistryCredentials) .build(); - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) - .setProgramArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); - if (getUseCurrentTimestamp()) { - mavenJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); - - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - MavenJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + .build(); try { BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); @@ -192,76 +120,4 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } - - /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then - * tries build configuration, otherwise returns null. - * - *

TODO: Consolidate with the other mojos. - * - * @param logger the {@link JibLogger} used to print warnings messages - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param usernameProperty the name of the username system property - * @param passwordProperty the name of the password system property - * @param auth the configured credentials - * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. - */ - @VisibleForTesting - @Nullable - static Authorization getImageAuth( - JibLogger logger, - String imageProperty, - String usernameProperty, - String passwordProperty, - AuthConfiguration auth) { - // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); - if (!Strings.isNullOrEmpty(commandlineUsername) - && !Strings.isNullOrEmpty(commandlinePassword)) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); - } - - // Warn if a system property is missing - if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - logger.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods."); - } - if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - logger.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods."); - } - - // Check auth configuration next; warn if they aren't both set - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index ee550c3df7..5df28313e9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,29 +16,15 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; -import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.nio.file.Paths; -import java.time.Instant; -import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -53,9 +39,6 @@ public class BuildTarMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "buildTar"; - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = HelpfulSuggestionsProvider.get("Building image tarball failed"); @@ -67,86 +50,26 @@ public void execute() throws MojoExecutionException { } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); - handleDeprecatedParameters(mavenJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); - - // Parses 'from' and 'to' into image reference. MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - ImageReference baseImage = parseImageReference(getBaseImage(), "from"); + ImageReference targetImage = mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); - // Checks Maven settings for registry credentials. - if (Boolean.getBoolean("sendCredentialsOverHttp")) { - mavenJibLogger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials( - Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenJibLogger); - Authorization fromAuthorization = - getImageAuth( - mavenJibLogger, - "from", - "jib.from.auth.username", - "jib.from.auth.password", - getBaseImageAuth()); - RegistryCredentials knownBaseRegistryCredentials = - fromAuthorization != null - ? new RegistryCredentials( - "jib-maven-plugin configuration", fromAuthorization) - : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); - - String mainClass = mavenProjectProperties.getMainClass(this); - - // Builds the BuildConfiguration. - // TODO: Consolidate with BuildImageMojo. - ImageConfiguration baseImageConfiguration = - ImageConfiguration.builder(baseImage) - .setCredentialHelper(getBaseImageCredentialHelperName()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + mavenJibLogger, this, mavenProjectProperties); + + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); - - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass)) - .setProgramArguments(getArgs()) - .setEnvironment(getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(getExposedPorts())); - if (getUseCurrentTimestamp()) { - mavenJibLogger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); - } - - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(mavenJibLogger) - .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfigurationBuilder.build()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()) - .setLayerConfigurations(mavenProjectProperties.getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(mavenProjectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); - if (getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); - } - - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - MavenJibLogger.disableHttpLogging(); - - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); - try { BuildStepsRunner.forBuildTar( Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), @@ -158,75 +81,4 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } - - /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then - * tries build configuration, otherwise returns null. - * - *

TODO: Consolidate with the other mojos. - * - * @param logger the {@link JibLogger} used to print warnings messages - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param usernameProperty the name of the username system property - * @param passwordProperty the name of the password system property - * @param auth the configured credentials - * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. - */ - @Nullable - private static Authorization getImageAuth( - JibLogger logger, - String imageProperty, - String usernameProperty, - String passwordProperty, - AuthConfiguration auth) { - // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); - if (!Strings.isNullOrEmpty(commandlineUsername) - && !Strings.isNullOrEmpty(commandlinePassword)) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); - } - - // Warn if a system property is missing - if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - logger.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods."); - } - if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - logger.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods."); - } - - // Check auth configuration next; warn if they aren't both set - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 04a627563e..4e5b6c562d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -114,19 +112,6 @@ public static class ContainerParameters { @Parameter private List ports = Collections.emptyList(); } - /** - * @param image the image reference string to parse. - * @param type name of the parameter being parsed (e.g. "to" or "from"). - * @return the {@link ImageReference} parsed from {@code from}. - */ - static ImageReference parseImageReference(String image, String type) { - try { - return ImageReference.parse(image); - } catch (InvalidImageReferenceException ex) { - throw new IllegalStateException("Parameter '" + type + "' is invalid", ex); - } - } - @Nullable @Parameter(defaultValue = "${session}", readonly = true) MavenSession session; @@ -202,6 +187,10 @@ void handleDeprecatedParameters(JibLogger logger) { } } + MavenSession getSession() { + return Preconditions.checkNotNull(session); + } + MavenProject getProject() { return Preconditions.checkNotNull(project); } @@ -276,6 +265,10 @@ Path getExtraDirectory() { return Paths.get(Preconditions.checkNotNull(extraDirectory)); } + SettingsDecrypter getSettingsDecrypter() { + return Preconditions.checkNotNull(settingsDecrypter); + } + @VisibleForTesting void setJvmFlags(List jvmFlags) { this.jvmFlags = jvmFlags; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 2651ea406d..c742ba6571 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -197,7 +197,7 @@ ImageReference getGeneratedTargetDockerTag( + "in your pom.xml, or use the -Dimage= commandline flag."); return ImageReference.of(null, project.getName(), project.getVersion()); } else { - return JibPluginConfiguration.parseImageReference(targetImage, "to"); + return PluginConfigurationProcessor.parseImageReference(targetImage, "to"); } } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java new file mode 100644 index 0000000000..879a0832cc --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -0,0 +1,248 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import java.time.Instant; +import javax.annotation.Nullable; +import org.apache.maven.plugin.MojoExecutionException; + +/** Configures and provides builders for the image building goals. */ +class PluginConfigurationProcessor { + + /** {@code User-Agent} header suffix to send to the registry. */ + private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; + + /** + * Sets up {@link BuildConfiguration} that is common among the image building goals. This includes + * setting up the base image reference/authorization, container configuration, cache + * configuration, and layer configuration. + * + * @param logger the logger used to display messages + * @param jibPluginConfiguration the {@link JibPluginConfiguration} providing the configuration + * data + * @param projectProperties used for providing additional information + * @return a new {@link PluginConfigurationProcessor} containing pre-configured builders + * @throws MojoExecutionException if the http timeout system property is misconfigured + */ + static PluginConfigurationProcessor processCommonConfiguration( + MavenJibLogger logger, + JibPluginConfiguration jibPluginConfiguration, + MavenProjectProperties projectProperties) + throws MojoExecutionException { + jibPluginConfiguration.handleDeprecatedParameters(logger); + SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); + + // TODO: Instead of disabling logging, have authentication credentials be provided + MavenJibLogger.disableHttpLogging(); + RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + + ImageReference baseImage = parseImageReference(jibPluginConfiguration.getBaseImage(), "from"); + + // Checks Maven settings for registry credentials. + if (Boolean.getBoolean("sendCredentialsOverHttp")) { + logger.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + + "this on a public network!"); + } + MavenSettingsServerCredentials mavenSettingsServerCredentials = + new MavenSettingsServerCredentials( + Preconditions.checkNotNull(jibPluginConfiguration.getSession()).getSettings(), + jibPluginConfiguration.getSettingsDecrypter(), + logger); + Authorization fromAuthorization = + getImageAuth( + logger, + "from", + "jib.from.auth.username", + "jib.from.auth.password", + jibPluginConfiguration.getBaseImageAuth()); + RegistryCredentials knownBaseRegistryCredentials = + fromAuthorization != null + ? new RegistryCredentials( + "jib-maven-plugin configuration", fromAuthorization) + : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + ImageConfiguration.Builder baseImageConfiguration = + ImageConfiguration.builder(baseImage) + .setCredentialHelper(jibPluginConfiguration.getBaseImageCredentialHelperName()) + .setKnownRegistryCredentials(knownBaseRegistryCredentials); + + String mainClass = projectProperties.getMainClass(jibPluginConfiguration); + ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder() + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibPluginConfiguration.getJvmFlags(), mainClass)) + .setProgramArguments(jibPluginConfiguration.getArgs()) + .setEnvironment(jibPluginConfiguration.getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())); + if (jibPluginConfiguration.getUseCurrentTimestamp()) { + logger.warn( + "Setting image creation time to current time; your image may not be reproducible."); + containerConfigurationBuilder.setCreationTime(Instant.now()); + } + + BuildConfiguration.Builder buildConfigurationBuilder = + BuildConfiguration.builder(logger) + .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) + .setLayerConfigurations(projectProperties.getLayerConfigurations()); + CacheConfiguration applicationLayersCacheConfiguration = + CacheConfiguration.forPath(projectProperties.getCacheDirectory()); + buildConfigurationBuilder.setApplicationLayersCacheConfiguration( + applicationLayersCacheConfiguration); + if (jibPluginConfiguration.getUseOnlyProjectCache()) { + buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( + applicationLayersCacheConfiguration); + } + + return new PluginConfigurationProcessor( + buildConfigurationBuilder, + baseImageConfiguration, + containerConfigurationBuilder, + mavenSettingsServerCredentials); + } + + /** + * @param image the image reference string to parse. + * @param type name of the parameter being parsed (e.g. "to" or "from"). + * @return the {@link ImageReference} parsed from {@code from}. + */ + static ImageReference parseImageReference(String image, String type) { + try { + return ImageReference.parse(image); + } catch (InvalidImageReferenceException ex) { + throw new IllegalStateException("Parameter '" + type + "' is invalid", ex); + } + } + + /** + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. + * + * @param logger the {@link JibLogger} used to print warnings messages + * @param imageProperty the image configuration's name (i.e. "from" or "to") + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. + */ + @Nullable + static Authorization getImageAuth( + JibLogger logger, + String imageProperty, + String usernameProperty, + String passwordProperty, + AuthConfiguration auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (!Strings.isNullOrEmpty(commandlineUsername) + && !Strings.isNullOrEmpty(commandlinePassword)) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + } + + // Warn if a system property is missing + if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + logger.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods."); + } + if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + logger.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods."); + } + + // Check auth configuration next; warn if they aren't both set + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + "<" + + imageProperty + + "> is missing from maven configuration; ignoring <" + + imageProperty + + "> section."); + return null; + } + + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } + + private final BuildConfiguration.Builder buildConfigurationBuilder; + private final ImageConfiguration.Builder baseImageConfigurationBuilder; + private final ContainerConfiguration.Builder containerConfigurationBuilder; + private final MavenSettingsServerCredentials mavenSettingsServerCredentials; + + private PluginConfigurationProcessor( + BuildConfiguration.Builder buildConfigurationBuilder, + ImageConfiguration.Builder baseImageConfigurationBuilder, + ContainerConfiguration.Builder containerConfigurationBuilder, + MavenSettingsServerCredentials mavenSettingsServerCredentials) { + this.buildConfigurationBuilder = buildConfigurationBuilder; + this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; + this.containerConfigurationBuilder = containerConfigurationBuilder; + this.mavenSettingsServerCredentials = mavenSettingsServerCredentials; + } + + BuildConfiguration.Builder getBuildConfigurationBuilder() { + return buildConfigurationBuilder; + } + + ImageConfiguration.Builder getBaseImageConfigurationBuilder() { + return baseImageConfigurationBuilder; + } + + ContainerConfiguration.Builder getContainerConfigurationBuilder() { + return containerConfigurationBuilder; + } + + MavenSettingsServerCredentials getMavenSettingsServerCredentials() { + return mavenSettingsServerCredentials; + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java similarity index 86% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java rename to jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index ecd7e323b4..21e1955d25 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -27,15 +27,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** - * Test for {@link BuildImageMojo}. - * - *

TODO: This only tests the {@link BuildImageMojo#getImageAuth(JibLogger, String, String, - * String, AuthConfiguration)} method, which is copy-pasted between the 3 build mojos. When we - * refactor, we'll need to move this test. - */ +/** Test for {@link PluginConfigurationProcessor}. */ @RunWith(MockitoJUnitRunner.class) -public class BuildImageMojoTest { +public class PluginConfigurationProcessorTest { @Mock private JibLogger mockLogger; @@ -50,7 +44,7 @@ public void testGetImageAuth() { System.setProperty("jib.test.auth.pass", "12345"); Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); Authorization actual = - BuildImageMojo.getImageAuth( + PluginConfigurationProcessor.getImageAuth( mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); Assert.assertNotNull(actual); Assert.assertEquals(expected.toString(), actual.toString()); @@ -60,7 +54,7 @@ public void testGetImageAuth() { System.clearProperty("jib.test.auth.pass"); expected = Authorizations.withBasicCredentials("vwxyz", "98765"); actual = - BuildImageMojo.getImageAuth( + PluginConfigurationProcessor.getImageAuth( mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); Assert.assertNotNull(actual); Assert.assertEquals(expected.toString(), actual.toString()); @@ -69,7 +63,7 @@ public void testGetImageAuth() { // Auth completely missing auth = new AuthConfiguration(); actual = - BuildImageMojo.getImageAuth( + PluginConfigurationProcessor.getImageAuth( mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); Assert.assertNull(actual); @@ -77,7 +71,7 @@ public void testGetImageAuth() { auth = new AuthConfiguration(); auth.setUsername("vwxyz"); actual = - BuildImageMojo.getImageAuth( + PluginConfigurationProcessor.getImageAuth( mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); Assert.assertNull(actual); Mockito.verify(mockLogger) @@ -88,7 +82,7 @@ public void testGetImageAuth() { auth = new AuthConfiguration(); auth.setPassword("98765"); actual = - BuildImageMojo.getImageAuth( + PluginConfigurationProcessor.getImageAuth( mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); Assert.assertNull(actual); Mockito.verify(mockLogger) From 04da505aa9d96865fa3e1a6c2985bca2a72ca6cc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 3 Aug 2018 16:21:26 -0400 Subject: [PATCH 0108/2020] Eliminate warnings (#792) --- examples/helloworld/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 62bae49723..9e63da7727 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -6,7 +6,9 @@ 1 + UTF-8 0.9.8 + 3.8.0 @@ -22,6 +24,7 @@ org.apache.maven.plugins maven-compiler-plugin + ${maven-compiler-plugin.version} 1.8 1.8 From 8ea2263b4c5c5121fd5223122f628d4ac9109199 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 6 Aug 2018 16:26:59 -0400 Subject: [PATCH 0109/2020] Consolidate auth validation in maven/gradle plugins (#801) --- .../jib/plugins/common/AuthProperty.java | 43 ++++++ .../ConfigurationPropertyValidator.java | 111 +++++++++++++++ .../common/SystemPropertyValidator.java | 45 ------- .../ConfigurationPropertyValidatorTest.java | 126 ++++++++++++++++++ .../common/SystemPropertyValidatorTest.java | 58 -------- .../tools/jib/gradle/AuthParameters.java | 37 ++++- .../tools/jib/gradle/BuildImageTask.java | 8 +- .../tools/jib/gradle/DockerContextTask.java | 4 +- .../tools/jib/gradle/ImageParameters.java | 4 +- .../cloud/tools/jib/gradle/JibExtension.java | 4 +- .../gradle/PluginConfigurationProcessor.java | 48 +------ .../PluginConfigurationProcessorTest.java | 68 ---------- .../cloud/tools/jib/maven/BuildImageMojo.java | 9 +- .../tools/jib/maven/DockerContextMojo.java | 4 +- .../jib/maven/JibPluginConfiguration.java | 31 ++++- .../maven/PluginConfigurationProcessor.java | 81 +---------- .../PluginConfigurationProcessorTest.java | 92 ------------- 17 files changed, 366 insertions(+), 407 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java new file mode 100644 index 0000000000..ea1740f363 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import javax.annotation.Nullable; + +/** Holds a username and password property. */ +public interface AuthProperty { + + @Nullable + String getUsername(); + + @Nullable + String getPassword(); + + /** + * Returns the full descriptor used to configure the {@link AuthProperty}'s username. + * + * @return the descriptor used to configure the username property (e.g. 'jib.to.auth.username') + */ + String getUsernamePropertyDescriptor(); + + /** + * Returns the full descriptor used to configure the {@link AuthProperty}'s password. + * + * @return the descriptor used to configure the password property (e.g. 'jib.to.auth.password') + */ + String getPasswordPropertyDescriptor(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java new file mode 100644 index 0000000000..441c018013 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import com.google.common.base.Strings; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** Validator for plugin configuration parameters and system properties. */ +public class ConfigurationPropertyValidator { + + /** + * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) + * values. + * + * @param exceptionFactory factory to create an exception with the given description + * @param the exception type to throw if invalid values + * @throws T if invalid values + */ + public static void checkHttpTimeoutProperty( + Function exceptionFactory) throws T { + String value = System.getProperty("jib.httpTimeout"); + if (value == null) { + return; + } + try { + if (Integer.parseInt(value) < 0) { + throw exceptionFactory.apply("jib.httpTimeout cannot be negative: " + value); + } + } catch (NumberFormatException ex) { + throw exceptionFactory.apply("jib.httpTimeout must be an integer: " + value); + } + } + + /** + * Gets an {@link Authorization} from a username and password. First tries system properties, then + * tries build configuration, otherwise returns null. + * + * @param logger the {@link JibLogger} used to print warnings messages + * @param usernameProperty the name of the username system property + * @param passwordProperty the name of the password system property + * @param auth the configured credentials + * @return a new {@link Authorization} from the system properties or build configuration, or + * {@code null} if neither is configured. + */ + @Nullable + public static Authorization getImageAuth( + JibLogger logger, String usernameProperty, String passwordProperty, AuthProperty auth) { + // System property takes priority over build configuration + String commandlineUsername = System.getProperty(usernameProperty); + String commandlinePassword = System.getProperty(passwordProperty); + if (!Strings.isNullOrEmpty(commandlineUsername) + && !Strings.isNullOrEmpty(commandlinePassword)) { + return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + } + + // Warn if a system property is missing + if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + logger.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods."); + } + if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + logger.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods."); + } + + // Check auth configuration next; warn if they aren't both set + if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + return null; + } + if (Strings.isNullOrEmpty(auth.getUsername())) { + logger.warn( + auth.getUsernamePropertyDescriptor() + + " is missing from build configuration; ignoring auth section."); + return null; + } + if (Strings.isNullOrEmpty(auth.getPassword())) { + logger.warn( + auth.getPasswordPropertyDescriptor() + + " is missing from build configuration; ignoring auth section."); + return null; + } + + return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + } + + private ConfigurationPropertyValidator() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java deleted file mode 100644 index 0f9bd0f7d4..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import java.util.function.Function; - -/** Validator for system properties. */ -public class SystemPropertyValidator { - - /** - * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) - * values. - * - * @param exceptionFactory factory to create an exception with the given description - * @param the exception type to throw if invalid values - * @throws T if invalid values - */ - public static void checkHttpTimeoutProperty( - Function exceptionFactory) throws T { - String value = System.getProperty("jib.httpTimeout"); - try { - if (value != null && Integer.parseInt(value) < 0) { - throw exceptionFactory.apply("jib.httpTimeout cannot be negative: " + value); - } - } catch (NumberFormatException ex) { - throw exceptionFactory.apply("jib.httpTimeout must be an integer: " + value); - } - } - - private SystemPropertyValidator() {} -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java new file mode 100644 index 0000000000..c628ebba23 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ConfigurationPropertyValidator}. */ +@RunWith(MockitoJUnitRunner.class) +public class ConfigurationPropertyValidatorTest { + + @Mock private JibLogger mockLogger; + @Mock private AuthProperty mockAuth; + + @After + public void tearDown() { + System.clearProperty("jib.httpTimeout"); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_ok() throws Exception { + Assert.assertNull(System.getProperty("jib.httpTimeout")); + ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_stringValue() { + System.setProperty("jib.httpTimeout", "random string"); + try { + ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); + Assert.fail("Should error with a non-integer timeout"); + } catch (Exception ex) { + Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); + } + } + + @Test + public void testCheckHttpTimeoutSystemProperty_negativeValue() { + System.setProperty("jib.httpTimeout", "-80"); + try { + ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); + Assert.fail("Should error with a negative timeout"); + } catch (Exception ex) { + Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); + } + } + + @Test + public void testGetImageAuth() { + Mockito.when(mockAuth.getUsernamePropertyDescriptor()).thenReturn("user"); + Mockito.when(mockAuth.getPasswordPropertyDescriptor()).thenReturn("pass"); + Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); + Mockito.when(mockAuth.getPassword()).thenReturn("98765"); + + // System properties set + System.setProperty("jib.test.auth.user", "abcde"); + System.setProperty("jib.test.auth.pass", "12345"); + Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); + Authorization actual = + ConfigurationPropertyValidator.getImageAuth( + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + + // Auth set in configuration + System.clearProperty("jib.test.auth.user"); + System.clearProperty("jib.test.auth.pass"); + expected = Authorizations.withBasicCredentials("vwxyz", "98765"); + actual = + ConfigurationPropertyValidator.getImageAuth( + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + Assert.assertNotNull(actual); + Assert.assertEquals(expected.toString(), actual.toString()); + Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); + + // Auth completely missing + Mockito.when(mockAuth.getUsername()).thenReturn(null); + Mockito.when(mockAuth.getPassword()).thenReturn(null); + actual = + ConfigurationPropertyValidator.getImageAuth( + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + Assert.assertNull(actual); + + // Password missing + Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); + Mockito.when(mockAuth.getPassword()).thenReturn(null); + actual = + ConfigurationPropertyValidator.getImageAuth( + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + Assert.assertNull(actual); + Mockito.verify(mockLogger) + .warn("pass is missing from build configuration; ignoring auth section."); + + // Username missing + Mockito.when(mockAuth.getUsername()).thenReturn(null); + Mockito.when(mockAuth.getPassword()).thenReturn("98765"); + actual = + ConfigurationPropertyValidator.getImageAuth( + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + Assert.assertNull(actual); + Mockito.verify(mockLogger) + .warn("user is missing from build configuration; ignoring auth section."); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java deleted file mode 100644 index 3e98bcdda0..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/SystemPropertyValidatorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link SystemPropertyValidator}. */ -public class SystemPropertyValidatorTest { - - @After - public void tearDown() { - System.clearProperty("jib.httpTimeout"); - } - - @Test - public void testCheckHttpTimeoutSystemProperty_ok() throws Exception { - Assert.assertNull(System.getProperty("jib.httpTimeout")); - SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); - } - - @Test - public void testCheckHttpTimeoutSystemProperty_stringValue() { - System.setProperty("jib.httpTimeout", "random string"); - try { - SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); - Assert.fail("Should error with a non-integer timeout"); - } catch (Exception ex) { - Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); - } - } - - @Test - public void testCheckHttpTimeoutSystemProperty_negativeValue() { - System.setProperty("jib.httpTimeout", "-80"); - try { - SystemPropertyValidator.checkHttpTimeoutProperty(Exception::new); - Assert.fail("Should error with a negative timeout"); - } catch (Exception ex) { - Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); - } - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index 27eca3eadd..757dfc0914 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -16,22 +16,52 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.common.base.Preconditions; import javax.annotation.Nullable; +import javax.inject.Inject; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; /** * A bean that configures authorization credentials to be used for a registry. This is configurable * with Groovy closures and can be validated when used as a task input. */ -public class AuthParameters { +public class AuthParameters implements AuthProperty { @Nullable private String username; @Nullable private String password; + private String usernameDescriptor; + private String passwordDescriptor; + + /** + * Constructs a new {@link AuthParameters}. + * + * @param descriptor the name of the auth configuration property + */ + @Inject + public AuthParameters(String descriptor) { + this.usernameDescriptor = descriptor + ".username"; + this.passwordDescriptor = descriptor + ".password"; + } + + @Internal + @Override + public String getUsernamePropertyDescriptor() { + return Preconditions.checkNotNull(usernameDescriptor); + } + + @Internal + @Override + public String getPasswordPropertyDescriptor() { + return Preconditions.checkNotNull(passwordDescriptor); + } @Input - @Nullable @Optional + @Override + @Nullable public String getUsername() { return username; } @@ -41,8 +71,9 @@ public void setUsername(String username) { } @Input - @Nullable @Optional + @Override + @Nullable public String getPassword() { return password; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 75af11265b..af5801040c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; @@ -82,8 +83,11 @@ public void buildImage() throws InvalidImageReferenceException { } RegistryCredentials knownTargetRegistryCredentials = null; Authorization toAuthorization = - PluginConfigurationProcessor.getImageAuthorization( - gradleJibLogger, "to", jibExtension.getFrom().getAuth()); + ConfigurationPropertyValidator.getImageAuth( + gradleJibLogger, + "jib.to.auth.username", + "jib.to.auth.password", + jibExtension.getTo().getAuth()); if (toAuthorization != null) { knownTargetRegistryCredentials = new RegistryCredentials("jib.to.auth", toAuthorization); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 8e1665f72b..9aba8f57df 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; @@ -101,7 +101,7 @@ public void generateDockerContext() { GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + ConfigurationPropertyValidator.checkHttpTimeoutProperty(GradleException::new); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java index 017c809668..26ae477042 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java @@ -39,8 +39,8 @@ public class ImageParameters { @Nullable private String credHelper; @Inject - public ImageParameters(ObjectFactory objectFactory) { - auth = objectFactory.newInstance(AuthParameters.class); + public ImageParameters(ObjectFactory objectFactory, String imageDescriptor) { + auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); } @Input diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 276a6177c3..d42e627c0f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -91,8 +91,8 @@ public JibExtension(Project project) { projectDir = project.getProjectDir().toPath(); ObjectFactory objectFactory = project.getObjects(); - from = objectFactory.newInstance(ImageParameters.class); - to = objectFactory.newInstance(ImageParameters.class); + from = objectFactory.newInstance(ImageParameters.class, "jib.from"); + to = objectFactory.newInstance(ImageParameters.class, "jib.to"); container = objectFactory.newInstance(ContainerParameters.class); jvmFlags = objectFactory.listProperty(String.class); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index a5e37dcbf9..23d4f926e3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -24,15 +24,12 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; -import com.google.common.base.Strings; import java.time.Instant; -import javax.annotation.Nullable; import org.gradle.api.GradleException; /** Configures and provides builders for the image building tasks. */ @@ -56,7 +53,7 @@ static PluginConfigurationProcessor processCommonConfiguration( JibLogger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) throws InvalidImageReferenceException { jibExtension.handleDeprecatedParameters(logger); - SystemPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + ConfigurationPropertyValidator.checkHttpTimeoutProperty(GradleException::new); // TODO: Instead of disabling logging, have authentication credentials be provided GradleJibLogger.disableHttpLogging(); @@ -69,7 +66,11 @@ static PluginConfigurationProcessor processCommonConfiguration( } RegistryCredentials knownBaseRegistryCredentials = null; Authorization fromAuthorization = - getImageAuthorization(logger, "from", jibExtension.getFrom().getAuth()); + ConfigurationPropertyValidator.getImageAuth( + logger, + "jib.from.auth.username", + "jib.from.auth.password", + jibExtension.getFrom().getAuth()); if (fromAuthorization != null) { knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); } @@ -110,41 +111,6 @@ static PluginConfigurationProcessor processCommonConfiguration( buildConfigurationBuilder, baseImageConfigurationBuilder, containerConfigurationBuilder); } - /** - * Validates and returns an {@link Authorization} from a configured {@link AuthParameters}. - * - * @param logger the {@link JibLogger} used to print warnings - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param auth the auth configuration to get the {@link Authorization} from - * @return the {@link Authorization}, or null if the username and password aren't both configured - */ - @Nullable - static Authorization getImageAuthorization( - JibLogger logger, String imageProperty, AuthParameters auth) { - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "jib." - + imageProperty - + ".auth.username is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "jib." - + imageProperty - + ".auth.password is null; ignoring jib." - + imageProperty - + ".auth section."); - return null; - } - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } - private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java deleted file mode 100644 index d7b3705244..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for {@link PluginConfigurationProcessor}. */ -@RunWith(MockitoJUnitRunner.class) -public class PluginConfigurationProcessorTest { - - @Mock private JibLogger mockLogger; - - @Test - public void testGetImageAuthorization() { - - // Auth set - AuthParameters auth = new AuthParameters(); - auth.setUsername("vwxyz"); - auth.setPassword("98765"); - Authorization expected = Authorizations.withBasicCredentials("vwxyz", "98765"); - Authorization actual = - PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); - - // Auth completely missing - auth = new AuthParameters(); - actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); - Assert.assertNull(actual); - - // Password missing - auth = new AuthParameters(); - auth.setUsername("vwxyz"); - actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); - Assert.assertNull(actual); - Mockito.verify(mockLogger).warn("jib.to.auth.password is null; ignoring jib.to.auth section."); - - // Username missing - auth = new AuthParameters(); - auth.setPassword("98765"); - actual = PluginConfigurationProcessor.getImageAuthorization(mockLogger, "to", auth); - Assert.assertNull(actual); - Mockito.verify(mockLogger).warn("jib.to.auth.username is null; ignoring jib.to.auth section."); - } -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 06906ceff8..e39d493980 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; @@ -83,12 +84,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { ImageReference targetImage = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); Authorization toAuthorization = - PluginConfigurationProcessor.getImageAuth( - mavenJibLogger, - "to", - "jib.to.auth.username", - "jib.to.auth.password", - getTargetImageAuth()); + ConfigurationPropertyValidator.getImageAuth( + mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); RegistryCredentials knownTargetRegistryCredentials = toAuthorization != null ? new RegistryCredentials("jib-maven-plugin configuration", toAuthorization) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index e08574fee5..010aa63da8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -54,7 +54,7 @@ public void execute() throws MojoExecutionException { MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); handleDeprecatedParameters(mavenJibLogger); - SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); + ConfigurationPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); Preconditions.checkNotNull(targetDir); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 4e5b6c562d..13c033f27b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -38,11 +39,17 @@ abstract class JibPluginConfiguration extends AbstractMojo { /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ - public static class AuthConfiguration { + public static class AuthConfiguration implements AuthProperty { @Nullable @Parameter private String username; - @Nullable @Parameter private String password; + private String usernameDescriptor; + private String passwordDescriptor; + + private AuthConfiguration(String descriptor) { + this.usernameDescriptor = descriptor + ""; + this.passwordDescriptor = descriptor + ""; + } @VisibleForTesting void setUsername(String username) { @@ -54,13 +61,25 @@ void setPassword(String password) { this.password = password; } + @Override + public String getUsernamePropertyDescriptor() { + return usernameDescriptor; + } + + @Override + public String getPasswordPropertyDescriptor() { + return passwordDescriptor; + } + + @Override @Nullable - String getUsername() { + public String getUsername() { return username; } + @Override @Nullable - String getPassword() { + public String getPassword() { return password; } } @@ -77,7 +96,7 @@ public static class FromConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(); + @Parameter private AuthConfiguration auth = new AuthConfiguration(""); } /** Configuration for {@code to} parameter, where image is required. */ @@ -87,7 +106,7 @@ public static class ToConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(); + @Parameter private AuthConfiguration auth = new AuthConfiguration(""); public void set(String image) { this.image = image; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 879a0832cc..4b16f0ba67 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -24,17 +23,13 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; -import com.google.cloud.tools.jib.plugins.common.SystemPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.time.Instant; -import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; /** Configures and provides builders for the image building goals. */ @@ -61,7 +56,7 @@ static PluginConfigurationProcessor processCommonConfiguration( MavenProjectProperties projectProperties) throws MojoExecutionException { jibPluginConfiguration.handleDeprecatedParameters(logger); - SystemPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); + ConfigurationPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); // TODO: Instead of disabling logging, have authentication credentials be provided MavenJibLogger.disableHttpLogging(); @@ -81,9 +76,8 @@ static PluginConfigurationProcessor processCommonConfiguration( jibPluginConfiguration.getSettingsDecrypter(), logger); Authorization fromAuthorization = - getImageAuth( + ConfigurationPropertyValidator.getImageAuth( logger, - "from", "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); @@ -145,75 +139,6 @@ static ImageReference parseImageReference(String image, String type) { } } - /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then - * tries build configuration, otherwise returns null. - * - * @param logger the {@link JibLogger} used to print warnings messages - * @param imageProperty the image configuration's name (i.e. "from" or "to") - * @param usernameProperty the name of the username system property - * @param passwordProperty the name of the password system property - * @param auth the configured credentials - * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. - */ - @Nullable - static Authorization getImageAuth( - JibLogger logger, - String imageProperty, - String usernameProperty, - String passwordProperty, - AuthConfiguration auth) { - // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); - if (!Strings.isNullOrEmpty(commandlineUsername) - && !Strings.isNullOrEmpty(commandlinePassword)) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); - } - - // Warn if a system property is missing - if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - logger.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods."); - } - if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - logger.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods."); - } - - // Check auth configuration next; warn if they aren't both set - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; - } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - "<" - + imageProperty - + "> is missing from maven configuration; ignoring <" - + imageProperty - + "> section."); - return null; - } - - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); - } - private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java deleted file mode 100644 index 21e1955d25..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for {@link PluginConfigurationProcessor}. */ -@RunWith(MockitoJUnitRunner.class) -public class PluginConfigurationProcessorTest { - - @Mock private JibLogger mockLogger; - - @Test - public void testGetImageAuth() { - AuthConfiguration auth = new AuthConfiguration(); - auth.setUsername("vwxyz"); - auth.setPassword("98765"); - - // System properties set - System.setProperty("jib.test.auth.user", "abcde"); - System.setProperty("jib.test.auth.pass", "12345"); - Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); - Authorization actual = - PluginConfigurationProcessor.getImageAuth( - mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); - - // Auth set in configuration - System.clearProperty("jib.test.auth.user"); - System.clearProperty("jib.test.auth.pass"); - expected = Authorizations.withBasicCredentials("vwxyz", "98765"); - actual = - PluginConfigurationProcessor.getImageAuth( - mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); - - // Auth completely missing - auth = new AuthConfiguration(); - actual = - PluginConfigurationProcessor.getImageAuth( - mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNull(actual); - - // Password missing - auth = new AuthConfiguration(); - auth.setUsername("vwxyz"); - actual = - PluginConfigurationProcessor.getImageAuth( - mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNull(actual); - Mockito.verify(mockLogger) - .warn( - " is missing from maven configuration; ignoring section."); - - // Username missing - auth = new AuthConfiguration(); - auth.setPassword("98765"); - actual = - PluginConfigurationProcessor.getImageAuth( - mockLogger, "to", "jib.test.auth.user", "jib.test.auth.pass", auth); - Assert.assertNull(actual); - Mockito.verify(mockLogger) - .warn( - " is missing from maven configuration; ignoring section."); - } -} From a502092c5727e68b77064c4946ed5fb61b506c82 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 6 Aug 2018 17:48:25 -0400 Subject: [PATCH 0110/2020] Consolidates Maven- and GradleLayerConfigurations. (#786) --- .../JavaDockerContextGenerator.java} | 52 +++--- .../jib/frontend/JavaLayerConfigurations.java | 156 ++++++++++++++++++ .../jib/plugins/common/MainClassResolver.java | 10 +- .../jib/plugins/common/ProjectProperties.java | 16 +- .../JavaDockerContextGeneratorTest.java} | 60 ++++--- .../frontend/JavaLayerConfigurationsTest.java | 85 ++++++++++ .../plugins/common/MainClassResolverTest.java | 15 +- .../tools/jib/gradle/DockerContextTask.java | 9 +- .../jib/gradle/GradleLayerConfigurations.java | 99 ++--------- .../jib/gradle/GradleProjectProperties.java | 39 +---- .../gradle/PluginConfigurationProcessor.java | 3 +- .../gradle/GradleLayerConfigurationsTest.java | 54 ++---- .../gradle/GradleProjectPropertiesTest.java | 6 +- .../tools/jib/maven/DockerContextMojo.java | 9 +- .../jib/maven/MavenLayerConfigurations.java | 102 ++---------- .../jib/maven/MavenProjectProperties.java | 39 +---- .../maven/PluginConfigurationProcessor.java | 3 +- .../maven/MavenLayerConfigurationsTest.java | 42 ++--- .../jib/maven/MavenProjectPropertiesTest.java | 5 +- 19 files changed, 404 insertions(+), 400 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{docker/DockerContextGenerator.java => frontend/JavaDockerContextGenerator.java} (84%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java rename jib-core/src/test/java/com/google/cloud/tools/jib/{docker/DockerContextGeneratorTest.java => frontend/JavaDockerContextGeneratorTest.java} (67%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java similarity index 84% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 2840a007c7..711f4f058a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -14,12 +14,11 @@ * the License. */ -package com.google.cloud.tools.jib.docker; +package com.google.cloud.tools.jib.frontend; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -35,7 +34,7 @@ import javax.annotation.Nullable; /** - * Generates a Docker context that mimics how Jib builds the image. + * Generates a Docker context for a Java application. * *

The image consists of a base image layer and 5 application layers under the directories: * @@ -49,7 +48,7 @@ * * Empty application layers are omitted. */ -public class DockerContextGenerator { +public class JavaDockerContextGenerator { private static final String DEPENDENCIES_LAYER_DIRECTORY = "libs"; private static final String SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY = "snapshot-libs"; @@ -105,22 +104,33 @@ private static void addIfNotEmpty( private List javaArguments = Collections.emptyList(); private List exposedPorts = Collections.emptyList(); - // TODO: Just take the LayerConfigurations. - public DockerContextGenerator( - LayerEntry dependenciesLayerEntry, - LayerEntry snapshotDependenciesLayerEntry, - LayerEntry resourcesLayerEntry, - LayerEntry classesLayerEntry, - LayerEntry extraFilesLayerEntry) { + /** + * Constructs a Docker context generator for a Java application. + * + * @param javaLayerConfigurations the {@link JavaLayerConfigurations} + */ + public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfigurations) { ImmutableList.Builder copyDirectivesBuilder = ImmutableList.builder(); - addIfNotEmpty(copyDirectivesBuilder, dependenciesLayerEntry, DEPENDENCIES_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, - snapshotDependenciesLayerEntry, + javaLayerConfigurations.getDependenciesLayerEntry(), + DEPENDENCIES_LAYER_DIRECTORY); + addIfNotEmpty( + copyDirectivesBuilder, + javaLayerConfigurations.getSnapshotDependenciesLayerEntry(), SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY); - addIfNotEmpty(copyDirectivesBuilder, resourcesLayerEntry, RESOURCES_LAYER_DIRECTORY); - addIfNotEmpty(copyDirectivesBuilder, classesLayerEntry, CLASSES_LAYER_DIRECTORY); - addIfNotEmpty(copyDirectivesBuilder, extraFilesLayerEntry, EXTRA_FILES_LAYER_DIRECTORY); + addIfNotEmpty( + copyDirectivesBuilder, + javaLayerConfigurations.getResourcesLayerEntry(), + RESOURCES_LAYER_DIRECTORY); + addIfNotEmpty( + copyDirectivesBuilder, + javaLayerConfigurations.getClassesLayerEntry(), + CLASSES_LAYER_DIRECTORY); + addIfNotEmpty( + copyDirectivesBuilder, + javaLayerConfigurations.getExtraFilesLayerEntry(), + EXTRA_FILES_LAYER_DIRECTORY); copyDirectives = copyDirectivesBuilder.build(); } @@ -131,7 +141,7 @@ public DockerContextGenerator( * @param baseImage the base image. * @return this */ - public DockerContextGenerator setBaseImage(String baseImage) { + public JavaDockerContextGenerator setBaseImage(String baseImage) { this.baseImage = baseImage; return this; } @@ -142,7 +152,7 @@ public DockerContextGenerator setBaseImage(String baseImage) { * @param jvmFlags the jvm flags. * @return this */ - public DockerContextGenerator setJvmFlags(List jvmFlags) { + public JavaDockerContextGenerator setJvmFlags(List jvmFlags) { this.jvmFlags = jvmFlags; return this; } @@ -153,7 +163,7 @@ public DockerContextGenerator setJvmFlags(List jvmFlags) { * @param mainClass the name of the main class. * @return this */ - public DockerContextGenerator setMainClass(String mainClass) { + public JavaDockerContextGenerator setMainClass(String mainClass) { this.mainClass = mainClass; return this; } @@ -164,7 +174,7 @@ public DockerContextGenerator setMainClass(String mainClass) { * @param javaArguments the list of arguments to pass into main. * @return this */ - public DockerContextGenerator setJavaArguments(List javaArguments) { + public JavaDockerContextGenerator setJavaArguments(List javaArguments) { this.javaArguments = javaArguments; return this; } @@ -175,7 +185,7 @@ public DockerContextGenerator setJavaArguments(List javaArguments) { * @param exposedPorts the list of port numbers/port ranges to expose * @return this */ - public DockerContextGenerator setExposedPorts(List exposedPorts) { + public JavaDockerContextGenerator setExposedPorts(List exposedPorts) { this.exposedPorts = exposedPorts; return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java new file mode 100644 index 0000000000..8a2b9e0d16 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** Builds {@link LayerConfiguration}s for a Java application. */ +public class JavaLayerConfigurations { + + /** Represents the different types of layers for a Java application. */ + @VisibleForTesting + enum LayerType { + DEPENDENCIES("dependencies", JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE), + SNAPSHOT_DEPENDENCIES( + "snapshot dependencies", JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE), + RESOURCES("resources", JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE), + CLASSES("classes", JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE), + EXTRA_FILES("extra files", "/"); + + private final String label; + private final String extractionPath; + + /** Initializes with a label for the layer and the layer files' default extraction path root. */ + LayerType(String label, String extractionPath) { + this.label = label; + this.extractionPath = extractionPath; + } + + @VisibleForTesting + String getLabel() { + return label; + } + + @VisibleForTesting + String getExtractionPath() { + return extractionPath; + } + } + + /** Builds with each layer's files. */ + public static class Builder { + + private Map> layerFilesMap = new EnumMap<>(LayerType.class); + + private Builder() { + for (LayerType layerType : LayerType.values()) { + layerFilesMap.put(layerType, new ArrayList<>()); + } + } + + public Builder setDependenciesFiles(List dependenciesFiles) { + layerFilesMap.put(LayerType.DEPENDENCIES, dependenciesFiles); + return this; + } + + public Builder setSnapshotDependenciesFiles(List snapshotDependenciesFiles) { + layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependenciesFiles); + return this; + } + + public Builder setResourcesFiles(List resourcesFiles) { + layerFilesMap.put(LayerType.RESOURCES, resourcesFiles); + return this; + } + + public Builder setClassesFiles(List classesFiles) { + layerFilesMap.put(LayerType.CLASSES, classesFiles); + return this; + } + + public Builder setExtraFiles(List extraFiles) { + layerFilesMap.put(LayerType.EXTRA_FILES, extraFiles); + return this; + } + + public JavaLayerConfigurations build() { + ImmutableMap.Builder layerConfigurationsMap = + ImmutableMap.builderWithExpectedSize(LayerType.values().length); + for (LayerType layerType : LayerType.values()) { + List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType)); + layerConfigurationsMap.put( + layerType, + LayerConfiguration.builder() + .addEntry(layerFiles, layerType.getExtractionPath()) + .setLabel(layerType.getLabel()) + .build()); + } + return new JavaLayerConfigurations(layerConfigurationsMap.build()); + } + } + + public static Builder builder() { + return new Builder(); + } + + private final ImmutableMap layerConfigurationMap; + + private JavaLayerConfigurations( + ImmutableMap layerConfigurationsMap) { + this.layerConfigurationMap = layerConfigurationsMap; + } + + public ImmutableList getLayerConfigurations() { + return layerConfigurationMap.values().asList(); + } + + public LayerEntry getDependenciesLayerEntry() { + return getLayerEntry(LayerType.DEPENDENCIES); + } + + public LayerEntry getSnapshotDependenciesLayerEntry() { + return getLayerEntry(LayerType.SNAPSHOT_DEPENDENCIES); + } + + public LayerEntry getResourcesLayerEntry() { + return getLayerEntry(LayerType.RESOURCES); + } + + public LayerEntry getClassesLayerEntry() { + return getLayerEntry(LayerType.CLASSES); + } + + public LayerEntry getExtraFilesLayerEntry() { + return getLayerEntry(LayerType.EXTRA_FILES); + } + + private LayerEntry getLayerEntry(LayerType layerType) { + return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)) + .getLayerEntries() + .get(0); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 3e6c66466d..46e29b9649 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -21,6 +21,8 @@ import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; import javax.annotation.Nullable; import javax.lang.model.SourceVersion; @@ -109,11 +111,11 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + projectProperties.getJarPluginName() + "; attempting to infer main class."); + ImmutableList classesSourceFiles = + projectProperties.getJavaLayerConfigurations().getClassesLayerEntry().getSourceFiles(); + MainClassFinder.Result mainClassFinderResult = - new MainClassFinder( - projectProperties.getClassesLayerEntry().getSourceFiles(), - projectProperties.getLogger()) - .find(); + new MainClassFinder(classesSourceFiles, projectProperties.getLogger()).find(); if (mainClassFinderResult.isSuccess()) { return mainClassFinderResult.getFoundMainClass(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 4df88a242d..9c74b0876c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -17,9 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.nio.file.Path; import javax.annotation.Nullable; @@ -33,7 +31,7 @@ public interface ProjectProperties { String getPluginName(); - ImmutableList getLayerConfigurations(); + JavaLayerConfigurations getJavaLayerConfigurations(); Path getCacheDirectory(); @@ -43,16 +41,6 @@ public interface ProjectProperties { @Nullable String getMainClassFromJar(); - LayerEntry getDependenciesLayerEntry(); - - LayerEntry getSnapshotDependenciesLayerEntry(); - - LayerEntry getResourcesLayerEntry(); - - LayerEntry getClassesLayerEntry(); - - LayerEntry getExtraFilesLayerEntry(); - /** * @param prefix the prefix message for the {@link HelpfulSuggestions}. * @return a {@link HelpfulSuggestions} instance for main class inference failure. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java similarity index 67% rename from jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 78e822c5d4..3d0fadf16d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.docker; +package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.image.LayerEntry; @@ -36,26 +36,27 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DockerContextGenerator}. */ +/** Tests for {@link JavaDockerContextGenerator}. */ @RunWith(MockitoJUnitRunner.class) -public class DockerContextGeneratorTest { +public class JavaDockerContextGeneratorTest { + private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; - private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; private static void assertSameFiles(Path directory1, Path directory2) throws IOException { Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); new DirectoryWalker(directory2) .walk( - directory2Path -> { - Assert.assertEquals( - directory1.relativize(directory1Paths.pop()), - directory2.relativize(directory2Path)); - }); + directory2Path -> + Assert.assertEquals( + directory1.relativize(directory1Paths.pop()), + directory2.relativize(directory2Path))); Assert.assertEquals(0, directory1Paths.size()); } @@ -68,6 +69,8 @@ private static ImmutableList listFilesInDirectory(Path directory) throws I @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; + @Test public void testGenerate() throws IOException, URISyntaxException { Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); @@ -87,17 +90,22 @@ public void testGenerate() throws IOException, URISyntaxException { Path targetDirectory = temporaryFolder.newFolder().toPath(); /* - * Deletes the directory so that DockerContextGenerator#generate does not throw + * Deletes the directory so that JavaDockerContextGenerator#generate does not throw * InsecureRecursiveDeleteException. */ Files.delete(targetDirectory); - new DockerContextGenerator( - new LayerEntry(expectedDependenciesFiles, EXPECTED_DEPENDENCIES_PATH), - new LayerEntry(expectedSnapshotDependenciesFiles, EXPECTED_DEPENDENCIES_PATH), - new LayerEntry(expectedResourcesFiles, EXPECTED_RESOURCES_PATH), - new LayerEntry(expectedClassesFiles, EXPECTED_CLASSES_PATH), - new LayerEntry(expectedExtraFiles, "/")) + Mockito.when(mockJavaLayerConfigurations.getDependenciesLayerEntry()) + .thenReturn(new LayerEntry(expectedDependenciesFiles, EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependenciesLayerEntry()) + .thenReturn(new LayerEntry(expectedSnapshotDependenciesFiles, EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getResourcesLayerEntry()) + .thenReturn(new LayerEntry(expectedResourcesFiles, EXPECTED_RESOURCES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) + .thenReturn(new LayerEntry(expectedClassesFiles, EXPECTED_CLASSES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) + .thenReturn(new LayerEntry(expectedExtraFiles, "/")); + new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage("somebaseimage") .generate(targetDirectory); @@ -117,13 +125,21 @@ public void testMakeDockerfile() throws IOException { List expectedJavaArguments = Arrays.asList("arg1", "arg2"); List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); + Mockito.when(mockJavaLayerConfigurations.getDependenciesLayerEntry()) + .thenReturn( + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependenciesLayerEntry()) + .thenReturn( + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getResourcesLayerEntry()) + .thenReturn( + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_RESOURCES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_CLASSES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) + .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), "/")); String dockerfile = - new DockerContextGenerator( - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH), - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH), - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_RESOURCES_PATH), - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_CLASSES_PATH), - new LayerEntry(ImmutableList.of(Paths.get("ignored")), "/")) + new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage(expectedBaseImage) .setJvmFlags(expectedJvmFlags) .setMainClass(expectedMainClass) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java new file mode 100644 index 0000000000..a043c4edad --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -0,0 +1,85 @@ +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.LayerEntry; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link JavaLayerConfigurations}. */ +public class JavaLayerConfigurationsTest { + + @Test + public void testDefault() { + JavaLayerConfigurations javaLayerConfigurations = JavaLayerConfigurations.builder().build(); + + LayerEntry dependenciesLayerEntry = javaLayerConfigurations.getDependenciesLayerEntry(); + LayerEntry snapshotDependenciesLayerEntry = + javaLayerConfigurations.getSnapshotDependenciesLayerEntry(); + LayerEntry resourcesLayerEntry = javaLayerConfigurations.getResourcesLayerEntry(); + LayerEntry classesLayerEntry = javaLayerConfigurations.getClassesLayerEntry(); + LayerEntry extraFilesLayerEntry = javaLayerConfigurations.getExtraFilesLayerEntry(); + + Assert.assertEquals( + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE, + dependenciesLayerEntry.getExtractionPath()); + Assert.assertEquals( + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE, + snapshotDependenciesLayerEntry.getExtractionPath()); + Assert.assertEquals( + JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE, + resourcesLayerEntry.getExtractionPath()); + Assert.assertEquals( + JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE, + classesLayerEntry.getExtractionPath()); + Assert.assertEquals("/", extraFilesLayerEntry.getExtractionPath()); + Assert.assertTrue(dependenciesLayerEntry.getSourceFiles().isEmpty()); + Assert.assertTrue(snapshotDependenciesLayerEntry.getSourceFiles().isEmpty()); + Assert.assertTrue(resourcesLayerEntry.getSourceFiles().isEmpty()); + Assert.assertTrue(classesLayerEntry.getSourceFiles().isEmpty()); + Assert.assertTrue(extraFilesLayerEntry.getSourceFiles().isEmpty()); + + List expectedLabels = new ArrayList<>(); + for (JavaLayerConfigurations.LayerType layerType : JavaLayerConfigurations.LayerType.values()) { + expectedLabels.add(layerType.getLabel()); + } + List actualLabels = new ArrayList<>(); + for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { + actualLabels.add(layerConfiguration.getLabel()); + } + Assert.assertEquals(expectedLabels, actualLabels); + } + + @Test + public void testSetFiles() { + List dependencyFiles = Collections.singletonList(Paths.get("dependency")); + List snapshotDependencyFiles = + Collections.singletonList(Paths.get("snapshot dependency")); + List resourceFiles = Collections.singletonList(Paths.get("resource")); + List classFiles = Collections.singletonList(Paths.get("class")); + List extraFiles = Collections.singletonList(Paths.get("extra file")); + + JavaLayerConfigurations javaLayerConfigurations = + JavaLayerConfigurations.builder() + .setDependenciesFiles(dependencyFiles) + .setSnapshotDependenciesFiles(snapshotDependencyFiles) + .setResourcesFiles(resourceFiles) + .setClassesFiles(classFiles) + .setExtraFiles(extraFiles) + .build(); + + List> expectedFiles = + Arrays.asList( + dependencyFiles, snapshotDependencyFiles, resourceFiles, classFiles, extraFiles); + List> actualFiles = new ArrayList<>(); + for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { + actualFiles.add(layerConfiguration.getLayerEntries().get(0).getSourceFiles()); + } + Assert.assertEquals(expectedFiles, actualFiles); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 242593a07a..3c1111c3b1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; @@ -39,6 +40,7 @@ public class MainClassResolverTest { @Mock private JibLogger mockBuildLogger; @Mock private ProjectProperties mockProjectProperties; @Mock private HelpfulSuggestions mockHelpfulSuggestions; + @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); @@ -49,6 +51,8 @@ public void setup() { Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) .thenReturn(mockHelpfulSuggestions); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); + Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) + .thenReturn(mockJavaLayerConfigurations); } @Test @@ -63,7 +67,7 @@ public void testResolveMainClass() throws MainClassInferenceException { @Test public void testResolveMainClass_notValid() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) .thenReturn(new LayerEntry(fakeClassesPath, "ignored")); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); @@ -74,7 +78,7 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { public void testResolveMainClass_multipleInferredWithBackup() throws MainClassInferenceException, URISyntaxException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) .thenReturn( new LayerEntry( ImmutableList.of( @@ -94,7 +98,7 @@ public void testResolveMainClass_multipleInferredWithBackup() @Test public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) .thenReturn( new LayerEntry( ImmutableList.of( @@ -120,7 +124,7 @@ public void testResolveMainClass_multipleInferredWithoutBackup() throws URISynta @Test public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); @@ -129,11 +133,12 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere @Test public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockProjectProperties.getClassesLayerEntry()) + Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); + } catch (MainClassInferenceException ex) { Mockito.verify(mockProjectProperties) .getMainClassHelpfulSuggestions("Main class was not found"); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 9aba8f57df..37469bf578 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -114,12 +114,7 @@ public void generateDockerContext() { // here. ExposedPortsParser.parse(jibExtension.getExposedPorts()); - new DockerContextGenerator( - gradleProjectProperties.getDependenciesLayerEntry(), - gradleProjectProperties.getSnapshotDependenciesLayerEntry(), - gradleProjectProperties.getResourcesLayerEntry(), - gradleProjectProperties.getClassesLayerEntry(), - gradleProjectProperties.getExtraFilesLayerEntry()) + new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) .setBaseImage(jibExtension.getBaseImage()) .setJvmFlags(jibExtension.getJvmFlags()) .setMainClass(mainClass) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 8d609089a0..d4534aa4af 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -16,10 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -34,29 +31,22 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; -/** Builds {@link LayerConfiguration}s based on inputs from a {@link Project}. */ +/** Builds {@link JavaLayerConfigurations} based on inputs from a {@link Project}. */ class GradleLayerConfigurations { /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; - private static final String DEPENDENCIES_LAYER_LABEL = "dependencies"; - private static final String SNAPSHOT_DEPENDENCIES_LAYER_LABEL = "snapshot dependencies"; - private static final String RESOURCES_LAYER_LABEL = "resources"; - private static final String CLASSES_LAYER_LABEL = "classes"; - private static final String EXTRA_FILES_LAYER_LABEL = "extra files"; - /** * Resolves the source files configuration for a Gradle {@link Project}. * * @param project the Gradle {@link Project} * @param gradleJibLogger the build logger for providing feedback about the resolution * @param extraDirectory path to the directory for the extra files layer - * @return a {@link GradleLayerConfigurations} for building the layers for the Gradle {@link - * Project} + * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ - static GradleLayerConfigurations getForProject( + static JavaLayerConfigurations getForProject( Project project, GradleJibLogger gradleJibLogger, Path extraDirectory) throws IOException { JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); @@ -124,79 +114,14 @@ static GradleLayerConfigurations getForProject( Collections.sort(classesFiles); Collections.sort(extraFiles); - return new GradleLayerConfigurations( - LayerConfiguration.builder() - .addEntry( - dependenciesFiles, JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .setLabel(DEPENDENCIES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry( - snapshotDependenciesFiles, - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .setLabel(SNAPSHOT_DEPENDENCIES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(resourcesFiles, JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE) - .setLabel(RESOURCES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(classesFiles, JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE) - .setLabel(CLASSES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(extraFiles, "/") - .setLabel(EXTRA_FILES_LAYER_LABEL) - .build()); - } - - private final LayerConfiguration dependenciesLayerConfiguration; - private final LayerConfiguration snapshotDependenciesLayerConfiguration; - private final LayerConfiguration resourcesLayerConfiguration; - private final LayerConfiguration classesLayerConfiguration; - private final LayerConfiguration extraFilesLayerConfiguration; - - // TODO: Consolidate with MavenLayerConfigurations. - /** Instantiate with {@link #getForProject}. */ - private GradleLayerConfigurations( - LayerConfiguration dependenciesLayerConfiguration, - LayerConfiguration snapshotDependenciesLayerConfiguration, - LayerConfiguration resourcesLayerConfiguration, - LayerConfiguration classesLayerConfiguration, - LayerConfiguration extraFilesLayerConfiguration) { - this.dependenciesLayerConfiguration = dependenciesLayerConfiguration; - this.snapshotDependenciesLayerConfiguration = snapshotDependenciesLayerConfiguration; - this.resourcesLayerConfiguration = resourcesLayerConfiguration; - this.classesLayerConfiguration = classesLayerConfiguration; - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; - } - - ImmutableList getLayerConfigurations() { - return ImmutableList.of( - dependenciesLayerConfiguration, - snapshotDependenciesLayerConfiguration, - resourcesLayerConfiguration, - classesLayerConfiguration, - extraFilesLayerConfiguration); - } - - LayerEntry getDependenciesLayerEntry() { - return dependenciesLayerConfiguration.getLayerEntries().get(0); + return JavaLayerConfigurations.builder() + .setDependenciesFiles(dependenciesFiles) + .setSnapshotDependenciesFiles(snapshotDependenciesFiles) + .setResourcesFiles(resourcesFiles) + .setClassesFiles(classesFiles) + .setExtraFiles(extraFiles) + .build(); } - LayerEntry getSnapshotDependenciesLayerEntry() { - return snapshotDependenciesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getResourcesLayerEntry() { - return resourcesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getClassesLayerEntry() { - return classesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getExtraFilesLayerEntry() { - return extraFilesLayerConfiguration.getLayerEntries().get(0); - } + private GradleLayerConfigurations() {} } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 32e32be4d5..8aa553db0a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -17,10 +17,9 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; @@ -28,7 +27,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -65,46 +63,21 @@ static GradleProjectProperties getForProject( private final Project project; private final GradleJibLogger gradleJibLogger; - private final GradleLayerConfigurations gradleLayerConfigurations; + private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( Project project, GradleJibLogger gradleJibLogger, - GradleLayerConfigurations gradleLayerConfigurations) { + JavaLayerConfigurations javaLayerConfigurations) { this.project = project; this.gradleJibLogger = gradleJibLogger; - this.gradleLayerConfigurations = gradleLayerConfigurations; + this.javaLayerConfigurations = javaLayerConfigurations; } @Override - public ImmutableList getLayerConfigurations() { - return gradleLayerConfigurations.getLayerConfigurations(); - } - - @Override - public LayerEntry getDependenciesLayerEntry() { - return gradleLayerConfigurations.getDependenciesLayerEntry(); - } - - @Override - public LayerEntry getSnapshotDependenciesLayerEntry() { - return gradleLayerConfigurations.getSnapshotDependenciesLayerEntry(); - } - - @Override - public LayerEntry getResourcesLayerEntry() { - return gradleLayerConfigurations.getResourcesLayerEntry(); - } - - @Override - public LayerEntry getClassesLayerEntry() { - return gradleLayerConfigurations.getClassesLayerEntry(); - } - - @Override - public LayerEntry getExtraFilesLayerEntry() { - return gradleLayerConfigurations.getExtraFilesLayerEntry(); + public JavaLayerConfigurations getJavaLayerConfigurations() { + return javaLayerConfigurations; } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 23d4f926e3..3e91b8a5a0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -97,7 +97,8 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations(projectProperties.getLayerConfigurations()); + .setLayerConfigurations( + projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(projectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 2adb007eac..c3e2fdd964 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -74,10 +75,8 @@ public Set getFiles() { @Mock private SourceSetOutput mockMainSourceSetOutput; @Mock private GradleJibLogger mockGradleJibLogger; - private GradleLayerConfigurations testGradleLayerConfigurations; - @Before - public void setUp() throws URISyntaxException, IOException { + public void setUp() throws URISyntaxException { Set classesFiles = ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI()).toFile()); FileCollection classesFileCollection = new TestFileCollection(classesFiles); @@ -109,14 +108,10 @@ public void setUp() throws URISyntaxException, IOException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - - testGradleLayerConfigurations = - GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); } @Test - public void test_correctFiles() throws URISyntaxException { + public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedDependenciesFiles = ImmutableList.of( Paths.get( @@ -140,21 +135,21 @@ public void test_correctFiles() throws URISyntaxException { Paths.get(Resources.getResource("application/classes").toURI()).resolve("some.class")); ImmutableList expectedExtraFiles = ImmutableList.of(); + JavaLayerConfigurations javaLayerConfigurations = + GradleLayerConfigurations.getForProject( + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); Assert.assertEquals( expectedDependenciesFiles, - testGradleLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); + javaLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( expectedSnapshotDependenciesFiles, - testGradleLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); + javaLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedResourcesFiles, - testGradleLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + expectedResourcesFiles, javaLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedClassesFiles, - testGradleLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + expectedClassesFiles, javaLayerConfigurations.getClassesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedExtraFiles, - testGradleLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); } @Test @@ -163,9 +158,8 @@ public void test_noClassesFiles() throws IOException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - testGradleLayerConfigurations = - GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); + GradleLayerConfigurations.getForProject( + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); Mockito.verify(mockGradleJibLogger) .warn("Could not find build output directory '" + nonexistentFile + "'"); @@ -177,7 +171,7 @@ public void test_noClassesFiles() throws IOException { public void test_extraFiles() throws URISyntaxException, IOException { Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - testGradleLayerConfigurations = + JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( mockProject, mockGradleJibLogger, extraFilesDirectory); @@ -188,24 +182,6 @@ public void test_extraFiles() throws URISyntaxException, IOException { Paths.get(Resources.getResource("layer/foo").toURI())); Assert.assertEquals( - expectedExtraFiles, - testGradleLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); - } - - @Test - public void test_correctPathsOnImage() { - Assert.assertEquals( - "/app/libs/", - testGradleLayerConfigurations.getDependenciesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/libs/", - testGradleLayerConfigurations.getSnapshotDependenciesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/resources/", - testGradleLayerConfigurations.getResourcesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/classes/", testGradleLayerConfigurations.getClassesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/", testGradleLayerConfigurations.getExtraFilesLayerEntry().getExtractionPath()); + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 2bac31a16a..6957abf93b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; @@ -44,7 +45,7 @@ public class GradleProjectPropertiesTest { @Mock private Project mockProject; @Mock private GradleJibLogger mockGradleJibLogger; @Mock private JibExtension mockJibExtension; - @Mock private GradleLayerConfigurations mockGradleLayerConfigurations; + @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -55,8 +56,7 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties( - mockProject, mockGradleJibLogger, mockGradleLayerConfigurations); + new GradleProjectProperties(mockProject, mockGradleJibLogger, mockJavaLayerConfigurations); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 010aa63da8..aedf4bc2c4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.docker.DockerContextGenerator; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -67,12 +67,7 @@ public void execute() throws MojoExecutionException { // here. ExposedPortsParser.parse(getExposedPorts()); - new DockerContextGenerator( - mavenProjectProperties.getDependenciesLayerEntry(), - mavenProjectProperties.getSnapshotDependenciesLayerEntry(), - mavenProjectProperties.getResourcesLayerEntry(), - mavenProjectProperties.getClassesLayerEntry(), - mavenProjectProperties.getExtraFilesLayerEntry()) + new JavaDockerContextGenerator(mavenProjectProperties.getJavaLayerConfigurations()) .setBaseImage(getBaseImage()) .setJvmFlags(getJvmFlags()) .setMainClass(mainClass) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 78f3e25411..c1047ce458 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -16,10 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -33,24 +30,18 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; -/** Builds {@link LayerConfiguration}s based on inputs from a {@link MavenProject}. */ +/** Builds {@link JavaLayerConfigurations} based on inputs from a {@link MavenProject}. */ class MavenLayerConfigurations { - private static final String DEPENDENCIES_LAYER_LABEL = "dependencies"; - private static final String SNAPSHOT_DEPENDENCIES_LAYER_LABEL = "snapshot dependencies"; - private static final String RESOURCES_LAYER_LABEL = "resources"; - private static final String CLASSES_LAYER_LABEL = "classes"; - private static final String EXTRA_FILES_LAYER_LABEL = "extra files"; - /** * Resolves the source files configuration for a {@link MavenProject}. * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer - * @return a new {@link MavenLayerConfigurations} for the project + * @return a {@link JavaLayerConfigurations} for the project * @throws IOException if collecting the project files fails */ - static MavenLayerConfigurations getForProject(MavenProject project, Path extraDirectory) + static JavaLayerConfigurations getForProject(MavenProject project, Path extraDirectory) throws IOException { Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory()); Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); @@ -112,83 +103,14 @@ static MavenLayerConfigurations getForProject(MavenProject project, Path extraDi Collections.sort(classesFiles); Collections.sort(extraFiles); - return new MavenLayerConfigurations( - LayerConfiguration.builder() - .addEntry( - dependenciesFiles, JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .setLabel(DEPENDENCIES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry( - snapshotDependenciesFiles, - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .setLabel(SNAPSHOT_DEPENDENCIES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(resourcesFiles, JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE) - .setLabel(RESOURCES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(classesFiles, JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE) - .setLabel(CLASSES_LAYER_LABEL) - .build(), - LayerConfiguration.builder() - .addEntry(extraFiles, "/") - .setLabel(EXTRA_FILES_LAYER_LABEL) - .build()); - } - - private final LayerConfiguration dependenciesLayerConfiguration; - private final LayerConfiguration snapshotDependenciesLayerConfiguration; - private final LayerConfiguration resourcesLayerConfiguration; - private final LayerConfiguration classesLayerConfiguration; - private final LayerConfiguration extraFilesLayerConfiguration; - - /** Instantiate with {@link #getForProject}. */ - private MavenLayerConfigurations( - LayerConfiguration dependenciesLayerConfiguration, - LayerConfiguration snapshotDependenciesLayerConfiguration, - LayerConfiguration resourcesLayerConfiguration, - LayerConfiguration classesLayerConfiguration, - LayerConfiguration extraFilesLayerConfiguration) { - this.dependenciesLayerConfiguration = dependenciesLayerConfiguration; - this.snapshotDependenciesLayerConfiguration = snapshotDependenciesLayerConfiguration; - this.resourcesLayerConfiguration = resourcesLayerConfiguration; - this.classesLayerConfiguration = classesLayerConfiguration; - this.extraFilesLayerConfiguration = extraFilesLayerConfiguration; - } - - /** - * Gets the list of {@link LayerConfiguration}s to use to build the container image. - * - * @return the list of {@link LayerConfiguration}s - */ - ImmutableList getLayerConfigurations() { - return ImmutableList.of( - dependenciesLayerConfiguration, - snapshotDependenciesLayerConfiguration, - resourcesLayerConfiguration, - classesLayerConfiguration, - extraFilesLayerConfiguration); - } - - LayerEntry getDependenciesLayerEntry() { - return dependenciesLayerConfiguration.getLayerEntries().get(0); + return JavaLayerConfigurations.builder() + .setDependenciesFiles(dependenciesFiles) + .setSnapshotDependenciesFiles(snapshotDependenciesFiles) + .setResourcesFiles(resourcesFiles) + .setClassesFiles(classesFiles) + .setExtraFiles(extraFiles) + .build(); } - LayerEntry getSnapshotDependenciesLayerEntry() { - return snapshotDependenciesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getResourcesLayerEntry() { - return resourcesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getClassesLayerEntry() { - return classesLayerConfiguration.getLayerEntries().get(0); - } - - LayerEntry getExtraFilesLayerEntry() { - return extraFilesLayerConfiguration.getLayerEntries().get(0); - } + private MavenLayerConfigurations() {} } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index c742ba6571..6c63a63fe3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -17,16 +17,14 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -66,46 +64,21 @@ static MavenProjectProperties getForProject( private final MavenProject project; private final MavenJibLogger mavenJibLogger; - private final MavenLayerConfigurations mavenLayerConfigurations; + private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, MavenJibLogger mavenJibLogger, - MavenLayerConfigurations mavenLayerConfigurations) { + JavaLayerConfigurations javaLayerConfigurations) { this.project = project; this.mavenJibLogger = mavenJibLogger; - this.mavenLayerConfigurations = mavenLayerConfigurations; + this.javaLayerConfigurations = javaLayerConfigurations; } @Override - public ImmutableList getLayerConfigurations() { - return mavenLayerConfigurations.getLayerConfigurations(); - } - - @Override - public LayerEntry getDependenciesLayerEntry() { - return mavenLayerConfigurations.getDependenciesLayerEntry(); - } - - @Override - public LayerEntry getSnapshotDependenciesLayerEntry() { - return mavenLayerConfigurations.getSnapshotDependenciesLayerEntry(); - } - - @Override - public LayerEntry getResourcesLayerEntry() { - return mavenLayerConfigurations.getResourcesLayerEntry(); - } - - @Override - public LayerEntry getClassesLayerEntry() { - return mavenLayerConfigurations.getClassesLayerEntry(); - } - - @Override - public LayerEntry getExtraFilesLayerEntry() { - return mavenLayerConfigurations.getExtraFilesLayerEntry(); + public JavaLayerConfigurations getJavaLayerConfigurations() { + return javaLayerConfigurations; } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 4b16f0ba67..ca4d5866f8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -109,7 +109,8 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) - .setLayerConfigurations(projectProperties.getLayerConfigurations()); + .setLayerConfigurations( + projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); CacheConfiguration applicationLayersCacheConfiguration = CacheConfiguration.forPath(projectProperties.getCacheDirectory()); buildConfigurationBuilder.setApplicationLayersCacheConfiguration( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 572f3561c5..8d50790c9e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -45,10 +46,8 @@ public class MavenLayerConfigurationsTest { @Mock private MavenProject mockMavenProject; @Mock private Build mockBuild; - private MavenLayerConfigurations testMavenLayerConfigurations; - @Before - public void setUp() throws IOException, URISyntaxException { + public void setUp() throws URISyntaxException { Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); @@ -65,13 +64,10 @@ public void setUp() throws IOException, URISyntaxException { testRepository.findArtifact("com.test", "dependency", "1.0.0"), testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); - - testMavenLayerConfigurations = - MavenLayerConfigurations.getForProject(mockMavenProject, Paths.get("nonexistent/path")); } @Test - public void test_correctFiles() throws URISyntaxException { + public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedDependenciesFiles = // on windows, these files may be in a different order, so sort ImmutableList.sortedCopyOf( @@ -94,24 +90,25 @@ public void test_correctFiles() throws URISyntaxException { Paths.get(Resources.getResource("application/output/package").toURI()), Paths.get(Resources.getResource("application/output/some.class").toURI())); + JavaLayerConfigurations javaLayerConfigurations = + MavenLayerConfigurations.getForProject(mockMavenProject, Paths.get("nonexistent/path")); Assert.assertEquals( expectedDependenciesFiles, - testMavenLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); + javaLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( expectedSnapshotDependenciesFiles, - testMavenLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); + javaLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedResourcesFiles, - testMavenLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + expectedResourcesFiles, javaLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); Assert.assertEquals( - expectedClassesFiles, testMavenLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + expectedClassesFiles, javaLayerConfigurations.getClassesLayerEntry().getSourceFiles()); } @Test public void test_extraFiles() throws URISyntaxException, IOException { Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - testMavenLayerConfigurations = + JavaLayerConfigurations javaLayerConfigurations = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory); ImmutableList expectedExtraFiles = @@ -121,24 +118,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { Paths.get(Resources.getResource("layer/foo").toURI())); Assert.assertEquals( - expectedExtraFiles, - testMavenLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); - } - - @Test - public void test_correctPathsOnImage() { - Assert.assertEquals( - "/app/libs/", testMavenLayerConfigurations.getDependenciesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/libs/", - testMavenLayerConfigurations.getSnapshotDependenciesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/resources/", - testMavenLayerConfigurations.getResourcesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/app/classes/", testMavenLayerConfigurations.getClassesLayerEntry().getExtractionPath()); - Assert.assertEquals( - "/", testMavenLayerConfigurations.getExtraFilesLayerEntry().getExtractionPath()); + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); } private Artifact makeArtifact(Path path) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 74af806cd4..f3e4d14392 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; @@ -34,7 +35,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; @Mock private MavenJibLogger mockMavenJibLogger; - @Mock private MavenLayerConfigurations mockMavenLayerConfigurations; + @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; private Xpp3Dom jarPluginConfiguration; @@ -50,7 +51,7 @@ public void setup() { Mockito.when(mockMavenProject.getVersion()).thenReturn("project-version"); mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockMavenJibLogger, mockMavenLayerConfigurations); + mockMavenProject, mockMavenJibLogger, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); From c32e8be57e5d9369c481f6f8e20fc1aac560d885 Mon Sep 17 00:00:00 2001 From: Hans <1850100+hpuac@users.noreply.github.com> Date: Tue, 7 Aug 2018 15:40:31 +0200 Subject: [PATCH 0111/2020] Fix grammar in README.md (#805) --- jib-gradle-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index c9693d5988..2ab2faafb6 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -4,7 +4,7 @@ # Jib - Containerize your Gradle Java project -Jib is [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. +Jib is a [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. For information about the project, see the [Jib project README](../README.md). For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). From c17381002bfbc4b0c0fac41544a9efcec4c7f00e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 Aug 2018 17:00:12 -0400 Subject: [PATCH 0112/2020] Do not put KEY into environment variable (#807) --- kokoro/continuous.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 33d2fd5b76..67535869f8 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -1,9 +1,6 @@ #!/bin/bash set -e - -echo ${JIB_INTEGRATION_TESTING_KEY} > ./keyfile.json - set -x gcloud components install docker-credential-gcr @@ -12,7 +9,7 @@ gcloud components install docker-credential-gcr export PATH=$PATH:/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/ # docker-credential-gcr uses GOOGLE_APPLICATION_CREDENTIALS as the credentials key file -export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/keyfile.json +export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_KEYSTORE_DIR}/72743_jib_integration_testing_key docker-credential-gcr configure-docker # Stops any left-over containers. From 232ef717ac65ff7c2b2c0032ae2ce9d5f43e3f56 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 8 Aug 2018 11:18:11 -0400 Subject: [PATCH 0113/2020] Fix parsing auth block in maven plugin (#811) --- .../jib/maven/JibPluginConfiguration.java | 48 +++++++++++-------- .../jib/maven/JibPluginConfigurationTest.java | 28 +++++++++-- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 13c033f27b..48fbb32b38 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -43,32 +43,17 @@ public static class AuthConfiguration implements AuthProperty { @Nullable @Parameter private String username; @Nullable @Parameter private String password; - private String usernameDescriptor; - private String passwordDescriptor; - - private AuthConfiguration(String descriptor) { - this.usernameDescriptor = descriptor + ""; - this.passwordDescriptor = descriptor + ""; - } - - @VisibleForTesting - void setUsername(String username) { - this.username = username; - } - - @VisibleForTesting - void setPassword(String password) { - this.password = password; - } + @Nullable private String usernameDescriptor; + @Nullable private String passwordDescriptor; @Override public String getUsernamePropertyDescriptor() { - return usernameDescriptor; + return Preconditions.checkNotNull(usernameDescriptor); } @Override public String getPasswordPropertyDescriptor() { - return passwordDescriptor; + return Preconditions.checkNotNull(passwordDescriptor); } @Override @@ -82,6 +67,21 @@ public String getUsername() { public String getPassword() { return password; } + + @VisibleForTesting + void setUsername(String username) { + this.username = username; + } + + @VisibleForTesting + void setPassword(String password) { + this.password = password; + } + + private void setPropertyDescriptors(String descriptorPrefix) { + this.usernameDescriptor = descriptorPrefix + ""; + this.passwordDescriptor = descriptorPrefix + ""; + } } /** @@ -96,7 +96,7 @@ public static class FromConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(""); + @Parameter private AuthConfiguration auth = new AuthConfiguration(); } /** Configuration for {@code to} parameter, where image is required. */ @@ -106,7 +106,7 @@ public static class ToConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(""); + @Parameter private AuthConfiguration auth = new AuthConfiguration(); public void set(String image) { this.image = image; @@ -168,6 +168,12 @@ public static class ContainerParameters { @Nullable @Component protected SettingsDecrypter settingsDecrypter; + /** Default constructor handles setting up auth property descriptors. */ + JibPluginConfiguration() { + to.auth.setPropertyDescriptors(""); + from.auth.setPropertyDescriptors(""); + } + /** * Warns about deprecated parameters in use. * diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 71de6176d8..01302a1917 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -20,6 +20,7 @@ import java.nio.file.Paths; import java.util.Arrays; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -32,14 +33,35 @@ public class JibPluginConfigurationTest { @Mock private JibLogger mockLogger; - @Test - public void testHandleDeprecatedParameters() { - JibPluginConfiguration testPluginConfiguration = + private JibPluginConfiguration testPluginConfiguration; + + @Before + public void setup() { + testPluginConfiguration = new JibPluginConfiguration() { @Override public void execute() {} }; + } + @Test + public void testAuthDefaults() { + Assert.assertEquals( + "", + testPluginConfiguration.getBaseImageAuth().getUsernamePropertyDescriptor()); + Assert.assertEquals( + "", + testPluginConfiguration.getBaseImageAuth().getPasswordPropertyDescriptor()); + Assert.assertEquals( + "", + testPluginConfiguration.getTargetImageAuth().getUsernamePropertyDescriptor()); + Assert.assertEquals( + "", + testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); + } + + @Test + public void testHandleDeprecatedParameters() { testPluginConfiguration.handleDeprecatedParameters(mockLogger); Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); From 189d9ccd3a154551e842b32ea1ddafe3856ac3ce Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 9 Aug 2018 10:57:28 -0400 Subject: [PATCH 0114/2020] Add a comprehensive maven integration test (#814) --- .../maven/BuildImageMojoIntegrationTest.java | 84 +++++++++++++++++++ .../cloud/tools/jib/maven/TestProject.java | 9 +- .../resources/projects/simple/auth/htpasswd | 2 + .../resources/projects/simple/pom-complex.xml | 77 +++++++++++++++++ .../src/main/java/com/test/HelloWorld.java | 6 ++ 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd create mode 100644 jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index a47e9cd74d..e36e5277ee 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -25,6 +25,7 @@ import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.maven.it.VerificationException; @@ -42,6 +43,10 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + @ClassRule + public static final TestProject complexTestProject = + new TestProject(testPlugin, "simple", "pom-complex.xml"); + @ClassRule public static final TestProject emptyTestProject = new TestProject(testPlugin, "empty"); @@ -192,4 +197,83 @@ public void testExecute_defaultTarget() { + "compile jib:build -Dimage=').")); } } + + @Test + public void testExecute_complex() + throws IOException, InterruptedException, VerificationException { + // Runs the Docker registry. + // TODO: Refactor into LocalRegistry + String containerName = "registry-" + UUID.randomUUID(); + new Command( + "docker", + "run", + "-d", + "-p", + "5000:5000", + "--restart=always", + "--name", + containerName, + "-v", + // Volume mount used for storing credentials + complexTestProject.getProjectRoot().resolve("auth") + ":/auth", + "-e", + "REGISTRY_AUTH=htpasswd", + "-e", + "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "-e", + "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", + "registry:2") + .run(); + + // Login in to push base image to local registry, then logout so we can test Jib's auth + try { + new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") + .run(); + new Command("docker", "pull", "gcr.io/distroless/java:latest").run(); + new Command( + "docker", "tag", "gcr.io/distroless/java:latest", "localhost:5000/distroless/java") + .run(); + new Command("docker", "push", "localhost:5000/distroless/java").run(); + new Command("docker", "logout", "localhost:5000").run(); + + // Run jib:build + Instant before = Instant.now(); + Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("-DsendCredentialsOverHttp=true"); + verifier.addCliOption("--file=pom-complex.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + // Verify output + new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") + .run(); + new Command("docker", "pull", "localhost:5000/complex-image").run(); + Assert.assertThat( + new Command("docker", "inspect", "localhost:5000/complex-image").run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", "localhost:5000/complex-image").run()); + Instant buildTime = + Instant.parse( + new Command("docker", "inspect", "-f", "{{.Created}}", "localhost:5000/complex-image") + .run() + .trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + new Command("docker", "logout", "localhost:5000").run(); + + } finally { + // Stops the local registry. + new Command("docker", "stop", containerName).run(); + new Command("docker", "rm", "-v", containerName).run(); + } + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index 38bcab034a..af17d18782 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -31,13 +31,20 @@ class TestProject extends TemporaryFolder implements Closeable { private final TestPlugin testPlugin; private final String projectDir; + private final String pomFilename; private Path projectRoot; /** Initialize to a specific project directory. */ TestProject(TestPlugin testPlugin, String projectDir) { + this(testPlugin, projectDir, "pom.xml"); + } + + /** Initialize to a specific project directory with a non-default pom.xml. */ + TestProject(TestPlugin testPlugin, String projectDir, String pomFilename) { this.testPlugin = testPlugin; this.projectDir = projectDir; + this.pomFilename = pomFilename; } Path getProjectRoot() { @@ -58,7 +65,7 @@ private void copyProject() throws IOException { .toPath(); // Puts the correct plugin version into the test project pom.xml. - Path pomXml = projectRoot.resolve("pom.xml"); + Path pomXml = projectRoot.resolve(pomFilename); Files.write( pomXml, new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) diff --git a/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd b/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd new file mode 100644 index 0000000000..73bbcb1eb5 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd @@ -0,0 +1,2 @@ +testuser:$2y$05$adsZRYZzCf/Hqp91zo4oJudcyT0wgNF9Z66zN1Yj0W6YZS0JFoOyG + diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml new file mode 100644 index 0000000000..1494531684 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + com.test + hello-world + 1 + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + localhost:5000/distroless/java + + testuser + testpassword + + + + localhost:5000/complex-image + + testuser + testpassword + + + + true + + An argument. + + com.test.HelloWorld + + -Xms512m + -Xdebug + + + 1000/tcp + 2000-2003/udp + + Docker + + true + true + + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 77a3cdc4a1..0bd8ae8d2f 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -18,6 +18,7 @@ import dependency.Greeting; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -44,5 +45,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); } + + // Prints jvm flags + for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + System.out.println(jvmFlag); + } } } From ebdd33fefe31f4634fc510feded6142cf96a15ca Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 9 Aug 2018 11:21:53 -0400 Subject: [PATCH 0115/2020] Adds link to Velocity talk/slides. (#816) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17e6c45007..121a739041 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Jib builds Docker and [OCI](https://github.com/opencontainers/image-spec) images *Jib as a container-building library for Java is work-in-progress. [Watch for updates.](https://github.com/GoogleContainerTools/jib/issues/337)* +For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). + ## Goals * **Fast** - Deploy your changes fast. Jib separates your application into multiple layers, splitting dependencies from classes. Now you don’t have to wait for Docker to rebuild your entire Java application - just deploy the layers that changed. @@ -39,7 +41,7 @@ See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). ## How Jib Works -Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html). +Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a similar existing container image build tool for the [Bazel build system](https://github.com/bazelbuild/bazel). From 46f1ea996af157c41dfc0b5305700cdd1d6ceb16 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 9 Aug 2018 13:21:48 -0400 Subject: [PATCH 0116/2020] Add comprehensive gradle integration test, add timestamped tag to complex maven test so image is unique (#817) --- .../jib/gradle/JibPluginIntegrationTest.java | 85 +++++++++++++++++++ .../resources/projects/simple/auth/htpasswd | 2 + .../projects/simple/complex-build.gradle | 44 ++++++++++ .../src/main/java/com/test/HelloWorld.java | 6 ++ .../maven/BuildImageMojoIntegrationTest.java | 15 ++-- .../resources/projects/simple/pom-complex.xml | 2 +- 6 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 60531bd6db..b39943d281 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.file.Files; import java.time.Instant; +import java.util.UUID; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -173,6 +174,90 @@ public void testBuild_defaultTarget() { } } + @Test + public void testBuild_complex() throws IOException, InterruptedException { + // Runs the Docker registry. + // TODO: Refactor into LocalRegistry + String containerName = "registry-" + UUID.randomUUID(); + new Command( + "docker", + "run", + "-d", + "-p", + "5000:5000", + "--restart=always", + "--name", + containerName, + "-v", + // Volume mount used for storing credentials + simpleTestProject.getProjectRoot().resolve("auth") + ":/auth", + "-e", + "REGISTRY_AUTH=htpasswd", + "-e", + "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "-e", + "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", + "registry:2") + .run(); + + // Login to push base image to local registry, then logout so we can test Jib's auth + try { + new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") + .run(); + new Command("docker", "pull", "gcr.io/distroless/java:latest").run(); + new Command( + "docker", "tag", "gcr.io/distroless/java:latest", "localhost:5000/distroless/java") + .run(); + new Command("docker", "push", "localhost:5000/distroless/java").run(); + new Command("docker", "logout", "localhost:5000").run(); + + String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); + + Instant beforeBuild = Instant.now(); + BuildResult buildResult = + simpleTestProject.build( + "clean", + JibPlugin.BUILD_IMAGE_TASK_NAME, + "-D_TARGET_IMAGE=" + targetImage, + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); + + new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") + .run(); + new Command("docker", "pull", targetImage).run(); + Assert.assertThat( + new Command("docker", "inspect", targetImage).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", targetImage).run()); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + new Command("docker", "logout", "localhost:5000").run(); + + } finally { + // Stops the local registry. + new Command("docker", "stop", containerName).run(); + new Command("docker", "rm", "-v", containerName).run(); + } + } + @Test public void testDockerDaemon_empty() throws IOException, InterruptedException { String targetImage = "emptyimage:gradle" + System.nanoTime(); diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd b/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd new file mode 100644 index 0000000000..73bbcb1eb5 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd @@ -0,0 +1,2 @@ +testuser:$2y$05$adsZRYZzCf/Hqp91zo4oJudcyT0wgNF9Z66zN1Yj0W6YZS0JFoOyG + diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle new file mode 100644 index 0000000000..e3642e0280 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = 'localhost:5000/distroless/java' + auth { + username = 'testuser' + password = 'testpassword' + } + } + to { + image = System.getProperty("_TARGET_IMAGE") + auth { + username = 'testuser' + password = 'testpassword' + } + } + container { + useCurrentTimestamp = true + args = ['An argument.'] + mainClass = 'com.test.HelloWorld' + jvmFlags = ['-Xms512m', '-Xdebug'] + ports = ['1000/tcp', '2000-2003/udp'] + } + allowInsecureRegistries = true + extraDirectory = file('src/main/custom-extra-dir') + + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 77a3cdc4a1..0bd8ae8d2f 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -18,6 +18,7 @@ import dependency.Greeting; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -44,5 +45,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); } + + // Prints jvm flags + for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + System.out.println(jvmFlag); + } } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index e36e5277ee..e8fe4ac61c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -201,6 +201,8 @@ public void testExecute_defaultTarget() { @Test public void testExecute_complex() throws IOException, InterruptedException, VerificationException { + String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); + // Runs the Docker registry. // TODO: Refactor into LocalRegistry String containerName = "registry-" + UUID.randomUUID(); @@ -225,7 +227,7 @@ public void testExecute_complex() "registry:2") .run(); - // Login in to push base image to local registry, then logout so we can test Jib's auth + // Login to push base image to local registry, then logout so we can test Jib's auth try { new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") .run(); @@ -239,6 +241,7 @@ public void testExecute_complex() // Run jib:build Instant before = Instant.now(); Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); verifier.addCliOption("-X"); verifier.addCliOption("-DsendCredentialsOverHttp=true"); @@ -249,9 +252,9 @@ public void testExecute_complex() // Verify output new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") .run(); - new Command("docker", "pull", "localhost:5000/complex-image").run(); + new Command("docker", "pull", targetImage).run(); Assert.assertThat( - new Command("docker", "inspect", "localhost:5000/complex-image").run(), + new Command("docker", "inspect", targetImage).run(), CoreMatchers.containsString( " \"ExposedPorts\": {\n" + " \"1000/tcp\": {},\n" @@ -261,12 +264,10 @@ public void testExecute_complex() + " \"2003/udp\": {}")); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", "localhost:5000/complex-image").run()); + new Command("docker", "run", targetImage).run()); Instant buildTime = Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", "localhost:5000/complex-image") - .run() - .trim()); + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); new Command("docker", "logout", "localhost:5000").run(); diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index 1494531684..e953679653 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -46,7 +46,7 @@ - localhost:5000/complex-image + ${_TARGET_IMAGE} testuser testpassword From 8a92c958b9de78acb79d6d5c71c9c5335c4011a5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 9 Aug 2018 17:05:44 -0400 Subject: [PATCH 0117/2020] Document adding extra files (#820) --- jib-gradle-plugin/README.md | 17 +++++++++++++++++ jib-maven-plugin/README.md | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 2ab2faafb6..e32dce6a33 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -234,6 +234,23 @@ jib { } ``` +### Adding Arbitrary Files to the Image + +*\* Note: this is an incubating feature and may change in the future.* + +You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). + +You can configure a different directory by using the `extraDirectory` parameter in your `build.gradle`: + +```groovy +jib { + ... + // Copies files from 'src/main/custom-extra-dir' instead of 'src/main/jib' + extraDirectory = file('src/main/custom-extra-dir') + ... +} +``` + ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 2833170932..38e256358a 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -285,6 +285,23 @@ In this configuration, the image: ``` +### Adding Arbitrary Files to the Image + +*\* Note: this is an incubating feature and may change in the future.* + +You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). + +You can configure a different directory by using the `extraDirectory` parameter in your `pom.xml`: + +```xml + + ... + // Copies files from 'src/main/custom-extra-dir' instead of 'src/main/jib' + ${project.basedir}/src/main/custom-extra-dir + ... + +``` + ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers) or [defined in your Maven settings](#using-maven-settings). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. From a0742fd277b5490a0965dba4443f5864f1250199 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 9 Aug 2018 18:58:21 -0400 Subject: [PATCH 0118/2020] Passthrough labels from base image (#804) * Passthrough labels from base image --- .../jib/builder/steps/BuildImageStep.java | 10 ++++---- .../google/cloud/tools/jib/image/Image.java | 24 +++++++++++++++---- .../jib/builder/steps/BuildImageStepTest.java | 16 ++++++++++--- .../image/json/ImageToJsonTranslatorTest.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 87da8a3c77..a5a2627b82 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -102,9 +103,10 @@ private Image afterCachedLayersSteps() imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); } - // Start with environment from base image and overlay build configuration - imageBuilder.addEnvironment( - NonBlockingSteps.get(pullBaseImageStep).getBaseImage().getEnvironment()); + // Parameters that we passthrough from the base image + Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); + imageBuilder.addEnvironment(baseImage.getEnvironment()); + imageBuilder.addLabels(baseImage.getLabels()); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); @@ -114,7 +116,7 @@ private Image afterCachedLayersSteps() imageBuilder.setEntrypoint(containerConfiguration.getEntrypoint()); imageBuilder.setJavaArguments(containerConfiguration.getProgramArguments()); imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); - imageBuilder.setLabels(containerConfiguration.getLabels()); + imageBuilder.addLabels(containerConfiguration.getLabels()); } // Gets the container configuration content descriptor. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 2619afaf88..30f6e409fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -32,12 +32,12 @@ public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); private ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); + private ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList javaArguments; @Nullable private ImmutableList exposedPorts; - @Nullable private ImmutableMap labels; /** * Sets the image creation time. @@ -109,13 +109,27 @@ public Builder setExposedPorts(@Nullable List exposedPorts) { } /** - * Sets the items in the "Labels" field in the container configuration. + * Add items to the "Labels" field in the container configuration. * * @param labels that map of labels to add * @return this */ - public Builder setLabels(@Nullable Map labels) { - this.labels = (labels == null) ? null : ImmutableMap.copyOf(labels); + public Builder addLabels(@Nullable Map labels) { + if (labels != null) { + labelsBuilder.putAll(labels); + } + return this; + } + + /** + * A an item to the "Labels" field in the container configuration. + * + * @param name that name of the label + * @param value the value of the label + * @return this + */ + public Builder addLabel(String name, String value) { + labelsBuilder.put(name, value); return this; } @@ -139,7 +153,7 @@ public Image build() { entrypoint, javaArguments, exposedPorts, - labels); + labelsBuilder.build()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index c8360db412..924b5a0ce8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -77,7 +77,10 @@ public void setUp() throws DigestException { Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); Image baseImage = - Image.builder().addEnvironment(ImmutableMap.of("NAME", "VALUE")).build(); + Image.builder() + .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE")) + .addLabel("base.label", "base.label.value") + .build(); Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) .thenReturn(Futures.immediateFuture(testCachedLayer)); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) @@ -116,7 +119,9 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup public void test_propagateBaseImageConfiguration() throws ExecutionException, InterruptedException { Mockito.when(mockContainerConfiguration.getEnvironmentMap()) - .thenReturn(ImmutableMap.of("BASE", "IMAGE")); + .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE")); + Mockito.when(mockContainerConfiguration.getLabels()) + .thenReturn(ImmutableMap.of("my.label", "my.label.value")); BuildImageStep buildImageStep = new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), @@ -128,6 +133,11 @@ public void test_propagateBaseImageConfiguration() mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep)); Image image = buildImageStep.getFuture().get().getFuture().get(); - Assert.assertEquals(ImmutableMap.of("NAME", "VALUE", "BASE", "IMAGE"), image.getEnvironment()); + Assert.assertEquals( + ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE"), + image.getEnvironment()); + Assert.assertEquals( + ImmutableMap.of("base.label", "base.label.value", "my.label", "my.label.value"), + image.getLabels()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 66945fbef4..00f9d7f770 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -65,7 +65,7 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP), new Port(3000, Protocol.UDP))); - testImageBuilder.setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); + testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1638e4f333..3dfd7afe54 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) + ### Changed - Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ca9147b957..e0d1c5ae51 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) + ### Changed - Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) From a990f4c9471a4a9e0b82bafd41bd6a9df9a88eb9 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 9 Aug 2018 23:59:36 -0400 Subject: [PATCH 0119/2020] Add FAQ entries for diagnosing connectivity issues (#770) --- CONTRIBUTING.md | 2 +- docs/faq.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e5e0fb75a..5ea9cf8d00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,6 +108,6 @@ Attach a debugger to a Gradle instance by running Gradle as follows: ```shell ./gradlew jib \ --no-daemon \ - -Dorg.gradle.jvmargs='-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5005,suspend=y' + -Dorg.gradle.jvmargs='-agentlib:jdwp:transport=dt_socket,server=y,address=5005,suspend=y' ``` diff --git a/docs/faq.md b/docs/faq.md index 8c8191da02..006dd943e8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -14,6 +14,10 @@ If a question you have is not answered below, please [submit an issue](/../../is [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ [Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ +[How do I configure a proxy?](#how-do-i-configure-a-proxy)\ +[How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ +[How can I examine network traffic?](#how-can-i-examine-network-traffic)\ +[How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ [How do I enable debugging?](#how-do-i-enable-debugging)\ [Why is my image created 48 years ago?](#why-is-my-image-created-48-years-ago)\ [I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ @@ -200,9 +204,60 @@ kubectl patch serviceaccount default \ See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). +### How do I configure a proxy? + +Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). + +### How can I diagnose problems pulling or pushing from remote registries? + +There are a few reasons why Jib may be unable to connect to a remote registry, including: + +- **Access requires a proxy.** See [_How do I configure a proxy?_](#how-do-i-configure-a-proxy) for details. +- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.8_). +- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.8_). +- **The registry does not support the [Docker Image Format V2 Schema 2](https://github.com/GoogleContainerTools/jib/issues/601)** (sometimes referred to as _v2-2_). This problem is usually shown by failures wth `INVALID_MANIFEST` errors. Some registries can be configured to support V2-2 such as [Artifactory](https://www.jfrog.com/confluence/display/RTF/Docker+Registry#DockerRegistry-LocalDockerRepositories) and [OpenShift](https://docs.openshift.com/container-platform/3.9/install_config/registry/extended_registry_configuration.html#middleware-repository-acceptschema2). Other registries, such as Quay.io/Quay Enterprise, are in the process of adding support. + +### How can I examine network traffic? + +It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jibSerialize` property. + +To see the HTTP traffic, create a `logging.properties` file with the following: +``` +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=ALL + +# CONFIG hides authentication data +# ALL includes authentication data +com.google.api.client.http.level=CONFIG +``` + +And then launch your build tool as follows: +```sh +mvn -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true ... +``` +or +```sh +gradle -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true ... +``` + +### How do I view debug logs for Jib? + +Maven: use `mvn -X -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. + +Gradle: use `grade --debug -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. + ### How do I enable debugging? -*TODO: Provide solution.* +If using the `distroless/java` base image, then use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept debug connections on port 5005, but not suspend: +``` +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 +``` + +Then connect your debugger to port 5005 on the given host. You can port-forward the container port to a localhost port for easy access. + +Using Docker: `docker run -p 5005:5005 ` + +Using Kubernetes: `kubectl port-forward 5005:5005` ### Why is my image created 48 years ago? From d2b027ff70ad5bc919a12f59d1bd11b0cfe16c27 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 10 Aug 2018 10:27:39 -0400 Subject: [PATCH 0120/2020] Allow LocalRegistry to require credentials (#818) --- .../com/google/cloud/tools/jib/Command.java | 5 + .../builder/BuildStepsIntegrationTest.java | 6 +- .../registry/BlobCheckerIntegrationTest.java | 7 +- .../registry/BlobPullerIntegrationTest.java | 7 +- .../registry/BlobPusherIntegrationTest.java | 4 +- .../tools/jib/registry/LocalRegistry.java | 136 ++++++++++++------ .../ManifestPullerIntegrationTest.java | 10 +- .../ManifestPusherIntegrationTest.java | 8 +- .../jib/gradle/JibPluginIntegrationTest.java | 121 ++++++---------- .../resources/projects/simple/auth/htpasswd | 2 - .../maven/BuildImageMojoIntegrationTest.java | 111 +++++--------- .../resources/projects/simple/auth/htpasswd | 2 - 12 files changed, 209 insertions(+), 210 deletions(-) delete mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd delete mode 100644 jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java index 9c10f9d4d3..02a9da20eb 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java @@ -35,6 +35,11 @@ public Command(String... command) { this.command = Arrays.asList(command); } + /** Instantiate with a command. */ + public Command(List command) { + this.command = command; + } + /** Runs the command. */ public String run() throws IOException, InterruptedException { return run(null); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index aa629169ec..2a36306bc5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -53,6 +53,8 @@ /** Integration tests for {@link BuildSteps}. */ public class BuildStepsIntegrationTest { + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + /** Lists the files in the {@code resourcePath} resources directory. */ private static ImmutableList getResourceFilesList(String resourcePath) throws URISyntaxException, IOException { @@ -62,8 +64,6 @@ private static ImmutableList getResourceFilesList(String resourcePath) } } - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final TestJibLogger logger = new TestJibLogger(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -103,7 +103,7 @@ public void testSteps_forBuildToDockerRegistry() logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); String imageReference = "localhost:5000/testimage:testtag"; - new Command("docker", "pull", imageReference).run(); + localRegistry.pull(imageReference); Assert.assertThat( new Command("docker", "inspect", imageReference).run(), CoreMatchers.containsString( diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index bb27fd4858..4ca75a8cee 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -32,7 +32,8 @@ public class BlobCheckerIntegrationTest { private static final EmptyJibLogger buildLogger = new EmptyJibLogger(); @Test - public void testCheck_exists() throws IOException, RegistryException { + public void testCheck_exists() throws IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = RegistryClient.factory(buildLogger, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -45,7 +46,9 @@ public void testCheck_exists() throws IOException, RegistryException { } @Test - public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { + public void testCheck_doesNotExist() + throws IOException, RegistryException, DigestException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = RegistryClient.factory(buildLogger, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 2e33057ba2..eb78e40f49 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -41,8 +41,9 @@ public class BlobPullerIntegrationTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void testPull() throws IOException, RegistryException { + public void testPull() throws IOException, RegistryException, InterruptedException { // Pulls the busybox image. + localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -61,7 +62,9 @@ public void testPull() throws IOException, RegistryException { } @Test - public void testPull_unknownBlob() throws RegistryException, IOException, DigestException { + public void testPull_unknownBlob() + throws RegistryException, IOException, DigestException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); DescriptorDigest nonexistentDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index e481689b23..cd7c352bf5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -33,7 +33,9 @@ public class BlobPusherIntegrationTest { private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test - public void testPush() throws DigestException, IOException, RegistryException { + public void testPush() + throws DigestException, IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); Blob testBlob = Blobs.from("crepecake"); // Known digest for 'crepecake' DescriptorDigest testBlobDigest = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 6b3759dffa..c8fe471f0f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -17,60 +17,88 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.Command; -import com.google.common.io.CharStreams; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; import java.util.UUID; +import javax.annotation.Nullable; import org.junit.rules.ExternalResource; -import org.junit.rules.TestRule; -/** {@link TestRule} that runs a local registry. */ +/** Runs a local registry. */ public class LocalRegistry extends ExternalResource { - private final int port; - - /** The name for the container running the registry. */ private final String containerName = "registry-" + UUID.randomUUID(); + private final int port; + @Nullable private final String username; + @Nullable private final String password; public LocalRegistry(int port) { + this(port, null, null); + } + + public LocalRegistry(int port, String username, String password) { this.port = port; + this.username = username; + this.password = password; } /** Starts the local registry. */ @Override - protected void before() throws Throwable { + protected void before() throws IOException, InterruptedException { // Runs the Docker registry. - new Command( - "docker", - "run", - "-d", - "-p", - port + ":5000", - "--restart=always", - "--name", - containerName, - "registry:2") - .run(); - - // Pulls 'busybox'. - new Command("docker", "pull", "busybox").run(); + ArrayList dockerTokens = + new ArrayList<>( + Arrays.asList( + "docker", + "run", + "-d", + "-p", + port + ":5000", + "--restart=always", + "--name", + containerName)); + if (username != null && password != null) { + // Generate the htpasswd file to store credentials + String credentialString = + new Command( + "docker", + "run", + "--entrypoint", + "htpasswd", + "registry:2", + "-Bbn", + username, + password) + .run(); + Path tempFolder = Files.createTempDirectory("auth"); + Files.write( + tempFolder.resolve("htpasswd"), credentialString.getBytes(StandardCharsets.UTF_8)); - // Tags 'busybox' to push to our local registry. - new Command("docker", "tag", "busybox", "localhost:" + port + "/busybox").run(); - - // Pushes 'busybox' to our local registry. - new Command("docker", "push", "localhost:" + port + "/busybox").run(); + // Run the Docker registry + dockerTokens.addAll( + Arrays.asList( + "-v", + // Volume mount used for storing credentials + tempFolder + ":/auth", + "-e", + "REGISTRY_AUTH=htpasswd", + "-e", + "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "-e", + "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd")); + } + dockerTokens.add("registry:2"); + new Command(dockerTokens).run(); } - /** Stops the local registry. */ @Override protected void after() { try { - // Stops the registry. + logout(); new Command("docker", "stop", containerName).run(); - - // Removes the container. new Command("docker", "rm", "-v", containerName).run(); } catch (InterruptedException | IOException ex) { @@ -78,16 +106,44 @@ protected void after() { } } - private void printLogs() throws IOException, InterruptedException { - Process process = Runtime.getRuntime().exec("docker logs " + containerName); - try (InputStreamReader inputStreamReader = - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { - System.out.println(CharStreams.toString(inputStreamReader)); + /** + * Pulls an image. + * + * @param from the image reference to pull + * @throws IOException if the pull command fails + * @throws InterruptedException if the pull command is interrupted + */ + public void pull(String from) throws IOException, InterruptedException { + login(); + new Command("docker", "pull", from).run(); + logout(); + } + + /** + * Pulls an image and pushes it to the local registry under a new tag. + * + * @param from the image reference to pull + * @param to the new location of the image (i.e. {@code localhost:[port]/[to]} + * @throws IOException if the commands fail + * @throws InterruptedException if the commands are interrupted + */ + public void pullAndPushToLocal(String from, String to) throws IOException, InterruptedException { + login(); + new Command("docker", "pull", from).run(); + new Command("docker", "tag", from, "localhost:" + port + "/" + to).run(); + new Command("docker", "push", "localhost:" + port + "/" + to).run(); + logout(); + } + + private void login() throws IOException, InterruptedException { + if (username != null && password != null) { + new Command("docker", "login", "localhost:" + port, "-u", username, "-p", password).run(); } - try (InputStreamReader inputStreamReader = - new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { - System.err.println(CharStreams.toString(inputStreamReader)); + } + + private void logout() throws IOException, InterruptedException { + if (username != null && password != null) { + new Command("docker", "logout", "localhost:" + port).run(); } - process.waitFor(); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index d61fdb7382..4f7ef6c60a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -33,7 +33,8 @@ public class ManifestPullerIntegrationTest { private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test - public void testPull_v21() throws IOException, RegistryException { + public void testPull_v21() throws IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -46,7 +47,8 @@ public void testPull_v21() throws IOException, RegistryException { } @Test - public void testPull_v22() throws IOException, RegistryException { + public void testPull_v22() throws IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); @@ -57,7 +59,9 @@ public void testPull_v22() throws IOException, RegistryException { } @Test - public void testPull_unknownManifest() throws RegistryException, IOException { + public void testPull_unknownManifest() + throws RegistryException, IOException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); try { RegistryClient registryClient = RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 5b52ec768f..5d4e204ab3 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -37,7 +37,9 @@ public class ManifestPusherIntegrationTest { private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); @Test - public void testPush_missingBlobs() throws IOException, RegistryException { + public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); + RegistryClient registryClient = RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); @@ -59,7 +61,9 @@ public void testPush_missingBlobs() throws IOException, RegistryException { /** Tests manifest pushing. This test is a comprehensive test of push and pull. */ @Test - public void testPush() throws DigestException, IOException, RegistryException { + public void testPush() + throws DigestException, IOException, RegistryException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); Blob testLayerBlob = Blobs.from("crepecake"); // Known digest for 'crepecake' DescriptorDigest testLayerBlobDigest = diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index b39943d281..8a24b76551 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -18,10 +18,10 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.file.Files; import java.time.Instant; -import java.util.UUID; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -34,6 +34,10 @@ /** Integration tests for {@link JibPlugin}. */ public class JibPluginIntegrationTest { + @ClassRule + public static final LocalRegistry localRegistry = + new LocalRegistry(5000, "testuser", "testpassword"); + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); @@ -176,86 +180,45 @@ public void testBuild_defaultTarget() { @Test public void testBuild_complex() throws IOException, InterruptedException { - // Runs the Docker registry. - // TODO: Refactor into LocalRegistry - String containerName = "registry-" + UUID.randomUUID(); - new Command( - "docker", - "run", - "-d", - "-p", - "5000:5000", - "--restart=always", - "--name", - containerName, - "-v", - // Volume mount used for storing credentials - simpleTestProject.getProjectRoot().resolve("auth") + ":/auth", - "-e", - "REGISTRY_AUTH=htpasswd", - "-e", - "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", - "-e", - "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", - "registry:2") - .run(); + String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); - // Login to push base image to local registry, then logout so we can test Jib's auth - try { - new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") - .run(); - new Command("docker", "pull", "gcr.io/distroless/java:latest").run(); - new Command( - "docker", "tag", "gcr.io/distroless/java:latest", "localhost:5000/distroless/java") - .run(); - new Command("docker", "push", "localhost:5000/distroless/java").run(); - new Command("docker", "logout", "localhost:5000").run(); - - String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); - - Instant beforeBuild = Instant.now(); - BuildResult buildResult = - simpleTestProject.build( - "clean", - JibPlugin.BUILD_IMAGE_TASK_NAME, - "-D_TARGET_IMAGE=" + targetImage, - "-DsendCredentialsOverHttp=true", - "-b=complex-build.gradle"); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); + // Pull distroless to local registry so we can test 'from' credentials + localRegistry.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") - .run(); - new Command("docker", "pull", targetImage).run(); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); - new Command("docker", "logout", "localhost:5000").run(); - - } finally { - // Stops the local registry. - new Command("docker", "stop", containerName).run(); - new Command("docker", "rm", "-v", containerName).run(); - } + Instant beforeBuild = Instant.now(); + BuildResult buildResult = + simpleTestProject.build( + "clean", + JibPlugin.BUILD_IMAGE_TASK_NAME, + "-D_TARGET_IMAGE=" + targetImage, + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); + + localRegistry.pull(targetImage); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", targetImage).run()); + Assert.assertThat( + new Command("docker", "inspect", targetImage).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @Test diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd b/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd deleted file mode 100644 index 73bbcb1eb5..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/auth/htpasswd +++ /dev/null @@ -1,2 +0,0 @@ -testuser:$2y$05$adsZRYZzCf/Hqp91zo4oJudcyT0wgNF9Z66zN1Yj0W6YZS0JFoOyG - diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index e8fe4ac61c..7c076fb8e2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -25,7 +26,6 @@ import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; -import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.maven.it.VerificationException; @@ -38,6 +38,10 @@ /** Integration tests for {@link BuildImageMojo}. */ public class BuildImageMojoIntegrationTest { + @ClassRule + public static final LocalRegistry localRegistry = + new LocalRegistry(5000, "testuser", "testpassword"); + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); @ClassRule @@ -203,78 +207,37 @@ public void testExecute_complex() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); - // Runs the Docker registry. - // TODO: Refactor into LocalRegistry - String containerName = "registry-" + UUID.randomUUID(); - new Command( - "docker", - "run", - "-d", - "-p", - "5000:5000", - "--restart=always", - "--name", - containerName, - "-v", - // Volume mount used for storing credentials - complexTestProject.getProjectRoot().resolve("auth") + ":/auth", - "-e", - "REGISTRY_AUTH=htpasswd", - "-e", - "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", - "-e", - "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", - "registry:2") - .run(); - - // Login to push base image to local registry, then logout so we can test Jib's auth - try { - new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") - .run(); - new Command("docker", "pull", "gcr.io/distroless/java:latest").run(); - new Command( - "docker", "tag", "gcr.io/distroless/java:latest", "localhost:5000/distroless/java") - .run(); - new Command("docker", "push", "localhost:5000/distroless/java").run(); - new Command("docker", "logout", "localhost:5000").run(); - - // Run jib:build - Instant before = Instant.now(); - Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.setAutoclean(false); - verifier.addCliOption("-X"); - verifier.addCliOption("-DsendCredentialsOverHttp=true"); - verifier.addCliOption("--file=pom-complex.xml"); - verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); - verifier.verifyErrorFreeLog(); - - // Verify output - new Command("docker", "login", "localhost:5000", "-u", "testuser", "-p", "testpassword") - .run(); - new Command("docker", "pull", targetImage).run(); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); - new Command("docker", "logout", "localhost:5000").run(); - - } finally { - // Stops the local registry. - new Command("docker", "stop", containerName).run(); - new Command("docker", "rm", "-v", containerName).run(); - } + // Pull distroless to local registry so we can test 'from' credentials + localRegistry.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + + // Run jib:build + Instant before = Instant.now(); + Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("-DsendCredentialsOverHttp=true"); + verifier.addCliOption("--file=pom-complex.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + // Verify output + localRegistry.pull(targetImage); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", targetImage).run()); + Assert.assertThat( + new Command("docker", "inspect", targetImage).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Instant buildTime = + Instant.parse( + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } } diff --git a/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd b/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd deleted file mode 100644 index 73bbcb1eb5..0000000000 --- a/jib-maven-plugin/src/test/resources/projects/simple/auth/htpasswd +++ /dev/null @@ -1,2 +0,0 @@ -testuser:$2y$05$adsZRYZzCf/Hqp91zo4oJudcyT0wgNF9Z66zN1Yj0W6YZS0JFoOyG - From 6b323fa9ff3b41205df4c0a91e7c112ab2f34a81 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 10 Aug 2018 14:13:23 -0400 Subject: [PATCH 0121/2020] Adds interface for CredentialRetriever. (#822) --- .../credentials/CredentialRetriever.java | 33 +++++++++++ .../credentials/Credentials.java | 56 +++++++++++++++++++ .../credentials/CredentialsTest.java | 44 +++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java new file mode 100644 index 0000000000..061369b3d0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration.credentials; + +import javax.annotation.Nullable; + +/** Retrieves credentials for a registry. */ +@FunctionalInterface +public interface CredentialRetriever { + + /** + * Fetches the credentials. Implementations must be thread-safe. + * + * @return the fetched credentials or {@code null} if no credentials could be fetched with this + * provider + */ + @Nullable + Credentials retrieve(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java new file mode 100644 index 0000000000..2e11ab07e8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration.credentials; + +import java.util.Objects; + +/** Holds credentials (username and password). */ +public class Credentials { + + private final String username; + private final String password; + + public Credentials(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Credentials)) { + return false; + } + Credentials otherCredentials = (Credentials) other; + return username.equals(otherCredentials.username) && password.equals(otherCredentials.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, password); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java new file mode 100644 index 0000000000..362e975b01 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration.credentials; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Credentials}. */ +public class CredentialsTest { + + @Test + public void testCredentialsHash() { + Credentials credentialsA1 = new Credentials("username", "password"); + Credentials credentialsA2 = new Credentials("username", "password"); + Credentials credentialsB1 = new Credentials("", ""); + Credentials credentialsB2 = new Credentials("", ""); + + Assert.assertEquals(credentialsA1, credentialsA2); + Assert.assertEquals(credentialsB1, credentialsB2); + Assert.assertNotEquals(credentialsA1, credentialsB1); + Assert.assertNotEquals(credentialsA1, credentialsB2); + + Set credentialsSet = + new HashSet<>(Arrays.asList(credentialsA1, credentialsA2, credentialsB1, credentialsB2)); + Assert.assertEquals(new HashSet<>(Arrays.asList(credentialsA2, credentialsB1)), credentialsSet); + } +} From 8dcc6dc191b0d58e830864fc4b5cec7bbf4a002a Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 10 Aug 2018 15:54:55 -0400 Subject: [PATCH 0122/2020] Auto build gradle lib-project dependencies (#821) * Auto build gradle lib-project dependencies Search through all configurations for dependencies of the style 'configurationName project(":path")' and have jib depend on the 'assemble' task of that project. This ensure jar dependencies are built before jib starts to package the project. --- jib-gradle-plugin/CHANGELOG.md | 2 + .../cloud/tools/jib/gradle/JibPlugin.java | 72 ++++++++++++----- .../cloud/tools/jib/gradle/JibPluginTest.java | 80 +++++++++++++++++++ 3 files changed, 136 insertions(+), 18 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3dfd7afe54..26958d78e8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Gradle project dependencies have their `assemble` task run before running a jib task ([#815](https://github.com/GoogleContainerTools/jib/issues/815)) + ## 0.9.8 ### Added diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 336c8e88be..c22ea8199a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -17,11 +17,17 @@ package com.google.cloud.tools.jib.gradle; import com.google.common.annotations.VisibleForTesting; +import java.util.List; +import java.util.stream.Collectors; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.plugins.BasePlugin; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { @@ -34,6 +40,42 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; + /** + * Collects all assemble tasks for project dependencies of the style "compile project(':mylib')" + * for any kind of configuration [compile, runtime, etc]. It potentially will collect common test + * libraries in configs like [test, integrationTest, etc], but it's either that or filter based on + * a configuration containing the word "test" which feels dangerous. + * + * @param project this project we are containerizing + * @return a list of "assemble" tasks associated with projects that this project depends on. + */ + @VisibleForTesting + static List getProjectDependencyAssembleTasks(Project project) { + return project + .getConfigurations() + .stream() + .map(Configuration::getDependencies) + .flatMap(DependencySet::stream) + .filter(ProjectDependency.class::isInstance) + .map(ProjectDependency.class::cast) + .map(ProjectDependency::getDependencyProject) + .map(subProject -> subProject.getTasks().getByPath(BasePlugin.ASSEMBLE_TASK_NAME)) + .collect(Collectors.toList()); + } + + private static void checkGradleVersion() { + if (GRADLE_MIN_VERSION.compareTo(GradleVersion.current()) > 0) { + throw new GradleException( + "Detected " + + GradleVersion.current() + + ", but jib requires " + + GRADLE_MIN_VERSION + + " or higher. You can upgrade by running 'gradle wrapper --gradle-version=" + + GRADLE_MIN_VERSION.getVersion() + + "'."); + } + } + @Override public void apply(Project project) { checkGradleVersion(); @@ -62,16 +104,23 @@ public void apply(Project project) { .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) .setJibExtension(jibExtension); - // Has all tasks depend on the 'classes' task. project.afterEvaluate( projectAfterEvaluation -> { try { + // Find project dependencies + List computedDependencies = + getProjectDependencyAssembleTasks(projectAfterEvaluation); + // Has all tasks depend on the 'classes' task. Task classesTask = projectAfterEvaluation.getTasks().getByPath("classes"); + computedDependencies.add(classesTask); - buildImageTask.dependsOn(classesTask); - dockerContextTask.dependsOn(classesTask); - buildDockerTask.dependsOn(classesTask); - buildTarTask.dependsOn(classesTask); + // dependsOn takes an Object... type + Object[] dependenciesArray = computedDependencies.toArray(); + + buildImageTask.dependsOn(dependenciesArray); + dockerContextTask.dependsOn(dependenciesArray); + buildDockerTask.dependsOn(dependenciesArray); + buildTarTask.dependsOn(dependenciesArray); } catch (UnknownTaskException ex) { throw new GradleException( @@ -82,17 +131,4 @@ public void apply(Project project) { } }); } - - private static void checkGradleVersion() { - if (GRADLE_MIN_VERSION.compareTo(GradleVersion.current()) > 0) { - throw new GradleException( - "Detected " - + GradleVersion.current() - + ", but jib requires " - + GRADLE_MIN_VERSION - + " or higher. You can upgrade by running 'gradle wrapper --gradle-version=" - + GRADLE_MIN_VERSION.getVersion() - + "'."); - } - } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index c92a4d7276..4e49a0aaa1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -16,10 +16,19 @@ package com.google.cloud.tools.jib.gradle; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.stream.Collectors; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.Assert; @@ -30,6 +39,12 @@ /** Tests for {@link JibPlugin}. */ public class JibPluginTest { + private static final ImmutableList KNOWN_JIB_TASKS = + ImmutableList.of( + JibPlugin.BUILD_IMAGE_TASK_NAME, + JibPlugin.BUILD_DOCKER_TASK_NAME, + JibPlugin.DOCKER_CONTEXT_TASK_NAME, + JibPlugin.BUILD_TAR_TASK_NAME); @Rule public TemporaryFolder testProjectRoot = new TemporaryFolder(); @Test @@ -68,4 +83,69 @@ public void testCheckGradleVersion_fail() throws IOException { + "'.")); } } + + @Test + public void testGetProjectDependencyAssembleTasks() { + // root project is our jib packaged service + Project rootProject = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + rootProject.getPluginManager().apply("java"); + + // our service DOES depend on this, and jib should trigger an assemble from this project + Project subProject = + ProjectBuilder.builder() + .withParent(rootProject) + .withProjectDir(testProjectRoot.getRoot()) + .withName("sub") + .build(); + subProject.getPluginManager().apply("java"); + + // our service doesn't depend on this, and jib should NOT trigger an assemble from this project + Project unrelatedSubProject = + ProjectBuilder.builder() + .withParent(rootProject) + .withProjectDir(testProjectRoot.getRoot()) + .withName("unrelated") + .build(); + unrelatedSubProject.getPluginManager().apply("java"); + + // equivalent of "compile project(':sub')" on the root(jib) project + rootProject + .getConfigurations() + .getByName("compile") + .getDependencies() + .add(rootProject.getDependencies().project(ImmutableMap.of("path", subProject.getPath()))); + + // programmatic check + Assert.assertEquals( + Collections.singletonList(":sub:assemble"), + JibPlugin.getProjectDependencyAssembleTasks(rootProject) + .stream() + .map(Task::getPath) + .collect(Collectors.toList())); + + // check by applying the jib plugin and inspect the task dependencies + rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + + // add a custom task that our jib tasks depend on to ensure we do not overwrite this dependsOn + Task dependencyTask = rootProject.getTasks().create("myCustomTask", task -> {}); + KNOWN_JIB_TASKS.forEach( + taskName -> rootProject.getTasks().getByPath(taskName).dependsOn(dependencyTask)); + + ((ProjectInternal) rootProject).evaluate(); + + KNOWN_JIB_TASKS.forEach( + taskName -> { + Assert.assertEquals( + ImmutableSet.of(":sub:assemble", ":classes", ":myCustomTask"), + rootProject + .getTasks() + .getByPath(taskName) + .getDependsOn() + .stream() + .map(Task.class::cast) + .map(Task::getPath) + .collect(Collectors.toSet())); + }); + } } From 5b75559e92f5516342bd793584e7ecd15caf93e9 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 10 Aug 2018 16:04:26 -0400 Subject: [PATCH 0123/2020] Adds list of CredentialRetrievers to BuildConfiguration. (#823) --- .../jib/configuration/ImageConfiguration.java | 33 ++++++++++++++++--- .../configuration/BuildConfigurationTest.java | 12 +++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index 532e30fbe8..d813bca9e8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -16,8 +16,12 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.util.List; import javax.annotation.Nullable; /** Immutable configuration options for an image reference with credentials. */ @@ -29,11 +33,12 @@ public static class Builder { private ImageReference imageReference; @Nullable private String credentialHelper; @Nullable private RegistryCredentials knownRegistryCredentials; + private ImmutableList credentialRetrievers = ImmutableList.of(); /** * Sets the credential helper name used for authenticating with the image's registry. * - * @param credentialHelper the credential helper's suffix. + * @param credentialHelper the credential helper's suffix * @return this */ public Builder setCredentialHelper(@Nullable String credentialHelper) { @@ -44,7 +49,7 @@ public Builder setCredentialHelper(@Nullable String credentialHelper) { /** * Sets known credentials used for authenticating with the image's registry. * - * @param knownRegistryCredentials the credentials. + * @param knownRegistryCredentials the credentials * @return this */ public Builder setKnownRegistryCredentials( @@ -53,13 +58,26 @@ public Builder setKnownRegistryCredentials( return this; } + /** + * Sets the providers for registry credentials. + * + * @param credentialRetrievers the list of {@link CredentialRetriever}s + * @return this + */ + public Builder setCredentialRetrievers(List credentialRetrievers) { + Preconditions.checkArgument(!credentialRetrievers.contains(null)); + this.credentialRetrievers = ImmutableList.copyOf(credentialRetrievers); + return this; + } + /** * Builds the {@link ImageConfiguration}. * * @return the corresponding {@link ImageConfiguration} */ public ImageConfiguration build() { - return new ImageConfiguration(imageReference, credentialHelper, knownRegistryCredentials); + return new ImageConfiguration( + imageReference, credentialHelper, knownRegistryCredentials, credentialRetrievers); } private Builder(ImageReference imageReference) { @@ -80,14 +98,17 @@ public static Builder builder(ImageReference imageReference) { private final ImageReference image; @Nullable private final String credentialHelper; @Nullable private final RegistryCredentials knownRegistryCredentials; + private final ImmutableList credentialRetrievers; private ImageConfiguration( ImageReference image, @Nullable String credentialHelper, - @Nullable RegistryCredentials knownRegistryCredentials) { + @Nullable RegistryCredentials knownRegistryCredentials, + ImmutableList credentialRetrievers) { this.image = image; this.credentialHelper = credentialHelper; this.knownRegistryCredentials = knownRegistryCredentials; + this.credentialRetrievers = credentialRetrievers; } public ImageReference getImage() { @@ -115,4 +136,8 @@ public String getCredentialHelper() { public RegistryCredentials getKnownRegistryCredentials() { return knownRegistryCredentials; } + + public ImmutableList getCredentialRetrievers() { + return credentialRetrievers; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index f47a825b0c..b93c3a4feb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port.Protocol; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.configuration.credentials.Credentials; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -51,6 +53,8 @@ public void testBuilder() { String expectedTargetImageCredentialHelperName = "anotherCredentialHelper"; RegistryCredentials expectedKnownTargetRegistryCredentials = Mockito.mock(RegistryCredentials.class); + List credentialRetrievers = + Collections.singletonList(() -> new Credentials("username", "password")); Instant expectedCreationTime = Instant.ofEpochSecond(10000); List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedJavaArguments = Arrays.asList("arg1", "arg2"); @@ -80,6 +84,7 @@ public void testBuilder() { expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .setCredentialHelper(expectedTargetImageCredentialHelperName) .setKnownRegistryCredentials(expectedKnownTargetRegistryCredentials) + .setCredentialRetrievers(credentialRetrievers) .build(); ContainerConfiguration containerConfiguration = ContainerConfiguration.builder() @@ -126,6 +131,13 @@ public void testBuilder() { Assert.assertEquals( expectedTargetImageCredentialHelperName, buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); + Assert.assertEquals( + new Credentials("username", "password"), + buildConfiguration + .getTargetImageConfiguration() + .getCredentialRetrievers() + .get(0) + .retrieve()); Assert.assertEquals( expectedJavaArguments, buildConfiguration.getContainerConfiguration().getProgramArguments()); From 4b809f062b048855757f400ac832195bb53e7811 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 13 Aug 2018 10:44:04 -0400 Subject: [PATCH 0124/2020] Add auth integration tests with different from/to registries and credentials (#831) --- .../jib/gradle/JibPluginIntegrationTest.java | 57 ++++++++++++++++++- .../projects/simple/complex-build.gradle | 4 +- .../maven/BuildImageMojoIntegrationTest.java | 55 ++++++++++++++++-- .../resources/projects/simple/pom-complex.xml | 4 +- 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 8a24b76551..2e49ef72e9 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -35,9 +35,13 @@ public class JibPluginIntegrationTest { @ClassRule - public static final LocalRegistry localRegistry = + public static final LocalRegistry localRegistry1 = new LocalRegistry(5000, "testuser", "testpassword"); + @ClassRule + public static final LocalRegistry localRegistry2 = + new LocalRegistry(6000, "testuser2", "testpassword2"); + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); @@ -180,10 +184,55 @@ public void testBuild_defaultTarget() { @Test public void testBuild_complex() throws IOException, InterruptedException { + String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); + + // Pull distroless to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + + Instant beforeBuild = Instant.now(); + BuildResult buildResult = + simpleTestProject.build( + "clean", + JibPlugin.BUILD_IMAGE_TASK_NAME, + "-D_TARGET_IMAGE=" + targetImage, + "-D_TARGET_USERNAME=testuser2", + "-D_TARGET_PASSWORD=testpassword2", + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); + + localRegistry2.pull(targetImage); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", targetImage).run()); + Assert.assertThat( + new Command("docker", "inspect", targetImage).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + } + + @Test + public void testBuild_complex_sameFromAndToRegistry() throws IOException, InterruptedException { String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); // Pull distroless to local registry so we can test 'from' credentials - localRegistry.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); Instant beforeBuild = Instant.now(); BuildResult buildResult = @@ -191,6 +240,8 @@ public void testBuild_complex() throws IOException, InterruptedException { "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-D_TARGET_IMAGE=" + targetImage, + "-D_TARGET_USERNAME=testuser", + "-D_TARGET_PASSWORD=testpassword", "-DsendCredentialsOverHttp=true", "-b=complex-build.gradle"); @@ -205,7 +256,7 @@ public void testBuild_complex() throws IOException, InterruptedException { buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); - localRegistry.pull(targetImage); + localRegistry1.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", new Command("docker", "run", targetImage).run()); diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index e3642e0280..554197c092 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -25,8 +25,8 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") auth { - username = 'testuser' - password = 'testpassword' + username = System.getProperty("_TARGET_USERNAME") + password = System.getProperty("_TARGET_PASSWORD") } } container { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 7c076fb8e2..35eed3b1a3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -39,9 +39,13 @@ public class BuildImageMojoIntegrationTest { @ClassRule - public static final LocalRegistry localRegistry = + public static final LocalRegistry localRegistry1 = new LocalRegistry(5000, "testuser", "testpassword"); + @ClassRule + public static final LocalRegistry localRegistry2 = + new LocalRegistry(6000, "testuser2", "testpassword2"); + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); @ClassRule @@ -205,24 +209,67 @@ public void testExecute_defaultTarget() { @Test public void testExecute_complex() throws IOException, InterruptedException, VerificationException { + String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); + + // Pull distroless to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + + // Run jib:build + Instant before = Instant.now(); + Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setSystemProperty("_TARGET_USERNAME", "testuser2"); + verifier.setSystemProperty("_TARGET_PASSWORD", "testpassword2"); + verifier.setSystemProperty("sendCredentialsOverHttp", "true"); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=pom-complex.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + // Verify output + localRegistry2.pull(targetImage); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + new Command("docker", "run", targetImage).run()); + Assert.assertThat( + new Command("docker", "inspect", targetImage).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Instant buildTime = + Instant.parse( + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + } + + @Test + public void testExecute_complex_sameFromAndToRegistry() + throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); // Pull distroless to local registry so we can test 'from' credentials - localRegistry.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); // Run jib:build Instant before = Instant.now(); Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setSystemProperty("_TARGET_USERNAME", "testuser"); + verifier.setSystemProperty("_TARGET_PASSWORD", "testpassword"); + verifier.setSystemProperty("sendCredentialsOverHttp", "true"); verifier.setAutoclean(false); verifier.addCliOption("-X"); - verifier.addCliOption("-DsendCredentialsOverHttp=true"); verifier.addCliOption("--file=pom-complex.xml"); verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); verifier.verifyErrorFreeLog(); // Verify output - localRegistry.pull(targetImage); + localRegistry1.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", new Command("docker", "run", targetImage).run()); diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index e953679653..76ff340f37 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -48,8 +48,8 @@ ${_TARGET_IMAGE} - testuser - testpassword + ${_TARGET_USERNAME} + ${_TARGET_PASSWORD} From 74b821f012d6c5f63d83ad1c901e96c46c99e252 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 13 Aug 2018 14:22:34 -0400 Subject: [PATCH 0125/2020] Move getGeneratedTargetDockerTag to ConfigurationPropertyValidator (#834) --- .../ConfigurationPropertyValidator.java | 36 ++++++++++++++++ .../plugins/common/HelpfulSuggestions.java | 28 ++++++++++++- .../plugins/common/BuildStepsRunnerTest.java | 5 ++- .../ConfigurationPropertyValidatorTest.java | 37 +++++++++++++++++ .../common/HelpfulSuggestionsTest.java | 8 +++- .../tools/jib/gradle/BuildDockerTask.java | 8 +++- .../cloud/tools/jib/gradle/BuildTarTask.java | 8 +++- .../jib/gradle/GradleProjectProperties.java | 33 --------------- .../gradle/HelpfulSuggestionsProvider.java | 5 ++- .../gradle/GradleProjectPropertiesTest.java | 29 ------------- .../tools/jib/maven/BuildDockerMojo.java | 41 +++++++++++-------- .../cloud/tools/jib/maven/BuildTarMojo.java | 41 +++++++++++-------- .../jib/maven/HelpfulSuggestionsProvider.java | 5 ++- .../jib/maven/MavenProjectProperties.java | 29 ------------- .../jib/maven/MavenProjectPropertiesTest.java | 25 ----------- 15 files changed, 183 insertions(+), 155 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 441c018013..a8a8b1d32d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; import java.util.function.Function; import javax.annotation.Nullable; @@ -107,5 +109,39 @@ public static Authorization getImageAuth( return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); } + /** + * Returns an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + * + * @param targetImage the configured target image reference + * @param logger the {@link JibLogger} used to show messages + * @param projectName the project name, as determined by the plugin + * @param projectVersion the project version, as determined by the plugin + * @param helpfulSuggestions used for generating the message notifying the user of the generated + * tag + * @return an {@link ImageReference} parsed from the configured target image, or one of the form + * {@code project-name:project-version} if target image is not configured + * @throws InvalidImageReferenceException if the configured or generated image reference is + * invalid + */ + public static ImageReference getGeneratedTargetDockerTag( + @Nullable String targetImage, + JibLogger logger, + String projectName, + String projectVersion, + HelpfulSuggestions helpfulSuggestions) + throws InvalidImageReferenceException { + if (Strings.isNullOrEmpty(targetImage)) { + logger.lifecycle(helpfulSuggestions.forGeneratedTag(projectName, projectVersion)); + + // Try to parse generated tag to verify that project name and version are valid (throws an + // exception if parse fails) + ImageReference.parse(projectName + ":" + projectVersion); + return ImageReference.of(null, projectName, projectVersion); + } else { + return ImageReference.parse(targetImage); + } + } + private ConfigurationPropertyValidator() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 6988db9269..2dd41232da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -28,6 +28,9 @@ public class HelpfulSuggestions { private final Function baseImageAuthConfiguration; private final String targetImageCredHelperConfiguration; private final Function targetImageAuthConfiguration; + private final String toImageConfiguration; + private final String buildConfigurationFilename; + private final String toImageFlag; /** * Creates a new {@link HelpfulSuggestions} with frontend-specific texts. @@ -42,6 +45,9 @@ public class HelpfulSuggestions { * for the target image * @param targetImageAuthConfiguration the way to define raw credentials for the target image - * takes the target image registry as an argument + * @param toImageConfiguration the configuration defining the target image + * @param toImageFlag the commandline flag used to set the target image + * @param buildConfigurationFilename the filename of the build configuration */ public HelpfulSuggestions( String messagePrefix, @@ -49,13 +55,19 @@ public HelpfulSuggestions( String baseImageCredHelperConfiguration, Function baseImageAuthConfiguration, String targetImageCredHelperConfiguration, - Function targetImageAuthConfiguration) { + Function targetImageAuthConfiguration, + String toImageConfiguration, + String toImageFlag, + String buildConfigurationFilename) { this.messagePrefix = messagePrefix; this.clearCacheCommand = clearCacheCommand; this.baseImageCredHelperConfiguration = baseImageCredHelperConfiguration; this.baseImageAuthConfiguration = baseImageAuthConfiguration; this.targetImageCredHelperConfiguration = targetImageCredHelperConfiguration; this.targetImageAuthConfiguration = targetImageAuthConfiguration; + this.toImageConfiguration = toImageConfiguration; + this.buildConfigurationFilename = buildConfigurationFilename; + this.toImageFlag = toImageFlag; } public String forHttpHostConnect() { @@ -135,6 +147,20 @@ public String forToNotConfigured(String parameter, String buildConfigFilename, S + "')."); } + public String forGeneratedTag(String projectName, String projectVersion) { + return "Tagging image with generated image reference " + + projectName + + ":" + + projectVersion + + ". If you'd like to specify a different tag, you can set the " + + toImageConfiguration + + " parameter in your " + + buildConfigurationFilename + + ", or use the " + + toImageFlag + + "= commandline flag."; + } + public String none() { return messagePrefix; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index e574a4eb5e..ae30f75bf1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -58,7 +58,10 @@ public class BuildStepsRunnerTest { "baseImageCredHelperConfiguration", registry -> "baseImageAuthConfiguration " + registry, "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry); + registry -> "targetImageAuthConfiguration " + registry, + "toConfig", + "toFlag", + "buildFile"); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index c628ebba23..2cf6401ed1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import org.junit.After; import org.junit.Assert; import org.junit.Test; @@ -123,4 +125,39 @@ public void testGetImageAuth() { Mockito.verify(mockLogger) .warn("user is missing from build configuration; ignoring auth section."); } + + @Test + public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceException { + HelpfulSuggestions helpfulSuggestions = + new HelpfulSuggestions( + "", "", "", unused -> "", "", unused -> "", "to", "--to", "build.txt"); + + // Target configured + ImageReference result = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + "a/b:c", mockLogger, "project-name", "project-version", helpfulSuggestions); + Assert.assertEquals("a/b", result.getRepository()); + Assert.assertEquals("c", result.getTag()); + Mockito.verify(mockLogger, Mockito.never()).lifecycle(Mockito.any()); + + // Target not configured + result = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + null, mockLogger, "project-name", "project-version", helpfulSuggestions); + Assert.assertEquals("project-name", result.getRepository()); + Assert.assertEquals("project-version", result.getTag()); + Mockito.verify(mockLogger) + .lifecycle( + "Tagging image with generated image reference project-name:project-version. If you'd " + + "like to specify a different tag, you can set the to parameter in your " + + "build.txt, or use the --to= commandline flag."); + + // Generated tag invalid + try { + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + null, mockLogger, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); + Assert.fail(); + } catch (InvalidImageReferenceException ignored) { + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 3d76e69c97..3ad46ae75c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -30,7 +30,10 @@ public class HelpfulSuggestionsTest { "baseImageCredHelperConfiguration", registry -> "baseImageAuthConfiguration " + registry, "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry); + registry -> "targetImageAuthConfiguration " + registry, + "toProperty", + "toFlag", + "buildFile"); @Test public void testSuggestions_smoke() { @@ -71,5 +74,8 @@ public void testSuggestions_smoke() { Assert.assertEquals( "messagePrefix, perhaps you should use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true", TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent()); + Assert.assertEquals( + "Tagging image with generated image reference project-name:project-version. If you'd like to specify a different tag, you can set the toProperty parameter in your buildFile, or use the toFlag= commandline flag.", + TEST_HELPFUL_SUGGESTIONS.forGeneratedTag("project-name", "project-version")); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2ecd16454d..84c53fc6b0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import javax.annotation.Nullable; @@ -77,7 +78,12 @@ public void buildDocker() throws InvalidImageReferenceException { getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); ImageReference targetImage = - gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + jibExtension.getTargetImage(), + gradleJibLogger, + getProject().getName(), + getProject().getVersion().toString(), + HELPFUL_SUGGESTIONS); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index c32b0cfabc..3829ae53e7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import java.nio.file.Paths; @@ -105,7 +106,12 @@ public void buildTar() throws InvalidImageReferenceException { getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); ImageReference targetImage = - gradleProjectProperties.getGeneratedTargetDockerTag(jibExtension, gradleJibLogger); + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + jibExtension.getTargetImage(), + gradleJibLogger, + getProject().getName(), + getProject().getVersion().toString(), + HELPFUL_SUGGESTIONS); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 8aa553db0a..7224362b64 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -18,15 +18,11 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -128,35 +124,6 @@ String getMainClass(JibExtension jibExtension) { } } - /** - * Returns an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - * - * @param jibExtension the plugin configuration parameters to generate the name from - * @param gradleJibLogger the logger used to notify users of the target image parameter - * @return an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - */ - ImageReference getGeneratedTargetDockerTag( - JibExtension jibExtension, GradleJibLogger gradleJibLogger) - throws InvalidImageReferenceException { - Preconditions.checkNotNull(jibExtension); - if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { - // TODO: Validate that project name and version are valid repository/tag - // TODO: Use HelpfulSuggestions - gradleJibLogger.lifecycle( - "Tagging image with generated image reference " - + project.getName() - + ":" - + project.getVersion().toString() - + ". If you'd like to specify a different tag, you can set the jib.to.image " - + "parameter in your build.gradle, or use the --image= commandline flag."); - return ImageReference.of(null, project.getName(), project.getVersion().toString()); - } else { - return ImageReference.parse(jibExtension.getTargetImage()); - } - } - /** * Returns the input files for a task. * diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java index 57fedd4e51..02c24f3ff6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java @@ -32,7 +32,10 @@ static HelpfulSuggestions get(String messagePrefix) { "from.credHelper", ignored -> "from.auth", "to.credHelper", - ignored -> "to.auth"); + ignored -> "to.auth", + "jib.to.image", + "--image", + "build.gradle"); } private HelpfulSuggestionsProvider() {} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 6957abf93b..d4ca36b881 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.Collections; @@ -44,7 +42,6 @@ public class GradleProjectPropertiesTest { @Mock private Jar mockJar2; @Mock private Project mockProject; @Mock private GradleJibLogger mockGradleJibLogger; - @Mock private JibExtension mockJibExtension; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private Manifest manifest; @@ -79,30 +76,4 @@ public void testGetMainClassFromJar_multiple() { .thenReturn(ImmutableSet.of(mockJar, mockJar2)); Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); } - - @Test - public void testGetDockerTag_configured() throws InvalidImageReferenceException { - Mockito.when(mockJibExtension.getTargetImage()).thenReturn("a/b:c"); - ImageReference result = - gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockGradleJibLogger); - Assert.assertEquals("a/b", result.getRepository()); - Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockGradleJibLogger, Mockito.never()).lifecycle(Mockito.any()); - } - - @Test - public void testGetDockerTag_notConfigured() throws InvalidImageReferenceException { - Mockito.when(mockProject.getName()).thenReturn("project-name"); - Mockito.when(mockProject.getVersion()).thenReturn("project-version"); - Mockito.when(mockJibExtension.getTargetImage()).thenReturn(null); - ImageReference result = - gradleProjectProperties.getGeneratedTargetDockerTag(mockJibExtension, mockGradleJibLogger); - Assert.assertEquals("project-name", result.getRepository()); - Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockGradleJibLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the jib.to.image parameter in your " - + "build.gradle, or use the --image= commandline flag."); - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 0419acd18a..eb17b42bea 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -21,8 +21,10 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import org.apache.maven.plugin.MojoExecutionException; @@ -55,28 +57,35 @@ public void execute() throws MojoExecutionException { MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - ImageReference targetImage = - mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); + try { + ImageReference targetImage = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + getTargetImage(), + mavenJibLogger, + getProject().getName(), + getProject().getVersion(), + HELPFUL_SUGGESTIONS); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - mavenJibLogger, this, mavenProjectProperties); + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + mavenJibLogger, this, mavenProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) + .build(); - try { BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); getLog().info(""); - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { + } catch (CacheDirectoryCreationException + | BuildStepsExecutionException + | InvalidImageReferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 5df28313e9..9524bea29a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -20,8 +20,10 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import java.nio.file.Paths; @@ -53,31 +55,38 @@ public void execute() throws MojoExecutionException { MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - ImageReference targetImage = - mavenProjectProperties.getGeneratedTargetDockerTag(getTargetImage(), mavenJibLogger); + try { + ImageReference targetImage = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + getTargetImage(), + mavenJibLogger, + getProject().getName(), + getProject().getVersion(), + HELPFUL_SUGGESTIONS); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - mavenJibLogger, this, mavenProjectProperties); + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + mavenJibLogger, this, mavenProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) + .build(); - try { BuildStepsRunner.forBuildTar( Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), buildConfiguration) .build(HELPFUL_SUGGESTIONS); getLog().info(""); - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { + } catch (CacheDirectoryCreationException + | BuildStepsExecutionException + | InvalidImageReferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java index a0f9124314..fe066fc5cc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java @@ -36,7 +36,10 @@ static HelpfulSuggestions get(String messagePrefix) { "", AUTH_CONFIGURATION_SUGGESTION, "", - AUTH_CONFIGURATION_SUGGESTION); + AUTH_CONFIGURATION_SUGGESTION, + "", + "-Dimage", + "pom.xml"); } private HelpfulSuggestionsProvider() {} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 6c63a63fe3..ffada9502f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -18,13 +18,11 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -146,31 +144,4 @@ String getMainClass(JibPluginConfiguration jibPluginConfiguration) throws MojoEx throw new MojoExecutionException(ex.getMessage(), ex); } } - - /** - * Returns an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - * - * @param targetImage the configured target image reference (can be empty) - * @param mavenJibLogger the logger used to notify users of the target image parameter - * @return an {@link ImageReference} parsed from the configured target image, or one of the form - * {@code project-name:project-version} if target image is not configured - */ - ImageReference getGeneratedTargetDockerTag( - @Nullable String targetImage, MavenJibLogger mavenJibLogger) { - if (Strings.isNullOrEmpty(targetImage)) { - // TODO: Validate that project name and version are valid repository/tag - // TODO: Use HelpfulSuggestions - mavenJibLogger.lifecycle( - "Tagging image with generated image reference " - + project.getName() - + ":" - + project.getVersion() - + ". If you'd like to specify a different tag, you can set the parameter " - + "in your pom.xml, or use the -Dimage= commandline flag."); - return ImageReference.of(null, project.getName(), project.getVersion()); - } else { - return PluginConfigurationProcessor.parseImageReference(targetImage, "to"); - } - } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index f3e4d14392..38804a180f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.ImageReference; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; @@ -47,8 +46,6 @@ public class MavenProjectPropertiesTest { @Before public void setup() { - Mockito.when(mockMavenProject.getName()).thenReturn("project-name"); - Mockito.when(mockMavenProject.getVersion()).thenReturn("project-version"); mavenProjectProperties = new MavenProjectProperties( mockMavenProject, mockMavenJibLogger, mockJavaLayerConfigurations); @@ -113,26 +110,4 @@ public void testGetMainClassFromJar_missingConfiguration() { public void testGetMainClassFromJar_missingPlugin() { Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } - - @Test - public void testGetDockerTag_configured() { - ImageReference result = - mavenProjectProperties.getGeneratedTargetDockerTag("a/b:c", mockMavenJibLogger); - Assert.assertEquals("a/b", result.getRepository()); - Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockMavenJibLogger, Mockito.never()).lifecycle(Mockito.any()); - } - - @Test - public void testGetDockerTag_notConfigured() { - ImageReference result = - mavenProjectProperties.getGeneratedTargetDockerTag(null, mockMavenJibLogger); - Assert.assertEquals("project-name", result.getRepository()); - Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockMavenJibLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the parameter in your " - + "pom.xml, or use the -Dimage= commandline flag."); - } } From d49a3425b0118c803b3f1a4221e60d8cc24cab3c Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 13 Aug 2018 15:01:42 -0400 Subject: [PATCH 0126/2020] Adds CredentialRetrieverFactory with initial one for Docker credential helpers. (#824) --- ...DockerCredentialHelperIntegrationTest.java | 9 +- .../RetrieveRegistryCredentialsStep.java | 6 +- .../{Credentials.java => Credential.java} | 10 +- .../credentials/CredentialRetriever.java | 7 +- .../frontend/CredentialRetrieverFactory.java | 105 ++++++++++++++++++ .../cloud/tools/jib/http/Authorization.java | 19 ++++ .../DockerConfigCredentialRetriever.java | 5 +- .../credentials/DockerCredentialHelper.java | 33 +++--- .../DockerCredentialHelperFactory.java | 15 ++- ...istentDockerCredentialHelperException.java | 6 +- ...verUrlDockerCredentialHelperException.java | 4 +- .../RetrieveRegistryCredentialsStepTest.java | 16 ++- .../configuration/BuildConfigurationTest.java | 8 +- ...edentialsTest.java => CredentialTest.java} | 26 ++--- .../CredentialRetrieverFactoryTest.java | 62 +++++++++++ .../DockerConfigCredentialRetrieverTest.java | 18 ++- .../credentials/DockerConfigTest.java | 38 +++---- 17 files changed, 304 insertions(+), 83 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/{Credentials.java => Credential.java} (81%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java rename jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/{CredentialsTest.java => CredentialTest.java} (52%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 265caa9a88..750ac14114 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -41,10 +41,9 @@ public void testRetrieveGCR() DockerCredentialHelper dockerCredentialHelper = new DockerCredentialHelperFactory().newDockerCredentialHelper("myregistry", "gcr"); - Authorization authorization = dockerCredentialHelper.retrieve(); - - // Checks that token received was base64 encoding of "myusername:mysecret". - Assert.assertEquals("bXl1c2VybmFtZTpteXNlY3JldA==", authorization.getToken()); + Credential credentials = dockerCredentialHelper.retrieve(); + Assert.assertEquals("myusername", credentials.getUsername()); + Assert.assertEquals("mysecret", credentials.getPassword()); } @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index e8d39454a8..7e49be35db 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -20,7 +20,9 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; @@ -196,10 +198,12 @@ Authorization retrieveFromCredentialHelper(String credentialHelperSuffix) buildLogger.info("Checking credentials from docker-credential-" + credentialHelperSuffix); try { - Authorization authorization = + Credential credential = dockerCredentialHelperFactory .newDockerCredentialHelper(registry, credentialHelperSuffix) .retrieve(); + Authorization authorization = + Authorizations.withBasicCredentials(credential.getUsername(), credential.getPassword()); logGotCredentialsFrom("docker-credential-" + credentialHelperSuffix); return authorization; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java similarity index 81% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index 2e11ab07e8..850ce70282 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credentials.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -19,12 +19,12 @@ import java.util.Objects; /** Holds credentials (username and password). */ -public class Credentials { +public class Credential { private final String username; private final String password; - public Credentials(String username, String password) { + public Credential(String username, String password) { this.username = username; this.password = password; } @@ -42,11 +42,11 @@ public boolean equals(Object other) { if (this == other) { return true; } - if (!(other instanceof Credentials)) { + if (!(other instanceof Credential)) { return false; } - Credentials otherCredentials = (Credentials) other; - return username.equals(otherCredentials.username) && password.equals(otherCredentials.password); + Credential otherCredential = (Credential) other; + return username.equals(otherCredential.username) && password.equals(otherCredential.password); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java index 061369b3d0..f4005dffaf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -25,9 +25,14 @@ public interface CredentialRetriever { /** * Fetches the credentials. Implementations must be thread-safe. * + *

Implementations should return {@code null} if no credentials could be fetched with this + * {@link CredentialRetriever} (and so other credential retrieval methods may be tried), or throw + * an exception something went wrong when fetching the credentials. + * * @return the fetched credentials or {@code null} if no credentials could be fetched with this * provider + * @throws Exception if the credential retrieval encountered an exception */ @Nullable - Credentials retrieve(); + Credential retrieve() throws Exception; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java new file mode 100644 index 0000000000..a22a2c30a4 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; +import com.google.common.annotations.VisibleForTesting; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** Static factories for various {@link CredentialRetriever}s. */ +public class CredentialRetrieverFactory { + + /** + * Creates a new {@link CredentialRetrieverFactory} for an image. + * + * @param imageReference the image the credential are for + * @param logger a logger for logging + * @return a new {@link CredentialRetrieverFactory} + */ + public static CredentialRetrieverFactory forImage( + ImageReference imageReference, JibLogger logger) { + return new CredentialRetrieverFactory(imageReference, logger); + } + + private final ImageReference imageReference; + private final JibLogger logger; + + private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logger) { + this.imageReference = imageReference; + this.logger = logger; + } + + /** + * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential + * helper, such as {@code docker-credential-gcr}. + * + * @param credentialHelperSuffix the credential helper executable suffix, following {@code + * docker-credential-} (ie. {@code gcr} for {@code docker-credential-gcr}) + * @return a new {@link CredentialRetriever} + */ + public CredentialRetriever dockerCredentialHelper(String credentialHelperSuffix) { + return dockerCredentialHelper( + Paths.get(DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix), + new DockerCredentialHelperFactory()); + } + + /** + * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential + * helper, such as {@code docker-credential-gcr}. + * + * @param credentialHelper the credential helper executable + * @return a new {@link CredentialRetriever} + * @see https://github.com/docker/docker-credential-helpers#development + */ + public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { + return dockerCredentialHelper(credentialHelper, new DockerCredentialHelperFactory()); + } + + @VisibleForTesting + CredentialRetriever dockerCredentialHelper( + Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) { + String registry = imageReference.getRegistry(); + + return () -> { + logger.info("Checking credentials from " + credentialHelper); + + try { + Credential credential = + dockerCredentialHelperFactory + .newDockerCredentialHelper(registry, credentialHelper) + .retrieve(); + logGotCredentialsFrom(credentialHelper.getFileName().toString()); + return credential; + + } catch (NonexistentServerUrlDockerCredentialHelperException ex) { + logger.info("No credentials for " + registry + " in " + credentialHelper); + return null; + } + }; + } + + private void logGotCredentialsFrom(String credentialSource) { + logger.info("Using " + credentialSource + " for " + imageReference.getRegistry()); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index f488439f75..bb5e76cc60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.http; +import java.util.Objects; + /** * Holds the credentials for an HTTP {@code Authorization} header. * @@ -46,4 +48,21 @@ public String getToken() { public String toString() { return scheme + " " + token; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Authorization)) { + return false; + } + Authorization otherAuthorization = (Authorization) other; + return scheme.equals(otherAuthorization.scheme) && token.equals(otherAuthorization.token); + } + + @Override + public int hashCode() { + return Objects.hash(scheme, token); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 648b95ff33..c27be07206 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry.credentials; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -121,7 +122,9 @@ private Authorization retrieve(DockerConfig dockerConfig, String registryAlias) if (dockerCredentialHelper != null) { try { // Tries with the given registry alias (may be the original registry). - return dockerCredentialHelper.retrieve(); + Credential credential = dockerCredentialHelper.retrieve(); + return Authorizations.withBasicCredentials( + credential.getUsername(), credential.getPassword()); } catch (IOException | NonexistentServerUrlDockerCredentialHelperException diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index e292867814..39fbfaf1f7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -18,8 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; @@ -29,6 +28,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import javax.annotation.Nullable; /** @@ -40,7 +40,7 @@ public class DockerCredentialHelper { private final String serverUrl; - private final String credentialHelperSuffix; + private final Path credentialHelper; /** Template for a Docker credential helper output. */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -54,30 +54,30 @@ private static class DockerCredentialsTemplate implements JsonTemplate { * Construct with {@link DockerCredentialHelperFactory}. * * @param serverUrl the server URL to pass into the credential helper - * @param credentialHelperSuffix the credential helper CLI suffix + * @param credentialHelper the path to the credential helper executable */ - DockerCredentialHelper(String serverUrl, String credentialHelperSuffix) { + DockerCredentialHelper(String serverUrl, Path credentialHelper) { this.serverUrl = serverUrl; - this.credentialHelperSuffix = credentialHelperSuffix; + this.credentialHelper = credentialHelper; } /** - * @return the Docker credentials by calling the corresponding CLI. - *

The credential helper CLI is called in the form: - *

{@code
+   * Calls the credential helper CLI in the form:
+   *
+   * 
{@code
    * echo -n  | docker-credential- get
    * }
* + * @return the Docker credentials by calling the corresponding CLI * @throws IOException if writing/reading process input/output fails. * @throws NonexistentServerUrlDockerCredentialHelperException if credentials are not found. * @throws NonexistentDockerCredentialHelperException if the credential helper CLI doesn't exist. */ - public Authorization retrieve() + public Credential retrieve() throws IOException, NonexistentServerUrlDockerCredentialHelperException, NonexistentDockerCredentialHelperException { try { - String credentialHelper = "docker-credential-" + credentialHelperSuffix; - String[] credentialHelperCommand = {credentialHelper, "get"}; + String[] credentialHelperCommand = {credentialHelper.toString(), "get"}; Process process = new ProcessBuilder(credentialHelperCommand).start(); try (OutputStream processStdin = process.getOutputStream()) { @@ -111,8 +111,7 @@ public Authorization retrieve() credentialHelper, serverUrl, output); } - return Authorizations.withBasicCredentials( - dockerCredentials.Username, dockerCredentials.Secret); + return new Credential(dockerCredentials.Username, dockerCredentials.Secret); } catch (JsonProcessingException ex) { throw new NonexistentServerUrlDockerCredentialHelperException( @@ -128,7 +127,7 @@ public Authorization retrieve() // Checks if the failure is due to a nonexistent credential helper CLI. if (ex.getMessage().contains("No such file or directory") || ex.getMessage().contains("cannot find the file")) { - throw new NonexistentDockerCredentialHelperException(credentialHelperSuffix, ex); + throw new NonexistentDockerCredentialHelperException(credentialHelper, ex); } throw ex; @@ -141,7 +140,7 @@ String getServerUrl() { } @VisibleForTesting - String getCredentialHelperSuffix() { - return credentialHelperSuffix; + Path getCredentialHelper() { + return credentialHelper; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java index 6f901480d2..b38314e540 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java @@ -16,18 +16,29 @@ package com.google.cloud.tools.jib.registry.credentials; +import java.nio.file.Path; +import java.nio.file.Paths; + /** Factory class for constructing {@link DockerCredentialHelper}. */ public class DockerCredentialHelperFactory { + public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-"; + public DockerCredentialHelperFactory() {} /** * @param registry the {@code ServerURL} stored by the credential helper - * @param credentialHelperSuffix the suffix of the docker-credential-[suffix] command to be run. + * @param credentialHelper the path to the Docker credential helper executable - usually in the + * form docker-credential-[suffix] * @return a {@link DockerCredentialHelper} retrieved from the command. */ + public DockerCredentialHelper newDockerCredentialHelper(String registry, Path credentialHelper) { + return new DockerCredentialHelper(registry, credentialHelper); + } + public DockerCredentialHelper newDockerCredentialHelper( String registry, String credentialHelperSuffix) { - return new DockerCredentialHelper(registry, credentialHelperSuffix); + return new DockerCredentialHelper( + registry, Paths.get(CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java index 0f15a483eb..af6d13bb1e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java @@ -16,10 +16,12 @@ package com.google.cloud.tools.jib.registry.credentials; +import java.nio.file.Path; + /** Thrown because the requested credential helper CLI does not exist. */ public class NonexistentDockerCredentialHelperException extends Exception { - NonexistentDockerCredentialHelperException(String credentialHelperSuffix, Throwable cause) { - super("The system does not have docker-credential-" + credentialHelperSuffix + " CLI", cause); + NonexistentDockerCredentialHelperException(Path credentialHelper, Throwable cause) { + super("The system does not have " + credentialHelper + " CLI", cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java index aee3a4e2f5..1ce09c9bb7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java @@ -16,11 +16,13 @@ package com.google.cloud.tools.jib.registry.credentials; +import java.nio.file.Path; + /** Thrown because the credential helper does not have credentials for the specified server URL. */ public class NonexistentServerUrlDockerCredentialHelperException extends Exception { NonexistentServerUrlDockerCredentialHelperException( - String credentialHelper, String serverUrl, String credentialHelperOutput) { + Path credentialHelper, String serverUrl, String credentialHelperOutput) { super( "The credential helper (" + credentialHelper diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 6447f037bc..b83834fe00 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -18,7 +18,9 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; @@ -41,6 +43,10 @@ public class RetrieveRegistryCredentialsStepTest { private static final String FAKE_TARGET_REGISTRY = "someRegistry"; + private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); + private static final Authorization FAKE_AUTHORIZATION = + Authorizations.withBasicCredentials( + FAKE_CREDENTIAL.getUsername(), FAKE_CREDENTIAL.getPassword()); @Mock private ListeningExecutorService mockListeningExecutorService; @Mock private ImageConfiguration mockImageConfiguration; @@ -74,7 +80,7 @@ public class RetrieveRegistryCredentialsStepTest { public void setUpMocks() throws NonexistentServerUrlDockerCredentialHelperException, NonexistentDockerCredentialHelperException, IOException { - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(mockAuthorization); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); Mockito.when(mockNonexistentServerUrlDockerCredentialHelper.retrieve()) .thenThrow(mockNonexistentServerUrlDockerCredentialHelperException); Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) @@ -86,11 +92,11 @@ public void testCall_useCredentialHelper() throws IOException, NonexistentDockerCredentialHelperException { Mockito.when( mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "someRegistry", "someOtherCredentialHelper")) + FAKE_TARGET_REGISTRY, "someOtherCredentialHelper")) .thenReturn(mockDockerCredentialHelper); Assert.assertEquals( - mockAuthorization, + FAKE_AUTHORIZATION, makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someOtherCredentialHelper", null) .call()); @@ -118,7 +124,7 @@ public void testCall_useDockerConfig() // Credential helper does not have credentials. Mockito.when( mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "someRegistry", "someCredentialHelper")) + FAKE_TARGET_REGISTRY, "someCredentialHelper")) .thenReturn(mockNonexistentServerUrlDockerCredentialHelper); Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(mockAuthorization); @@ -144,7 +150,7 @@ public void testCall_inferCommonCredentialHelpers() .thenReturn(mockNonexistentDockerCredentialHelper); Assert.assertEquals( - mockAuthorization, + FAKE_AUTHORIZATION, makeRetrieveRegistryCredentialsStep("something.gcr.io", null, null).call()); Mockito.verify(mockBuildLogger).info("Using docker-credential-gcr for something.gcr.io"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index b93c3a4feb..ebaa7befb7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port.Protocol; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.configuration.credentials.Credentials; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -40,7 +40,7 @@ public class BuildConfigurationTest { @Test - public void testBuilder() { + public void testBuilder() throws Exception { String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; String expectedBaseImageTag = "baseimagetag"; @@ -54,7 +54,7 @@ public void testBuilder() { RegistryCredentials expectedKnownTargetRegistryCredentials = Mockito.mock(RegistryCredentials.class); List credentialRetrievers = - Collections.singletonList(() -> new Credentials("username", "password")); + Collections.singletonList(() -> new Credential("username", "password")); Instant expectedCreationTime = Instant.ofEpochSecond(10000); List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedJavaArguments = Arrays.asList("arg1", "arg2"); @@ -132,7 +132,7 @@ public void testBuilder() { expectedTargetImageCredentialHelperName, buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); Assert.assertEquals( - new Credentials("username", "password"), + new Credential("username", "password"), buildConfiguration .getTargetImageConfiguration() .getCredentialRetrievers() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java similarity index 52% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index 362e975b01..ef46f29e21 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -22,23 +22,23 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link Credentials}. */ -public class CredentialsTest { +/** Tests for {@link Credential}. */ +public class CredentialTest { @Test public void testCredentialsHash() { - Credentials credentialsA1 = new Credentials("username", "password"); - Credentials credentialsA2 = new Credentials("username", "password"); - Credentials credentialsB1 = new Credentials("", ""); - Credentials credentialsB2 = new Credentials("", ""); + Credential credentialA1 = new Credential("username", "password"); + Credential credentialA2 = new Credential("username", "password"); + Credential credentialB1 = new Credential("", ""); + Credential credentialB2 = new Credential("", ""); - Assert.assertEquals(credentialsA1, credentialsA2); - Assert.assertEquals(credentialsB1, credentialsB2); - Assert.assertNotEquals(credentialsA1, credentialsB1); - Assert.assertNotEquals(credentialsA1, credentialsB2); + Assert.assertEquals(credentialA1, credentialA2); + Assert.assertEquals(credentialB1, credentialB2); + Assert.assertNotEquals(credentialA1, credentialB1); + Assert.assertNotEquals(credentialA1, credentialB2); - Set credentialsSet = - new HashSet<>(Arrays.asList(credentialsA1, credentialsA2, credentialsB1, credentialsB2)); - Assert.assertEquals(new HashSet<>(Arrays.asList(credentialsA2, credentialsB1)), credentialsSet); + Set credentialSet = + new HashSet<>(Arrays.asList(credentialA1, credentialA2, credentialB1, credentialB2)); + Assert.assertEquals(new HashSet<>(Arrays.asList(credentialA2, credentialB1)), credentialSet); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java new file mode 100644 index 0000000000..0ffb9e3c57 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link CredentialRetrieverFactory}. */ +@RunWith(MockitoJUnitRunner.class) +public class CredentialRetrieverFactoryTest { + + @Mock private JibLogger mockJibLogger; + @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private DockerCredentialHelper mockDockerCredentialHelper; + + @Test + public void testDockerCredentialHelper() throws Exception { + CredentialRetrieverFactory credentialProviderFactory = + CredentialRetrieverFactory.forImage( + ImageReference.of("registry", null, null), mockJibLogger); + Credential expectedCredential = new Credential("username", "password"); + + Mockito.when( + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "registry", Paths.get("docker-credential-helper"))) + .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(expectedCredential); + + Assert.assertEquals( + expectedCredential, + credentialProviderFactory + .dockerCredentialHelper( + Paths.get("docker-credential-helper"), mockDockerCredentialHelperFactory) + .retrieve()); + + Mockito.verify(mockJibLogger).info("Using docker-credential-helper for registry"); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 7f3ed392a5..66f2c7f5dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -16,7 +16,9 @@ package com.google.cloud.tools.jib.registry.credentials; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -34,7 +36,11 @@ @RunWith(MockitoJUnitRunner.class) public class DockerConfigCredentialRetrieverTest { - @Mock private Authorization mockAuthorization; + private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); + private static final Authorization FAKE_AUTHORIZATION = + Authorizations.withBasicCredentials( + FAKE_CREDENTIAL.getUsername(), FAKE_CREDENTIAL.getPassword()); + @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @@ -46,7 +52,7 @@ public void setUp() NonexistentDockerCredentialHelperException, IOException { dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(mockAuthorization); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); } @Test @@ -79,7 +85,7 @@ public void testRetrieve_useCredsStore() throws IOException { "just registry", dockerConfigFile, mockDockerCredentialHelperFactory); Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertEquals(FAKE_AUTHORIZATION, authorization); } @Test @@ -94,7 +100,7 @@ public void testRetrieve_useCredsStore_withProtocol() throws IOException { "with.protocol", dockerConfigFile, mockDockerCredentialHelperFactory); Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertEquals(FAKE_AUTHORIZATION, authorization); } @Test @@ -109,7 +115,7 @@ public void testRetrieve_useCredHelper() throws IOException { "another registry", dockerConfigFile, mockDockerCredentialHelperFactory); Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertEquals(FAKE_AUTHORIZATION, authorization); } @Test @@ -132,7 +138,7 @@ public void testRetrieve_credentialFromAlias() throws IOException { "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(mockAuthorization, authorization); + Assert.assertEquals(FAKE_AUTHORIZATION, authorization); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index ce6f1e0052..b3eb529099 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -45,20 +45,20 @@ public void test_fromJson() throws URISyntaxException, IOException { Assert.assertNull(dockerConfig.getAuthFor("just registry")); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("some registry").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("some registry").getCredentialHelper()); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("some other registry").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("some other registry").getCredentialHelper()); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("just registry").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("just registry").getCredentialHelper()); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelper()); Assert.assertEquals( - "another credential helper", - dockerConfig.getCredentialHelperFor("another registry").getCredentialHelperSuffix()); + Paths.get("docker-credential-another credential helper"), + dockerConfig.getCredentialHelperFor("another registry").getCredentialHelper()); Assert.assertNull(dockerConfig.getCredentialHelperFor("unknonwn registry")); } @@ -96,8 +96,8 @@ public void testGetCredentialHelperFor() throws URISyntaxException, IOException new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("just registry").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("just registry").getCredentialHelper()); } @Test @@ -108,8 +108,8 @@ public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IO new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelper()); } @Test @@ -120,8 +120,8 @@ public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, I new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", - dockerConfig.getCredentialHelperFor("with.suffix").getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("with.suffix").getCredentialHelper()); } @Test @@ -133,10 +133,8 @@ public void testGetCredentialHelperFor_withProtocolAndSuffix() new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - "some credential store", - dockerConfig - .getCredentialHelperFor("with.protocol.and.suffix") - .getCredentialHelperSuffix()); + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("with.protocol.and.suffix").getCredentialHelper()); } @Test From 5344b59c3785a662d3240a4098426c0f28ec7b40 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 13 Aug 2018 15:35:56 -0400 Subject: [PATCH 0127/2020] Disable mount+from (#833) --- .../tools/jib/builder/steps/PushBlobStep.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 0bf10ab9b4..5fb0210d00 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -86,18 +86,8 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc return blobDescriptor; } - // If base and target images are in the same registry, then use mount/from to try mounting the - // BLOB from the base image repository to the target image repository and possibly avoid - // having to push the BLOB. See - // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. - boolean sameRegistry = - buildConfiguration - .getBaseImageConfiguration() - .getImageRegistry() - .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()); - String mountFrom = - sameRegistry ? buildConfiguration.getBaseImageConfiguration().getImageRepository() : null; - registryClient.pushBlob(blobDescriptor.getDigest(), blob, mountFrom); + // todo: leverage cross-repository mounts + registryClient.pushBlob(blobDescriptor.getDigest(), blob, null); return blobDescriptor; } From cb64ed4f5decf00b6a0a22fb742e5c31205d34b4 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 13 Aug 2018 17:59:18 -0400 Subject: [PATCH 0128/2020] Adds CredentialRetrieverFactory#inferCredentialHelper. (#825) --- .../frontend/CredentialRetrieverFactory.java | 93 +++++++++++++++++-- .../CredentialRetrieverFactoryTest.java | 60 +++++++++++- 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index a22a2c30a4..8d2b251101 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -21,14 +21,26 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; /** Static factories for various {@link CredentialRetriever}s. */ public class CredentialRetrieverFactory { + /** + * Defines common credential helpers to use as defaults. Maps from registry suffix to credential + * helper suffix. + */ + private static final ImmutableMap COMMON_CREDENTIAL_HELPERS = + ImmutableMap.of("gcr.io", "gcr", "amazonaws.com", "ecr-login"); + /** * Creates a new {@link CredentialRetrieverFactory} for an image. * @@ -41,14 +53,19 @@ public static CredentialRetrieverFactory forImage( return new CredentialRetrieverFactory(imageReference, logger); } - private final ImageReference imageReference; private final JibLogger logger; + private ImageReference imageReference; private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logger) { this.imageReference = imageReference; this.logger = logger; } + public CredentialRetrieverFactory setImageReference(ImageReference imageReference) { + this.imageReference = imageReference; + return this; + } + /** * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential * helper, such as {@code docker-credential-gcr}. @@ -76,29 +93,85 @@ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return dockerCredentialHelper(credentialHelper, new DockerCredentialHelperFactory()); } + /** + * Creates a new {@link CredentialRetriever} that tries common Docker credential helpers to + * retrieve credentials based on the registry of the image, such as {@code docker-credential-gcr} + * for images with the registry as {@code gcr.io}. + * + * @return a new {@link CredentialRetriever} + */ + public CredentialRetriever inferCredentialHelper() { + return inferCredentialHelper(new DockerCredentialHelperFactory()); + } + + @VisibleForTesting + CredentialRetriever inferCredentialHelper( + DockerCredentialHelperFactory dockerCredentialHelperFactory) { + List inferredCredentialHelperSuffixes = new ArrayList<>(); + for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) { + if (!imageReference.getRegistry().endsWith(registrySuffix)) { + continue; + } + String inferredCredentialHelperSuffix = COMMON_CREDENTIAL_HELPERS.get(registrySuffix); + if (inferredCredentialHelperSuffix == null) { + throw new IllegalStateException("No COMMON_CREDENTIAL_HELPERS should be null"); + } + inferredCredentialHelperSuffixes.add(inferredCredentialHelperSuffix); + } + + return () -> { + for (String inferredCredentialHelperSuffix : inferredCredentialHelperSuffixes) { + try { + return retrieveFromDockerCredentialHelper( + Paths.get( + DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + + inferredCredentialHelperSuffix), + dockerCredentialHelperFactory); + + } catch (NonexistentDockerCredentialHelperException ex) { + if (ex.getMessage() != null) { + // Warns the user that the specified (or inferred) credential helper is not on the + // system. + logger.warn(ex.getMessage()); + if (ex.getCause() != null && ex.getCause().getMessage() != null) { + logger.info(" Caused by: " + ex.getCause().getMessage()); + } + } + } + } + return null; + }; + } + @VisibleForTesting CredentialRetriever dockerCredentialHelper( Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) { - String registry = imageReference.getRegistry(); - return () -> { logger.info("Checking credentials from " + credentialHelper); try { - Credential credential = - dockerCredentialHelperFactory - .newDockerCredentialHelper(registry, credentialHelper) - .retrieve(); - logGotCredentialsFrom(credentialHelper.getFileName().toString()); - return credential; + return retrieveFromDockerCredentialHelper(credentialHelper, dockerCredentialHelperFactory); } catch (NonexistentServerUrlDockerCredentialHelperException ex) { - logger.info("No credentials for " + registry + " in " + credentialHelper); + logger.info( + "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); return null; } }; } + private Credential retrieveFromDockerCredentialHelper( + Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) + throws NonexistentServerUrlDockerCredentialHelperException, + NonexistentDockerCredentialHelperException, IOException { + Credential credentials = + dockerCredentialHelperFactory + .newDockerCredentialHelper(imageReference.getRegistry(), credentialHelper) + .retrieve(); + logGotCredentialsFrom(credentialHelper.getFileName().toString()); + return credentials; + } + private void logGotCredentialsFrom(String credentialSource) { logger.info("Using " + credentialSource + " for " + imageReference.getRegistry()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 0ffb9e3c57..d55945b042 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -21,8 +21,12 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; +import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; +import java.io.IOException; import java.nio.file.Paths; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -33,30 +37,76 @@ @RunWith(MockitoJUnitRunner.class) public class CredentialRetrieverFactoryTest { + private static final Credential FAKE_CREDENTIALS = new Credential("username", "password"); + @Mock private JibLogger mockJibLogger; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @Mock private DockerCredentialHelper mockDockerCredentialHelper; + @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; + + @Mock + private NonexistentDockerCredentialHelperException mockNonexistentDockerCredentialHelperException; + + @Before + public void setUp() + throws NonexistentServerUrlDockerCredentialHelperException, + NonexistentDockerCredentialHelperException, IOException { + Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); + Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) + .thenThrow(mockNonexistentDockerCredentialHelperException); + } @Test public void testDockerCredentialHelper() throws Exception { - CredentialRetrieverFactory credentialProviderFactory = + CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( ImageReference.of("registry", null, null), mockJibLogger); - Credential expectedCredential = new Credential("username", "password"); Mockito.when( mockDockerCredentialHelperFactory.newDockerCredentialHelper( "registry", Paths.get("docker-credential-helper"))) .thenReturn(mockDockerCredentialHelper); - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(expectedCredential); Assert.assertEquals( - expectedCredential, - credentialProviderFactory + FAKE_CREDENTIALS, + credentialRetrieverFactory .dockerCredentialHelper( Paths.get("docker-credential-helper"), mockDockerCredentialHelperFactory) .retrieve()); Mockito.verify(mockJibLogger).info("Using docker-credential-helper for registry"); } + + @Test + public void testInferCredentialHelper() throws Exception { + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage( + ImageReference.of("something.gcr.io", null, null), mockJibLogger); + Mockito.when( + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "something.gcr.io", Paths.get("docker-credential-gcr"))) + .thenReturn(mockDockerCredentialHelper); + Mockito.when( + mockDockerCredentialHelperFactory.newDockerCredentialHelper( + "something.amazonaws.com", Paths.get("docker-credential-ecr-login"))) + .thenReturn(mockNonexistentDockerCredentialHelper); + + Assert.assertEquals( + FAKE_CREDENTIALS, + credentialRetrieverFactory + .inferCredentialHelper(mockDockerCredentialHelperFactory) + .retrieve()); + Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); + + Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); + Mockito.when(mockNonexistentDockerCredentialHelperException.getCause()) + .thenReturn(new IOException("the root cause")); + Assert.assertNull( + credentialRetrieverFactory + .setImageReference(ImageReference.of("something.amazonaws.com", null, null)) + .inferCredentialHelper(mockDockerCredentialHelperFactory) + .retrieve()); + Mockito.verify(mockJibLogger).warn("warning"); + Mockito.verify(mockJibLogger).info(" Caused by: the root cause"); + } } From 537d744a437c405791114bb2f51c5bb6004496f4 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 13 Aug 2018 18:23:59 -0400 Subject: [PATCH 0129/2020] Fixes macOS auth integration test. (#836) --- .../com/google/cloud/tools/jib/registry/LocalRegistry.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index c8fe471f0f..7c698d70fa 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.UUID; @@ -73,7 +74,10 @@ protected void before() throws IOException, InterruptedException { username, password) .run(); - Path tempFolder = Files.createTempDirectory("auth"); + // Creates the temporary directory in /tmp since that is one of the default directories + // mounted into Docker. + // See: https://docs.docker.com/docker-for-mac/osxfs + Path tempFolder = Files.createTempDirectory(Paths.get("/tmp"), ""); Files.write( tempFolder.resolve("htpasswd"), credentialString.getBytes(StandardCharsets.UTF_8)); From 7c26d771a17162b433f3813c511ab7832e384e7a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 14 Aug 2018 12:18:37 -0400 Subject: [PATCH 0130/2020] Reduce duplicate code in build image integration tests (#837) --- .../jib/gradle/JibPluginIntegrationTest.java | 210 +++++++----------- .../maven/BuildImageMojoIntegrationTest.java | 107 ++++----- 2 files changed, 116 insertions(+), 201 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 2e49ef72e9..2433db95ee 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -28,6 +28,7 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -54,28 +55,32 @@ private static String buildAndRun(TestProject testProject, String imageReference BuildResult buildResult = testProject.build( "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); + assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); + new Command("docker", "pull", imageReference).run(); + assertHasExposedPorts(imageReference); + return new Command("docker", "run", imageReference).run(); + } - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); + private static String buildAndRunComplex( + String imageReference, String username, String password, LocalRegistry targetRegistry) + throws IOException, InterruptedException { + BuildResult buildResult = + simpleTestProject.build( + "clean", + JibPlugin.BUILD_IMAGE_TASK_NAME, + "-D_TARGET_IMAGE=" + imageReference, + "-D_TARGET_USERNAME=" + username, + "-D_TARGET_PASSWORD=" + password, + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - new Command("docker", "pull", imageReference).run(); - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); + targetRegistry.pull(imageReference); + assertHasExposedPorts(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -84,28 +89,31 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String BuildResult buildResult = testProject.build( "clean", JibPlugin.BUILD_DOCKER_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); + assertBuildSuccess( + buildResult, JibPlugin.BUILD_DOCKER_TASK_NAME, "Built image to Docker daemon as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + assertHasExposedPorts(imageReference); + return new Command("docker", "run", imageReference).run(); + } + /** + * Asserts that the test project build output indicates a success. + * + * @param buildResult the builds results of the project under test + * @param taskName the name of the Jib task that was run + * @param successMessage a Jib-specific success message to check for + */ + private static void assertBuildSuccess( + BuildResult buildResult, String taskName, String successMessage) { BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibBuildDockerTask = buildResult.task(":" + JibPlugin.BUILD_DOCKER_TASK_NAME); + BuildTask jibTask = buildResult.task(":" + taskName); Assert.assertNotNull(classesTask); Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibBuildDockerTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibBuildDockerTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built image to Docker daemon as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - return new Command("docker", "run", imageReference).run(); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(successMessage)); } /** @@ -126,6 +134,32 @@ private static void assertSimpleCreationTimeIsAfter(Instant before, String image Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); } + /** + * Asserts that the test project has the required exposed ports. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertHasExposedPorts(String imageReference) + throws IOException, InterruptedException { + Assert.assertThat( + new Command("docker", "inspect", imageReference).run(), + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + } + + @Before + public void setup() throws IOException, InterruptedException { + // Pull distroless and push to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + } + @Test public void testBuild_empty() throws IOException, InterruptedException { String targetImage = @@ -185,90 +219,20 @@ public void testBuild_defaultTarget() { @Test public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); - - // Pull distroless to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - Instant beforeBuild = Instant.now(); - BuildResult buildResult = - simpleTestProject.build( - "clean", - JibPlugin.BUILD_IMAGE_TASK_NAME, - "-D_TARGET_IMAGE=" + targetImage, - "-D_TARGET_USERNAME=testuser2", - "-D_TARGET_PASSWORD=testpassword2", - "-DsendCredentialsOverHttp=true", - "-b=complex-build.gradle"); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); - - localRegistry2.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @Test public void testBuild_complex_sameFromAndToRegistry() throws IOException, InterruptedException { String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); - - // Pull distroless to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - Instant beforeBuild = Instant.now(); - BuildResult buildResult = - simpleTestProject.build( - "clean", - JibPlugin.BUILD_IMAGE_TASK_NAME, - "-D_TARGET_IMAGE=" + targetImage, - "-D_TARGET_USERNAME=testuser", - "-D_TARGET_PASSWORD=testpassword", - "-DsendCredentialsOverHttp=true", - "-b=complex-build.gradle"); - - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.BUILD_IMAGE_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built and pushed image as ")); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); - - localRegistry1.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -310,20 +274,13 @@ public void testBuildTar_simple() throws IOException, InterruptedException { simpleTestProject.build( "clean", JibPlugin.BUILD_TAR_TASK_NAME, "-D_TARGET_IMAGE=" + targetImage); - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibBuildTarTask = buildResult.task(":" + JibPlugin.BUILD_TAR_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibBuildTarTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibBuildTarTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Built image tarball at ")); + assertBuildSuccess(buildResult, JibPlugin.BUILD_TAR_TASK_NAME, "Built image tarball at "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); new Command("docker", "load", "--input", outputPath).run(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); + assertHasExposedPorts(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -332,15 +289,8 @@ public void testDockerContext() throws IOException, InterruptedException { BuildResult buildResult = simpleTestProject.build("clean", JibPlugin.DOCKER_CONTEXT_TASK_NAME, "--info"); - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibDockerContextTask = buildResult.task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibDockerContextTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibDockerContextTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); + assertBuildSuccess( + buildResult, JibPlugin.DOCKER_CONTEXT_TASK_NAME, "Created Docker context at "); String imageName = "jib-gradle-plugin/integration-test" + System.nanoTime(); new Command( @@ -355,15 +305,7 @@ public void testDockerContext() throws IOException, InterruptedException { .toString()) .run(); - Assert.assertThat( - new Command("docker", "inspect", imageName).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); + assertHasExposedPorts(imageName); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 35eed3b1a3..ea49317fdd 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -32,6 +32,7 @@ import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -95,6 +96,37 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole verifier.verifyErrorFreeLog(); new Command("docker", "pull", imageReference).run(); + assertHasExposedPorts(imageReference); + return new Command("docker", "run", imageReference).run(); + } + + private static String buildAndRunComplex( + String imageReference, String username, String password, LocalRegistry targetRegistry) + throws VerificationException, IOException, InterruptedException { + Instant before = Instant.now(); + Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setSystemProperty("_TARGET_USERNAME", username); + verifier.setSystemProperty("_TARGET_PASSWORD", password); + verifier.setSystemProperty("sendCredentialsOverHttp", "true"); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=pom-complex.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + verifier.verifyErrorFreeLog(); + + // Verify output + targetRegistry.pull(imageReference); + assertHasExposedPorts(imageReference); + Instant buildTime = + Instant.parse( + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); + Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + return new Command("docker", "run", imageReference).run(); + } + + private static void assertHasExposedPorts(String imageReference) + throws IOException, InterruptedException { Assert.assertThat( new Command("docker", "inspect", imageReference).run(), CoreMatchers.containsString( @@ -104,7 +136,6 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole + " \"2001/udp\": {},\n" + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); - return new Command("docker", "run", imageReference).run(); } private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOException { @@ -123,6 +154,12 @@ private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOExc return -1; } + @Before + public void setup() throws IOException, InterruptedException { + // Pull distroless to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + } + @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { String targetImage = @@ -210,81 +247,17 @@ public void testExecute_defaultTarget() { public void testExecute_complex() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); - - // Pull distroless to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - - // Run jib:build - Instant before = Instant.now(); - Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.setSystemProperty("_TARGET_USERNAME", "testuser2"); - verifier.setSystemProperty("_TARGET_PASSWORD", "testpassword2"); - verifier.setSystemProperty("sendCredentialsOverHttp", "true"); - verifier.setAutoclean(false); - verifier.addCliOption("-X"); - verifier.addCliOption("--file=pom-complex.xml"); - verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); - verifier.verifyErrorFreeLog(); - - // Verify output - localRegistry2.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); } @Test public void testExecute_complex_sameFromAndToRegistry() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); - - // Pull distroless to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - - // Run jib:build - Instant before = Instant.now(); - Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.setSystemProperty("_TARGET_USERNAME", "testuser"); - verifier.setSystemProperty("_TARGET_PASSWORD", "testpassword"); - verifier.setSystemProperty("sendCredentialsOverHttp", "true"); - verifier.setAutoclean(false); - verifier.addCliOption("-X"); - verifier.addCliOption("--file=pom-complex.xml"); - verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); - verifier.verifyErrorFreeLog(); - - // Verify output - localRegistry1.pull(targetImage); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", - new Command("docker", "run", targetImage).run()); - Assert.assertThat( - new Command("docker", "inspect", targetImage).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); } } From fe05fbfdfeaeaf68366f03bc9bfbb7c9aed0af0a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 14 Aug 2018 13:39:05 -0400 Subject: [PATCH 0131/2020] Adds CredentialRetrieverFactory#dockerConfig. (#828) --- .../RetrieveRegistryCredentialsStep.java | 7 ++- .../configuration/credentials/Credential.java | 1 + .../frontend/CredentialRetrieverFactory.java | 43 +++++++++++++++++++ .../cloud/tools/jib/http/Authorizations.java | 2 +- .../DockerConfigCredentialRetriever.java | 28 ++++++------ .../RetrieveRegistryCredentialsStepTest.java | 4 +- .../CredentialRetrieverFactoryTest.java | 22 ++++++++++ .../DockerConfigCredentialRetrieverTest.java | 36 +++++++--------- .../credentials/DockerConfigTest.java | 15 +++++-- .../src/test/resources/json/dockerconfig.json | 6 +-- .../json/dockerconfig_index_docker_io_v1.json | 2 +- 11 files changed, 120 insertions(+), 46 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 7e49be35db..fc9cc27294 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -168,8 +168,11 @@ public Authorization call() throws IOException, NonexistentDockerCredentialHelpe // Tries to get registry credentials from the Docker config. try { - Authorization dockerConfigAuthorization = dockerConfigCredentialRetriever.retrieve(); - if (dockerConfigAuthorization != null) { + Credential dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); + if (dockerConfigCredentials != null) { + Authorization dockerConfigAuthorization = + Authorizations.withBasicCredentials( + dockerConfigCredentials.getUsername(), dockerConfigCredentials.getPassword()); buildLogger.info("Using credentials from Docker config for " + registry); return dockerConfigAuthorization; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index 850ce70282..03c64fb296 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -18,6 +18,7 @@ import java.util.Objects; +// TODO: Move to lower-level package - probably at same level as Authorization. /** Holds credentials (username and password). */ public class Credential { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 8d2b251101..9c4d777a05 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; @@ -104,6 +105,30 @@ public CredentialRetriever inferCredentialHelper() { return inferCredentialHelper(new DockerCredentialHelperFactory()); } + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Docker config + * (located at {@code $USER_HOME/.docker/config.json}). + * + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig() { + return dockerConfig(new DockerConfigCredentialRetriever(imageReference.getRegistry())); + } + + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from a custom path + * to a Docker config. + * + * @param dockerConfigFile the path to the Docker config file + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig(Path dockerConfigFile) { + return dockerConfig( + new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile)); + } + @VisibleForTesting CredentialRetriever inferCredentialHelper( DockerCredentialHelperFactory dockerCredentialHelperFactory) { @@ -160,6 +185,24 @@ CredentialRetriever dockerCredentialHelper( }; } + @VisibleForTesting + CredentialRetriever dockerConfig( + DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { + return () -> { + try { + Credential dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); + if (dockerConfigCredentials != null) { + logger.info("Using credentials from Docker config for " + imageReference.getRegistry()); + return dockerConfigCredentials; + } + + } catch (IOException ex) { + logger.info("Unable to parse Docker config"); + } + return null; + }; + } + private Credential retrieveFromDockerCredentialHelper( Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) throws NonexistentServerUrlDockerCredentialHelperException, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java index b393dd4258..7463d689b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java @@ -39,7 +39,7 @@ public static Authorization withBasicCredentials(String username, String secret) String credentials = username + ":" + secret; String token = new String( - Base64.encodeBase64(credentials.getBytes(StandardCharsets.US_ASCII)), + Base64.encodeBase64(credentials.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); return new Authorization("Basic", token); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index c27be07206..4ea32f8a7a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -16,14 +16,14 @@ package com.google.cloud.tools.jib.registry.credentials; +import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -61,7 +61,7 @@ public DockerConfigCredentialRetriever(String registry) { } @VisibleForTesting - DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { + public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; this.dockerCredentialHelperFactory = new DockerCredentialHelperFactory(); @@ -80,11 +80,11 @@ public DockerConfigCredentialRetriever(String registry) { /** * Retrieves credentials for a registry. Tries all possible known aliases. * - * @return {@link Authorization} found for {@code registry}, or {@code null} if not found + * @return {@link Credential} found for {@code registry}, or {@code null} if not found * @throws IOException if failed to parse the config JSON */ @Nullable - public Authorization retrieve() throws IOException { + public Credential retrieve() throws IOException { DockerConfigTemplate dockerConfigTemplate = loadDockerConfigTemplate(); if (dockerConfigTemplate == null) { return null; @@ -93,9 +93,9 @@ public Authorization retrieve() throws IOException { DockerConfig dockerConfig = new DockerConfig(dockerConfigTemplate); for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { - Authorization authorization = retrieve(dockerConfig, registryAlias); - if (authorization != null) { - return authorization; + Credential credentials = retrieve(dockerConfig, registryAlias); + if (credentials != null) { + return credentials; } } return null; @@ -109,11 +109,15 @@ public Authorization retrieve() throws IOException { * @return the retrieved credentials, or {@code null} if none are found */ @Nullable - private Authorization retrieve(DockerConfig dockerConfig, String registryAlias) { + private Credential retrieve(DockerConfig dockerConfig, String registryAlias) { // First, tries to find defined auth. String auth = dockerConfig.getAuthFor(registryAlias); if (auth != null) { - return Authorizations.withBasicToken(auth); + // 'auth' is a basic authentication token that should be parsed back into credentials + String usernameColonPassword = new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); + String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); + String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); + return new Credential(username, password); } // Then, tries to use a defined credHelpers credential helper. @@ -122,9 +126,7 @@ private Authorization retrieve(DockerConfig dockerConfig, String registryAlias) if (dockerCredentialHelper != null) { try { // Tries with the given registry alias (may be the original registry). - Credential credential = dockerCredentialHelper.retrieve(); - return Authorizations.withBasicCredentials( - credential.getUsername(), credential.getPassword()); + return dockerCredentialHelper.retrieve(); } catch (IOException | NonexistentServerUrlDockerCredentialHelperException diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index b83834fe00..949d8de4fa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -127,10 +127,10 @@ public void testCall_useDockerConfig() FAKE_TARGET_REGISTRY, "someCredentialHelper")) .thenReturn(mockNonexistentServerUrlDockerCredentialHelper); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(mockAuthorization); + Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(FAKE_CREDENTIAL); Assert.assertEquals( - mockAuthorization, + FAKE_AUTHORIZATION, makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someCredentialHelper", null) .call()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index d55945b042..8a53de4bd8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; @@ -42,6 +43,12 @@ public class CredentialRetrieverFactoryTest { @Mock private JibLogger mockJibLogger; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @Mock private DockerCredentialHelper mockDockerCredentialHelper; + @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; + + /** + * A {@link DockerCredentialHelper} that throws {@link + * NonexistentDockerCredentialHelperException}. + */ @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; @Mock @@ -109,4 +116,19 @@ public void testInferCredentialHelper() throws Exception { Mockito.verify(mockJibLogger).warn("warning"); Mockito.verify(mockJibLogger).info(" Caused by: the root cause"); } + + @Test + public void testDockerConfig() throws Exception { + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage( + ImageReference.of("registry", null, null), mockJibLogger); + + Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(FAKE_CREDENTIALS); + + Assert.assertEquals( + FAKE_CREDENTIALS, + credentialRetrieverFactory.dockerConfig(mockDockerConfigCredentialRetriever).retrieve()); + + Mockito.verify(mockJibLogger).info("Using credentials from Docker config for registry"); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 66f2c7f5dc..02ca597989 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -37,9 +35,6 @@ public class DockerConfigCredentialRetrieverTest { private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); - private static final Authorization FAKE_AUTHORIZATION = - Authorizations.withBasicCredentials( - FAKE_CREDENTIAL.getUsername(), FAKE_CREDENTIAL.getPassword()); @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @@ -68,9 +63,10 @@ public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile, null); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertNotNull(authorization); - Assert.assertEquals("some auth", authorization.getToken()); + Credential credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertNotNull(credentials); + Assert.assertEquals("some", credentials.getUsername()); + Assert.assertEquals("auth", credentials.getPassword()); } @Test @@ -84,8 +80,7 @@ public void testRetrieve_useCredsStore() throws IOException { new DockerConfigCredentialRetriever( "just registry", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(FAKE_AUTHORIZATION, authorization); + Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); } @Test @@ -99,8 +94,7 @@ public void testRetrieve_useCredsStore_withProtocol() throws IOException { new DockerConfigCredentialRetriever( "with.protocol", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(FAKE_AUTHORIZATION, authorization); + Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); } @Test @@ -114,8 +108,7 @@ public void testRetrieve_useCredHelper() throws IOException { new DockerConfigCredentialRetriever( "another registry", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(FAKE_AUTHORIZATION, authorization); + Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); } @Test @@ -137,8 +130,7 @@ public void testRetrieve_credentialFromAlias() throws IOException { new DockerConfigCredentialRetriever( "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals(FAKE_AUTHORIZATION, authorization); + Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); } @Test @@ -150,8 +142,10 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException new DockerConfigCredentialRetriever( "index.docker.io", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals("token for index.docker.io/v1/", authorization.getToken()); + Credential credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertNotNull(credentials); + Assert.assertEquals("token for", credentials.getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.getPassword()); } @Test @@ -163,7 +157,9 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax new DockerConfigCredentialRetriever( "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); - Authorization authorization = dockerConfigCredentialRetriever.retrieve(); - Assert.assertEquals("token for index.docker.io/v1/", authorization.getToken()); + Credential credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertNotNull(credentials); + Assert.assertEquals("token for", credentials.getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.getPassword()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index b3eb529099..e3a59d3744 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -16,11 +16,13 @@ package com.google.cloud.tools.jib.registry.credentials; +import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Assert; @@ -29,6 +31,10 @@ /** Tests for {@link DockerConfig}. */ public class DockerConfigTest { + private static String decodeBase64(String base64String) { + return new String(Base64.decodeBase64(base64String), StandardCharsets.UTF_8); + } + @Test public void test_fromJson() throws URISyntaxException, IOException { // Loads the JSON string. @@ -38,10 +44,11 @@ public void test_fromJson() throws URISyntaxException, IOException { DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(jsonFile, DockerConfigTemplate.class)); - Assert.assertEquals("some auth", dockerConfig.getAuthFor("some registry")); - Assert.assertEquals("some other auth", dockerConfig.getAuthFor("some other registry")); - Assert.assertEquals("token", dockerConfig.getAuthFor("registry")); - Assert.assertEquals("token", dockerConfig.getAuthFor("https://registry")); + Assert.assertEquals("some:auth", decodeBase64(dockerConfig.getAuthFor("some registry"))); + Assert.assertEquals( + "some:other:auth", decodeBase64(dockerConfig.getAuthFor("some other registry"))); + Assert.assertEquals("token", decodeBase64(dockerConfig.getAuthFor("registry"))); + Assert.assertEquals("token", decodeBase64(dockerConfig.getAuthFor("https://registry"))); Assert.assertNull(dockerConfig.getAuthFor("just registry")); Assert.assertEquals( diff --git a/jib-core/src/test/resources/json/dockerconfig.json b/jib-core/src/test/resources/json/dockerconfig.json index ce7eed37a7..56934697b8 100644 --- a/jib-core/src/test/resources/json/dockerconfig.json +++ b/jib-core/src/test/resources/json/dockerconfig.json @@ -1,8 +1,8 @@ { "auths":{ - "some other registry":{"auth":"some other auth"}, - "some registry":{"auth":"some auth","password":"ignored"}, - "https://registry":{"auth":"token"}, + "some other registry":{"auth":"c29tZTpvdGhlcjphdXRo"}, + "some registry":{"auth":"c29tZTphdXRo","password":"ignored"}, + "https://registry":{"auth":"dG9rZW4="}, "just registry":{}, "https://with.protocol":{}, diff --git a/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json b/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json index 29329ce6be..65ff209005 100644 --- a/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json +++ b/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json @@ -1,5 +1,5 @@ { "auths":{ - "index.docker.io/v1/":{"auth":"token for index.docker.io/v1/"} + "index.docker.io/v1/":{"auth":"dG9rZW4gZm9yOiBpbmRleC5kb2NrZXIuaW8vdjEv"} } } \ No newline at end of file From f30dcbec4d420b96d15abd046dbca6e60a938eba Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 14 Aug 2018 14:39:04 -0400 Subject: [PATCH 0132/2020] Fixes Base64 encoding to string. (#838) --- .../java/com/google/cloud/tools/jib/http/Authorizations.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java index 7463d689b8..6e5c61c031 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java @@ -37,10 +37,7 @@ public static Authorization withBearerToken(String token) { */ public static Authorization withBasicCredentials(String username, String secret) { String credentials = username + ":" + secret; - String token = - new String( - Base64.encodeBase64(credentials.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8); + String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); return new Authorization("Basic", token); } From fb0fc383a39913eeafd8ba31e977d9a65314c056 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 15 Aug 2018 16:26:51 -0400 Subject: [PATCH 0133/2020] Moves slf4j to test-compile only. (#842) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 80eb2cb667..b151bcf71f 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -39,11 +39,11 @@ dependencies { compile 'org.apache.commons:commons-compress:1.17' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.slf4j:slf4j-api:1.7.25' compile 'org.javassist:javassist:3.22.0-GA' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.12.0' + testCompile 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin apt 'com.uber.nullaway:nullaway:0.4.2' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 3a86caff99..ae0271bd7a 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -61,11 +61,11 @@ dependencies { compile 'org.apache.commons:commons-compress:1.17' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.slf4j:slf4j-api:1.7.25' compile 'org.javassist:javassist:3.22.0-GA' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.12.0' + testCompile 'org.slf4j:slf4j-api:1.7.25' compile gradleApi() diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 978928914b..ca278fa0ba 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -71,12 +71,6 @@ 2.9.6 compile - - org.slf4j - slf4j-api - 1.7.25 - compile - org.javassist javassist @@ -116,13 +110,18 @@ 4.12 test - org.mockito mockito-core 2.12.0 test + + org.slf4j + slf4j-api + 1.7.25 + test + org.apache.maven.plugin-testing From e24105327c79690ee1485eb13e8214308f9910d4 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 15 Aug 2018 17:29:30 -0400 Subject: [PATCH 0134/2020] Makes list of credential retrievers. (#839) --- .../jib/configuration/ImageConfiguration.java | 3 +- .../frontend/CredentialRetrieverFactory.java | 7 +++ .../ConfigurationPropertyValidator.java | 10 ++-- .../ConfigurationPropertyValidatorTest.java | 19 +++--- .../tools/jib/gradle/BuildImageTask.java | 44 +++++++++++--- .../gradle/PluginConfigurationProcessor.java | 46 +++++++++++--- .../cloud/tools/jib/maven/BuildImageMojo.java | 59 +++++++++++++++--- .../maven/MavenSettingsServerCredentials.java | 12 ++-- .../maven/PluginConfigurationProcessor.java | 60 +++++++++++++++---- .../MavenSettingsServerCredentialsTest.java | 57 +++--------------- 10 files changed, 210 insertions(+), 107 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index d813bca9e8..d9e7f02f13 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -59,7 +59,8 @@ public Builder setKnownRegistryCredentials( } /** - * Sets the providers for registry credentials. + * Sets the providers for registry credentials. The order determines the priority in which the + * retrieval methods are attempted. * * @param credentialRetrievers the list of {@link CredentialRetriever}s * @return this diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 9c4d777a05..0d7bfbf205 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -67,6 +67,13 @@ public CredentialRetrieverFactory setImageReference(ImageReference imageReferenc return this; } + public CredentialRetriever known(Credential credential, String credentialSource) { + return () -> { + logGotCredentialsFrom(credentialSource); + return credential; + }; + } + /** * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential * helper, such as {@code docker-credential-gcr}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index a8a8b1d32d..e0965d604d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; @@ -52,7 +52,7 @@ public static void checkHttpTimeoutProperty( } /** - * Gets an {@link Authorization} from a username and password. First tries system properties, then + * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. * * @param logger the {@link JibLogger} used to print warnings messages @@ -63,14 +63,14 @@ public static void checkHttpTimeoutProperty( * {@code null} if neither is configured. */ @Nullable - public static Authorization getImageAuth( + public static Credential getImageCredential( JibLogger logger, String usernameProperty, String passwordProperty, AuthProperty auth) { // System property takes priority over build configuration String commandlineUsername = System.getProperty(usernameProperty); String commandlinePassword = System.getProperty(passwordProperty); if (!Strings.isNullOrEmpty(commandlineUsername) && !Strings.isNullOrEmpty(commandlinePassword)) { - return Authorizations.withBasicCredentials(commandlineUsername, commandlinePassword); + return new Credential(commandlineUsername, commandlinePassword); } // Warn if a system property is missing @@ -106,7 +106,7 @@ public static Authorization getImageAuth( return null; } - return Authorizations.withBasicCredentials(auth.getUsername(), auth.getPassword()); + return new Credential(auth.getUsername(), auth.getPassword()); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 2cf6401ed1..ee9d8830bf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -17,8 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import org.junit.After; @@ -79,9 +78,9 @@ public void testGetImageAuth() { // System properties set System.setProperty("jib.test.auth.user", "abcde"); System.setProperty("jib.test.auth.pass", "12345"); - Authorization expected = Authorizations.withBasicCredentials("abcde", "12345"); - Authorization actual = - ConfigurationPropertyValidator.getImageAuth( + Credential expected = new Credential("abcde", "12345"); + Credential actual = + ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertNotNull(actual); Assert.assertEquals(expected.toString(), actual.toString()); @@ -89,9 +88,9 @@ public void testGetImageAuth() { // Auth set in configuration System.clearProperty("jib.test.auth.user"); System.clearProperty("jib.test.auth.pass"); - expected = Authorizations.withBasicCredentials("vwxyz", "98765"); + expected = new Credential("vwxyz", "98765"); actual = - ConfigurationPropertyValidator.getImageAuth( + ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertNotNull(actual); Assert.assertEquals(expected.toString(), actual.toString()); @@ -101,7 +100,7 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = - ConfigurationPropertyValidator.getImageAuth( + ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertNull(actual); @@ -109,7 +108,7 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = - ConfigurationPropertyValidator.getImageAuth( + ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertNull(actual); Mockito.verify(mockLogger) @@ -119,7 +118,7 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = - ConfigurationPropertyValidator.getImageAuth( + ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertNull(actual); Mockito.verify(mockLogger) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index af5801040c..a5d3743afb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -19,7 +19,10 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -29,6 +32,8 @@ import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import java.util.ArrayList; +import java.util.List; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -81,20 +86,45 @@ public void buildImage() throws InvalidImageReferenceException { .forToNotConfigured( "'jib.to.image'", "build.gradle", "gradle jib --image ")); } - RegistryCredentials knownTargetRegistryCredentials = null; - Authorization toAuthorization = - ConfigurationPropertyValidator.getImageAuth( + + ImageReference targetImage = ImageReference.parse(jibExtension.getTargetImage()); + + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage(targetImage, gradleJibLogger); + Credential toCredential = + ConfigurationPropertyValidator.getImageCredential( gradleJibLogger, "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); - if (toAuthorization != null) { - knownTargetRegistryCredentials = new RegistryCredentials("jib.to.auth", toAuthorization); + RegistryCredentials knownTargetRegistryCredentials = null; + CredentialRetriever knownCredentialRetriever = null; + if (toCredential != null) { + knownTargetRegistryCredentials = + new RegistryCredentials( + "jib.to.auth", + Authorizations.withBasicCredentials( + toCredential.getUsername(), toCredential.getPassword())); + knownCredentialRetriever = credentialRetrieverFactory.known(toCredential, "jib.to.auth"); } + // Makes credential retriever list. + List credentialRetrievers = new ArrayList<>(); + String credentialHelperSuffix = jibExtension.getTo().getCredHelper(); + if (credentialHelperSuffix != null) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); + } + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); + } + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + ImageConfiguration targetImageConfiguration = - ImageConfiguration.builder(ImageReference.parse(jibExtension.getTargetImage())) + ImageConfiguration.builder(targetImage) .setCredentialHelper(jibExtension.getTo().getCredHelper()) .setKnownRegistryCredentials(knownTargetRegistryCredentials) + .setCredentialRetrievers(credentialRetrievers) .build(); PluginConfigurationProcessor pluginConfigurationProcessor = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 3e91b8a5a0..c0b46833aa 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -21,15 +21,20 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import org.gradle.api.GradleException; /** Configures and provides builders for the image building tasks. */ @@ -59,26 +64,49 @@ static PluginConfigurationProcessor processCommonConfiguration( GradleJibLogger.disableHttpLogging(); RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); + ImageReference baseImage = ImageReference.parse(jibExtension.getBaseImage()); + if (Boolean.getBoolean("sendCredentialsOverHttp")) { logger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } - RegistryCredentials knownBaseRegistryCredentials = null; - Authorization fromAuthorization = - ConfigurationPropertyValidator.getImageAuth( + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage(baseImage, logger); + Credential fromCredential = + ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); - if (fromAuthorization != null) { - knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); + RegistryCredentials knownBaseRegistryCredentials = null; + CredentialRetriever knownCredentialRetriever = null; + if (fromCredential != null) { + knownBaseRegistryCredentials = + new RegistryCredentials( + "jib.from.auth", + Authorizations.withBasicCredentials( + fromCredential.getUsername(), fromCredential.getPassword())); + knownCredentialRetriever = credentialRetrieverFactory.known(fromCredential, "jib.from.auth"); + } + // Makes credential retriever list. + List credentialRetrievers = new ArrayList<>(); + String credentialHelperSuffix = jibExtension.getFrom().getCredHelper(); + if (credentialHelperSuffix != null) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); + } + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); } + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration.Builder baseImageConfigurationBuilder = - ImageConfiguration.builder(ImageReference.parse(jibExtension.getBaseImage())) - .setCredentialHelper(jibExtension.getFrom().getCredHelper()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials); + ImageConfiguration.builder(baseImage) + .setCredentialHelper(credentialHelperSuffix) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .setCredentialRetrievers(credentialRetrievers); String mainClass = projectProperties.getMainClass(jibExtension); ContainerConfiguration.Builder containerConfigurationBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index e39d493980..c66a13aa8a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -19,7 +19,10 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -29,7 +32,9 @@ import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -83,19 +88,55 @@ public void execute() throws MojoExecutionException, MojoFailureException { ImageReference targetImage = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); - Authorization toAuthorization = - ConfigurationPropertyValidator.getImageAuth( + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage(targetImage, mavenJibLogger); + Credential toCredential = + ConfigurationPropertyValidator.getImageCredential( mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); - RegistryCredentials knownTargetRegistryCredentials = - toAuthorization != null - ? new RegistryCredentials("jib-maven-plugin configuration", toAuthorization) - : pluginConfigurationProcessor - .getMavenSettingsServerCredentials() - .retrieve(targetImage.getRegistry()); + RegistryCredentials knownTargetRegistryCredentials = null; + CredentialRetriever knownCredentialRetriever = null; + if (toCredential == null) { + toCredential = + pluginConfigurationProcessor + .getMavenSettingsServerCredentials() + .retrieve(targetImage.getRegistry()); + if (toCredential != null) { + knownTargetRegistryCredentials = + new RegistryCredentials( + MavenSettingsServerCredentials.CREDENTIAL_SOURCE, + Authorizations.withBasicCredentials( + toCredential.getUsername(), toCredential.getPassword())); + knownCredentialRetriever = + credentialRetrieverFactory.known( + toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); + } + } else { + knownTargetRegistryCredentials = + new RegistryCredentials( + "jib-maven-plugin configuration", + Authorizations.withBasicCredentials( + toCredential.getUsername(), toCredential.getPassword())); + knownCredentialRetriever = + credentialRetrieverFactory.known( + toCredential, "jib-maven-plugin configuration"); + } + // Makes credential retriever list. + List credentialRetrievers = new ArrayList<>(); + String credentialHelperSuffix = getTargetImageCredentialHelperName(); + if (credentialHelperSuffix != null) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); + } + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); + } + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) .setCredentialHelper(getTargetImageCredentialHelperName()) .setKnownRegistryCredentials(knownTargetRegistryCredentials) + .setCredentialRetrievers(credentialRetrievers) .build(); BuildConfiguration buildConfiguration = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 61e31e8226..3a06d98b5d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -16,8 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.common.annotations.VisibleForTesting; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,7 +36,7 @@ */ class MavenSettingsServerCredentials { - @VisibleForTesting static final String CREDENTIAL_SOURCE = "Maven settings"; + public static final String CREDENTIAL_SOURCE = "Maven settings"; // pattern cribbed directly from // https://github.com/sonatype/plexus-cipher/blob/master/src/main/java/org/sonatype/plexus/components/cipher/DefaultPlexusCipher.java @@ -83,7 +82,7 @@ static boolean isEncrypted(String password) { * @throws MojoExecutionException if the credentials could not be retrieved */ @Nullable - RegistryCredentials retrieve(@Nullable String registry) throws MojoExecutionException { + Credential retrieve(@Nullable String registry) throws MojoExecutionException { if (registry == null) { return null; } @@ -119,9 +118,6 @@ RegistryCredentials retrieve(@Nullable String registry) throws MojoExecutionExce + " appears to be encrypted, but there is no decrypter available"); } - return new RegistryCredentials( - CREDENTIAL_SOURCE, - Authorizations.withBasicCredentials( - registryServer.getUsername(), registryServer.getPassword())); + return new Credential(registryServer.getUsername(), registryServer.getPassword()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index ca4d5866f8..92691a8603 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -20,9 +20,12 @@ import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; @@ -30,6 +33,8 @@ import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import org.apache.maven.plugin.MojoExecutionException; /** Configures and provides builders for the image building goals. */ @@ -75,21 +80,56 @@ static PluginConfigurationProcessor processCommonConfiguration( Preconditions.checkNotNull(jibPluginConfiguration.getSession()).getSettings(), jibPluginConfiguration.getSettingsDecrypter(), logger); - Authorization fromAuthorization = - ConfigurationPropertyValidator.getImageAuth( + CredentialRetrieverFactory credentialRetrieverFactory = + CredentialRetrieverFactory.forImage(baseImage, logger); + Credential fromCredential = + ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); - RegistryCredentials knownBaseRegistryCredentials = - fromAuthorization != null - ? new RegistryCredentials( - "jib-maven-plugin configuration", fromAuthorization) - : mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + RegistryCredentials knownBaseRegistryCredentials = null; + CredentialRetriever knownCredentialRetriever = null; + if (fromCredential == null) { + fromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + if (fromCredential != null) { + knownBaseRegistryCredentials = + new RegistryCredentials( + MavenSettingsServerCredentials.CREDENTIAL_SOURCE, + Authorizations.withBasicCredentials( + fromCredential.getUsername(), fromCredential.getPassword())); + knownCredentialRetriever = + credentialRetrieverFactory.known( + fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); + } + } else { + knownBaseRegistryCredentials = + new RegistryCredentials( + "jib-maven-plugin configuration", + Authorizations.withBasicCredentials( + fromCredential.getUsername(), fromCredential.getPassword())); + knownCredentialRetriever = + credentialRetrieverFactory.known( + fromCredential, "jib-maven-plugin configuration"); + } + + // Makes credential retriever list. + List credentialRetrievers = new ArrayList<>(); + String credentialHelperSuffix = jibPluginConfiguration.getBaseImageCredentialHelperName(); + if (credentialHelperSuffix != null) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); + } + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); + } + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration.Builder baseImageConfiguration = ImageConfiguration.builder(baseImage) - .setCredentialHelper(jibPluginConfiguration.getBaseImageCredentialHelperName()) - .setKnownRegistryCredentials(knownBaseRegistryCredentials); + .setCredentialHelper(credentialHelperSuffix) + .setKnownRegistryCredentials(knownBaseRegistryCredentials) + .setCredentialRetrievers(credentialRetrievers); String mainClass = projectProperties.getMainClass(jibPluginConfiguration); ContainerConfiguration.Builder containerConfigurationBuilder = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index a3d2c3d3c8..a8bc6b57f8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -16,9 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import java.util.Collections; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Server; @@ -57,35 +55,20 @@ public void testRetrieve_found() throws MojoExecutionException { Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - RegistryCredentials registryCredentials = - testMavenSettingsServerCredentials.retrieve("server1"); + Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertEquals(new Credential("server1 username", "server1 password"), credential); - Assert.assertNotNull(registryCredentials); - Assert.assertEquals( - MavenSettingsServerCredentials.CREDENTIAL_SOURCE, - registryCredentials.getCredentialSource()); - - Authorization retrievedServer1Authorization = registryCredentials.getAuthorization(); - Assert.assertNotNull(retrievedServer1Authorization); - Assert.assertEquals( - Authorizations.withBasicCredentials("server1 username", "server1 password").toString(), - retrievedServer1Authorization.toString()); Mockito.verifyZeroInteractions(mockLogger); } @Test public void testRetrieve_notFound() throws MojoExecutionException { - RegistryCredentials registryCredentials = - testMavenSettingsServerCredentials.retrieve("serverUnknown"); - - Assert.assertNull(registryCredentials); + Assert.assertNull(testMavenSettingsServerCredentials.retrieve("serverUnknown")); } @Test public void testRetrieve_withNullServer() throws MojoExecutionException { - RegistryCredentials registryCredentials = testMavenSettingsServerCredentials.retrieve(null); - - Assert.assertNull(registryCredentials); + Assert.assertNull(testMavenSettingsServerCredentials.retrieve(null)); } @Test @@ -94,19 +77,8 @@ public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionExcep Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); - RegistryCredentials registryCredentials = - testMavenSettingsServerCredentials.retrieve("server1"); - - Assert.assertNotNull(registryCredentials); - Assert.assertEquals( - MavenSettingsServerCredentials.CREDENTIAL_SOURCE, - registryCredentials.getCredentialSource()); - - Authorization retrievedServer1Authorization = registryCredentials.getAuthorization(); - Assert.assertNotNull(retrievedServer1Authorization); - Assert.assertEquals( - Authorizations.withBasicCredentials("server1 username", "{COQLCE6DU6GtcS5P=}").toString(), - retrievedServer1Authorization.toString()); + Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertEquals(new Credential("server1 username", "{COQLCE6DU6GtcS5P=}"), credential); Mockito.verify(mockLogger) .warn( "Server password for registry server1 appears to be encrypted, " @@ -130,19 +102,8 @@ public void testRetrieve_withDecrypter_success() throws MojoExecutionException { Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - RegistryCredentials registryCredentials = - testMavenSettingsServerCredentials.retrieve("server1"); - - Assert.assertNotNull(registryCredentials); - Assert.assertEquals( - MavenSettingsServerCredentials.CREDENTIAL_SOURCE, - registryCredentials.getCredentialSource()); - - Authorization retrievedServer1Authorization = registryCredentials.getAuthorization(); - Assert.assertNotNull(retrievedServer1Authorization); - Assert.assertEquals( - Authorizations.withBasicCredentials("server1 username", "server1 password").toString(), - retrievedServer1Authorization.toString()); + Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertEquals(new Credential("server1 username", "server1 password"), credential); Mockito.verify(mockDecrypter).decrypt(Mockito.any()); Mockito.verify(mockResult).getProblems(); From 0687ec1b3162682130d6376bb11de401ae3f584b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 16 Aug 2018 11:10:34 -0400 Subject: [PATCH 0135/2020] Move plugins.common package to new module (#845) --- build.sh | 16 +- jib-core/gradle.properties | 2 +- jib-gradle-plugin/build.gradle | 4 + jib-maven-plugin/pom.xml | 10 +- jib-plugins-common/.gitignore | 11 ++ jib-plugins-common/README.md | 3 + jib-plugins-common/build.gradle | 98 ++++++++++ jib-plugins-common/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54413 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + jib-plugins-common/gradlew | 172 ++++++++++++++++++ jib-plugins-common/gradlew.bat | 84 +++++++++ jib-plugins-common/settings.gradle | 1 + .../jib/plugins/common/AuthProperty.java | 0 .../common/BuildStepsExecutionException.java | 0 .../jib/plugins/common/BuildStepsRunner.java | 3 +- .../ConfigurationPropertyValidator.java | 0 .../plugins/common/HelpfulSuggestions.java | 0 .../common/MainClassInferenceException.java | 0 .../jib/plugins/common/MainClassResolver.java | 0 .../jib/plugins/common/ProjectProperties.java | 0 .../plugins/common/BuildStepsRunnerTest.java | 0 .../ConfigurationPropertyValidatorTest.java | 0 .../common/HelpfulSuggestionsTest.java | 0 .../plugins/common/MainClassResolverTest.java | 0 25 files changed, 403 insertions(+), 7 deletions(-) create mode 100644 jib-plugins-common/.gitignore create mode 100644 jib-plugins-common/README.md create mode 100644 jib-plugins-common/build.gradle create mode 100644 jib-plugins-common/gradle.properties create mode 100644 jib-plugins-common/gradle/wrapper/gradle-wrapper.jar create mode 100644 jib-plugins-common/gradle/wrapper/gradle-wrapper.properties create mode 100755 jib-plugins-common/gradlew create mode 100644 jib-plugins-common/gradlew.bat create mode 100644 jib-plugins-common/settings.gradle rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java (99%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java (100%) rename {jib-core => jib-plugins-common}/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java (100%) rename {jib-core => jib-plugins-common}/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java (100%) rename {jib-core => jib-plugins-common}/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java (100%) rename {jib-core => jib-plugins-common}/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java (100%) rename {jib-core => jib-plugins-common}/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java (100%) diff --git a/build.sh b/build.sh index 4c5c8534a7..0fb6579626 100755 --- a/build.sh +++ b/build.sh @@ -8,8 +8,8 @@ gradleOptions="" usage() { eval 1>&2 - echo "Simple builder for Jib for jib-core, jib-maven-plugin, and jib-gradle-plugin" - echo "use: $0 [-qe] [clean | core | maven | gradle | all]" + echo "Simple builder for Jib for jib-core, jib-plugins-common, jib-maven-plugin, and jib-gradle-plugin" + echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | all]" echo " -q quick mode: skip tests, formatting" echo " -e show error information (mvn: -e, gradle: --stacktrace)" exit 1 @@ -34,7 +34,7 @@ done shift `expr $OPTIND - 1` if [ $# -eq 0 ]; then - set -- core gradle maven + set -- core plugins gradle maven fi set -e # exit on error @@ -42,6 +42,7 @@ for target in "$@"; do case "$target" in clean) doBuild jib-core ./gradlew $gradleOptions clean + doBuild jib-plugins-common ./gradlew $gradleOptions clean doBuild jib-gradle-plugin ./gradlew $gradleOptions clean doBuild jib-maven-plugin ./mvnw $mavenOptions clean ;; @@ -55,6 +56,15 @@ for target in "$@"; do fi ;; + plugins) + if [ "$quickMode" = false ]; then + doBuild jib-plugins-common ./gradlew $gradleOptions googleJavaFormat build + else + doBuild jib-plugins-common ./gradlew $gradleOptions build \ + --exclude-task test --exclude-task check + fi + ;; + maven) if [ "$quickMode" = false ]; then doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 783163c04f..da3fb3f1a2 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.0-SNAPSHOT \ No newline at end of file +version = 0.1.0-SNAPSHOT diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index ae0271bd7a..cea2a0f450 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -31,10 +31,14 @@ sourceSets { main { java.srcDir file('../jib-core/src/main/java') resources.srcDir file('../jib-core/src/main/resources') + java.srcDir file('../jib-plugins-common/src/main/java') + resources.srcDir file('../jib-plugins-common/src/main/resources') } test { java.srcDir file('../jib-core/src/test/java') resources.srcDir file('../jib-core/src/test/resources') + java.srcDir file('../jib-plugins-common/src/test/java') + resources.srcDir file('../jib-plugins-common/src/test/resources') } integrationTest { java { diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index ca278fa0ba..6d9f5f84c3 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -209,12 +209,12 @@ - org.codehaus.mojo build-helper-maven-plugin 3.0.0 + add-jib-core-sources generate-sources @@ -224,6 +224,7 @@ ${basedir}/../jib-core/src/main/java + ${basedir}/../jib-plugins-common/src/main/java @@ -238,6 +239,9 @@ ${basedir}/../jib-core/src/main/resources + + ${basedir}/../jib-plugins-common/src/main/resources + @@ -250,6 +254,7 @@ ${basedir}/../jib-core/src/test/java + ${basedir}/../jib-plugins-common/src/test/java @@ -264,6 +269,9 @@ ${basedir}/../jib-core/src/test/resources + + ${basedir}/../jib-plugins-common/src/test/resources + diff --git a/jib-plugins-common/.gitignore b/jib-plugins-common/.gitignore new file mode 100644 index 0000000000..898930fb52 --- /dev/null +++ b/jib-plugins-common/.gitignore @@ -0,0 +1,11 @@ +build +target +out +*.iml +*.ipr +*.iws +.idea +.gradle +/.settings +/.classpath +/.project diff --git a/jib-plugins-common/README.md b/jib-plugins-common/README.md new file mode 100644 index 0000000000..2f23133d4f --- /dev/null +++ b/jib-plugins-common/README.md @@ -0,0 +1,3 @@ +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-plugins-common)](https://github.com/igrigorik/ga-beacon) + +Common code for Jib plugins. NOT intended for use outside of `jib-maven-plugin` or `jib-gradle-plugin` in its current state. \ No newline at end of file diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle new file mode 100644 index 0000000000..250df6233d --- /dev/null +++ b/jib-plugins-common/build.gradle @@ -0,0 +1,98 @@ +plugins { + id 'java' + id 'checkstyle' + id 'com.github.sherter.google-java-format' version '0.7.1' + id 'net.ltgt.apt' version '0.13' + id 'net.ltgt.errorprone' version '0.0.13' +} + +group 'com.google.cloud.tools' + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 +compileJava.options.encoding = 'UTF-8' + +repositories { + mavenCentral() +} + +sourceSets { + main { + java.srcDir file('../jib-core/src/main/java') + resources.srcDir file('../jib-core/src/main/resources') + } + test { + java.srcDir file('../jib-core/src/test/java') + resources.srcDir file('../jib-core/src/test/resources') + } +} + +dependencies { + // Make sure these are consistent with jib-maven-plugin. + compile 'com.google.http-client:google-http-client:1.23.0' + compile 'org.apache.commons:commons-compress:1.17' + compile 'com.google.guava:guava:23.5-jre' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + compile 'org.javassist:javassist:3.22.0-GA' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:2.12.0' + testCompile 'org.slf4j:slf4j-api:1.7.25' + + // NullAway errorprone plugin + apt 'com.uber.nullaway:nullaway:0.4.2' + errorprone 'com.google.errorprone:error_prone_core:2.2.0' +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.6' +} + +test { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } +} + +// Adds NullAway errorprone checks. +tasks.withType(JavaCompile) { + if (!name.toLowerCase().contains("test")) { + options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] + } +} + +// Fail build on javadoc warnings +tasks.withType(Javadoc) { + options.addBooleanOption('Xwerror', true) +} +assemble.dependsOn javadoc + +tasks.withType(Test) { + reports.html.setDestination file("${reporting.baseDir}/${name}") +} + +/* GOOGLE JAVA FORMAT */ +googleJavaFormat { + toolVersion = '1.6' +} +check.dependsOn verifyGoogleJavaFormat +/* GOOGLE JAVA FORMAT */ + +/* CHECKSTYLE */ +checkstyle { + toolVersion = "7.6.1" + + // get the google_checks.xml file from the checkstyle jar and take out the java checks + def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() + def fileExtensionsBefore = '' + def fileExtensionsAfter = '' + def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) + assert !googleChecks.equals(googleChecksNoJava) + + config = resources.text.fromString(googleChecksNoJava) + + maxErrors = 0 + maxWarnings = 0 +} +/* CHECKSTYLE */ diff --git a/jib-plugins-common/gradle.properties b/jib-plugins-common/gradle.properties new file mode 100644 index 0000000000..da3fb3f1a2 --- /dev/null +++ b/jib-plugins-common/gradle.properties @@ -0,0 +1 @@ +version = 0.1.0-SNAPSHOT diff --git a/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar b/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..0d4a9516871afd710a9d84d89e31ba77745607bd GIT binary patch literal 54413 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 literal 0 HcmV?d00001 diff --git a/jib-plugins-common/gradle/wrapper/gradle-wrapper.properties b/jib-plugins-common/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..bf3de21830 --- /dev/null +++ b/jib-plugins-common/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jib-plugins-common/gradlew b/jib-plugins-common/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/jib-plugins-common/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/jib-plugins-common/gradlew.bat b/jib-plugins-common/gradlew.bat new file mode 100644 index 0000000000..e95643d6a2 --- /dev/null +++ b/jib-plugins-common/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-plugins-common/settings.gradle b/jib-plugins-common/settings.gradle new file mode 100644 index 0000000000..86f932126b --- /dev/null +++ b/jib-plugins-common/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jib-plugins-common' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 81e649353a..78595c32da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.cache.Caches.Initializer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; @@ -101,7 +100,7 @@ private static Initializer getCacheInitializer(BuildConfiguration buildConfigura ? CacheConfiguration.forDefaultUserLevelCacheDirectory() : buildConfiguration.getBaseImageLayersCacheConfiguration(); - return new Caches.Initializer( + return new Initializer( baseImageLayersCacheConfiguration.getCacheDirectory(), applicationLayersCacheConfiguration.shouldEnsureOwnership(), applicationLayersCacheConfiguration.getCacheDirectory(), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java similarity index 100% rename from jib-core/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java similarity index 100% rename from jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java similarity index 100% rename from jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java similarity index 100% rename from jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java similarity index 100% rename from jib-core/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java From f2c3eed022ae934d73c2662fa8977295898a91fe Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 16 Aug 2018 13:19:51 -0400 Subject: [PATCH 0136/2020] Add plugins-common to kokoro scripts (#846) --- kokoro/continuous.bat | 1 + kokoro/continuous.sh | 1 + kokoro/presubmit.bat | 1 + kokoro/presubmit.sh | 1 + 4 files changed, 4 insertions(+) diff --git a/kokoro/continuous.bat b/kokoro/continuous.bat index 64f7499bf1..d0cb4542ee 100755 --- a/kokoro/continuous.bat +++ b/kokoro/continuous.bat @@ -14,6 +14,7 @@ set JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing REM TODO: Enable integration tests once docker works (b/73345382). cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ +cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -X && ^ cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 67535869f8..fc32908ce5 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -20,5 +20,6 @@ docker kill $(docker ps --all --quiet) || true export JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) +(cd github/jib/jib-plugins-common; ./gradlew clean build --info --stacktrace) (cd github/jib/jib-maven-plugin; ./mvnw clean install -P integration-tests -B -U -X) (cd github/jib/jib-gradle-plugin; ./gradlew clean build integrationTest --info --stacktrace) diff --git a/kokoro/presubmit.bat b/kokoro/presubmit.bat index 7ce597959f..c66c6833ac 100755 --- a/kokoro/presubmit.bat +++ b/kokoro/presubmit.bat @@ -10,6 +10,7 @@ REM Stops any left-over containers. REM FOR /f "tokens=*" %%i IN ('docker ps -aq') DO docker rm -vf %%i cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ +cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -X && ^ cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index 2df5c013e5..14e29594db 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -13,5 +13,6 @@ docker kill $(docker ps --all --quiet) || true cd github/jib (cd jib-core; ./gradlew clean build integrationTest --info --stacktrace) +(cd jib-plugins-common; ./gradlew clean build --info --stacktrace) (cd jib-maven-plugin; ./mvnw clean install -B -U -X) (cd jib-gradle-plugin; ./gradlew clean build --info --stacktrace) From 1f0f6fa6c22beec00cf8b0791d1e4505e0ab9be9 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 16 Aug 2018 14:45:25 -0400 Subject: [PATCH 0137/2020] Changes RetrieveRegistryCredentialsStep to use credential retrievers list. (#847) --- .../builder/steps/AuthenticatePushStep.java | 14 +- .../jib/builder/steps/PullBaseImageStep.java | 24 ++- .../RetrieveRegistryCredentialsStep.java | 152 ++------------ .../credentials/CredentialRetriever.java | 12 +- .../frontend/CredentialRetrieverFactory.java | 7 + .../RetrieveRegistryCredentialsStepTest.java | 198 ++++++------------ 6 files changed, 126 insertions(+), 281 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index eb07ac890f..8d0f985eca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -20,7 +20,9 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryException; @@ -76,8 +78,12 @@ public Authorization call() String.format( DESCRIPTION, buildConfiguration.getTargetImageConfiguration().getImageRegistry()))) { - Authorization registryCredentials = - NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); + Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); + Authorization registryAuthorization = + registryCredential == null + ? null + : Authorizations.withBasicCredentials( + registryCredential.getUsername(), registryCredential.getPassword()); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( @@ -87,9 +93,9 @@ public Authorization call() .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { - return registryCredentials; + return registryAuthorization; } - return registryAuthenticator.setAuthorization(registryCredentials).authenticatePush(); + return registryAuthenticator.setAuthorization(registryAuthorization).authenticatePush(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 821c9a65a6..95f0c2eadc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -22,7 +22,9 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -128,12 +130,16 @@ public BaseImageWithAuthorization call() RetrieveRegistryCredentialsStep retrieveBaseRegistryCredentialsStep = RetrieveRegistryCredentialsStep.forBaseImage(directExecutorService, buildConfiguration); - Authorization registryCredentials = - NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); + Credential registryCredential = NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); + Authorization registryAuthorization = + registryCredential == null + ? null + : Authorizations.withBasicCredentials( + registryCredential.getUsername(), registryCredential.getPassword()); try { return new BaseImageWithAuthorization( - pullBaseImage(registryCredentials), registryCredentials); + pullBaseImage(registryAuthorization), registryAuthorization); } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. @@ -152,11 +158,11 @@ public BaseImageWithAuthorization call() "Failed to retrieve authentication challenge for registry that required token authentication"); throw registryUnauthorizedException; } - registryCredentials = - registryAuthenticator.setAuthorization(registryCredentials).authenticatePull(); + registryAuthorization = + registryAuthenticator.setAuthorization(registryAuthorization).authenticatePull(); return new BaseImageWithAuthorization( - pullBaseImage(registryCredentials), registryCredentials); + pullBaseImage(registryAuthorization), registryAuthorization); } } } @@ -165,7 +171,7 @@ public BaseImageWithAuthorization call() /** * Pulls the base image. * - * @param registryCredentials authentication credentials to possibly use + * @param registryAuthorization authentication credentials to possibly use * @return the pulled image * @throws IOException when an I/O exception occurs during the pulling * @throws RegistryException if communicating with the registry caused a known error @@ -175,7 +181,7 @@ public BaseImageWithAuthorization call() * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - private Image pullBaseImage(@Nullable Authorization registryCredentials) + private Image pullBaseImage(@Nullable Authorization registryAuthorization) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { RegistryClient registryClient = @@ -184,7 +190,7 @@ private Image pullBaseImage(@Nullable Authorization registryCredentials) buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) - .setAuthorization(registryCredentials) + .setAuthorization(registryAuthorization) .newRegistryClient(); ManifestTemplate manifestTemplate = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index fc9cc27294..ed053eceee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -21,33 +21,20 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; import java.util.concurrent.Callable; import javax.annotation.Nullable; /** Attempts to retrieve registry credentials. */ -class RetrieveRegistryCredentialsStep implements AsyncStep, Callable { +class RetrieveRegistryCredentialsStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Retrieving registry credentials for %s"; - /** - * Defines common credential helpers to use as defaults. Maps from registry suffix to credential - * helper suffix. - */ - private static final ImmutableMap COMMON_CREDENTIAL_HELPERS = - ImmutableMap.of("gcr.io", "gcr", "amazonaws.com", "ecr-login"); - /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { @@ -55,8 +42,7 @@ static RetrieveRegistryCredentialsStep forBaseImage( listeningExecutorService, buildConfiguration.getBuildLogger(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getCredentialHelper(), - buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials()); + buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } /** Retrieves credentials for the target image. */ @@ -66,121 +52,46 @@ static RetrieveRegistryCredentialsStep forTargetImage( listeningExecutorService, buildConfiguration.getBuildLogger(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getCredentialHelper(), - buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials()); + buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } private final JibLogger buildLogger; private final String registry; - @Nullable private final String credentialHelperSuffix; - @Nullable private final RegistryCredentials knownRegistryCredentials; - private final DockerCredentialHelperFactory dockerCredentialHelperFactory; - private final DockerConfigCredentialRetriever dockerConfigCredentialRetriever; + private final ImmutableList credentialRetrievers; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; @VisibleForTesting RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, JibLogger buildLogger, String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials, - DockerCredentialHelperFactory dockerCredentialHelperFactory, - DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { + ImmutableList credentialRetrievers) { this.buildLogger = buildLogger; this.registry = registry; - this.credentialHelperSuffix = credentialHelperSuffix; - this.knownRegistryCredentials = knownRegistryCredentials; - this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; - this.dockerConfigCredentialRetriever = dockerConfigCredentialRetriever; + this.credentialRetrievers = credentialRetrievers; listenableFuture = listeningExecutorService.submit(this); } - /** Instantiate with {@link #forBaseImage} or {@link #forTargetImage}. */ - private RetrieveRegistryCredentialsStep( - ListeningExecutorService listeningExecutorService, - JibLogger buildLogger, - String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials) { - this( - listeningExecutorService, - buildLogger, - registry, - credentialHelperSuffix, - knownRegistryCredentials, - new DockerCredentialHelperFactory(), - new DockerConfigCredentialRetriever(registry)); - } - @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override @Nullable - public Authorization call() throws IOException, NonexistentDockerCredentialHelperException { + public Credential call() throws CredentialRetrievalException { buildLogger.lifecycle(String.format(DESCRIPTION, registry) + "..."); try (Timer ignored = new Timer(buildLogger, String.format(DESCRIPTION, registry))) { - // Tries to get registry credentials from Docker credential helpers. - if (credentialHelperSuffix != null) { - Authorization authorization = retrieveFromCredentialHelper(credentialHelperSuffix); - if (authorization != null) { - return authorization; + for (CredentialRetriever credentialRetriever : credentialRetrievers) { + Credential credential = credentialRetriever.retrieve(); + if (credential != null) { + return credential; } } - // Tries to get registry credentials from known registry credentials. - if (knownRegistryCredentials != null) { - logGotCredentialsFrom(knownRegistryCredentials.getCredentialSource()); - return knownRegistryCredentials.getAuthorization(); - } - - // Tries to infer common credential helpers for known registries. - for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) { - if (registry.endsWith(registrySuffix)) { - try { - String commonCredentialHelper = COMMON_CREDENTIAL_HELPERS.get(registrySuffix); - if (commonCredentialHelper == null) { - throw new IllegalStateException("No COMMON_CREDENTIAL_HELPERS should be null"); - } - Authorization authorization = retrieveFromCredentialHelper(commonCredentialHelper); - if (authorization != null) { - return authorization; - } - - } catch (NonexistentDockerCredentialHelperException ex) { - if (ex.getMessage() != null) { - // Warns the user that the specified (or inferred) credential helper is not on the - // system. - buildLogger.warn(ex.getMessage()); - if (ex.getCause() != null && ex.getCause().getMessage() != null) { - buildLogger.info(" Caused by: " + ex.getCause().getMessage()); - } - } - } - } - } - - // Tries to get registry credentials from the Docker config. - try { - Credential dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); - if (dockerConfigCredentials != null) { - Authorization dockerConfigAuthorization = - Authorizations.withBasicCredentials( - dockerConfigCredentials.getUsername(), dockerConfigCredentials.getPassword()); - buildLogger.info("Using credentials from Docker config for " + registry); - return dockerConfigAuthorization; - } - - } catch (IOException ex) { - buildLogger.info("Unable to parse Docker config"); - } - /* * If no credentials found, give an info (not warning because in most cases, the base image is * public and does not need extra credentials) and return null. @@ -189,35 +100,4 @@ public Authorization call() throws IOException, NonexistentDockerCredentialHelpe return null; } } - - /** - * Attempts to retrieve authorization for the registry using {@code - * docker-credential-[credentialHelperSuffix]}. - */ - @VisibleForTesting - @Nullable - Authorization retrieveFromCredentialHelper(String credentialHelperSuffix) - throws NonexistentDockerCredentialHelperException, IOException { - buildLogger.info("Checking credentials from docker-credential-" + credentialHelperSuffix); - - try { - Credential credential = - dockerCredentialHelperFactory - .newDockerCredentialHelper(registry, credentialHelperSuffix) - .retrieve(); - Authorization authorization = - Authorizations.withBasicCredentials(credential.getUsername(), credential.getPassword()); - logGotCredentialsFrom("docker-credential-" + credentialHelperSuffix); - return authorization; - - } catch (NonexistentServerUrlDockerCredentialHelperException ex) { - buildLogger.info( - "No credentials for " + registry + " in docker-credential-" + credentialHelperSuffix); - return null; - } - } - - private void logGotCredentialsFrom(String credentialSource) { - buildLogger.info("Using " + credentialSource + " for " + registry); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java index f4005dffaf..9a715a192e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -22,6 +22,14 @@ @FunctionalInterface public interface CredentialRetriever { + /** Thrown if something went wrong during {@link CredentialRetriever#retrieve}. */ + class CredentialRetrievalException extends Exception { + + public CredentialRetrievalException(Throwable cause) { + super(cause); + } + } + /** * Fetches the credentials. Implementations must be thread-safe. * @@ -31,8 +39,8 @@ public interface CredentialRetriever { * * @return the fetched credentials or {@code null} if no credentials could be fetched with this * provider - * @throws Exception if the credential retrieval encountered an exception + * @throws CredentialRetrievalException if the credential retrieval encountered an exception */ @Nullable - Credential retrieve() throws Exception; + Credential retrieve() throws CredentialRetrievalException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 0d7bfbf205..41cfea1fde 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; @@ -169,6 +170,9 @@ CredentialRetriever inferCredentialHelper( logger.info(" Caused by: " + ex.getCause().getMessage()); } } + + } catch (NonexistentServerUrlDockerCredentialHelperException | IOException ex) { + throw new CredentialRetrievalException(ex); } } return null; @@ -188,6 +192,9 @@ CredentialRetriever dockerCredentialHelper( logger.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); return null; + + } catch (NonexistentDockerCredentialHelperException | IOException ex) { + throw new CredentialRetrievalException(ex); } }; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 949d8de4fa..61a2c0fa05 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -17,21 +17,17 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; -import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -42,141 +38,83 @@ @RunWith(MockitoJUnitRunner.class) public class RetrieveRegistryCredentialsStepTest { - private static final String FAKE_TARGET_REGISTRY = "someRegistry"; - private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); - private static final Authorization FAKE_AUTHORIZATION = - Authorizations.withBasicCredentials( - FAKE_CREDENTIAL.getUsername(), FAKE_CREDENTIAL.getPassword()); - @Mock private ListeningExecutorService mockListeningExecutorService; - @Mock private ImageConfiguration mockImageConfiguration; - @Mock private JibLogger mockBuildLogger; - - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; - @Mock private DockerCredentialHelper mockDockerCredentialHelper; - /** - * A {@link DockerCredentialHelper} that throws {@link - * NonexistentServerUrlDockerCredentialHelperException}. - */ - @Mock private DockerCredentialHelper mockNonexistentServerUrlDockerCredentialHelper; - /** - * A {@link DockerCredentialHelper} that throws {@link - * NonexistentDockerCredentialHelperException}. - */ - @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - - @Mock private Authorization mockAuthorization; - - @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; - - @Mock - private NonexistentServerUrlDockerCredentialHelperException - mockNonexistentServerUrlDockerCredentialHelperException; - - @Mock - private NonexistentDockerCredentialHelperException mockNonexistentDockerCredentialHelperException; - - @Before - public void setUpMocks() - throws NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); - Mockito.when(mockNonexistentServerUrlDockerCredentialHelper.retrieve()) - .thenThrow(mockNonexistentServerUrlDockerCredentialHelperException); - Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockNonexistentDockerCredentialHelperException); - } + @Mock private JibLogger mockJibLogger; @Test - public void testCall_useCredentialHelper() - throws IOException, NonexistentDockerCredentialHelperException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - FAKE_TARGET_REGISTRY, "someOtherCredentialHelper")) - .thenReturn(mockDockerCredentialHelper); + public void testCall_retrieved() throws CredentialRetrievalException { + BuildConfiguration buildConfiguration = + makeFakeBuildConfiguration( + Arrays.asList(() -> null, () -> new Credential("baseusername", "basepassword")), + Arrays.asList( + () -> new Credential("targetusername", "targetpassword"), + () -> new Credential("ignored", "ignored"))); Assert.assertEquals( - FAKE_AUTHORIZATION, - makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someOtherCredentialHelper", null) + new Credential("baseusername", "basepassword"), + RetrieveRegistryCredentialsStep.forBaseImage( + mockListeningExecutorService, buildConfiguration) .call()); - - Mockito.verify(mockBuildLogger) - .info("Using docker-credential-someOtherCredentialHelper for " + FAKE_TARGET_REGISTRY); - } - - @Test - public void testCall_useKnownRegistryCredentials() - throws IOException, NonexistentDockerCredentialHelperException { Assert.assertEquals( - mockAuthorization, - makeRetrieveRegistryCredentialsStep( - FAKE_TARGET_REGISTRY, - null, - new RegistryCredentials("credentialSource", mockAuthorization)) + new Credential("targetusername", "targetpassword"), + RetrieveRegistryCredentialsStep.forTargetImage( + mockListeningExecutorService, buildConfiguration) .call()); - - Mockito.verify(mockBuildLogger).info("Using credentialSource for " + FAKE_TARGET_REGISTRY); } @Test - public void testCall_useDockerConfig() - throws IOException, NonexistentDockerCredentialHelperException { - // Credential helper does not have credentials. - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - FAKE_TARGET_REGISTRY, "someCredentialHelper")) - .thenReturn(mockNonexistentServerUrlDockerCredentialHelper); - - Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(FAKE_CREDENTIAL); - - Assert.assertEquals( - FAKE_AUTHORIZATION, - makeRetrieveRegistryCredentialsStep(FAKE_TARGET_REGISTRY, "someCredentialHelper", null) + public void testCall_none() throws CredentialRetrievalException { + BuildConfiguration buildConfiguration = + makeFakeBuildConfiguration(Arrays.asList(() -> null, () -> null), Collections.emptyList()); + Assert.assertNull( + RetrieveRegistryCredentialsStep.forBaseImage( + mockListeningExecutorService, buildConfiguration) .call()); - - Mockito.verify(mockBuildLogger) - .info("Using credentials from Docker config for " + FAKE_TARGET_REGISTRY); + Mockito.verify(mockJibLogger) + .info("No credentials could be retrieved for registry baseregistry"); + Assert.assertNull( + RetrieveRegistryCredentialsStep.forTargetImage( + mockListeningExecutorService, buildConfiguration) + .call()); + Mockito.verify(mockJibLogger) + .info("No credentials could be retrieved for registry targetregistry"); } @Test - public void testCall_inferCommonCredentialHelpers() - throws IOException, NonexistentDockerCredentialHelperException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper("something.gcr.io", "gcr")) - .thenReturn(mockDockerCredentialHelper); - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "something.amazonaws.com", "ecr-login")) - .thenReturn(mockNonexistentDockerCredentialHelper); - - Assert.assertEquals( - FAKE_AUTHORIZATION, - makeRetrieveRegistryCredentialsStep("something.gcr.io", null, null).call()); - Mockito.verify(mockBuildLogger).info("Using docker-credential-gcr for something.gcr.io"); - - Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); - Mockito.when(mockNonexistentDockerCredentialHelperException.getCause()) - .thenReturn(new IOException("the root cause")); - Assert.assertNull( - makeRetrieveRegistryCredentialsStep("something.amazonaws.com", null, null).call()); - Mockito.verify(mockBuildLogger).warn("warning"); - Mockito.verify(mockBuildLogger).info(" Caused by: the root cause"); + public void testCall_exception() { + CredentialRetrievalException credentialRetrievalException = + Mockito.mock(CredentialRetrievalException.class); + BuildConfiguration buildConfiguration = + makeFakeBuildConfiguration( + Collections.singletonList( + () -> { + throw credentialRetrievalException; + }), + Collections.emptyList()); + try { + RetrieveRegistryCredentialsStep.forBaseImage(mockListeningExecutorService, buildConfiguration) + .call(); + Assert.fail("Should have thrown exception"); + + } catch (CredentialRetrievalException ex) { + Assert.assertSame(credentialRetrievalException, ex); + } } - /** Creates a fake {@link RetrieveRegistryCredentialsStep} for {@code registry}. */ - private RetrieveRegistryCredentialsStep makeRetrieveRegistryCredentialsStep( - String registry, - @Nullable String credentialHelperSuffix, - @Nullable RegistryCredentials knownRegistryCredentials) { - Mockito.when(mockImageConfiguration.getImageRegistry()).thenReturn(FAKE_TARGET_REGISTRY); - - return new RetrieveRegistryCredentialsStep( - mockListeningExecutorService, - mockBuildLogger, - registry, - credentialHelperSuffix, - knownRegistryCredentials, - mockDockerCredentialHelperFactory, - mockDockerConfigCredentialRetriever); + private BuildConfiguration makeFakeBuildConfiguration( + List baseCredentialRetrievers, + List targetCredentialRetrievers) { + ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); + ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); + return BuildConfiguration.builder(mockJibLogger) + .setBaseImageConfiguration( + ImageConfiguration.builder(baseImage) + .setCredentialRetrievers(baseCredentialRetrievers) + .build()) + .setTargetImageConfiguration( + ImageConfiguration.builder(targetImage) + .setCredentialRetrievers(targetCredentialRetrievers) + .build()) + .build(); } } From ba180eefe6787507dd5300b01f53a6244418bd07 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 16 Aug 2018 15:31:21 -0400 Subject: [PATCH 0138/2020] Change missing build output directory message (#843) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/gradle/GradleLayerConfigurations.java | 6 +++--- .../tools/jib/gradle/GradleLayerConfigurationsTest.java | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 26958d78e8..1866122a56 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Changed - Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) +- Changed logging level of missing build output directory message ([#677](https://github.com/GoogleContainerTools/jib/issues/677)) ### Fixed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index d4534aa4af..67a01f17df 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -61,13 +61,13 @@ static JavaLayerConfigurations getForProject( // Adds each file in each classes output directory to the classes files list. FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); + gradleJibLogger.info("Adding corresponding output directories of source sets to image"); for (File classesOutputDirectory : classesOutputDirectories) { if (Files.notExists(classesOutputDirectory.toPath())) { - // Warns that output directory was not found. - gradleJibLogger.warn( - "Could not find build output directory '" + classesOutputDirectory + "'"); + gradleJibLogger.info("\t'" + classesOutputDirectory + "' (not found, skipped)"); continue; } + gradleJibLogger.info("\t'" + classesOutputDirectory + "'"); try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { classFileStream.forEach(classesFiles::add); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index c3e2fdd964..c06b5bf93e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -162,7 +162,8 @@ public void test_noClassesFiles() throws IOException { mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); Mockito.verify(mockGradleJibLogger) - .warn("Could not find build output directory '" + nonexistentFile + "'"); + .info("Adding corresponding output directories of source sets to image"); + Mockito.verify(mockGradleJibLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); Mockito.verify(mockGradleJibLogger) .warn("No classes files were found - did you compile your project?"); } From 876975ec70b58fdc7d12e347eb7ae6c0e98c0447 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 20 Aug 2018 14:01:38 -0400 Subject: [PATCH 0139/2020] Maven release v0.9.9 (#857) * preparing release 0.9.9 * 0.9.10-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 6d9f5f84c3..2012e9c359 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.9-SNAPSHOT + 0.9.10-SNAPSHOT maven-plugin Jib From 37edc6b46df638d699e86cc2f0000546280a97f3 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 20 Aug 2018 14:35:57 -0400 Subject: [PATCH 0140/2020] Gradle release v0.9.9 (#858) * [Gradle Release Plugin] - pre tag commit: 'v0.9.9-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.9.10-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 23ced72bf4..40d318afdc 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.9-SNAPSHOT +version = 0.9.10-SNAPSHOT From 81639cc5155cf9da45872457b57fa3f8675c3857 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 20 Aug 2018 15:02:46 -0400 Subject: [PATCH 0141/2020] Removes setCredHelper, setKnownRegistryCredentials. (#851) --- .../jib/configuration/ImageConfiguration.java | 49 +------ .../configuration/BuildConfigurationTest.java | 23 +--- .../tools/jib/gradle/BuildDockerTask.java | 21 ++- .../tools/jib/gradle/BuildImageTask.java | 32 ++--- .../cloud/tools/jib/gradle/BuildTarTask.java | 17 ++- .../tools/jib/gradle/DockerContextTask.java | 16 ++- .../GradleHelpfulSuggestionsBuilder.java | 79 +++++++++++ .../jib/gradle/GradleProjectProperties.java | 6 - .../gradle/HelpfulSuggestionsProvider.java | 42 ------ .../gradle/PluginConfigurationProcessor.java | 29 ++-- .../tools/jib/maven/BuildDockerMojo.java | 21 ++- .../cloud/tools/jib/maven/BuildImageMojo.java | 37 +++-- .../cloud/tools/jib/maven/BuildTarMojo.java | 18 ++- .../tools/jib/maven/DockerContextMojo.java | 15 +- .../jib/maven/HelpfulSuggestionsProvider.java | 46 ------- .../maven/MavenHelpfulSuggestionsBuilder.java | 91 ++++++++++++ .../jib/maven/MavenProjectProperties.java | 6 - .../maven/PluginConfigurationProcessor.java | 32 ++--- .../jib/plugins/common/BuildStepsRunner.java | 48 +------ .../plugins/common/HelpfulSuggestions.java | 130 ++++++++++++------ .../jib/plugins/common/MainClassResolver.java | 19 ++- .../jib/plugins/common/ProjectProperties.java | 6 - .../plugins/common/BuildStepsRunnerTest.java | 76 +--------- .../ConfigurationPropertyValidatorTest.java | 15 +- .../common/HelpfulSuggestionsTest.java | 23 ++-- .../plugins/common/MainClassResolverTest.java | 15 +- 26 files changed, 448 insertions(+), 464 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index d9e7f02f13..9f4f4de9ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -18,11 +18,9 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.List; -import javax.annotation.Nullable; /** Immutable configuration options for an image reference with credentials. */ public class ImageConfiguration { @@ -31,33 +29,8 @@ public class ImageConfiguration { public static class Builder { private ImageReference imageReference; - @Nullable private String credentialHelper; - @Nullable private RegistryCredentials knownRegistryCredentials; private ImmutableList credentialRetrievers = ImmutableList.of(); - /** - * Sets the credential helper name used for authenticating with the image's registry. - * - * @param credentialHelper the credential helper's suffix - * @return this - */ - public Builder setCredentialHelper(@Nullable String credentialHelper) { - this.credentialHelper = credentialHelper; - return this; - } - - /** - * Sets known credentials used for authenticating with the image's registry. - * - * @param knownRegistryCredentials the credentials - * @return this - */ - public Builder setKnownRegistryCredentials( - @Nullable RegistryCredentials knownRegistryCredentials) { - this.knownRegistryCredentials = knownRegistryCredentials; - return this; - } - /** * Sets the providers for registry credentials. The order determines the priority in which the * retrieval methods are attempted. @@ -77,8 +50,7 @@ public Builder setCredentialRetrievers(List credentialRetri * @return the corresponding {@link ImageConfiguration} */ public ImageConfiguration build() { - return new ImageConfiguration( - imageReference, credentialHelper, knownRegistryCredentials, credentialRetrievers); + return new ImageConfiguration(imageReference, credentialRetrievers); } private Builder(ImageReference imageReference) { @@ -97,18 +69,11 @@ public static Builder builder(ImageReference imageReference) { } private final ImageReference image; - @Nullable private final String credentialHelper; - @Nullable private final RegistryCredentials knownRegistryCredentials; private final ImmutableList credentialRetrievers; private ImageConfiguration( - ImageReference image, - @Nullable String credentialHelper, - @Nullable RegistryCredentials knownRegistryCredentials, - ImmutableList credentialRetrievers) { + ImageReference image, ImmutableList credentialRetrievers) { this.image = image; - this.credentialHelper = credentialHelper; - this.knownRegistryCredentials = knownRegistryCredentials; this.credentialRetrievers = credentialRetrievers; } @@ -128,16 +93,6 @@ public String getImageTag() { return image.getTag(); } - @Nullable - public String getCredentialHelper() { - return credentialHelper; - } - - @Nullable - public RegistryCredentials getKnownRegistryCredentials() { - return knownRegistryCredentials; - } - public ImmutableList getCredentialRetrievers() { return credentialRetrievers; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index ebaa7befb7..f82652c2c4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.nio.file.Paths; @@ -37,6 +36,7 @@ import org.junit.Test; import org.mockito.Mockito; +/** Tests for {@link BuildConfiguration}. */ public class BuildConfigurationTest { @Test @@ -44,15 +44,9 @@ public void testBuilder() throws Exception { String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; String expectedBaseImageTag = "baseimagetag"; - String expectedBaseImageCredentialHelperName = "credentialhelper"; - RegistryCredentials expectedKnownBaseRegistryCredentials = - Mockito.mock(RegistryCredentials.class); String expectedTargetServerUrl = "someotherserver"; String expectedTargetImageName = "targetimage"; String expectedTargetTag = "targettag"; - String expectedTargetImageCredentialHelperName = "anotherCredentialHelper"; - RegistryCredentials expectedKnownTargetRegistryCredentials = - Mockito.mock(RegistryCredentials.class); List credentialRetrievers = Collections.singletonList(() -> new Credential("username", "password")); Instant expectedCreationTime = Instant.ofEpochSecond(10000); @@ -75,15 +69,11 @@ public void testBuilder() throws Exception { ImageConfiguration.builder( ImageReference.of( expectedBaseImageServerUrl, expectedBaseImageName, expectedBaseImageTag)) - .setCredentialHelper(expectedBaseImageCredentialHelperName) - .setKnownRegistryCredentials(expectedKnownBaseRegistryCredentials) .build(); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder( ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) - .setCredentialHelper(expectedTargetImageCredentialHelperName) - .setKnownRegistryCredentials(expectedKnownTargetRegistryCredentials) .setCredentialRetrievers(credentialRetrievers) .build(); ContainerConfiguration containerConfiguration = @@ -117,9 +107,6 @@ public void testBuilder() throws Exception { expectedBaseImageName, buildConfiguration.getBaseImageConfiguration().getImageRepository()); Assert.assertEquals( expectedBaseImageTag, buildConfiguration.getBaseImageConfiguration().getImageTag()); - Assert.assertEquals( - expectedBaseImageCredentialHelperName, - buildConfiguration.getBaseImageConfiguration().getCredentialHelper()); Assert.assertEquals( expectedTargetServerUrl, buildConfiguration.getTargetImageConfiguration().getImageRegistry()); @@ -128,9 +115,6 @@ public void testBuilder() throws Exception { buildConfiguration.getTargetImageConfiguration().getImageRepository()); Assert.assertEquals( expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); - Assert.assertEquals( - expectedTargetImageCredentialHelperName, - buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); Assert.assertEquals( new Credential("username", "password"), buildConfiguration @@ -185,11 +169,6 @@ public void testBuilder_default() { .setTargetImageConfiguration(targetImageConfiguration) .build(); - Assert.assertNull(buildConfiguration.getBaseImageConfiguration().getCredentialHelper()); - Assert.assertNull(buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials()); - Assert.assertNull(buildConfiguration.getTargetImageConfiguration().getCredentialHelper()); - Assert.assertNull( - buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 84c53fc6b0..ea8629ba28 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -37,8 +37,7 @@ /** Builds a container image and exports to the default Docker daemon. */ public class BuildDockerTask extends DefaultTask { - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build to Docker daemon failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; @Nullable private JibExtension jibExtension; @@ -67,7 +66,8 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildDocker() throws InvalidImageReferenceException { if (!new DockerClient().isDockerInstalled()) { - throw new GradleException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); + throw new GradleException( + HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } // Asserts required @Input parameters are not null. @@ -77,13 +77,16 @@ public void buildDocker() throws InvalidImageReferenceException { GradleProjectProperties.getForProject( getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); + ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), gradleJibLogger, getProject().getName(), getProject().getVersion().toString(), - HELPFUL_SUGGESTIONS); + gradleHelpfulSuggestionsBuilder.build()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( @@ -99,9 +102,17 @@ public void buildDocker() throws InvalidImageReferenceException { pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); + HelpfulSuggestions helpfulSuggestions = + gradleHelpfulSuggestionsBuilder + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .build(); + // Uses a directory in the Gradle build cache as the Jib cache. try { - BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { throw new GradleException(ex.getMessage(), ex.getCause()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index a5d3743afb..194633b209 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -22,14 +22,12 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.util.ArrayList; @@ -44,8 +42,7 @@ /** Builds a container image. */ public class BuildImageTask extends DefaultTask { - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build image failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build image failed"; @Nullable private JibExtension jibExtension; @@ -82,9 +79,11 @@ public void buildImage() throws InvalidImageReferenceException { if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { throw new GradleException( - HelpfulSuggestionsProvider.get("Missing target image parameter") - .forToNotConfigured( - "'jib.to.image'", "build.gradle", "gradle jib --image ")); + HelpfulSuggestions.forToNotConfigured( + "Missing target image parameter", + "'jib.to.image'", + "build.gradle", + "gradle jib --image ")); } ImageReference targetImage = ImageReference.parse(jibExtension.getTargetImage()); @@ -97,14 +96,8 @@ public void buildImage() throws InvalidImageReferenceException { "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); - RegistryCredentials knownTargetRegistryCredentials = null; CredentialRetriever knownCredentialRetriever = null; if (toCredential != null) { - knownTargetRegistryCredentials = - new RegistryCredentials( - "jib.to.auth", - Authorizations.withBasicCredentials( - toCredential.getUsername(), toCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known(toCredential, "jib.to.auth"); } // Makes credential retriever list. @@ -122,8 +115,6 @@ public void buildImage() throws InvalidImageReferenceException { ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) - .setCredentialHelper(jibExtension.getTo().getCredHelper()) - .setKnownRegistryCredentials(knownTargetRegistryCredentials) .setCredentialRetrievers(credentialRetrievers) .build(); @@ -142,8 +133,17 @@ public void buildImage() throws InvalidImageReferenceException { .setTargetFormat(jibExtension.getFormat()) .build(); + HelpfulSuggestions helpfulSuggestions = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageHasConfiguredCredentials(toCredential != null) + .build(); + try { - BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { throw new GradleException(ex.getMessage(), ex.getCause()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 3829ae53e7..9b722be8d0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -40,8 +40,7 @@ /** Builds a container image to a tarball. */ public class BuildTarTask extends DefaultTask { - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Building image tarball failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Building image tarball failed"; @Nullable private JibExtension jibExtension; @@ -105,13 +104,15 @@ public void buildTar() throws InvalidImageReferenceException { GradleProjectProperties.getForProject( getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), gradleJibLogger, getProject().getName(), getProject().getVersion().toString(), - HELPFUL_SUGGESTIONS); + gradleHelpfulSuggestionsBuilder.build()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( @@ -127,10 +128,18 @@ public void buildTar() throws InvalidImageReferenceException { pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); + HelpfulSuggestions helpfulSuggestions = + gradleHelpfulSuggestionsBuilder + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .build(); + // Uses a directory in the Gradle build cache as the Jib cache. try { BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) - .build(HELPFUL_SUGGESTIONS); + .build(helpfulSuggestions); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { throw new GradleException(ex.getMessage(), ex.getCause()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 37469bf578..1ce773c093 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; @@ -126,17 +127,18 @@ public void generateDockerContext() { } catch (InsecureRecursiveDeleteException ex) { throw new GradleException( - HelpfulSuggestionsProvider.get( - "Export Docker context failed because cannot clear directory '" - + getTargetDir() - + "' safely") - .forDockerContextInsecureRecursiveDelete(getTargetDir()), + HelpfulSuggestions.forDockerContextInsecureRecursiveDelete( + "Export Docker context failed because cannot clear directory '" + + getTargetDir() + + "' safely", + getTargetDir()), ex); } catch (IOException ex) { throw new GradleException( - HelpfulSuggestionsProvider.get("Export Docker context failed") - .suggest("check if the command-line option `--jibTargetDir` is set correctly"), + HelpfulSuggestions.suggest( + "Export Docker context failed", + "check if the command-line option `--jibTargetDir` is set correctly"), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java new file mode 100644 index 0000000000..ca978ef3bb --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import javax.annotation.Nullable; + +/** Builder for Gradle-specific {@link HelpfulSuggestions}. */ +class GradleHelpfulSuggestionsBuilder { + + private final String messagePrefix; + private final JibExtension jibExtension; + + @Nullable private ImageReference baseImageReference; + @Nullable private ImageReference targetImageReference; + private boolean baseImageHasConfiguredCredentials; + private boolean targetImageHasConfiguredCredentials; + + GradleHelpfulSuggestionsBuilder(String messagePrefix, JibExtension jibExtension) { + this.messagePrefix = messagePrefix; + this.jibExtension = jibExtension; + } + + GradleHelpfulSuggestionsBuilder setBaseImageReference(ImageReference baseImageReference) { + this.baseImageReference = baseImageReference; + return this; + } + + GradleHelpfulSuggestionsBuilder setBaseImageHasConfiguredCredentials( + boolean areKnownCredentialsDefined) { + baseImageHasConfiguredCredentials = areKnownCredentialsDefined; + return this; + } + + GradleHelpfulSuggestionsBuilder setTargetImageReference(ImageReference targetImageReference) { + this.targetImageReference = targetImageReference; + return this; + } + + GradleHelpfulSuggestionsBuilder setTargetImageHasConfiguredCredentials( + boolean areKnownCredentialsDefined) { + targetImageHasConfiguredCredentials = areKnownCredentialsDefined; + return this; + } + + HelpfulSuggestions build() { + boolean isCredHelperDefinedForBaseImage = jibExtension.getFrom().getCredHelper() != null; + boolean isCredHelperDefinedForTargetImage = jibExtension.getTo().getCredHelper() != null; + return new HelpfulSuggestions( + messagePrefix, + "gradle clean", + baseImageReference, + !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, + "from.credHelper", + ignored -> "from.auth", + targetImageReference, + !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, + "to.credHelper", + ignored -> "to.auth", + "jib.to.image", + "--image", + "build.gradle"); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 7224362b64..273932180f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; @@ -76,11 +75,6 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } - @Override - public HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix) { - return HelpfulSuggestionsProvider.get(prefix); - } - @Override public JibLogger getLogger() { return gradleJibLogger; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java deleted file mode 100644 index 02c24f3ff6..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/HelpfulSuggestionsProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; - -/** Provider for Maven-specific {@link HelpfulSuggestions}. */ -class HelpfulSuggestionsProvider { - - /** - * @param messagePrefix the prefix - * @return a new {@link HelpfulSuggestions} with the specified message prefix - */ - static HelpfulSuggestions get(String messagePrefix) { - return new HelpfulSuggestions( - messagePrefix, - "gradle clean", - "from.credHelper", - ignored -> "from.auth", - "to.credHelper", - ignored -> "to.auth", - "jib.to.image", - "--image", - "build.gradle"); - } - - private HelpfulSuggestionsProvider() {} -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index c0b46833aa..8c69d2dc33 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -26,15 +26,14 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; import org.gradle.api.GradleException; /** Configures and provides builders for the image building tasks. */ @@ -79,14 +78,8 @@ static PluginConfigurationProcessor processCommonConfiguration( "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); - RegistryCredentials knownBaseRegistryCredentials = null; CredentialRetriever knownCredentialRetriever = null; if (fromCredential != null) { - knownBaseRegistryCredentials = - new RegistryCredentials( - "jib.from.auth", - Authorizations.withBasicCredentials( - fromCredential.getUsername(), fromCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known(fromCredential, "jib.from.auth"); } // Makes credential retriever list. @@ -103,10 +96,7 @@ static PluginConfigurationProcessor processCommonConfiguration( credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration.Builder baseImageConfigurationBuilder = - ImageConfiguration.builder(baseImage) - .setCredentialHelper(credentialHelperSuffix) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) - .setCredentialRetrievers(credentialRetrievers); + ImageConfiguration.builder(baseImage).setCredentialRetrievers(credentialRetrievers); String mainClass = projectProperties.getMainClass(jibExtension); ContainerConfiguration.Builder containerConfigurationBuilder = @@ -137,20 +127,26 @@ static PluginConfigurationProcessor processCommonConfiguration( } return new PluginConfigurationProcessor( - buildConfigurationBuilder, baseImageConfigurationBuilder, containerConfigurationBuilder); + buildConfigurationBuilder, + baseImageConfigurationBuilder, + containerConfigurationBuilder, + fromCredential); } private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; + @Nullable private final Credential baseImageCredential; private PluginConfigurationProcessor( BuildConfiguration.Builder buildConfigurationBuilder, ImageConfiguration.Builder baseImageConfigurationBuilder, - ContainerConfiguration.Builder containerConfigurationBuilder) { + ContainerConfiguration.Builder containerConfigurationBuilder, + @Nullable Credential baseImageCredential) { this.buildConfigurationBuilder = buildConfigurationBuilder; this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; this.containerConfigurationBuilder = containerConfigurationBuilder; + this.baseImageCredential = baseImageCredential; } BuildConfiguration.Builder getBuildConfigurationBuilder() { @@ -164,4 +160,9 @@ ImageConfiguration.Builder getBaseImageConfigurationBuilder() { ContainerConfiguration.Builder getContainerConfigurationBuilder() { return containerConfigurationBuilder; } + + @Nullable + Credential getBaseImageCredential() { + return baseImageCredential; + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index eb17b42bea..5cecb6ed35 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -39,8 +39,7 @@ public class BuildDockerMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build to Docker daemon failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; @Override public void execute() throws MojoExecutionException { @@ -50,7 +49,8 @@ public void execute() throws MojoExecutionException { } if (!new DockerClient().isDockerInstalled()) { - throw new MojoExecutionException(HELPFUL_SUGGESTIONS.forDockerNotInstalled()); + throw new MojoExecutionException( + HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); @@ -58,13 +58,16 @@ public void execute() throws MojoExecutionException { MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); try { + MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); + ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), mavenJibLogger, getProject().getName(), getProject().getVersion(), - HELPFUL_SUGGESTIONS); + mavenHelpfulSuggestionsBuilder.build()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( @@ -80,7 +83,15 @@ public void execute() throws MojoExecutionException { pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); - BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(HELPFUL_SUGGESTIONS); + HelpfulSuggestions helpfulSuggestions = + mavenHelpfulSuggestionsBuilder + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .build(); + + BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); getLog().info(""); } catch (CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c66a13aa8a..c95ddc340b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -22,14 +22,12 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.util.ArrayList; @@ -48,8 +46,7 @@ public class BuildImageMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "build"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Build image failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build image failed"; @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -73,9 +70,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Parses 'to' into image reference. if (Strings.isNullOrEmpty(getTargetImage())) { throw new MojoFailureException( - HelpfulSuggestionsProvider.get("Missing target image parameter") - .forToNotConfigured( - "", "pom.xml", "mvn compile jib:build -Dimage=")); + HelpfulSuggestions.forToNotConfigured( + "Missing target image parameter", + "", + "pom.xml", + "mvn compile jib:build -Dimage=")); } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); @@ -93,7 +92,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { Credential toCredential = ConfigurationPropertyValidator.getImageCredential( mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); - RegistryCredentials knownTargetRegistryCredentials = null; CredentialRetriever knownCredentialRetriever = null; if (toCredential == null) { toCredential = @@ -101,21 +99,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { .getMavenSettingsServerCredentials() .retrieve(targetImage.getRegistry()); if (toCredential != null) { - knownTargetRegistryCredentials = - new RegistryCredentials( - MavenSettingsServerCredentials.CREDENTIAL_SOURCE, - Authorizations.withBasicCredentials( - toCredential.getUsername(), toCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known( toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { - knownTargetRegistryCredentials = - new RegistryCredentials( - "jib-maven-plugin configuration", - Authorizations.withBasicCredentials( - toCredential.getUsername(), toCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known( toCredential, "jib-maven-plugin configuration"); @@ -134,8 +122,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) - .setCredentialHelper(getTargetImageCredentialHelperName()) - .setKnownRegistryCredentials(knownTargetRegistryCredentials) .setCredentialRetrievers(credentialRetrievers) .build(); @@ -150,8 +136,17 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) .build(); + HelpfulSuggestions helpfulSuggestions = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageHasConfiguredCredentials(toCredential != null) + .build(); + try { - BuildStepsRunner.forBuildImage(buildConfiguration).build(HELPFUL_SUGGESTIONS); + BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); getLog().info(""); } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 9524bea29a..c27ee3a5e7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -41,8 +41,7 @@ public class BuildTarMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "buildTar"; - private static final HelpfulSuggestions HELPFUL_SUGGESTIONS = - HelpfulSuggestionsProvider.get("Building image tarball failed"); + private static final String HELPFUL_SUGGESTIONS_PREFIX = "Building image tarball failed"; @Override public void execute() throws MojoExecutionException { @@ -56,13 +55,16 @@ public void execute() throws MojoExecutionException { MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); try { + MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); + ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), mavenJibLogger, getProject().getName(), getProject().getVersion(), - HELPFUL_SUGGESTIONS); + mavenHelpfulSuggestionsBuilder.build()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( @@ -78,10 +80,18 @@ public void execute() throws MojoExecutionException { pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); + HelpfulSuggestions helpfulSuggestions = + mavenHelpfulSuggestionsBuilder + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.getBaseImageCredential() != null) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .build(); + BuildStepsRunner.forBuildTar( Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), buildConfiguration) - .build(HELPFUL_SUGGESTIONS); + .build(helpfulSuggestions); getLog().info(""); } catch (CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index aedf4bc2c4..0fdfd38448 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -79,17 +80,17 @@ public void execute() throws MojoExecutionException { } catch (InsecureRecursiveDeleteException ex) { throw new MojoExecutionException( - HelpfulSuggestionsProvider.get( - "Export Docker context failed because cannot clear directory '" - + targetDir - + "' safely") - .forDockerContextInsecureRecursiveDelete(targetDir), + HelpfulSuggestions.forDockerContextInsecureRecursiveDelete( + "Export Docker context failed because cannot clear directory '" + + targetDir + + "' safely", + targetDir), ex); } catch (IOException ex) { throw new MojoExecutionException( - HelpfulSuggestionsProvider.get("Export Docker context failed") - .suggest("check if `targetDir` is set correctly"), + HelpfulSuggestions.suggest( + "Export Docker context failed", "check if `targetDir` is set correctly"), ex); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java deleted file mode 100644 index fe066fc5cc..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/HelpfulSuggestionsProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import java.util.function.Function; - -/** Provider for Maven-specific {@link HelpfulSuggestions}. */ -class HelpfulSuggestionsProvider { - - private static final Function AUTH_CONFIGURATION_SUGGESTION = - registry -> "set credentials for '" + registry + "' in your Maven settings"; - - /** - * @param messagePrefix the prefix - * @return a new {@link HelpfulSuggestions} with the specified message prefix - */ - static HelpfulSuggestions get(String messagePrefix) { - return new HelpfulSuggestions( - messagePrefix, - "mvn clean", - "", - AUTH_CONFIGURATION_SUGGESTION, - "", - AUTH_CONFIGURATION_SUGGESTION, - "", - "-Dimage", - "pom.xml"); - } - - private HelpfulSuggestionsProvider() {} -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java new file mode 100644 index 0000000000..4c6ea09a8a --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** Builder for Maven-specific {@link HelpfulSuggestions}. */ +class MavenHelpfulSuggestionsBuilder { + + private static final Function AUTH_CONFIGURATION_SUGGESTION = + registry -> "set credentials for '" + registry + "' in your Maven settings"; + + private final String messagePrefix; + private final JibPluginConfiguration jibPluginConfiguration; + + @Nullable private ImageReference baseImageReference; + @Nullable private ImageReference targetImageReference; + private boolean baseImageHasConfiguredCredentials; + private boolean targetImageHasConfiguredCredentials; + + MavenHelpfulSuggestionsBuilder( + String messagePrefix, JibPluginConfiguration jibPluginConfiguration) { + this.messagePrefix = messagePrefix; + this.jibPluginConfiguration = jibPluginConfiguration; + } + + MavenHelpfulSuggestionsBuilder setBaseImageReference(ImageReference baseImageReference) { + this.baseImageReference = baseImageReference; + return this; + } + + MavenHelpfulSuggestionsBuilder setBaseImageHasConfiguredCredentials( + boolean areKnownCredentialsDefined) { + baseImageHasConfiguredCredentials = areKnownCredentialsDefined; + return this; + } + + MavenHelpfulSuggestionsBuilder setTargetImageReference(ImageReference targetImageReference) { + this.targetImageReference = targetImageReference; + return this; + } + + MavenHelpfulSuggestionsBuilder setTargetImageHasConfiguredCredentials( + boolean areKnownCredentialsDefined) { + targetImageHasConfiguredCredentials = areKnownCredentialsDefined; + return this; + } + + /** + * Builds the {@link HelpfulSuggestions}. + * + * @return the {@link HelpfulSuggestions} + */ + HelpfulSuggestions build() { + boolean isCredHelperDefinedForBaseImage = + jibPluginConfiguration.getTargetImageCredentialHelperName() != null; + boolean isCredHelperDefinedForTargetImage = + jibPluginConfiguration.getTargetImageCredentialHelperName() != null; + return new HelpfulSuggestions( + messagePrefix, + "mvn clean", + baseImageReference, + !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, + "", + AUTH_CONFIGURATION_SUGGESTION, + targetImageReference, + !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, + "", + AUTH_CONFIGURATION_SUGGESTION, + "", + "-Dimage", + "pom.xml"); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index ffada9502f..a26c5ea09d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; @@ -79,11 +78,6 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } - @Override - public HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix) { - return HelpfulSuggestionsProvider.get(prefix); - } - @Override public JibLogger getLogger() { return mavenJibLogger; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 92691a8603..33357e8f1c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -25,16 +25,15 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; /** Configures and provides builders for the image building goals. */ @@ -88,26 +87,15 @@ static PluginConfigurationProcessor processCommonConfiguration( "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); - RegistryCredentials knownBaseRegistryCredentials = null; CredentialRetriever knownCredentialRetriever = null; if (fromCredential == null) { fromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); if (fromCredential != null) { - knownBaseRegistryCredentials = - new RegistryCredentials( - MavenSettingsServerCredentials.CREDENTIAL_SOURCE, - Authorizations.withBasicCredentials( - fromCredential.getUsername(), fromCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known( fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { - knownBaseRegistryCredentials = - new RegistryCredentials( - "jib-maven-plugin configuration", - Authorizations.withBasicCredentials( - fromCredential.getUsername(), fromCredential.getPassword())); knownCredentialRetriever = credentialRetrieverFactory.known( fromCredential, "jib-maven-plugin configuration"); @@ -126,10 +114,7 @@ static PluginConfigurationProcessor processCommonConfiguration( credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration.Builder baseImageConfiguration = - ImageConfiguration.builder(baseImage) - .setCredentialHelper(credentialHelperSuffix) - .setKnownRegistryCredentials(knownBaseRegistryCredentials) - .setCredentialRetrievers(credentialRetrievers); + ImageConfiguration.builder(baseImage).setCredentialRetrievers(credentialRetrievers); String mainClass = projectProperties.getMainClass(jibPluginConfiguration); ContainerConfiguration.Builder containerConfigurationBuilder = @@ -164,7 +149,8 @@ static PluginConfigurationProcessor processCommonConfiguration( buildConfigurationBuilder, baseImageConfiguration, containerConfigurationBuilder, - mavenSettingsServerCredentials); + mavenSettingsServerCredentials, + fromCredential); } /** @@ -184,16 +170,19 @@ static ImageReference parseImageReference(String image, String type) { private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; private final MavenSettingsServerCredentials mavenSettingsServerCredentials; + @Nullable private final Credential baseImageCredential; private PluginConfigurationProcessor( BuildConfiguration.Builder buildConfigurationBuilder, ImageConfiguration.Builder baseImageConfigurationBuilder, ContainerConfiguration.Builder containerConfigurationBuilder, - MavenSettingsServerCredentials mavenSettingsServerCredentials) { + MavenSettingsServerCredentials mavenSettingsServerCredentials, + @Nullable Credential baseImageCredential) { this.buildConfigurationBuilder = buildConfigurationBuilder; this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; this.containerConfigurationBuilder = containerConfigurationBuilder; this.mavenSettingsServerCredentials = mavenSettingsServerCredentials; + this.baseImageCredential = baseImageCredential; } BuildConfiguration.Builder getBuildConfigurationBuilder() { @@ -211,4 +200,9 @@ ContainerConfiguration.Builder getContainerConfigurationBuilder() { MavenSettingsServerCredentials getMavenSettingsServerCredentials() { return mavenSettingsServerCredentials; } + + @Nullable + Credential getBaseImageCredential() { + return baseImageCredential; + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 78595c32da..513fc74182 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -109,7 +109,6 @@ private static Initializer getCacheInitializer(BuildConfiguration buildConfigura private static void handleRegistryUnauthorizedException( RegistryUnauthorizedException registryUnauthorizedException, - BuildConfiguration buildConfiguration, HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { if (registryUnauthorizedException.getHttpResponseException().getStatusCode() @@ -121,46 +120,10 @@ private static void handleRegistryUnauthorizedException( registryUnauthorizedException); } else { - boolean isImageForBase = - registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getBaseImageConfiguration().getImageRegistry()) - && registryUnauthorizedException - .getRepository() - .equals(buildConfiguration.getBaseImageConfiguration().getImageRepository()); - boolean isImageForTarget = - registryUnauthorizedException - .getRegistry() - .equals(buildConfiguration.getTargetImageConfiguration().getImageRegistry()) - && registryUnauthorizedException - .getRepository() - .equals(buildConfiguration.getTargetImageConfiguration().getImageRepository()); - boolean areBaseImageCredentialsConfigured = - buildConfiguration.getBaseImageConfiguration().getCredentialHelper() != null - || buildConfiguration.getBaseImageConfiguration().getKnownRegistryCredentials() - != null; - boolean areTargetImageCredentialsConfigured = - buildConfiguration.getTargetImageConfiguration().getCredentialHelper() != null - || buildConfiguration.getTargetImageConfiguration().getKnownRegistryCredentials() - != null; - - if (isImageForBase && !areBaseImageCredentialsConfigured) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forNoCredentialHelpersDefinedForBaseImage( - registryUnauthorizedException.getRegistry()), - registryUnauthorizedException); - } - if (isImageForTarget && !areTargetImageCredentialsConfigured) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forNoCredentialHelpersDefinedForTargetImage( - registryUnauthorizedException.getRegistry()), - registryUnauthorizedException); - } - - // Credential helper probably was not configured correctly or did not have the necessary - // credentials. throw new BuildStepsExecutionException( - helpfulSuggestions.forCredentialsNotCorrect(registryUnauthorizedException.getRegistry()), + helpfulSuggestions.forNoCredentialsDefined( + registryUnauthorizedException.getRegistry(), + registryUnauthorizedException.getRepository()), registryUnauthorizedException); } } @@ -228,9 +191,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut } else if (exceptionDuringBuildSteps instanceof RegistryUnauthorizedException) { handleRegistryUnauthorizedException( - (RegistryUnauthorizedException) exceptionDuringBuildSteps, - buildConfiguration, - helpfulSuggestions); + (RegistryUnauthorizedException) exceptionDuringBuildSteps, helpfulSuggestions); } else if (exceptionDuringBuildSteps instanceof RegistryCredentialsNotSentException) { throw new BuildStepsExecutionException( @@ -243,7 +204,6 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository(), (HttpResponseException) exceptionDuringBuildSteps.getCause()), - buildConfiguration, helpfulSuggestions); } else if (exceptionDuringBuildSteps instanceof UnknownHostException) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 2dd41232da..dd2dfea9bc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -16,16 +16,67 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.common.base.Preconditions; import java.nio.file.Path; import java.util.function.Function; +import javax.annotation.Nullable; /** Builds messages that provides suggestions on how to fix the error. */ public class HelpfulSuggestions { + /** + * @param messagePrefix the initial message text + * @param parameter the parameter name (e.g. 'to.image' or {@literal }) + * @param buildConfigFilename the name of the build config (build.gradle or pom.xml) + * @param command an example command for passing the parameter via commandline + * @return a suggested fix for a missing target image configuration + */ + public static String forToNotConfigured( + String messagePrefix, String parameter, String buildConfigFilename, String command) { + return suggest( + messagePrefix, + "add a " + + parameter + + " configuration parameter to your " + + buildConfigFilename + + " or set the parameter via the commandline (e.g. '" + + command + + "')."); + } + + public static String forDockerNotInstalled(String messagePrefix) { + return suggest( + messagePrefix, "make sure Docker is installed and you have correct privileges to run it"); + } + + public static String forMainClassNotFound(String messagePrefix, String pluginName) { + return suggest(messagePrefix, "add a `mainClass` configuration to " + pluginName); + } + + public static String forDockerContextInsecureRecursiveDelete( + String messagePrefix, String directory) { + return suggest( + messagePrefix, "clear " + directory + " manually before creating the Docker context"); + } + + /** + * @param messagePrefix the initial message text + * @param suggestion a suggested fix for the problem described by {@link #messagePrefix} + * @return the message containing the suggestion + */ + public static String suggest(String messagePrefix, String suggestion) { + return messagePrefix + ", perhaps you should " + suggestion; + } + private final String messagePrefix; private final String clearCacheCommand; + @Nullable private final ImageReference baseImageReference; + private final boolean noCredentialsDefinedForBaseImage; private final String baseImageCredHelperConfiguration; private final Function baseImageAuthConfiguration; + @Nullable private final ImageReference targetImageReference; + private final boolean noCredentialsDefinedForTargetImage; private final String targetImageCredHelperConfiguration; private final Function targetImageAuthConfiguration; private final String toImageConfiguration; @@ -37,10 +88,16 @@ public class HelpfulSuggestions { * * @param messagePrefix the initial message text * @param clearCacheCommand the command for clearing the cache + * @param baseImageReference the base image reference + * @param noCredentialsDefinedForBaseImage {@code true} if no credentials were defined for the + * base image; {@code false} otherwise * @param baseImageCredHelperConfiguration the configuration defining the credential helper name * for the base image * @param baseImageAuthConfiguration the way to define raw credentials for the base image - takes * the base image registry as an argument + * @param targetImageReference the target image reference + * @param noCredentialsDefinedForTargetImage {@code true} if no credentials were defined for the + * base image; {@code false} otherwise * @param targetImageCredHelperConfiguration the configuration defining the credential helper name * for the target image * @param targetImageAuthConfiguration the way to define raw credentials for the target image - @@ -52,8 +109,12 @@ public class HelpfulSuggestions { public HelpfulSuggestions( String messagePrefix, String clearCacheCommand, + @Nullable ImageReference baseImageReference, + boolean noCredentialsDefinedForBaseImage, String baseImageCredHelperConfiguration, Function baseImageAuthConfiguration, + @Nullable ImageReference targetImageReference, + boolean noCredentialsDefinedForTargetImage, String targetImageCredHelperConfiguration, Function targetImageAuthConfiguration, String toImageConfiguration, @@ -61,8 +122,12 @@ public HelpfulSuggestions( String buildConfigurationFilename) { this.messagePrefix = messagePrefix; this.clearCacheCommand = clearCacheCommand; + this.baseImageReference = baseImageReference; + this.noCredentialsDefinedForBaseImage = noCredentialsDefinedForBaseImage; this.baseImageCredHelperConfiguration = baseImageCredHelperConfiguration; this.baseImageAuthConfiguration = baseImageAuthConfiguration; + this.targetImageReference = targetImageReference; + this.noCredentialsDefinedForTargetImage = noCredentialsDefinedForTargetImage; this.targetImageCredHelperConfiguration = targetImageCredHelperConfiguration; this.targetImageAuthConfiguration = targetImageAuthConfiguration; this.toImageConfiguration = toImageConfiguration; @@ -94,18 +159,24 @@ public String forHttpStatusCodeForbidden(String imageReference) { return suggest("make sure you have permissions for " + imageReference); } - public String forNoCredentialHelpersDefinedForBaseImage(String registry) { - return forNoCredentialHelpersDefined( - baseImageCredHelperConfiguration, baseImageAuthConfiguration.apply(registry)); - } - - public String forNoCredentialHelpersDefinedForTargetImage(String registry) { - return forNoCredentialHelpersDefined( - targetImageCredHelperConfiguration, targetImageAuthConfiguration.apply(registry)); - } - - public String forCredentialsNotCorrect(String registry) { - return suggest("make sure your credentials for '" + registry + "' are set up correctly"); + public String forNoCredentialsDefined(String registry, String repository) { + Preconditions.checkNotNull(baseImageReference); + Preconditions.checkNotNull(targetImageReference); + if (noCredentialsDefinedForBaseImage + && registry.equals(baseImageReference.getRegistry()) + && repository.equals(baseImageReference.getRepository())) { + return forNoCredentialHelpersDefined( + baseImageCredHelperConfiguration, baseImageAuthConfiguration.apply(registry)); + } + if (noCredentialsDefinedForTargetImage + && registry.equals(targetImageReference.getRegistry()) + && repository.equals(targetImageReference.getRepository())) { + return forNoCredentialHelpersDefined( + targetImageCredHelperConfiguration, targetImageAuthConfiguration.apply(registry)); + } + // Credential helper probably was not configured correctly or did not have the necessary + // credentials. + return forCredentialsNotCorrect(registry); } public String forCredentialsNotSent() { @@ -113,40 +184,11 @@ public String forCredentialsNotSent() { "use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true"); } - public String forDockerContextInsecureRecursiveDelete(String directory) { - return suggest("clear " + directory + " manually before creating the Docker context"); - } - - public String forMainClassNotFound(String pluginName) { - return suggest("add a `mainClass` configuration to " + pluginName); - } - - public String forDockerNotInstalled() { - return suggest("make sure Docker is installed and you have correct privileges to run it"); - } - public String forInsecureRegistry() { return suggest( "use a registry that supports HTTPS or set the configuration parameter 'allowInsecureRegistries'"); } - /** - * @param parameter the parameter name (e.g. 'to.image' or {@literal }) - * @param buildConfigFilename the name of the build config (build.gradle or pom.xml) - * @param command an example command for passing the parameter via commandline - * @return a suggested fix for a missing target image configuration - */ - public String forToNotConfigured(String parameter, String buildConfigFilename, String command) { - return suggest( - "add a " - + parameter - + " configuration parameter to your " - + buildConfigFilename - + " or set the parameter via the commandline (e.g. '" - + command - + "')."); - } - public String forGeneratedTag(String projectName, String projectVersion) { return "Tagging image with generated image reference " + projectName @@ -170,7 +212,7 @@ public String none() { * @return the message containing the suggestion */ public String suggest(String suggestion) { - return messagePrefix + ", perhaps you should " + suggestion; + return suggest(messagePrefix, suggestion); } private String forNoCredentialHelpersDefined( @@ -181,4 +223,8 @@ private String forNoCredentialHelpersDefined( + "' or " + authConfiguration); } + + private String forCredentialsNotCorrect(String registry) { + return suggest("make sure your credentials for '" + registry + "' are set up correctly"); + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 46e29b9649..ae130f2e5b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -125,23 +125,20 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert switch (mainClassFinderResult.getErrorType()) { case MAIN_CLASS_NOT_FOUND: throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Main class was not found") - .forMainClassNotFound(projectProperties.getPluginName())); + HelpfulSuggestions.forMainClassNotFound( + "Main class was not found", projectProperties.getPluginName())); case MULTIPLE_MAIN_CLASSES: throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: " - + String.join(", ", mainClassFinderResult.getFoundMainClasses())) - .forMainClassNotFound(projectProperties.getPluginName())); + HelpfulSuggestions.forMainClassNotFound( + "Multiple valid main classes were found: " + + String.join(", ", mainClassFinderResult.getFoundMainClasses()), + projectProperties.getPluginName())); case IO_EXCEPTION: throw new MainClassInferenceException( - projectProperties - .getMainClassHelpfulSuggestions("Failed to get main class") - .forMainClassNotFound(projectProperties.getPluginName()), + HelpfulSuggestions.forMainClassNotFound( + "Failed to get main class", projectProperties.getPluginName()), mainClassFinderResult.getErrorCause()); default: diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 9c74b0876c..2c9ac5dd65 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -40,10 +40,4 @@ public interface ProjectProperties { /** @return the name of the main class configured in a jar plugin, or null if none is found. */ @Nullable String getMainClassFromJar(); - - /** - * @param prefix the prefix message for the {@link HelpfulSuggestions}. - * @return a {@link HelpfulSuggestions} instance for main class inference failure. - */ - HelpfulSuggestions getMainClassHelpfulSuggestions(String prefix); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index ae30f75bf1..9a8c7d4f19 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -25,8 +25,8 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; @@ -55,8 +55,12 @@ public class BuildStepsRunnerTest { new HelpfulSuggestions( "messagePrefix", "clearCacheCommand", + ImageReference.of("someregistry", "somerepository", null), + false, "baseImageCredHelperConfiguration", registry -> "baseImageAuthConfiguration " + registry, + ImageReference.of("toRegistry", "toRepository", null), + false, "targetImageCredHelperConfiguration", registry -> "targetImageAuthConfiguration " + registry, "toConfig", @@ -72,8 +76,6 @@ public class BuildStepsRunnerTest { @Mock private HttpResponseException mockHttpResponseException; @Mock private ExecutionException mockExecutionException; @Mock private BuildConfiguration mockBuildConfiguration; - @Mock private ImageConfiguration mockFromImageConfiguration; - @Mock private ImageConfiguration mockToImageConfiguration; private BuildStepsRunner testBuildImageStepsRunner; @@ -82,10 +84,6 @@ public void setUpMocks() { testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps); Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); - Mockito.when(mockBuildConfiguration.getBaseImageConfiguration()) - .thenReturn(mockFromImageConfiguration); - Mockito.when(mockBuildConfiguration.getTargetImageConfiguration()) - .thenReturn(mockToImageConfiguration); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( @@ -213,46 +211,13 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("someregistry"); - Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("somerepository"); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForBaseImage("someregistry"), - ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_sameRegistry() - throws CacheDirectoryNotOwnedException, InterruptedException, ExecutionException, - CacheMetadataCorruptedException, CacheDirectoryCreationException, IOException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("toRegistry"); - Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("toRepository"); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("toRegistry"); - Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("fromRepository"); - Mockito.when(mockToImageConfiguration.getImageRegistry()).thenReturn("toRegistry"); - Mockito.when(mockToImageConfiguration.getImageRepository()).thenReturn("toRepository"); - try { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForTargetImage("toRegistry"), + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry", "somerepository"), ex.getMessage()); Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); } @@ -276,35 +241,6 @@ public void testBuildImage_executionException_registryCredentialsNotSentExceptio } } - @Test - public void testBuildImage_executionException_registryUnauthorizedException_other() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); - Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); - - Mockito.when(mockFromImageConfiguration.getImageRegistry()).thenReturn("someregistry"); - Mockito.when(mockFromImageConfiguration.getImageRepository()).thenReturn("somerepository"); - Mockito.when(mockFromImageConfiguration.getCredentialHelper()) - .thenReturn("some-credential-helper"); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forCredentialsNotCorrect("someregistry"), ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - @Test public void testBuildImage_executionException_other() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index ee9d8830bf..b54252716f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -34,6 +34,7 @@ public class ConfigurationPropertyValidatorTest { @Mock private JibLogger mockLogger; @Mock private AuthProperty mockAuth; + @Mock private ImageReference mockImageReference; @After public void tearDown() { @@ -129,7 +130,19 @@ public void testGetImageAuth() { public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceException { HelpfulSuggestions helpfulSuggestions = new HelpfulSuggestions( - "", "", "", unused -> "", "", unused -> "", "to", "--to", "build.txt"); + "", + "", + mockImageReference, + false, + "", + unused -> "", + mockImageReference, + false, + "", + unused -> "", + "to", + "--to", + "build.txt"); // Target configured ImageReference result = diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 3ad46ae75c..8d67146005 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.image.ImageReference; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; @@ -27,8 +28,12 @@ public class HelpfulSuggestionsTest { new HelpfulSuggestions( "messagePrefix", "clearCacheCommand", + ImageReference.of("baseregistry", "baserepository", null), + true, "baseImageCredHelperConfiguration", registry -> "baseImageAuthConfiguration " + registry, + ImageReference.of("targetregistry", "targetrepository", null), + false, "targetImageCredHelperConfiguration", registry -> "targetImageAuthConfiguration " + registry, "toProperty", @@ -53,23 +58,21 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should make sure you have permissions for imageReference", TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("imageReference")); Assert.assertEquals( - "messagePrefix, perhaps you should set a credential helper name with the configuration 'baseImageCredHelperConfiguration' or baseImageAuthConfiguration registry", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForBaseImage("registry")); + "messagePrefix, perhaps you should set a credential helper name with the configuration 'baseImageCredHelperConfiguration' or baseImageAuthConfiguration baseregistry", + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("baseregistry", "baserepository")); Assert.assertEquals( - "messagePrefix, perhaps you should set a credential helper name with the configuration 'targetImageCredHelperConfiguration' or targetImageAuthConfiguration registry", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialHelpersDefinedForTargetImage("registry")); - Assert.assertEquals( - "messagePrefix, perhaps you should make sure your credentials for 'registry' are set up correctly", - TEST_HELPFUL_SUGGESTIONS.forCredentialsNotCorrect("registry")); + "messagePrefix, perhaps you should make sure your credentials for 'targetregistry' are set up correctly", + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("targetregistry", "targetrepository")); Assert.assertEquals( "messagePrefix, perhaps you should clear directory manually before creating the Docker context", - TEST_HELPFUL_SUGGESTIONS.forDockerContextInsecureRecursiveDelete("directory")); + HelpfulSuggestions.forDockerContextInsecureRecursiveDelete("messagePrefix", "directory")); Assert.assertEquals( "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", - TEST_HELPFUL_SUGGESTIONS.forMainClassNotFound("plugin")); + HelpfulSuggestions.forMainClassNotFound("messagePrefix", "plugin")); Assert.assertEquals( "messagePrefix, perhaps you should add a parameter configuration parameter to your buildFile or set the parameter via the commandline (e.g. 'command').", - TEST_HELPFUL_SUGGESTIONS.forToNotConfigured("parameter", "buildFile", "command")); + HelpfulSuggestions.forToNotConfigured( + "messagePrefix", "parameter", "buildFile", "command")); Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); Assert.assertEquals( "messagePrefix, perhaps you should use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true", diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 3c1111c3b1..4fd855b130 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -24,11 +24,11 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -39,7 +39,6 @@ public class MainClassResolverTest { @Mock private JibLogger mockBuildLogger; @Mock private ProjectProperties mockProjectProperties; - @Mock private HelpfulSuggestions mockHelpfulSuggestions; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); @@ -48,8 +47,6 @@ public class MainClassResolverTest { public void setup() { Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); - Mockito.when(mockProjectProperties.getMainClassHelpfulSuggestions(ArgumentMatchers.any())) - .thenReturn(mockHelpfulSuggestions); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) .thenReturn(mockJavaLayerConfigurations); @@ -115,9 +112,10 @@ public void testResolveMainClass_multipleInferredWithoutBackup() throws URISynta Assert.fail(); } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions( - "Multiple valid main classes were found: HelloWorld, multi.layered.HelloMoon"); + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Multiple valid main classes were found: HelloWorld, multi.layered.HelloMoon")); } } @@ -140,8 +138,7 @@ public void testResolveMainClass_noneInferredWithoutBackup() { Assert.fail(); } catch (MainClassInferenceException ex) { - Mockito.verify(mockProjectProperties) - .getMainClassHelpfulSuggestions("Main class was not found"); + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("Main class was not found")); } } From e3cf8bd18df35b43aa16de02c14b95c3ecf99ca2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 20 Aug 2018 15:16:19 -0400 Subject: [PATCH 0142/2020] Fix xml comment in maven readme (#859) --- jib-maven-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 38e256358a..2e16b692a3 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -296,7 +296,7 @@ You can configure a different directory by using the `extraDirectory` parameter ```xml ... - // Copies files from 'src/main/custom-extra-dir' instead of 'src/main/jib' + ${project.basedir}/src/main/custom-extra-dir ... From e2ea51a7e504709efed08f61788ed29668546800 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 21 Aug 2018 11:12:07 -0400 Subject: [PATCH 0143/2020] Renames NonexistentDockerCredentialHelperException to DockerCredentialHelperNotFoundException. (#861) --- .../DockerCredentialHelperIntegrationTest.java | 6 +++--- .../jib/frontend/CredentialRetrieverFactory.java | 8 ++++---- .../DockerConfigCredentialRetriever.java | 2 +- .../credentials/DockerCredentialHelper.java | 6 +++--- ...DockerCredentialHelperNotFoundException.java} | 4 ++-- .../frontend/CredentialRetrieverFactoryTest.java | 16 +++++++--------- .../DockerConfigCredentialRetrieverTest.java | 2 +- 7 files changed, 21 insertions(+), 23 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/{NonexistentDockerCredentialHelperException.java => DockerCredentialHelperNotFoundException.java} (84%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 750ac14114..d06d081139 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -34,7 +34,7 @@ public class DockerCredentialHelperIntegrationTest { @Test public void testRetrieveGCR() throws IOException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, URISyntaxException, InterruptedException { + DockerCredentialHelperNotFoundException, URISyntaxException, InterruptedException { new Command("docker-credential-gcr", "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); @@ -57,7 +57,7 @@ public void testRetrieve_nonexistentCredentialHelper() Assert.fail("Retrieve should have failed for nonexistent credential helper"); - } catch (NonexistentDockerCredentialHelperException ex) { + } catch (DockerCredentialHelperNotFoundException ex) { Assert.assertEquals( "The system does not have docker-credential-fake-cloud-provider CLI", ex.getMessage()); } @@ -65,7 +65,7 @@ public void testRetrieve_nonexistentCredentialHelper() @Test public void testRetrieve_nonexistentServerUrl() - throws IOException, NonexistentDockerCredentialHelperException { + throws IOException, DockerCredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = new DockerCredentialHelperFactory().newDockerCredentialHelper("fake.server.url", "gcr"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 41cfea1fde..ec1ec134a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; @@ -161,7 +161,7 @@ CredentialRetriever inferCredentialHelper( + inferredCredentialHelperSuffix), dockerCredentialHelperFactory); - } catch (NonexistentDockerCredentialHelperException ex) { + } catch (DockerCredentialHelperNotFoundException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper is not on the // system. @@ -193,7 +193,7 @@ CredentialRetriever dockerCredentialHelper( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); return null; - } catch (NonexistentDockerCredentialHelperException | IOException ex) { + } catch (DockerCredentialHelperNotFoundException | IOException ex) { throw new CredentialRetrievalException(ex); } }; @@ -220,7 +220,7 @@ CredentialRetriever dockerConfig( private Credential retrieveFromDockerCredentialHelper( Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) throws NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { + DockerCredentialHelperNotFoundException, IOException { Credential credentials = dockerCredentialHelperFactory .newDockerCredentialHelper(imageReference.getRegistry(), credentialHelper) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 4ea32f8a7a..52e5d29c6a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -130,7 +130,7 @@ private Credential retrieve(DockerConfig dockerConfig, String registryAlias) { } catch (IOException | NonexistentServerUrlDockerCredentialHelperException - | NonexistentDockerCredentialHelperException ex) { + | DockerCredentialHelperNotFoundException ex) { // Ignores credential helper retrieval exceptions. } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 39fbfaf1f7..e1e26d6bca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -71,11 +71,11 @@ private static class DockerCredentialsTemplate implements JsonTemplate { * @return the Docker credentials by calling the corresponding CLI * @throws IOException if writing/reading process input/output fails. * @throws NonexistentServerUrlDockerCredentialHelperException if credentials are not found. - * @throws NonexistentDockerCredentialHelperException if the credential helper CLI doesn't exist. + * @throws DockerCredentialHelperNotFoundException if the credential helper CLI doesn't exist. */ public Credential retrieve() throws IOException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException { + DockerCredentialHelperNotFoundException { try { String[] credentialHelperCommand = {credentialHelper.toString(), "get"}; @@ -127,7 +127,7 @@ public Credential retrieve() // Checks if the failure is due to a nonexistent credential helper CLI. if (ex.getMessage().contains("No such file or directory") || ex.getMessage().contains("cannot find the file")) { - throw new NonexistentDockerCredentialHelperException(credentialHelper, ex); + throw new DockerCredentialHelperNotFoundException(credentialHelper, ex); } throw ex; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java similarity index 84% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java index af6d13bb1e..c626aa8a38 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java @@ -19,9 +19,9 @@ import java.nio.file.Path; /** Thrown because the requested credential helper CLI does not exist. */ -public class NonexistentDockerCredentialHelperException extends Exception { +public class DockerCredentialHelperNotFoundException extends Exception { - NonexistentDockerCredentialHelperException(Path credentialHelper, Throwable cause) { + DockerCredentialHelperNotFoundException(Path credentialHelper, Throwable cause) { super("The system does not have " + credentialHelper + " CLI", cause); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 8a53de4bd8..24908de8ac 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.NonexistentDockerCredentialHelperException; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; import java.io.IOException; import java.nio.file.Paths; @@ -46,21 +46,19 @@ public class CredentialRetrieverFactoryTest { @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; /** - * A {@link DockerCredentialHelper} that throws {@link - * NonexistentDockerCredentialHelperException}. + * A {@link DockerCredentialHelper} that throws {@link DockerCredentialHelperNotFoundException}. */ @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - @Mock - private NonexistentDockerCredentialHelperException mockNonexistentDockerCredentialHelperException; + @Mock private DockerCredentialHelperNotFoundException mockDockerCredentialHelperNotFoundException; @Before public void setUp() throws NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { + DockerCredentialHelperNotFoundException, IOException { Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockNonexistentDockerCredentialHelperException); + .thenThrow(mockDockerCredentialHelperNotFoundException); } @Test @@ -105,8 +103,8 @@ public void testInferCredentialHelper() throws Exception { .retrieve()); Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); - Mockito.when(mockNonexistentDockerCredentialHelperException.getMessage()).thenReturn("warning"); - Mockito.when(mockNonexistentDockerCredentialHelperException.getCause()) + Mockito.when(mockDockerCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); + Mockito.when(mockDockerCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertNull( credentialRetrieverFactory diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 02ca597989..36774fc7dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -44,7 +44,7 @@ public class DockerConfigCredentialRetrieverTest { @Before public void setUp() throws URISyntaxException, NonexistentServerUrlDockerCredentialHelperException, - NonexistentDockerCredentialHelperException, IOException { + DockerCredentialHelperNotFoundException, IOException { dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); From 60c7b2b1e4853f984e63589278dd5da384f998c8 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 21 Aug 2018 11:21:46 -0400 Subject: [PATCH 0144/2020] Adds DefaultCredentialRetrievers to consolidate generation of CredentialRetrievers list. (#860) --- .../frontend/CredentialRetrieverFactory.java | 22 +++- .../tools/jib/gradle/BuildImageTask.java | 27 ++--- .../gradle/PluginConfigurationProcessor.java | 27 ++--- .../cloud/tools/jib/maven/BuildImageMojo.java | 37 ++---- .../maven/PluginConfigurationProcessor.java | 36 ++---- .../common/DefaultCredentialRetrievers.java | 109 ++++++++++++++++++ .../DefaultCredentialRetrieversTest.java | 84 ++++++++++++++ 7 files changed, 246 insertions(+), 96 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index ec1ec134a8..98eafda679 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -63,11 +63,24 @@ private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logg this.logger = logger; } + /** + * Sets the image reference the {@link CredentialRetriever}s should retrieve credentials for. + * + * @param imageReference the image reference + * @return this + */ public CredentialRetrieverFactory setImageReference(ImageReference imageReference) { this.imageReference = imageReference; return this; } + /** + * Creates a new {@link CredentialRetriever} that returns a known {@link Credential}. + * + * @param credential the known credential + * @param credentialSource the source of the credentials (for logging) + * @return a new {@link CredentialRetriever} + */ public CredentialRetriever known(Credential credential, String credentialSource) { return () -> { logGotCredentialsFrom(credentialSource); @@ -79,14 +92,11 @@ public CredentialRetriever known(Credential credential, String credentialSource) * Creates a new {@link CredentialRetriever} for retrieving credentials via a Docker credential * helper, such as {@code docker-credential-gcr}. * - * @param credentialHelperSuffix the credential helper executable suffix, following {@code - * docker-credential-} (ie. {@code gcr} for {@code docker-credential-gcr}) + * @param credentialHelper the credential helper executable * @return a new {@link CredentialRetriever} */ - public CredentialRetriever dockerCredentialHelper(String credentialHelperSuffix) { - return dockerCredentialHelper( - Paths.get(DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix), - new DockerCredentialHelperFactory()); + public CredentialRetriever dockerCredentialHelper(String credentialHelper) { + return dockerCredentialHelper(Paths.get(credentialHelper), new DockerCredentialHelperFactory()); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 194633b209..46c4f76ef0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -20,18 +20,16 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.util.ArrayList; -import java.util.List; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -88,34 +86,23 @@ public void buildImage() throws InvalidImageReferenceException { ImageReference targetImage = ImageReference.parse(jibExtension.getTargetImage()); - CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage(targetImage, gradleJibLogger); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(targetImage, gradleJibLogger)); Credential toCredential = ConfigurationPropertyValidator.getImageCredential( gradleJibLogger, "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); - CredentialRetriever knownCredentialRetriever = null; if (toCredential != null) { - knownCredentialRetriever = credentialRetrieverFactory.known(toCredential, "jib.to.auth"); + defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth"); } - // Makes credential retriever list. - List credentialRetrievers = new ArrayList<>(); - String credentialHelperSuffix = jibExtension.getTo().getCredHelper(); - if (credentialHelperSuffix != null) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); - } - if (knownCredentialRetriever != null) { - credentialRetrievers.add(knownCredentialRetriever); - } - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); - credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getTo().getCredHelper()); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) - .setCredentialRetrievers(credentialRetrievers) + .setCredentialRetrievers(defaultCredentialRetrievers.asList()) .build(); PluginConfigurationProcessor pluginConfigurationProcessor = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 8c69d2dc33..70e9cf683a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -22,17 +22,15 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.registry.RegistryClient; import java.time.Instant; -import java.util.ArrayList; -import java.util.List; import javax.annotation.Nullable; import org.gradle.api.GradleException; @@ -70,33 +68,22 @@ static PluginConfigurationProcessor processCommonConfiguration( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } - CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage(baseImage, logger); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); Credential fromCredential = ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); - CredentialRetriever knownCredentialRetriever = null; if (fromCredential != null) { - knownCredentialRetriever = credentialRetrieverFactory.known(fromCredential, "jib.from.auth"); + defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth"); } - // Makes credential retriever list. - List credentialRetrievers = new ArrayList<>(); - String credentialHelperSuffix = jibExtension.getFrom().getCredHelper(); - if (credentialHelperSuffix != null) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); - } - if (knownCredentialRetriever != null) { - credentialRetrievers.add(knownCredentialRetriever); - } - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); - credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getFrom().getCredHelper()); ImageConfiguration.Builder baseImageConfigurationBuilder = - ImageConfiguration.builder(baseImage).setCredentialRetrievers(credentialRetrievers); + ImageConfiguration.builder(baseImage) + .setCredentialRetrievers(defaultCredentialRetrievers.asList()); String mainClass = projectProperties.getMainClass(jibExtension); ContainerConfiguration.Builder containerConfigurationBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c95ddc340b..3786423cf2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -20,19 +20,17 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -87,42 +85,31 @@ public void execute() throws MojoExecutionException, MojoFailureException { ImageReference targetImage = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); - CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage(targetImage, mavenJibLogger); + + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(targetImage, mavenJibLogger)); Credential toCredential = ConfigurationPropertyValidator.getImageCredential( mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); - CredentialRetriever knownCredentialRetriever = null; if (toCredential == null) { toCredential = pluginConfigurationProcessor .getMavenSettingsServerCredentials() .retrieve(targetImage.getRegistry()); if (toCredential != null) { - knownCredentialRetriever = - credentialRetrieverFactory.known( - toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); + defaultCredentialRetrievers.setKnownCredential( + toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { - knownCredentialRetriever = - credentialRetrieverFactory.known( - toCredential, "jib-maven-plugin configuration"); - } - // Makes credential retriever list. - List credentialRetrievers = new ArrayList<>(); - String credentialHelperSuffix = getTargetImageCredentialHelperName(); - if (credentialHelperSuffix != null) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); + defaultCredentialRetrievers.setKnownCredential( + toCredential, "jib-maven-plugin configuration"); } - if (knownCredentialRetriever != null) { - credentialRetrievers.add(knownCredentialRetriever); - } - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); - credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + defaultCredentialRetrievers.setCredentialHelperSuffix(getTargetImageCredentialHelperName()); + ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage) - .setCredentialRetrievers(credentialRetrievers) + .setCredentialRetrievers(defaultCredentialRetrievers.asList()) .build(); BuildConfiguration buildConfiguration = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 33357e8f1c..b2bd0a7bcb 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -21,18 +21,16 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.base.Preconditions; import java.time.Instant; -import java.util.ArrayList; -import java.util.List; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; @@ -79,42 +77,30 @@ static PluginConfigurationProcessor processCommonConfiguration( Preconditions.checkNotNull(jibPluginConfiguration.getSession()).getSettings(), jibPluginConfiguration.getSettingsDecrypter(), logger); - CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage(baseImage, logger); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); Credential fromCredential = ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); - CredentialRetriever knownCredentialRetriever = null; if (fromCredential == null) { fromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); if (fromCredential != null) { - knownCredentialRetriever = - credentialRetrieverFactory.known( - fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); + defaultCredentialRetrievers.setKnownCredential( + fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { - knownCredentialRetriever = - credentialRetrieverFactory.known( - fromCredential, "jib-maven-plugin configuration"); + defaultCredentialRetrievers.setKnownCredential( + fromCredential, "jib-maven-plugin configuration"); } + defaultCredentialRetrievers.setCredentialHelperSuffix( + jibPluginConfiguration.getBaseImageCredentialHelperName()); - // Makes credential retriever list. - List credentialRetrievers = new ArrayList<>(); - String credentialHelperSuffix = jibPluginConfiguration.getBaseImageCredentialHelperName(); - if (credentialHelperSuffix != null) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelperSuffix)); - } - if (knownCredentialRetriever != null) { - credentialRetrievers.add(knownCredentialRetriever); - } - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); - credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); ImageConfiguration.Builder baseImageConfiguration = - ImageConfiguration.builder(baseImage).setCredentialRetrievers(credentialRetrievers); + ImageConfiguration.builder(baseImage) + .setCredentialRetrievers(defaultCredentialRetrievers.asList()); String mainClass = projectProperties.getMainClass(jibPluginConfiguration); ContainerConfiguration.Builder containerConfigurationBuilder = diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java new file mode 100644 index 0000000000..29d379924c --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Generates a list of default {@link CredentialRetriever}s. + * + *

The retrievers are, in order of first-checked to last-checked: + * + *

    + *
  1. {@link CredentialRetrieverFactory#dockerCredentialHelper} for a known credential helper, if + * set + *
  2. {@link CredentialRetrieverFactory#known} for known credential, if set + *
  3. {@link CredentialRetrieverFactory#inferCredentialHelper} + *
  4. {@link CredentialRetrieverFactory#dockerConfig} + *
+ */ +public class DefaultCredentialRetrievers { + + /** + * Creates a new {@link DefaultCredentialRetrievers} with a given {@link + * CredentialRetrieverFactory}. + * + * @param credentialRetrieverFactory the {@link CredentialRetrieverFactory} to generate the {@link + * CredentialRetriever}s + * @return a new {@link DefaultCredentialRetrievers} + */ + public static DefaultCredentialRetrievers init( + CredentialRetrieverFactory credentialRetrieverFactory) { + return new DefaultCredentialRetrievers(credentialRetrieverFactory); + } + + private final CredentialRetrieverFactory credentialRetrieverFactory; + + @Nullable private CredentialRetriever knownCredentialRetriever; + @Nullable private String credentialHelperSuffix; + + private DefaultCredentialRetrievers(CredentialRetrieverFactory credentialRetrieverFactory) { + this.credentialRetrieverFactory = credentialRetrieverFactory; + } + + /** + * Sets the known {@link Credential} to use in the default credential retrievers. + * + * @param knownCredential the known credential + * @param credentialSource the source of the known credential (for logging) + * @return this + */ + public DefaultCredentialRetrievers setKnownCredential( + Credential knownCredential, String credentialSource) { + knownCredentialRetriever = credentialRetrieverFactory.known(knownCredential, credentialSource); + return this; + } + + /** + * Sets the suffix for a known credential helper. + * + * @param credentialHelperSuffix the known credential helper suffix (following {@code + * docker-credential-}) + * @return this + */ + public DefaultCredentialRetrievers setCredentialHelperSuffix( + @Nullable String credentialHelperSuffix) { + this.credentialHelperSuffix = credentialHelperSuffix; + return this; + } + + /** + * Makes a list of {@link CredentialRetriever}s. + * + * @return the list of {@link CredentialRetriever}s + */ + public List asList() { + List credentialRetrievers = new ArrayList<>(); + if (credentialHelperSuffix != null) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper( + DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); + } + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); + } + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + return credentialRetrievers; + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java new file mode 100644 index 0000000000..c5ccf3652b --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DefaultCredentialRetrievers}. */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultCredentialRetrieversTest { + + @Mock private CredentialRetrieverFactory mockCredentialRetrieverFactory; + @Mock private CredentialRetriever mockDockerCredentialHelperCredentialRetriever; + @Mock private CredentialRetriever mockKnownCredentialRetriever; + @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; + @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; + + @Before + public void setUp() { + Mockito.when(mockCredentialRetrieverFactory.dockerCredentialHelper(Mockito.anyString())) + .thenReturn(mockDockerCredentialHelperCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.known(Mockito.any(), Mockito.anyString())) + .thenReturn(mockKnownCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.inferCredentialHelper()) + .thenReturn(mockInferCredentialHelperCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.dockerConfig()) + .thenReturn(mockDockerConfigCredentialRetriever); + } + + @Test + public void testInitAsList() { + List credentialRetrievers = + DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); + Assert.assertEquals( + Arrays.asList( + mockInferCredentialHelperCredentialRetriever, mockDockerConfigCredentialRetriever), + credentialRetrievers); + } + + @Test + public void testInitAsList_all() { + Credential knownCredential = new Credential("username", "password"); + List credentialRetrievers = + DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) + .setKnownCredential(knownCredential, "credentialSource") + .setCredentialHelperSuffix("credentialHelperSuffix") + .asList(); + Assert.assertEquals( + Arrays.asList( + mockDockerCredentialHelperCredentialRetriever, + mockKnownCredentialRetriever, + mockInferCredentialHelperCredentialRetriever, + mockDockerConfigCredentialRetriever), + credentialRetrievers); + + Mockito.verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource"); + Mockito.verify(mockCredentialRetrieverFactory) + .dockerCredentialHelper("docker-credential-credentialHelperSuffix"); + } +} From d876d03a920b638fa6ba627220f90b635d081122 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 21 Aug 2018 11:35:12 -0400 Subject: [PATCH 0145/2020] Update version strings for 0.9.9 (#862) --- CONTRIBUTING.md | 4 ++-- docs/faq.md | 4 ++-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 12 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ea9cf8d00..ee9f0c4721 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,12 +88,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.9-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.10-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.8' + // id 'com.google.cloud.tools.jib' version '0.9.9' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/docs/faq.md b/docs/faq.md index 006dd943e8..46f724d657 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -213,8 +213,8 @@ Jib currently requires configuring your build tool to use the appropriate [Java There are a few reasons why Jib may be unable to connect to a remote registry, including: - **Access requires a proxy.** See [_How do I configure a proxy?_](#how-do-i-configure-a-proxy) for details. -- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.8_). -- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.8_). +- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.9_). +- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.9_). - **The registry does not support the [Docker Image Format V2 Schema 2](https://github.com/GoogleContainerTools/jib/issues/601)** (sometimes referred to as _v2-2_). This problem is usually shown by failures wth `INVALID_MANIFEST` errors. Some registries can be configured to support V2-2 such as [Artifactory](https://www.jfrog.com/confluence/display/RTF/Docker+Registry#DockerRegistry-LocalDockerRepositories) and [OpenShift](https://docs.openshift.com/container-platform/3.9/install_config/registry/extended_registry_configuration.html#middleware-repository-acceptschema2). Other registries, such as Quay.io/Quay Enterprise, are in the process of adding support. ### How can I examine network traffic? diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index fd6f63a265..64525c4995 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.8' + id 'com.google.cloud.tools.jib' version '0.9.9' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 9e63da7727..ec074768cc 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.9.8 + 0.9.9 3.8.0 diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index d3198e82b5..5e7e33bc96 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.8' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.9' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 00b48112f2..c822d7d8cb 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -39,7 +39,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.8 + 0.9.9
diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index d23605b239..83d5c0e19c 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.8' + id 'com.google.cloud.tools.jib' version '0.9.9' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index f8d81cf022..f1b0975058 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.9.8 + 0.9.9 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1866122a56..8b4add3da7 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.9 + +### Added + - Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) ### Changed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index e32dce6a33..3b5251cc1c 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.8' + id 'com.google.cloud.tools.jib' version '0.9.9' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index e0d1c5ae51..55a7d7d283 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.9 + +### Added + - Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) ### Changed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 2e16b692a3..fafd5faf66 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.8:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.9:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.8:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.9:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.8 + 0.9.9 myimage From 02107eecf649b94425f43b83f68090c2e7fcb2cb Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 21 Aug 2018 22:19:06 -0400 Subject: [PATCH 0146/2020] Lock cache metadata files for access (#849) --- .../google/cloud/tools/jib/blob/Blobs.java | 27 +++++++++++++++++ .../google/cloud/tools/jib/cache/Cache.java | 12 +++----- .../tools/jib/json/JsonTemplateMapper.java | 23 ++++++++++++++ .../google/cloud/tools/jib/blob/BlobTest.java | 30 +++++++++++++++++++ .../jib/json/JsonTemplateMapperTest.java | 30 +++++++++++++++++++ 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index c41268f40d..6ead1eaa8b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -19,8 +19,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; /** Static methods for {@link Blob}. */ public class Blobs { @@ -71,5 +76,27 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { return byteArrayOutputStream.toByteArray(); } + /** + * Writes the BLOB to a file with an exclusive lock. + * + * @param blob the BLOB to to write + * @param blobFile the file to write to + * @return the {@link BlobDescriptor} of the written BLOB + * @throws IOException if writing the BLOB fails + */ + public static BlobDescriptor writeToFileWithLock(Blob blob, Path blobFile) throws IOException { + EnumSet createOrTruncate = + EnumSet.of( + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + // channel is closed by outputStream.close() + FileChannel channel = FileChannel.open(blobFile, createOrTruncate); + channel.lock(); // released when channel is closed + try (OutputStream outputStream = Channels.newOutputStream(channel)) { + return blob.writeTo(outputStream); + } + } + private Blobs() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 2721546e46..e24fe304ea 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -16,13 +16,12 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; -import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; -import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.NotDirectoryException; import java.nio.file.Path; @@ -60,9 +59,9 @@ private static CacheMetadata loadCacheMetadata(Path cacheDirectory) try { CacheMetadataTemplate cacheMetadataJson = - JsonTemplateMapper.readJsonFromFile(cacheMetadataJsonFile, CacheMetadataTemplate.class); + JsonTemplateMapper.readJsonFromFileWithLock( + cacheMetadataJsonFile, CacheMetadataTemplate.class); return CacheMetadataTranslator.fromTemplate(cacheMetadataJson, cacheDirectory); - } catch (IOException ex) { // The cache metadata is probably corrupted. throw new CacheMetadataCorruptedException(ex); @@ -139,9 +138,6 @@ private void saveCacheMetadata(Path cacheDirectory) throws IOException { CacheMetadataTemplate cacheMetadataJson = CacheMetadataTranslator.toTemplate(cacheMetadataBuilder.build()); - try (OutputStream fileOutputStream = - new BufferedOutputStream(Files.newOutputStream(cacheMetadataJsonFile))) { - JsonTemplateMapper.toBlob(cacheMetadataJson).writeTo(fileOutputStream); - } + Blobs.writeToFileWithLock(JsonTemplateMapper.toBlob(cacheMetadataJson), cacheMetadataJsonFile); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index ad95a6824f..ec82105b68 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -20,8 +20,12 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.List; // TODO: Add JsonFactory for HTTP response parsing. @@ -63,6 +67,25 @@ public static T readJsonFromFile(Path jsonFile, Class child type of {@link JsonTemplate} + * @param jsonFile a file containing a JSON string + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonFile} + * @throws IOException if an error occurred during reading the file or parsing the JSON + */ + public static T readJsonFromFileWithLock( + Path jsonFile, Class templateClass) throws IOException { + // channel is closed by inputStream.close() + FileChannel channel = FileChannel.open(jsonFile, StandardOpenOption.READ); + channel.lock(0, Long.MAX_VALUE, true); // shared lock, released by channel close + try (InputStream inputStream = Channels.newInputStream(channel)) { + return objectMapper.readValue(inputStream, templateClass); + } + } + /** * Deserializes a JSON object from a JSON string. * diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index 8023446b22..a7e75ebd54 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -29,6 +29,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -66,6 +67,35 @@ public void testFromBlobWriter() throws IOException { verifyBlobWriteTo(expected, Blobs.from(writer)); } + @Test + public void testWriteToFileWithLock_newFile() throws IOException { + String expected = "crepecake"; + Path blobFile = Files.createTempFile("blob", "bin"); + Assert.assertTrue(Files.deleteIfExists(blobFile)); // ensure it doesn't exist + + Blobs.writeToFileWithLock(Blobs.from(expected), blobFile); + + Assert.assertTrue(Files.exists(blobFile)); + verifyBlobWriteTo(expected, Blobs.from(blobFile)); + } + + @Test + public void testWriteToFileWithLock_existingFile() throws IOException { + Path blobFile = Files.createTempFile("blob", "bin"); + // write out more bytes to ensure properly truncated + byte[] dataBytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + Files.write(blobFile, dataBytes, StandardOpenOption.WRITE); + Assert.assertTrue(Files.exists(blobFile)); + Assert.assertEquals(10, Files.size(blobFile)); + + String expected = "crepecake"; + Blobs.writeToFileWithLock(Blobs.from(expected), blobFile); + + Assert.assertTrue(Files.exists(blobFile)); + Assert.assertEquals(9, Files.size(blobFile)); + verifyBlobWriteTo(expected, Blobs.from(blobFile)); + } + /** Checks that the {@link Blob} streams the expected string. */ private void verifyBlobWriteTo(String expected, Blob blob) throws IOException { OutputStream outputStream = new ByteArrayOutputStream(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 875d3eabae..be018515d0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -89,4 +90,33 @@ public void testWriteJson() throws DigestException, IOException, URISyntaxExcept Assert.assertEquals(expectedJson, jsonStream.toString()); } + + @Test + public void testReadJsonWithLock() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("json/basic.json").toURI()); + + // Deserializes into a metadata JSON object. + TestJson testJson = JsonTemplateMapper.readJsonFromFileWithLock(jsonFile, TestJson.class); + + Assert.assertThat(testJson.number, CoreMatchers.is(54)); + Assert.assertThat(testJson.text, CoreMatchers.is("crepecake")); + Assert.assertThat( + testJson.digest, + CoreMatchers.is( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"))); + Assert.assertThat(testJson.innerObject, CoreMatchers.instanceOf(TestJson.InnerObject.class)); + Assert.assertThat(testJson.innerObject.number, CoreMatchers.is(23)); + Assert.assertThat( + testJson.innerObject.texts, CoreMatchers.is(Arrays.asList("first text", "second text"))); + Assert.assertThat( + testJson.innerObject.digests, + CoreMatchers.is( + Arrays.asList( + DescriptorDigest.fromDigest( + "sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"), + DescriptorDigest.fromHash( + "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")))); + // ignore testJson.list + } } From 57d18e6356d80acaa673a3d74dea6ee702a02424 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 22 Aug 2018 10:34:03 -0400 Subject: [PATCH 0147/2020] Add labels to gradle config (#866) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../jib/gradle/JibPluginIntegrationTest.java | 20 +++++++++++++++++++ .../projects/default-target/build.gradle | 1 + .../resources/projects/empty/build.gradle | 1 + .../resources/projects/simple/build.gradle | 1 + .../projects/simple/complex-build.gradle | 1 + .../tools/jib/gradle/ContainerParameters.java | 12 +++++++++++ .../cloud/tools/jib/gradle/JibExtension.java | 7 +++++++ .../gradle/PluginConfigurationProcessor.java | 3 ++- .../tools/jib/gradle/JibExtensionTest.java | 5 +++++ 10 files changed, 52 insertions(+), 1 deletion(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 8b4add3da7..7d927f5cab 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 2433db95ee..5f16e9d883 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -81,6 +81,7 @@ private static String buildAndRunComplex( targetRegistry.pull(imageReference); assertHasExposedPorts(imageReference); + assertHasLabels(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -94,6 +95,7 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); assertHasExposedPorts(imageReference); + assertHasLabels(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -154,6 +156,24 @@ private static void assertHasExposedPorts(String imageReference) + " \"2003/udp\": {}")); } + /** + * Asserts that the test project has the required labels. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertHasLabels(String imageReference) + throws IOException, InterruptedException { + Assert.assertThat( + new Command("docker", "inspect", imageReference).run(), + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); + } + @Before public void setup() throws IOException, InterruptedException { // Pull distroless and push to local registry so we can test 'from' credentials diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle index 87edcf9f3b..316276249b 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle @@ -18,6 +18,7 @@ jib { container { args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] } // Does not have tests use user-level cache for base image layers. diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index 0fb84889b4..7f20ccc2d9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -19,6 +19,7 @@ jib { } container { ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] } // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 7301397b6f..3d91c631bd 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -23,6 +23,7 @@ jib { useCurrentTimestamp = true args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] } extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index 554197c092..9976101f72 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -35,6 +35,7 @@ jib { mainClass = 'com.test.HelloWorld' jvmFlags = ['-Xms512m', '-Xdebug'] ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] } allowInsecureRegistries = true extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index cca369fdd9..5265222438 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -37,6 +38,7 @@ public class ContainerParameters { private List args = Collections.emptyList(); private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); + private Map labels = Collections.emptyMap(); @Input @Optional @@ -98,4 +100,14 @@ public List getPorts() { public void setPorts(List ports) { this.ports = ports; } + + @Input + @Optional + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index d42e627c0f..feaf68e56a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -25,6 +25,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import org.gradle.api.Action; import org.gradle.api.Project; @@ -256,6 +257,12 @@ List getExposedPorts() { return container.getPorts(); } + @Internal + @Optional + Map getLabels() { + return container.getLabels(); + } + @Internal @Optional boolean getUseCurrentTimestamp() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 70e9cf683a..c4a275af34 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -92,7 +92,8 @@ static PluginConfigurationProcessor processCommonConfiguration( JavaEntrypointConstructor.makeDefaultEntrypoint( jibExtension.getJvmFlags(), mainClass)) .setProgramArguments(jibExtension.getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())); + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) + .setLabels(jibExtension.getLabels()); if (jibExtension.getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 64a8419f7e..7e63c8dc19 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.Collections; import org.gradle.api.Project; @@ -105,6 +106,7 @@ public void testContainer() { container.setMainClass("mainClass"); container.setArgs(Arrays.asList("arg1", "arg2", "arg3")); container.setPorts(Arrays.asList("1000", "2000-2010", "3000")); + container.setLabels(ImmutableMap.of("label1", "value1", "label2", "value2")); container.setFormat(ImageFormat.OCI); }); Assert.assertEquals( @@ -114,6 +116,9 @@ public void testContainer() { Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getContainer().getArgs()); Assert.assertEquals( Arrays.asList("1000", "2000-2010", "3000"), testJibExtension.getContainer().getPorts()); + Assert.assertEquals( + ImmutableMap.of("label1", "value1", "label2", "value2"), + testJibExtension.getContainer().getLabels()); Assert.assertEquals(OCIManifestTemplate.class, testJibExtension.getContainer().getFormat()); } From f61cf97e44faa1ca45898a8951d2e4eda7f607c6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 22 Aug 2018 11:32:02 -0400 Subject: [PATCH 0148/2020] Add labels to maven config (#867) --- jib-maven-plugin/CHANGELOG.md | 2 ++ .../tools/jib/maven/JibPluginConfiguration.java | 11 ++++++----- .../jib/maven/PluginConfigurationProcessor.java | 3 ++- .../maven/BuildDockerMojoIntegrationTest.java | 10 +++++++++- .../jib/maven/BuildImageMojoIntegrationTest.java | 16 ++++++++++++---- .../resources/projects/default-target/pom.xml | 4 ++++ .../src/test/resources/projects/empty/pom.xml | 4 ++++ .../resources/projects/simple/pom-complex.xml | 4 ++++ .../src/test/resources/projects/simple/pom.xml | 4 ++++ 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 55a7d7d283..eca7fbe05c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 48fbb32b38..6fda990e9e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -129,6 +129,8 @@ public static class ContainerParameters { private String format = "Docker"; @Parameter private List ports = Collections.emptyList(); + + @Parameter private Map labels = Collections.emptyMap(); } @Nullable @@ -273,6 +275,10 @@ List getExposedPorts() { return container.ports; } + Map getLabels() { + return container.labels; + } + String getFormat() { return Preconditions.checkNotNull(container.format); } @@ -319,11 +325,6 @@ void setProject(MavenProject project) { this.project = project; } - @VisibleForTesting - void setTargetImage(@Nullable String targetImage) { - this.to.image = targetImage; - } - @VisibleForTesting void setExtraDirectory(String extraDirectory) { this.extraDirectory = extraDirectory; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index b2bd0a7bcb..d6ce31295a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -110,7 +110,8 @@ static PluginConfigurationProcessor processCommonConfiguration( jibPluginConfiguration.getJvmFlags(), mainClass)) .setProgramArguments(jibPluginConfiguration.getArgs()) .setEnvironment(jibPluginConfiguration.getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())); + .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())) + .setLabels(jibPluginConfiguration.getLabels()); if (jibPluginConfiguration.getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 906e29e653..6944a015f4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -56,8 +56,9 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe verifier.executeGoal("jib:" + BuildDockerMojo.GOAL_NAME); verifier.verifyErrorFreeLog(); + String dockerInspect = new Command("docker", "inspect", imageReference).run(); Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), + dockerInspect, CoreMatchers.containsString( " \"ExposedPorts\": {\n" + " \"1000/tcp\": {},\n" @@ -65,6 +66,13 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe + " \"2001/udp\": {},\n" + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); return new Command("docker", "run", imageReference).run(); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index ea49317fdd..c8aef461c4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -96,7 +96,7 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole verifier.verifyErrorFreeLog(); new Command("docker", "pull", imageReference).run(); - assertHasExposedPorts(imageReference); + assertDockerInspectParameters(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -117,7 +117,7 @@ private static String buildAndRunComplex( // Verify output targetRegistry.pull(imageReference); - assertHasExposedPorts(imageReference); + assertDockerInspectParameters(imageReference); Instant buildTime = Instant.parse( new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); @@ -125,10 +125,11 @@ private static String buildAndRunComplex( return new Command("docker", "run", imageReference).run(); } - private static void assertHasExposedPorts(String imageReference) + private static void assertDockerInspectParameters(String imageReference) throws IOException, InterruptedException { + String dockerInspect = new Command("docker", "inspect", imageReference).run(); Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), + dockerInspect, CoreMatchers.containsString( " \"ExposedPorts\": {\n" + " \"1000/tcp\": {},\n" @@ -136,6 +137,13 @@ private static void assertHasExposedPorts(String imageReference) + " \"2001/udp\": {},\n" + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); } private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOException { diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml index 3ac515e2da..c413787d0e 100644 --- a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml @@ -44,6 +44,10 @@ 1000/tcp 2000-2003/udp + + value1 + value2 + true diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 6a7b746b27..b91a48c8ee 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -36,6 +36,10 @@ 1000/tcp 2000-2003/udp + + value1 + value2 + true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index 76ff340f37..dc98ada0ba 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -66,6 +66,10 @@ 1000/tcp 2000-2003/udp + + value1 + value2 + Docker true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index eb5331900a..1f9485fd48 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -48,6 +48,10 @@ 1000/tcp 2000-2003/udp + + value1 + value2 + true From c096765570ac2fb1c847a0aadc48ad83b6e69f72 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 22 Aug 2018 14:22:11 -0400 Subject: [PATCH 0149/2020] Add labels to docker context generator (#870) --- .../frontend/JavaDockerContextGenerator.java | 29 +++++++++++++++++ .../JavaDockerContextGeneratorTest.java | 11 +++++++ jib-core/src/test/resources/sampleDockerfile | 3 ++ .../jib/gradle/JibPluginIntegrationTest.java | 32 ++++++------------- .../tools/jib/gradle/DockerContextTask.java | 1 + .../tools/jib/maven/DockerContextMojo.java | 1 + .../DockerContextMojoIntegrationTest.java | 10 +++++- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 711f4f058a..799e8e171f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -31,6 +31,8 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.annotation.Nullable; /** @@ -103,6 +105,7 @@ private static void addIfNotEmpty( private String mainClass = ""; private List javaArguments = Collections.emptyList(); private List exposedPorts = Collections.emptyList(); + private Map labels = Collections.emptyMap(); /** * Constructs a Docker context generator for a Java application. @@ -190,6 +193,17 @@ public JavaDockerContextGenerator setExposedPorts(List exposedPorts) { return this; } + /** + * Sets the labels + * + * @param labels the map of labels + * @return this + */ + public JavaDockerContextGenerator setLabels(Map labels) { + this.labels = labels; + return this; + } + /** * Creates the Docker context in {@code #targetDirectory}. * @@ -238,6 +252,9 @@ public void generate(Path targetDirectory) throws IOException { * * EXPOSE [port] * [More EXPOSE instructions, if necessary] + * LABEL [key1]="[value1]" \ + * [key2]="[value2]" \ + * [...] * ENTRYPOINT java [jvm flags] -cp [classpaths] [main class] * CMD [main class args] * }
@@ -256,10 +273,22 @@ String makeDockerfile() throws JsonProcessingException { .append(" ") .append(copyDirective.extractionPath); } + dockerfile.append("\n"); for (String port : exposedPorts) { dockerfile.append("\nEXPOSE ").append(port); } + + boolean firstLabel = true; + for (Entry label : labels.entrySet()) { + dockerfile + .append(firstLabel ? "\nLABEL " : " \\\n ") + .append(label.getKey()) + .append("=") + .append(objectMapper.writeValueAsString(label.getValue())); + firstLabel = false; + } + dockerfile .append("\nENTRYPOINT ") .append( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 3d0fadf16d..9bf01cd230 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -30,6 +31,7 @@ import java.util.Arrays; import java.util.Deque; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.Assert; import org.junit.Rule; @@ -124,6 +126,14 @@ public void testMakeDockerfile() throws IOException { String expectedMainClass = "SomeMainClass"; List expectedJavaArguments = Arrays.asList("arg1", "arg2"); List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); + Map expectedLabels = + ImmutableMap.of( + "key1", + "value", + "key2", + "value with\\backslashes\"and\\\\\"\"quotes\"\\", + "key3", + "value3"); Mockito.when(mockJavaLayerConfigurations.getDependenciesLayerEntry()) .thenReturn( @@ -145,6 +155,7 @@ public void testMakeDockerfile() throws IOException { .setMainClass(expectedMainClass) .setJavaArguments(expectedJavaArguments) .setExposedPorts(exposedPorts) + .setLabels(expectedLabels) .makeDockerfile(); // Need to split/rejoin the string here to avoid cross-platform troubles diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index d15e47f901..48af0b1114 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -8,5 +8,8 @@ COPY root / EXPOSE 1000/tcp EXPOSE 2000-2010/udp +LABEL key1="value" \ + key2="value with\\backslashes\"and\\\\\"\"quotes\"\\" \ + key3="value3" ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources/:/app/classes/:/app/libs/*","SomeMainClass"] CMD ["arg1","arg2"] diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 5f16e9d883..a1cd24811b 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -59,7 +59,7 @@ private static String buildAndRun(TestProject testProject, String imageReference Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); new Command("docker", "pull", imageReference).run(); - assertHasExposedPorts(imageReference); + assertDockerInspect(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -80,8 +80,7 @@ private static String buildAndRunComplex( Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); targetRegistry.pull(imageReference); - assertHasExposedPorts(imageReference); - assertHasLabels(imageReference); + assertDockerInspect(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -94,8 +93,7 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String buildResult, JibPlugin.BUILD_DOCKER_TASK_NAME, "Built image to Docker daemon as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - assertHasExposedPorts(imageReference); - assertHasLabels(imageReference); + assertDockerInspect(imageReference); return new Command("docker", "run", imageReference).run(); } @@ -137,16 +135,17 @@ private static void assertSimpleCreationTimeIsAfter(Instant before, String image } /** - * Asserts that the test project has the required exposed ports. + * Asserts that the test project has the required exposed ports and labels. * * @param imageReference the image to test * @throws IOException if the {@code docker inspect} command fails to run * @throws InterruptedException if the {@code docker inspect} command is interrupted */ - private static void assertHasExposedPorts(String imageReference) + private static void assertDockerInspect(String imageReference) throws IOException, InterruptedException { + String dockerInspect = new Command("docker", "inspect", imageReference).run(); Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), + dockerInspect, CoreMatchers.containsString( " \"ExposedPorts\": {\n" + " \"1000/tcp\": {},\n" @@ -154,19 +153,8 @@ private static void assertHasExposedPorts(String imageReference) + " \"2001/udp\": {},\n" + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); - } - - /** - * Asserts that the test project has the required labels. - * - * @param imageReference the image to test - * @throws IOException if the {@code docker inspect} command fails to run - * @throws InterruptedException if the {@code docker inspect} command is interrupted - */ - private static void assertHasLabels(String imageReference) - throws IOException, InterruptedException { Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), + dockerInspect, CoreMatchers.containsString( " \"Labels\": {\n" + " \"key1\": \"value1\",\n" @@ -300,7 +288,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { new Command("docker", "load", "--input", outputPath).run(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); - assertHasExposedPorts(targetImage); + assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -325,7 +313,7 @@ public void testDockerContext() throws IOException, InterruptedException { .toString()) .run(); - assertHasExposedPorts(imageName); + assertDockerInspect(imageName); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 1ce773c093..ca21ab27c2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -121,6 +121,7 @@ public void generateDockerContext() { .setMainClass(mainClass) .setJavaArguments(jibExtension.getArgs()) .setExposedPorts(jibExtension.getExposedPorts()) + .setLabels(jibExtension.getLabels()) .generate(Paths.get(targetDir)); gradleJibLogger.lifecycle("Created Docker context at " + targetDir); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 0fdfd38448..528e1c8809 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -74,6 +74,7 @@ public void execute() throws MojoExecutionException { .setMainClass(mainClass) .setJavaArguments(getArgs()) .setExposedPorts(getExposedPorts()) + .setLabels(getLabels()) .generate(Paths.get(targetDir)); mavenJibLogger.lifecycle("Created Docker context at " + targetDir); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 99f275ae54..fef2748db7 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -51,8 +51,9 @@ public void testExecute() throws VerificationException, IOException, Interrupted String imageName = "jib/integration-test" + System.nanoTime(); new Command("docker", "build", "-t", imageName, dockerContextDirectory.toString()).run(); + String dockerInspect = new Command("docker", "inspect", imageName).run(); Assert.assertThat( - new Command("docker", "inspect", imageName).run(), + dockerInspect, CoreMatchers.containsString( " \"ExposedPorts\": {\n" + " \"1000/tcp\": {},\n" @@ -60,6 +61,13 @@ public void testExecute() throws VerificationException, IOException, Interrupted + " \"2001/udp\": {},\n" + " \"2002/udp\": {},\n" + " \"2003/udp\": {}")); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); From 940684b8b6d5b68b66516a3a1d3a1f1e33e75eab Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 23 Aug 2018 10:02:39 -0400 Subject: [PATCH 0150/2020] Proposal RFC: Java Agents and More Layers (#455) --- .../archives/java_agents_and_more_layers.md | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 proposals/archives/java_agents_and_more_layers.md diff --git a/proposals/archives/java_agents_and_more_layers.md b/proposals/archives/java_agents_and_more_layers.md new file mode 100644 index 0000000000..43edfc0322 --- /dev/null +++ b/proposals/archives/java_agents_and_more_layers.md @@ -0,0 +1,294 @@ +# Proposal: Give users ability to add Java agents, arbitrary files, and separate dependencies + +Implemented in: **v0.9.5** + +## Motivation + +There are 3 feature requests that may be able to be solved together. These are: + +- Give users ability to add Java agents ([#378](/../../issues/378)) +- Give users ability to copy arbitrary files to the image ([#213](/../../issues/213)) +- Separate frequently changing from non-changing dependencies for more incrementality ([#403](/../../issues/403)) + +## Synopsis of the issues + +### Give users ability to add Java agents ([#378](/../../issues/378)) + +Currently, Jib only adds application files to the container image. These include dependency JARs, resource files, and classes. However, Java developers often run their applications with Java Agents that execute as part of the JVM invocation. + +For example, in [this Dockerfile example](https://github.com/saturnism/spring-petclinic-gcp/blob/master/docker/Dockerfile), the Cloud Debugger and Cloud Profiler agents are added to the container image. The agent archives are first downloaded, and then extracted to their own directory on the image. The archives contains the `.so` file to pass to the java invocation in the form of: + +```shell +java -agentpath:/path/to/agent/files/someagent.so ... +``` + +### Give users ability to copy arbitrary files to the image ([#213](/../../issues/213)) + +Users may wish to add other files for use in the image. Currently, the way to do this would be to place the files within the application resources. However, this conflates the classpath of the application, the file can only be reached under the `/app/resources` path, and changes to the file would mean repushing all the resources. Therefore, users should have some way of adding files to a new custom layer. + +### Separate frequently changing from non-changing dependencies for more incrementality ([#403](/../../issues/403)) + +The dependencies layer tends to be the largest layer in the image, since it contains all the dependency JARs. However, not all dependencies are the same. Some may change more frequently than others (especially `-SNAPSHOT` versions). Therefore, users should have some way to group different dependencies into different layers. For example, separating released artifacts from snapshot artifacts, and grouping dependencies from a BOM into a layer, or separating more frequently changed ones from less frequently changed ones to reduce the amortized push time. + +## Proposal + +The proposal is to define a new convention for adding more files to the Jib container. + +The user can add arbitrary files to the image by placing them in a `src/main/jib` directory. This will copy all files within `src/main/jib` to the image's root directory `/`, maintaining the same structure. For example, if the user has a text file at `src/main/jib/dir/hello.txt`, then the built image have `/dir/hello.txt`. + +The directory should also be able to be configured to override the `src/main/jib` default. + +## Alternative Proposals + +### Rejected Proposal 1 + +**The alternative proposal was rejected** because we deemed that it required too much extra configuration on the part of the user. + +The proposal is to keep the configuration for adding additional files and Java agents separate from the configuration for separating the application layers into thinner layers. The point of this is to keep the configuration simple and not allow arbitrary user-controlled layering. + +The configuration for adding additional files (including Java agents) would look something like: + +*(The exact configuration is to be decided before this PR is merged. Alternative naming for this parameter include: `additionalFiles`, `extraFiles`, and `copyFiles`.)* + +*Maven* + +```xml + + + path/to/file + /path/on/image + + + another/file + /another/path/on/image + + +``` + +*Gradle* + +```groovy +addFile 'path/to/file', '/path/on/image' +addFile 'another/file', '/another/path/on/image' +``` + +*The semantics of `from` and `to` will mostly be similar to [Dockerfile `COPY`](https://docs.docker.com/engine/reference/builder/#copy). However, glob matching won't be supported.* + +For separating application layers into thinner layers, the solution will only separate dependencies for simplicity. Jib will *automatically* separate `-SNAPSHOT` dependencies and dependencies with the same group as the project into a separate layer. + +In the future, we may consider allowing the user to configure this in the form of something like what was originally suggested in [#403](/../../issues/403): + +```xml +com.yourcompany.*, *-SNAPSHOT +``` + +This would match dependencies that are in the `com.yourcompany` package and `SNAPSHOT` dependencies and place these in a new volatile-dependencies layer. + +### Rejected Proposal 2 + +**The alternative proposal was rejected** because we deemed that layering should be an implementation detail that should not be exposed to the user. + +Currently (`v0.9.1`), Jib's configuration looks like: + +```xml + + ... + ... + ... + + +``` + +- `from` defines the base image and credentials +- `to` defines the target image and credentials +- `container` defines container configuration like JVM flags, program arguments, and exposed ports + +The alternative proposal is to add another top-level configuration object called `layers` to add additional layers with custom files. The configuration would look like: + +```xml + + + + + path/to/file + /path/on/image + + + org.springframework:*, org.hibernate:*... + static/, *.jpg + my.package.that.does.not.change.much.* + + +``` + +The user can choose whatever subset of `layer` parameters to define. + +Parameter | Description +--- | --- +files | In the form `:`, where the file `` is added to the image at path ``. +matchDependencies | Matches dependencies with the given patterns and adds those dependency artifacts into this layer rather than the original dependencies layer. +matchResources | Like `matchDependencies`, but for resource files. +matchClasses | ... but for classes files. + +For implementation, `files` should be implemented first to support Java agent usage and `match*` could be added in later updates. + +And similarly for Gradle: + +```groovy +layer { + file 'path/to/file', '/path/on/image' + matchDependencies = 'org.springframework:*, org.hibernate:*...' + matchResources = 'static/, *.jpg' + matchClasses = 'my.package.that.does.not.change.much.*' +} +``` + +## Other options considered + +### Give users ability to add Java agents ([#378](/../../issues/378)) + +#### Option 1 - Specific configuration for each agent + +Provide agent-specific configuration options similar to [CloudFoundry BuildPacks](https://github.com/cloudfoundry/java-buildpack). This would mean that **each agent would have a different set of configuration parameters** specifically for that agent. Each agent-specific implementation would automatically download and add the agent files to the container image and automatically configure the JVM flags. + +**Benefit:** User would be given the exact and minimal controls over specific agents. +**Downside:** We would have to implement and maintain custom implementations for each agent we wish to support. + +#### Option 2 - `agents` configuration + +Provide configuration options to add agents by specifying the archive download location or local files. For example, the configuration could look like: + +```xml + + + ${project.basedir}/myagent-archive.tar.gz + /opt/myagent + + + + + -agentpath:/opt/myagent/myagent.so=-some_option=yes + + +``` + +#### Option 3 - copy to image + +Solve this as part of *### Give users ability to copy arbitrary files to the image ([#213](/../../issues/213))*. + +### Give users ability to copy arbitrary files to the image ([#213](/../../issues/213)) + +#### Option 1 - Single custom layer + +The user defines files to copy, along with their destinations on the image. For example: + +```xml + + + path/to/file + /path/on/image + + +``` + +All files defined would be placed in a single layer. + +**Benefit:** Prevents possible large number of layers. +**Downside:** All files are in same layer. + +#### Option 2 - Multiple custom layers + +The user can define their own custom layers, and define what files to copy into each layer. For example: + +```xml + + + + + path/to/file + /path/on/image + + + + +``` + +**Benefit:** Can be used to solve all three issues (with more options for other things to add to such custom layers) +**Downside:** Quite verbose + +#### Option 3 - Multiple automatic layers + +The user defines files to copy like in *Option 1*, but a separate layer is generated for each copy. + +#### Option 4 - Multiple custom layers by layer ID + +Like in *Option 1*, but the user can set a layer ID for each copy. Copies with the same layer ID are placed in the same layer. For example: + +```xml + + + path/to/file + /path/on/image + somelayername + + + path/to/another/file + /another/path/on/image + somelayername ↠this goes in the same layer as the first + + + path/to/yet/another/file + /path/on/image + someotherlayername + + +``` + +**Benefit:** Keeps the configuration concise + +### Separate frequently changing from non-changing dependencies for more incrementality ([#403](/../../issues/403)) + +#### Option 1 - Top-level configuration + +Configuration options `silentDependencies` and `mutableDependencies`, as recommended in [#403](/../../issues/403). Example: + +```xml +org.springframework:*, org.hibernate:*, *:commons-*, org.webjars:* +com.yourcompany:*,*:*:*-SNAPSHOT +``` + +**Benefit:** Simple and understandable +**Downside:** Does not solve any of the other issues + +#### Option 2 - for classes, resources, and dependencies + +The user would be able to define patterns to match against both dependencies and any other files that go in the application layers. These matched files are taken from the `classes`, `resources`, and `dependencies` layers and placed into 3 new layers - `silentClasses`, `silentResources`, and `silentDependencies`. The configuration would look like: + +```xml +org.springframework:*, org.hibernate:*... +static/, *.jpg +my.package.that.does.not.change.much +``` + +**Benefit:** Supports splitting of all application layers into thinner layers. +**Downside:** Does not solve any of the other issues + +#### Option 3 - in custom layers + +Similar to the above *[Option 2 - Multiple custom layers](#option-2---multiple-custom-layers)*: + +```xml + + + ... + org.springframework:*, org.hibernate:*... + static/, *.jpg + my.package.that.does.not.change.much + + +``` + +In the above example, each configuration option for `additionalLayer` is optional. + +**Benefits:** Solves all 3 issues +**Downside:** Might be confusing From 4932ade99c8a0838a064b702722b26a9883fabee Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 23 Aug 2018 14:40:01 -0400 Subject: [PATCH 0151/2020] Create new cache interfaces under a new package ncache. (#869) --- .../tools/jib/ncache/CacheReadEntry.java | 67 +++++++++++++++++ .../cloud/tools/jib/ncache/CacheStorage.java | 72 +++++++++++++++++++ .../tools/jib/ncache/CacheWriteEntry.java | 52 ++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java new file mode 100644 index 0000000000..d2ccac9d6a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.util.Optional; + +/** + * Represents a cache entry for a layer stored in the cache. Implementations must be + * immutable. + */ +public interface CacheReadEntry { + + /** + * Gets the digest of the layer. + * + * @return the layer digest + */ + DescriptorDigest getLayerDigest(); + + /** + * Gets the diff ID of the layer. The diff ID is the digest of the uncompressed layer contents, + * whereas the {@link #getLayerDigest} is the digest of the compressed layer contents. + * + * @return the layer diff ID + */ + DescriptorDigest getLayerDiffId(); + + /** + * Gets the size of the layer, in bytes. + * + * @return the layer size + */ + long getLayerSize(); + + /** + * Gets the {@link Blob} for the layer. This {@link Blob} should be able to be used multiple + * times. + * + * @return the layer {@link Blob} + */ + Blob getLayerBlob(); + + /** + * Gets the optional metadata blob for the layer. The metadata is in the same format as supplied + * when writing to the cache with {@link CacheWriteEntry}. This {@link Blob} should be able to be + * used multiple times. + * + * @return the metadata {@link Blob} + */ + Optional getMetadataBlob(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java new file mode 100644 index 0000000000..277654ec54 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +/** + * Interface for queries to a cache storage engine. + * + *

The cache storage engine stores layer data as {@link CacheWriteEntry}s. These entries are read + * out as {@link CacheReadEntry}s. Cache entries can be retrieved by the layer digest. + * + *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). + * The selectors do not need to be unique. An example of a selector could be the digest of the list + * of source file paths that constructed the cached layer. + * + *

Implementations must be thread-safe and should be immutable. + */ +public interface CacheStorage { + + /** + * Saves the {@link CacheWriteEntry}. + * + * @param cacheWriteEntry the {@link CacheWriteEntry} + * @return the {@link CacheReadEntry} for the written {@link CacheWriteEntry} + * @throws IOException if an I/O exception occurs + */ + CacheReadEntry save(CacheWriteEntry cacheWriteEntry) throws IOException; + + /** + * Lists all the layer digests stored. + * + * @return the list of layer digests (that can be retrieved via {@link #retrieve} + * @throws IOException if an I/O exception occurs + */ + List listDigests() throws IOException; + + /** + * Retrieves the {@link CacheReadEntry} for the layer with digest {@code layerDigest}. + * + * @param layerDigest the layer digest + * @return the {@link CacheReadEntry} referenced by the layer digest + * @throws IOException if an I/O exception occurs + */ + Optional retrieve(DescriptorDigest layerDigest) throws IOException; + + /** + * Queries for layer digests that can be selected with the {@code selector}. + * + * @param selector the selector to query with + * @return the list of layer digests selected + * @throws IOException if an I/O exception occurs + */ + List listDigestsBySelector(DescriptorDigest selector) throws IOException; +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java new file mode 100644 index 0000000000..ca806fbc9f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.util.Optional; + +/** Represents layer data to write to the cache. Implementations must be immutable. */ +public interface CacheWriteEntry { + + /** + * Gets the {@link Blob} to write as the layer contents. + * + * @return the layer {@link Blob} + */ + Blob getLayerBlob(); + + /** + * Gets the optional selector digest to also reference this layer data. A selector digest may be a + * secondary identifier for a layer that is distinct from the default layer digest. + * + *

For example, it is useful as an inexpensive alternative reference to a layer compared to + * calculating the primary layer digest (SHA256 of compressed tarball). + * + * @return the selector digest + */ + Optional getSelector(); + + /** + * Gets the optional {@link Blob} to write as the arbitrary layer metadata. + * + *

For example, the metadata could contain last modified time, layer types, layer sources, etc. + * + * @return the metadata {@link Blob} + */ + Optional getMetadataBlob(); +} From e2cfdd16b400116be916b512b8c0c0e10f4b564f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 27 Aug 2018 16:02:59 -0400 Subject: [PATCH 0152/2020] Update credential retrieval order (#882) Uses known credentials ahead of known credhelper and inferred credentials (e.g. Maven settings) after known credhelper --- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../maven/PluginConfigurationProcessor.java | 2 +- .../common/DefaultCredentialRetrievers.java | 22 +++++++++++++++++-- .../DefaultCredentialRetrieversTest.java | 16 +++++++++++--- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 3786423cf2..7ee26587c4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -98,7 +98,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .getMavenSettingsServerCredentials() .retrieve(targetImage.getRegistry()); if (toCredential != null) { - defaultCredentialRetrievers.setKnownCredential( + defaultCredentialRetrievers.setInferredCredential( toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index d6ce31295a..d54c6cc190 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -88,7 +88,7 @@ static PluginConfigurationProcessor processCommonConfiguration( if (fromCredential == null) { fromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); if (fromCredential != null) { - defaultCredentialRetrievers.setKnownCredential( + defaultCredentialRetrievers.setInferredCredential( fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); } } else { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 29d379924c..71cd7d0ad3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -55,6 +55,7 @@ public static DefaultCredentialRetrievers init( private final CredentialRetrieverFactory credentialRetrieverFactory; @Nullable private CredentialRetriever knownCredentialRetriever; + @Nullable private CredentialRetriever inferredCredentialRetriever; @Nullable private String credentialHelperSuffix; private DefaultCredentialRetrievers(CredentialRetrieverFactory credentialRetrieverFactory) { @@ -74,6 +75,20 @@ public DefaultCredentialRetrievers setKnownCredential( return this; } + /** + * Sets the inferred {@link Credential} to use in the default credential retrievers. + * + * @param inferredCredential the inferred credential + * @param credentialSource the source of the inferred credential (for logging) + * @return this + */ + public DefaultCredentialRetrievers setInferredCredential( + Credential inferredCredential, String credentialSource) { + inferredCredentialRetriever = + credentialRetrieverFactory.known(inferredCredential, credentialSource); + return this; + } + /** * Sets the suffix for a known credential helper. * @@ -94,13 +109,16 @@ public DefaultCredentialRetrievers setCredentialHelperSuffix( */ public List asList() { List credentialRetrievers = new ArrayList<>(); + if (knownCredentialRetriever != null) { + credentialRetrievers.add(knownCredentialRetriever); + } if (credentialHelperSuffix != null) { credentialRetrievers.add( credentialRetrieverFactory.dockerCredentialHelper( DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); } - if (knownCredentialRetriever != null) { - credentialRetrievers.add(knownCredentialRetriever); + if (inferredCredentialRetriever != null) { + credentialRetrievers.add(inferredCredentialRetriever); } credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index c5ccf3652b..72cb63849b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -36,15 +36,22 @@ public class DefaultCredentialRetrieversTest { @Mock private CredentialRetrieverFactory mockCredentialRetrieverFactory; @Mock private CredentialRetriever mockDockerCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockKnownCredentialRetriever; + @Mock private CredentialRetriever mockInferredCredentialRetriever; @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; + private final Credential knownCredential = new Credential("username", "password"); + private final Credential inferredCredential = new Credential("username2", "password2"); + @Before public void setUp() { Mockito.when(mockCredentialRetrieverFactory.dockerCredentialHelper(Mockito.anyString())) .thenReturn(mockDockerCredentialHelperCredentialRetriever); - Mockito.when(mockCredentialRetrieverFactory.known(Mockito.any(), Mockito.anyString())) + Mockito.when(mockCredentialRetrieverFactory.known(knownCredential, "credentialSource")) .thenReturn(mockKnownCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.known(inferredCredential, "inferredCredentialSource")) + .thenReturn(mockInferredCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.inferCredentialHelper()) .thenReturn(mockInferCredentialHelperCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.dockerConfig()) @@ -63,21 +70,24 @@ public void testInitAsList() { @Test public void testInitAsList_all() { - Credential knownCredential = new Credential("username", "password"); List credentialRetrievers = DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) .setKnownCredential(knownCredential, "credentialSource") + .setInferredCredential(inferredCredential, "inferredCredentialSource") .setCredentialHelperSuffix("credentialHelperSuffix") .asList(); Assert.assertEquals( Arrays.asList( - mockDockerCredentialHelperCredentialRetriever, mockKnownCredentialRetriever, + mockDockerCredentialHelperCredentialRetriever, + mockInferredCredentialRetriever, mockInferCredentialHelperCredentialRetriever, mockDockerConfigCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource"); + Mockito.verify(mockCredentialRetrieverFactory) + .known(inferredCredential, "inferredCredentialSource"); Mockito.verify(mockCredentialRetrieverFactory) .dockerCredentialHelper("docker-credential-credentialHelperSuffix"); } From 54e1e2b438bb62c9f4aeab0ba84bc06e33f619d6 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 28 Aug 2018 10:00:54 -0400 Subject: [PATCH 0153/2020] Adds entries for all directories in layer tarball. (#772) --- .../jib/image/ReproducibleLayerBuilder.java | 67 ++++++++++++++++-- .../image/ReproducibleLayerBuilderTest.java | 69 ++++++++++--------- jib-gradle-plugin/CHANGELOG.md | 2 + jib-maven-plugin/CHANGELOG.md | 2 + 4 files changed, 102 insertions(+), 38 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index dd0fc73469..35fe881629 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -19,12 +19,16 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import com.google.common.collect.ImmutableList; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** @@ -34,6 +38,51 @@ */ public class ReproducibleLayerBuilder { + /** + * Holds a list of {@link TarArchiveEntry}s with unique extraction paths. The list also includes + * all parent directories for each extraction path. + */ + private static class UniqueTarArchiveEntries { + + /** + * Uses the current directory to act as the file input to TarArchiveEntry (since all directories + * are treated the same in {@link TarArchiveEntry#TarArchiveEntry(File, String)}, except for + * modification time, which is wiped away in {@link #build}). + */ + private static final File DIRECTORY_FILE = Paths.get(".").toFile(); + + private final List entries = new ArrayList<>(); + private final Set names = new HashSet<>(); + + /** + * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of + * the parent directories on the extraction path. + * + * @param tarArchiveEntry the {@link TarArchiveEntry} + */ + private void add(TarArchiveEntry tarArchiveEntry) { + if (names.contains(tarArchiveEntry.getName())) { + return; + } + + // Adds all directories along extraction paths to explicitly set permissions for those + // directories. + Path namePath = Paths.get(tarArchiveEntry.getName()); + if (namePath.getParent() != namePath.getRoot()) { + add(new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString())); + } + + entries.add(tarArchiveEntry); + names.add(tarArchiveEntry.getName()); + } + + private List getSortedEntries() { + List sortedEntries = new ArrayList<>(entries); + sortedEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + return sortedEntries; + } + } + /** * Builds the {@link TarArchiveEntry}s for adding this {@link LayerEntry} to a tarball archive. * @@ -44,6 +93,7 @@ private static List buildAsTarArchiveEntries(LayerEntry layerEn throws IOException { List tarArchiveEntries = new ArrayList<>(); + // Adds the files to extract relative to the extraction path. for (Path sourceFile : layerEntry.getSourceFiles()) { if (Files.isDirectory(sourceFile)) { new DirectoryWalker(sourceFile) @@ -101,19 +151,24 @@ public ReproducibleLayerBuilder addFiles(List sourceFiles, String extracti * @throws IOException if walking the source files fails */ public UnwrittenLayer build() throws IOException { - List filesystemEntries = new ArrayList<>(); + UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. - for (LayerEntry layerEntry : layerEntries.build()) { - filesystemEntries.addAll(buildAsTarArchiveEntries(layerEntry)); + List layerEntries = this.layerEntries.build(); + for (LayerEntry layerEntry : layerEntries) { + // Converts layerEntry to list of TarArchiveEntrys. + List tarArchiveEntries = buildAsTarArchiveEntries(layerEntry); + // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and + // adds parent directories for each extraction path. + tarArchiveEntries.forEach(uniqueTarArchiveEntries::add); } - // Sorts the entries by name. - filesystemEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + // Gets the entries sorted by extraction path. + List sortedFilesystemEntries = uniqueTarArchiveEntries.getSortedEntries(); // Adds all the files to a tar stream. TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - for (TarArchiveEntry entry : filesystemEntries) { + for (TarArchiveEntry entry : sortedFilesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. entry.setModTime(0); entry.setGroupId(0); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 3d433ce5ac..b2a2719f08 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,8 +18,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.BufferedOutputStream; @@ -72,6 +70,23 @@ private static void verifyNextTarArchiveEntry( Assert.assertEquals(expectedString, extractedString); } + /** + * Verifies that the next {@link TarArchiveEntry} in the {@link TarArchiveInputStream} is a + * directory with correct permissions. + * + * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from + * @param expectedExtractionPath the expected extraction path of the next entry + * @throws IOException if an I/O exception occurs + */ + private static void verifyNextTarArchiveEntryIsDirectory( + TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath) + throws IOException { + TarArchiveEntry extractionPathEntry = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals(expectedExtractionPath, extractionPathEntry.getName()); + Assert.assertTrue(extractionPathEntry.isDirectory()); + Assert.assertEquals(TarArchiveEntry.DEFAULT_DIR_MODE, extractionPathEntry.getMode()); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test @@ -95,37 +110,27 @@ public void testBuild() throws URISyntaxException, IOException { // Reads the file back. try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(Files.newInputStream(temporaryFile))) { - // Verifies that blobA was added. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); - - // Verifies that all the files have been added to the tarball stream. - ImmutableList layerDirectoryPaths = - new DirectoryWalker(layerDirectory).filter(path -> !path.equals(layerDirectory)).walk(); - for (Path path : layerDirectoryPaths) { - TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); - - StringBuilder expectedExtractionPath = new StringBuilder("extract/here/apple"); - for (Path pathComponent : layerDirectory.getParent().relativize(path)) { - expectedExtractionPath.append("/").append(pathComponent); - } - // Check path-equality because there might be an appended backslash in the header - // filename. - Assert.assertEquals( - Paths.get(expectedExtractionPath.toString()), Paths.get(header.getName())); - - // If is a normal file, checks that the file contents match. - if (Files.isRegularFile(path)) { - String expectedFileString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - - String extractedFileString = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - - Assert.assertEquals(expectedFileString, extractedFileString); - } - } - - // Verifies that blobA was added to the other location. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/b/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/a/b/bar", + Paths.get(Resources.getResource("layer/a/b/bar").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/c/cat", + Paths.get(Resources.getResource("layer/c/cat").toURI())); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/foo", + Paths.get(Resources.getResource("layer/foo").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/banana/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/banana/blobA", blobA); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7d927f5cab..fb13f79b06 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + ## 0.9.9 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index eca7fbe05c..f820668e87 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + ## 0.9.9 ### Added From 1e5975542b186e808ca9cf7deefbd3d362f5792b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 28 Aug 2018 10:47:17 -0400 Subject: [PATCH 0154/2020] Revert "Adds entries for all directories in layer tarball. (#772)" (#888) This reverts commit 54e1e2b438bb62c9f4aeab0ba84bc06e33f619d6. --- .../jib/image/ReproducibleLayerBuilder.java | 67 ++---------------- .../image/ReproducibleLayerBuilderTest.java | 69 +++++++++---------- jib-gradle-plugin/CHANGELOG.md | 2 - jib-maven-plugin/CHANGELOG.md | 2 - 4 files changed, 38 insertions(+), 102 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 35fe881629..dd0fc73469 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -19,16 +19,12 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import com.google.common.collect.ImmutableList; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** @@ -38,51 +34,6 @@ */ public class ReproducibleLayerBuilder { - /** - * Holds a list of {@link TarArchiveEntry}s with unique extraction paths. The list also includes - * all parent directories for each extraction path. - */ - private static class UniqueTarArchiveEntries { - - /** - * Uses the current directory to act as the file input to TarArchiveEntry (since all directories - * are treated the same in {@link TarArchiveEntry#TarArchiveEntry(File, String)}, except for - * modification time, which is wiped away in {@link #build}). - */ - private static final File DIRECTORY_FILE = Paths.get(".").toFile(); - - private final List entries = new ArrayList<>(); - private final Set names = new HashSet<>(); - - /** - * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of - * the parent directories on the extraction path. - * - * @param tarArchiveEntry the {@link TarArchiveEntry} - */ - private void add(TarArchiveEntry tarArchiveEntry) { - if (names.contains(tarArchiveEntry.getName())) { - return; - } - - // Adds all directories along extraction paths to explicitly set permissions for those - // directories. - Path namePath = Paths.get(tarArchiveEntry.getName()); - if (namePath.getParent() != namePath.getRoot()) { - add(new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString())); - } - - entries.add(tarArchiveEntry); - names.add(tarArchiveEntry.getName()); - } - - private List getSortedEntries() { - List sortedEntries = new ArrayList<>(entries); - sortedEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); - return sortedEntries; - } - } - /** * Builds the {@link TarArchiveEntry}s for adding this {@link LayerEntry} to a tarball archive. * @@ -93,7 +44,6 @@ private static List buildAsTarArchiveEntries(LayerEntry layerEn throws IOException { List tarArchiveEntries = new ArrayList<>(); - // Adds the files to extract relative to the extraction path. for (Path sourceFile : layerEntry.getSourceFiles()) { if (Files.isDirectory(sourceFile)) { new DirectoryWalker(sourceFile) @@ -151,24 +101,19 @@ public ReproducibleLayerBuilder addFiles(List sourceFiles, String extracti * @throws IOException if walking the source files fails */ public UnwrittenLayer build() throws IOException { - UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); + List filesystemEntries = new ArrayList<>(); // Adds all the layer entries as tar entries. - List layerEntries = this.layerEntries.build(); - for (LayerEntry layerEntry : layerEntries) { - // Converts layerEntry to list of TarArchiveEntrys. - List tarArchiveEntries = buildAsTarArchiveEntries(layerEntry); - // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and - // adds parent directories for each extraction path. - tarArchiveEntries.forEach(uniqueTarArchiveEntries::add); + for (LayerEntry layerEntry : layerEntries.build()) { + filesystemEntries.addAll(buildAsTarArchiveEntries(layerEntry)); } - // Gets the entries sorted by extraction path. - List sortedFilesystemEntries = uniqueTarArchiveEntries.getSortedEntries(); + // Sorts the entries by name. + filesystemEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); // Adds all the files to a tar stream. TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - for (TarArchiveEntry entry : sortedFilesystemEntries) { + for (TarArchiveEntry entry : filesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. entry.setModTime(0); entry.setGroupId(0); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index b2a2719f08..3d433ce5ac 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.BufferedOutputStream; @@ -70,23 +72,6 @@ private static void verifyNextTarArchiveEntry( Assert.assertEquals(expectedString, extractedString); } - /** - * Verifies that the next {@link TarArchiveEntry} in the {@link TarArchiveInputStream} is a - * directory with correct permissions. - * - * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from - * @param expectedExtractionPath the expected extraction path of the next entry - * @throws IOException if an I/O exception occurs - */ - private static void verifyNextTarArchiveEntryIsDirectory( - TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath) - throws IOException { - TarArchiveEntry extractionPathEntry = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals(expectedExtractionPath, extractionPathEntry.getName()); - Assert.assertTrue(extractionPathEntry.isDirectory()); - Assert.assertEquals(TarArchiveEntry.DEFAULT_DIR_MODE, extractionPathEntry.getMode()); - } - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test @@ -110,27 +95,37 @@ public void testBuild() throws URISyntaxException, IOException { // Reads the file back. try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(Files.newInputStream(temporaryFile))) { - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); + // Verifies that blobA was added. verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/"); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/"); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/b/"); - verifyNextTarArchiveEntry( - tarArchiveInputStream, - "extract/here/apple/layer/a/b/bar", - Paths.get(Resources.getResource("layer/a/b/bar").toURI())); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); - verifyNextTarArchiveEntry( - tarArchiveInputStream, - "extract/here/apple/layer/c/cat", - Paths.get(Resources.getResource("layer/c/cat").toURI())); - verifyNextTarArchiveEntry( - tarArchiveInputStream, - "extract/here/apple/layer/foo", - Paths.get(Resources.getResource("layer/foo").toURI())); - verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/banana/"); + + // Verifies that all the files have been added to the tarball stream. + ImmutableList layerDirectoryPaths = + new DirectoryWalker(layerDirectory).filter(path -> !path.equals(layerDirectory)).walk(); + for (Path path : layerDirectoryPaths) { + TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); + + StringBuilder expectedExtractionPath = new StringBuilder("extract/here/apple"); + for (Path pathComponent : layerDirectory.getParent().relativize(path)) { + expectedExtractionPath.append("/").append(pathComponent); + } + // Check path-equality because there might be an appended backslash in the header + // filename. + Assert.assertEquals( + Paths.get(expectedExtractionPath.toString()), Paths.get(header.getName())); + + // If is a normal file, checks that the file contents match. + if (Files.isRegularFile(path)) { + String expectedFileString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + String extractedFileString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + + Assert.assertEquals(expectedFileString, extractedFileString); + } + } + + // Verifies that blobA was added to the other location. verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/banana/blobA", blobA); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index fb13f79b06..7d927f5cab 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,8 +11,6 @@ All notable changes to this project will be documented in this file. ### Fixed -- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) - ## 0.9.9 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index f820668e87..eca7fbe05c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,8 +11,6 @@ All notable changes to this project will be documented in this file. ### Fixed -- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) - ## 0.9.9 ### Added From c8a0a13bd07e5c7d506fe2e2be590d213ecbbd99 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 28 Aug 2018 11:04:49 -0400 Subject: [PATCH 0155/2020] Only warn if inferred credential helper threw NonexistentServerUrlDockerCredentialHelperException (#883) --- .../tools/jib/frontend/CredentialRetrieverFactory.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 98eafda679..978138a282 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -171,17 +171,17 @@ CredentialRetriever inferCredentialHelper( + inferredCredentialHelperSuffix), dockerCredentialHelperFactory); - } catch (DockerCredentialHelperNotFoundException ex) { + } catch (DockerCredentialHelperNotFoundException + | NonexistentServerUrlDockerCredentialHelperException ex) { if (ex.getMessage() != null) { - // Warns the user that the specified (or inferred) credential helper is not on the - // system. + // Warns the user that the specified (or inferred) credential helper cannot be used. logger.warn(ex.getMessage()); if (ex.getCause() != null && ex.getCause().getMessage() != null) { logger.info(" Caused by: " + ex.getCause().getMessage()); } } - } catch (NonexistentServerUrlDockerCredentialHelperException | IOException ex) { + } catch (IOException ex) { throw new CredentialRetrievalException(ex); } } From 2fd78a8c8846d8a036582eb873458c35b170834e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 28 Aug 2018 11:24:55 -0400 Subject: [PATCH 0156/2020] Rename credential helper exceptions (#884) --- ...DockerCredentialHelperIntegrationTest.java | 12 +++---- .../RetrieveRegistryCredentialsStep.java | 2 +- .../credentials/CredentialRetriever.java | 9 +---- .../frontend/CredentialRetrieverFactory.java | 18 +++++----- ...=> CredentialHelperNotFoundException.java} | 4 +-- ...ialHelperUnhandledServerUrlException.java} | 4 +-- .../CredentialRetrievalException.java | 35 +++++++++++++++++++ .../DockerConfigCredentialRetriever.java | 4 +-- .../credentials/DockerCredentialHelper.java | 21 +++++------ .../RetrieveRegistryCredentialsStepTest.java | 2 +- .../CredentialRetrieverFactoryTest.java | 20 +++++------ .../DockerConfigCredentialRetrieverTest.java | 4 +-- 12 files changed, 81 insertions(+), 54 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/{DockerCredentialHelperNotFoundException.java => CredentialHelperNotFoundException.java} (83%) rename jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/{NonexistentServerUrlDockerCredentialHelperException.java => CredentialHelperUnhandledServerUrlException.java} (88%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index d06d081139..29f4f2282b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -33,8 +33,8 @@ public class DockerCredentialHelperIntegrationTest { /** Tests retrieval via {@code docker-credential-gcr} CLI. */ @Test public void testRetrieveGCR() - throws IOException, NonexistentServerUrlDockerCredentialHelperException, - DockerCredentialHelperNotFoundException, URISyntaxException, InterruptedException { + throws IOException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException, URISyntaxException, InterruptedException { new Command("docker-credential-gcr", "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); @@ -48,7 +48,7 @@ public void testRetrieveGCR() @Test public void testRetrieve_nonexistentCredentialHelper() - throws IOException, NonexistentServerUrlDockerCredentialHelperException { + throws IOException, CredentialHelperUnhandledServerUrlException { try { DockerCredentialHelper fakeDockerCredentialHelper = new DockerCredentialHelperFactory().newDockerCredentialHelper("", "fake-cloud-provider"); @@ -57,7 +57,7 @@ public void testRetrieve_nonexistentCredentialHelper() Assert.fail("Retrieve should have failed for nonexistent credential helper"); - } catch (DockerCredentialHelperNotFoundException ex) { + } catch (CredentialHelperNotFoundException ex) { Assert.assertEquals( "The system does not have docker-credential-fake-cloud-provider CLI", ex.getMessage()); } @@ -65,7 +65,7 @@ public void testRetrieve_nonexistentCredentialHelper() @Test public void testRetrieve_nonexistentServerUrl() - throws IOException, DockerCredentialHelperNotFoundException { + throws IOException, CredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = new DockerCredentialHelperFactory().newDockerCredentialHelper("fake.server.url", "gcr"); @@ -74,7 +74,7 @@ public void testRetrieve_nonexistentServerUrl() Assert.fail("Retrieve should have failed for nonexistent server URL"); - } catch (NonexistentServerUrlDockerCredentialHelperException ex) { + } catch (CredentialHelperUnhandledServerUrlException ex) { Assert.assertThat( ex.getMessage(), CoreMatchers.containsString( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index ed053eceee..a8393dca70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java index 9a715a192e..cb863e209b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -16,20 +16,13 @@ package com.google.cloud.tools.jib.configuration.credentials; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import javax.annotation.Nullable; /** Retrieves credentials for a registry. */ @FunctionalInterface public interface CredentialRetriever { - /** Thrown if something went wrong during {@link CredentialRetriever#retrieve}. */ - class CredentialRetrievalException extends Exception { - - public CredentialRetrievalException(Throwable cause) { - super(cause); - } - } - /** * Fetches the credentials. Implementations must be thread-safe. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 978138a282..b5a0bfe4c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -19,12 +19,12 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperNotFoundException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -171,8 +171,8 @@ CredentialRetriever inferCredentialHelper( + inferredCredentialHelperSuffix), dockerCredentialHelperFactory); - } catch (DockerCredentialHelperNotFoundException - | NonexistentServerUrlDockerCredentialHelperException ex) { + } catch (CredentialHelperNotFoundException + | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. logger.warn(ex.getMessage()); @@ -198,12 +198,12 @@ CredentialRetriever dockerCredentialHelper( try { return retrieveFromDockerCredentialHelper(credentialHelper, dockerCredentialHelperFactory); - } catch (NonexistentServerUrlDockerCredentialHelperException ex) { + } catch (CredentialHelperUnhandledServerUrlException ex) { logger.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); return null; - } catch (DockerCredentialHelperNotFoundException | IOException ex) { + } catch (CredentialHelperNotFoundException | IOException ex) { throw new CredentialRetrievalException(ex); } }; @@ -229,8 +229,8 @@ CredentialRetriever dockerConfig( private Credential retrieveFromDockerCredentialHelper( Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) - throws NonexistentServerUrlDockerCredentialHelperException, - DockerCredentialHelperNotFoundException, IOException { + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { Credential credentials = dockerCredentialHelperFactory .newDockerCredentialHelper(imageReference.getRegistry(), credentialHelper) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java similarity index 83% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java index c626aa8a38..affbe77953 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperNotFoundException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java @@ -19,9 +19,9 @@ import java.nio.file.Path; /** Thrown because the requested credential helper CLI does not exist. */ -public class DockerCredentialHelperNotFoundException extends Exception { +public class CredentialHelperNotFoundException extends CredentialRetrievalException { - DockerCredentialHelperNotFoundException(Path credentialHelper, Throwable cause) { + CredentialHelperNotFoundException(Path credentialHelper, Throwable cause) { super("The system does not have " + credentialHelper + " CLI", cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java similarity index 88% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java index 1ce09c9bb7..da173514ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/NonexistentServerUrlDockerCredentialHelperException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java @@ -19,9 +19,9 @@ import java.nio.file.Path; /** Thrown because the credential helper does not have credentials for the specified server URL. */ -public class NonexistentServerUrlDockerCredentialHelperException extends Exception { +public class CredentialHelperUnhandledServerUrlException extends CredentialRetrievalException { - NonexistentServerUrlDockerCredentialHelperException( + CredentialHelperUnhandledServerUrlException( Path credentialHelper, String serverUrl, String credentialHelperOutput) { super( "The credential helper (" diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java new file mode 100644 index 0000000000..f07da2fe25 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; + +/** Thrown if something went wrong during {@link CredentialRetriever#retrieve}. */ +public class CredentialRetrievalException extends Exception { + + CredentialRetrievalException(String message, Throwable cause) { + super(message, cause); + } + + CredentialRetrievalException(String message) { + super(message); + } + + public CredentialRetrievalException(Throwable cause) { + super(cause); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 52e5d29c6a..a17826f632 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -129,8 +129,8 @@ private Credential retrieve(DockerConfig dockerConfig, String registryAlias) { return dockerCredentialHelper.retrieve(); } catch (IOException - | NonexistentServerUrlDockerCredentialHelperException - | DockerCredentialHelperNotFoundException ex) { + | CredentialHelperUnhandledServerUrlException + | CredentialHelperNotFoundException ex) { // Ignores credential helper retrieval exceptions. } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index e1e26d6bca..4a5389d062 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -69,13 +69,14 @@ private static class DockerCredentialsTemplate implements JsonTemplate { * } * * @return the Docker credentials by calling the corresponding CLI - * @throws IOException if writing/reading process input/output fails. - * @throws NonexistentServerUrlDockerCredentialHelperException if credentials are not found. - * @throws DockerCredentialHelperNotFoundException if the credential helper CLI doesn't exist. + * @throws IOException if writing/reading process input/output fails + * @throws CredentialHelperUnhandledServerUrlException if no credentials could be found for the + * corresponding server + * @throws CredentialHelperNotFoundException if the credential helper CLI doesn't exist */ public Credential retrieve() - throws IOException, NonexistentServerUrlDockerCredentialHelperException, - DockerCredentialHelperNotFoundException { + throws IOException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException { try { String[] credentialHelperCommand = {credentialHelper.toString(), "get"}; @@ -90,14 +91,14 @@ public Credential retrieve() // Throws an exception if the credential store does not have credentials for serverUrl. if (output.contains("credentials not found in native keychain")) { - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } if (output.isEmpty()) { try (InputStreamReader processStderrReader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { String errorOutput = CharStreams.toString(processStderrReader); - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, errorOutput); } } @@ -107,14 +108,14 @@ public Credential retrieve() JsonTemplateMapper.readJson(output, DockerCredentialsTemplate.class); if (Strings.isNullOrEmpty(dockerCredentials.Username) || Strings.isNullOrEmpty(dockerCredentials.Secret)) { - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } return new Credential(dockerCredentials.Username, dockerCredentials.Secret); } catch (JsonProcessingException ex) { - throw new NonexistentServerUrlDockerCredentialHelperException( + throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } } @@ -127,7 +128,7 @@ public Credential retrieve() // Checks if the failure is due to a nonexistent credential helper CLI. if (ex.getMessage().contains("No such file or directory") || ex.getMessage().contains("cannot find the file")) { - throw new DockerCredentialHelperNotFoundException(credentialHelper, ex); + throw new CredentialHelperNotFoundException(credentialHelper, ex); } throw ex; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 61a2c0fa05..b7e7dc8714 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -21,8 +21,8 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever.CredentialRetrievalException; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.Arrays; import java.util.Collections; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 24908de8ac..a168bedc5a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; +import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperNotFoundException; -import com.google.cloud.tools.jib.registry.credentials.NonexistentServerUrlDockerCredentialHelperException; import java.io.IOException; import java.nio.file.Paths; import org.junit.Assert; @@ -45,20 +45,18 @@ public class CredentialRetrieverFactoryTest { @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; - /** - * A {@link DockerCredentialHelper} that throws {@link DockerCredentialHelperNotFoundException}. - */ + /** A {@link DockerCredentialHelper} that throws {@link CredentialHelperNotFoundException}. */ @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - @Mock private DockerCredentialHelperNotFoundException mockDockerCredentialHelperNotFoundException; + @Mock private CredentialHelperNotFoundException mockCredentialHelperNotFoundException; @Before public void setUp() - throws NonexistentServerUrlDockerCredentialHelperException, - DockerCredentialHelperNotFoundException, IOException { + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockDockerCredentialHelperNotFoundException); + .thenThrow(mockCredentialHelperNotFoundException); } @Test @@ -103,8 +101,8 @@ public void testInferCredentialHelper() throws Exception { .retrieve()); Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); - Mockito.when(mockDockerCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); - Mockito.when(mockDockerCredentialHelperNotFoundException.getCause()) + Mockito.when(mockCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); + Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertNull( credentialRetrieverFactory diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 36774fc7dc..5e33541464 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -43,8 +43,8 @@ public class DockerConfigCredentialRetrieverTest { @Before public void setUp() - throws URISyntaxException, NonexistentServerUrlDockerCredentialHelperException, - DockerCredentialHelperNotFoundException, IOException { + throws URISyntaxException, CredentialHelperUnhandledServerUrlException, + CredentialHelperNotFoundException, IOException { dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); From 529da821db63ecbfbd6580d88ba4e9b26bd873cc Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 28 Aug 2018 17:25:08 -0400 Subject: [PATCH 0157/2020] Adds default implementations for CacheWriteEntry and CacheReadEntry. (#874) --- .../jib/ncache/DefaultCacheReadEntry.java | 126 ++++++++++++++++++ .../jib/ncache/DefaultCacheWriteEntry.java | 76 +++++++++++ .../jib/ncache/DefaultCacheReadEntryTest.java | 99 ++++++++++++++ .../ncache/DefaultCacheWriteEntryTest.java | 52 ++++++++ 4 files changed, 353 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java new file mode 100644 index 0000000000..50d0874f8a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.base.Preconditions; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Default implementation of {@link CacheReadEntry}. */ +public class DefaultCacheReadEntry implements CacheReadEntry { + + /** Builds a {@link CacheReadEntry}. */ + public static class Builder { + + @Nullable private DescriptorDigest layerDigest; + @Nullable private DescriptorDigest layerDiffId; + private long layerSize = -1; + @Nullable private Blob layerBlob; + @Nullable private Blob metadataBlob; + + private Builder() {} + + public Builder setLayerDigest(DescriptorDigest layerDigest) { + this.layerDigest = layerDigest; + return this; + } + + public Builder setLayerDiffId(DescriptorDigest layerDiffId) { + this.layerDiffId = layerDiffId; + return this; + } + + public Builder setLayerSize(long layerSize) { + this.layerSize = layerSize; + return this; + } + + public Builder setLayerBlob(Blob layerBlob) { + this.layerBlob = layerBlob; + return this; + } + + public Builder setMetadataBlob(@Nullable Blob metadataBlob) { + this.metadataBlob = metadataBlob; + return this; + } + + public CacheReadEntry build() { + return new DefaultCacheReadEntry( + Preconditions.checkNotNull(layerDigest, "layerDigest required"), + Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), + layerSize, + Preconditions.checkNotNull(layerBlob, "layerBlob required"), + metadataBlob); + } + } + + /** + * Creates a new {@link Builder} for a {@link CacheReadEntry}. + * + * @return the new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + private final DescriptorDigest layerDigest; + private final DescriptorDigest layerDiffId; + private final long layerSize; + private final Blob layerBlob; + @Nullable private final Blob metadataBlob; + + private DefaultCacheReadEntry( + DescriptorDigest layerDigest, + DescriptorDigest layerDiffId, + long layerSize, + Blob layerBlob, + @Nullable Blob metadataBlob) { + this.layerDigest = layerDigest; + this.layerDiffId = layerDiffId; + this.layerSize = layerSize; + this.layerBlob = layerBlob; + this.metadataBlob = metadataBlob; + } + + @Override + public DescriptorDigest getLayerDigest() { + return layerDigest; + } + + @Override + public DescriptorDigest getLayerDiffId() { + return layerDiffId; + } + + @Override + public long getLayerSize() { + return layerSize; + } + + @Override + public Blob getLayerBlob() { + return layerBlob; + } + + @Override + public Optional getMetadataBlob() { + return Optional.ofNullable(metadataBlob); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java new file mode 100644 index 0000000000..8b76c6f710 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.util.Optional; +import javax.annotation.Nullable; + +/** A default implementation of {@link CacheWriteEntry}. */ +public class DefaultCacheWriteEntry implements CacheWriteEntry { + + /** + * Constructs a {@link CacheWriteEntry} with only the layer {@link Blob}. + * + * @param layerBlob the layer {@link Blob} + * @return the new {@link CacheWriteEntry} + */ + public static CacheWriteEntry layerOnly(Blob layerBlob) { + return new DefaultCacheWriteEntry(layerBlob, null, null); + } + + /** + * Constructs a {@link CacheWriteEntry} with a layer {@link Blob}, an additional selector digest, + * and a metadata {@link Blob}. + * + * @param layerBlob the layer {@link Blob} + * @param selector the selector digest + * @param metadataBlob the metadata {@link Blob} + * @return the new {@link CacheWriteEntry} + */ + public static CacheWriteEntry withSelectorAndMetadata( + Blob layerBlob, DescriptorDigest selector, Blob metadataBlob) { + return new DefaultCacheWriteEntry(layerBlob, selector, metadataBlob); + } + + private final Blob layerBlob; + @Nullable private final DescriptorDigest selector; + @Nullable private final Blob metadataBlob; + + private DefaultCacheWriteEntry( + Blob layerBlob, @Nullable DescriptorDigest selector, @Nullable Blob metadataBlob) { + this.layerBlob = layerBlob; + this.selector = selector; + this.metadataBlob = metadataBlob; + } + + @Override + public Blob getLayerBlob() { + return layerBlob; + } + + @Override + public Optional getSelector() { + return Optional.ofNullable(selector); + } + + @Override + public Optional getMetadataBlob() { + return Optional.ofNullable(metadataBlob); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java new file mode 100644 index 0000000000..bcd2dc15c5 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DefaultCacheReadEntry}. */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultCacheReadEntryTest { + + @Mock private DescriptorDigest mockLayerDigest; + @Mock private DescriptorDigest mockLayerDiffId; + + @Test + public void testBuilder_fail() { + try { + DefaultCacheReadEntry.builder().build(); + Assert.fail("missing required"); + + } catch (NullPointerException ex) { + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDigest")); + } + + try { + DefaultCacheReadEntry.builder().setLayerDigest(mockLayerDigest).build(); + Assert.fail("missing required"); + + } catch (NullPointerException ex) { + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDiffId")); + } + + try { + DefaultCacheReadEntry.builder() + .setLayerDigest(mockLayerDigest) + .setLayerDiffId(mockLayerDiffId) + .build(); + Assert.fail("missing required"); + + } catch (NullPointerException ex) { + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerBlob")); + } + } + + @Test + public void testBuilder_noMetadataBlob() throws IOException { + CacheReadEntry cacheReadEntry = + DefaultCacheReadEntry.builder() + .setLayerDigest(mockLayerDigest) + .setLayerDiffId(mockLayerDiffId) + .setLayerSize(1337) + .setLayerBlob(Blobs.from("layerBlob")) + .build(); + Assert.assertEquals(mockLayerDigest, cacheReadEntry.getLayerDigest()); + Assert.assertEquals(mockLayerDiffId, cacheReadEntry.getLayerDiffId()); + Assert.assertEquals(1337, cacheReadEntry.getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheReadEntry.getLayerBlob())); + Assert.assertFalse(cacheReadEntry.getMetadataBlob().isPresent()); + } + + @Test + public void testBuilder_withMetadataBlob() throws IOException { + CacheReadEntry cacheReadEntry = + DefaultCacheReadEntry.builder() + .setLayerDigest(mockLayerDigest) + .setLayerDiffId(mockLayerDiffId) + .setLayerSize(1337) + .setLayerBlob(Blobs.from("layerBlob")) + .setMetadataBlob(Blobs.from("metadataBlob")) + .build(); + Assert.assertEquals(mockLayerDigest, cacheReadEntry.getLayerDigest()); + Assert.assertEquals(mockLayerDiffId, cacheReadEntry.getLayerDiffId()); + Assert.assertEquals(1337, cacheReadEntry.getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheReadEntry.getLayerBlob())); + Assert.assertEquals( + "metadataBlob", Blobs.writeToString(cacheReadEntry.getMetadataBlob().orElse(null))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java new file mode 100644 index 0000000000..b8dead86e8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DefaultCacheWriteEntry}. */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultCacheWriteEntryTest { + + @Mock private DescriptorDigest mockSelector; + + @Test + public void testLayerOnly() throws IOException { + CacheWriteEntry cacheWriteEntry = DefaultCacheWriteEntry.layerOnly(Blobs.from("layerBlob")); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWriteEntry.getLayerBlob())); + Assert.assertFalse(cacheWriteEntry.getSelector().isPresent()); + Assert.assertFalse(cacheWriteEntry.getMetadataBlob().isPresent()); + } + + @Test + public void testWithSelectorAndMetadata() throws IOException { + CacheWriteEntry cacheWriteEntry = + DefaultCacheWriteEntry.withSelectorAndMetadata( + Blobs.from("layerBlob"), mockSelector, Blobs.from("metadataBlob")); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWriteEntry.getLayerBlob())); + Assert.assertEquals(mockSelector, cacheWriteEntry.getSelector().orElse(null)); + Assert.assertEquals( + "metadataBlob", Blobs.writeToString(cacheWriteEntry.getMetadataBlob().orElse(null))); + } +} From 907827f857880b041bfac9676e5596cee55301f4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 28 Aug 2018 17:35:25 -0400 Subject: [PATCH 0158/2020] Write history as part of layer metadata (#877) --- .../builder/BuildStepsIntegrationTest.java | 13 +- .../jib/builder/steps/BuildImageStep.java | 50 ++++-- .../google/cloud/tools/jib/image/Image.java | 23 +++ .../json/ContainerConfigurationTemplate.java | 23 +++ .../tools/jib/image/json/HistoryEntry.java | 163 ++++++++++++++++++ .../jib/image/json/ImageToJsonTranslator.java | 5 + .../jib/image/json/JsonToImageTranslator.java | 4 + .../jib/builder/steps/BuildImageStepTest.java | 67 +++++++ .../ContainerConfigurationTemplateTest.java | 29 ++++ .../image/json/ImageToJsonTranslatorTest.java | 14 +- .../image/json/JsonToImageTranslatorTest.java | 14 ++ .../test/resources/json/containerconfig.json | 2 +- .../json/translated_ocimanifest.json | 2 +- .../json/translated_v22manifest.json | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 16 files changed, 396 insertions(+), 17 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 2a36306bc5..3ff8f6d9ae 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -113,6 +113,9 @@ public void testSteps_forBuildToDockerRegistry() + " \"2001/tcp\": {},\n" + " \"2002/tcp\": {},\n" + " \"3000/udp\": {}")); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib")); + Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } @@ -138,17 +141,18 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() public void testSteps_forBuildToDockerDaemon() throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + String imageReference = "testdocker"; BuildConfiguration buildConfiguration = getBuildConfiguration( ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of(null, "testdocker", null)); + ImageReference.of(null, imageReference, null)); Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToDockerDaemon( buildConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) .run(); - String dockerContainerConfig = new Command("docker", "inspect", "testdocker").run(); + String dockerContainerConfig = new Command("docker", "inspect", imageReference).run(); Assert.assertThat( dockerContainerConfig, CoreMatchers.containsString( @@ -165,8 +169,11 @@ public void testSteps_forBuildToDockerDaemon() + " \"key1\": \"value1\",\n" + " \"key2\": \"value2\"\n" + " }")); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib")); + Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", "testdocker").run()); + "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index a5a2627b82..e0785ef516 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -25,10 +25,12 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -94,22 +96,50 @@ private Image afterCachedLayersSteps() try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { + Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); + ContainerConfiguration containerConfiguration = + buildConfiguration.getContainerConfiguration(); + + // Base image layers + List baseImageLayers = + NonBlockingSteps.get(pullAndCacheBaseImageLayersStep); + for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : baseImageLayers) { imageBuilder.addLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)); } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); - } - // Parameters that we passthrough from the base image - Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); + // Passthrough config and count non-empty history entries + int nonEmptyLayerCount = 0; + for (HistoryEntry historyObject : baseImage.getHistory()) { + imageBuilder.addHistory(historyObject); + if (!historyObject.hasCorrespondingLayer()) { + nonEmptyLayerCount++; + } + } imageBuilder.addEnvironment(baseImage.getEnvironment()); imageBuilder.addLabels(baseImage.getLabels()); - ContainerConfiguration containerConfiguration = - buildConfiguration.getContainerConfiguration(); + // Add history elements for non-empty layers that don't have one yet + Instant layerCreationTime = + containerConfiguration == null ? Instant.EPOCH : containerConfiguration.getCreationTime(); + for (int count = 0; count < baseImageLayers.size() - nonEmptyLayerCount; count++) { + imageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(layerCreationTime) + .setComment("auto-generated by Jib") + .build()); + } + + // Add built layers/configuration + for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : + buildAndCacheApplicationLayerSteps) { + imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); + imageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(layerCreationTime) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()); + } if (containerConfiguration != null) { imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); imageBuilder.setCreated(containerConfiguration.getCreationTime()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 30f6e409fb..3e9478494b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.time.Instant; @@ -31,6 +32,7 @@ public class Image { public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); + private ImmutableList.Builder historyBuilder = ImmutableList.builder(); private ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); private ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); @@ -145,10 +147,22 @@ public Builder addLayer(T layer) throws LayerPropertyNotFoundException { return this; } + /** + * Adds a history element to the image. + * + * @param history the history object to add + * @return this + */ + public Builder addHistory(HistoryEntry history) { + historyBuilder.add(history); + return this; + } + public Image build() { return new Image<>( created, imageLayersBuilder.build(), + historyBuilder.build(), environmentBuilder.build(), entrypoint, javaArguments, @@ -167,6 +181,9 @@ public static Builder builder() { /** The layers of the image, in the order in which they are applied. */ private final ImageLayers layers; + /** The commands used to build each layer of the image */ + private final ImmutableList history; + /** Environment variable definitions for running the image, in the format {@code NAME=VALUE}. */ @Nullable private final ImmutableMap environment; @@ -185,6 +202,7 @@ public static Builder builder() { private Image( @Nullable Instant created, ImageLayers layers, + ImmutableList history, @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @Nullable ImmutableList javaArguments, @@ -192,6 +210,7 @@ private Image( @Nullable ImmutableMap labels) { this.created = created; this.layers = layers; + this.history = history; this.environment = environment; this.entrypoint = entrypoint; this.javaArguments = javaArguments; @@ -232,4 +251,8 @@ public ImmutableMap getLabels() { public ImmutableList getLayers() { return layers.getLayers(); } + + public ImmutableList getHistory() { + return history; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 6eae869a42..3aaaf32775 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -42,6 +42,18 @@ * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} } * "Labels": { "com.example.label": "value" } * }, + * "history": [ + * { + * "author": "Jib", + * "created": "1970-01-01T00:00:00Z", + * "created_by": "jib" + * }, + * { + * "author": "Jib", + * "created": "1970-01-01T00:00:00Z", + * "created_by": "jib" + * } + * ] * "rootfs": { * "diff_ids": [ * "sha256:2aebd096e0e237b447781353379722157e6c2d434b9ec5a0d63f2a6f07cf90c2", @@ -70,6 +82,9 @@ public class ContainerConfigurationTemplate implements JsonTemplate { /** Execution parameters that should be used as a base when running the container. */ private final ConfigurationObjectTemplate config = new ConfigurationObjectTemplate(); + /** Describes the history of each layer. */ + private final List history = new ArrayList<>(); + /** Layer content digests that are used to build the container filesystem. */ private final RootFilesystemObjectTemplate rootfs = new RootFilesystemObjectTemplate(); @@ -137,10 +152,18 @@ public void addLayerDiffId(DescriptorDigest diffId) { rootfs.diff_ids.add(diffId); } + public void addHistoryEntry(HistoryEntry historyEntry) { + history.add(historyEntry); + } + List getDiffIds() { return rootfs.diff_ids; } + List getHistory() { + return history; + } + @Nullable String getCreated() { return created; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java new file mode 100644 index 0000000000..2ff9e9bea1 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java @@ -0,0 +1,163 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.json.JsonTemplate; +import java.time.Instant; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Represents an item in the container configuration's {@code history} list. + * + * @see OCI + * image spec ({@code history} field) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HistoryEntry implements JsonTemplate { + + public static class Builder { + + @Nullable private Instant creationTimestamp; + @Nullable private String author; + @Nullable private String createdBy; + @Nullable private String comment; + @Nullable private Boolean emptyLayer; + + public Builder setCreationTimestamp(Instant creationTimestamp) { + this.creationTimestamp = creationTimestamp; + return this; + } + + public Builder setAuthor(String author) { + this.author = author; + return this; + } + + public Builder setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public Builder setComment(String comment) { + this.comment = comment; + return this; + } + + public Builder setEmptyLayer(Boolean emptyLayer) { + this.emptyLayer = emptyLayer; + return this; + } + + public HistoryEntry build() { + return new HistoryEntry( + creationTimestamp == null ? null : creationTimestamp.toString(), + author, + createdBy, + comment, + emptyLayer); + } + + private Builder() {} + } + + /** + * Creates a builder for a {@link HistoryEntry}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** The ISO-8601 formatted timestamp at which the image was created. */ + @JsonProperty("created") + @Nullable + private String creationTimestamp; + + /** The name of the author specified when committing the image. */ + @JsonProperty("author") + @Nullable + private String author; + + /** The command used to build the layer. */ + @JsonProperty("created_by") + @Nullable + private String createdBy; + + /** A custom message set when creating the layer. */ + @JsonProperty("comment") + @Nullable + private String comment; + + /** + * Whether or not the entry corresponds to a layer in the container ({@code @Nullable Boolean} to + * make field optional). + */ + @JsonProperty("empty_layer") + @Nullable + private Boolean emptyLayer; + + public HistoryEntry() {} + + private HistoryEntry( + @Nullable String creationTimestamp, + @Nullable String author, + @Nullable String createdBy, + @Nullable String comment, + @Nullable Boolean emptyLayer) { + this.author = author; + this.creationTimestamp = creationTimestamp; + this.createdBy = createdBy; + this.comment = comment; + this.emptyLayer = emptyLayer; + } + + /** + * Returns whether or not the history object corresponds to a layer in the container. + * + * @return {@code true} if the history object corresponds to a layer in the container + */ + @JsonIgnore + public boolean hasCorrespondingLayer() { + return emptyLayer == null ? false : emptyLayer; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof HistoryEntry) { + HistoryEntry otherHistory = (HistoryEntry) other; + return Objects.equals(otherHistory.creationTimestamp, creationTimestamp) + && Objects.equals(otherHistory.author, author) + && Objects.equals(otherHistory.createdBy, createdBy) + && Objects.equals(otherHistory.comment, comment) + && Objects.equals(otherHistory.emptyLayer, emptyLayer); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(author, creationTimestamp, createdBy, comment, emptyLayer); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 824bbc6229..6fb575be9b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -115,6 +115,11 @@ public Blob getContainerConfigurationBlob() { template.addLayerDiffId(layer.getDiffId()); } + // Adds the history. + for (HistoryEntry historyObject : image.getHistory()) { + template.addHistoryEntry(historyObject); + } + // Sets the creation time. Instant#toString() returns an ISO-8601 formatted string. template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index a6ac326c1f..37c3367408 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -110,6 +110,7 @@ public static Image toImage( } List diffIds = containerConfigurationTemplate.getDiffIds(); + List historyObjects = containerConfigurationTemplate.getHistory(); if (layers.size() != diffIds.size()) { throw new LayerCountMismatchException( @@ -124,6 +125,9 @@ public static Image toImage( imageBuilder.addLayer(new ReferenceLayer(noDiffIdLayer.getBlobDescriptor(), diffId)); } + for (HistoryEntry historyObject : historyObjects) { + imageBuilder.addHistory(historyObject); + } if (containerConfigurationTemplate.getCreated() != null) { try { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 924b5a0ce8..c94b16c888 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Futures; @@ -55,6 +56,8 @@ public class BuildImageStepTest { @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStep; private DescriptorDigest testDescriptorDigest; + private HistoryEntry nonEmptyLayerHistory; + private HistoryEntry emptyLayerHistory; @Before public void setUp() throws DigestException { @@ -76,10 +79,27 @@ public void setUp() throws DigestException { Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + nonEmptyLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("JibBase") + .setCreatedBy("jib-test") + .build(); + emptyLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("JibBase") + .setCreatedBy("jib-test") + .setEmptyLayer(true) + .build(); + Image baseImage = Image.builder() .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE")) .addLabel("base.label", "base.label.value") + .addHistory(nonEmptyLayerHistory) + .addHistory(emptyLayerHistory) + .addHistory(emptyLayerHistory) .build(); Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) .thenReturn(Futures.immediateFuture(testCachedLayer)); @@ -139,5 +159,52 @@ public void test_propagateBaseImageConfiguration() Assert.assertEquals( ImmutableMap.of("base.label", "base.label.value", "my.label", "my.label.value"), image.getLabels()); + + Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); + Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); + Assert.assertEquals(image.getHistory().get(2), emptyLayerHistory); + } + + @Test + public void test_generateHistoryObjects() throws ExecutionException, InterruptedException { + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + + // Make sure history is as expected + HistoryEntry expectedAddedBaseLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setComment("auto-generated by Jib") + .build(); + HistoryEntry expectedApplicationLayerHistory = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib") + .build(); + + // Base layers (1 non-empty propagated, 2 empty propagated, 2 non-empty generated) + Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); + Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); + Assert.assertEquals(image.getHistory().get(2), emptyLayerHistory); + Assert.assertEquals(image.getHistory().get(3), expectedAddedBaseLayerHistory); + Assert.assertEquals(image.getHistory().get(4), expectedAddedBaseLayerHistory); + + // Application layers (3 generated) + Assert.assertEquals(image.getHistory().get(5), expectedApplicationLayerHistory); + Assert.assertEquals(image.getHistory().get(6), expectedApplicationLayerHistory); + Assert.assertEquals(image.getHistory().get(7), expectedApplicationLayerHistory); + + // Should be exactly 8 total + Assert.assertEquals(8, image.getHistory().size()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index e8f3a7acbc..87562783c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.Resources; @@ -29,6 +30,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Instant; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -62,6 +64,19 @@ public void testToJson() throws IOException, URISyntaxException, DigestException containerConfigJson.addLayerDiffId( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); + containerConfigJson.addHistoryEntry( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build()); + containerConfigJson.addHistoryEntry( + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()); // Serializes the JSON object. ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); @@ -93,5 +108,19 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), containerConfigJson.getLayerDiffId(0)); + Assert.assertEquals( + ImmutableList.of( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build(), + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()), + containerConfigJson.getHistory()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 00f9d7f770..ee92dac87c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -73,7 +73,19 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { CachedLayer fakeLayer = new CachedLayer(Mockito.mock(Path.class), new BlobDescriptor(1000, fakeDigest), fakeDigest); testImageBuilder.addLayer(fakeLayer); - + testImageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build()); + testImageBuilder.addHistory( + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()); imageToJsonTranslator = new ImageToJsonTranslator(testImageBuilder.build()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 267d3915f7..4a1b31adca 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -168,6 +168,20 @@ private void testToImage_buildable( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), layers.get(0).getDiffId()); + Assert.assertEquals( + ImmutableList.of( + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Bazel") + .setCreatedBy("bazel build ...") + .setEmptyLayer(true) + .build(), + HistoryEntry.builder() + .setCreationTimestamp(Instant.ofEpochSecond(20)) + .setAuthor("Jib") + .setCreatedBy("jib") + .build()), + image.getHistory()); Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated()); Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index 2ae9606472..d0ffb8e451 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"}},"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/json/translated_ocimanifest.json index 01a2f68253..3c7daf77af 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b988b86bf80435bdfe7b2f361f5e5838bdf2abd5dcb5ff298122504f8fb80e95","size":396},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5ee46ef07ac9a0b4d5bc5d2733e524a9fd9a3cd9bbe88c75276fbd03f01bc709","size":579},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index 18641bc137..d01327ec73 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:b988b86bf80435bdfe7b2f361f5e5838bdf2abd5dcb5ff298122504f8fb80e95","size":396},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:5ee46ef07ac9a0b4d5bc5d2733e524a9fd9a3cd9bbe88c75276fbd03f01bc709","size":579},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7d927f5cab..1552a14679 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index eca7fbe05c..01c2e7980a 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) ### Changed From 17425fcab0f3eb5e650185b1b0a5d9fb1cc0a7f0 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 29 Aug 2018 10:49:13 -0400 Subject: [PATCH 0159/2020] Fixes CacheStorage interface and renames CacheReadEntry/CacheWriteEntry to CacheEntry/CacheWrite. (#896) --- .../{CacheReadEntry.java => CacheEntry.java} | 6 +-- .../cloud/tools/jib/ncache/CacheStorage.java | 22 +++++------ .../{CacheWriteEntry.java => CacheWrite.java} | 2 +- ...eReadEntry.java => DefaultCacheEntry.java} | 14 +++---- ...WriteEntry.java => DefaultCacheWrite.java} | 24 ++++++------ ...ryTest.java => DefaultCacheEntryTest.java} | 38 +++++++++---------- ...ryTest.java => DefaultCacheWriteTest.java} | 22 +++++------ 7 files changed, 64 insertions(+), 64 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/ncache/{CacheReadEntry.java => CacheEntry.java} (91%) rename jib-core/src/main/java/com/google/cloud/tools/jib/ncache/{CacheWriteEntry.java => CacheWrite.java} (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/ncache/{DefaultCacheReadEntry.java => DefaultCacheEntry.java} (90%) rename jib-core/src/main/java/com/google/cloud/tools/jib/ncache/{DefaultCacheWriteEntry.java => DefaultCacheWrite.java} (70%) rename jib-core/src/test/java/com/google/cloud/tools/jib/ncache/{DefaultCacheReadEntryTest.java => DefaultCacheEntryTest.java} (67%) rename jib-core/src/test/java/com/google/cloud/tools/jib/ncache/{DefaultCacheWriteEntryTest.java => DefaultCacheWriteTest.java} (68%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java similarity index 91% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java index d2ccac9d6a..0369e0b1d3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheReadEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java @@ -24,7 +24,7 @@ * Represents a cache entry for a layer stored in the cache. Implementations must be * immutable. */ -public interface CacheReadEntry { +public interface CacheEntry { /** * Gets the digest of the layer. @@ -58,8 +58,8 @@ public interface CacheReadEntry { /** * Gets the optional metadata blob for the layer. The metadata is in the same format as supplied - * when writing to the cache with {@link CacheWriteEntry}. This {@link Blob} should be able to be - * used multiple times. + * when writing to the cache with {@link CacheWrite}. This {@link Blob} should be able to be used + * multiple times. * * @return the metadata {@link Blob} */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 277654ec54..1846945169 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -24,8 +24,8 @@ /** * Interface for queries to a cache storage engine. * - *

The cache storage engine stores layer data as {@link CacheWriteEntry}s. These entries are read - * out as {@link CacheReadEntry}s. Cache entries can be retrieved by the layer digest. + *

The cache storage engine stores layer data as {@link CacheWrite}s. These entries are read out + * as {@link CacheEntry}s. Cache entries can be retrieved by the layer digest. * *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). * The selectors do not need to be unique. An example of a selector could be the digest of the list @@ -36,13 +36,13 @@ public interface CacheStorage { /** - * Saves the {@link CacheWriteEntry}. + * Saves the {@link CacheWrite}. * - * @param cacheWriteEntry the {@link CacheWriteEntry} - * @return the {@link CacheReadEntry} for the written {@link CacheWriteEntry} + * @param cacheWrite the {@link CacheWrite} + * @return the {@link CacheEntry} for the written {@link CacheWrite} * @throws IOException if an I/O exception occurs */ - CacheReadEntry save(CacheWriteEntry cacheWriteEntry) throws IOException; + CacheEntry write(CacheWrite cacheWrite) throws IOException; /** * Lists all the layer digests stored. @@ -53,20 +53,20 @@ public interface CacheStorage { List listDigests() throws IOException; /** - * Retrieves the {@link CacheReadEntry} for the layer with digest {@code layerDigest}. + * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheReadEntry} referenced by the layer digest + * @return the {@link CacheEntry} referenced by the layer digest * @throws IOException if an I/O exception occurs */ - Optional retrieve(DescriptorDigest layerDigest) throws IOException; + Optional retrieve(DescriptorDigest layerDigest) throws IOException; /** * Queries for layer digests that can be selected with the {@code selector}. * * @param selector the selector to query with - * @return the list of layer digests selected + * @return the layer digest selected, or {@link Optional#empty} if none found * @throws IOException if an I/O exception occurs */ - List listDigestsBySelector(DescriptorDigest selector) throws IOException; + Optional select(DescriptorDigest selector) throws IOException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java index ca806fbc9f..48b90d3dc5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWriteEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java @@ -21,7 +21,7 @@ import java.util.Optional; /** Represents layer data to write to the cache. Implementations must be immutable. */ -public interface CacheWriteEntry { +public interface CacheWrite { /** * Gets the {@link Blob} to write as the layer contents. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java similarity index 90% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java index 50d0874f8a..ae80bfe570 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java @@ -22,10 +22,10 @@ import java.util.Optional; import javax.annotation.Nullable; -/** Default implementation of {@link CacheReadEntry}. */ -public class DefaultCacheReadEntry implements CacheReadEntry { +/** Default implementation of {@link CacheEntry}. */ +public class DefaultCacheEntry implements CacheEntry { - /** Builds a {@link CacheReadEntry}. */ + /** Builds a {@link CacheEntry}. */ public static class Builder { @Nullable private DescriptorDigest layerDigest; @@ -61,8 +61,8 @@ public Builder setMetadataBlob(@Nullable Blob metadataBlob) { return this; } - public CacheReadEntry build() { - return new DefaultCacheReadEntry( + public CacheEntry build() { + return new DefaultCacheEntry( Preconditions.checkNotNull(layerDigest, "layerDigest required"), Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), layerSize, @@ -72,7 +72,7 @@ public CacheReadEntry build() { } /** - * Creates a new {@link Builder} for a {@link CacheReadEntry}. + * Creates a new {@link Builder} for a {@link CacheEntry}. * * @return the new {@link Builder} */ @@ -86,7 +86,7 @@ public static Builder builder() { private final Blob layerBlob; @Nullable private final Blob metadataBlob; - private DefaultCacheReadEntry( + private DefaultCacheEntry( DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java similarity index 70% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java index 8b76c6f710..f05050079a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java @@ -21,38 +21,38 @@ import java.util.Optional; import javax.annotation.Nullable; -/** A default implementation of {@link CacheWriteEntry}. */ -public class DefaultCacheWriteEntry implements CacheWriteEntry { +/** A default implementation of {@link CacheWrite}. */ +public class DefaultCacheWrite implements CacheWrite { /** - * Constructs a {@link CacheWriteEntry} with only the layer {@link Blob}. + * Constructs a {@link CacheWrite} with only the layer {@link Blob}. * * @param layerBlob the layer {@link Blob} - * @return the new {@link CacheWriteEntry} + * @return the new {@link CacheWrite} */ - public static CacheWriteEntry layerOnly(Blob layerBlob) { - return new DefaultCacheWriteEntry(layerBlob, null, null); + public static CacheWrite layerOnly(Blob layerBlob) { + return new DefaultCacheWrite(layerBlob, null, null); } /** - * Constructs a {@link CacheWriteEntry} with a layer {@link Blob}, an additional selector digest, - * and a metadata {@link Blob}. + * Constructs a {@link CacheWrite} with a layer {@link Blob}, an additional selector digest, and a + * metadata {@link Blob}. * * @param layerBlob the layer {@link Blob} * @param selector the selector digest * @param metadataBlob the metadata {@link Blob} - * @return the new {@link CacheWriteEntry} + * @return the new {@link CacheWrite} */ - public static CacheWriteEntry withSelectorAndMetadata( + public static CacheWrite withSelectorAndMetadata( Blob layerBlob, DescriptorDigest selector, Blob metadataBlob) { - return new DefaultCacheWriteEntry(layerBlob, selector, metadataBlob); + return new DefaultCacheWrite(layerBlob, selector, metadataBlob); } private final Blob layerBlob; @Nullable private final DescriptorDigest selector; @Nullable private final Blob metadataBlob; - private DefaultCacheWriteEntry( + private DefaultCacheWrite( Blob layerBlob, @Nullable DescriptorDigest selector, @Nullable Blob metadataBlob) { this.layerBlob = layerBlob; this.selector = selector; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java similarity index 67% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java index bcd2dc15c5..3f8f302e46 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheReadEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java @@ -26,9 +26,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DefaultCacheReadEntry}. */ +/** Tests for {@link DefaultCacheEntry}. */ @RunWith(MockitoJUnitRunner.class) -public class DefaultCacheReadEntryTest { +public class DefaultCacheEntryTest { @Mock private DescriptorDigest mockLayerDigest; @Mock private DescriptorDigest mockLayerDiffId; @@ -36,7 +36,7 @@ public class DefaultCacheReadEntryTest { @Test public void testBuilder_fail() { try { - DefaultCacheReadEntry.builder().build(); + DefaultCacheEntry.builder().build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -44,7 +44,7 @@ public void testBuilder_fail() { } try { - DefaultCacheReadEntry.builder().setLayerDigest(mockLayerDigest).build(); + DefaultCacheEntry.builder().setLayerDigest(mockLayerDigest).build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -52,7 +52,7 @@ public void testBuilder_fail() { } try { - DefaultCacheReadEntry.builder() + DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .build(); @@ -65,35 +65,35 @@ public void testBuilder_fail() { @Test public void testBuilder_noMetadataBlob() throws IOException { - CacheReadEntry cacheReadEntry = - DefaultCacheReadEntry.builder() + CacheEntry cacheEntry = + DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .setLayerSize(1337) .setLayerBlob(Blobs.from("layerBlob")) .build(); - Assert.assertEquals(mockLayerDigest, cacheReadEntry.getLayerDigest()); - Assert.assertEquals(mockLayerDiffId, cacheReadEntry.getLayerDiffId()); - Assert.assertEquals(1337, cacheReadEntry.getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheReadEntry.getLayerBlob())); - Assert.assertFalse(cacheReadEntry.getMetadataBlob().isPresent()); + Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); + Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); + Assert.assertEquals(1337, cacheEntry.getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheEntry.getLayerBlob())); + Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); } @Test public void testBuilder_withMetadataBlob() throws IOException { - CacheReadEntry cacheReadEntry = - DefaultCacheReadEntry.builder() + CacheEntry cacheEntry = + DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .setLayerSize(1337) .setLayerBlob(Blobs.from("layerBlob")) .setMetadataBlob(Blobs.from("metadataBlob")) .build(); - Assert.assertEquals(mockLayerDigest, cacheReadEntry.getLayerDigest()); - Assert.assertEquals(mockLayerDiffId, cacheReadEntry.getLayerDiffId()); - Assert.assertEquals(1337, cacheReadEntry.getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheReadEntry.getLayerBlob())); + Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); + Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); + Assert.assertEquals(1337, cacheEntry.getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheEntry.getLayerBlob())); Assert.assertEquals( - "metadataBlob", Blobs.writeToString(cacheReadEntry.getMetadataBlob().orElse(null))); + "metadataBlob", Blobs.writeToString(cacheEntry.getMetadataBlob().orElse(null))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java similarity index 68% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java index b8dead86e8..a80e2df946 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java @@ -25,28 +25,28 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DefaultCacheWriteEntry}. */ +/** Tests for {@link DefaultCacheWrite}. */ @RunWith(MockitoJUnitRunner.class) -public class DefaultCacheWriteEntryTest { +public class DefaultCacheWriteTest { @Mock private DescriptorDigest mockSelector; @Test public void testLayerOnly() throws IOException { - CacheWriteEntry cacheWriteEntry = DefaultCacheWriteEntry.layerOnly(Blobs.from("layerBlob")); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWriteEntry.getLayerBlob())); - Assert.assertFalse(cacheWriteEntry.getSelector().isPresent()); - Assert.assertFalse(cacheWriteEntry.getMetadataBlob().isPresent()); + CacheWrite cacheWrite = DefaultCacheWrite.layerOnly(Blobs.from("layerBlob")); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); + Assert.assertFalse(cacheWrite.getSelector().isPresent()); + Assert.assertFalse(cacheWrite.getMetadataBlob().isPresent()); } @Test public void testWithSelectorAndMetadata() throws IOException { - CacheWriteEntry cacheWriteEntry = - DefaultCacheWriteEntry.withSelectorAndMetadata( + CacheWrite cacheWrite = + DefaultCacheWrite.withSelectorAndMetadata( Blobs.from("layerBlob"), mockSelector, Blobs.from("metadataBlob")); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWriteEntry.getLayerBlob())); - Assert.assertEquals(mockSelector, cacheWriteEntry.getSelector().orElse(null)); + Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); + Assert.assertEquals(mockSelector, cacheWrite.getSelector().orElse(null)); Assert.assertEquals( - "metadataBlob", Blobs.writeToString(cacheWriteEntry.getMetadataBlob().orElse(null))); + "metadataBlob", Blobs.writeToString(cacheWrite.getMetadataBlob().orElse(null))); } } From f67186e4e30ff5b82bddcde2699f36ebfbbc84b4 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 29 Aug 2018 11:04:37 -0400 Subject: [PATCH 0160/2020] Adds entries for all directories in layer tarball. (#891) --- .../jib/image/ReproducibleLayerBuilder.java | 99 ++++++++++++++----- .../image/ReproducibleLayerBuilderTest.java | 97 ++++++++++++------ jib-gradle-plugin/CHANGELOG.md | 2 + jib-maven-plugin/CHANGELOG.md | 2 + 4 files changed, 147 insertions(+), 53 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index dd0fc73469..1a07b515f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -18,13 +18,19 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.tar.TarStreamBuilder; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** @@ -34,41 +40,80 @@ */ public class ReproducibleLayerBuilder { + /** + * Holds a list of {@link TarArchiveEntry}s with unique extraction paths. The list also includes + * all parent directories for each extraction path. + */ + private static class UniqueTarArchiveEntries { + + /** + * Uses the current directory to act as the file input to TarArchiveEntry (since all directories + * are treated the same in {@link TarArchiveEntry#TarArchiveEntry(File, String)}, except for + * modification time, which is wiped away in {@link #build}). + */ + private static final File DIRECTORY_FILE = Paths.get(".").toFile(); + + private final List entries = new ArrayList<>(); + private final Set names = new HashSet<>(); + + /** + * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of + * the parent directories on the extraction path. + * + * @param tarArchiveEntry the {@link TarArchiveEntry} + */ + private void add(TarArchiveEntry tarArchiveEntry) { + if (names.contains(tarArchiveEntry.getName())) { + return; + } + + // Adds all directories along extraction paths to explicitly set permissions for those + // directories. + Path namePath = Paths.get(tarArchiveEntry.getName()); + if (namePath.getParent() != namePath.getRoot()) { + add(new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString())); + } + + entries.add(tarArchiveEntry); + names.add(tarArchiveEntry.getName()); + } + + private List getSortedEntries() { + List sortedEntries = new ArrayList<>(entries); + sortedEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + return sortedEntries; + } + } + /** * Builds the {@link TarArchiveEntry}s for adding this {@link LayerEntry} to a tarball archive. * * @return the list of {@link TarArchiveEntry} * @throws IOException if walking a source file that is a directory failed */ - private static List buildAsTarArchiveEntries(LayerEntry layerEntry) - throws IOException { + @VisibleForTesting + static List buildAsTarArchiveEntries(LayerEntry layerEntry) throws IOException { List tarArchiveEntries = new ArrayList<>(); + // Adds the files to extract relative to the extraction path. for (Path sourceFile : layerEntry.getSourceFiles()) { if (Files.isDirectory(sourceFile)) { new DirectoryWalker(sourceFile) .filterRoot() + .filter(path -> !Files.isDirectory(path)) .walk( path -> { - /* - * Builds the same file path as in the source file for extraction. The iteration - * is necessary because the path needs to be in Unix-style. - */ - StringBuilder subExtractionPath = - new StringBuilder(layerEntry.getExtractionPath()); Path sourceFileRelativePath = sourceFile.getParent().relativize(path); - for (Path sourceFileRelativePathComponent : sourceFileRelativePath) { - subExtractionPath.append('/').append(sourceFileRelativePathComponent); - } + Path extractionPath = + Paths.get(layerEntry.getExtractionPath()).resolve(sourceFileRelativePath); tarArchiveEntries.add( - new TarArchiveEntry(path.toFile(), subExtractionPath.toString())); + new TarArchiveEntry(path.toFile(), extractionPath.toString())); }); - } else { + Path extractionPath = + Paths.get(layerEntry.getExtractionPath()).resolve(sourceFile.getFileName()); TarArchiveEntry tarArchiveEntry = - new TarArchiveEntry( - sourceFile.toFile(), - layerEntry.getExtractionPath() + "/" + sourceFile.getFileName()); + new TarArchiveEntry(sourceFile.toFile(), extractionPath.toString()); tarArchiveEntries.add(tarArchiveEntry); } } @@ -101,19 +146,26 @@ public ReproducibleLayerBuilder addFiles(List sourceFiles, String extracti * @throws IOException if walking the source files fails */ public UnwrittenLayer build() throws IOException { - List filesystemEntries = new ArrayList<>(); + UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. - for (LayerEntry layerEntry : layerEntries.build()) { - filesystemEntries.addAll(buildAsTarArchiveEntries(layerEntry)); + List layerEntries = this.layerEntries.build(); + for (LayerEntry layerEntry : layerEntries) { + // Converts layerEntry to list of TarArchiveEntrys. + List tarArchiveEntries = buildAsTarArchiveEntries(layerEntry); + // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and + // adds parent directories for each extraction path. + tarArchiveEntries.forEach(uniqueTarArchiveEntries::add); } - // Sorts the entries by name. - filesystemEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); + // Gets the entries sorted by extraction path. + List sortedFilesystemEntries = uniqueTarArchiveEntries.getSortedEntries(); + + Set names = new HashSet<>(); // Adds all the files to a tar stream. TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - for (TarArchiveEntry entry : filesystemEntries) { + for (TarArchiveEntry entry : sortedFilesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. entry.setModTime(0); entry.setGroupId(0); @@ -121,6 +173,9 @@ public UnwrittenLayer build() throws IOException { entry.setUserName(""); entry.setGroupName(""); + Verify.verify(!names.contains(entry.getName())); + names.add(entry.getName()); + tarStreamBuilder.addTarArchiveEntry(entry); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 3d433ce5ac..f9a84496c4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; @@ -35,6 +34,7 @@ import java.nio.file.attribute.FileTime; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.hamcrest.CoreMatchers; @@ -72,8 +72,53 @@ private static void verifyNextTarArchiveEntry( Assert.assertEquals(expectedString, extractedString); } + /** + * Verifies that the next {@link TarArchiveEntry} in the {@link TarArchiveInputStream} is a + * directory with correct permissions. + * + * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from + * @param expectedExtractionPath the expected extraction path of the next entry + * @throws IOException if an I/O exception occurs + */ + private static void verifyNextTarArchiveEntryIsDirectory( + TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath) + throws IOException { + TarArchiveEntry extractionPathEntry = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals(expectedExtractionPath, extractionPathEntry.getName()); + Assert.assertTrue(extractionPathEntry.isDirectory()); + Assert.assertEquals(TarArchiveEntry.DEFAULT_DIR_MODE, extractionPathEntry.getMode()); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void testBuildAsTarArchiveEntries() throws URISyntaxException, IOException { + Path testDirectory = Paths.get(Resources.getResource("layer").toURI()); + Path testFile = Paths.get(Resources.getResource("fileA").toURI()); + + List tarArchiveEntries = + ReproducibleLayerBuilder.buildAsTarArchiveEntries( + new LayerEntry(ImmutableList.of(testDirectory, testFile), "/app/")); + + List expectedTarArchiveEntries = + ImmutableList.of( + new TarArchiveEntry( + testDirectory.resolve("a").resolve("b").resolve("bar").toFile(), + "/app/layer/a/b/bar"), + new TarArchiveEntry( + testDirectory.resolve("c").resolve("cat").toFile(), "/app/layer/c/cat"), + new TarArchiveEntry(testDirectory.resolve("foo").toFile(), "/app/layer/foo"), + new TarArchiveEntry(testFile.toFile(), "/app/fileA")); + + Assert.assertEquals(expectedTarArchiveEntries.size(), tarArchiveEntries.size()); + for (int entryIndex = 0; entryIndex < expectedTarArchiveEntries.size(); entryIndex++) { + TarArchiveEntry expectedTarArchiveEntry = expectedTarArchiveEntries.get(entryIndex); + TarArchiveEntry tarArchiveEntry = tarArchiveEntries.get(entryIndex); + Assert.assertEquals(expectedTarArchiveEntry.getFile(), tarArchiveEntry.getFile()); + Assert.assertEquals(expectedTarArchiveEntry.getName(), tarArchiveEntry.getName()); + } + } + @Test public void testBuild() throws URISyntaxException, IOException { Path layerDirectory = Paths.get(Resources.getResource("layer").toURI()); @@ -95,37 +140,27 @@ public void testBuild() throws URISyntaxException, IOException { // Reads the file back. try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(Files.newInputStream(temporaryFile))) { - // Verifies that blobA was added. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); - - // Verifies that all the files have been added to the tarball stream. - ImmutableList layerDirectoryPaths = - new DirectoryWalker(layerDirectory).filter(path -> !path.equals(layerDirectory)).walk(); - for (Path path : layerDirectoryPaths) { - TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); - - StringBuilder expectedExtractionPath = new StringBuilder("extract/here/apple"); - for (Path pathComponent : layerDirectory.getParent().relativize(path)) { - expectedExtractionPath.append("/").append(pathComponent); - } - // Check path-equality because there might be an appended backslash in the header - // filename. - Assert.assertEquals( - Paths.get(expectedExtractionPath.toString()), Paths.get(header.getName())); - - // If is a normal file, checks that the file contents match. - if (Files.isRegularFile(path)) { - String expectedFileString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); - - String extractedFileString = - CharStreams.toString( - new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - - Assert.assertEquals(expectedFileString, extractedFileString); - } - } - - // Verifies that blobA was added to the other location. + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/"); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/b/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/a/b/bar", + Paths.get(Resources.getResource("layer/a/b/bar").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/c/cat", + Paths.get(Resources.getResource("layer/c/cat").toURI())); + verifyNextTarArchiveEntry( + tarArchiveInputStream, + "extract/here/apple/layer/foo", + Paths.get(Resources.getResource("layer/foo").toURI())); + verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/banana/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/banana/blobA", blobA); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1552a14679..0eacebaa1d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + ## 0.9.9 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 01c2e7980a..ccebb0341c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) + ## 0.9.9 ### Added From b7cf2def6db8cfe75cab4ae3508e9d667260fde4 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 30 Aug 2018 00:20:53 +0900 Subject: [PATCH 0161/2020] Make sure to add assemble task dependencies after base plugin evaluation. (#880) --- .../jib/gradle/JibPluginIntegrationTest.java | 19 ++++++++ .../multiproject/a_packaged/build.gradle | 21 +++++++++ .../src/main/java/com/test/Empty.java | 6 +++ .../multiproject/b_dependency/build.gradle | 1 + .../src/main/java/com/test/Empty.java | 6 +++ .../projects/multiproject/build.gradle | 1 + .../projects/multiproject/settings.gradle | 2 + .../cloud/tools/jib/gradle/JibPlugin.java | 47 +++++++++++-------- .../cloud/tools/jib/gradle/JibPluginTest.java | 8 ++-- 9 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index a1cd24811b..068624aa33 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -47,6 +47,9 @@ public class JibPluginIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + @ClassRule + public static final TestProject multiprojectTestProject = new TestProject("multiproject"); + @ClassRule public static final TestProject defaultTargetTestProject = new TestProject("default-target"); @@ -350,4 +353,20 @@ public void testDockerContext() throws IOException, InterruptedException { "Export Docker context failed because cannot clear directory")); } } + + @Test + public void testMultiProject() { + BuildResult buildResult = + multiprojectTestProject.build( + "clean", ":a_packaged:" + JibPlugin.DOCKER_CONTEXT_TASK_NAME, "--info"); + + BuildTask classesTask = buildResult.task(":a_packaged:classes"); + BuildTask jibTask = buildResult.task(":a_packaged:" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); + } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle new file mode 100644 index 0000000000..a96169ea32 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'com.google.cloud.tools.jib' + id 'java' +} + +dependencies { + compile project(':b_dependency') +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java new file mode 100644 index 0000000000..f5940d0dab --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java @@ -0,0 +1,6 @@ +package com.test; + +public class Empty { + + public static void main(String[] args) {} +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle new file mode 100644 index 0000000000..9900fd60df --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle @@ -0,0 +1 @@ +apply plugin: 'java-library' diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java new file mode 100644 index 0000000000..f5940d0dab --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java @@ -0,0 +1,6 @@ +package com.test; + +public class Empty { + + public static void main(String[] args) {} +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle new file mode 100644 index 0000000000..33eec26740 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle @@ -0,0 +1 @@ +apply plugin: 'base' diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle new file mode 100644 index 0000000000..ea19453ad3 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle @@ -0,0 +1,2 @@ +include ':a_packaged' +include ':b_dependency' diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index c22ea8199a..93d2cef04a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -41,16 +41,16 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; /** - * Collects all assemble tasks for project dependencies of the style "compile project(':mylib')" - * for any kind of configuration [compile, runtime, etc]. It potentially will collect common test - * libraries in configs like [test, integrationTest, etc], but it's either that or filter based on - * a configuration containing the word "test" which feels dangerous. + * Collects all project dependencies of the style "compile project(':mylib')" for any kind of + * configuration [compile, runtime, etc]. It potentially will collect common test libraries in + * configs like [test, integrationTest, etc], but it's either that or filter based on a + * configuration containing the word "test" which feels dangerous. * * @param project this project we are containerizing - * @return a list of "assemble" tasks associated with projects that this project depends on. + * @return a list of projects that this project depends on. */ @VisibleForTesting - static List getProjectDependencyAssembleTasks(Project project) { + static List getProjectDependencies(Project project) { return project .getConfigurations() .stream() @@ -59,7 +59,6 @@ static List getProjectDependencyAssembleTasks(Project project) { .filter(ProjectDependency.class::isInstance) .map(ProjectDependency.class::cast) .map(ProjectDependency::getDependencyProject) - .map(subProject -> subProject.getTasks().getByPath(BasePlugin.ASSEMBLE_TASK_NAME)) .collect(Collectors.toList()); } @@ -107,21 +106,31 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { try { - // Find project dependencies - List computedDependencies = - getProjectDependencyAssembleTasks(projectAfterEvaluation); // Has all tasks depend on the 'classes' task. Task classesTask = projectAfterEvaluation.getTasks().getByPath("classes"); - computedDependencies.add(classesTask); - - // dependsOn takes an Object... type - Object[] dependenciesArray = computedDependencies.toArray(); - - buildImageTask.dependsOn(dependenciesArray); - dockerContextTask.dependsOn(dependenciesArray); - buildDockerTask.dependsOn(dependenciesArray); - buildTarTask.dependsOn(dependenciesArray); + buildImageTask.dependsOn(classesTask); + dockerContextTask.dependsOn(classesTask); + buildDockerTask.dependsOn(classesTask); + buildTarTask.dependsOn(classesTask); + // Find project dependencies and add a dependency to their assemble task. We make sure + // to only add the dependency after BasePlugin is evaluated as otherwise the assemble + // task may not be available yet. + List computedDependencies = getProjectDependencies(projectAfterEvaluation); + for (Project dependencyProject : computedDependencies) { + dependencyProject + .getPlugins() + .withType( + BasePlugin.class, + unused -> { + Task assembleTask = + dependencyProject.getTasks().getByPath(BasePlugin.ASSEMBLE_TASK_NAME); + buildImageTask.dependsOn(assembleTask); + dockerContextTask.dependsOn(assembleTask); + buildDockerTask.dependsOn(assembleTask); + buildTarTask.dependsOn(assembleTask); + }); + } } catch (UnknownTaskException ex) { throw new GradleException( "Could not find task 'classes' on project " diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 4e49a0aaa1..72e9e8aa96 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -85,7 +85,7 @@ public void testCheckGradleVersion_fail() throws IOException { } @Test - public void testGetProjectDependencyAssembleTasks() { + public void testProjectDependencyAssembleTasksAreRun() { // root project is our jib packaged service Project rootProject = ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); @@ -118,10 +118,10 @@ public void testGetProjectDependencyAssembleTasks() { // programmatic check Assert.assertEquals( - Collections.singletonList(":sub:assemble"), - JibPlugin.getProjectDependencyAssembleTasks(rootProject) + Collections.singletonList(":sub"), + JibPlugin.getProjectDependencies(rootProject) .stream() - .map(Task::getPath) + .map(Project::getPath) .collect(Collectors.toList())); // check by applying the jib plugin and inspect the task dependencies From 49fae437583ff0147664de965647a0cea5815d14 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 29 Aug 2018 11:28:31 -0400 Subject: [PATCH 0162/2020] Note classpath difference with Gradle `run` task (#886) --- jib-gradle-plugin/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 0eacebaa1d..99c0f49d2b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -22,7 +22,8 @@ All notable changes to this project will be documented in this file. ### Changed -- Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) +- Reordered classpath in entrypoint to use _resources_, _classes_, and then _dependencies_, to allow dependency patching + ([#777](https://github.com/GoogleContainerTools/jib/issues/777)). Note that this classpath ordering differs from that used by Gradle's `run` task. - Changed logging level of missing build output directory message ([#677](https://github.com/GoogleContainerTools/jib/issues/677)) ### Fixed From 17862eb6933e4e8633caced98d327bccc11495c2 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 29 Aug 2018 11:39:36 -0400 Subject: [PATCH 0163/2020] Enhance build.sh (#887) - add `it` target to run integration tests - make `-e` for gradle use `--info --stacktrace` to see test errors --- build.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 0fb6579626..eca20c54b6 100755 --- a/build.sh +++ b/build.sh @@ -9,9 +9,10 @@ usage() { eval 1>&2 echo "Simple builder for Jib for jib-core, jib-plugins-common, jib-maven-plugin, and jib-gradle-plugin" - echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | all]" + echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | all | it]" + echo " 'all' is the same as 'core plugins gradle maven'" echo " -q quick mode: skip tests, formatting" - echo " -e show error information (mvn: -e, gradle: --stacktrace)" + echo " -e show error information (mvn: -e, gradle: --stacktrace --info)" exit 1 } @@ -26,7 +27,7 @@ while getopts qe c; do case $c in q) quickMode=true;; e) mavenOptions="$mavenOptions -e" - gradleOptions="$gradleOptions --stacktrace" + gradleOptions="$gradleOptions --stacktrace --info" ;; \?) usage;; esac @@ -84,5 +85,10 @@ for target in "$@"; do fi ;; + it) + doBuild jib-core ./gradlew $gradleOptions integrationTest + doBuild jib-maven-plugin ./mvnw $mavenOptions -Pintegration-tests verify -U + doBuild jib-gradle-plugin ./gradlew $gradleOptions integrationTest + ;; esac done From 69bdcc976d6dbdecd0584c80f0b4d3b3988cfe83 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 29 Aug 2018 11:48:28 -0400 Subject: [PATCH 0164/2020] Adds FAQ about using debug base image. (#897) --- docs/faq.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 46f724d657..b1f78869a7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -6,6 +6,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [But, I'm not a Java developer.](#but-im-not-a-java-developer)\ [How do I run the image I built?](#how-do-i-run-the-image-i-built)\ +[Where is bash?](#where-is-bash)\ [How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint)\ @@ -41,6 +42,42 @@ kubectl run jib-deployment --image= For more information, see [steps 4-6 of the Kubernetes Engine deployment tutorial](https://cloud.google.com/kubernetes-engine/docs/tutorials/hello-app#step_4_create_a_container_cluster). +#### Where is bash? + +By default, Jib uses [`distroless/java`](https://github.com/GoogleContainerTools/distroless/tree/master/java) as the base image. Distroless images contain only runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Check out the [distroless project](https://github.com/GoogleContainerTools/distroless#distroless-docker-images) for more information about distroless images. + +If you would like to include a shell for debugging, set the base image to `gcr.io/distroless/java:debug` instead. The shell will be located at `/busybox/sh`. Note that `:debug` images are **not** recommended for production use. + +

+Configuring a base image in Maven +

+ +In [`jib-maven-plugin`](../jib-maven-plugin), you can use the `gcr.io/distroless/java:debug` base image by adding the following configuration: + +```xml + + + gcr.io/distroless/java:debug + + +``` +

+
+ +
+Configuring a base image in Gradle +

+ +In [`jib-gradle-plugin`](../jib-gradle-plugin), you can use the `gcr.io/distroless/java:debug` base image by adding the following configuration: + +```groovy +jib.from.image = 'gcr.io/distroless/java:debug' +``` +

+

+ +You can then run the image in shell form with Docker: `docker run -it --entrypoint /busybox/sh ` + ### How do I set parameters for my image at runtime? #### JVM Flags From fb292241ae0f4298605c5cb8eb660c371b6ad58e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 29 Aug 2018 14:14:59 -0400 Subject: [PATCH 0165/2020] Make createdBy in history configurable (#899) --- .../builder/BuildStepsIntegrationTest.java | 5 ++-- .../jib/builder/steps/BuildImageStep.java | 2 +- .../jib/configuration/BuildConfiguration.java | 25 +++++++++++++++++-- .../tools/jib/image/json/HistoryEntry.java | 5 ++++ .../jib/builder/steps/BuildImageStepTest.java | 1 + .../configuration/BuildConfigurationTest.java | 6 ++++- .../jib/gradle/JibPluginIntegrationTest.java | 6 +++++ .../gradle/PluginConfigurationProcessor.java | 1 + .../maven/PluginConfigurationProcessor.java | 1 + .../maven/BuildImageMojoIntegrationTest.java | 2 ++ 10 files changed, 48 insertions(+), 6 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 3ff8f6d9ae..a3324416c3 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -114,7 +114,7 @@ public void testSteps_forBuildToDockerRegistry() + " \"2002/tcp\": {},\n" + " \"3000/udp\": {}")); String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib")); + Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); @@ -170,7 +170,7 @@ public void testSteps_forBuildToDockerDaemon() + " \"key2\": \"value2\"\n" + " }")); String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib")); + Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); @@ -223,6 +223,7 @@ private BuildConfiguration getBuildConfiguration( .setContainerConfiguration(containerConfiguration) .setAllowInsecureRegistries(true) .setLayerConfigurations(fakeLayerConfigurations) + .setCreatedBy("jib-integration-test") .build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index e0785ef516..b1b1b58248 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -137,7 +137,7 @@ private Image afterCachedLayersSteps() HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") - .setCreatedBy("jib") + .setCreatedBy(buildConfiguration.getCreatedBy()) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index f5ad1f866f..129156b600 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -38,6 +38,7 @@ public static class Builder { private boolean allowInsecureRegistries = false; private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = V22ManifestTemplate.class; + private String createdBy = "jib"; private JibLogger buildLogger; @@ -135,6 +136,18 @@ public Builder setLayerConfigurations(List layerConfiguratio return this; } + /** + * Sets the command that created the image layers (for the "Created By" field in the container's + * history). + * + * @param createdBy the field value + * @return this + */ + public Builder setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + /** * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. * @@ -171,7 +184,8 @@ public BuildConfiguration build() { baseImageLayersCacheConfiguration, targetFormat, allowInsecureRegistries, - layerConfigurations); + layerConfigurations, + createdBy); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -199,6 +213,7 @@ public static Builder builder(JibLogger buildLogger) { private Class targetFormat; private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; + private final String createdBy; /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( @@ -210,7 +225,8 @@ private BuildConfiguration( @Nullable CacheConfiguration baseImageLayersCacheConfiguration, Class targetFormat, boolean allowInsecureRegistries, - ImmutableList layerConfigurations) { + ImmutableList layerConfigurations, + String createdBy) { this.buildLogger = buildLogger; this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; @@ -220,6 +236,7 @@ private BuildConfiguration( this.targetFormat = targetFormat; this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; + this.createdBy = createdBy; } public JibLogger getBuildLogger() { @@ -243,6 +260,10 @@ public Class getTargetFormat() { return targetFormat; } + public String getCreatedBy() { + return createdBy; + } + /** * Gets the location of the cache for storing application layers. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java index 2ff9e9bea1..267c5c7bee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java @@ -160,4 +160,9 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(author, creationTimestamp, createdBy, comment, emptyLayer); } + + @Override + public String toString() { + return createdBy == null ? "" : createdBy; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index c94b16c888..04e3705fc2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -73,6 +73,7 @@ public void setUp() throws DigestException { Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); + Mockito.when(mockBuildConfiguration.getCreatedBy()).thenReturn("jib"); Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index f82652c2c4..8995921957 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -64,6 +64,7 @@ public void testBuilder() throws Exception { List expectedLayerConfigurations = Collections.singletonList( LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build()); + String expectedCreatedBy = "createdBy"; ImageConfiguration baseImageConfiguration = ImageConfiguration.builder( @@ -94,7 +95,8 @@ public void testBuilder() throws Exception { .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) .setTargetFormat(OCIManifestTemplate.class) .setAllowInsecureRegistries(true) - .setLayerConfigurations(expectedLayerConfigurations); + .setLayerConfigurations(expectedLayerConfigurations) + .setCreatedBy(expectedCreatedBy); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -141,6 +143,7 @@ public void testBuilder() throws Exception { Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); Assert.assertEquals( expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals(expectedCreatedBy, buildConfiguration.getCreatedBy()); } @Test @@ -176,6 +179,7 @@ public void testBuilder_default() { Assert.assertEquals(buildConfiguration.getTargetFormat(), V22ManifestTemplate.class); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); + Assert.assertEquals("jib", buildConfiguration.getCreatedBy()); } @Test diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 068624aa33..1c507ed355 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -63,6 +63,8 @@ private static String buildAndRun(TestProject testProject, String imageReference new Command("docker", "pull", imageReference).run(); assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); return new Command("docker", "run", imageReference).run(); } @@ -84,6 +86,8 @@ private static String buildAndRunComplex( targetRegistry.pull(imageReference); assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); return new Command("docker", "run", imageReference).run(); } @@ -97,6 +101,8 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); return new Command("docker", "run", imageReference).run(); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index c4a275af34..4f15919e19 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -102,6 +102,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) + .setCreatedBy("jib-gradle-plugin") .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index d54c6cc190..478d9deeba 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -120,6 +120,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) + .setCreatedBy("jib-maven-plugin") .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index c8aef461c4..214ece9d98 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -144,6 +144,8 @@ private static void assertDockerInspectParameters(String imageReference) + " \"key1\": \"value1\",\n" + " \"key2\": \"value2\"\n" + " }")); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-maven-plugin")); } private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOException { From 9bfff0ea004d4471c4425a66a09be63da12cdfd5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 30 Aug 2018 09:48:58 -0400 Subject: [PATCH 0166/2020] Propagate WorkingDir in container configuration (#902) * Propagate WorkingDir in container configuration * No need for null check * Update CHANGELOG files * Fix and add Javadocs --- .../jib/builder/steps/BuildImageStep.java | 1 + .../google/cloud/tools/jib/image/Image.java | 35 +++++++++++++++---- .../json/ContainerConfigurationTemplate.java | 19 ++++++++-- .../jib/image/json/ImageToJsonTranslator.java | 3 ++ .../jib/image/json/JsonToImageTranslator.java | 2 ++ .../jib/builder/steps/BuildImageStepTest.java | 2 ++ .../ContainerConfigurationTemplateTest.java | 2 ++ .../image/json/ImageToJsonTranslatorTest.java | 1 + .../image/json/JsonToImageTranslatorTest.java | 1 + .../test/resources/json/containerconfig.json | 2 +- .../json/translated_ocimanifest.json | 2 +- .../json/translated_v22manifest.json | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 14 files changed, 62 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index b1b1b58248..9b8d4b292a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -117,6 +117,7 @@ private Image afterCachedLayersSteps() } imageBuilder.addEnvironment(baseImage.getEnvironment()); imageBuilder.addLabels(baseImage.getLabels()); + imageBuilder.setWorkingDirectory(baseImage.getWorkingDirectory()); // Add history elements for non-empty layers that don't have one yet Instant layerCreationTime = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 3e9478494b..140965fde6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -40,6 +40,7 @@ public static class Builder { @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList javaArguments; @Nullable private ImmutableList exposedPorts; + @Nullable private String workingDirectory; /** * Sets the image creation time. @@ -111,9 +112,9 @@ public Builder setExposedPorts(@Nullable List exposedPorts) { } /** - * Add items to the "Labels" field in the container configuration. + * Adds items to the "Labels" field in the container configuration. * - * @param labels that map of labels to add + * @param labels the map of labels to add * @return this */ public Builder addLabels(@Nullable Map labels) { @@ -124,9 +125,9 @@ public Builder addLabels(@Nullable Map labels) { } /** - * A an item to the "Labels" field in the container configuration. + * Adds an item to the "Labels" field in the container configuration. * - * @param name that name of the label + * @param name the name of the label * @param value the value of the label * @return this */ @@ -135,6 +136,17 @@ public Builder addLabel(String name, String value) { return this; } + /** + * Sets the item in the "WorkingDir" field in the container configuration. + * + * @param workingDirectory the working directory + * @return this + */ + public Builder setWorkingDirectory(@Nullable String workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + /** * Adds a layer to the image. * @@ -167,7 +179,8 @@ public Image build() { entrypoint, javaArguments, exposedPorts, - labelsBuilder.build()); + labelsBuilder.build(), + workingDirectory); } } @@ -199,6 +212,9 @@ public static Builder builder() { /** Labels on the container configuration */ @Nullable private final ImmutableMap labels; + /** Working directory on the container configuration */ + @Nullable private final String workingDirectory; + private Image( @Nullable Instant created, ImageLayers layers, @@ -207,7 +223,8 @@ private Image( @Nullable ImmutableList entrypoint, @Nullable ImmutableList javaArguments, @Nullable ImmutableList exposedPorts, - @Nullable ImmutableMap labels) { + @Nullable ImmutableMap labels, + @Nullable String workingDirectory) { this.created = created; this.layers = layers; this.history = history; @@ -216,6 +233,7 @@ private Image( this.javaArguments = javaArguments; this.exposedPorts = exposedPorts; this.labels = labels; + this.workingDirectory = workingDirectory; } @Nullable @@ -248,6 +266,11 @@ public ImmutableMap getLabels() { return labels; } + @Nullable + public String getWorkingDirectory() { + return workingDirectory; + } + public ImmutableList getLayers() { return layers.getLayers(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 3aaaf32775..649061e03b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -38,9 +38,10 @@ * "config": { * "Env": ["/usr/bin/java"], * "Entrypoint": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], - * "Cmd": ["arg1", "arg2"] - * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} } - * "Labels": { "com.example.label": "value" } + * "Cmd": ["arg1", "arg2"], + * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} }, + * "Labels": { "com.example.label": "value" }, + * "WorkingDir": "/home/user/workspace" * }, * "history": [ * { @@ -106,6 +107,9 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { /** Labels. */ @Nullable private Map Labels; + + /** Working directory. */ + @Nullable private String WorkingDir; } /** @@ -148,6 +152,10 @@ public void setContainerLabels(@Nullable Map labels) { config.Labels = labels; } + public void setContainerWorkingDir(@Nullable String workingDirectory) { + config.WorkingDir = workingDirectory; + } + public void addLayerDiffId(DescriptorDigest diffId) { rootfs.diff_ids.add(diffId); } @@ -194,6 +202,11 @@ Map getContainerLabels() { return config.Labels; } + @Nullable + String getContainerWorkingDir() { + return config.WorkingDir; + } + @VisibleForTesting DescriptorDigest getLayerDiffId(int index) { return rootfs.diff_ids.get(index); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 6fb575be9b..1795e4cda8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -138,6 +138,9 @@ public Blob getContainerConfigurationBlob() { // Sets the labels. template.setContainerLabels(image.getLabels()); + // Sets the working directory. + template.setContainerWorkingDir(image.getWorkingDirectory()); + // Serializes into JSON. return JsonTemplateMapper.toBlob(template); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 37c3367408..1ac0abd815 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -162,6 +162,8 @@ public static Image toImage( } } + imageBuilder.setWorkingDirectory(containerConfigurationTemplate.getContainerWorkingDir()); + return imageBuilder.build(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 04e3705fc2..4b1461f191 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -98,6 +98,7 @@ public void setUp() throws DigestException { Image.builder() .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE")) .addLabel("base.label", "base.label.value") + .setWorkingDirectory("/base/working/directory") .addHistory(nonEmptyLayerHistory) .addHistory(emptyLayerHistory) .addHistory(emptyLayerHistory) @@ -160,6 +161,7 @@ public void test_propagateBaseImageConfiguration() Assert.assertEquals( ImmutableMap.of("base.label", "base.label.value", "my.label", "my.label.value"), image.getLabels()); + Assert.assertEquals("/base/working/directory", image.getWorkingDirectory()); Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 87562783c1..d25654429c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -60,6 +60,7 @@ public void testToJson() throws IOException, URISyntaxException, DigestException "3000/udp", ImmutableMap.of())); containerConfigJson.setContainerLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); + containerConfigJson.setContainerWorkingDir("/some/workspace"); containerConfigJson.addLayerDiffId( DescriptorDigest.fromDigest( @@ -104,6 +105,7 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti Assert.assertEquals( ImmutableMap.of("key1", "value1", "key2", "value2"), containerConfigJson.getContainerLabels()); + Assert.assertEquals("/some/workspace", containerConfigJson.getContainerWorkingDir()); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index ee92dac87c..726cdd53b8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -66,6 +66,7 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { new Port(2000, Protocol.TCP), new Port(3000, Protocol.UDP))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); + testImageBuilder.setWorkingDirectory("/some/workspace"); DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 4a1b31adca..4b0dd995f6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -185,6 +185,7 @@ private void testToImage_buildable( Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated()); Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint()); Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); + Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); Assert.assertEquals( ImmutableList.of( new Port(1000, Protocol.TCP), diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index d0ffb8e451..4cb079a8f3 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace"},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/json/translated_ocimanifest.json index 3c7daf77af..5403c875db 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5ee46ef07ac9a0b4d5bc5d2733e524a9fd9a3cd9bbe88c75276fbd03f01bc709","size":579},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:64b29673c04ae315eb40e66ac8b0898c62ae6e75a09a45c1b89b7ab6adfba8e1","size":610},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index d01327ec73..a817387134 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:5ee46ef07ac9a0b4d5bc5d2733e524a9fd9a3cd9bbe88c75276fbd03f01bc709","size":579},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:64b29673c04ae315eb40e66ac8b0898c62ae6e75a09a45c1b89b7ab6adfba8e1","size":610},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 99c0f49d2b..6f22272723 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ccebb0341c..7597102e39 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) ### Changed From b907abeafa14891d7c3f4ecfd1398e78b81b485d Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 30 Aug 2018 11:07:28 -0400 Subject: [PATCH 0167/2020] Some renaming and minor fixes (#904) --- .../jib/frontend/JavaEntrypointConstructor.java | 14 +++++++------- .../jib/frontend/JavaLayerConfigurations.java | 2 +- .../com/google/cloud/tools/jib/image/Image.java | 8 ++++---- .../jib/image/json/JsonToImageTranslator.java | 2 +- .../google/cloud/tools/jib/image/ImageTest.java | 4 ++-- .../jib/image/json/ImageToJsonTranslatorTest.java | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index dc4d677e30..fdafd8d958 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -51,13 +51,13 @@ public static List makeEntrypoint( List classpathElements, List jvmFlags, String mainClass) { String classpathString = String.join(":", classpathElements); - List entrypointBuilder = new ArrayList<>(4 + jvmFlags.size()); - entrypointBuilder.add("java"); - entrypointBuilder.addAll(jvmFlags); - entrypointBuilder.add("-cp"); - entrypointBuilder.add(classpathString); - entrypointBuilder.add(mainClass); - return entrypointBuilder; + List entrypoint = new ArrayList<>(4 + jvmFlags.size()); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(classpathString); + entrypoint.add(mainClass); + return entrypoint; } private JavaEntrypointConstructor() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 8a2b9e0d16..5bcafc7023 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -64,7 +64,7 @@ String getExtractionPath() { /** Builds with each layer's files. */ public static class Builder { - private Map> layerFilesMap = new EnumMap<>(LayerType.class); + private final Map> layerFilesMap = new EnumMap<>(LayerType.class); private Builder() { for (LayerType layerType : LayerType.values()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 140965fde6..75aae81807 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -32,9 +32,9 @@ public class Image { public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); - private ImmutableList.Builder historyBuilder = ImmutableList.builder(); - private ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); - private ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); + private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); + private final ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); + private final ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @@ -73,7 +73,7 @@ public Builder addEnvironment(@Nullable Map environment) { * @param value the value to set it to * @return this */ - public Builder setEnvironmentVariable(String name, String value) { + public Builder addEnvironmentVariable(String name, String value) { environmentBuilder.put(name, value); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 1ac0abd815..fd8c7ec8f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -158,7 +158,7 @@ public static Image toImage( throw new BadContainerConfigurationFormatException( "Invalid environment variable definition: " + environmentVariable); } - imageBuilder.setEnvironmentVariable(matcher.group("name"), matcher.group("value")); + imageBuilder.addEnvironmentVariable(matcher.group("name"), matcher.group("value")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index f21fe3396b..6710b13ad8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -49,8 +49,8 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Image image = Image.builder() .setCreated(Instant.ofEpochSecond(10000)) - .setEnvironmentVariable("crepecake", "is great") - .setEnvironmentVariable("VARIABLE", "VALUE") + .addEnvironmentVariable("crepecake", "is great") + .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) .setJavaArguments(Arrays.asList("arg1", "arg2")) .setExposedPorts( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 726cdd53b8..05a87808dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -56,8 +56,8 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { Image.Builder testImageBuilder = Image.builder(); testImageBuilder.setCreated(Instant.ofEpochSecond(20)); - testImageBuilder.setEnvironmentVariable("VAR1", "VAL1"); - testImageBuilder.setEnvironmentVariable("VAR2", "VAL2"); + testImageBuilder.addEnvironmentVariable("VAR1", "VAL1"); + testImageBuilder.addEnvironmentVariable("VAR2", "VAL2"); testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); testImageBuilder.setJavaArguments(Arrays.asList("arg1", "arg2")); testImageBuilder.setExposedPorts( From 02f7f41874223e1e6acf2a40648b5b3695877397 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 30 Aug 2018 11:37:42 -0400 Subject: [PATCH 0168/2020] Adds micronaut example. (#893) --- examples/helloworld/README.md | 2 + examples/micronaut/README.md | 54 ++++++ examples/micronaut/build.gradle | 45 +++++ .../micronaut/dockerize-micronaut-jib.gif | Bin 0 -> 268744 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/micronaut/gradlew | 172 ++++++++++++++++++ examples/micronaut/gradlew.bat | 84 +++++++++ examples/micronaut/settings.gradle | 1 + .../example/micronaut/HelloController.groovy | 14 ++ .../java/example/micronaut/Application.java | 10 + .../src/main/resources/application.yml | 5 + .../micronaut/src/main/resources/logback.xml | 14 ++ .../micronaut/HelloControllerSpec.groovy | 31 ++++ examples/multi-module/README.md | 4 +- examples/spring-boot-draft/README.md | 4 +- 16 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 examples/micronaut/README.md create mode 100644 examples/micronaut/build.gradle create mode 100644 examples/micronaut/dockerize-micronaut-jib.gif create mode 100644 examples/micronaut/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/micronaut/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/micronaut/gradlew create mode 100644 examples/micronaut/gradlew.bat create mode 100644 examples/micronaut/settings.gradle create mode 100644 examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy create mode 100644 examples/micronaut/src/main/java/example/micronaut/Application.java create mode 100644 examples/micronaut/src/main/resources/application.yml create mode 100644 examples/micronaut/src/main/resources/logback.xml create mode 100644 examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md index 769b60c2d7..657775fc65 100644 --- a/examples/helloworld/README.md +++ b/examples/helloworld/README.md @@ -5,3 +5,5 @@ To build the image: 1. In `pom.xml`, replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project. 1. Run `mvn compile jib:build`. + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/helloworld)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/micronaut/README.md b/examples/micronaut/README.md new file mode 100644 index 0000000000..bb91526a83 --- /dev/null +++ b/examples/micronaut/README.md @@ -0,0 +1,54 @@ +# Containerize a [Micronaut](http://micronaut.io/) app with Jib + +This is an example of how to easily build a Docker image for a [Micronaut framework Groovy/Java application](http://guides.micronaut.io/creating-your-first-micronaut-app-groovy/guide/index.html) with Jib. + + +

+ + Dockerize Micronaut app with Jib + +

+ +## Quickstart + +### With Docker + +```shell +./gradlew jibDockerBuild + +docker run -d -p 8080:8080 micronaut-jib:0.1 +``` +```shell +curl localhost:8080/hello +> Hello World +``` + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Dockerize%20and%20run%20a%20%22Hello%20World%22%20%40Java%20%40micronautfw%20app%20with%20%23Jib%20in%20seconds&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/micronaut&hashtags=docker,kubernetes) + +### With Kubernetes + +```shell +IMAGE= + +./gradlew jib --image=$IMAGE + +kubectl run micronaut-jib --image=$IMAGE --port=8080 --restart=Never + +# Wait until pod is running +kubectl port-forward micronaut-jib 8080 > /dev/null 2>&1 & +``` +```shell +curl localhost:8080/hello +> Hello World +``` + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run%20a%20%22Hello%20World%22%20%40java%20%40micronautfw%20app%20on%20%23Kubernetes%20with%20%23Jib%20in%20seconds&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/micronaut&hashtags=docker,kubernetes) + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). +Learn [more about Micronaut](https://micronaut.io). + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/micronaut)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle new file mode 100644 index 0000000000..5e0c798395 --- /dev/null +++ b/examples/micronaut/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'application' + id 'groovy' + id 'io.spring.dependency-management' version '1.0.6.RELEASE' + id 'net.ltgt.apt-idea' version '0.18' + id 'com.google.cloud.tools.jib' version '0.9.9' +} + +version '0.1' +group 'example.micronaut-jib' + +repositories { + mavenLocal() + mavenCentral() + maven { url 'https://jcenter.bintray.com' } +} + +dependencyManagement { + imports { + mavenBom 'io.micronaut:bom:1.0.0.M4' + } +} + +dependencies { + annotationProcessor 'io.micronaut:inject-java' + compile 'io.micronaut:http-client' + compile 'io.micronaut:http-server-netty' + compile 'io.micronaut:runtime-groovy' + compile 'io.micronaut:inject' + compile 'io.micronaut:runtime' + compileOnly 'io.micronaut:inject-groovy' + compileOnly 'io.micronaut:inject-java' + runtime 'ch.qos.logback:logback-classic:1.2.3' + testCompile 'io.micronaut:inject-groovy' + testCompile('org.spockframework:spock-core:1.1-groovy-2.4') { + exclude group: 'org.codehaus.groovy', module: 'groovy-all' + } + testCompile 'io.micronaut:inject-java' +} + +compileJava.options.compilerArgs += '-parameters' +compileTestJava.options.compilerArgs += '-parameters' + +/** CHANGE THIS TO THE IMAGE YOU WANT TO PUSH TO */ +// jib.to.image = 'gcr.io/PROJECT_ID/micronaut-jib' diff --git a/examples/micronaut/dockerize-micronaut-jib.gif b/examples/micronaut/dockerize-micronaut-jib.gif new file mode 100644 index 0000000000000000000000000000000000000000..463ca2197141cf39bdba940bcff06d4a168a5f79 GIT binary patch literal 268744 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 z zh(B1><>)4j!F@Q_s-h2?-=>An(E2E^RWus}3wMi41VuApWlHMgDDZbbyc``_OoQ$VB5I-ZM@-#@L;HE(Dqy`hO()gNirs(q28vU+ndxj|Y z`_X!=5bIV3DIr-U+Ew-X+I22|Rfzr(GEJ2|SDESw8rGvtZ>}crRZS|FjaydzN{_mf zEO_Yx%^6F1U8M(Y@KwVh27V}hg%ku<(j4SBQ|6jsB+Er>eWvg<=5aN;Ce+T8ntGjx zU`6}|9VaU{7NixVsYXP@sg$6V{8eXGP5W}7lqFtAM|-F*`x=#5(}9P~GGx->&ld@%fm&t-mp)1X@kHj)D`9nqZskTr%_AP&}y zNKTF6awgAYIBf{T2eKi~l@Moh6QQSQ1mT`w0)*RQ!cbLOSk9wQ!}R-CfZL*O4ohT+ zAGyY0U~)<n~b=j$Pm7Gmoh)q+kP1~tWr<`qXi0xpn?SoU>Q8~NG5WCr4yTwzx6*>E-A@b`nHe{bVA3~Xtpx+}T&HBwnP2M##2K=0)B^pGTb1sHI%{W;34oyy&Dik2$WI? zlDhFs5}$8w3S_tu$=(juc}9ZBb=7Wg@!scDWRx>EtyO{sPS-s>S@dwli0a-BPW<2+ zEOon6^I0I{3DfkI2^=K?W0P^yFXb6pdcj#y0T2X&Bm#GHD6Dg(^hlgSgA9bto_%U{ z2@d=o#*PeU<`?9?ZIi%AO<5VtaU=92-c^bJC1rs)L;sz_XZ9fryzL#08SZp}3PI`E z$`d?|8R2d;f$lvyHFiC0JJ-Mt2vWgfEu@H&6C-r-SgG5Lhp|TNH;SrU}Lrd8P^8RULG8VH+ac)ln=q57nK6lP|SOC}V_9pA=Dl zzmu()ycWWM2Z9XLLy$x{l1xFj3t7pr!RcCcA6j`jDm7%QseTb)!D)%6zR5P&MDef` zgBx#=c<_5X_Sw;>4f68l_6WvDRGAfLqrnH`)!TRz$Q4V$Sp~@4sLRjQNRkKa1CPkb zb`6n1O_g-e4#)sC-4s)fUk#`RD5z9(dncIjaxW<_ELZ2b?N$}F^mlre4q|UqdNjZT z11y6^{iRfc+|&h3YvYfc9}JMX{ZhzZ*&vl}Tw3s@%1H(6F2X^;8E-##x=aCQHILcW zcWyv<%S!#0iBta%eFm@UqKa?%Z$x7x4$R+aOQ$U&4>CkW-F=Pv$th}BK45t(&jQ;# z@93SC9}n7cX#j?Mia(bga^Ul#2{#cKGEu@jC0EeP+PAumKUj9b`V8=;1d9PsRZwHq zD0Lo$CCI1B@%7{-nrx?9SIW~%qEP-~PdkOO}K5H~U`1y_NwtCT>FK3O}5w3iMKz3E=6mFz>8BT0N{i2d8Sgo+VFc==;tEXXvN+nJ5z7e9$hc7e9V*qx5VE;J!< zv>FUt9aH|GE1xTHr{1Ml$Np%6SxW42hX9dmf8)KOE-_o{m&Qpy!3cR9>SDm96JOAW+^}!FwLWTX$>v+0Kw` zt+HZAZ@oply&X`~VEdIJB7JZ$0qDCR2;$77CKQ4NoU(@=Qd|cg@U&-(3YN6QjeEZ% zMpa2|FrtK=K`5@IN*9Kv0VtdZE?1N>e9O5!JUhadu`#DtN@ezIrl;7miAEPj90D@e z%?R7h6q zWTDbp8%tBiM<2|!9?h{Ew7mbT$G^j+Gj|WJJQ^<7y1V?;QN2juM;sLx3J|8?Hb5x3 zB7YIUmFb+l#Q}3W@AKL%*HR{0jxK+tjrXk01MB}VZEk%YusRWn4kF@hd&e9iP{7zK z(FBqiWFig^)WFXRaSbry^@@iI`c7J?2Dr;v;gqosgSw6~h){ST7o83OE_aZR0AYac zYkU3Oa$#D6Lf9N&?+}3X5HB_iZ#*)$={BwdG`9|?UDv#=*`ML)^iI)A5qqrs%I!JT zHGbonmWERTjG>_;7G&MjIJMKyCvFC`O1hwu+Yd^K8tot zb5B~Uk4DQmzU<9jqQhDvN-};F$ICj20nYziV4{N`Iw98qc_>dc{*r1bJz;BTeQRWX zQgos24ZM$1%CAs+|=!XdF}JR+0IC9~;#LZi~@Fd_jw zlp;WfK>dQlV&h`7p_~Ao#JKH#!{h(*IlXSbhl`Jsmz$rXr>n2Cx4XZ?$IH*t*W2IY z=j-qD_xt|?3@C6Q!GZ=4B21`oA;X3aA3}^MaU#Wv7B6DVsBt65jvhaP3@LIX$&w~d zqD-lBCCipBU&4$jb0*E2HgDq0sdFdKo<4s94Jvdf(V|9=B2B7PpbsBQpF)i)bt=`W zRT^5XlJw zQ?`+vmX2hXg+U9r2cI+%tk;H*?2(ZsjWB}2=8JMd(c_Ca1W8YIFl?Yf44kAe#R4mI znA{a+Dj21Wh9Zhgbz_b(!X|*O5CSJgy73`+nlM_B0X)ojVw+ zI>2fY&qgb46~`W+L8OPSAZ!2!P_Tl#8zfr90_Fk=tphBw`|gfTEWj)Yo2US7vqc!- z?g}!1*@UHC459y@l>{4%FcJosyMYWldRi^CVCK8R2KxqJECm>J9B&lHn(@LWp*|aN z!a{WXF%i#Vu)qs2U?4!nMMPk($0mn-Yyk7R3$zmyyZlJI?~2>R0XZ0uDyadC$^(nF z)&@0;MpSJ>4$f7P!;DYwpu?+G*uVo1IDlMhaHH-3B-c5pO)7m6h>b7S%8G4(d$o$4 z!`4e+9e8d81CHFB14K~w*)vZquX_R7dBfvMyy;%AzQVHw!$N?tvJeMW60&q=Lc$3p z(}r4QmzclM8CCO;>#Bb6;^+cp^ z-TFds$pxJ7z`r)(y#Q#SOTil*7gmrYfDLX5mg=5?9+xfS+1Z35 zIT8P%1thPHN|K}|3RJ+=389aaj6sqR5TGHMkx)+%Ko~<8PS)dUX(I8 z=6juy0Lr2JACCBARD`1w1TdkqV_If(MBX9CKKrQ`Ynfgvm`{kO`AI z!UX>(fC(;;362~BE2q=aLcCO%o@k{9UeU8xc3UJhr9<@M2`kybKw1ld9;sDJ%pf3+7 zfQT}|h6dQD4n+7k85-eMzQNm$QfM3but%@kbI*)+I3pVphg1Q=5jO#gPAEQ-pJNaq zK%aPn!Ielyp2bzej93I81!tNn>Zc?y8&G!;QFAf za8i`m^DJ&omxGmws%0J^?elK@v}OK&_TUp&LM0K}eW?(dRXk;yxRSc6*7k3y&Rw;LKdwS%A8i}fc zCbe=rsO(aplkn#0YT`-0^s@vnZRjg>S33GOrlmJ|gh`(H$gktXc#xbYBNrNha2)Bp ziDU~)Wx-QXwp6xF2R}B9TZ>9of(9-y@lZ{W-TcmLK25DoZpN5SF}HAx&KYc*|JsMO zwz&{_$m`4U>XA9_r?C6!;+o6+!uv8d=I(?ZYm&CdL67!2rv2|rBVwr{r;fh`t=kA_ zD%@cRIi|bgg(!Q9sC0CJ3yAl*ig)0<2YO~N>RxXoj2Y7cH?#l2``zu6mQ_T2|4!cV z4S-p4VdZnLJL4PPbsib{q~i(e3D}-HKIf0|b`NkQG!9Yjsw=s>De|^&p*qWtbO;L| zfd%8-L=2mH0Det1t7$$bZza3eDhip3Sq+RL-b#qQf^j-^AN3-rO$Tb*fvS>l^xH#! z2)Sp%taJbjJ00H%(5AXN$S#B~s!oC{hW`y z3Jwk*sx8*k0bTY{!SnfE3{N-;XNudZp1j(NmI)Ny;p2wk=cgf)pXcw3v6(rqZwiH5@ zbQ}mONiqpb943LotoCMJh;s9y^v z;cCs^5q=Miy%^51+Rpum&aqm6OyBm&2Ygh{J!x1N7-7r-Q7!`C@nN5hh!APb7!bk? zU$GYObruzdLv@@11-yVGO4TnpfB_^A>Nr3)E=>OkWaEx3KsI8Ffn{U21fUvJ(TWL8Az$T&;1r>+ z(FW9LrQ&=T6bcc|l?W3Zz_QiGn{3TA*+~E6kO~Y%4l|AeFPi-_g`O$lmR zQhIF?Bt_EDypL_(R%RZ_Z!TrQy-xHHXC!e+#-$rs6~bm3+$5AvCpH0NLeF+B8~~;w zjJ%+E%t#9PToCSxHhq>9))Or9;)UH%uk28GjwJ}qr4xeZTK3#pvYHc$r_*H^E2`Ev zDW*6uCLP_&_fV#`d|@ap+#xNOX@b#0TE`%2L4w*sbiCu1os>!GkqRV`8XZ;Lm638S zz@QLGAe{{BAxIUNX#Tt#6!;RmrHlU#6_ASJRRpA{L220~B}syWXqS*Ewt+_=Q&=V)5sgq4_?oQ^LC=}ZtG(yn*eSv6ia?I3uc0YCZ4WuEnqpy-V0EEj z?&q!$;eWD2ZAhgY^v0v&L2o?D1jQNPag1*8=@Q^tZ9EEY)W)WU0jL@PYk_J3h^jkj zDjjU)5p=4guFI-UL8R8ft13Zn7(fo~0dq7*sy@PSd;zH{L95our{^4W?imTPI?CUCAR zH_T_Ny{lg>+)Y|r}a&jM}G3hmGmZP6O-(IRcqD(%uTZPPmK(?V_3O6}BAZPi-s z)naYdYVFo?ZP$A3*Me=>itX5vZP}Xb*`jUQs_ojcZQHu-+rn*Y7)RXF?Hde$uhxec z-0c$VEh=P(aK^y_6afFK)@{VrzziU6;?6(~1PBcz?$#tOdK|9eDsBx7f#V`>J56ro zGVT;?uHps<=FR}*A}-B+Zs``m>COOrsP5wuf$Dbd5VS6fP_E*#F5+75<>Iaoh%Vl4 zm@3Ty3y3JdsAk~?Z0&+>0EjMqL~a1I?(Xt#;<5_zx-Jw@@8~M8^b$eyMlbT_ZuWYw z5IisT3c>Xfhxrb{^}bh_3=C0^6u=t{{^G5La(k}I`FZ6cr`)Y5CZB!h%4!H7R9pZ1nvTp{D zE&`XY0w?h58Ug*auqQjg4|g&Fx3T1IvH=saDyK0Lkn$ko z?jNsm97}O4!?C7)0EV~`5in*Mty~mj$AL*zk^qw;QF0|KCg;+y3#W1bG;<~wGAN6# z?e#GOvk3nV<8dgP@)2xq4sC7^gY);o?$(TRGz)QX^sN9GvioK*8CU1yZQ`Is=_WMf z5@_N)^Vbt1bGhnoJD;*S+ukiZvot?*5?HW5=dd~_vN_MO5sa@KD=`#Pu7ySLItQ>o z3yAqq-~`rJ>2a}ipck0c^FCAI01p5*540O+a}l64Eu%3tCqe64E;FO9Aj>gpQSj@A zuk7x0O4lwhShVZ5G!<~+CXSzd84B&hnNo=wNoOnjCV&mwE={X+N(b~Ew{iPUvlOWA zNn>^M+Vl!HG%MdN4bZ?7>+~!0H01&{`+_t12K5wUbacV<2!5LrfWY)vz~H3ZFa5Gp zn=$_=voA_F^D_(dI(IV_IB)_Gw*MBiHp8?wr|M!K0UuknD}(apB6Jua8Brd|^dLYw zt{#zyFkt_fA&ai%rgCA=@?m3e5HEBTB=%}MHYlg{6mxTID?wuCwkThAZ_lwGL-T{c zvu8sA_iULGEDI_1rfCOu6#F!K)Uj~?GfQJNEq|^I?{@440Bno$bXV^yJFXA2FAzsD zbvyJ~*RbNUK@YQtcW?7!Z?2kP@^DL3ad#bcN>ebcm+VBPb90|UUvF?w?&+fO1Do(f z`!w?Us{WEU`@Zu39(aLIID1Pp@?LoDa&l$sFMK$#f;&M3jN=4xV>Je+u;}*^Jf{D_ z7)+uxKvOg|GAxT(6EF zwDu(^=Al=LkLkn}r)=gXjZ>XWZw-7WvV`9_SX;9c6gd%ixo_Y0V5d3m-Y)gF^_VC5 z;;wOIYja#9FP#@bSak{JWgFNjs5B)fJqtl!Td9^0?A@aEC?I;G7r{xB@!jfbRDZ#u zC&8lot`Sf=HQ=f{8+xaE`lo|>sEhijlX|I}`l+LOs;l~{vwEw$`m4iwtjqeW(|WDj z`mN)7u48PdE&{M70jWCbG5GpQ40}T08m#L2HelkVw}BO00J*wXaT3Fnp@je7$-x2m zBePe-as6nr&%uRs$6w>YEKI;CK2&q_0SP7|kwCi`q|f|*ftY*(3lI;4I>UrGV1psG z8wda`SVx$a!h)Fl9cY)+kiZ1QkGJR51HpS7WEt}m38y=}m)-`GQ284)!1FZx6i~Y% zIOr}*!@Z3`#5Fr*ijQzq+8z|Vf1|s?CqZduc^{Ofa=DWRSd|}KytjF@8x$TK$U7r| z50M;mGlgDhr(OX^ySyY2iP2Nd(i{wVh4Ihq#T%WyAz`wRii-U==bchxkO}@Oq&rn(iWWmyG!1Cn$(6qSItKR>y6p)uOi@LCj z&%le)h%q_pW`v0S$jnTYnYaKyNiLJ18#C0%akmO4LFMf6aovp%l$Tf8kN1`4?)4W zOYB1dZg|WFtf8jcwVo6 z8)^XP&;m0{x;!BHGbsx7yxdDwMS{pv#S9~gI5`_4Yw-wK>L7hJ^i&cHSkT=?c};6E z!z(;onhF2`PGaWs+f78)-sbN1{ss>hA15z2KSxhjUuW+YxHb_<%6FVT6AbNgu|6XsD^-G1OT5BFPZw$O42LZl@2E`0zhD^Ous`A9?2@B0M50J z5rw7VH;~{Jl@F~|%%wYhFwZM_l6m%O)7X!su)b}ol z3L*=W;7o#G01Uh zr(z-F(SZue2q{v6Vr&2d8;U@kv`6imvKFWaC<{$q2ebj)0+1VFg@dk8pcMunyfD4x zX{0>5u2Y*lL~*op-&x)IAR+?%J{&sta9}8iha?!Th=phh2dZkeu?(8|KqBirixj+z z5WWBq!?N&(V*$SMhL}z&_HIk?kw_Q}fW61O(rcUuD4<}4Lw_c$` zxdaRt-_TLHG8}@)v6JkG#Ucw5z|W~TiqZl{g+4K2Ln6QsMgU#>Lq<$w@T4*cx!w?@ zgtGM6qCctPpumL<0`$hfN$b%h&_N$5%mt2u5=1@V03gIn_tq>zMj{M7qSN|59a7dh zQxvBuKDTkzNvHHY6<0XYG@?yH;iM25%S`3xz&8-BG0Ip0&`3}>y~H+KZMWsNTW`Ms z!wXooIiSTAR(N4os0bal3~Af7vzd6Anx{ltA#

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.properties b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..115e6ac0aa --- /dev/null +++ b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/micronaut/gradlew b/examples/micronaut/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/examples/micronaut/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/micronaut/gradlew.bat b/examples/micronaut/gradlew.bat new file mode 100644 index 0000000000..e95643d6a2 --- /dev/null +++ b/examples/micronaut/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/micronaut/settings.gradle b/examples/micronaut/settings.gradle new file mode 100644 index 0000000000..3770a05315 --- /dev/null +++ b/examples/micronaut/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'micronaut-jib' diff --git a/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy new file mode 100644 index 0000000000..8566266880 --- /dev/null +++ b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy @@ -0,0 +1,14 @@ +package example.micronaut + +import groovy.transform.CompileStatic +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@CompileStatic +@Controller("/hello") // <1> +class HelloController { + @Get("/") // <2> + String index() { + "Hello World" // <3> + } +} diff --git a/examples/micronaut/src/main/java/example/micronaut/Application.java b/examples/micronaut/src/main/java/example/micronaut/Application.java new file mode 100644 index 0000000000..63711495e4 --- /dev/null +++ b/examples/micronaut/src/main/java/example/micronaut/Application.java @@ -0,0 +1,10 @@ +package example.micronaut; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} \ No newline at end of file diff --git a/examples/micronaut/src/main/resources/application.yml b/examples/micronaut/src/main/resources/application.yml new file mode 100644 index 0000000000..078029e9f6 --- /dev/null +++ b/examples/micronaut/src/main/resources/application.yml @@ -0,0 +1,5 @@ +micronaut: + application: + name: examples + server: + port: 8080 \ No newline at end of file diff --git a/examples/micronaut/src/main/resources/logback.xml b/examples/micronaut/src/main/resources/logback.xml new file mode 100644 index 0000000000..afaebf8e17 --- /dev/null +++ b/examples/micronaut/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy new file mode 100644 index 0000000000..5680fa0158 --- /dev/null +++ b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy @@ -0,0 +1,31 @@ +package example.micronaut + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.RxHttpClient +import io.micronaut.runtime.server.EmbeddedServer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class HelloControllerSpec extends Specification { + + + @Shared + @AutoCleanup // <1> + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) // <2> + + @Shared + @AutoCleanup + RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) // <3> + + void "test hello world response"() { + when: + HttpRequest request = HttpRequest.GET('/hello') // <4> + String rsp = client.toBlocking().retrieve(request) + + then: + rsp == "Hello World" + } + +} \ No newline at end of file diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index 8f18413f28..36d069f589 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -69,4 +69,6 @@ Visit the IP in your web browser and you should see: ``` Hello Jib Multimodule -``` \ No newline at end of file +``` + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/multi-module)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/spring-boot-draft/README.md b/examples/spring-boot-draft/README.md index ff8143c788..8ffc77a1dd 100644 --- a/examples/spring-boot-draft/README.md +++ b/examples/spring-boot-draft/README.md @@ -124,4 +124,6 @@ settings.xml(`${MAVEN_HOME}/conf/settings.xml`)(You should config this is you ar -``` \ No newline at end of file +``` + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot-draft)](https://github.com/igrigorik/ga-beacon) From 6a2551e1f3e7c0d16c66c91eee8b38b0dd9580a6 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 30 Aug 2018 14:52:45 -0400 Subject: [PATCH 0169/2020] Adds link to Micronaut example. (#910) --- examples/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/README.md b/examples/README.md index 61e0ce9433..92930223e3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,3 +15,7 @@ See [multi-module](multi-module) for containerizing projects with multiple modul + +### Micronaut example + +See [micronaut](micronaut) for containerizing a [Micronaut framework](https://micronaut.io/) Groovy/Java application. From 7bf07db533e7b707eb4c7838d591512e03bb0fe8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 30 Aug 2018 15:52:26 -0400 Subject: [PATCH 0170/2020] Remove DockerCredentialHelperFactory (#903) --- ...DockerCredentialHelperIntegrationTest.java | 7 +- .../frontend/CredentialRetrieverFactory.java | 126 ++++++++---------- .../registry/credentials/DockerConfig.java | 14 +- .../DockerConfigCredentialRetriever.java | 94 +++++-------- .../credentials/DockerCredentialHelper.java | 16 ++- .../DockerCredentialHelperFactory.java | 44 ------ .../CredentialRetrieverFactoryTest.java | 86 +++++++----- .../DockerConfigCredentialRetrieverTest.java | 63 ++++----- .../common/DefaultCredentialRetrievers.java | 4 +- 9 files changed, 184 insertions(+), 270 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 29f4f2282b..086edc597b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -38,8 +38,7 @@ public void testRetrieveGCR() new Command("docker-credential-gcr", "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); - DockerCredentialHelper dockerCredentialHelper = - new DockerCredentialHelperFactory().newDockerCredentialHelper("myregistry", "gcr"); + DockerCredentialHelper dockerCredentialHelper = new DockerCredentialHelper("myregistry", "gcr"); Credential credentials = dockerCredentialHelper.retrieve(); Assert.assertEquals("myusername", credentials.getUsername()); @@ -51,7 +50,7 @@ public void testRetrieve_nonexistentCredentialHelper() throws IOException, CredentialHelperUnhandledServerUrlException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory().newDockerCredentialHelper("", "fake-cloud-provider"); + new DockerCredentialHelper("", "fake-cloud-provider"); fakeDockerCredentialHelper.retrieve(); @@ -68,7 +67,7 @@ public void testRetrieve_nonexistentServerUrl() throws IOException, CredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelperFactory().newDockerCredentialHelper("fake.server.url", "gcr"); + new DockerCredentialHelper("fake.server.url", "gcr"); fakeDockerCredentialHelper.retrieve(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index b5a0bfe4c3..dca1288e65 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -36,6 +36,13 @@ /** Static factories for various {@link CredentialRetriever}s. */ public class CredentialRetrieverFactory { + /** Used for passing in mock {@link DockerCredentialHelper}s for testing. */ + @VisibleForTesting + @FunctionalInterface + interface DockerCredentialHelperFactory { + DockerCredentialHelper create(String registry, Path credentialHelper); + } + /** * Defines common credential helpers to use as defaults. Maps from registry suffix to credential * helper suffix. @@ -56,22 +63,21 @@ public static CredentialRetrieverFactory forImage( } private final JibLogger logger; - private ImageReference imageReference; + private final ImageReference imageReference; + private final DockerCredentialHelperFactory dockerCredentialHelperFactory; private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logger) { - this.imageReference = imageReference; - this.logger = logger; + this(imageReference, logger, DockerCredentialHelper::new); } - /** - * Sets the image reference the {@link CredentialRetriever}s should retrieve credentials for. - * - * @param imageReference the image reference - * @return this - */ - public CredentialRetrieverFactory setImageReference(ImageReference imageReference) { + @VisibleForTesting + CredentialRetrieverFactory( + ImageReference imageReference, + JibLogger logger, + DockerCredentialHelperFactory dockerCredentialHelperFactory) { this.imageReference = imageReference; - return this; + this.logger = logger; + this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } /** @@ -96,7 +102,7 @@ public CredentialRetriever known(Credential credential, String credentialSource) * @return a new {@link CredentialRetriever} */ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { - return dockerCredentialHelper(Paths.get(credentialHelper), new DockerCredentialHelperFactory()); + return dockerCredentialHelper(Paths.get(credentialHelper)); } /** @@ -109,7 +115,21 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { * href="https://github.com/docker/docker-credential-helpers#development">https://github.com/docker/docker-credential-helpers#development */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { - return dockerCredentialHelper(credentialHelper, new DockerCredentialHelperFactory()); + return () -> { + logger.info("Checking credentials from " + credentialHelper); + + try { + return retrieveFromDockerCredentialHelper(credentialHelper); + + } catch (CredentialHelperUnhandledServerUrlException ex) { + logger.info( + "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); + return null; + + } catch (IOException ex) { + throw new CredentialRetrievalException(ex); + } + }; } /** @@ -120,36 +140,6 @@ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { * @return a new {@link CredentialRetriever} */ public CredentialRetriever inferCredentialHelper() { - return inferCredentialHelper(new DockerCredentialHelperFactory()); - } - - /** - * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Docker config - * (located at {@code $USER_HOME/.docker/config.json}). - * - * @return a new {@link CredentialRetriever} - * @see DockerConfigCredentialRetriever - */ - public CredentialRetriever dockerConfig() { - return dockerConfig(new DockerConfigCredentialRetriever(imageReference.getRegistry())); - } - - /** - * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from a custom path - * to a Docker config. - * - * @param dockerConfigFile the path to the Docker config file - * @return a new {@link CredentialRetriever} - * @see DockerConfigCredentialRetriever - */ - public CredentialRetriever dockerConfig(Path dockerConfigFile) { - return dockerConfig( - new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile)); - } - - @VisibleForTesting - CredentialRetriever inferCredentialHelper( - DockerCredentialHelperFactory dockerCredentialHelperFactory) { List inferredCredentialHelperSuffixes = new ArrayList<>(); for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) { if (!imageReference.getRegistry().endsWith(registrySuffix)) { @@ -167,9 +157,8 @@ CredentialRetriever inferCredentialHelper( try { return retrieveFromDockerCredentialHelper( Paths.get( - DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX - + inferredCredentialHelperSuffix), - dockerCredentialHelperFactory); + DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + + inferredCredentialHelperSuffix)); } catch (CredentialHelperNotFoundException | CredentialHelperUnhandledServerUrlException ex) { @@ -189,24 +178,28 @@ CredentialRetriever inferCredentialHelper( }; } - @VisibleForTesting - CredentialRetriever dockerCredentialHelper( - Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) { - return () -> { - logger.info("Checking credentials from " + credentialHelper); - - try { - return retrieveFromDockerCredentialHelper(credentialHelper, dockerCredentialHelperFactory); - - } catch (CredentialHelperUnhandledServerUrlException ex) { - logger.info( - "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); - return null; + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Docker config + * (located at {@code $USER_HOME/.docker/config.json}). + * + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig() { + return dockerConfig(new DockerConfigCredentialRetriever(imageReference.getRegistry())); + } - } catch (CredentialHelperNotFoundException | IOException ex) { - throw new CredentialRetrievalException(ex); - } - }; + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from a custom path + * to a Docker config. + * + * @param dockerConfigFile the path to the Docker config file + * @return a new {@link CredentialRetriever} + * @see DockerConfigCredentialRetriever + */ + public CredentialRetriever dockerConfig(Path dockerConfigFile) { + return dockerConfig( + new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile)); } @VisibleForTesting @@ -227,13 +220,12 @@ CredentialRetriever dockerConfig( }; } - private Credential retrieveFromDockerCredentialHelper( - Path credentialHelper, DockerCredentialHelperFactory dockerCredentialHelperFactory) + private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { Credential credentials = dockerCredentialHelperFactory - .newDockerCredentialHelper(imageReference.getRegistry(), credentialHelper) + .create(imageReference.getRegistry(), credentialHelper) .retrieve(); logGotCredentialsFrom(credentialHelper.getFileName().toString()); return credentials; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index c45a4afa1b..ac572686ac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; -import com.google.common.annotations.VisibleForTesting; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -82,12 +81,6 @@ String getAuthFor(String registry) { return authEntry != null ? authEntry.getValue().getAuth() : null; } - @VisibleForTesting - @Nullable - DockerCredentialHelper getCredentialHelperFor(String registry) { - return getCredentialHelperFor(new DockerCredentialHelperFactory(), registry); - } - /** * Determines a {@link DockerCredentialHelper} to use for {@code registry}. * @@ -103,13 +96,12 @@ DockerCredentialHelper getCredentialHelperFor(String registry) { * registry */ @Nullable - DockerCredentialHelper getCredentialHelperFor( - DockerCredentialHelperFactory dockerCredentialHelperFactory, String registry) { + DockerCredentialHelper getCredentialHelperFor(String registry) { List> registryMatchers = getRegistryMatchersFor(registry); Map.Entry firstMatchInAuths = findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); if (dockerConfigTemplate.getCredsStore() != null && firstMatchInAuths != null) { - return dockerCredentialHelperFactory.newDockerCredentialHelper( + return new DockerCredentialHelper( firstMatchInAuths.getKey(), dockerConfigTemplate.getCredsStore()); } Map.Entry firstMatchInCredHelpers = @@ -117,7 +109,7 @@ DockerCredentialHelper getCredentialHelperFor( if (firstMatchInCredHelpers == null) { return null; } - return dockerCredentialHelperFactory.newDockerCredentialHelper( + return new DockerCredentialHelper( firstMatchInCredHelpers.getKey(), firstMatchInCredHelpers.getValue()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index a17826f632..5337853c99 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -54,7 +54,6 @@ public class DockerConfigCredentialRetriever { private final String registry; private final Path dockerConfigFile; - private final DockerCredentialHelperFactory dockerCredentialHelperFactory; public DockerConfigCredentialRetriever(String registry) { this(registry, DOCKER_CONFIG_FILE); @@ -64,17 +63,6 @@ public DockerConfigCredentialRetriever(String registry) { public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; - this.dockerCredentialHelperFactory = new DockerCredentialHelperFactory(); - } - - @VisibleForTesting - DockerConfigCredentialRetriever( - String registry, - Path dockerConfigFile, - DockerCredentialHelperFactory dockerCredentialHelperFactory) { - this.registry = registry; - this.dockerConfigFile = dockerConfigFile; - this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } /** @@ -85,71 +73,51 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { */ @Nullable public Credential retrieve() throws IOException { - DockerConfigTemplate dockerConfigTemplate = loadDockerConfigTemplate(); - if (dockerConfigTemplate == null) { + if (!Files.exists(dockerConfigFile)) { return null; } - - DockerConfig dockerConfig = new DockerConfig(dockerConfigTemplate); - - for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { - Credential credentials = retrieve(dockerConfig, registryAlias); - if (credentials != null) { - return credentials; - } - } - return null; + DockerConfig dockerConfig = + new DockerConfig( + JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class)); + return retrieve(dockerConfig); } /** * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @param dockerConfig the {@link DockerConfig} to retrieve from - * @param registryAlias the registry alias to use * @return the retrieved credentials, or {@code null} if none are found */ + @VisibleForTesting @Nullable - private Credential retrieve(DockerConfig dockerConfig, String registryAlias) { - // First, tries to find defined auth. - String auth = dockerConfig.getAuthFor(registryAlias); - if (auth != null) { - // 'auth' is a basic authentication token that should be parsed back into credentials - String usernameColonPassword = new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); - String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); - String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); - return new Credential(username, password); - } - - // Then, tries to use a defined credHelpers credential helper. - DockerCredentialHelper dockerCredentialHelper = - dockerConfig.getCredentialHelperFor(dockerCredentialHelperFactory, registryAlias); - if (dockerCredentialHelper != null) { - try { - // Tries with the given registry alias (may be the original registry). - return dockerCredentialHelper.retrieve(); + Credential retrieve(DockerConfig dockerConfig) { + for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { + // First, tries to find defined auth. + String auth = dockerConfig.getAuthFor(registryAlias); + if (auth != null) { + // 'auth' is a basic authentication token that should be parsed back into credentials + String usernameColonPassword = + new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); + String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); + String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); + return new Credential(username, password); + } - } catch (IOException - | CredentialHelperUnhandledServerUrlException - | CredentialHelperNotFoundException ex) { - // Ignores credential helper retrieval exceptions. + // Then, tries to use a defined credHelpers credential helper. + DockerCredentialHelper dockerCredentialHelper = + dockerConfig.getCredentialHelperFor(registryAlias); + if (dockerCredentialHelper != null) { + try { + // Tries with the given registry alias (may be the original registry). + return dockerCredentialHelper.retrieve(); + + } catch (IOException + | CredentialHelperUnhandledServerUrlException + | CredentialHelperNotFoundException ex) { + // Ignores credential helper retrieval exceptions. + } } } - return null; } - - /** - * Loads the Docker config JSON and caches it. - * - * @throws IOException if failed to parse the config JSON - */ - @Nullable - private DockerConfigTemplate loadDockerConfigTemplate() throws IOException { - // Loads the Docker config. - if (!Files.exists(dockerConfigFile)) { - return null; - } - - return JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 4a5389d062..d86a03c11b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -29,6 +29,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.Paths; import javax.annotation.Nullable; /** @@ -39,6 +40,8 @@ */ public class DockerCredentialHelper { + public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-"; + private final String serverUrl; private final Path credentialHelper; @@ -51,16 +54,20 @@ private static class DockerCredentialsTemplate implements JsonTemplate { } /** - * Construct with {@link DockerCredentialHelperFactory}. + * Constructs a new {@link DockerCredentialHelper}. * * @param serverUrl the server URL to pass into the credential helper * @param credentialHelper the path to the credential helper executable */ - DockerCredentialHelper(String serverUrl, Path credentialHelper) { + public DockerCredentialHelper(String serverUrl, Path credentialHelper) { this.serverUrl = serverUrl; this.credentialHelper = credentialHelper; } + DockerCredentialHelper(String registry, String credentialHelperSuffix) { + this(registry, Paths.get(CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); + } + /** * Calls the credential helper CLI in the form: * @@ -135,11 +142,6 @@ public Credential retrieve() } } - @VisibleForTesting - String getServerUrl() { - return serverUrl; - } - @VisibleForTesting Path getCredentialHelper() { return credentialHelper; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java deleted file mode 100644 index b38314e540..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry.credentials; - -import java.nio.file.Path; -import java.nio.file.Paths; - -/** Factory class for constructing {@link DockerCredentialHelper}. */ -public class DockerCredentialHelperFactory { - - public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-"; - - public DockerCredentialHelperFactory() {} - - /** - * @param registry the {@code ServerURL} stored by the credential helper - * @param credentialHelper the path to the Docker credential helper executable - usually in the - * form docker-credential-[suffix] - * @return a {@link DockerCredentialHelper} retrieved from the command. - */ - public DockerCredentialHelper newDockerCredentialHelper(String registry, Path credentialHelper) { - return new DockerCredentialHelper(registry, credentialHelper); - } - - public DockerCredentialHelper newDockerCredentialHelper( - String registry, String credentialHelperSuffix) { - return new DockerCredentialHelper( - registry, Paths.get(CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index a168bedc5a..85869bff63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -18,13 +18,15 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.cloud.tools.jib.registry.credentials.DockerConfigCredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Before; @@ -40,8 +42,27 @@ public class CredentialRetrieverFactoryTest { private static final Credential FAKE_CREDENTIALS = new Credential("username", "password"); + /** + * Returns a {@link DockerCredentialHelperFactory} that checks given parameters upon creating a + * {@link DockerCredentialHelper} instance. + * + * @param expectedRegistry the expected registry given to the factory + * @param expectedCredentialHelper the expected credential helper path given to the factory + * @param returnedCredentialHelper the mock credential helper to return + * @return a new {@link DockerCredentialHelperFactory} + */ + private static DockerCredentialHelperFactory getTestFactory( + String expectedRegistry, + Path expectedCredentialHelper, + DockerCredentialHelper returnedCredentialHelper) { + return (registry, credentialHelper) -> { + Assert.assertEquals(expectedRegistry, registry); + Assert.assertEquals(expectedCredentialHelper, credentialHelper); + return returnedCredentialHelper; + }; + } + @Mock private JibLogger mockJibLogger; - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; @@ -60,61 +81,60 @@ public void setUp() } @Test - public void testDockerCredentialHelper() throws Exception { + public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage( - ImageReference.of("registry", null, null), mockJibLogger); - - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "registry", Paths.get("docker-credential-helper"))) - .thenReturn(mockDockerCredentialHelper); + new CredentialRetrieverFactory( + ImageReference.of("registry", null, null), + mockJibLogger, + getTestFactory( + "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); Assert.assertEquals( FAKE_CREDENTIALS, credentialRetrieverFactory - .dockerCredentialHelper( - Paths.get("docker-credential-helper"), mockDockerCredentialHelperFactory) + .dockerCredentialHelper(Paths.get("docker-credential-helper")) .retrieve()); Mockito.verify(mockJibLogger).info("Using docker-credential-helper for registry"); } @Test - public void testInferCredentialHelper() throws Exception { + public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage( - ImageReference.of("something.gcr.io", null, null), mockJibLogger); - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "something.gcr.io", Paths.get("docker-credential-gcr"))) - .thenReturn(mockDockerCredentialHelper); - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "something.amazonaws.com", Paths.get("docker-credential-ecr-login"))) - .thenReturn(mockNonexistentDockerCredentialHelper); + new CredentialRetrieverFactory( + ImageReference.of("something.gcr.io", null, null), + mockJibLogger, + getTestFactory( + "something.gcr.io", + Paths.get("docker-credential-gcr"), + mockDockerCredentialHelper)); Assert.assertEquals( - FAKE_CREDENTIALS, - credentialRetrieverFactory - .inferCredentialHelper(mockDockerCredentialHelperFactory) - .retrieve()); + FAKE_CREDENTIALS, credentialRetrieverFactory.inferCredentialHelper().retrieve()); Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); + } + + @Test + public void testInferCredentialHelper_warn() throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + new CredentialRetrieverFactory( + ImageReference.of("something.amazonaws.com", null, null), + mockJibLogger, + getTestFactory( + "something.amazonaws.com", + Paths.get("docker-credential-ecr-login"), + mockNonexistentDockerCredentialHelper)); Mockito.when(mockCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); - Assert.assertNull( - credentialRetrieverFactory - .setImageReference(ImageReference.of("something.amazonaws.com", null, null)) - .inferCredentialHelper(mockDockerCredentialHelperFactory) - .retrieve()); + Assert.assertNull(credentialRetrieverFactory.inferCredentialHelper().retrieve()); Mockito.verify(mockJibLogger).warn("warning"); Mockito.verify(mockJibLogger).info(" Caused by: the root cause"); } @Test - public void testDockerConfig() throws Exception { + public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( ImageReference.of("registry", null, null), mockJibLogger); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 5e33541464..92a8b16f31 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -37,7 +37,7 @@ public class DockerConfigCredentialRetrieverTest { private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); @Mock private DockerCredentialHelper mockDockerCredentialHelper; - @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private DockerConfig mockDockerConfig; private Path dockerConfigFile; @@ -46,7 +46,6 @@ public void setUp() throws URISyntaxException, CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); - Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); } @@ -61,7 +60,7 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { @Test public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", dockerConfigFile, null); + new DockerConfigCredentialRetriever("some registry", dockerConfigFile); Credential credentials = dockerConfigCredentialRetriever.retrieve(); Assert.assertNotNull(credentials); @@ -70,45 +69,36 @@ public void testRetrieve_hasAuth() throws IOException { } @Test - public void testRetrieve_useCredsStore() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "just registry", "some credential store")) + public void testRetrieve_useCredsStore() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("just registry")) .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "just registry", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("just registry", dockerConfigFile); - Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); + Assert.assertEquals( + FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); } @Test - public void testRetrieve_useCredsStore_withProtocol() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "https://with.protocol", "some credential store")) + public void testRetrieve_useCredsStore_withProtocol() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("with.protocol")) .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "with.protocol", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("with.protocol", dockerConfigFile); - Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); + Assert.assertEquals( + FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); } @Test - public void testRetrieve_useCredHelper() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "another registry", "another credential helper")) + public void testRetrieve_useCredHelper() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("another registry")) .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "another registry", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("another registry", dockerConfigFile); - Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); + Assert.assertEquals( + FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); } @Test @@ -120,17 +110,14 @@ public void testRetrieve_none() throws IOException { } @Test - public void testRetrieve_credentialFromAlias() throws IOException { - Mockito.when( - mockDockerCredentialHelperFactory.newDockerCredentialHelper( - "index.docker.io", "index.docker.io credential helper")) + public void testRetrieve_credentialFromAlias() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("registry.hub.docker.com")) .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); - Assert.assertEquals(FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve()); + Assert.assertEquals( + FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); } @Test @@ -139,8 +126,7 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "index.docker.io", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); Credential credentials = dockerConfigCredentialRetriever.retrieve(); Assert.assertNotNull(credentials); @@ -154,8 +140,7 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever( - "registry.hub.docker.com", dockerConfigFile, mockDockerCredentialHelperFactory); + new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); Credential credentials = dockerConfigCredentialRetriever.retrieve(); Assert.assertNotNull(credentials); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 71cd7d0ad3..e2cfddc4cb 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelperFactory; +import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; @@ -115,7 +115,7 @@ public List asList() { if (credentialHelperSuffix != null) { credentialRetrievers.add( credentialRetrieverFactory.dockerCredentialHelper( - DockerCredentialHelperFactory.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); + DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); } if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); From 223c031e02c3010d092e943a981a4857e8a0e179 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 30 Aug 2018 16:28:14 -0400 Subject: [PATCH 0171/2020] Add unzip util method (#906) * Add unzip util method * Eliminate Zip-Slip vulnernability (#908) --- .../tools/jib/plugins/common/ZipUtil.java | 64 ++++++++++++ .../tools/jib/plugins/common/ZipUtilTest.java | 91 ++++++++++++++++++ .../src/test/resources/test-archives/test.zip | Bin 0 -> 1166 bytes .../resources/test-archives/zip-slip-win.zip | Bin 0 -> 547 bytes .../test/resources/test-archives/zip-slip.zip | Bin 0 -> 545 bytes 5 files changed, 155 insertions(+) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java create mode 100644 jib-plugins-common/src/test/resources/test-archives/test.zip create mode 100644 jib-plugins-common/src/test/resources/test-archives/zip-slip-win.zip create mode 100644 jib-plugins-common/src/test/resources/test-archives/zip-slip.zip diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java new file mode 100644 index 0000000000..dbaa98ce7c --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.common.io.ByteStreams; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** Utility class for Zip archives. */ +public class ZipUtil { + + /** + * Unzips {@code archive} into {@code destination}. + * + * @param archive zip archive to unzip + * @param destination target root for unzipping + * @throws IOException when I/O error occurs + */ + public static void unzip(Path archive, Path destination) throws IOException { + String canonicalDestination = destination.toFile().getCanonicalPath(); + + try (InputStream fileIn = Files.newInputStream(archive); + ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(fileIn))) { + + for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { + Path entryPath = destination.resolve(entry.getName()); + + String canonicalTarget = entryPath.toFile().getCanonicalPath(); + if (!canonicalTarget.startsWith(canonicalDestination + File.separator)) { + throw new IOException("Blocked unzipping files outside destination: " + entry.getName()); + } + + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + try (OutputStream out = Files.newOutputStream(entryPath)) { + ByteStreams.copy(zipIn, out); + } + } + } + } + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java new file mode 100644 index 0000000000..777b0d10cc --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link ZipUtil}. */ +public class ZipUtilTest { + + @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testUnzip() throws URISyntaxException, IOException { + verifyUnzip(tempFolder.getRoot().toPath()); + } + + @Test + public void testUnzip_nonExistingDestination() throws URISyntaxException, IOException { + Path destination = tempFolder.getRoot().toPath().resolve("non/exisiting"); + verifyUnzip(destination); + + Assert.assertTrue(Files.exists(destination)); + } + + @Test + public void testZipSlipVulnerability_windows() throws URISyntaxException { + Assume.assumeTrue(System.getProperty("os.name").startsWith("Windows")); + + Path archive = Paths.get(Resources.getResource("test-archives/zip-slip-win.zip").toURI()); + verifyZipSlipSafe(archive); + } + + @Test + public void testZipSlipVulnerability_unix() throws URISyntaxException { + Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); + + Path archive = Paths.get(Resources.getResource("test-archives/zip-slip.zip").toURI()); + verifyZipSlipSafe(archive); + } + + private void verifyUnzip(Path destination) throws URISyntaxException, IOException { + Path archive = Paths.get(Resources.getResource("test-archives/test.zip").toURI()); + + ZipUtil.unzip(archive, destination); + + Assert.assertTrue(Files.isDirectory(destination.resolve("my-zip/some/sub/folder"))); + + Path file1 = destination.resolve("file1.txt"); + Path file2 = destination.resolve("my-zip/file2.txt"); + Path file3 = destination.resolve("my-zip/some/sub/folder/file3.txt"); + Assert.assertEquals("file1", Files.readAllLines(file1).get(0)); + Assert.assertEquals("file2", Files.readAllLines(file2).get(0)); + Assert.assertEquals("file3", Files.readAllLines(file3).get(0)); + } + + private void verifyZipSlipSafe(Path archive) { + try { + ZipUtil.unzip(archive, tempFolder.getRoot().toPath()); + Assert.fail("Should block Zip-Slip"); + } catch (IOException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith("Blocked unzipping files outside destination: ")); + } + } +} diff --git a/jib-plugins-common/src/test/resources/test-archives/test.zip b/jib-plugins-common/src/test/resources/test-archives/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..ea23d0d20008d6f491706d94357b9dfe755ee38f GIT binary patch literal 1166 zcmb8t%}N4M6u|K_&Q)=0F}ni8A|hx}p)G6EIs%~&Fo_@?NmDT*lLEO@Spo-fa+jCl=%t@NhT`Oou?dR`sOvEjq>YB#IkR>dn)CN{OT4d1ee! zsa84jE+n{#V-?1YMqvAH{xyLcqO&j>ER`$X##*D*@K?8mD(R8c`8PS?0v~aKPhIgm z@YzqNTXV|m;}Yo@yd3lH(Gx!_p93$2^AeO*P!Ew8Gn7Thie>Ckam)spxbU@2mnxOG zOeHcK$MjG*Mp}Rc*`P?-Odz_z#oLG|6Z%`#1LA7WpsJh@H~isz33^Ntw~*~#FX9(p z0*xHtEG>Krr8Ql);R>;pw*3=BYDGc0KYu@GUy3JDvua0&2cWD;S< n9Sk5dKwwE@D3BbG5CK|>5-0)QtZX1BF##bX(5E~g-!cFIhBtZG literal 0 HcmV?d00001 diff --git a/jib-plugins-common/src/test/resources/test-archives/zip-slip.zip b/jib-plugins-common/src/test/resources/test-archives/zip-slip.zip new file mode 100644 index 0000000000000000000000000000000000000000..38b3f499de0163e62ca15ce18350a9d9a477a51b GIT binary patch literal 545 zcmWIWW@h1H0D=Au{XYEp{-1?`Y!K#PkYPyA&ri`SsVE5z;bdU8U359h4v0%DxEUB( zzA-W|u!sQFm1JZVD*#cV0!Xz&eqJh90MJm76a&LlprHwl)s`S02)6*So}T`Ippx7I z{nWC|9FT|Lj?Pm62|-=W$Rx*%D=;L0E@xl>dYWNLBZ!3v8dgZqpan~SHzSh>Gwx6T jnE?Vz8bg8PfCLE8QsgiR@MdKLxrhk}K_2A>d6oeH^pk5C literal 0 HcmV?d00001 From 84a50029164ed6a5d9de3de9cb8f442686d093f8 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 30 Aug 2018 16:53:26 -0400 Subject: [PATCH 0172/2020] Add support for defining an entrypoint (#873) --- .../frontend/JavaDockerContextGenerator.java | 26 +--- .../JavaDockerContextGeneratorTest.java | 5 +- jib-gradle-plugin/CHANGELOG.md | 1 + .../tools/jib/gradle/ContainerParameters.java | 11 ++ .../tools/jib/gradle/DockerContextTask.java | 15 +- .../gradle/PluginConfigurationProcessor.java | 14 +- .../tools/jib/gradle/JibExtensionTest.java | 3 + .../PluginConfigurationProcessorTest.java | 120 ++++++++++++++++ jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/DockerContextMojo.java | 14 +- .../jib/maven/JibPluginConfiguration.java | 6 + .../maven/PluginConfigurationProcessor.java | 16 ++- .../PluginConfigurationProcessorTest.java | 129 ++++++++++++++++++ 13 files changed, 325 insertions(+), 36 deletions(-) create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 799e8e171f..8369034bef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -101,8 +101,7 @@ private static void addIfNotEmpty( private final ImmutableList copyDirectives; @Nullable private String baseImage; - private List jvmFlags = Collections.emptyList(); - private String mainClass = ""; + private List entrypoint = Collections.emptyList(); private List javaArguments = Collections.emptyList(); private List exposedPorts = Collections.emptyList(); private Map labels = Collections.emptyMap(); @@ -150,24 +149,13 @@ public JavaDockerContextGenerator setBaseImage(String baseImage) { } /** - * Sets the JVM flags used in the {@code ENTRYPOINT}. + * Sets the entrypoint to be used as the {@code ENTRYPOINT}. * - * @param jvmFlags the jvm flags. + * @param entrypoint the entrypoint. * @return this */ - public JavaDockerContextGenerator setJvmFlags(List jvmFlags) { - this.jvmFlags = jvmFlags; - return this; - } - - /** - * Sets the main class used in the {@code ENTRYPOINT}. - * - * @param mainClass the name of the main class. - * @return this - */ - public JavaDockerContextGenerator setMainClass(String mainClass) { - this.mainClass = mainClass; + public JavaDockerContextGenerator setEntrypoint(List entrypoint) { + this.entrypoint = entrypoint; return this; } @@ -291,9 +279,7 @@ String makeDockerfile() throws JsonProcessingException { dockerfile .append("\nENTRYPOINT ") - .append( - objectMapper.writeValueAsString( - JavaEntrypointConstructor.makeDefaultEntrypoint(jvmFlags, mainClass))) + .append(objectMapper.writeValueAsString(entrypoint)) .append("\nCMD ") .append(objectMapper.writeValueAsString(javaArguments)); return dockerfile.toString(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 9bf01cd230..69f3b92ec8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -151,8 +151,9 @@ public void testMakeDockerfile() throws IOException { String dockerfile = new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage(expectedBaseImage) - .setJvmFlags(expectedJvmFlags) - .setMainClass(expectedMainClass) + .setEntrypoint( + JavaEntrypointConstructor.makeDefaultEntrypoint( + expectedJvmFlags, expectedMainClass)) .setJavaArguments(expectedJavaArguments) .setExposedPorts(exposedPorts) .setLabels(expectedLabels) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 6f22272723..09cf6a77f9 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `container.entrypoint` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) - Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 5265222438..8f2388e030 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -34,6 +34,7 @@ public class ContainerParameters { private boolean useCurrentTimestamp = false; private List jvmFlags = Collections.emptyList(); + private List entrypoint = Collections.emptyList(); @Nullable private String mainClass; private List args = Collections.emptyList(); private ImageFormat format = ImageFormat.Docker; @@ -50,6 +51,16 @@ public void setUseCurrentTimestamp(boolean useCurrentTimestamp) { this.useCurrentTimestamp = useCurrentTimestamp; } + @Input + @Optional + public List getEntrypoint() { + return entrypoint; + } + + public void setEntrypoint(List entrypoint) { + this.entrypoint = entrypoint; + } + @Input @Optional public List getJvmFlags() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index ca21ab27c2..4ec6f0e3d8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -18,12 +18,14 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; import java.nio.file.Paths; +import java.util.List; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -107,9 +109,17 @@ public void generateDockerContext() { GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); - String mainClass = gradleProjectProperties.getMainClass(jibExtension); String targetDir = getTargetDir(); + List entrypoint = jibExtension.getContainer().getEntrypoint(); + if (entrypoint.isEmpty()) { + String mainClass = gradleProjectProperties.getMainClass(jibExtension); + entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint(jibExtension.getJvmFlags(), mainClass); + } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { + gradleJibLogger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + } + try { // Validate port input, but don't save the output because we don't want the ranges expanded // here. @@ -117,8 +127,7 @@ public void generateDockerContext() { new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) .setBaseImage(jibExtension.getBaseImage()) - .setJvmFlags(jibExtension.getJvmFlags()) - .setMainClass(mainClass) + .setEntrypoint(entrypoint) .setJavaArguments(jibExtension.getArgs()) .setExposedPorts(jibExtension.getExposedPorts()) .setLabels(jibExtension.getLabels()) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 4f15919e19..c59c518a52 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.registry.RegistryClient; import java.time.Instant; +import java.util.List; import javax.annotation.Nullable; import org.gradle.api.GradleException; @@ -85,12 +86,17 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - String mainClass = projectProperties.getMainClass(jibExtension); + List entrypoint = jibExtension.getContainer().getEntrypoint(); + if (entrypoint.isEmpty()) { + String mainClass = projectProperties.getMainClass(jibExtension); + entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint(jibExtension.getJvmFlags(), mainClass); + } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { + logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + } ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibExtension.getJvmFlags(), mainClass)) + .setEntrypoint(entrypoint) .setProgramArguments(jibExtension.getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) .setLabels(jibExtension.getLabels()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 7e63c8dc19..371d4d09ef 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -103,12 +103,15 @@ public void testContainer() { testJibExtension.container( container -> { container.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); + container.setEntrypoint(Arrays.asList("foo", "bar", "baz")); container.setMainClass("mainClass"); container.setArgs(Arrays.asList("arg1", "arg2", "arg3")); container.setPorts(Arrays.asList("1000", "2000-2010", "3000")); container.setLabels(ImmutableMap.of("label1", "value1", "label2", "value2")); container.setFormat(ImageFormat.OCI); }); + Assert.assertEquals( + Arrays.asList("foo", "bar", "baz"), testJibExtension.getContainer().getEntrypoint()); Assert.assertEquals( Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getContainer().getJvmFlags()); Assert.assertEquals("mainClass", testJibExtension.getContainer().getMainClass()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java new file mode 100644 index 0000000000..44b6ce8f69 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PluginConfigurationProcessor}. */ +@RunWith(MockitoJUnitRunner.class) +public class PluginConfigurationProcessorTest { + + @Mock GradleJibLogger mockGradleJibLogger; + @Mock JibExtension mockJibExtension; + @Mock ImageParameters mockImageParameters; + @Mock ContainerParameters mockContainerParameters; + @Mock GradleProjectProperties mockProjectProperties; + + @Before + public void setUp() throws Exception { + Mockito.doReturn("gcr.io/distroless/java").when(mockJibExtension).getBaseImage(); + Mockito.doReturn(mockImageParameters).when(mockJibExtension).getFrom(); + Mockito.doReturn(new AuthParameters("mock")).when(mockImageParameters).getAuth(); + Mockito.doReturn(mockContainerParameters).when(mockJibExtension).getContainer(); + Mockito.doReturn(Collections.emptyList()).when(mockContainerParameters).getEntrypoint(); + + Mockito.doReturn(JavaLayerConfigurations.builder().build()) + .when(mockProjectProperties) + .getJavaLayerConfigurations(); + Mockito.doReturn("java.lang.Object").when(mockProjectProperties).getMainClass(mockJibExtension); + } + + /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ + @Test + public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockGradleJibLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + Assert.assertEquals( + Arrays.asList( + "java", "-cp", "/app/resources/:/app/classes/:/app/libs/*", "java.lang.Object"), + configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockGradleJibLogger); + } + + @Test + public void testEntrypoint() throws InvalidImageReferenceException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockContainerParameters) + .getEntrypoint(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockGradleJibLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockGradleJibLogger); + } + + @Test + public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockContainerParameters) + .getEntrypoint(); + Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockJibExtension).getJvmFlags(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockGradleJibLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verify(mockGradleJibLogger) + .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + } + + @Test + public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockContainerParameters) + .getEntrypoint(); + Mockito.doReturn("java.util.Object").when(mockJibExtension).getMainClass(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockGradleJibLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verify(mockGradleJibLogger) + .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + } + + // TODO should test other behaviours +} diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7597102e39..cf06404eab 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) - Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 528e1c8809..7cc54807a7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; @@ -25,6 +26,7 @@ import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; import java.nio.file.Paths; +import java.util.List; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -61,7 +63,14 @@ public void execute() throws MojoExecutionException { MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); - String mainClass = mavenProjectProperties.getMainClass(this); + + List entrypoint = getEntrypoint(); + if (entrypoint.isEmpty()) { + String mainClass = mavenProjectProperties.getMainClass(this); + entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass); + } else if (getMainClass() != null || !getJvmFlags().isEmpty()) { + mavenJibLogger.warn(" and are ignored when is specified"); + } try { // Validate port input, but don't save the output because we don't want the ranges expanded @@ -70,8 +79,7 @@ public void execute() throws MojoExecutionException { new JavaDockerContextGenerator(mavenProjectProperties.getJavaLayerConfigurations()) .setBaseImage(getBaseImage()) - .setJvmFlags(getJvmFlags()) - .setMainClass(mainClass) + .setEntrypoint(entrypoint) .setJavaArguments(getArgs()) .setExposedPorts(getExposedPorts()) .setLabels(getLabels()) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 6fda990e9e..8233883703 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -118,6 +118,8 @@ public static class ContainerParameters { @Parameter private boolean useCurrentTimestamp = false; + @Parameter private List entrypoint = Collections.emptyList(); + @Parameter private List jvmFlags = Collections.emptyList(); @Nullable @Parameter private String mainClass; @@ -253,6 +255,10 @@ boolean getUseCurrentTimestamp() { return container.useCurrentTimestamp; } + List getEntrypoint() { + return container.entrypoint; + } + List getJvmFlags() { return container.jvmFlags; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 478d9deeba..f59aa7fadd 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.base.Preconditions; import java.time.Instant; +import java.util.List; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; @@ -102,12 +103,19 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - String mainClass = projectProperties.getMainClass(jibPluginConfiguration); + List entrypoint = jibPluginConfiguration.getEntrypoint(); + if (entrypoint.isEmpty()) { + String mainClass = projectProperties.getMainClass(jibPluginConfiguration); + entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibPluginConfiguration.getJvmFlags(), mainClass); + } else if (jibPluginConfiguration.getMainClass() != null + || !jibPluginConfiguration.getJvmFlags().isEmpty()) { + logger.warn(" and are ignored when is specified"); + } ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - jibPluginConfiguration.getJvmFlags(), mainClass)) + .setEntrypoint(entrypoint) .setProgramArguments(jibPluginConfiguration.getArgs()) .setEnvironment(jibPluginConfiguration.getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java new file mode 100644 index 0000000000..1068674bb9 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import java.util.Arrays; +import java.util.Collections; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.settings.Settings; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PluginConfigurationProcessor}. */ +@RunWith(MockitoJUnitRunner.class) +public class PluginConfigurationProcessorTest { + + @Mock MavenJibLogger mockMavenJibLogger; + @Mock JibPluginConfiguration mockJibPluginConfiguration; + @Mock MavenProjectProperties mockProjectProperties; + @Mock MavenSession mockMavenSession; + @Mock Settings mockMavenSettings; + + @Before + public void setUp() throws Exception { + Mockito.doReturn(mockMavenSession).when(mockJibPluginConfiguration).getSession(); + Mockito.doReturn(mockMavenSettings).when(mockMavenSession).getSettings(); + + Mockito.doReturn("gcr.io/distroless/java").when(mockJibPluginConfiguration).getBaseImage(); + Mockito.doReturn(new AuthConfiguration()).when(mockJibPluginConfiguration).getBaseImageAuth(); + Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getEntrypoint(); + Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getJvmFlags(); + Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getArgs(); + Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getExposedPorts(); + + Mockito.doReturn(JavaLayerConfigurations.builder().build()) + .when(mockProjectProperties) + .getJavaLayerConfigurations(); + Mockito.doReturn("java.lang.Object") + .when(mockProjectProperties) + .getMainClass(mockJibPluginConfiguration); + } + + /** Test with our default mocks, which try to mimic the bare Maven configuration. */ + @Test + public void testPluginConfigurationProcessor_defaults() throws MojoExecutionException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + Assert.assertEquals( + Arrays.asList( + "java", "-cp", "/app/resources/:/app/classes/:/app/libs/*", "java.lang.Object"), + configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockMavenJibLogger); + } + + @Test + public void testEntrypoint() throws MojoExecutionException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockJibPluginConfiguration) + .getEntrypoint(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockMavenJibLogger); + } + + @Test + public void testEntrypoint_warningOnJvmFlags() throws MojoExecutionException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockJibPluginConfiguration) + .getEntrypoint(); + Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockJibPluginConfiguration).getJvmFlags(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verify(mockMavenJibLogger) + .warn(" and are ignored when is specified"); + } + + @Test + public void testEntrypoint_warningOnMainclass() throws MojoExecutionException { + Mockito.doReturn(Arrays.asList("custom", "entrypoint")) + .when(mockJibPluginConfiguration) + .getEntrypoint(); + Mockito.doReturn("java.util.Object").when(mockJibPluginConfiguration).getMainClass(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Mockito.verify(mockMavenJibLogger) + .warn(" and are ignored when is specified"); + } + + // TODO should test other behaviours +} From 46e1751ba4a453b8658a68b6c1680553c23692b7 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 31 Aug 2018 08:59:09 -0400 Subject: [PATCH 0173/2020] Log zip filename when blocking Zip-Slip (#912) --- .../com/google/cloud/tools/jib/plugins/common/ZipUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java index dbaa98ce7c..07ffb2c994 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java @@ -48,7 +48,8 @@ public static void unzip(Path archive, Path destination) throws IOException { String canonicalTarget = entryPath.toFile().getCanonicalPath(); if (!canonicalTarget.startsWith(canonicalDestination + File.separator)) { - throw new IOException("Blocked unzipping files outside destination: " + entry.getName()); + String offender = entry.getName() + " from " + archive; + throw new IOException("Blocked unzipping files outside destination: " + offender); } if (entry.isDirectory()) { From a7171278359c5f058a5ab8fc84f6436197f1462e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 31 Aug 2018 11:01:09 -0400 Subject: [PATCH 0174/2020] Maven release v0.9.10 (#915) * preparing release 0.9.10 * 0.9.11-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 2012e9c359..38e8c26a25 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.10-SNAPSHOT + 0.9.11-SNAPSHOT maven-plugin Jib From 1e7f670a97a59f92d76d2086d5ad278a397b8073 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 31 Aug 2018 11:07:32 -0400 Subject: [PATCH 0175/2020] Gradle release v0.9.10 (#916) * [Gradle Release Plugin] - pre tag commit: 'v0.9.10-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.9.11-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 40d318afdc..44f017a1d2 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.10-SNAPSHOT +version = 0.9.11-SNAPSHOT From 3694767a12ed86d0a28eaaab18a60f69c06d3ff7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 31 Aug 2018 12:20:52 -0400 Subject: [PATCH 0176/2020] Update READMEs for v0.9.10 (#871) --- jib-gradle-plugin/README.md | 4 ++++ jib-maven-plugin/README.md | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 3b5251cc1c..aad9fb7b38 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -195,8 +195,10 @@ Property | Type | Default | Description `mainClass` | `String` | *Inferred\** | The main class to launch your application from. `args` | `List` | *None* | Default main method arguments to run your application with. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. +`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -213,6 +215,7 @@ In this configuration, the image: * Is pushed to `localhost:5000/my-image:built-with-jib` * Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp +* Has two labels (key1:value1 and key2:value2) * Is built as OCI format ```groovy @@ -229,6 +232,7 @@ jib { mainClass = 'mypackage.MyApp' args = ['some', 'args'] ports = ['1000', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] format = 'OCI' } } diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index fafd5faf66..5fd1ea2ecf 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -236,8 +236,10 @@ Property | Type | Default | Description `mainClass` | string | *Inferred\** | The main class to launch the application from. `args` | list | *None* | Default main method arguments to run your application with. `ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). +`labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. +`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -254,6 +256,7 @@ In this configuration, the image: * Is pushed to `localhost:5000/my-image:built-with-jib` * Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp +* Has two labels (key1:value1 and key2:value2) * Is built as OCI format ```xml @@ -280,6 +283,10 @@ In this configuration, the image: 1000 2000-2003/udp + + value1 + value2 + OCI From 52075147a4f8ee65388a318ce0eb50057a89528b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 31 Aug 2018 12:33:01 -0400 Subject: [PATCH 0177/2020] Use --password-stdin for docker login in integration tests instead of -p (#914) --- .../com/google/cloud/tools/jib/registry/LocalRegistry.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 7c698d70fa..d2e867bc0c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -141,7 +141,8 @@ public void pullAndPushToLocal(String from, String to) throws IOException, Inter private void login() throws IOException, InterruptedException { if (username != null && password != null) { - new Command("docker", "login", "localhost:" + port, "-u", username, "-p", password).run(); + new Command("docker", "login", "localhost:" + port, "-u", username, "--password-stdin") + .run(password.getBytes(StandardCharsets.UTF_8)); } } From e205be050298be0c9ba04bd790aedfef59ef07e9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 31 Aug 2018 13:03:15 -0400 Subject: [PATCH 0178/2020] Update version strings for 0.9.10 release (#918) --- CONTRIBUTING.md | 4 ++-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 12 files changed, 29 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee9f0c4721..0f66b6308f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,12 +88,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.10-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.11-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.9' + // id 'com.google.cloud.tools.jib' version '0.9.10' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 64525c4995..b54d8353e4 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.9' + id 'com.google.cloud.tools.jib' version '0.9.10' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index ec074768cc..7b1281e182 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.9.9 + 0.9.10 3.8.0 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 5e0c798395..629870624b 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.9.9' + id 'com.google.cloud.tools.jib' version '0.9.10' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 5e7e33bc96..0cf1eef852 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.9' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.10' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index c822d7d8cb..58ab252374 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -39,7 +39,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.9 + 0.9.10 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 83d5c0e19c..583a10596b 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.9' + id 'com.google.cloud.tools.jib' version '0.9.10' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index f1b0975058..4e02ce963b 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.9.9 + 0.9.10 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 09cf6a77f9..2d86a39241 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,13 +5,19 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.10 + +### Added + - `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) - `container.entrypoint` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) - Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) -### Changed - ### Fixed - Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index aad9fb7b38..247f0b974e 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.9' + id 'com.google.cloud.tools.jib' version '0.9.10' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index cf06404eab..1d3fe59b1e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,13 +5,19 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.10 + +### Added + - `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) - `` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) - `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) - Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) -### Changed - ### Fixed - Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 5fd1ea2ecf..9ffe79d5a7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.9:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.10:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.9:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.10:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.9 + 0.9.10 myimage From 1e6cde84947be6630bf99049d4af28caea7431c2 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 31 Aug 2018 14:51:11 -0400 Subject: [PATCH 0179/2020] Adds Spring Boot on Kubernetes example. (#895) --- examples/README.md | 4 + .../.mvn/wrapper/MavenWrapperDownloader.java | 110 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48336 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + examples/spring-boot-kubernetes/README.md | 40 +++ examples/spring-boot-kubernetes/build.gradle | 20 ++ .../dockerize-spring-boot-jib.gif | Bin 0 -> 596754 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/spring-boot-kubernetes/gradlew | 172 +++++++++++ examples/spring-boot-kubernetes/gradlew.bat | 84 +++++ examples/spring-boot-kubernetes/mvnw | 286 ++++++++++++++++++ examples/spring-boot-kubernetes/mvnw.cmd | 161 ++++++++++ examples/spring-boot-kubernetes/pom.xml | 36 +++ .../src/main/java/hello/Application.java | 13 + .../src/main/java/hello/HelloController.java | 13 + 16 files changed, 945 insertions(+) create mode 100755 examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java create mode 100755 examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.jar create mode 100755 examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties create mode 100644 examples/spring-boot-kubernetes/README.md create mode 100644 examples/spring-boot-kubernetes/build.gradle create mode 100644 examples/spring-boot-kubernetes/dockerize-spring-boot-jib.gif create mode 100644 examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/spring-boot-kubernetes/gradlew create mode 100644 examples/spring-boot-kubernetes/gradlew.bat create mode 100755 examples/spring-boot-kubernetes/mvnw create mode 100755 examples/spring-boot-kubernetes/mvnw.cmd create mode 100644 examples/spring-boot-kubernetes/pom.xml create mode 100644 examples/spring-boot-kubernetes/src/main/java/hello/Application.java create mode 100644 examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java diff --git a/examples/README.md b/examples/README.md index 92930223e3..9c6c506234 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,10 @@ See [multi-module](multi-module) for containerizing projects with multiple modul You can find usage by Gradle or Maven with SpringBoot in [spring-boot](spring-boot-draft#Quickstart) --> +### Spring Boot Kubernetes example + +See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). + ### Micronaut example See [micronaut](micronaut) for containerizing a [Micronaut framework](https://micronaut.io/) Groovy/Java application. diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java b/examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000000..d475a89ce1 --- /dev/null +++ b/examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.jar b/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..08ebbb67f088c53eac9a4e2cb019b93f69a1e49c GIT binary patch literal 48336 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 H*Ps3mB7Q<; literal 0 HcmV?d00001 diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties b/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000000..00d32aab1d --- /dev/null +++ b/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/examples/spring-boot-kubernetes/README.md b/examples/spring-boot-kubernetes/README.md new file mode 100644 index 0000000000..23f2a5dbd2 --- /dev/null +++ b/examples/spring-boot-kubernetes/README.md @@ -0,0 +1,40 @@ +# Dockerize and deploy a Spring Boot application to Kubernetes + +This is an example of how to easily build a Docker image for a Spring Boot application with Jib and deploy it to Kubernetes with `kubectl`. + + +

+ + Dockerize Spring Boot app with Jib and deploy to Kubernetes + +

+ +## Try it yourself + +*Make sure you have `kubectl` [configured with a cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster).* + +```shell +IMAGE= + +./mvnw compile jib:build -Dimage=$IMAGE + +kubectl run spring-boot-jib --image=$IMAGE --port=8080 --restart=Never + +# Wait until pod is running +kubectl port-forward spring-boot-jib 8080 > /dev/null 2>&1 & +``` +```shell +curl localhost:8080 +> Greetings from Kubernetes! +``` + + +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run+a+%40springboot+app+on+%23Kubernetes+in+seconds+%40kubernetesio+%23jib+%23java&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot-kubernetes&hashtags=docker) + +*For Gradle just run `./gradlew jib --image=$IMAGE` instead.* + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot-kubernetes)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle new file mode 100644 index 0000000000..9d892f8026 --- /dev/null +++ b/examples/spring-boot-kubernetes/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' version '2.0.4.RELEASE' + id 'io.spring.dependency-management' version '1.0.6.RELEASE' + id 'com.google.cloud.tools.jib' version '0.9.10' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compile('org.springframework.boot:spring-boot-starter-web') +} + diff --git a/examples/spring-boot-kubernetes/dockerize-spring-boot-jib.gif b/examples/spring-boot-kubernetes/dockerize-spring-boot-jib.gif new file mode 100644 index 0000000000000000000000000000000000000000..0b9db9cd5e3931ee9d937c9f065d1ec8a2021146 GIT binary patch literal 596754 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

^14~5deO1#@tn`ik&$dk%BXi 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�Lh>1wJ4rLh4@2STWV#5P?! z=;9PDc|De|Ec3j~3~U@g343~?*zPB&wWBa408K{N!YF!3GJHYF7NBW^QE0xn&&Ts# zAl~4NWQ~vL>g3NXl!wW|ZA9TkZv)@lo8CiZ+;cov*;6#}L&>Cgx8hSsr0Yw5J`M)Q z7<<(<4}jh6fmGOEo*#}xCUxszA)*06!u~TfOd{!oTpksujG>(*_k;|EM}>$6>}9;` z?D@ui@{0%o6gZbxrc}P@uS|mJ(rhI`lRHdM+wqv4Kd$dM+r^nOLD;EGAjDQgC`BK!E^|QdKo>Gf(}@%m&!Y)G;s0BVmhA8o`b^;G6oILn3hK2=NBVFOd5y>h zKZ#2U9pwGINEv}p+`(8kVBL|KM@_}Vg6UuN&&X$L9jAQstL+!gX%pk5y>6_$#0*VD zsKn!^H==5?UX}>w1-B5YQDH>S-W{9^7ms(%jdN31k}|{n&Zo|rhRF3PFOevT1~`-6 z5oy&laDIx0e8J!~vz5&`r8s{dL#xX7W==wpbG-S7M!(=7s}QE#G&nh%gtXc*-yM z3gJ{W9?pepU|~0m)7e?XB$%nrl!n7>6kE&n2|vPCr!9o0(n?G{L|^xq1#8*X|fUp zpxPBz-FYa_(1^9o&8FLnrb@u1npT76Ahc4-BI!llliw2eQPoYC?IJWd9e+FpkeT3d zQ4cSgdC@;UHW7}Is(d+_OzOI7xiNzj;g@vP9k*52k3cSydg!YOZDNsVd?hgascVRd zTy9TO=%`b9-$+eqK_zyD-s76pA18E314+NWWzcoiMzE#ZKN+0B}Y93vxYM&=Ak53t?|Boyr={qfpqkv-hEe2z{yu7 zA6yZKyb|Cws z^K5vs0)5+cDcYCeMupsGh2~+ua>Gm(zC6M}<-x!ujz%OsP#I~V&7~MAo{eaD(=t8ZZ4$h7HxRsiG^-RlC)gF^lmg?G%?3mx>e2FA6gup>?^FN z(blGqw1?5o`h+d-15VC7iswqQ2Y~#E7+9tdg9MxCmU6KDz4C~!`&inC+)p#5es~e& zMMRG-2^i@Ve-w7MmG-5ulTsS?gHgQReqh+w@rbXN71R#EL=HpF^-K9QkA%1n8XMZLJ4U+ot8$v~B$O?(mbkyhfP-TFlHdj5DG&63_X zN=^~uenxuw=d5J%Gs zvT#gIS5FoPHA;_)8NM#X^Atzf909}mL{w4|Ajn7-fU?{Swf4*x0~La4jY78WugguC zI+)@=Dojf}S#Y1=%Se~7>PCG=QWu}6#Xefk6lT@n1X%`jQ;Rmw()b_U;t44;a0GDq zxWz^-B5QTktvVD7wrccV>ys(qT{8;$f`z;imOzN<1sl)Qt=nq%8<6E7b6o2gzi)r` zU6Z0={K{7}0{Qb`#JR0A7vPjdcu)9^eXgaOF|pIjbkVC=XRX2(9PE zJ}0l??4zFO|3*;ve zlfy~mXz#v5U&eF!XBDzyUM)>MORI%`t+H>%xLP8|_Yc7E`I~DVCfPZ|=8p;eGm$4T z@Ri=tI%L@WrKt0rU~MgQ>7JfEBh~$qjy|L3;0g;%_*KqG*?XB^uDWyQ!T!Tj$sr6b zNG{S}?F`0g0Um!AbL0}@7X&Q1|KkuBt4{`Za-7?}l730y!r7VlFj?>{XFI}W-|p40 zXvpu1WwtK@vckVncBB;7pNsd0SHkkq?Cd*T9u}!+#djTrCKd@I4LrWZE`*t<)*-7_ ze)lxK+vpsV9t3|4>rTTQ+aP1?BU)N}ODk@`bgI+~zGx%>jAxPtW4O`pjErzeln$mS z4A6fIrHx1_Vca6K*&Vjry%(j3{`_0yW}PB9*LmquFMGdYdB2ACpuzZ{CHtUb`JjvT zu-Eu-F#B+H`S1tr(TwrY&+Mbc<)ana;|=5Eo$TX-<>SA!f6tBozOpvG{f>D>dqUO! z7b6f8v~nUEdP0;#kDGJKD}PFddPiw;#sWTjzCyuDch1mvCb~i|uyTGEaxNE0FO_p~ zDtGZl`b@>-619PLX-G$@OLz6B_tGYZ!hGdwF67GHMAzFQ+ zaJrk0-s?{}49P1u??Y}1q#3evZna+B)&!nbnA}wX?>eOEo9XV|`|k!(6g?~Vc{%qp zbYv4b55n>f8~G0#D`v}dJewyQ7W&CX1?uX9j0|DPvCKMF7GX_VCciqX^X zwO-WXQ;B+czw+*>v|9P!rOEBnofCRd9Z&b~61(rZdn#j?Ot+CJ_n@cYV)rZmT|q^m zL7}0z*Z?tdTtISd6hCKNsba~clr4xdz+fMww-BwjOk&$@~py?8m>@Vkwn)( zhoS%2$g>+e^zU{+U40q5D9m?eVQb<&uU}*FaFUe2`pP23Nh8Ghn*_W#+<3|+n)cP^ z=q5pgZ!Fvv>&k;$j8jZXB=*g)U3Y0pK2wDa4rmFya?0l8yA4eg@4;oje*1{<&i8aC zXrIOodM2^4;}I`+SM!yb34k0L!30`pU9q@nSCzZ-34D_jwov;`x~r&vi|wl;@lslnNw8t zyV-JT;H#EY0M*U@c2$l^0xpd^(RdfSA#e4|7avczFyS_jG!IUbeSa)4x}-YKHWyV= z2HzV?`wr-$(8zNUXoPyXK^Tm?DfsBBCqW^2K8cZ-cmb2SV30OHLl~VA>ZyrJZN2~w zp;KTt51~qLU5z3xW)BU=Q=c>=;*}t`h~!Mx>yP9Mm|TzNQf|$Qqt(66jpYgOVg@o* zFBHV9Nh~k|g(OOu<4BjqiV~F{p=&APnb7T6Vam@eX>#FF>!0k4U*|z&k}(;R_zXT< zroU5YV@-5VAGQhm;5@Vy_52pPg#qKXD^1lN5x36$$XQ% z)7Qpn6*ExtC*8@j)IKW7p`)4X7j@~&C)UBTDp-ws)Snit>q z(zGnOqgtB(eB5vn`r~{4r)4D&?a_TXgveNID2OS$bv;IKxiu(Uj<#*{Ov1Qr>yt%x z+jf@wa@$T$2yOdrL5gwvUP(cA`+h~ka{EC|FKx$R!wkx}?ms?QB$V<=goWIl9h^87-5}%bylGomY$QE1myV@TA+W6+=wE-|Q6RXb+(g!({=x z5d5xYG%y^NzE=PDdowT=ANjCR@!TIoO%MG1FO>Lt7X*L-fX2D75EvYX#^niGgQ&t} z?WU(nM6i5+1tf`N0eXq$CeZdG`Bh=uoO;C2f3-3=B&C!S#TR486FI{IHKH1KSF zkSNgQ9Xd55LCnw#337Tg34?CXr#|_Hg~69>jii2knsP3k$QNBQ`Gl|JyE%dpyyQ^~ z#KW2hw)TM_D;!nwJn~rj%+W} zwlk#*j!g{4LINP%2pGuIOOb}-B(#>l2f|4oLW+RnF3WVIS@HqNGWZiB{Qz_~k?9Ig z+;ss{BnD?&E}Qczkk1sr9t2gand8ssp)TG&CQp?l)f9ctyonye*G~pB6+i-~c_tu;rd&KpPfdg5lAC8MUA`$l7*eRMsM8+|)5+5&*qVw4>@otma!H8+q$$7o z5I|0ZRx>Aeb>^Q#!=3%9g5OQWD*ffQHoxVn=;`INt?*(SmZIkid;ondpdXR-GS4+gfi?}ZuYHMNW>pqIA2x2HBsBH0V%sEfelxcCkNn9 zr-&JN%EQtu@I}KKDqpnotT05i>`p)32-JoFKPFM$uX>(n)ntYlq>*Y$eq>E(blrJ~ z@9AK2o~hc2Wz0vzVvn%@t}8S+XTxBY+2_`YWHWN%3qAk!mWFgI3S;@$tnL!T8qG*R zzsnXJGPFw8oAJ?~o0;onCMM^$aHG~C%v=Mh{=PGaOR%~p(Ts8$m|M`P1|^_Sxz{{= zL?9o;U1IvlHlADPd*%X_f$S$KJUuW-gODt!j|&ZWUkGLAYxvx?o-HhB3i@_g)oh#_ z4BmAECl{K+^AiySeC(7gK41<-`ViXuZhWgbK{RcAG)~8l;o`vBf`RWP?T68%_6}~&t*N;2fG~OM(ALquLrJ>db!d?ak9Vo3D2wx|d zn7V|zSFB=nrT0>N{~P`IVuKR3++XqSG$BNClt$5INX6L=J>kBE%|v`uJnKxeMRHpp zM||AD`8;bwaz|oC{D<%Hc@Eltxm}5=80U)urk8ulCK5AQ#}_4XFZVTbB<3odFDoow z9(+gnigz^~U)FTR9lXPM@oO}Izpmh+%-ZC|;$7NR3tRq?Th3kQs`E$COPlO7Z6Q_tt~5gP*I zGHYDO^3)}#3Nc@L<722M0w6I;D>n^4UR@c=`v2iIzpxT}t!rBo=XLmhjeH=r}XvU~e ziQFE!UGTqQEwlpJ6|b2{+%T(u`Sv5&xKhS>22w z)67qakue&!{Q#&Zj;Y}qttwV%l8BsX{!EXvp|D!-YB2o2}kG6{jO zKC2e+>u&G_Z`7Jvh)ZpVXXf)in%9oyS5Oy7wimppw%`L9y}$}J69bPYlr zO#v${#xTu+7%k;3ZA~j3gDo9%D_#37T{o+@K3i{tt@NU|^pdUgGf`Xm`BnzyTL$%3 zhV5I1Jyu2%<88!7z=fg*9GudJjD=w<6Z`D8Tpgjw&mBfG_P^8~4~tq(5>Os7CKB6a zVN*CVlMk-gLNv&NBy6>#ZjY!@&{MpFVlgmn3>*e*{h)%;i2wOD1sKu;Fhw+{>39ow zTTE}KaZnL=f2#Um*7^+w1Ow3VN!#=gqDCruyYTU$Fk5>Ssx<>Mj!f!F!_GMal3D8y z0(!tkP3%n#EXxu{P3mZt;+9HsoAA--Eo+i*0N&U7Lm4IZuqBnF`J8STw#F>>5S2oP z4VD-py9^fpF6INM)F-XqRf3q?ABMfV2nhY<-mlH^`IkW_3c)N64YnTj^4U>|`sJ1O z`E!vHFhid?NsEL$6Y7D)(3=PJ{ZeO;#}*qx!_UBMM56g&YqU~Y`-~=XL(w_(>&+rQ zFo4ksc3Z^AVrWZoGFZ33w#mwG^cUEV?a;AWKNG^{XgvrBW%ru&Y+LCu?1j6X!ii~^ zVZ?pNnl^0@^Pp&+PXv66Eumpxc6f>#wqlYtPagnnRnb2Rf)yHr+1MZUE;|`yf6lju z?AB~5Dt?<-tS2wy+ZF*VMKte~HCdJ+_CTx)TI5Nx0#0Q%t3lRc`_6HcNk>5`cB3+R zNYC!QW|vg&k!}%+AV{q}5mJ-wBqK9;6m;E3P>KRTlXf&A`!$R|aw&D}JP?r#^Lq5u zp8JNi{lw~O<*j|?y29F)^hj190&NngFO=qQBvU9N<6CXlx1~Wy0)Q5kHy{2qRDyky z&}rm}`sRcttmPFFQyMBStpM%sA znN^2jZJ}dwyCDW8rEot&~%%Fx@Tl<&$YnM|x5q-}Q zTD*>$CsT_>2&NSdr;~JpnsE?quA12VLEh?9%_Big=@g^U+|7puO?ez0 zrAOP4H{>29QImmt1% zBVORF9{=H-g;BY$~cLNfGckkdQEX>^X#di8p0F+X4s#yA)MEe#T z1m_<-zw146KN9vMnP`SeLV(7et|3}zrf&dt*{&;QETZkGa$?Da;YZvK<10R-f<~)B z=R>GG4SMS=!>2!)dh8lxqt*VCK^nEs-GQwC@>8j1DBPHlKJ67v1{&Meqs5vO3{+>& zJH`PWkri8uOGQX(dkw9EYs8ZSN`fdM1XUj->grtZvkE`KDX@%lna-zAC}~ukOa# zPT#bRQ1DlhCeOsr&o5zH16S^_l}%~G7#NjM2!V$T-GwZ+tt>WBPR>JK<=hYJOF4@KV##Sjmrm zP5d-9y%EY9K&7hZjDkXwkr^<1N*IU3Cq`FIGZLg7zu|g0T#q->35dkQN!gb+0fp}I0w*uqehG(wJ;jJ%mJ^fS&N1!6SMPc&Tlt_KQ}Zp1 zSCp2up`{3wy zyjia(dHI-vs~m`7OrnYg?^(YB|5aj&lC;5`8+mU69PLFsjoiy@-U^TcG(7LFS}OGn zmprWGk`n4fG>c2XUYBnNo7=}c+Z&JDP2u|82*VC-y+Q=fC9gr_dx5PTv{-J>>#y39rJfwTw8=_dQWywO~xes=i&%}Z83jE2GbE64l}9Sn^t0N zFH*4@Z;<9kEW6dKeMEwa-X>wRdvZviQmAxI4*D4_7qQmjQ1uqCSE2ynIa1_m3er#HRD6{>Mm6ynL(aROL415 z=st^T0|r@le|{S0qK#^pFa|T=LnlADm3Rg#l>h@RiA<&@9NmH@*)G%D_0c5!OJBE)h?-H)F( zjx4n+%OX~@bNd$l^M5y&4L+`0aIALC=W88$2J|;z=(g#-{L^w4%8uDa-jmg8k8{6G zU7?8)6hNfJ&f2aSzXRCF3|9~Z~>Zw=m2UdrRg6xp$bJ>_H!toe(NSf z=bv}z5&af!zx)(R<{Ug1AHoN>+(z~HC^2MAe$h9O=YLLTI@vP_6)gOb6_nu=Th zH!ZjU?}d3dCAvT&f|@b-#ra`g*x2h_cX{kvdsIXc0Nv}rZ;b4ioCnM9WagO$nxBnsET|iGBs8ymW1iMeljpp=wD`)f|fcs%-aAt}6TZ z%PIN%wpL+M_ojo;YYjz~CT*=ZuY`40aJ;+4d|bd9$`iV$0-CpcLIkAj&DbSuO{~~T zVQ3<-R9dF+-Lh$h6H0&_)(!dIv?P}QRFB?#>TV|4m#Wg7N0ExdnI3Sj7L{|h6-jc* zGFRl}2VU}Y6}(aZSKQ*l;;R4@rm5ZBZ|4+rsNuI}5+bQU#y>C<4L%R-#Sn_>*?fR@ zQLA|j?|_&^+$)w`G;%4=ZovNpAV+;gz@C`K>;Q;YmyBf}Y~w(^6rkh`ObylEX8ZXz zUnv*d4BCrQ$Wt(damjHZ8Pfr1H@RV@1*8suUAV0&7@CNz0Ql!Ft`cK_Yj0^Wl){}E zW1+`v2aUrTamEOjL&coG2GDS4;=*mJ_n{Lc{i#~F%B!iWBdkv!>9%616zhBKH~SqA z$Jaq=iW#}QrYZHhC!|nO$)*}n!aGC#%%&ly>a}VsVVGgpMk}IcdWLSOo&F199*2A`8k1f9YF|-7CUvskMD=9j2F|{lUiruXB)*nI-fPZC9Qv{N zt?VMvRyMwuBj4K*qRsMC?jkoX!9jg!X3$sv(SqS(ZMnu+$sciYp-*g;#9!#CLTQ)i ziFjxWPsCY3Foh?Sg31fVoveePCVa-g<05+(t0Pmvmv6)FHFCEun}o|A!Q6kA;ucAQ zuzO29{ z!@j~28eFTx(*|M%UK3NIoeYL1gjzDQh`&~j2h#Jleqk7Q7e_y~v?^Z2VO7k1tr7WT z{=4hRkkvq>C24Zz3w`GNJE^QcKb^i6WW}a0Ji`ABk@kFMi&N2M)bMdSP1DSZ3*sUU zzcTxrQ)wwwB(7$!JCi8{91?D7Q*-v5$yx#1i1~g}b4{Q5@{~N6m}^t_^pPPw3bm2k z5`W_}Ka)+$WG!{s_9ozJCIKlP8eSYCdwB`B>95UT!xL1Ez$k$wRY4nQ}TsHEhSR8?LkUpe&nG>y2g zT8X*YOkWC;Lg4wTXc;9nwRQCkjj}WtfVMV@CRt$C8`8E`0Kcmn8W0#7ZawkiM*|HH z07B_8GxxKzgucBEszKhyKGwJafNn1x))H(UxM8<4IRBbIy19i@12|~GSd~hm4zviL z#$%@xomFtR|GVWP!%y7(vJA^OC&5E?1%n9&619NZZlz!`R@~CQrfC2sh?j@_sc+U7 z6iEJdUlP%y4MaivltgUnVf`2+Ocs`I7P9><&j?j1e1O^wijDIrD^pIx4Kr^wf$0^@tb~AmIWfS+))diC!b$2 zD7e7{cZ!yPq~RIoRwwNIV&V?&KU`(4KkcPQGn)IFzR zo53d43rmx-mSyn?C($UA27{e*0EC?0YldX9CkVIb1Q`@2o{B9Cb_pjC$I#tH;^T4- zA+*}qSy5mNjNOQEPHA%_KrSz}mt3-eSq z;U71IH2xjVo(27a|3{`|`1H03?0lG!MpR^4EGE9BQ7`tRJI)O(32eC+8mduT#wrh1 z=U2zk6i<(_C!%zrNQ65!<-5NigfCheW+cV{$#|x)T=<4^LovDOlZF9N0({BA0z)tb z;7dw=Rv?lDZU^HQ6D%a)jy+K*Vu`(#$x~tGM8k;!?b{VKPE$ry)bs$@fl)laOTit# zOYN%GWu{~jh_q|>2pb@M2m%AFspmCuJU0qyCAwG@Wg4;YHMAvxJ1)Y}^czla8#5f` zVAEhvbxgG(HGEhCku@|b+65&7s!`%9_Kp3*x7}!cB5OxMs{%=-Wj(sEVz#E96CR=r zU2GR3<7#%E9wxrrqKev)+JnYeiT%)8G@=(3#tojHl}KFbnaZG7Mn=U*@XUy6KSnfH zL`$L6H|6~jpggrH1N zi{D0&!cv~rOSM{fJ0qpKxCWtCeyp3MuLW(!?Q#d47hVdONev(>4gUX%swiA|;sITd9uq z*j(_Dk4YR!+$Kv&h+mMmw3{KE2xfkTnRRFM=RS{&P&iJae<6*SGhdj&^p$!qKu)|q zND^(85xtIT780BfBo8xVrZJEu5S<+OhYt&u*^AzZ3`!BLN3aM^h4Hw{#i|IX_{r2+ zS!+)YpgHEI+>?i6MYlcsd2%fC13OxIyHCLJk}ZR%5X3bs@P$Qe@bfG)45;sh=w0Yb zU3CeTSDIF6({ukq%b0sN1j_TC)+BqEB5O@JsgP_0iQbeXbPiRDMJTL&?ORBaKG0_- zywrrEEv!M1cIoeM?QNC0rs&f3CvAdv^0ALb#Y^@S$ee5AP}mfk+BijRM&3nys_bny*$pb#-mT?S0Ldv}OcKdLb{khDRa5ay zCs?th4~!|mTikBCt(EItW!9RO+-yp#QZbcQJfS1VD6_20)f<+9bkDl22lMaD@?r)z zL`SGrn#Y7-6D9@)rr&XbCQp-3}xnMRpaX-miOYPmN<@4+ZR>Q@AU+!|Fl6ts^W+=7eWSxo{HTb&g zjp6Opv0=qLP0!fn=mFxuVm})SsLqTVh3W@|cBfnvkH{ya_!}=GkB(>AAACP9y4dG+ zeqZ7?W{)tEl}UTajHTgC0bgcir3YjiGLj*&S2p^>#R}02Wuo**y>*!q+}xtvN|@Pp z)|=gm1tYmlvX=eETLLbRR$=$PqjjEO~=I5#Mf_mLwopln2ZDaDQ(i=2>Iz+QEWghF}V@;Sbx?U{*|4 z(UAvnTuZ|jv^ffEDH-la^4J}6WvU9U#x42Or))EnQ?}{T*ZI+-@D36QakIJiL(gKK ze|!n)d97qLy61IH&E^8Bx@kl*qp_|!N3A0aHPIxIq^3XOJm`*pGk zqPe!W01~l8$=cT3&yhDgM~bxpxM#^}XyOjV{+%3JCzQ%UrgZk(xa+#Jc=wd z3jmqIvz30pUyYXd(K#$X@Eb1CR~(cQ+R*6Z*3J3-q~}lKOUCUbw{900bwS1=E zQXCp~-0$o~MM|<^zmk6B<%liE=ON<*Xib|3la7ScZdqvv#@-M(8yZ`Gz zw$B^F_A!QnUjF&(!G?%m`?xkfm6fFP@F2?hdBNtz2#>qcwUb$;Z}{KqZ)zYds$-$6 z!Czncm^-M(xZaA|93A`{!(O9#-SowG`ngAAtASG7k5Brl@3GrD0(q|Gct09>ssY&NQ#Xm_dK785G68a3=Imp$$Vx zX-D=a(OWzXn_2>2R|HT{1}9m@G3~7xDpADONF&I7hZOsj$6SF90l8W)0;lG2E-A&d>@|@KW@S@c4@$!hr;)?igQfeR$&% z`bcD#Y7zc`v_Y~34{VcFC_13{BMfrGpvW*fh|edY;FfM*KjE++3?%mC_@$prUTFIL zWnu8!jUc)pS(|iKMTYOpm=C0S8)N{I7i8L!Jn65VQO;=Zx#46CDH09R4q@Q8Xa!S> z%HS{ul3zF`r&1K9MV?#{S89f%V!Xotp@$9XNF@#FNMBMq!NaXEXd}aD#@MZrJ^k}o zsBm>y&lM3&VqO+vM(hi58u`Ifsd7B69uS1IAT+XB_qFCi$jCMo3>v710g=Ox&TI^9 z^dNTr7w^{cWUN$u98%3ZM_IWlKyg;Lqx1*i;;F`cpTf zh4#ubP^~r)BN1?2nn+FIb3qW{JtXYQ7fTrhLD(kyd$@zFUXr0lhQ5Wm=tX{SA7!>{v6YUR+JDGSrq=F$Rim2?Labb9|9lsFUH!-30*7}&XF=`n)dbv$PbCN z_D-_;5M;jd;j5>OpLbfIw}7-hoA-s)pHGU*VxR3m@tTbBCZ?o!in@qhaexZ}8;b;2 ztFKXBsNRsVd}MMXKcvtqb0I$_L?7Y#>ouW995^b8&nCJg15(eTZni6#4tBJ{bomL* zV7{@}5{c93`yiwrL4lEZgOO^;6bT1pMV%^4Q4nqvVzcpQG-cS%+Oe1AVuiNKADdzE z?YaorU|BtBL^!Q?*%5jq_T7B-1fWsplj1<~?kQr?p$>G>@Q|~dKO!=GN--F(^$Y6q zbF@N(OueCd*(#PF1eiC%RjUFWwQCm(G4}pQCrcyS2b* z20&>8$Q+-+MiD@&!`vBx`x;6u*c!S}Z;Qv2ueJIS>>~5*b-~c3w^p)J+g8Dp2jFXW z(H*9XT^$CFwK@5QV>+^gk3H+iUBiP@<$@30SeB0PRQjMOd2J62sq-du_1#?!dq1o9 z{uT@YmdkVM|FNGtx%3QdJVV{uq4u!AnSq2^|=%4KKrL1(;4CZBdCN_|6Lbl*sw^fnSEKc z4^{8$sv+g~v$>RMz-niT()MO_f%0M^Hw{J+iVkNlMak-)7Q11JvOmF(=ld_-ZBI z-BM*ma#N&XKi*=~Yyg;8!gQGU|Lg!}>dp4>1^5jCi)Zz<$*~Cw#kH2iC|RZFrMxV; zd4JJ#9^Qi9Ho|z4>zEMYwZP|mllpoM`c|N(I*dB)3gbC?pg$~kak5ShyFTq!&VQ93 zrES{uBdAFns)ES}k_LKxk}3XB=J-jcS%{FzNd-(NZ1B<-ENG5ppr?rcypB=y3s^Ks z2m3kCQ)H`_+aZg``Oa$~)ay3283&)3#-m|8t)UCc0lwLwGznNf1&#IE6m8TwZKfV3 zSG57!1D~s~B-dd&(ClIX3F)6Sh_13O?Q&V|U7QSc-Ayl2{T_c~+%UShuLG2*B%7nj zE^7lX<}+krXmp8nf_xywp*-BR8kWh%_qtWY1#O`{ZPWTvIR`EI4%TQI-BramY8Bbo5QY4&tQ$4PAPK52Y|ot%_ZV7G#+f}Ssx@1| zHB!zoQzO29y7^Gb`L{1DDWxj2u=-2bv{WbLB>Hr!<5BrWWq=(pF{QGLUsI4F zd44VN&N9i;a*+31@!F1*6g3vsCf>Wo5jUW6VJX?GzP=ULxLBP26}~edoAn4{Gu#~+^{(StE5n>ntgg$iK&5U!WrV&igF zlD`V*C$5M-lTUvCU_3$1UA6l~Tz0Z|p7@QKpvFntOLVx(Y4e+;SGKYI5DpTYKge;v zscW*Tnh{BUQ{)YG|1lTN3AJRSyxYf26Q-yrC{1|99scPTogRx<=)yTLY*BTiXSnWB zGIc;aQ-4f@NVHYhhen%Ebqx3S5R=WQk^wt4@vnAoVUN*9Z)qY?lW7w>hhOS6W||ti zz@~F;V*->yIVpLQRF?Vmn|*fC)jw_Ltz@T|^L)kmrD8;EiFkSWuZlVy)ZC;=JJt*-3+IPwOZ~#l~uMfEeWlq1UO-ua& zI0^W_zXE>MKR_D)pnvT&d)NEB`~mDwpWF6KCsp~W;|Z3~!|3?>jPl&_73h~^)fb(< zB`&RBZi&Bge3p7T3*aZ%RpSMjmy7Uz6VY87*@bs2FR@48SwwyVFTdlG82O`yzjD{R zAguZ2oeM^hKWWV$ZlVovciaMqwxI8xBL3xSUQ+4;X37WH9Ljd649bIz72tzIOWv0B z6O6Ie?(f#y!iepHOy*n5{II+n5NTIn#y+^tx5KkTXlZon%fH&mF zh2%z1(l7~}gG0bE$h4Snd}AqbnEQm22w&s34DhU{C9a)@^=?Ce56GnxMF*QC^?^>* z0i|(;YYW%#mYB$$tl8*O4=1-wzHe^UvM$#Gp*?B}Qafmx%E&R?o$YRqY8vL-XO8+% zOJymJ?>n`y{NdSkK=RVUWDmPYe_%G$yND`5eK&z`w|;WxqKLJ{U5)AWUUvKj4^p-9 zLX{2(vL6RwTXjyH{uw*!uM zvyb<`A0I9sA3q+S(EdG>`g>vg_bTA;b@t!e?|<)?|2{tc1<;*AsXwC^% z*9p$b2`=gcpYD`U`jpt@lr->^Jm-|M>y&!slooYLPj|*Bea38Z#u|9Wo^!_8^+ZZO z<3XMA(VYuOp9`6siv*sF<(x}&olCBqOQFtTPs`=f7xE?-3V|0&ITy-Z7pf~4>Zl7i z-KD1VrMAhXZs4U}&ZR-urP0czG3wHk?h0itePwBKWgU2Bn{#E~b>+Bn<&3&=q5J11 z{m;YXpJ(7dubh8AUH|-6{so}^A?U7yrLRLxuEPVbBXh2!yRKtbuH#YHiF7x~(l@Cl zH|c>l896tZT{mA=Zn9A~xpcSr(zk^sx5a_Cr8&3dUAL7hx7DcITDrS>>AOagyXL^V z)||WcuDfq5ci&NWNV@wT>H9vD`+>mwp`81XuKTf-`w7(jB;CWb^uw&l!+hYwLe9hQ zu7{6r2{v->I^`ALIyA^=f zG=ImpZ~Reod~lb<+R1$keht^8x~WqF0Zq5$`sp)5K`r-`hM99Bq0}jt@?LZ#Opb3I z0C*K6bOl92lIW7QB%smvGqr?-y-gdn~1%Ptr0041O1CIPFomHgIP2$v5Z@8uo)jnjHr*Uy zDkm(5nsp>XFm&}%m2%qi(?a(fLTlB}f_C}Ot#L|FU><4}(!=Ss zjXvoPqhDrr;p|uieP=Y6Gbd)W*b||w!1>fs1V|9;3)bGCd=>F22zjz6^k%xq_CKId zy0%)I``&z$?_1*UU~U6?dEv2zdkE2}dvxfZv9bgYG9iM(h92{Rphuc#TgnjOgEq8I2sVoI2j$P2iI5x|{wd zzLplNG`k)YA(03Sw*n~Tc9zy_#RIp$Fy)G_J2U(b0AfI$zW}-kjnJe!{{rSatGW*! zn2t9X_-f&l%Z$h^5&tY1PzD1NfX+Q5pqm2*5OEVxl7YT5qJZ_-Tg!@FdSRkR<_>d& zK?l1~&&UL^81Vu8bkop0`l`^64kkpj?zGjs{1VJ-WE+#rGR-WLwjyp43WNh30Sc!$ z0mwj2R313R7!=+h$rw73c)_+f6}ssw3|a}HPMp3P2n7{B66Od~^iw zPEMT^8a`3+Gg3Dv;pl)J$$PZVE`*%b2nyu%bdgo@tj5MDBt_PU{|-75)KXqGr6N>H zt@^eW19S?L-g@mdizBmx3G0AphS3oBlrQF(~qMEtUal7Mk)B_rG< zP6S&x0oXth6q3*}iJDRQ`2b<3Sb#Z}Ye}H*4-#?u?Xmw+0el9GTnovy=IDwj62KCE4Bi%_pvDPmFbN#|0^gVu zj03Dp1s_NN5kzSOu2};gf$T{Y-j^o(VXFuviDN4k=f;gki0r_@_G+=00V{-Kwn%>6|p*`88i4aYM4ll zMOfOpviBL_neb=Mxe0=XBZF!}F_FI{z%UQw03dDZHb=mR7r8mF>OtWq-h|ru1{0}# zl0=-vF(>PU0u?PDh?+Q1^np(EhxrCaCXEb3q27x zCECZyIBifaBT_}nmCv2@<10@@LOhY#RHv$@nNgMMRKvkK3Bcf72~a4igqlm7hD&%< z2rJ3B%CLwMz-iLj;V0cG!J3Ul09>o7FQtXhc**IG0hN~IP_fEQf)0LjJ!3z5_sC^M zA!-8kYFT|zjE^0`qZJ|7NC6u`!J2DQ*kk7`;9`nvC{1-p#!`U?-WK0*|)vt1%f1le7m45 zVJe%-VGb)HP-4j(Ch_>@Qb&+!78_wJ=6s1>OX1gWN}i;!hifPg*%jfE zLYt4^G_4wezE%dqtBDZnx;5C#pp*ijz?EuslQwUFnI3#Ig=_$P7B_*?Q>FRcZ&_p- z-~o5Z`_h_kn>8Q+LSTRcZjqD*i~t575_hWiV;Kj`Aq}UlfCvHr@fPU#TXXFIjYdyL&@oIoC!$43EVlCP}~Oe5ikHT&AqZ+ zc~&V(Y!(TpUZosf4$N8r|J{ZQG%PO=escj_0#5RB0@8C?aa3H~EQyd^0}3Ezu_R(Z zJ9h=dVPF&*eBj|F7=hcdc?E8l3s?ylfimLtT3L;I!7h)Ag2ldf6_^hZGKcU-vRf{a zyMjhWSz66YU;;dSRAtt_fdNoznWC2X_hd{{Xl2TVzML% za4eiug>Z|Knw;pg+0**T1dxz5TcIp_IUVji5zw~;_T3e#M|jv6mw|t^dXK6tGvV)l4%S2*t9ve-x>U4)ugI^&q@eV4 zZ~kh*Pe>vW2&gu)|1Swf55nSa=~B-EFA(NhPXjk_6CfzuOlT260!Sjl7#6{?u1vCU zLJ^9vQgptgbD>wj}^4K`>CoLGl?(1KcYCv}H&LiUuEo&koKD zXv(BK56P15Uzo58a`3nwWg?V;S~g$-`{CQ7fZd9sK4uUnge3{%B>`-NQ?yBQg2560 zMBkne11r%ICyE0zQ4@pfF`QrwWK9UhU>`h@5&^+}3Swz^Pz^k>1ip?OL{Sift`xn@ zhDd-MtRNIm|88#ffCR@wli?Tr;1oHlvLNIamv0K3pbQGl z(7X&5u~FNMfEq8+8^3XXHc=eMksKodP86+x@HZo3P5>ksj;O49XE7 z^HCpT!)+4a0broL;sx@O2o4Hh0zP1Dyw4tysUQC_AVp&#BT^!}U>_^eA}>-V#?K>c zaUBbTT2yf&M@J)>0VG4xB<&F+Q&J^Y5*$o`Wza?iHjbI#0AWMPD31~;lTs;{k|~?gDW4K5qf#oTk}9jxDz6eNvr;R!k}JE?E58yf z!%{5A|B@`r(k#ysEz?ph*OD#U(ki|_7ws`JNlX{9@t_zJCE7p;3P2iVK?ryZ4HQ!}M^iM{ zz}%z(_HyBhu5BTf1_3(L0$md}_vTQTK&c|7L=;jRl%?{r;o06rG)Pl8htn_B!v%JR z8E&!)b_I*HjXB*Be&#V5glr|QXqM>FIzMd-?2Pn+;VUYlB>!X{pfe9Dr|`IeJO2vM zh?70rlP)n!PE3zT1nD$#Cv)5;Jj+uGQ$j85u|LZ|Qh?0Fk_uSzldQB84ql)H0^pr0 z|Bw_C4b1SLtH8d7i{n-Ok*KU)C??um&C<8H-iqh zUc8&Coq(XC4N5gXSX5|`jg8>dQrb6U5f}@{^BLE@L={JOR9J^q6TwS2H>C@Sp!y~y1C}*d{_qg}&#^v$S`q*jjIim5tbD+U{g7)0X79lP}QXa); ze^v}PU~J*lZ}m{rkf3JORSWC}YYLZHEEjL&b8p`xWSD^hE$nFpviwpmRp8{5%r<6M zmvw(DP1c|d-ax@3hq$nfJcq0~q3!TUmSEA!U0Mik^~}qpCIkdPdnUpJ93Tx47SM9( z6fmfLT#0}TXeYw2{#>C06o=f3AbFS9Kwf4l2+?C6V(SX2cBjB&9t0mkq8(?*JEZ1+ zyeAT`#Cop+bWX@B=qSwiNO|-(W4o=ZAmoDv%f@ymepHBl%Fk0E{{)D9;@1=?X-ueg zTS3gy4ItQ_6MO60GuXRrtg^?(pj%7&VQF<>pcyCwGWVpDn z2Y6LBRR~Tx=cX24ShlVL8m7mjhypMSi}rBQdb=ndUrpP9*sdnRmi%{Qx3naT0@teM zmS{16(d^K^f`SLg7VH&P0*G0hc$1v#i|6L640w0#Q;2!PhV&=e{MU|}CV)ffj+tO- zje(}Y^I2nY#;X&5R}*pLs|bjaviLwBEkgE;<)M?OP_ujw(B?u^UWD-vZQQW1%t z#*A-dL?Sg+2nrsi!(+y&pEgQ|lGqh+SRa`9L_~xy32)eL{tiq>)D>kWYfk#TP`C7<3x%yEZqv&r)+qO z%;>FH3uKQtd%_QWvc>k8hOZ3ZCe2e$6dGoX88oiql?9p#!|5zN&rWG^xcFQ3rlTQn zAnu1JD5eZE%LDFNs;Bxc&I*79Oe}szx_;)Lb9YqM|9GHOS>UB{Sw{ztiv^N)sb&u#QrddIpa5;Erlv>+ z-)n+0xtVt)_ef8|2>5oN8NzICnSor|N4L|Is&&ytr3!EQn6+< zqHj~lZybWEZyUE|;;jmz7>2~CnYmE~nh8F8tb?Ya{Z(6^Cbk!O+5mZiT2LPxyC7NF zu7d%u$>7+m4jKm5ra#I+aWhzy^P>W7u^laQ%Daz8BS4qpHwhR-S%JHWcI zb2@>Yv71B7s6)gr4k^wYuz%wj|F$T*KRJwC|3Q@H&APx1$1t-P&wB`Ro5Cx6m|jzQ z4VT%5ZEw3oNQRBL5wW|r_cT|TYO$iiT=QRZg}Luw2)DMnEdrLgO~vcaBm}@hnvqXe zbO4xZd4yM`+4$ai(RvHc!Q(mq1`3kEMr$K?ya3t=mO-^OyT)R0-p+VdYVEAEEtvN$ zz~-9UXq;yh%mF`{+Qj*!T~DW&2KxJ9RT8%;WD;v1G?_$7*2Fi;Nc=}Ivft- z&Xhaoz~V4&;!tjpZ^lTfj(*?f;YPqfu!$%VfIG$}Ss=jZxZ?s=6pW9s<3wbxEl=)H zz~t;;7h?Rnt>$k^0IRZ(Sg53&8YN+V2OaudcxFg~~)K2hRz zz@7ejo5*RYKIl`nleWm^5e-kE|Epw>^E?NB2_$gmpCRGTzMh{N$ts{?-K!c#5MJiW z1m4>4Pz4&~s{l7I56O=TD8TK-n*bJ`N!4ut4*>8fz^|u0Flt^beBsnxIUTV{@P$L& z^nPk5L+xAT7{^7h^VS~t5Xlx$=YvOf^hXU3F)HLRPv}s(m4F19fz%&t>otPAUzBFz zzT)Nk40uI3XWycLu>9uG8HJy;=2o4N`cXJFv6I3oy7dfqw~m@YAscb}A#otl9{W+) z7c28mSP>ML5gW7d4h#tucX$ap4j9A03A$waGshFTUlx4PMzs9Mt77}p|K9IdQnIMf zfS@EG5R1kmvM?$kn@*x~|A_0eEP3zQDpHSlo<;r&ZFL zzyTOC$M#m9QMCoi z>nm)m>}Cfa$`J;@0iIy5?=Nt0Q^x_otx+xjuW->GfQC_r3qTAP^Y!lqK#B*p_cwSr zk~a6#iy8qjwa9F{`#XHR{5*ZV{XKrZ{ysmyIzWO4Fc_$)@=P4Ugx>xI99WPKG;hu} zRFDBh&wwuwuvBGXa4`u>%yLR&D#u z#D{_h4ydTW)>{DzD$p7@kN~CIz=8*dGqHE>1_=VlGH&enG33aSCsVF$`7-9rnm2Rq z?D;e3(4t3^E^YcW>eQ-Nvu^GBHSE~3XVb22`!?>}x_9&L?fW?)^LX@Z!glFK_-l`t<5!Ye84!MS=M75sj&HM9JO2 zN(vEf(uTfN?NudT3GuntAA$)gn8$hzI`|-05mbS~fm8sskr(PAnD5s3_Oru5mO%ErW18W(?Uo|GDWF^xd19v zqb)!h6&0TjdMc{o9J(s250*oO1yyVagA5}WQGg6G$boCF7TG!om7GFhQVSEvS_G}R z(h4B1Fc46{M<5N;!UrEb(5nn8jzO%kMRG{3wQTYFY_uV9LMs<0P}>5T1HjOdOWWW{ zLWgv=ixg@AiU zAtkXnIybf93rYh87|nbL4b+*M@Q4DH2Bq*J0U%@q)Td1&WdPJcBbC5`6f_k>88sWk zDF{qaEv`0@2TYH4kUZP-?LC&;Qo|9+IL48lVwCPFuL?RM01WsQsmq0qa1x+bMj`?@0Bis%0ocVJ3gh;S zA%{ZhL3my$o}1Khj2#Nk5z~kPC3+!9BS^wYw&MknwC9XcQmJn(s$A3-6~|@zua3@q zCT)`PtUac1fo;0k1P^Eor~tB>KjOn5T`;LMMkyUk8PgiBh6p$n&j1f#K(DlMrBeN@ zm72`N3C3B3Ke14nqr4K2+<llC<4X9=YGooiLV)JDl}8*O!hAd$+uY8}9%L$rb{)xg@^zP7M^eeOsCE8Xc*##EW? z>Q%;fLZUhsSlQIZnR4nuL27ZYGHeQ41#z`P$j^{5z@j(F2Ts!_|L{sR`;ayPT1@!D zR)ezj?JIp+vpHcQFLP05Mv1D9MRLHnvP_2IwDI1QcyJDZcmf(mpe-U~c*D^Ba4)DU z;t|WEWzzuY3L4Qi)&0SzMyTNg8yUeMQHs1_y_`DmFaQr@0d=pEV3C}`)FZ^|0C=Lf^^Ls+WE3=j(H97BIuH$MV@O&IcBO^`0-yv2I6%1* zNZ1H6{SX2hLerjdfTtmmf(97aejV)Iu#0_ca$32S@Qy~14vKGs)bq~%Hp89+F7Q*V z4Ju&*n;)V&39ll>No20XSzSHUpW&3EUwtDM)Vj1HBrYiL9@-{w>d=`P%|(u|cT;#l z&z;(@}oDTtHB1Cv~R!xoG>p1Uo`-ykqrdy&by2DUcr%`{FA*kKmx=NcCF#51HGNlD!*os z^yVS$5>H2O6F`AToKTfbpn&GL0hsQ(wHQM8Pn@y%g4jd7_8+7=?sFfCOxuSeTF^2u zqRgRN0zV5EUW69-LxlrqAU*-`gnE--AL$2!5WxV%5>$kJx&NXP{Dl&^BNd10E8+=S z=)(DJuYO+YzWwg6OeCX0e4<2_cI(%YGuU>2Exo`0{;$zQRK!I98vTLU{$WW0Ouz^D z&H-l0|2-fCqKB_xMe%t(p%XqK z6h@&GP9YUmp%q>s7G|LqZXp+Tp%;E37`BFh9D$n1$C{*5?}=Xp%AkUjA%U!*V!PVOz4q_9~p&`!E=FEj6<_M2bSUFix>Ih;t9f=1L zqJkVrF$kh1ZsH+!V&r(9F>v1#=m{CFiOB8YV&&Q^Qcwh#=3-3_#~M7KDj zvcLedGz11Jo=j3Y)}Bf>;x@U1XXJ%*)7x%Ovn)wEW{Ss-H4` zBy0%_f6HLrm4<8Z$icpd1^i4y78cHsYL8O)G9GEhdj|w~jG)#gDa1Ks35LH$|_;BX4Fa_If zkcsRL^i;v?AjR8(fHk~;@Co0HArE?%-YkWuA8bxa&E^5xrho=VDy7^KfsSO}%1X-Ri^7@}+juB9&05BwYRsnCG9T%vBh^84ap; z=8h~V`xHqL>7Tvv!Z3x-DOrPstZ0S;s7DT{k@7}?8YoKD*8=g>eTdqVhT?V#CuF78 zdG%a5bx2Z>}OeWzQx=6gUuj&u->E+;w4z)5`)=~Mtg z_JX7c3&p^T(o7WLKm(85REsNZfAi?K>y2&XD zm5qIhdCgu$DU~bb0Yl!3&xxIhXq+y@;2!AZ6rG8h0?IClSNLgDUMTIDy#4aq6ksi2Cm>IZB*5F)#c~FBv0xQ|uQ62zor7MrMs}XcqfgPWPt)aoTRKgzZ zSTO6uIqcb$L6ANSjNMJtwwOi%8I?ZFlod&irAZ@-=H1w*8K4L+Y$Y2E#iJfsYrlD z{MmKBfS?5%i$zM3TT=fBtr~%0ODbqx$yzI!2pmh9*zY`saZ^_CBe6r z=;n!l61ICa!pa%l#1PKKk?QLjLB|;cFxlibL1NKO=GZhs)GS?`q@F)j zr2?m!|B`xZUIBoUMBKgU+;t*wUM+*q+MJb+h#p$c3cv52$uA7=#KG}Sw`JX0aUBg2 za0LH=*G)nMmE8!Oo!d4A0HdzfX2fSbCh&P|*3leuZY<>8g$Zb0-`ViynSe)~@UwK> z2Gol>xqm^Uo{?^D_;J z|LKSc(f+b8oLT)MbK?lJGt-e^PQYg(hD9;6NMJx;T%9%d&og(kH-9rYhqE}3GdY*D zIiE8+r?Wb*Gds7lJHInL$Fn@oGdN4v=d(WVGe7sUKc`F*m|+2QE#tW1d=3zcWC>%Rts~1>K56hRa4NAWJA@q%Z(a{|lP*W5PJWJ2pZ*hNgs0f|E{6 z#hhB`Y>YjYBZt@v$UGy%kjzQPG*^S6Qf@H%4&BbM=MFh>JwfID22vy}^8qP}|8ttE z2Th<;RBDU-%?t$^Bv41jy? zlit#H@s6rmNzpVQwghYEjx_Z*@{1R>C~AsP{x(7xS!YVY+#0o!9>d6KPxln2LtZMi z7F)NsP)_5GsNTj&F7r_3_^VQLupoL;YVU42p?9+a7|bCPm+fMLP&a))VKUk4k`Zs8 zVq^E@D@Ej~f8!~U$x5!klSB>h=3!m+9Wc4T3zf*~gbYDQk&{4)X*oQs|CefWpwu^q zFJUUlx5^U07sH#;Y`~n9I4?Lt2luPyMe%!H_EJu6KxKek%?!hxu}hbBhwnHOa;O;y zl7E*Pzs5-~S0<68RR_^UZtWI9lxvMn5M8eBiQ&nNCYK(CYi@Owjp{g$cli6#Il*yDrV-AX)64Zz z8^JV!i?UnZnM#2y28Tcnbz!a0zc` z!J#lxJUU#*oFy1Q<3$z5rTe1f_}<2)`f|I^_;t7kd>w5uW>S%pfTFh?#RjNx!Z*bj zm#rA5AY5TZ=8a7Z!mSxo9=*hs=&|e%VbK?-!0P3B*W|IMV;<~Tf^-W!%MWuT6STpA z0VAj3t&ATVxPkV$K}T5MAQ(F%Q#<*I;wKw*p2kSaA3fEA#6 z-QPXl=e^$VJ>U1e-~T<}2Yz#mAwsN4xEV+huma(4GlK}e<7dYY!MvX0&!?=y9ZEjq zFZJVZ{&1j*3Pdp}<&1ZC4|Gj3n-EVkYKBw31cYI#I=lA`8 zz(B!4!a~DC#6-o#zk(Bhm=Y@xfCI}XDaWQu02BeFj)Ni<79tcXN&}Ciw+T2t2>}>6A?rv|u0^bymk@6AA zsc=l(6H$ZNC4h0Hf|iD)h0++2snk`ZH&ZYSL4gjJuUkpf4S~mG9sx?70ua!oirGbs z88vRK^3CH%kRe5mBw5nrNt7v7$|-Qc$&h{N9wnsr2!Mkm9ExPHqSK-jf2`a99g>1y zI+rvl+Qf;(Cr^l#W+90cS+!bO+12Y;|FB`jjwM^xY}s5+VrH7jb{!H6MgGxa z(84a0pHYGW9Ey*~5i07GwtPzm}{u@#SxgUf=$G{Q33ow?EyW zmD}864>4CD8O{F#swq zOEq;1GE-G`RaRSdHI5fIJm|e4${5f9HgFXqEd1aPWU8c@cqUhLUnu}@qW}I`@d1sz~co~)i9)N*?9gV2@XfAqI zdTFMcc6vo-DtyT?q^s`eX{@u>dTXx9V#WjrHV~!K3S_nV?D4u*du_JccKdC(dvCt`_WN(Z0~dU7!V5S2aKsZ=d~wDbcl>e4BbR(~$}6}0a?CT=d~?n_ z_xy9vLl=E?(n~je8nja<@pRT(cl}}2W1sJJ+H1ExIoWeh{dV4a_gyUAgRlB`;)^#v zB;k{%`FQ4=cOG@+qnBQz|L3c>zV+#|*M73=yZ7Gn?ZX#8jPJ`gpYrk3SD%XW+jpPw z_2XavefsMkTz>q;w|{^B_0Io)-Stm^1I!x$4X8Q-CQyNJJ75DH$dv^~P=aE5UrE_`>M0P=;uOVGV7_wi(LjhCAdT z>2i3S9tKf}ocm#MhIm9IR<4MVNn#V7_(Uj1QHoQfVim1;MJ#4fi(BMk7rppJFosc# zVJnHcaFBriLL_|nK zB4GdpU?d|!mV`h?|KUTALnN_G$Jt0%lB>1Wl-=xSrBN`rVA_GSXxrZZ;HG%|rVVp*bk1Gsd(izV57(OAG zafG^sa0nx&|5bBVQInCcV%Stq`DE8pnUN1sgaAJj*^XMwCQ4tt6bpmMSgaW4EEI6k zo(_tSS&iWR1`Lo^ zV-zTv@nMWp?`T-dc0sZOg$!mTT7Uo;E2l`?Y%6m^P&kQp3NI)@Xsfl6Xr*<8S8CS- z81MlW+_jRvn5-7a%CI6qc4055%U8AVf=39qlP*y%7zCzKtSHI2RVprCAH>^>W~BgZ z5*M)!R^7q6Q$}!O$;bS3LbzcL~*hnEqL$)1~mtYaQFj%lmc

=F2znNq!x8`)vV{g9xxk%=$8NmWetXSe%@s%;k++_ ziqVmdrR@%(s9B%K(}#{R)x1Ok#d5-6cw5yY)Ld9yAWYe-)Kwx(MeS7I0?yqI{|a^u zVeEW{M0fKZPpQ9TExuApgKA@U0PLMOVRyn*eDwh*G#4YIQr24!+3x~cj>8DaFnJF0Pzyw^JbHM<*!hCDcn9h z2k80kcijoJdp`Kj{(D?S-mLd19$7CrRzJUg&7gZe8hHt0LcjlVi3DJm6u2gY@jd%e zt&rm{Byhf=YqTmrtOIy}Qcy0h+r7+)2a%G32B`$0Gmth|0!|PT^zxKS|4;!@(SSFI zCIBcwg7GE**noeUtkDC2G0;9Ope=v-i}27B8dL%%IIl~9mrdA!xFZ0@7?5?4K?5Ly z+K7iCaKR>oDO)fNfD#5zhzDWdgb=&}AS8vtxDo?U0i;Vi7x+E{ASYdunfKU2BP0SW zEVVA^v(y9jUU6lbXvM2w0}pu!>4 z1x%cRNVE;Wat%)C6d6Rh@~}fJG^S2my_hovPP?c6tAb}rKY%jBA^?m=+(dR-Df5$p zR0OFogoU+0Lp2maB%m!EY(ttDK^!E4n20{{xJ5iHkS@eSVO&Fe{}7dbi7DRrslaf> z@GC`;x{^Z_5?gcvN34lWfCb@;f?k}K666H?lR!hHMF6NlSf~^i%!`w|!EBt1eOyLh zD#a^Q!;GT>^B^!yx~J?bg5Ih|V$=%>1i2_s#(%OnPc)F=n~PDT!VeS$lPZvR0EOA` zn4x%)M=}95Ye21XgH-4Qet1Gfx{|%K!g4GMo_Ma9(E=%13&x0uw-v<6 zz+1;FsYys!g@}j=wBm;Wuq!G1M6ME&hES*Xqo{Raf(;mrUD`jk>PdF!yJMI&Dd?+*6C*AbUD40%q zsLt?GO!8F8Brv?y<4(iS&H>;~dy%`@0F12b$|U$oDG*O)NQltR2! zEv-zLk~RKfRay(kEzs084UuA9)DhLwo{)!Yij@>~i7G_`KJm8+;FK19#dx>{UrB&B zbG}=M2!bgn70|!y8iEI~tqb$h!W390pouPp$S37j2v7mXyw*?f6?964YkCR=O}62? zj%2Ksa481lOMnLWkBYD@45L_usJO!<3Lo@Cr9j!bYs)JjQM&NANBD_~ID)KuseZMK ze`Tp-V2rWB65Qj}`V?0?HHHSs(4}pGrcGG*6wRCb+9}OXtpkwLN(ZV`kdjTzmyKB~ z$lCBMg{y@(Hi@&X?OK>x+hc)Mp@rKB|LKG%&=HR!#E4}8M6ubn%@Um`r+5nxkW<)k z>Dav!S=%UDFq46d1j`$&0Ei>ooi(MpB@!sJi%s!b@UzTM{lvb-!>N5lc8jUIji+Qu z0TfeABG>@Vl>)-EGZ{cwe(frleWa|Dyoxzn)L4auh}fFtultior?A(tT+3O(7hm`Z z=|kNEvkak?MQ+7~4Ge&i468??fbV;PfE~o#@Rcci3CD~`r20D|m7l5-oqG&0(m*VRR}|6PKg}KVz5FYfW0H3m1e46FZj|xq}l-f*?kRMdc`}Nn+yGn z2$~?@_W&v|u)BzYU}z=VXEla+|DysACctx@NOhuwa_ZmVNa4zLT3Aht){tS5bOHsA z-su_u6L8zNV%;QI;k&zD6Gq-o%sPw}U$kYZ<$H;TK;RR_GU@-N)9-dk^ z6(!?6R)v&e@ZjQDl|{x4T`lOvnOWo{9?^}dSH{&>5oH6^1KM>3s08?c9YKKez1el} zm44}{j%r_o1d#2(K~(maiXFrycH$|3z-bx=m}wFLGpr0y&yO zJzEmZ)^3F2R(@qz9v5Sd-=nzVS&0N#J>C|sf@nrZex&H(=z!v@*7QVaiJCv#{e|ub z<-A?zVR7h-?PS*_;nxKZcm`)q4cwEaT9ZDn_8kR^PC0-+<_#eiUCjtBHsn7f>A^zR zA<^jJE8}~#WRKvisb=*5g4AtO`EP91(?x zLXh~RJ~nBRx}@xMkb#oB2+EDX*63z@JBC>7PPC?10qbUUF2&JIZOk-VX=&}RrH2)! z8rj~6lXKn9=Gl)f#!o%lcHHe?Hl`O*Y+Rs#SAec8jo`G>gx)f&!L+K;CS&DZJs8-L zmojcIIH$lu>z=mlhAxca#!B5)>715Dw65vZf&rVjD+!Bleh6vXU~bm!ZFmE1O)}J= zFq7=I=Q+mSqLM65`j8PwrCdDbSkCP1wG4g%j}U1PnqbwwAb{3m=JaRMHWlA^XHAJFrVi>-}06!Na5DLG>7pO!~&7i zb14T9-ZC!v3OuX!T*p{kJEd~Ys$og_2WR2*0jJg>QS`-N^l`ZoM@MyeAm?F(k1W5& zzWYF=|1*@KQ32|ZMnfrp2)}T&x)T_90CaGc1SkRcwuLvaWyf{W4QPM>uown#fDp0k zFGv=}3II2u&-tZFLu>#y@%AB%!)Q{9h=ADw2#!+Y7Y8T-6PS%0Q4<(olRk-)Pqgc8 z-*^2g00@wPawq^8*p8`4ma?IDCs%1 zsrR{{fe|~05%AzMIaODObl1G#xgUbMhk8q_fIB(*vp)d?*Z^|K#_r?pnIKneF6Y|edl`y5cT8_eZD7sNpR8_aDa8sG)Wo5 zvxPu;FMQ9Zhu{YkK|%jF`4a_Oehy00P2-1t=N;F%f}a zIUY%bpL9y4E``qG6jbeczdu9fT3A_!S)`Hx0*J5hxIE5D1E6d`YP>815OE7r|3(HL zOKop+WGh7m8gXrDfPF4$NqvlIZv`iRa7~Fz3kO+zig6vJStx4+b!m84qot>(3R#nW zS(mkQbulHq1RX?^1e|uEOA$uKhjyNDFP(0N!q;*gm05TIWk8z0k+^au&Df}wC<2P! z-+7W(=|>50+5qNrx@lwfwChd=@Jk(bcnvFeZd|=g%9a&*G^dH3N$!mJJ4THk!-4Y( z%JJh&&Z{D-?)(eb%Ha_MBA;pEgUHkzdEBmibQ0?ptSiLk1*m1ur9uD~Awh#^XCksJ zj&Kq@1(W6ylj{^q`QY&Uad%d{O$l5rLa9Uoy$z%Qu>m) zU}cz!RXg*DHaLeu$Ra5yt2SGJY)2uH^;kn>_7qrpX|qg@Q(!Tn1g*vZg)+{x>xbn z8?YptY#tSH+cgf&C5L^mJLS6aZ`-1FSm2 zuDtp;tUgumNP1_%>%F5B~s?eT?gte%Ua+|OtEXx|LaNEot1cY& z9Viwf0x81o9QAf9)B(r}EKMi<##_q(_WJ9rt<$KX@G&C*+-5ER%Ks6ty2v0PKmjE{ z`vSk_@rrJ`@@gAvt%{)A?l|9u+_JOxp1f?t3D0ctg$|EU@-vP!Ok92+%Y3lJ!o(cz zHMIVGEz0%Kd`h+9R;%&F>haav#4AJ1^~x`=3hTzGmTmUgXs50A+Jt@WwlJ^c%-h>^ z*KPORwfu4~u&->1HyI3Uqwn9dEer!$@v#;T03k%H7CMQ~v3TQz$2{`nRjl`+%10&PUxk_j$-QmF+fB#vggh#Z*2GO`|rR9Z?^8iPB1}t z^a-UX@ys_raUBM<(82>1A7X$C(^I4Q-(7X-bmgooGE;@vbpIgz_nSw4_4eksfWGtb zoc}iiEnwPT_#3&7iu~~RKXLo-y9WL2#~S<*M}P5ipa0-TKk}8!2rF8k1lv&!;aTv4 z7|bBsCdic6_ze_3;h+fpR~2JG#Rv|poIF&J!qI_3b#o$70RINUQ@n75IE>c~TZg+G z`tXN}Xc7#Mkslx$QBVM|(GiKqI}JMViBODUW`3xR18jgukWj$}hPcEoehmW~P=Oc^ zAi545?*LXz02jSDHZ88Pe;{zO7!1fKQ~wP}02o+R1~7m@y9z)*>B&G$FMxsc zK48=(48W^_OX35*DuWk!6|7z*ms<1rfRcGt2_|R(ThZs#sT%gMh)t|FLGgjtaO!F@ z%?AKNxCHn(p`$r*!iu_Q2R;xqgze#n1Qsz5_oN^wa44)|SIgSgx|W#k2uUIHAXDY2 z0$vO<02-0D9~Ing9vBG;8cDGl+m?kKM)=c5F>!?5W&~6weXVq-OWo?K(i<@_L?oh1 z+aYpw7691i5@7HEGPG3)q!7qWdtty=tY`rCxr9BSAP;z{*1Gu3uYUI%$w)+S0AxuB zS1IVESIjnVZZL(RUe;X^MGn9Vp-2%31phtC-j}}^&aj3zJi*6NCBe$`E>S&~!~hmR zdeXxSZOFo1$o8fy9XV0Q;1B@=$n?W4=CF=;%;O$^4S&q|&Vb(%S$?&v1Bl}XZ4ky_ z{N<#OV1dGnuMkt!UD(H1&a#%b9GvBCM!+MsSY#3Qy#d%HiTSLAqH4p_S~!^qg_N=D zI0sz@bNSAA&hvEfcMC8Vv{760Lkhmvl?}84dbBYzd&4{oB76#86PB=Z7=0CBNb%B` z&a|dC&FM~i`qQ8ewWvo;>QbBf)TmCir^7c3?hd)Zaj=QLx>80p_vRMc1KNwXagq8e zVamF$vz~`d>|(Pv5*4VG31~Wnu>X$W%MJ*u6pj&A7(hF$tylmA8!!SGK!8(JNI?S% z;F>SI;!*`5Z+g-VgJ-)T+AvsyePUArBSix(6uhx3+HCB8`}^N4{Yzj7Gl>%(!5Mfe zf_#(A50`-V2z)U3RMd0?Uf>&O7Unm=H_q{n_fu4cA&Q*fV1n*gCpFbc0DNS+fZVJ` zQsaRVV7>(G!WGEUs?NF2ch2*k`~2rX54zBYPV}N1UFY?BDdB{r^o?LkS|d2sx1JTR zD|`IvSkF2(U;Rp}YyInB5Bo8>uF4M3&odeqyV}>zc3ldQmKH#XDjeGOy4(Hkrv!l& z8JTw$h*6B-KDyusPx!(c{{QfZPrTw6&-lhW{_&8HyyPcO`N~`V@|e%O<~Psz&U^mz zpbx$1M^F0FoBs5uPrd3_&-&K8{`Ihrz3gXC``X+7_PEcz?sw1o-uwRdzz@Fghfn*xPmO$b}HC{Fc^ba zSAjBEgErW2G|EYPg1M*oJQShHw~%ayW-{Sci6a zhj^HWdbo#t*oS`jhk&?J=7Uf9q(B^GMr?&o;>A4R;w|k2GE1Z?c9uKBqEt5JHLTHy z=QIN|@HL#sPrbm2-}C~am?<*=ibqw7l&Ffb!!VubMXX31u}A;|FpE{lSw%ouq!R!J z!zL9+Pj{mt(Bck*BSMHKK^Mn0Foq(+XihY+13S&)AJ!)Q&n(59#QRaL|tHsD(MP0r`|8CZJoxHCzS& z1!F-9eWZ6U=a2t*0FR{@A#o{zFj5~-Qj?e#2#H3MVU7o(a3$jn=AsBK$20JAj0;qA z|5FV8p$l}^O)oHyJAjeGz>W6^9^eR#F8NF_NdPYZj@n2%9gvN>(~dj913#&bg!2ME zU<2hCFF6T~Ehz^;DU|g{lwa5tTOx67q6>qTZ*j5}NR$N!wr@G+W_I8uDB@nr!Z0O} zjJ~K-u~P@RMu`X%FY8g3U1Evd#EtPNm+6?7*9eVCxs61*lsh>(H<>#mKeeS5`aD=`I@vzIwI*mYN;@Dxh?bqjM?-8JkSG=c?vWDj!?NRG*AP{ z*-SSnn6dMXVhsX!KKvbp@ZEP@%V3n}XE>|4AHWH30@<5Ce)=FG>I- zApZj*U`7=n0t3JR6CfdJ^`Pek2S3`P3>rUd^#HCZ4SU5_1VmOudZO#X12=F3=g9+F z8j~|X12<3uJaCU&N&sDorQXRCTAHS3S^!phrDvKc;P{ttFauVqooPx2X*#BHFs5w^ zopMm7H;|`7NvB~-9C}KoZ5pV%!lpvm0bY6o-U+CJYN%JLrP#>>j~b|4ny7^e2Yh-1 zKH#U!f~nd$lisM41kk5TgQ=Cej>f5_dwQr*8I%~3sM@I!pQ@>`+KDr8r9B{~f(k8+ zN~Vt*sgF9T*wBu=c&u~4s=6u%oT?E6kf!s9tk@u^Y8tC~3N6Z70A6~Yx5%EoaQ|PY z2|KWMA{c@=bCDATmUhArJ^=9tRp1OYWFb>g5BgIT01y(zSRh$2T9i;)(h(F|aS{#6 zSdNt%4xwO0bOp=Sa44DprsWcJq*>;%QS2}dC)5#jpj`6+B9w!R-s4~yi?L2&5SjIh zBnK#e#uO~cls@?nGYPX$X`P_Tlw1cU&K)a4hOOrH;14zrXSxb#T+nh^z z18FO!X&ac}2)AP!4{3`7P}u?V$hE^zt==+@O*yM~dkpDlw=nCfbt|^7xc`)i8wc(9 zwJ?jEa3HpidkvBMvXm>cRJ)xWFt=PgnN*9IVllUBi<2))nO!TnOc|a^h?xi$U4D=N zC8Y$cgRVoP2PM;Io#S;Lr6~3h0`uBsyD?ld%0nHIa0eiF{Tgc^;j#2VaT}ltz_B=P zU~>8fIn8ARloLJ|2VMs-Myo+_yJY~tp>2|3a=>9`_wifJn-R_HShhG^nXOC0 zQrW>lYY;Yk!#g>|mn*~2=)n#BwumD*6w@GZm-3g~m zJh)C=!WWE_aSOR&Otwr3!{qp;agf5JTeCBJ!?bz~b3DZm9L6rWlxe$@FW|$ftF%Bn zvwr)!;u^c=(J_59pP_RyDB~$(vLn6=IR@YWGN78UVF*+(X$H^*zj-72LZ6A`0BfKr z=6ad|*=X?*F^qE#?E99%p>W!zaKS)leZVMS0kE{Vj%q?*T0@IA5wg6d5`r`f@4FEV zyA}ulUL^1fzjj`&wg&!n4#G;KtOSuDZlSo{&)~UB^d!4ixnJXQS z@VGRES(!Qe&5FyEC_T@8pw0pi&}baUATWCih3jsktU!T^-`EXXYF(j|S-1mM(D zi_uQ&y0@awilCl6?3bMw(@&Ta?Wu7c(-pvQyI3&{9Vrwb36@QICd;LPQs5j?5NL6U zqM?j6a+%h;Kw%)ZIL^?U?>e%y7+_=Iy~zv~UwN8*ZT}Ztfxi?AJIawf%XpHE@?iwi z*laC6Ahr&%5M>`03Ijl9yi#F5@s-J{(1(E9lle|E?6>Wxv%+AVVN0|mx5ZISx+AQ? z>sZelfvS2z&MSSYEN#Zat;NIL(M%DuUMc4^U|E2G3=yQt z$Bg6i3kVOcT4sl0wx%O7BocmL01q%%Ruy0k+6QIT0!{wdl5ykx@!D5VnC)1|D@=}f z8^}!K+hI<+&b_F6xu^%u#rv%0?7fqg8O36J;69D#yJDP~s@#Yv+wA?9eCwEYY_={- z#^z1je#rw%LraaFl2c$2jn-WxkWVP2A!6xnDf1 z3qHfX-RA%B=4%eLa;nwXJp-q{)jY82y57%K{Z2N_(2Bt8Ki%q5+s;J@yQe^|4}!iZ z5Ds|}2_fLfJ2Bai78Z2it}s3Z&4M_CM*k`xZr9qon{x0d6L$h4PP}aqW6NP zSKP)jY>oJx+?^W_gM0%T&BPB)=ay;f1wQFfERDII@>`wYeIV;{AhtT-wsfBKV7%T= z+0M^M^CN%r_*?*jyWO;|5gsx&G-W|pIJ?lW5=vZ&-?tPE#h@CC? z;IS(vGX`M>&{)xq*IS`XWI2(y4slWbI|eAjkdF`y5QNgh;HDgtMW;({Svyh--~n&Cl(Kw14T`{&A)9@20b|oIqO;qJ9H9(DGLg1Oy?0w%g5!M&!_^ zBouJF-jL9-Nvqc>wYcL^bH<%cKpmQlH8XXVyNcD%=vHXO5?(kQea+qBmWDu_g!1l^ z#1hMfXmV~IALE3&0tuRqI{%FL5P9r2yS{{=jQS=aAtl#3=1M8eT$!#aOKEFuZ*g;V zcX@k#e}RLAhlz_-SpZ0EIRIirs0vO15QIzst^o`@gg~y9*Iq8JHK;m}Q?|{RV@i0t zwW*l3f9QWu76~xw_kW_NY5h^_(=FLAW&lDAG57C@G=bc*Z6iodlsI&{-esVo06+?j z6BW8(XyyeBY0aJ$STT~oyKNS{0bGe9fS_v+cNGwalK?Yn3ze-a(cu8fAs8YG!B7+% zfDI0Y_=`~!7*V52JEo&Dz~nV&x-KQc5o%HzfUIr}m@$ha+96?%K)a%Y$^x@yM$u)Y z37lI|eq9xKBL;^Kw*QxEL?OfEZWXk!GZjvd#b^@Y8 z!-c-;BOCw@NM`|L7|i8`m3Z>03S@#p$*PAskOrQfj0!+GBoPW`$dEk5Yo-HnX=1S# z`dE#)zD6tM1^>9BYpcsH__(A@@QNcuOW;xylMh$y_+o%16{0MaOKc-kxR~_z7%X}~1{vsS5u$63G_enQa73Yj5ya_;pZ!P#(0~VEkYNI*P$=Z21V(`18h$!} z!C#S37y*V6sOtA$15ogn;BuIfV1yAEXh4^P5-0%%6|fNqMVwv~2>^#N23My~8~=hfXaEL}N21|_b4wV3f}+@gkK?2UNH>KB0w@4t2NuCE;F~kv#o(9o6Bve? z^!*7)B-yd*=K|&x*}rQxP~n3KK4?KJYp$t4ga8BqmrDwR zgoi)|fJ=%3cqn3tRc+C7OCe_9L2*3{Q`}P{ZdmJ4h5RCQj2U91Bk{#AcRaE;ozPN; zQ^>kbH%twcB7oe)*upg9#L&T!M@qNr*tK}bAxv8exS@x=0oN^*OKw>G^eqVAvkEw7 zI2|>`cqm|oCTz&g2OC;7p1NJtX!LOFxVvHRwYs|COqdXv2-p3APiQ%f^Mn(Y9Sm>>N^4|xGHJl%dB+lsj7zO31OJGq zfbv*n_|mZ$gDE@gE|+`^1r!F8h(N6G6|UUmmIy~l$}BQChNJ^56OkQg8lig1S-=&7 z$;?|cFqPElCU80!PH~QtoaHRV2MHiU5yD48LMUQ|#6=4e;BW&!bYZ(-m_rp1%?$Xt zi3#>GBO_j9L2De@6ClvT9MysXO?j7tkZ6+%4M2|BkN^k{5F(Q0@F`~aA~t@ulcR7{ zVxoX3L_*YokTr!SLs$UJ0(MeRr7MT@es5Pnji0-^SBS`+}I0id#; zmA;Vy^zkP)TE+{!=|B&{jEX2PH!54a;1Xd{rd6dAyGrEcSI5zTCko-WO8^= z{q%4;WGxR(1h@fZSa-}}8s`m#scJ2Uz`9X}W2I_P!E|mM8^St0;1&}T-s5^pZ1Vg#HEyX)D zAhhca;bBzjgK`%JjO@;hP7m8|avyeGtBnjlbVToXp&MU-JS@FRd&7GoXa=?D&$|H8 zW-jzMgX=cxf_+#E63}Xl08?%^!uTGhG>*za8hu`tw2D*VYrO(p=-xb(PNN!eGR2?`2 zQkn+U6W{3)jca8uo7v>C3O3kS&TjNDM$K+AOf+ikWl<3uD*vmpXDz7hI+@u?!K6$X zM9E`g7>+XP7PmNw`7Cxvlh;lzHvPi=Zhh~Y-{*9J3c3dHNp}zgLKP|t5|HeEYY+r1 z*ucOSJ{}W%Km|oPfC|i(<=mWZ52A*2r0T4hTI;4TWGO<))3X_lmpbC({F0s|Ny*TEikv5%eXWjFiT(Vlj-ubu5}cl+Dn9(TFVo$htF``z)LcfIeO?|t|C z-vJ+Z!4IDBg*W`+5ubR)FP`y@cl_faA9=}7p7NEq{Qu=KpLxx1p7Wje{O3U*deM)b z^rbib=~170)vuoQt#|$FVIO$|Nj9n00nRW39tYS@Bk4o0Tplo8L$Bz@Btw(0wr() zDX;=9@B%R~12u31Ij{pg@B=|G1VwNJNw5S>@B~pX1yyhbS+E6N@C9Ko24!#tX|M)u z@CI=(2X$}acFbu_T49Tzz&F~D-Fb&mk4cV{_-S7?JFb?H#4gs*`?C=ipFc0-` z5BYF|=#cjMFc1ZC5DBpm4e<{Dkn#?35gD-&9q|z%5z-QI@*=SkE%6dDF%va0JSI`{ zHt`cdF%(5{6b-QxBaakOF%?yD6ThMjqwwMvG9&@8JV#eoly^yk??-R01m+73Zl!dp&GBjK(H|zi-H?Ribli{ z9P5AudPrKz!5qEO08(fn6pD^8zyQRNM*qk$ALP;E4xk(QU+#k}x?De}?0_E8+!F&*)-Annl`R|Fd!G9$y$A@va%dT1gQ0wm|L9YL}v zA1y8=`-vuj zQYSr-)vM29R z9*43TQ8KB@@c_2+EPt{mHp)8Wy zyyCr>q$#ScHnHJ0pRFm{5jIQHH|a<=uL6&3lQwq~8+x;f7)CkcfggqwH)k^*a!5CU zb2(WkIH!{~h4VJI(>SYBA@T@3nUh^uh&!{h9Ky2>zEeHNlQ?rzJI|9mi_@FGIRA4!_47Hq(?64eAAl1<+Y>qoG(Q=XI@=RKbFx7Bb3X?ZLa)<6 zlQTo5vpnI`Lp3x+6+%EC^h4*fLZ4GYH?%%G^h7VzMEi3@BUC_D6hA$*H523sz~%#Z zlt&M0;(C-vB_K0^G)RY(3jd1qYd+veqhd#cbY!q5NPQF zVHvhz9kv-2_VFHeVkx#_H8Eld&|)=qW0w(QH;`jNHe`9xV>M7@P4;A2kz`92Wm&dm zH&JC_HfCjZW@)x&ZT4nyHfMEqXL+_~efDR8HfV)*XoZZo%U&= zHfp7o24A26NrKF{HrawkNv5`JX%+--5-#@%ACN+$FoYUr1TN(k z9KYxQ+R39_KyCp*1th?{bb$hvDnF>AZJ|`8q9UPq>PPA6NOdA@;S#B+Ak$bV;mQGV zp-KbHHgidKh5uLx1-45b?5Ra=h>5(YD2C`Y*Jv|AC5TQGbfMvjxWPC55kt;%jF?lP zKs0vykvBEBcNMl|7;UA#K^_X1Az*;fvL*o-tpOI{NwNePPyl&}N*5vkTt47hT3`Yw z7fG-zq85NDQlJ84pam*!T7qZ;9)JNpzy-d-A5h={7%hIct9rAc0^)@Mw3jMI*D2sd zUQ_^ceK&w{RMKpMA&B>N8RVnr=tmZcfgj=pIf11NFaueJNBF!d zibmk*m7yV)yLXnC*>(x?;h@2CEm=A8_!>MSn87&>GXhZ1#hag@n0exiRAGywg+U0y zk^cdL0bpi|qlAQ9_nc_C6v+4%3P7H*L6!3bq&y&;37QOB7!Sn3rt~-}L?WG!nTGM1 zLA>l2@Z&2eBy?jrCzn%#U-tmuc?~LBh@s>_3%aDg@SsUpkF!WsZJ0g(*p&0R3PhrU zNo-1%I2%}ci;JkGba)&9LZ(1vCPcb`Nx-CynhOv5qb|Y%jy6rLU7HGrc^lvBshUDUtocMGG$wL81sEA1 zz$iv_GKboUqL^q}xTtpRs6KBSq_zu`bI4R*JG!Bel?~u2l*6c+qN56drpog`A~Hk2 z`{En`YpDQtp)KR60K1_|bs8jP!1JrTyoA3-Kmh)G!YRDME&ReUJi|47!#TXeJ^aH#Jj6wO#7Vrw zP5i`BJjGRf#aX$9cTRef-CPJjjK7$p4AF$c_BS zkvz$je94)-$({Vkp*+f^e9Ec3%B}p$u{_JQe9O7K%f0-|!92{xe9Xzb%+37F(LBx7 ze9hUs&E5RX;XKaee9q~-&h7lp@jTD$J3H*)~o#*zoR)c4%E(-XQJO~65gV6sSwZJ~?X zLvW2Nh&Z|y^qYm$__+P?1y~o_0NB{^ya3|21&pNJp&cBg!K#A+rT_Cu)yvz^m_3|0 zx77;=M>0EI`Xd~6cUq?HDIn$~npoWP92=)XpA9W+T$wBI_#CuFuWh3w7){|<8P*F& zur1lHWk4$mK(7_X4B|xuI$#3KW&rS)y%&u~AArHpv;|z*7p&RdYeB1PJ=>6dTI7M# zK8mNsDB;6K1~{4Ey<9qFKDe>rjdL3tGQc@mYN*})7KAg5+=+#H)yV4~q}zsyK<0kS>zf+9iD+~ zV#H|7V1-N{!FM>k9F65Uze!}YowQo?>3ai?e%{T%j4^BmR-f+K#oo3547O3Mn?Dz* z7$P)X_bn2W`*5T=9VsQleJEPO;HM{M8!{hQfy>7qb^ZGr%@Bae^3Jww$8Xh7hDlRfM zIzB>1O41c^0K^Clsb#RmSP>MQTxO0HFtngP8F+|+0ROCx#TvN+Xvu=E03Z~h?RK!# z5?Epus6NOb%Ury&-Z~pC8~~sQg6AUOAczFjCbb-^_4lMwGM`aTsC2drUOv=yG z*W2IY=j-qD_xt|?3@C8mnM$L0ok_9g35qbFL}4PpM$|WEtpKEGwyp>{X9bq+BGQY% zuVBLh6v$OENyd%NUMx$IVTd&_E(c(c=I%)_S|JH^G^aCxGB4^jV)z!r0l}tDpF)i) zbt=`WRMdpG*|<0K*eDdNerk+Apbzz&!iZ!t?K|mrx~rLPoqw)dNu3T zu3y6z#Ff+#4s94Z+}pKCNl!#9!D1U+u;X&E_WrCrx5HgwqtMtCywL*bmEeX9U?SjI z$tuOFSKJ^XR}~_A*SZ}5qpYXc?%%_YFMmG$`u1JVHWGo2=cD01i&4@~Q%Dxr5Uvd8 zRx|+kH^vLOB0vV)`^yR0?87!zV0UCi>h(ZG+!0><&-8E1E1(LN@01PmU z0KqE`YzbO6%)PS22n;kpL9-u){5(!+nmY)?R>yekc4k6b{Edfgs zFj^$Fo2TCxkS%Y(Bn)T~VKrogiPJa)Z4#M{@C<^+b-<`$96IBfHyboiuE{5(oU+O* zvuwy>BhW?@6BRqW&I?QwKy%H5nUANtLw;w$GtmUxJj3d@<&Y0$<5RN&A64(0_EVDLB)Kt~Eq*g+2-zyQWt2Lomp z3(-hJgxSy_X*Tp0Al?rOZdsrg3?KpQQ85VzxFY?!=tZgYid$l6()*r4#^JR^dBDjb zpo)_|oUEjOk@-eqPROqW>Q);ekQk-lE^pYP7}vVS5VGhjiHXCH zf80pBip3G@4AB`2)JgJ;@W4ANMqX6}7qy_AgB(hy{Ai1k+^JJWfh9>xdJk(v z>^RBTQpSt`0~uIBo)eG&Z3=)4HnnsNBWM9yP@xe47J#P}@S&P``T;48K|QE=fGXH1 zhBF!U0odq(3}CRY1iTa(9Fpe)0JQ+X5Wz7a>CPu2V++r0pezw8pqhY@QtkkQongUN zEaY+{XF%X6HviHH1S()2RTxlU2^8!B62LG6u~iCgjTu+RkXQ5M;!tmK=y7~%gk~k6 zsYVDMqqKR%xR$jnnWWt^ZWPpoZg!c+I!J%s(hVJ%G`6yJL``HD*2@6&VL1||0rn`( zWX%f8dlQ~_|8!I@*KNcD*3$Eys)Lw^7o~71> zES9hD*33eqCDss2GDHsGZ~&1tkY~yv-Xr+2x> z`I8vLIu>!TlO|M{sx%Juod}cZP_x}Ihg}8{T@nvjo>YkzQ+XiC=pq4 zqwz|Df+u=`1ou4<54+XjLnE3G*4R+bfaYchjkwHm!ia&85t-j4MFZnQTW%BML$9oRCJl*)?z_|ricNs zZs>f2J?v@NHzS`;=ddKF45pQKyYrZU282B71lmPpjv<3%jM=TXpdqI-0TytEbkM;>m=Te4E_EyeghW+DfRwl&WzaFuycAI9kh*E@7XQu% zrUSL~*jpJ&?lGV@@-aDl^h#y?1z2H0Cn<-jiGiJTn+ixna>QRT@&822MOU!l zU#vw4XApsBRKXl<3m4`HXwbr+8K4{tfyXJA5qR8s`QEdQo)LiIetn)xEZ`f;f+g@B zhLqL@0!40t;0={kE+|D>)dVkKJQC1PSGzF-b&#a;wk27RA- zfW#%O9pibD4cWqQl^+s9#QV^~9)$%I8i9ozg@BM(#Sx7$X(1j?LlGP^% z1F2aHlg-taxdf!p)fiYHA2yoMsh4j zbrk>=I?*EX#P&hM&}oBc9b9r;<3k+4ACAF-3=4o6M;iKE`>}*Mwo?LPo3quGwmpo& z31eU^BnvcT5iB4{vP)d`$^AH@gV-DfCEYqKfJAy^tO?I4{aQ{EK^GjrQT!yu{KvH| zWkbMYRBl*USRQY%iJNdq7@!>~Hp7~1i3=D23b0geOh60Z%mk)l8d(|v*a@EKiQjc4 zIxImR$>M?~KnXZfax{yXpotMs2RM3!yChUkX$tL#gOhZLM4na)l*I<10zLs)3OvaK zR%I-Z1votkdsw4N(4;dkBMKN5PubFHO@mQ=04gvGN}>xbq5p$DEmUMqrlrsZGYZ)a zp+ZlMCP~OZ&(M?Uy_o^Y6DsILLZQ}T!pLKWB)A>slkAHeaptq}#LZFH2$%pmq$L!T z00mg5RB~r*b>X93i>zoLDas0oRRp3iqBjJa`aMe!Mwh&Wp>=5F7zmw+6#>bKT$-IA zc-+wnnq&uD3$F3dH)tcaF_#I>S?f8Mbx0Nfo@X?^*R|L}G=N7p@S%Rz-%22ggHDsq zycuFis1yV!4;Bjg1%_iN=)he@U~wpuwI~8~XN-~*#LT0=m}r~@9i@>LgLWlz8Nm3R z*l@V#5HLW6mcsxbS0lPYOp2rzbdx@Qk~cI+Ni@s}4*%#k49JZ!sekI46o^s^5#(ht z5Ln%(3rf_rw1mi1#EpbrhQg;5Fc6pq2*VJSntsDtIH(GpQ?)IaB?(p>&EpX<5DwX4 z5)^2BBAgi5P?=zlc2*G=$!MfXY91JkqajVtY|qOiXEqoOi5gAL{7j%8&&!Bv(O@dF zF+r(nBJ-STU0G^+B#p@}#tf3`&djPTWXJ1>L9UKKD^2RJ0&B1e>#!1Qu^Q{KB5SfL z>#{OyvpVauLTj{2>$Fm9wOZ@7Vr#Z)>$Y-hw|eWhf@`>n>$s9@xti;_qHDUU>$%7uyz1pkVXvbBo>b~AW?cD3X0{?7N^p3Ai1-~lK3&?@>98VYA-1#_& z^CV}^JWs$pdraA=(X6Z zP+=7TDLdR)&1wS#9Fm@5q{b3RMp$gno@<=>>4K#w4n_h<{e%T}X33_9ol2x(u7o-$ zEdnB_7Uq7KnqmW27v>C{Ae=i#8JFj0(g@Q$mXAxM;LwG*N(WmNGq|9o>v5B5VpdjknTjoOVw$|1@f|5P1oHy{EL@)vh?g9Xo z0|;EHwAg{weM3$nN0QPNijvv2O=Q$M44H&e74pUtcmnqli}AK^=UtZkHlUSB87na2 zE~x3{MsERAD@p|_@rHl##P~VOdRbXVhJ!u3Rp%8RPdds=BoseXYp494&|I4gaprU1sB%97;q6E zu(matZ4GG&hV7&QEh9AH14zRRxBp2nTxQ~O=Kr0@84Lh)Siu}6KmbfkV$dQEPXqM0 zThQ2=V2PU_ih%kRsRv^O3|K~lb|2`<00U6OCz654X$cJUCOAZ~G!$0~zyJ{&aUwr! zaFK@puH^$ZBl2(p6jNFrp@5()hcu|+lKGz;{4BhvK>?oGZ3P2Ieu8=t9f@QdIv~mw zqW}{OA-`e428@6u=OdjF$R-n@7YHJi8qjau77bf6CMSR*8#A*;Ms+yyKR#gEF+(E= z5HpfM5IP1?B!e=+s3-F;DVtf2@J3%`i@zjBvXSWfYSgP-sjfwHpe^yI;jcdy(q*0V zB}oIYS%CoA$}~gBG4nIBQvYA0T_2>moE1JZBOu5+!c!2g(yq(_nKTHbj5FKaf`dI= z`VdkN!>Kn^ln5L&SuL?5`EQ^^Y3E&Zat445Nl{0G7WVaXOB3t#dIXf#qX0Dm7WhUN z(A_9H24*aw3UqQD5D1!8OFPRmrP-PMWnqm%tRnf2OtI# z!cDWHF1Ykp11n5t5Ht56S2jXkMA|M)qW0DSndov!1_M#gViG*S3LL;y&(1}wsykl@ zx}|lQ>TfJKhH~lk65$3~t7tZZ^%6JHa~-dr8Yc+3S--LR8nQ2dSw;Aa#rG~Su?_H z%i|SGCQ6(MKGA^~Isjhs1h42LxXH^O_7TZSm=>L;Xci{zz`!Xu0RWtVEm*=;!*o(; ztV``CEN9^v7!v^05?ZW_P4$dbc>x)(n`)Q9ZR0m5erKfl%5QU@Bg}TgSpk<`gT9&P z7nId=H`oo;+=}k!wTu<|nNWZ)LpU*n&z&C!r6-mGZ$+pBg%@~!i#XWPXkFzT_OilO zz6B)AwsWMEihu`H>RXcjC@EM2M4se3^CDh51A*rjNKM9a8w6g5cVp=IdbsEhijlX|HydY-$5sjGUGqk5~m`m4iwtjpl4 z)48nM`mN)7uIqaG(7KxM`mY0funT*z_d2T&`>`W?vMc)~7CV(M`?EuPv`f43HM@vU z`?X_xwrl$>RC|7J`?rI8xGQ_NXI8u0+mEPzXHdg!w4G-d1J|4(}rj0gu^dP1FJ5O2in9p{JsB} z#3Skm`@3Xx1IG(ImIx8XD?BCeV#Mov$luU?w>1llaPbKYX~?`V)H{}}yuFJ&Nt{8% zmwa2Gup8X6@82@woRL_r0l%sIT@cYZl*o&RC4XFdDc z_kG=BQ3~H{lOVb8=C~KWM@{7aS@Ha{P5)w&T=7*|^mtwL)Th6Bx{8yUeH8#-xpTPfB1PKJ)*6!y7NAbl&>>qHg4J*gG}&?7}hj z?1S)?|9GCC^u)vQAGTOyF6^Zy_E+rq$Oh;#-|2R6+=K^vVE^ZL)A*T^5&|I&xQKAE zaU`j;o4k=hrg3nB_@G8r#w~Yq5KL`Ojkg&R+@0^flz!IFD|77=bMO!T5Fzl^KG7Dx zmt27TPuaff&X+d6ayk0?d^bC@HJp$FcAbZZ9B(1VYKtas+vKyv|2X@FMki$zRy4Ht zkIpP@y!-g|>MxX-is>55$y0KxWpN!KZfsG6Xby_`ms;5|P-G#i5#w=m{jWoI8D?1< zGp6y(0vKV&d(A2RDlI2~ycW2M0JGYIY>J-H~`T`Pk>0 z#HBxm^8C%iWWt(>DB^zS1FaQDot*O!iS_1JhZ>=L3QWE&T`U>}5)X+Ny#qZ*s+nUI z3*HFvE@Y^ceMx*Seow6wRfS(M7tFI>sQ5;E=}QRzW*Mg|lfuu96RSGkz<+8Ml%I{! zlG;?!C!4-HF42Gg8-+V_noJ@Z4l1xZcbUy%R7z&BzHoa{DCjs_ZvDe!xk4cp#%L3A zIMtwErBPvXb#JpBYd@aMXnXCu9j*Ccw!-%2{`=8rER4x6e1Cf;i&?YM?q|@^QiW2= zeV^NqFB|QSbCveLulZI-V{w@s{)AtAT&luO*#C{Z{`zh_g_-Ve)UT_rALgnY$Ioy7 z{>9=VQJ~PBP81G_8d4cb?S|+AF()Eb@VGfTlnI2Vk*Y*ehh3^9N)Qn>QcaF-HF85W z5p_ySw{CT6$3zhgTF?4!4SN4+5lzMihuxaYu@F%$))bB&E%rxhqS~CrZav!ERf(cH zyiN5zI{cl}qPl{EhdsK&;}9`DkvWcDJ+YT+V)_!V-FhYQKO~ARFLYI#8pwU07Bf`1 zIqWr5!a~H2u*x{~f*-g)b4%8n#@V!CJF?FDki$ZGvcQDQXl(F4VB(~O#*|P zGflWq>JsMWmhS!Lma6p5kir;xr$n+}{3r`0T%PLJ4*qxkPDcVjQi?#0S5nk8BfQ_* zvnol_#+y6~4X5z^%9xCc_Pb(R4c=&xr z$}zfy-zmv3acvVFrST9HkK>{h)gxK@{OFSPV{?2a^Ikr+aA6Sq4W!;Pi!0}8|Uz_LUuD3pV4my9cCvd}L{!sFBW zLu`a?vh2Nc43-c*lOqwZcaHUdC5S*V}ANN{$?hZW*>cj z!->Wts7ulgw#Umpk~AgP35KmPuap8`^Y{Mwb$&YX_%rDI+P)i9?avxY63L~4)16Rs z<2~_xPAT>;NEqYEE2>xoQbebXj7~Yi(P#>W>cmjeIKRKL0RD&tq}trsTM29X-W7Ze z;*~bUqriai;epCrsVn#;7!??^syrtLG6phKP2GWx_pU|&e3^Q-F1$F^2f8pQjL3N?9)r=Jr6scKtJU7?~KN21X zdU-1hM(?K63|G7!>H-O&+wb2S-hz1*m1I$ZvUHj)=4oa%$lkZ(F$RFR1j4#0PIE&D z*T6I;sBuLT<#a&HmFx*kkBGx+ZaS&${7^MiE!~);K8lSrxg=GZ>)a1lPmcxUq$RoS zTg0rzNvZ-U0UQ`e?`h>)(UrOj)mq71rWvSm2qXtVg@eeUSh1Ki0oOlB5GY)khF>e2 zy-1k6!PiY^@1e^Z*GkP>b&YLOjXEtj>UwSLqJL55s{Zvce~COMVF9Wh(}$_&?gC z7d3xoQZyiLuv`D;X{j{e7j}{dQ?O(a|CNoG7mW8-GWjM3q`2eih|ZS)cgnDM?k!*jj#HfM(Gq6UPTrCi^? zPUn;qx|uHG>+xB2a_ys7cKe*ArU7PR;NP4$gHd<0{l{kMUw8R6tW z%8ukYURh8dRk=TQWvAp~;FZ_D9}CCZyge+v(u1)-`d^oF7VX-;l=Sxw>v~u|-s2Du zcePeXoG1b8<_;w$F^ob}7`xp=(Z;zDOy|;NiynQ@!Z)f* zh+0fl({2S+KYP9f9(C47J{!nOW~DOObvg>nSV~uKar{2w<8Tw1f!L(-lX~hgq;dp( z7p2)Jvk=VOFN zvAn}3kfT)n>XE<<=h6!c(uB9MPa2d@eI>vON!RLz+d$G7tySD0g zLF0S_JSWj0zclEqi6kEA)WW%;dSf2)X2rZ~Bv7pL9%AP#KWZgj6dUR!a4SyX=1}7& z#%dsMH*l35pggv6mGkE&|H>TM(0%=9D|40wiHQr8i zYx+#BT1e*GhXogjaroZlc)2R>4*s~A*i!>AB#o`^)D&bx&SgqE1~0(inH5l{QrhmZ zYn)IPo)951?I!7Ba|kNp!(^+LKD!ANli0*g7lt6JjDSP=c{8OgZ!zsU<<(P^*9jqo zUJ3s(kdi8EGM4Uz-|EI?1d z%{VLo0K=l<1GEI_9?jPWJ@=KZT4@f^X}9UzdJ?v9CpFvc3fB`VdwJYfitjc2;ah){ zLFdl*+mV=;-+yD#aWMgje(@=(Y3Ui6S=ofWGiEFR zjOhVs0DRq=#*b&%=N*hTi&4mrPUGzT4>c%1a2Rm~yD9FeL0NoKj|0BENW%+2){vS* z9wF75IWXKwG~-qo`Ad%}!*=IK(7WK7URD-DKzb$FQG@sA_ViW}%i+`DhnP5U1`(rD*$Aw<{@il|62k9Gav&tv z@0sHeYwbduklBD|* zE}E_{D&omRYsBBUP;A@-qT%`Uqhv+AErcesVE$CHKdEFhn7+BbBS4Kj)mp3Xi~qCL zS{`tD|5x(Dx1`(!W#qnm!l`7xQhJPJS>F%@zHa}!1e)2r!XpW`e__}&byYE2M+*#i zRB%3A<3EC4(vZ@!Gv6~E!Jr7bYxZnn)nroCaRtTEkVG0V==Me|*d9`{ zqC!C4Re*^jI<39SdZx0kNv`!LJR(ToJOgl&pVN@zlVl#d#+N4IvPk*W?_%IwC`$^^ zC?^sOz0WdZiciAQQm@to;%JPcYb;>-=5SoH~^!GvgEC9S_sH{ zpWEIR#U`gVhM6t$!-HLT`}bv8{66m2YCg(F!1E&l>CgO9%_R>+Xi0n|nv53R#~N7q z^P4^HEw%}+KfUY}oCok1)}9f5Ml`|b=sWtTRQv0M%YMSFi&2UrIxhX>OB}q-e2#mA;2eG-q z!)!*XmNPOg!XwM0Xxx7=@O+8IPx@g0?F$2RO39dc3VyA94#8146$n^NhGTZt6aBDdRR?^C-P@QRxOE50axbT$v zlP%)&tk5e!cbO7l_!2B+t@$mU+>C@_oL=Pv6PhZd0R1r0tCH-QN!gCTT+G_`te?p) zF^qUm^3PVzj2XF%2nqq(_L+Ly!zLTDa>(r;fD3*sl}I_tik?ct{U|V3PewKwu8rD* zZGoRYENmi5KA@LH&#|39WlaTN!ayI;l#+qTK=_UwNq)0Dg@rF1AE?F0hv$^`JLB}7 z(>na=Arn_Hh&7@qj)1Y#y>8BBzQ@5Xo7w=mjK&&6qm8R5)iovGWEo56r_s<=+$0m7 z8ZdTD)j9I9Oa2&&6TspC*8yS>YLF97MH!yFuQZKVX&N8wZlWT3D36U+I9mYEJr_M) zZSt)tdK07Lk9<1JF#w8;NmJJ9InL7FH^g%V-wm0v_Hn#zlm*M^!BeeeDiSq-m6m-( z_lAhv>3OdB8g7c3Wtq=@BNL}y19#b3{?PO}zl3czf{VO?3S_JTJw{lEhdlzHnb3Q& z&>`Mf5z0CA1b>LzcS1g=IP~oyJ6L?h@v@Ho%~O+n@tzvqnWA`N=*KLpbr+EN7S968 zvjDQ&0HWZ1^i4W1&tUqI5;?m6UdN9@Y^*A-v>pq&#R^50l&!u-Q(+_3nPyh*b4Of# z^$uGYi~QTbgQUx+L*beEctY_n0S{DS)v;(J(_;g0cIk|x3k7qU51*j~E_&hW5YGaN zYbgJUa*xyruqV}D${7Os*15!S62LD5Zl$gy)`^VL*nci}RsVjH960nDZY#Rl@$w^{)F#Cm9@af)Bb(D@q9j<)!5cTUa==iy1O)eW)n*__A|SbMK2# z5X^veP6=1u@tJ%Q0lu<)jJ-+4enB+etJxrY0U_p@B?y0`DU~|db{FGP@S3(m#*6U0 ztA6F2cw_#8s;*tdh$!mxmJx?wuV-E%-{!OCVAfk)D7!Qv-Oc9foU`e6|F5lbZUYXr z6v6RPGuyGS>kXdr{B$6hcTTct>FK#%oVJlLnc}I$U6$YTeKT=Nw~wp8)+eR;@VH2; zh{XQLES~Sdtqw12O&AIG8bKTWRM*>XwAj!-DMmh8e10+9Z9xwWuKM7DOw$b@@x{y` znkB(BbK#qT8?{?i`c0e)QJ1TS{bSr2i{6aaT+$J{)eiU@m;<%6A*O@+i}1#RM#@7s zuP>hoUHAiEGfU#ohH)r=$8Mj2isWx9MU`QNAU98nOQ@QAcsC;2)gwP|=7r{vo)`Bt znKU2XeBDNmAIG*qPIEHEaWpWE9j+e2a1y{@TAes!sG7h0|X2^d5!-AjCcd&t_JqHu5^k{DfpyR zA*%21^+oi&@;kAUStG$A$J#67#r$E)9+Kv`@nwC${pazqU;s)5AEDyR)qM0&0|gS7 zCogO6F0sT-iLd*LO|JOu{?X7c{)Ii>Nk{#om@%gm7q0gwY&6e00opYD-MW+Ia{SMV zuXHKq!rY6{=NU}sXVgC{(*^Eb7fJfZPg#P`vq`uz4<`hc&ZJ2yIZ#%`ALk4uyT03X zO5@#LtU{LQ0(KDc7d4}D?|q1nmX(aD){t= z8V)zp=Q&+GObk{!!LJDsrib!7K#@dVTOM|WN{V<1t=foO*|hc32J1 zcY9JQEU_sf%v32+yaRy|?s{}ZfL*+P!XVwGlNUcHPMVnEa~#9pb8eR`#!utWrDA6D zs&Mw>Xq6&wtpMtzU7z(7602jT&S_nu71i`>SY^F~EX;oClk;0{2(N@mFvP+uF@|G~ zQbU6QlB5}#dcVL-u_{Gy^9F)z2?2S~>bgfK%|LXFRnEFu__!J4qzPv~$$#vL{$Rl% zZVZrPxXW)rk$b5clCk$SwQp`r9jzWrb){7tr~P}KoHXMs(HpPk1a7+pq2nZ4x6-22 zJ=BeZvE{}NE%o52Aev$;X$5M{h~^YNfS9H_bD4%~-5n&b8XN%&79piLW@nF{%Di8l z@`@AqtN{s;PBlekw1sC_T4Wr<)4z*R)me$B9Vflg6cgIbj0h(gF-sy{r-uQm$wiM; z(IS&wY%Cs1#ayXR=hKu5bD~^Q?X>`UQIloq2Lm8bW-7_=VrHpO?WlMKJFS@JTkfk= z&F9VF!W$hcjbt5@yl@IKWp=QlhK0b>%+PQMZxR^6p%+ABBz2%oz~+K#u-${F{;&$@ z@gn5XOyyL6ROT9WUJ|F2YN~`6kS|RbH}5P-s(n3^Xn$n+NW(eIEA~~evQoaRV=5Gj zZ7KkHMYT!bj?AN7J`)ahtAc*&D2MAJ3gDx-IJM+ zUhCmeheN~;*M)i95UP-QvdQ9h~@_wpL4Q^|Eq|vn-q&}7;?RkH@W=D8PjeJhe55vD-OM-0sG_fPfze{*O@;TR>9;-=jA13{)SLB7H|d3dBp+2SA`q!_~xG z)jP$?%Cw72bs_&QVGf_94RR`12a~A6>Hf{emwT8bV(uQMbqlf+3cZiY0&+wQSNc4Y z?4$8=U48m@;#>`KiUM>a-+!d85`#S)<{%hqU6UnuhpT14x$|862W4yZ*C#rYoCfaDX6yjO@&p$C5b{t{)$W08vo&6DaPggvySuTz)*ZcL zKG?pXBJ#2SU=Gqy=5Yua385d%;%^kA>F+Y@k4_rYruVCK^PH@&5%SJW5?4{d0FrJT zx2RF?8dPXQNz1PHeW#YZ`P$r&u8oiYIA2&I(6aL>nti$g!ahdcFy?xt6xr;oF2nlv zxBBKnNUbYCZUu=l)(YRW-_?bB=OC4EN$cRel!517L97Y-*O@nWLH;4eO~mslOc?u;7@Mj!h0zrULL zy>FDM28oSSJErk}?-t)xVaoiM-D-xJVTV;T&EKhL*t`*-KX2U8ZrzUKH zZFqf9fwM)n<3I7`gM!JVFKrNpToEtjEb0J-mE)*oNHpA84)?)Rxv53C%yTuO3epEH z_PBm9Jo{A|#${N=Y77A8Gnx*qPD>8~?ge5VW2nu~DLUkr*@$#FmsVY`SNHMO#EFRL z?boC$)?`Q5_~GA=TGts}*L$~aps03mb)d)Dfpy zsng2mv)5^9UTczitW%-W-@4Ig)O$}qw7>23U8g4Ijn04oPq)`vt3AQ`lbylDtrr!# zgF>o9;Xib`7X~{&jK96aXBvUrN$yk02iX>fx?S(3v+Mr*J=F7Y!Jpgby#RIZr~OJo5C% zr%_;PIY_s!YCLY3k8*x%nL1eu6H z&uKVl`xe(pRXEc~h2z(`t-yz}(YNl+d&!z1yV$!k;AMmh0|4|S zdNaq1Ege4)sKtF04zEphLmTa9!MBV7h*VLDt1M|jI9rMu)f;*^jG_7vM9;B;q7_C3 zpa@ATon6y7R*W%toU_$V63{0rcXbSbGv}iBLtZFYfIu}E@VXbsS?+EZfqoR~`-Z*p zI8mlC7Y)-XgFbvu1E!6Me5`@{;FWet1{VxXRL`Sj1FOjntK-V_ccSy8%5-_^lmLyo zJz(G}D4EO`J8dFv0KxfAp48YsMiB^($Q;lJ2UxezG4`FgF$yDBz>ha59zH3oja01# zl|Kvo*i?;QmR#BNg48YbI}bm&eK+}cNs~FxiZA6%%o$dhfZa?4AQVMnzyPm2%haf- zl?`T;v;KD+jwR-yaFV$L>62$9W=}f_=YS~{~ zw-())k=C8jq$>DfRNWpVXYA8wi)JgYuO<7^H_)Vi@Rl!J7Rt>a5~Of_f(saR@S6Er zPy^TQTN>Bpt75NPg58~1D7>rAzVFFv_ETe!Df5;o7;qE10PSkVd?7t~lJgAgK^SR` zRQ{YJr7V)86=PkEsv)EqiEMePcK;+kv(ys$Ywuf(OZxlD=1)oA&YwGh)G8seb5l$) zhrwn>#^qEi%|r~})kOI!^2}y<>A_AoQYz$FV5Pa;z9tSZQU=u5<09gazIHveS$QE- zog1BMx^}Ctw+G`$d=cReD{EQva6p|Di%#Vft^EvJSD<8gq>W^&0F9(1o&P*HPs#VH-y&XCG8;O*CzSJ3pmUO;@A}_mc&)~p?l{=We%AXzw6e*0xW)7V`6KCS%~z|@ zdG#)jh>`QpjGfcXFX`9m8o*cQ-~4xjEBQzQwD@CcaQKW~j}WeCghr#3l~wBSmPB$^{To2XmN0>b;))dC}vye0zyF+>760klpi z9$wX@YN`+7;|P*nub=gGB8>mlB}R2s_nkl$6rYxqc;N}~UF6A8FKh)`iKPpU0wl{x=5 zA{4j+i=?GMd^Oi$AvAN@ zzQ|`Ri2XA*098U3NZs>C-qCpF5%C%MFTHY?&g~NXcYn`EX^?J;5k>WQ**$?`CmT;` z2`p`X?BqQv(=4cn^M7{w&5AO#fyC5$!B)B#L6T}K=o>!gq+wRhuIr#N#XDKD9KzQ@ z0j62^?|JlfwFP^v?RgniI9>kMG%N7ohw+K=+4sMmSnkn-WBP zuT>}X-6X@8C%r~7ciRCO_OPAxj$jA>h6a7bpB0@6=rP`(=o^m>s@Eg75)z{3mU7;< zCb^fj&NR21%Hkh>-PbBA$SShuihQ-bNu?ilClN>{+F5F6%RZ8o&_9b8-Vg3gXlPLV zJeO=K^R(n$e~T`Ws0`ieUQFNPV0EVs9LT0;{`&_*zguP19z<-fiupz~&e^Bqf4H8Ih9hY54y}XC|9sc!*H(~hi*92hy+hUV{j^XF$1!Loe!19o39s4-#R_m#uQgJe5!KYvw!wWkY>oC!@hsz=|zVu?aE)Z zFL8<&$nnvnAO*jQ8{#>hP8}ANIDw$hg-Rbs`^bv^q;3(k%CU?k>PGZ&c#qGwP2SOqsv$KyNVpx%p;{B zI)3i4|Dd5*f@b;gH7L%pMH3tQ@dp2MsYo6**oc&?dp(7{!VoA9F1sxWt1^rNMc%VTYPfA~ii`Bxoovf^Q@C z_!X*cpM`xl5!kn|KGd7MF!Io1)fz}HdIch90sRwU{riKLF@QaDE5%(5J*=VS!V;PZ z$GN*Yi^nV5>?W}R$pNlnT~nzH{Sw+ke0p0chFWnJDY2X;AU0Im;H9X|t+-4-A2i1q z8)}h?M7s&Q@`|G*VWQxICOcPTym^x3-L;x8&!6WOn&YAtPa{|=om6p~mWTV0!42sd zo=V}uyLt?8I)S@fr4FY2Ppo1Y)Dq9&LSi2+p4aI-ys*6&3T@fDl61JgodlCb4Pn#20HDAe{Kp( zQd106iTV(($sNKTcA2Xv<;SRPAvniP+NS`E$bC2|3*7xnH9^0GLVYB(0!20c(7^Cp zP&g`iOW^ckGj+z0d>U_!)~Q^aH=*uo%0{%0?09)BRTQ|kR2bZ4qdmF6kPK|d(IbnT zOocqKxY{FTsoW$srTfQ0t6KqI#ADxWsfazWD zgZQLSsUFaig8~~AXx0Nr76V?n0%T{&92>=Pul)*WB~zb<+|xEGF96s$loBF}y8F24 zjU-*ql!ubDQ@G12yHmC&UROIk-R5tVF#ryTxB6P)Q z`0cGL6wO%v8H0b{NW5v`{5O%Djj(s?@d`j!z~b4CEPY?VVMosm?bcGWe?o&U^X-kI zw(A^c07`RwCGwZ>9TJ_{eeVtbvJ>Gc7>m1hG4gPmSHb{N5K2V=@W7Kw2%&#Ui zU30u_bD~Xia%6LAX>|f0}bS-(ZEd@3$MUgEfr7fk;TFSRuDu1<9)3w&h zw$|IUHsTY%6szv(isG1P^)9Z?o=r}v#zkCYLJ>7;_-(NVeAov8tz$L}!bqHRYRa0M zxN@ZS&@+T)$rDEe+O|5bHyzi+`hMZ=6Dv%d96s*&v-V+SD7{#)M5{~^g7d*A0v7Kl zr6~E6G*Z%#+J)nX)uO_1WZPkDZ2*hrylcwCYOYloZq;qrTe_}6kVMOXYzoGDJRk^L zir1w=Ml{T$wOO@t5AM9?7;*;2iB$YmiYwX^44aa_du*%jnu?Y*}f$^09Yic}wW!7YO0N$#k`o8ofF^>x*-Eo(zJrQqMmgdI5CZd5At z0ZAxQ%)eEHp-%dSjZ8+v3Rw+dM?%aF9P?Dgo~emq&3Ryj>^Kgp06PYGCxq)862V?b z5h5WZIU=cRBKM{wTn{|mkP-&`IAAnfhDwA?OiInDpXE3@d<{&#%WjO2WLOi)>*>2t z5sqsUP85?&OObK*36{`;`23d0ZN{gz-l5IxWu6bl~o4;I!)5GVWm zRCxiawlf^r9c^s{&z<+7J%hmelr0-8AF@G2QXzOo5SS4};SIWpTG39dsP!gdGFaVu zori}-AwYc8)E(;aTt!qpi~|B2Q&pU?g*)!5eS!g|PSD*M2?PtgKs~HqY@~OcT_3_m zlA|h~TAHL{uEHB7Is-O!^66VsCY0C2YFdvMCChC!%9?R%#(lK72yPIPX)@m(W;vyz zZh%hu`0Ba<(LW#^^@=Jvwh@OK)`L)8#Hb1)=5?r!`K4lVV$&P2POVw(fu?#^ZT1LN zV2+BOj9$)Lim3-?YJ5ltldY=MzGgtVXU>Y^lG`oUZLjv`R=VlUpob2p|a72}onxvb}gRa?YCOPJC zdRgDj+kn()ezOpXw$*z;`Fv&ff8i(v8RO;|KQ4xt&Lm!ub93zi_~67WNjVp&=`Ce$ zr;K&Hx^KbcILBFxz1A? zR&t@YW8`H1R*ymwmb0SLU0kSQ5MZ3vyd(w%XP!-HpgR*&xH^B)j^5thGxB&JTs)Kl-5YiN-zVn-p_4aYx35D|1tt)d^@(_ih$Dna^o~GHG>lE?p7{; zY2Zu@7OV;ov@aCM-M*4)R8hY8 z1ranW&G7kicJR$59iaC4rT7r zBO=}l5F!=Q;-WiwxvR=aEmVrsh`Rq7st+uEyZ$KSu03iMp&lbxtNE_1BQ8R&3lL1a>jZ@85Kuc)PiELvJvjDuN}#lV_ao?C36%G4EO4viE1|+FUqNg_8$4*X zK|;%SwLlQ%xBl!fu0?#hVc8ew^uDBBcyOJ>B$Lwn#Wgao#q8)5((mi^!zT{Z-{?%y zE)8#%QXMSgzt3lXhA*9Rbi!G_0jBfptJ&jHnP4I$D_-z(T4L9YW}y(CWM>page8eR zr}pt5oQ2QciGA$($Emnn>Gi2UoPo@JcxC$icdz@ud6|L-mboRAQclPO$!W{l=K(Uz zllSIq*dP5uL_mL1^c2tBC0BfEMu*tT60zCo^Ou9yBn(UXC}4&si4&cv_bzmC9Z)e! zYWr7uKO*~cc!qVelIc9J2_=s1R$^!B&Fs+4b1%&ZC2+gRPsgBDUN1c%Hi%iQhVR^7 z22)v5WhZ5{>hsDC&JVwO>GT(V<9xN<`f0`1`x2sI9jE<@*f`yriD%h) zh?irpKkjFC*wxH(4`ai1wZjINq3xdyiI2v-ViOLl;Nmi|TUzNTK8{Z3zkbh$gI=j_ZF9;K|y8uk&huB~W<#&4eQaJLA z(2R@ZLaP-iM*9syu7>1=m@?5Y%7LLI^2)|P2pL3O^BsgBcNRH1JXtG1h}BgmkB9GE zLpPrVm)kr&y5h9_+z9L@W_Vswk+1Y>0$!`+%8)c*dkJg{&dm2_L#la|L3%tsx*>hm2rgyKp*@iXdI@t3xwn|;2 zHVBWA3i?SMdwI61Y<%mJZuVAZa{{hYp%!;$Ys#Izw(Tgc8ff}zlfG>~O__&IsuYXK zkn2j28~(L_A;|SaGVFrV*G=7Z&BLagX3eP|Uq1fH1!ad@oHnIcqiV00&+AU-xgv$K zFu$F_K>yE1cu+2POMP5w#q!&=cR-}2%;l4CO+f7ASnyrB?3_(vtLN*n(O*|;4(zTw zWo7>>xwjmzjDC3YEQ2!~a5d#PetZ6jB!&SU^{afcMW{-_4ebmz%UdH+g4v+eCvaI8 zA1Q~qSx=)$m`bWk5bsY`rbv_tji*Vq1w@*oD8NFKl-8NLtVh`9gOBPIjXtZh^nr`r z6(Nm#3)9STgeLQ>X#%h2EA?*CNjk(vaU_g2Z5E_^Ac}-*OsLAGEJ9dK0jGomOEcGeW~#CbnT@Qh{$Y38BcJ1MU_!!x~<{ReLej6K>&ROQ0*52(K zo3?>V=CAD|zHYpBj3c7bsu$38L4$MDH#@CLUYv>wg!+apqO)zn)LrWIsouCWeciPE zlSpLwHqh3`fR-Y?A48Q4md$==57yor<8hC=u>}W-><-go3zgA3@OO89+J|^TL()N{ z23C8L|76yjDLHO$S~7Qe_3eT5ZmFV3xn4KDY@%5yaR%P^8Ha^zZ=?$SIld?`dj)~roTzZkn)x&5uMbJ^BE2|GbtlT?mYPA1*X zRZi~1M~tBe6?1)T0?q8i1jlPpGiqmSJMH@>2WiRppdF;k1&a(dX~y zRx2L#R#WeL^&kJML{n#C?fkgYrADk41a*J^z?~#354f>KUw`{e`PLTuSLAB8E?N#H zQ)#~EU0Xx@vE*tf5K+&NU2Vk#D@tsNcCs&;K#N}l0Ot`&aVMpO6b^fdc-w5A@Xs@< z&3U90(r$HY`mi7g)NhCXdLZ8dr3aTX2@i)-t!3l|KaQvg;; z4|$S5sCf~vp;A2ZyXIkgJer{^t9U^LST%l+yd1)#I3xkjEQ}#uh`+IxCXWNn&c2uP z?0L6#``zTP|=mGNQY6(OUY!3Ccg91v7n=CQm3`RDT}f1O}tB z0m}TwU7aKSz|toBA{=Q7byqDcWesX+Bv8En7A~H;DRO^6RPfs*oZTg-hpsr3KGQ21@H>ubKcP-b`zWa1Kp@y+-gdW=Iy!{1=nwM)}y9TVuVMr3oMQ zF$Zxenl!7nMt4s?o}rVCy@)Q1%ARsw2OccaxlzzF{rt1L2z^sUq$$FZ7bUoL)mQgZ zJZ|mJsme*tIv--dA7?5Vr$&d;w;E%!P6+YKl}+@d!-SAHp$Rkj3|Z9>oGediz-~^m zw23Oz^_&>VkG#~}L!nh9MnVCuT2imcB-l+Kml#D1<*bPeW}E7vaPIx4m{eg8nkTsP z$%KoDjU^0jO4tQ3Kg6OTUtP#-pPa@f8*N_Qk-DJlQeAo8CuvIPC3nq6 zJk`c>O)wuR7XV(y2j8dtsby!%QW~8tO}Y#{%P&)P;x&A%2CH0DP$68(Rlc22t~l~p zZQQQn^UwTk(*;HHSoVjYI+-`W>aaY|)b?e0_TnME78x6!jG@Ymo2?(AtsP~vpY-y> zj^4-2#kq{I>8ZV|`O2U2r5uHpflK4vvgmP zGR{$*xojde?48)?!vk*}3qdFgtQV`YbznwG7G6M#U$SF5YVDV%jR;K(V!mNnq$q=fu8 ziFB|KX_PG%T|z&x!+@4)4Pam&KYG--^co18+qYw4nU%4Mr>gJUmBV`z3DPslY|C*) z9J}M=FsxAK&KYR{IcNpDbH;>gO$dl4?i^pq73xAJLv;HG8e|bhb6Lxj*`{Gf-&shC z8WXbFe|MKrPAJqHP`PiZ@z$BewnZGX^O*Y?A6{34VK~zzahOzkBo*dvR<~3B!LO@! zme8;Mu>=Z>s^+6gf3=gz@JY%idAkrXVKG4qB*ps~hy5N1$Gr~uzhx?0{QwL8WW7qX zjgHVf6~@`gXqu`+WtgI73Au~U)Xbo!ilvBexSgW%$Yx*fBuxg~efQudDlA6MjdrK% z^4NtC0A58kKKq37 zyb(*x65fk?mc(jYzlpb2UUa4DnuJc2s4wav1~|4Zh{iL!>?J}47f1?mmfIwM&bO_i zY+~CkQfqb?s|bu6CP6Wcyih&3*suP_MgSy`VUGZD!1_8{bZAnX%JsX!6uG`JC4BY!2k=##JBFhL z2aiBqO~lpQVI@h*ikfgEIF_}VB1G=sPJ`aM(4&CWfL@lP?Ee-u%7f^szov{=;)fcCe<1@7Cma;<6 zNu`E+bug<{iAs0d zt|aiX=e+Cx&~;Z)QN3Z<@MnghySqcWyQI6jrMo+2W*E8#M5G&}8>AT;X(?&xMo>V} zzc{aNuJs*!=X>q7_Sya1`+9y?&bf@4`h7y(+ah}@EGK{~lO%E(Ob6k1yqCB|)o0a| z^O&V@?`Xolm(dSHTg=#A+CHc@c#pBnatE{8iBBt40&Mf_yY#%@{ZacQe;`TY+jj?O zrn~B0{oem)PBJi~s^);{btS6;g07v98622hB>2)kSi84#t;A#aS^-_j7&r26V3dWD z<~m<;hS8pdxr|lZ6d-Us8vuvo>tGIN3lRQb5~M}rotg^oPJqtLvOZkUlcfzcrUJi; z%7}PUFfr*_R%x?`7|kBF_37{lrLfO{bWJf*Y87$m_(^-3I`{ovslXUTjJHJ-KtBWs zsSbO-KWy3EQ*bw`ay^;_h)M98I%Si2TX1emcG1_?gB3z(Z!>@Xs9VZI9cWzvKvm`P z{qw8herWQEq`?bn$)8469ae|pzFv!%kBHegj6K(4NN+=Lnw1)sPP(P>KoFerQ?W-3w&l;XL0u|yReLe3YZ6r z4?jADnAsHbV)xiHg{xx)w_tJK-dSOKQu$p`Vc8{;#o%3hSA&?EY8mHe-W5SG6fk)! zx+x2?SWRxb+BZOdqoPb*|(I$ zPtn{c>IZQflQHW7(;62&zay3O2snj_I)B2Fj|D~rh;ub&djN#hs$zP)4SCFA8@h&b zXcHNirP!NaP;mnBe!8Ab5MhfMDN97Mi~TobPcd21P2pteoF#eKM3z56A%+%BZHD7{ z<@jBUT#8jPSO~ZEoshhFu(aVQz5StSv94T0>_ME(ZeB$u-L>( zfY0@7>~JVG)eAfeNkK^ZI9ViZ4~*{#CL|dr79J z)9`r9o_O1*cn8S@m+%CSaL8MBOU{hEU}4K}5^m3@MAIHAiFfTu%e$?hq^u2ip@?g8 zokP)bd$weX>JvH+(&MnJz0qiN(mbV?3jU?LUG1}IUwGPM7#}O9ppyeFb%)-HFwU0I zZEdn5hrJiUi>SFo{WlQ47yB`J7D7(Y=&kI~wD=f7D54x1ffM1s5E!N-&NTg$<+Lnz zL+EV7;XZ!+nZq-)6`ALQvBS(is}8lK z@cU2cyt^0eoTp(SNy1a2R(yrf`56BBY#Q}w-`LkIL()(VYppR|XHHO9NY_88}K1BuVO#|9fD^aJ|`}mvFE4`TZtRa{M zhBR?E>*{4&j#;j@%uIfIo8p^Gi#;jQmuGv1XImoTM#H@P^uKPT%e?qgq zbK>zQ{K+zgAN&lRp!%F#Y`lPqpI4=xJnY72fk+^1H)gvws&@_CM;scT0dUvS&R-N_ z{&lNm&f@f2?_+kbPbNm@r7*;0Y9#?mBin0C9%aQ-A7l3ZbN-NV7G8tqeR-zn`H}OO zV~?kJ4_#h=C}+2E>PA@6AA-p@RURFx9_F>?*1I3fxhJHb7(ntjEIadnXGYL^KlnG* zepKMT=AM||)zWirhyM+|fdbkQVTER`ngSBAGZ@@UqZa9Yl67RBN}PbuKhEknj+f_z ztkTf9`d)-&sga$&p{_(Vp0)t!2T9coE|RBOWO(X+z>=+gW>!Guc@pKXBmU(Zu6h4d ze9CPs^(JHe(OXr+ivI>B$`^K^8Lx8V&8sSbglZU{=F58C20Qj9|mJnF>c>h`Cx|CN$oq*hQ&{3 zk)xZIuTlV#uQPAC!tXsw@rNFGeXT|KqrUHB%*6N?LAk$jOXp(fgPX2V#mtR6MzdwEXQC@#hl=hQU zQDe4Z2f~<{9W2E#yjfc}zg+u~5P(GoXAZESN>0%l73yUpP-7kG@EmmP*y8(e^=uY< zDAemM|NoGC)c>DyzeU~%7|(GYECMibN?B1r0aW2c!F28fA+nMD0VM>3#CZTfRkYop ztQ`fp$reHNOrQb>x`AoH5RP0gh4~=oK!pZOQC(Z#*ql_z(ap;*C@d;AE-^Lp&AY?|1!!ePLR;8a7l zCi<;5L&4F&jXQ_qM2hwl$Z;)OH9z~av`Z^T5XXGMtL1~ax}X<}CGxQ#eC}XQL(zOk zjqwK!CS!n$RSk`C-31St6R+f8+*?mGq5K|DQa(0!V;-srX+|v!45BtLe#Ks9kU(V)4)_;S3CQ#0<+!qPywO40 zzaXzU05P>`<^b`>Ec#!!H*OC0b^9Gnl7vg#?hVDF5%Nce>wBq-p5|@~s2VNT`=z`8 zJWhIeVU+E(X!lO?=hwaAI6{HfvcJ67f0r8^+Q|HOeDeR0``Z>NLWvPHDm)Vta_cCx zh4@k^CQDu!3|Dxgj9IvPbrrZsY{9Hc;FwpQKpVJGo{0SDJc`Mi6bQi~XLBh}S&81L zNEH~Rs!S7Ewx~>(ILxce_`k?KstTb@Vp)}?&XQl1tu4G+^+sQbx;n?$$g=vaxnq8H zu65vM^}AOv>Y6;KY|EN_w~G9l0i0!K%a&()JX_B1OQODSx@CpW{7c)$ zkyxF_K+N20D^i8GYWR9q%k#FArL+(8*%Pac+QEly8 zp2c;ek?{>3%T`|bZHM*nrvC3+4R3ov+K`s9f9sj7kyFpc_E}*s*7hE8+NRDG*C*b_ zRmZ}n?gz{5CY|m7iQJ!sTl0$@RTMV&rRHt(i(ZY=whTn|So=}l9~QO@{o34a@%;}( z*NTKc6<#1QS&Ib2tT=a2tz(2oQ-b59#x~!=P*z24leB2A=u?bwbnVluZ*1ECuX4ZB zJ}WRr*D)uuV$(4%aa7c?AalRdvG@{=zH>>L)Haw!m&~Sx%#%I6E{QE>2}!Okfq#Yj zsV+Q3293->0o&I6J$Oya-N$%SJ6vvsxQjSEbnV(!4o%FGBk$?ztSjYd60`X&|RTPO+_H}@Bb20yt%`tDT5U3!Y9sySPup4OS>o==# zyUw#opI5aK<%;1Zk-*~T1V2lK_LPD#ZU7s0%(phtk2Ao={(dCS2DFf80?Z2*`&Cc(yzM7y)MK7)!^SCN6@0wZ4ugqdLfB3=O?&A)E(&FA|{C;7-eV zN&IS1FO1?+;kxWSJt)wb$6v_)(e6M#rdEjqONa2F-VKt6EDN*0Q}yCjQ9u(kfQT#F zV|_r5OtmG#Z4J^CSk9YC>zgQ6knun(4LJ%k)i|+o+nZw%s%F`LR4GE8sQeoViu{_B z3x{080NG70Uy*ceSRu-5pv<;BPN_GiM0)Z<=?7I+963%Ur00iN1iUp_pA8`3uB667 z#Rg>ghf@jqL-=TfLOGx80yW}B1!g8Pm-CnygYGB!2g(3IWD4&5T8~`)jmT=x7R!+m z3Qn0*mYv>+?Qu#Bc@jhuCbUO_S3it<7&r#4o-5#1nf00+e)9ZRCCSxQMy+plp4$dS z6KdV)d!^%;+XZF|%{ZJmrXuwbHc1dPbokgSS0M=QVK)bD#5X+&OXMRsgihM$oje_Q zCaDmtct~A=&NjQls$@s4qy;x6gG3?ka?JfWJhZeo07E1gO>J3=U8RcrZ}cR9ZWQwG z!~!IYm?p&)oXwVHQYC;UDc2iA5(UfANs|h^8Ma`=_J_>u;fgO`A{O~u1K9`>3#u-N zPgy-ir5LKUN|aWGxfz{Rlbq+u7!rouojKUgjS4!cfH;a;QB}Se4YgcAToS4qRM$y2 zitCD#S|I#@%c}4%=ZGI0Yay~M@xIikuWLgKdwWB+r*AWX|E)f!~iEr3|9C{-yI3&BPRPisJq&(3b z1#c;SVR0E3*7wB(ch{$SoF53NjV_-XU1w2fFJ0sWTMnl!vEUG*w72xo=c+q8{viJ#c$_x1%@_gjzn z8y>{D!5e#M$|B#jOWc39Clu!OV`+Ths%_Fc_8IcHAND%g_H*-AIFW~A^9uB?2?8Al^fR)yd!HsO< zC;=;#O)`JN`wHOWV*_6vJPgk0Ir?Fj1^PUl$FrJx>#Ac8XU9F33aFVW+E#M0&4F=C z`~}&LamYaph(b*~grd%%()&&EJaO{&`NmEsFGSe?&DoQH^(>hjjRGtg3SkEI`5arN zkrgn*O@1LrPOl2ElH|Hg7&V8~i_P~g21DrHM(6391F73lO+o+;`cvaCq6LC~E9FgN zQms1QavhBay;v~!swwjQtSmk%+~l3h!e}|W*d!WXo378Er!XxT0cwU8drA=A^CB?1 z%2e?oXwwDaJ0(-?n>XI?5Zn2MY(J_JA$Y{B0=v4s`y7s8p8M-k^CNb4x@2zvTQz`) z4&rZ`O^w?CN{0zT6v%_2OuailavkYWB$is%8zn=Rw=4s&`O!mE@L?&u3@tf%pG9q+ z3UKzXj-U|zp&!vno$Z}tP|{DNg;on;c2wyAIuf{1?hkNd)-qq@=>;q!T*z`h3Qs5o zIFpwpDAidoakv+T6gmw~iNWK2ONq1qc{LE#@JQ4udSFOMm}78=0Ia*?kE~IV+zs3{ zu>~DgkozzOpw;NBV9mo$h&?8MqmJL<6Q|H_9eH7|*{1UvQx4(%H{U&;gVYmb>@>+!G?1@tl>Wfeh$zQ2e$ol_ZiWzU~?r- z<1g4}QM&^NOM@fv{rDr#Z_Vdiicx~u1W@>KfZZSJMGqCkcO9lJQzo0FSC@FupBKfu zH;gj9wx*o)K=0Bo*gI1r#!p@OWD}HrLdqzx9HS>3ITK?~&V&HUPCa*0%6FD-#_y}G zniA(hEPQNIphf5N|IWv|7W8U&q67dyS>c2VLTZbGX$-B23f&N+Qj9qR-{C4GfxKoa zP(y`NmzgaZB!nRJ5ZnSF63E_`y0;~Gie{Xs8fIHU-}yjPcr!+%5Trc^9YS`Z56h%I z4kSnirbxwd7sZ#IVBd*V(D(ux6nt+{www<^lT>`l+5IYs;FZkbHBiHAEJwRIuxX<0 z3t%y|b-c>2IM4c5nT>qm?6Bc)Q2NUrYrb4wbc{=wf${f2O@P5I9t6bu1oK|lE2%c|_WLSnJcj=;E=J?8tHfMW^OAOQ+re>zDgm84N*hcgpQR_S>X z3GHVY=@Aj<(hIE~4)vCme#{^}EihY8Fw%2j<{mLiN_f|+&v{|VE)6x425Q+6&1!<1 zn~wQ3vfT%jbTq5oTB}i%qIwo2$0$Wbc0i${2|FcSh4$*P=AFx#C6v_>YW17?)Wy*U z0vGV$@a3KI+N`$xdaM(C!fNN@acPWacF^5bqsnbnQI6lhWqjNAFVN0G#BxerbsD6J zl`cB3*PGk2 z2~6bar>^Y_0Kpp4==W9^pBPxq$T{EpX%(FdS#{u#o2$6R|VaE(7t;=pm^ZyI>MZ6FRVgy3RQ{peGefR6CtlQuV1FXTE69^_c|4-zoqxSb-!xMMrWXKodz=a zl6a?PP!t%Vi-%g&TM@cNE1?})ut`_`A-hKDoF=ssG%{x%kApxTDSAmN3WT^Cdp#J` zl*w}IQcY>(`s(sVFKXn?uXnE7L!rfe@3n#z#!nbim1u-I!RX$NGCoA6V8f!Cve`%H z`?N(a2Ok$Uo{fxh_Ghm)0%O_8%g-dh`5Dkss8S1;s&+dq-m#acJr8jXpXq+B_lyq; zO_ha)yrDeg5~acSiN(%%!{>BRZ(@sMhTj&F!a#vc3?5o1}DWeu@R>$9S8!}z3eFk!ldwo2DiH9cfG|KD!* z@edMiF~9mvm(!X`&fSV@f-WL-#-0p;C-dP}m` zx7OlabI0)aBv`QS>hM?(L(rJ2taWXIPxWYoS^*S|Gzo9O-Mm#(C`n$p+t$PUP1EX8 zG)}NmNlJD5sH!AeV?5i;aH^*YZ1B4cR_Ijyg2l)QK+z^9jB9kZ%ChK_%wDDka$ZE- z^m-C?wcn+M0=9(Wz0=zeQeLJiZ8&fW^0DpWY9X1M>s!w~Cft@^B)6?Y1bq8^6v5d~ z>vZItb=Xdse&I(h+^(^aBq&F3)1iur7|(PT|NU%#Y;}FIZ(fgEuEHaH1ec#uiV3F= z7M@wSZ?Xk1rQ5ECE_VL#$r`Z_r<~-myhD4VBrxpH84_timZkWg-VK|p zgvgoPRJ{*H@J4PG1sk)%%B6*mC@Z(3Vt{C#&^nR_z`8 zTo{b&VPQndUy+6L)PNL3W+eJ2Oj zCu{yy^Js}L*F?s9NE``-h)l6LZO#?D(BM7y1|q5!U^RC*h4pemSdTCz0IUHaLp(Xi zPK}gnq%)d+6GzL8lBCqhPuB#Z^(<#$DyRngi!z2T8;L;vj(w>(cj~d`g!UvkEu0k# zzL(p5I|e{+MUoLjWSwPNBWw(BMOxy+3nO>{vOUVr`^ f~kJv%5-WA{*smaB{HBa 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 GkpDl+oE_f) literal 0 HcmV?d00001 diff --git a/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..115e6ac0aa --- /dev/null +++ b/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/spring-boot-kubernetes/gradlew b/examples/spring-boot-kubernetes/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/examples/spring-boot-kubernetes/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/spring-boot-kubernetes/gradlew.bat b/examples/spring-boot-kubernetes/gradlew.bat new file mode 100644 index 0000000000..e95643d6a2 --- /dev/null +++ b/examples/spring-boot-kubernetes/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/spring-boot-kubernetes/mvnw b/examples/spring-boot-kubernetes/mvnw new file mode 100755 index 0000000000..961a825001 --- /dev/null +++ b/examples/spring-boot-kubernetes/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/spring-boot-kubernetes/mvnw.cmd b/examples/spring-boot-kubernetes/mvnw.cmd new file mode 100755 index 0000000000..03d90e960b --- /dev/null +++ b/examples/spring-boot-kubernetes/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml new file mode 100644 index 0000000000..78537f18bf --- /dev/null +++ b/examples/spring-boot-kubernetes/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + example + spring-boot-k8s-example + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.0.4.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + + 1.8 + + + + + + com.google.cloud.tools + jib-maven-plugin + 0.9.10 + + + + diff --git a/examples/spring-boot-kubernetes/src/main/java/hello/Application.java b/examples/spring-boot-kubernetes/src/main/java/hello/Application.java new file mode 100644 index 0000000000..096bd4c9d8 --- /dev/null +++ b/examples/spring-boot-kubernetes/src/main/java/hello/Application.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java b/examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..8b9ace4914 --- /dev/null +++ b/examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Greetings from Kubernetes!"; + } +} From e9e55b4800112cfc0d08ea9569e9818b3880d0c3 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 31 Aug 2018 15:01:40 -0400 Subject: [PATCH 0180/2020] Add skip parameter to base plugin configuration (#911) * Add skip parameter to base plugin configuration - can use system property -Djib.skip=true to skip if part of lifecycle - still requires "required parameters" on jib to be configured even if skipping, since maven doesn't provide a api for actual goal skipping --- .../tools/jib/maven/BuildDockerMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildImageMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildTarMojo.java | 4 ++ .../tools/jib/maven/DockerContextMojo.java | 4 ++ .../jib/maven/JibPluginConfiguration.java | 7 +++ .../maven/BuildDockerMojoIntegrationTest.java | 8 +++ .../maven/BuildImageMojoIntegrationTest.java | 8 +++ .../maven/BuildTarMojoIntegrationTest.java | 8 +++ .../DockerContextMojoIntegrationTest.java | 7 +++ .../tools/jib/maven/SkippedGoalVerifier.java | 51 +++++++++++++++++++ 10 files changed, 105 insertions(+) create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 5cecb6ed35..ca47ba60af 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -43,6 +43,10 @@ public class BuildDockerMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if (isSkipped()) { + getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); + return; + } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 7ee26587c4..36486f18f2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -48,6 +48,10 @@ public class BuildImageMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { + if (isSkipped()) { + getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); + return; + } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index c27ee3a5e7..be3f301f57 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -45,6 +45,10 @@ public class BuildTarMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if (isSkipped()) { + getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); + return; + } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 7cc54807a7..dee00aee5a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -50,6 +50,10 @@ public class DockerContextMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + if (isSkipped()) { + getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); + return; + } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 8233883703..ad16af6450 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -170,6 +170,9 @@ public static class ContainerParameters { @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) private String extraDirectory; + @Parameter(defaultValue = "false", property = "jib.skip") + private boolean skip; + @Nullable @Component protected SettingsDecrypter settingsDecrypter; /** Default constructor handles setting up auth property descriptors. */ @@ -302,6 +305,10 @@ Path getExtraDirectory() { return Paths.get(Preconditions.checkNotNull(extraDirectory)); } + boolean isSkipped() { + return skip; + } + SettingsDecrypter getSettingsDecrypter() { return Preconditions.checkNotNull(settingsDecrypter); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 6944a015f4..b062a4283d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -42,6 +42,9 @@ public class BuildDockerMojoIntegrationTest { public static final TestProject defaultTargetTestProject = new TestProject(testPlugin, "default-target"); + @ClassRule + public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + /** * Builds and runs jib:buildDocker on a project at {@code projectRoot} pushing to {@code * imageReference}. @@ -110,4 +113,9 @@ public void testExecute_defaultTarget() defaultTargetTestProject.getProjectRoot(), "default-target-name:default-target-version")); } + + @Test + public void testExecute_skipJibGoal() throws VerificationException, IOException { + SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildDockerMojo.GOAL_NAME); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 214ece9d98..1e5341544d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -59,6 +59,9 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject emptyTestProject = new TestProject(testPlugin, "empty"); + @ClassRule + public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + @ClassRule public static final TestProject defaultTargetTestProject = new TestProject(testPlugin, "default-target"); @@ -270,4 +273,9 @@ public void testExecute_complex_sameFromAndToRegistry() "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); } + + @Test + public void testExecute_skipJibGoal() throws VerificationException, IOException { + SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildImageMojo.GOAL_NAME); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index c979fd6195..3245f35578 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -32,6 +32,9 @@ public class BuildTarMojoIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + @ClassRule + public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + /** * Builds and runs jib:buildTar on a project at {@code projectRoot} pushing to {@code * imageReference}. @@ -67,4 +70,9 @@ public void testExecute_simple() throws VerificationException, IOException, Inte new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } + + @Test + public void testExecute_skipJibGoal() throws VerificationException, IOException { + SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildTarMojo.GOAL_NAME); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index fef2748db7..b6eb974e29 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -38,6 +38,9 @@ public class DockerContextMojoIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + @ClassRule + public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + @Test public void testExecute() throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); @@ -72,4 +75,8 @@ public void testExecute() throws VerificationException, IOException, Interrupted Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); } + + public void testExecute_skipJibGoal() throws VerificationException, IOException { + SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildDockerMojo.GOAL_NAME); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java new file mode 100644 index 0000000000..426ef839c4 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; + +/** A simple verifier utility to test goal skipping accross all our jib goals. */ +class SkippedGoalVerifier { + + /** Verify that a jib goal is skipped */ + static void verifyGoalIsSkipped(TestProject testProject, String goal) + throws VerificationException, IOException { + Verifier verifier = new Verifier(testProject.getProjectRoot().toString()); + verifier.setAutoclean(false); + verifier.setSystemProperty("jib.skip", "true"); + + verifier.executeGoal("jib:" + goal); + + Path logFile = Paths.get(verifier.getBasedir(), verifier.getLogFileName()); + Assert.assertThat( + new String(Files.readAllBytes(logFile), StandardCharsets.UTF_8), + CoreMatchers.containsString( + "[INFO] Skipping containerization because jib-maven-plugin: skip = true\n" + + "[INFO] ------------------------------------------------------------------------\n" + + "[INFO] BUILD SUCCESS")); + } + + private SkippedGoalVerifier() {} +} From 1dd28e1224158bf9cb9d01e6a5edb6423c2d8bbf Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 31 Aug 2018 16:33:42 -0400 Subject: [PATCH 0181/2020] Update copyright header (#920) * Update copyright header * Fix Google Inc too --- examples/helloworld/src/main/java/example/HelloWorld.java | 2 +- .../java/com/google/cloud/tools/jib/Command.java | 2 +- .../java/com/google/cloud/tools/jib/EmptyJibLogger.java | 2 +- .../google/cloud/tools/jib/IntegrationTestingConfiguration.java | 2 +- .../cloud/tools/jib/builder/BuildStepsIntegrationTest.java | 2 +- .../registry/AuthenticationMethodRetrieverIntegrationTest.java | 2 +- .../cloud/tools/jib/registry/BlobCheckerIntegrationTest.java | 2 +- .../cloud/tools/jib/registry/BlobPullerIntegrationTest.java | 2 +- .../cloud/tools/jib/registry/BlobPusherIntegrationTest.java | 2 +- .../java/com/google/cloud/tools/jib/registry/LocalRegistry.java | 2 +- .../cloud/tools/jib/registry/ManifestPullerIntegrationTest.java | 2 +- .../cloud/tools/jib/registry/ManifestPusherIntegrationTest.java | 2 +- .../jib/registry/RegistryAuthenticatorIntegrationTest.java | 2 +- .../credentials/DockerCredentialHelperIntegrationTest.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/JibLogger.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/ProjectInfo.java | 2 +- jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java | 2 +- .../main/java/com/google/cloud/tools/jib/async/AsyncStep.java | 2 +- .../main/java/com/google/cloud/tools/jib/async/AsyncSteps.java | 2 +- .../java/com/google/cloud/tools/jib/async/NonBlockingSteps.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/blob/Blob.java | 2 +- .../java/com/google/cloud/tools/jib/blob/BlobDescriptor.java | 2 +- .../main/java/com/google/cloud/tools/jib/blob/BlobWriter.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/blob/Blobs.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java | 2 +- .../java/com/google/cloud/tools/jib/blob/InputStreamBlob.java | 2 +- .../main/java/com/google/cloud/tools/jib/blob/StringBlob.java | 2 +- .../main/java/com/google/cloud/tools/jib/blob/WriterBlob.java | 2 +- .../java/com/google/cloud/tools/jib/builder/BuildSteps.java | 2 +- .../cloud/tools/jib/builder/steps/AuthenticatePushStep.java | 2 +- .../jib/builder/steps/BuildAndCacheApplicationLayerStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/BuildImageStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/FinalizingStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/LoadDockerStep.java | 2 +- .../tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java | 2 +- .../jib/builder/steps/PullAndCacheBaseImageLayersStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PullBaseImageStep.java | 2 +- .../com/google/cloud/tools/jib/builder/steps/PushBlobStep.java | 2 +- .../tools/jib/builder/steps/PushContainerConfigurationStep.java | 2 +- .../com/google/cloud/tools/jib/builder/steps/PushImageStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PushLayersStep.java | 2 +- .../jib/builder/steps/RetrieveRegistryCredentialsStep.java | 2 +- .../com/google/cloud/tools/jib/builder/steps/StepsRunner.java | 2 +- .../google/cloud/tools/jib/builder/steps/WriteTarFileStep.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/cache/Cache.java | 2 +- .../cloud/tools/jib/cache/CacheDirectoryCreationException.java | 2 +- .../cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java | 2 +- .../main/java/com/google/cloud/tools/jib/cache/CacheFiles.java | 2 +- .../java/com/google/cloud/tools/jib/cache/CacheMetadata.java | 2 +- .../cloud/tools/jib/cache/CacheMetadataCorruptedException.java | 2 +- .../google/cloud/tools/jib/cache/CacheMetadataTranslator.java | 2 +- .../main/java/com/google/cloud/tools/jib/cache/CacheReader.java | 2 +- .../main/java/com/google/cloud/tools/jib/cache/CacheWriter.java | 2 +- .../main/java/com/google/cloud/tools/jib/cache/CachedLayer.java | 2 +- .../google/cloud/tools/jib/cache/CachedLayerWithMetadata.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/cache/Caches.java | 2 +- .../java/com/google/cloud/tools/jib/cache/LayerMetadata.java | 2 +- .../tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java | 2 +- .../cache/json/CacheMetadataLayerPropertiesObjectTemplate.java | 2 +- .../cloud/tools/jib/cache/json/CacheMetadataTemplate.java | 2 +- .../cloud/tools/jib/configuration/BuildConfiguration.java | 2 +- .../cloud/tools/jib/configuration/CacheConfiguration.java | 2 +- .../cloud/tools/jib/configuration/ContainerConfiguration.java | 2 +- .../cloud/tools/jib/configuration/ImageConfiguration.java | 2 +- .../cloud/tools/jib/configuration/LayerConfiguration.java | 2 +- .../java/com/google/cloud/tools/jib/configuration/Port.java | 2 +- .../cloud/tools/jib/configuration/credentials/Credential.java | 2 +- .../jib/configuration/credentials/CredentialRetriever.java | 2 +- .../java/com/google/cloud/tools/jib/docker/DockerClient.java | 2 +- .../google/cloud/tools/jib/docker/ImageToTarballTranslator.java | 2 +- .../cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java | 2 +- .../com/google/cloud/tools/jib/filesystem/DirectoryWalker.java | 2 +- .../com/google/cloud/tools/jib/filesystem/FileOperations.java | 2 +- .../com/google/cloud/tools/jib/filesystem/PathConsumer.java | 2 +- .../com/google/cloud/tools/jib/filesystem/UserCacheHome.java | 2 +- .../cloud/tools/jib/frontend/CredentialRetrieverFactory.java | 2 +- .../com/google/cloud/tools/jib/frontend/ExposedPortsParser.java | 2 +- .../cloud/tools/jib/frontend/JavaDockerContextGenerator.java | 2 +- .../cloud/tools/jib/frontend/JavaEntrypointConstructor.java | 2 +- .../cloud/tools/jib/frontend/JavaLayerConfigurations.java | 2 +- .../com/google/cloud/tools/jib/frontend/MainClassFinder.java | 2 +- .../google/cloud/tools/jib/hash/CountingDigestOutputStream.java | 2 +- .../java/com/google/cloud/tools/jib/http/Authorization.java | 2 +- .../java/com/google/cloud/tools/jib/http/Authorizations.java | 2 +- .../java/com/google/cloud/tools/jib/http/BlobHttpContent.java | 2 +- .../main/java/com/google/cloud/tools/jib/http/Connection.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/http/Request.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/http/Response.java | 2 +- .../java/com/google/cloud/tools/jib/image/DescriptorDigest.java | 2 +- .../java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/image/Image.java | 2 +- .../main/java/com/google/cloud/tools/jib/image/ImageFormat.java | 2 +- .../main/java/com/google/cloud/tools/jib/image/ImageLayers.java | 2 +- .../java/com/google/cloud/tools/jib/image/ImageReference.java | 2 +- .../cloud/tools/jib/image/InvalidImageReferenceException.java | 2 +- .../src/main/java/com/google/cloud/tools/jib/image/Layer.java | 2 +- .../cloud/tools/jib/image/LayerCountMismatchException.java | 2 +- .../main/java/com/google/cloud/tools/jib/image/LayerEntry.java | 2 +- .../cloud/tools/jib/image/LayerPropertyNotFoundException.java | 2 +- .../java/com/google/cloud/tools/jib/image/ReferenceLayer.java | 2 +- .../google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java | 2 +- .../google/cloud/tools/jib/image/ReproducibleLayerBuilder.java | 2 +- .../java/com/google/cloud/tools/jib/image/UnwrittenLayer.java | 2 +- .../image/json/BadContainerConfigurationFormatException.java | 2 +- .../cloud/tools/jib/image/json/BuildableManifestTemplate.java | 2 +- .../tools/jib/image/json/ContainerConfigurationTemplate.java | 2 +- .../tools/jib/image/json/DescriptorDigestDeserializer.java | 2 +- .../cloud/tools/jib/image/json/DescriptorDigestSerializer.java | 2 +- .../com/google/cloud/tools/jib/image/json/HistoryEntry.java | 2 +- .../cloud/tools/jib/image/json/ImageToJsonTranslator.java | 2 +- .../cloud/tools/jib/image/json/JsonToImageTranslator.java | 2 +- .../com/google/cloud/tools/jib/image/json/ManifestTemplate.java | 2 +- .../google/cloud/tools/jib/image/json/OCIManifestTemplate.java | 2 +- .../tools/jib/image/json/UnknownManifestFormatException.java | 2 +- .../google/cloud/tools/jib/image/json/V21ManifestTemplate.java | 2 +- .../google/cloud/tools/jib/image/json/V22ManifestTemplate.java | 2 +- .../main/java/com/google/cloud/tools/jib/json/JsonTemplate.java | 2 +- .../com/google/cloud/tools/jib/json/JsonTemplateMapper.java | 2 +- .../com/google/cloud/tools/jib/json/ListOfJsonTemplate.java | 2 +- .../main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java | 2 +- .../java/com/google/cloud/tools/jib/ncache/CacheStorage.java | 2 +- .../main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java | 2 +- .../com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java | 2 +- .../com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java | 2 +- .../cloud/tools/jib/registry/AuthenticationMethodRetriever.java | 2 +- .../java/com/google/cloud/tools/jib/registry/BlobChecker.java | 2 +- .../java/com/google/cloud/tools/jib/registry/BlobPuller.java | 2 +- .../java/com/google/cloud/tools/jib/registry/BlobPusher.java | 2 +- .../java/com/google/cloud/tools/jib/registry/ErrorCodes.java | 2 +- .../cloud/tools/jib/registry/InsecureRegistryException.java | 2 +- .../com/google/cloud/tools/jib/registry/ManifestPuller.java | 2 +- .../com/google/cloud/tools/jib/registry/ManifestPusher.java | 2 +- .../com/google/cloud/tools/jib/registry/RegistryAliasGroup.java | 2 +- .../jib/registry/RegistryAuthenticationFailedException.java | 2 +- .../google/cloud/tools/jib/registry/RegistryAuthenticator.java | 2 +- .../com/google/cloud/tools/jib/registry/RegistryClient.java | 2 +- .../tools/jib/registry/RegistryCredentialsNotSentException.java | 2 +- .../google/cloud/tools/jib/registry/RegistryEndpointCaller.java | 2 +- .../cloud/tools/jib/registry/RegistryEndpointProvider.java | 2 +- .../tools/jib/registry/RegistryEndpointRequestProperties.java | 2 +- .../google/cloud/tools/jib/registry/RegistryErrorException.java | 2 +- .../cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java | 2 +- .../com/google/cloud/tools/jib/registry/RegistryException.java | 2 +- .../cloud/tools/jib/registry/RegistryNoResponseException.java | 2 +- .../cloud/tools/jib/registry/RegistryUnauthorizedException.java | 2 +- .../cloud/tools/jib/registry/UnexpectedBlobDigestException.java | 2 +- .../registry/credentials/CredentialHelperNotFoundException.java | 2 +- .../CredentialHelperUnhandledServerUrlException.java | 2 +- .../jib/registry/credentials/CredentialRetrievalException.java | 2 +- .../cloud/tools/jib/registry/credentials/DockerConfig.java | 2 +- .../registry/credentials/DockerConfigCredentialRetriever.java | 2 +- .../tools/jib/registry/credentials/DockerCredentialHelper.java | 2 +- .../tools/jib/registry/credentials/RegistryCredentials.java | 2 +- .../jib/registry/credentials/json/DockerConfigTemplate.java | 2 +- .../cloud/tools/jib/registry/json/ErrorEntryTemplate.java | 2 +- .../cloud/tools/jib/registry/json/ErrorResponseTemplate.java | 2 +- .../java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java | 2 +- .../src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java | 2 +- .../java/com/google/cloud/tools/jib/builder/TestJibLogger.java | 2 +- .../builder/steps/BuildAndCacheApplicationLayerStepTest.java | 2 +- .../cloud/tools/jib/builder/steps/BuildImageStepTest.java | 2 +- .../jib/builder/steps/RetrieveRegistryCredentialsStepTest.java | 2 +- .../java/com/google/cloud/tools/jib/cache/CacheFilesTest.java | 2 +- .../com/google/cloud/tools/jib/cache/CacheMetadataTest.java | 2 +- .../cloud/tools/jib/cache/CacheMetadataTranslatorTest.java | 2 +- .../java/com/google/cloud/tools/jib/cache/CacheReaderTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/cache/CacheTest.java | 2 +- .../java/com/google/cloud/tools/jib/cache/CacheWriterTest.java | 2 +- .../java/com/google/cloud/tools/jib/cache/CachedLayerTest.java | 2 +- .../cloud/tools/jib/cache/CachedLayerWithMetadataTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/cache/CachesTest.java | 2 +- .../cloud/tools/jib/cache/PlatformSpecificMetadataJson.java | 2 +- .../cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java | 2 +- .../cloud/tools/jib/configuration/BuildConfigurationTest.java | 2 +- .../cloud/tools/jib/configuration/CacheConfigurationTest.java | 2 +- .../tools/jib/configuration/ContainerConfigurationTest.java | 2 +- .../tools/jib/configuration/credentials/CredentialTest.java | 2 +- .../com/google/cloud/tools/jib/docker/DockerClientTest.java | 2 +- .../cloud/tools/jib/docker/ImageToTarballTranslatorTest.java | 2 +- .../tools/jib/docker/json/DockerLoadManifestTemplateTest.java | 2 +- .../google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java | 2 +- .../google/cloud/tools/jib/filesystem/FileOperationsTest.java | 2 +- .../google/cloud/tools/jib/filesystem/UserCacheHomeTest.java | 2 +- .../tools/jib/frontend/CredentialRetrieverFactoryTest.java | 2 +- .../google/cloud/tools/jib/frontend/ExposedPortsParserTest.java | 2 +- .../tools/jib/frontend/JavaDockerContextGeneratorTest.java | 2 +- .../cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java | 2 +- .../google/cloud/tools/jib/frontend/MainClassFinderTest.java | 2 +- .../cloud/tools/jib/hash/CountingDigestOutputStreamTest.java | 2 +- .../java/com/google/cloud/tools/jib/http/ConnectionTest.java | 2 +- .../java/com/google/cloud/tools/jib/http/MockConnection.java | 2 +- .../test/java/com/google/cloud/tools/jib/http/RequestTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/http/ResponseTest.java | 2 +- .../java/com/google/cloud/tools/jib/http/TestWebServer.java | 2 +- .../google/cloud/tools/jib/http/WithServerConnectionTest.java | 2 +- .../com/google/cloud/tools/jib/image/DescriptorDigestTest.java | 2 +- .../java/com/google/cloud/tools/jib/image/ImageLayersTest.java | 2 +- .../com/google/cloud/tools/jib/image/ImageReferenceTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/image/ImageTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/image/LayerTest.java | 2 +- .../cloud/tools/jib/image/ReproducibleLayerBuilderTest.java | 2 +- .../jib/image/json/ContainerConfigurationTemplateTest.java | 2 +- .../cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java | 2 +- .../cloud/tools/jib/image/json/JsonToImageTranslatorTest.java | 2 +- .../cloud/tools/jib/image/json/OCIManifestTemplateTest.java | 2 +- .../cloud/tools/jib/image/json/V21ManifestTemplateTest.java | 2 +- .../cloud/tools/jib/image/json/V22ManifestTemplateTest.java | 2 +- .../com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java | 2 +- .../google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java | 2 +- .../google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java | 2 +- .../tools/jib/registry/AuthenticationMethodRetrieverTest.java | 2 +- .../com/google/cloud/tools/jib/registry/BlobCheckerTest.java | 2 +- .../com/google/cloud/tools/jib/registry/BlobPullerTest.java | 2 +- .../com/google/cloud/tools/jib/registry/BlobPusherTest.java | 2 +- .../com/google/cloud/tools/jib/registry/ManifestPullerTest.java | 2 +- .../com/google/cloud/tools/jib/registry/ManifestPusherTest.java | 2 +- .../google/cloud/tools/jib/registry/RegistryAliasGroupTest.java | 2 +- .../cloud/tools/jib/registry/RegistryAuthenticatorTest.java | 2 +- .../com/google/cloud/tools/jib/registry/RegistryClientTest.java | 2 +- .../cloud/tools/jib/registry/RegistryEndpointCallerTest.java | 2 +- .../tools/jib/registry/RegistryErrorExceptionBuilderTest.java | 2 +- .../credentials/DockerConfigCredentialRetrieverTest.java | 2 +- .../cloud/tools/jib/registry/credentials/DockerConfigTest.java | 2 +- .../com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java | 2 +- .../google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/TestProject.java | 2 +- .../default-target/src/main/java/com/test/HelloWorld.java | 2 +- .../projects/simple/src/main/java/com/test/HelloWorld.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/AuthParameters.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/BuildImageTask.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../com/google/cloud/tools/jib/gradle/ContainerParameters.java | 2 +- .../com/google/cloud/tools/jib/gradle/DockerContextTask.java | 2 +- .../cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java | 2 +- .../cloud/tools/jib/gradle/GradleLayerConfigurations.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/ImageParameters.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/JibExtension.java | 2 +- .../main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java | 2 +- .../cloud/tools/jib/gradle/PluginConfigurationProcessor.java | 2 +- .../gradle-plugins/com.google.cloud.tools.jib.properties | 2 +- .../cloud/tools/jib/gradle/GradleProjectPropertiesTest.java | 2 +- .../com/google/cloud/tools/jib/gradle/JibExtensionTest.java | 2 +- .../java/com/google/cloud/tools/jib/gradle/JibPluginTest.java | 2 +- .../tools/jib/gradle/PluginConfigurationProcessorTest.java | 2 +- jib-maven-plugin/config/copyright.header | 2 +- .../java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 2 +- .../java/com/google/cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../java/com/google/cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../com/google/cloud/tools/jib/maven/DockerContextMojo.java | 2 +- .../google/cloud/tools/jib/maven/JibPluginConfiguration.java | 2 +- .../cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java | 2 +- .../java/com/google/cloud/tools/jib/maven/MavenJibLogger.java | 2 +- .../google/cloud/tools/jib/maven/MavenLayerConfigurations.java | 2 +- .../google/cloud/tools/jib/maven/MavenProjectProperties.java | 2 +- .../cloud/tools/jib/maven/MavenSettingsServerCredentials.java | 2 +- .../cloud/tools/jib/maven/PluginConfigurationProcessor.java | 2 +- .../cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java | 2 +- .../cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java | 2 +- .../cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java | 2 +- .../cloud/tools/jib/maven/JibPluginConfigurationTest.java | 2 +- .../cloud/tools/jib/maven/MavenLayerConfigurationsTest.java | 2 +- .../cloud/tools/jib/maven/MavenProjectPropertiesTest.java | 2 +- .../tools/jib/maven/MavenSettingsServerCredentialsTest.java | 2 +- .../cloud/tools/jib/maven/PluginConfigurationProcessorTest.java | 2 +- .../test/java/com/google/cloud/tools/jib/maven/TestPlugin.java | 2 +- .../test/java/com/google/cloud/tools/jib/maven/TestProject.java | 2 +- .../default-target/src/main/java/com/test/HelloWorld.java | 2 +- .../projects/simple/src/main/java/com/test/HelloWorld.java | 2 +- .../com/google/cloud/tools/jib/plugins/common/AuthProperty.java | 2 +- .../tools/jib/plugins/common/BuildStepsExecutionException.java | 2 +- .../google/cloud/tools/jib/plugins/common/BuildStepsRunner.java | 2 +- .../jib/plugins/common/ConfigurationPropertyValidator.java | 2 +- .../tools/jib/plugins/common/DefaultCredentialRetrievers.java | 2 +- .../cloud/tools/jib/plugins/common/HelpfulSuggestions.java | 2 +- .../tools/jib/plugins/common/MainClassInferenceException.java | 2 +- .../cloud/tools/jib/plugins/common/MainClassResolver.java | 2 +- .../cloud/tools/jib/plugins/common/ProjectProperties.java | 2 +- .../java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java | 2 +- .../cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java | 2 +- .../jib/plugins/common/ConfigurationPropertyValidatorTest.java | 2 +- .../jib/plugins/common/DefaultCredentialRetrieversTest.java | 2 +- .../cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java | 2 +- .../cloud/tools/jib/plugins/common/MainClassResolverTest.java | 2 +- .../com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java | 2 +- 287 files changed, 287 insertions(+), 287 deletions(-) diff --git a/examples/helloworld/src/main/java/example/HelloWorld.java b/examples/helloworld/src/main/java/example/HelloWorld.java index a6b526cfd8..e4185f27f7 100644 --- a/examples/helloworld/src/main/java/example/HelloWorld.java +++ b/examples/helloworld/src/main/java/example/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java index 02a9da20eb..6aaca14f8e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java index ed8148c97d..760c7034b2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java index 64b92854ff..f39ef24abc 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index a3324416c3..e19795cc0c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 80033d5fcd..e4d3441b5d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 4ca75a8cee..fbe5ee14df 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index eb78e40f49..703bcc0d83 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index cd7c352bf5..c9334887e5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index d2e867bc0c..04f2bb1920 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 4f7ef6c60a..8ea84df34a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 5d4e204ab3..33e752d21a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index c8ca3b37a8..2f9502a916 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 086edc597b..581469be27 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java index 1629c57d20..36ad403c86 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java index c9ae060cb9..72d80c55be 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java index f9aeac0a70..2b7f7fd0ad 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java index f4d04248d6..bab5363449 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java index 9fdf13e093..0bb3771a0e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java index 0258dcd120..d65b3f5c9a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java index 38bf268460..eb1d7d605e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java index e41f0286e2..0b067cc912 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java index 5a9fd0ae0e..acf393a524 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 6ead1eaa8b..21a253001c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java index 745774eb7c..5c978c2dfb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java index d616d32230..46273b691f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java index e0c4386c8c..62e07e1297 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java index d7b16cfac1..c47dc3e037 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 851eadc67c..d57612f27c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 8d0f985eca..e5f86f69cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index e70a9e60b1..a3653c2ea7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 9b8d4b292a..07ee21fd2c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index 17eb0ce79a..00a2715ba6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 0b7a989f1a..29ab413c0f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 8d68f8141d..f5425ba5c6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index c5c926a19e..aeabfd4bc1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 95f0c2eadc..7833b34630 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 5fb0210d00..61a15431a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 5d3079c630..f20b410c4d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 92d7bf0266..93758461e3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index f890e9bd06..9ebcb31e52 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index a8393dca70..3c2b02c3b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 435a51d98d..67153a0602 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 6181c7bc5c..ba9eb3a967 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index e24fe304ea..e9bfd08954 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java index b017b0df21..f15ae06189 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java index 44d29cf4d0..df9ff6b5ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java index a4e46faad5..10ab5a8f47 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java index fd8e38a74f..08548da129 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java index c1ade9954c..c92590a68f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java index 36c5175bef..9064c3bac6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java index 4ae37b0a05..572d0040ba 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java index 69b55f06ae..9aa1538684 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index e1cf18184f..cf0d33a199 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java index c979ac62c6..c087c78766 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java index 877a31ae37..3c5a5294f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java index 5350b6313b..e16c467d61 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java index 6f678b18d5..3c1b982a3a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java index 67e1d65f3f..beecb4d5fa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java index 302940c275..48f8f7580f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 129156b600..24cc8dfd24 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java index 5d665cae98..f8250d918c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 2170ca4e4b..5193f92208 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index 9f4f4de9ca..0ab65af9d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index b87774e6a8..c6555e7fa7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java index 88f7f90cfb..734743bec3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index 03c64fb296..a516a2c2d8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java index cb863e209b..a607c817f2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 1ffe8ae5aa..8420aeb8ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java index 2de8a737f1..81ec90606f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java index 78c9d44494..07ad7d0230 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java index c40244c480..9ba412ba6c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java index 2f93e5550e..75f9df0917 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java index 73747cdc98..993d0f6b4a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java index 3fd83824ce..92ef1d49cc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index dca1288e65..1c2b537002 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java index be451081fc..9294f037e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 8369034bef..e4d593e0f0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index fdafd8d958..71b3569edd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 5bcafc7023..82d25488fd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index e6e6e3fc28..58f3a71ea2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java index 5ee76c3f50..b2e872c2a2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index bb5e76cc60..0bb13e407d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java index 6e5c61c031..e8ae835a23 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java index 54be335739..b61dd9a96a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 8beae8bbcf..5602b53622 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java index 6c00d8ace3..e6ef54f7f0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java index b86afd388a..a9f66d235b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java index aa786254e7..974f431fe6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java index 0de5df3ebb..0a88a112bc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 75aae81807..f4b99da095 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java index ed80a0346d..b5ff8dcb4a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. all rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java index 8f6326d3ca..bd92686353 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index cef5a181ff..14407866e4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java index f3267334a1..225c8ee75d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java index af4ae5d15c..d60f4274e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java index 33772fd7d1..a18b032f02 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 1c159b8286..9fcc743cae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java index a438eab78c..3424bbd741 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java index 410c8bb643..fb33a2fc06 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java index 699933a93c..fc6df8b49d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 1a07b515f6..2349422f21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java index 40d46ff9fd..126d4c9bf5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java index 5b7d20caef..0497db95f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index bc6d2a4441..b7aa818591 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 649061e03b..e1115bbcc0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java index 2cafda59da..95e61b97ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java index 52c5746554..83c28fd37f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java index 267c5c7bee..edf9bb9bab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 1795e4cda8..88ca26f703 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index fd8c7ec8f6..7d14fad871 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java index 5a6d8717af..5abeeb7c10 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java index 806b24f295..9a27048c35 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java index 37b936e052..1651d343c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java index 23aa89ebf2..03d72a09c6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java index fc6db4cd6d..7707b90eff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java index 612c3030c0..9b72c1bc9e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index ec82105b68..82042ef3dc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java index 93ec52d80d..e1d58de333 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java index 0369e0b1d3..816e866457 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 1846945169..13740e79b0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java index 48b90d3dc5..4273c3458d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java index ae80bfe570..0c388fed02 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java index f05050079a..e77017fc29 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index 20da894e47..8ed80541f2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index ed117fea53..e439a97c61 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index 842bf001eb..1dccf926b2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 515b450b99..5316ff0268 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java index 6b45b3953f..23515a75a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java index 2459c3f839..14d100ef47 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index 48114c893b..a5ba631c57 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 856fcba5c5..47aab078f5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java index 58c988414d..112d5c494a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java index a9abd630b8..c00785febe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 49d0a4232e..9785ed8b42 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index c8e698dae1..568fa6200d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java index 11fa8623c1..4e95f9baf0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 7a35a64ce2..b24428e441 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index bcf7f99001..c2dda8ecf9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index c7b59d6c6a..4483344f74 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java index 78bdfa50e2..757f71b65c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java index af2ee867fb..7126750626 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java index c14cda8208..8a4e307030 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java index 1bfe1caae5..6022614ce5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java index d65dd32071..5897e4d2e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java index cba9273eb6..98ca83fe5b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java index affbe77953..859aff2902 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java index da173514ce..f009a37888 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java index f07da2fe25..14089fe1b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index ac572686ac..64cdb6bb28 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 5337853c99..05027c641c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index d86a03c11b..d837db1d6a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java index 750ccf3010..e2fc2ccb14 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index e3d6d1ecca..2895b18413 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java index 5d26bdc695..09257600ad 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java index 8dd07a5ffa..20663bfc23 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index fc4b357623..832bd04de5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index a7e75ebd54..7bee8fe1de 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java index d40d9e92c2..f535b58e56 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 943c218951..ae1bc4bfb9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 4b1461f191..44575cff90 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index b7e7dc8714..29120a2e8f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java index 8d39def005..0dabe7836b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java index ea66f3a641..fdf3b08670 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java index f99b52f5b1..1d67705029 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java index c66041d657..c657117b4d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index c00cc3c54b..c24f286ad2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java index 3f70700e7e..a447d08baa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java index dd3fc1429d..38effaba14 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java index 750d918ca8..eed8db7262 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java index d79ec3fcf8..ca6290ad23 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java index d90c6a7da1..2bd35ae14c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java index 37574db6da..b5e2c1a1cb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 8995921957..8db52564e0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java index 614bb5326d..61383c2ac1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index 24144177c2..3e6494d28e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index ef46f29e21..7d8b345f52 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 8eecd61854..cf24b94078 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java index 5716bba928..3eeaac9009 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java index 9ff48bc797..735574e30b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java index 5f605f258d..e37009e3b1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java index b1471799dd..25863a610f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java index 36f9c2d0b6..2da1bcfb24 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 85869bff63..9ad70785d5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index c339020e9c..8837e25e93 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 69f3b92ec8..6a3f1401ae 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index 5ea9a8d8bc..99898a5cca 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 0279d9875d..0182f3677e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index 9cece042c2..865a63136f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index 5eb6160cf6..99e6e9946b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java index 3b6665168c..3470ba8d19 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java index d142022d23..012ee51df6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java index 6a0bb53845..bde361410e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index ed590ff010..3561e58b00 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index cbfd61ab98..0a3ef3a125 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java index 4341bd1101..58461734f4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index 7d66fce37e..497745e110 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java index d07008e262..7c760c5f74 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 6710b13ad8..c0f4245d0c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java index 5dc765c49d..3e70fae524 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index f9a84496c4..9c86e41f30 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index d25654429c..5029b13990 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 05a87808dc..ea7c5d0b48 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 4b0dd995f6..50faad39b8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java index b7c0bacd0e..15c5e0dbf9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index 234aa226d6..95db92aeda 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index 2163081509..91f4f0f99a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index be018515d0..a942f4fa45 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java index 3f8f302e46..c189318296 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java index a80e2df946..bb4853a36f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 62698a2140..27ecd37aea 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 2bbe2d4392..2ec250c417 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index 8345f7a165..b3ed09c768 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index fed49e88f3..e24444aba1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index 99e694cfe4..57d10cd471 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index f2de99e8a2..29a83a1e78 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java index 90dc98e2d8..7481842154 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index e843a4488a..e13d4ecf71 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 8d564318c1..4d9f8ad09f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index a6a4685c2a..7c781d59e7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java index cafc90c7b7..f32311e3ad 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 92a8b16f31..a5e9ce58f8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index e3a59d3744..8a13d2d473 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index a5cceb87ef..2ea09946a2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. All rights reserved. + * Copyright 2017 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 1c507ed355..9a3ff565da 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java index 966c9ea421..536024bb53 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java index 5fc6d1cf3a..a49407401d 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 0bd8ae8d2f..2102d6331b 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index 757dfc0914..e76aa7fca0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index ea8629ba28..c9e1599cc0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 46c4f76ef0..343ba8e262 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 9b722be8d0..ecf3fb1a84 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 8f2388e030..b67eff1a1a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 4ec6f0e3d8..871463a28c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java index ca978ef3bb..d530000097 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java index 416f8fd3dc..be4e166909 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 67a01f17df..299d5bcbbb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java index 26ae477042..9616762248 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index feaf68e56a..00c57cb419 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 93d2cef04a..cb94d3ad33 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index c59c518a52..a9b15776cc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties b/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties index 20d8a6de70..9f1e4df0c3 100644 --- a/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties +++ b/jib-gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.google.cloud.tools.jib.properties @@ -1,5 +1,5 @@ # -# Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index d4ca36b881..ae3ac0b4b9 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 371d4d09ef..c175fe71cc 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 72e9e8aa96..d3d85b7ae5 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 44b6ce8f69..26294b0253 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/jib-maven-plugin/config/copyright.header b/jib-maven-plugin/config/copyright.header index bd7ddacfd4..c864889627 100644 --- a/jib-maven-plugin/config/copyright.header +++ b/jib-maven-plugin/config/copyright.header @@ -1,5 +1,5 @@ ^/\*$ -^ \* Copyright 2018 Google LLC\. All rights reserved\.$ + * Copyright 2018 Google LLC. ^ \*$ ^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$ ^ \* use this file except in compliance with the License\. You may obtain a copy of$ diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index ca47ba60af..429ec83d94 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 36486f18f2..61745ff779 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index be3f301f57..cf31bf9301 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index dee00aee5a..c4f4028235 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index ad16af6450..27abed44e4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java index 4c6ea09a8a..1efe58d463 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java index 8cce9108ad..47eced95ab 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index c1047ce458..7cd6fad6f1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a26c5ea09d..fd2c3d5c4a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 3a06d98b5d..88fa4b5795 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index f59aa7fadd..551f46aa22 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index b062a4283d..7f6860e8c4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 1e5341544d..70a7d54748 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 3245f35578..b04bf8fb76 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index b6eb974e29..b8346e8de6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 01302a1917..935f896f6d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 8d50790c9e..489c1771b1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 38804a180f..036fb93133 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index a8bc6b57f8..f95725fe06 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index 1068674bb9..0cd2f5e7e9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java index 44543bc588..0455a90f51 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index af17d18782..232b3acbf2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java index 5fc6d1cf3a..a49407401d 100644 --- a/jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 0bd8ae8d2f..2102d6331b 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java index ea1740f363..e62af16bc7 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java index b74bf9aa58..68e010900d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 513fc74182..33dc62d1b0 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index e0965d604d..26e09f7c46 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index e2cfddc4cb..e12602f48a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index dd2dfea9bc..d610bf1979 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java index d54de94966..20ec36f25f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassInferenceException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index ae130f2e5b..a32bbee7b9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 2c9ac5dd65..b87b00c116 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java index 07ffb2c994..d81abb1630 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 9a8c7d4f19..2ed1bbefae 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index b54252716f..08151544ce 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 72cb63849b..6a93e4e95c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 8d67146005..596e72519e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 4fd855b130..6d7c0cd35f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java index 777b0d10cc..03e83ffec5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of From f51e1e36d1ca77e975a1ea028adbb1f69ee15c10 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 4 Sep 2018 17:17:03 -0400 Subject: [PATCH 0182/2020] Adds JibEvent, JibEventType, and EventHandlers. (#921) --- .../cloud/tools/jib/event/EventHandlers.java | 101 ++++++++++++++++++ .../cloud/tools/jib/event/JibEvent.java | 20 ++++ .../cloud/tools/jib/event/JibEventType.java | 39 +++++++ .../tools/jib/event/EventHandlersTest.java | 91 ++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java new file mode 100644 index 0000000000..99e8e58e5c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -0,0 +1,101 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** Builds a set of event handlers to handle {@link JibEvent}s. */ +public class EventHandlers { + + /** Handles an emitted {@link JibEvent}. */ + public static class Handler { + + private final Class eventClass; + private final Consumer eventConsumer; + + private Handler(Class eventClass, Consumer eventConsumer) { + this.eventClass = eventClass; + this.eventConsumer = eventConsumer; + } + + /** + * Handles a {@link JibEvent}. + * + * @param jibEvent the event to handle + * @return {@code true} if this {@link Handler} handled the event; {@code false} if this {@link + * Handler} does not handle the event + */ + public boolean handle(JibEvent jibEvent) { + if (eventClass.isInstance(jibEvent)) { + eventConsumer.accept(eventClass.cast(jibEvent)); + return true; + } + return false; + } + } + + // Maps from JibEvent class to handlers for that event type. + private final Map, List>> handlers = + new HashMap<>(); + + /** + * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. + * The order in which handlers are added is the order in which they are called when the event is + * emitted. + * + *

Note: Implementations of {@code eventConsumer} must be thread-safe. + * + * @param eventType the event type that {@code eventConsumer} should handle + * @param eventConsumer the event handler + * @param the type of {@code eventClass} + * @return this + */ + public EventHandlers add( + JibEventType eventType, Consumer eventConsumer) { + Class eventClass = eventType.getEventClass(); + if (!handlers.containsKey(eventClass)) { + handlers.put(eventClass, new ArrayList<>()); + } + handlers.get(eventClass).add(new Handler<>(eventClass, eventConsumer)); + return this; + } + + /** + * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link + * #add(JibEventType, Consumer)} for more details. + * + * @param eventConsumer the event handler + * @return this + */ + public EventHandlers add(Consumer eventConsumer) { + return add(JibEventType.ALL, eventConsumer); + } + + /** + * Gets the handlers added. + * + * @return the map from {@link JibEvent} type to a list of {@link Handler}s + */ + public Map, List>> getHandlers() { + return Collections.unmodifiableMap(handlers); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java new file mode 100644 index 0000000000..412525890a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java @@ -0,0 +1,20 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +/** Type for events emitted by Jib Core. */ +public interface JibEvent {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java new file mode 100644 index 0000000000..7cc2134fd8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.common.annotations.VisibleForTesting; + +/** Holds references to all {@link JibEvent} types. */ +public class JibEventType { + + /** All event types. */ + public static final JibEventType ALL = new JibEventType<>(JibEvent.class); + + // TODO: Add entries for all JibEvent types. + + private final Class eventClass; + + @VisibleForTesting + JibEventType(Class eventClass) { + this.eventClass = eventClass; + } + + Class getEventClass() { + return eventClass; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java new file mode 100644 index 0000000000..726807ef63 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** Tests for {@link EventHandlers}. */ +public class EventHandlersTest { + + /** Test {@link JibEvent}. */ + private interface TestJibEvent1 extends JibEvent { + + String getPayload(); + } + + /** Test implementation of {@link JibEvent}. */ + private static class TestJibEvent2 implements JibEvent { + + @Nullable private String message; + + private void assertMessageCorrect(String name) { + Assert.assertEquals("Hello " + name, message); + } + + private void sayHello(String name) { + Assert.assertNull(message); + message = "Hello " + name; + } + } + + @Test + public void testAdd() { + int[] counter = new int[1]; + EventHandlers eventHandlers = + new EventHandlers() + .add( + new JibEventType<>(TestJibEvent1.class), + testJibEvent1 -> { + Assert.assertEquals("payload", testJibEvent1.getPayload()); + }) + .add( + new JibEventType<>(TestJibEvent2.class), + testJibEvent2 -> testJibEvent2.sayHello("Jib")) + .add(jibEvent -> counter[0]++); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(JibEvent.class)); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent1.class)); + Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent2.class)); + Assert.assertEquals(1, eventHandlers.getHandlers().get(JibEvent.class).size()); + Assert.assertEquals(1, eventHandlers.getHandlers().get(TestJibEvent1.class).size()); + Assert.assertEquals(1, eventHandlers.getHandlers().get(TestJibEvent2.class).size()); + + TestJibEvent1 mockTestJibEvent1 = Mockito.mock(TestJibEvent1.class); + Mockito.when(mockTestJibEvent1.getPayload()).thenReturn("payload"); + TestJibEvent2 testJibEvent2 = new TestJibEvent2(); + + // Checks that the handlers handled their respective event types. + Assert.assertTrue( + eventHandlers.getHandlers().get(JibEvent.class).get(0).handle(mockTestJibEvent1)); + Assert.assertTrue(eventHandlers.getHandlers().get(JibEvent.class).get(0).handle(testJibEvent2)); + Assert.assertTrue( + eventHandlers.getHandlers().get(TestJibEvent1.class).get(0).handle(mockTestJibEvent1)); + Assert.assertTrue( + eventHandlers.getHandlers().get(TestJibEvent2.class).get(0).handle(testJibEvent2)); + Assert.assertFalse( + eventHandlers.getHandlers().get(TestJibEvent1.class).get(0).handle(testJibEvent2)); + Assert.assertFalse( + eventHandlers.getHandlers().get(TestJibEvent2.class).get(0).handle(mockTestJibEvent1)); + + Assert.assertEquals(2, counter[0]); + Mockito.verify(mockTestJibEvent1).getPayload(); + Mockito.verifyNoMoreInteractions(mockTestJibEvent1); + testJibEvent2.assertMessageCorrect("Jib"); + } +} From 499c3d6192b5039a677bb4ef2b4b057c30c5eb09 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 5 Sep 2018 10:47:59 -0400 Subject: [PATCH 0183/2020] Add plugin configuration for environment variables (#930) * Add plugin configuration for environment variables * Add skip to changelog --- .../builder/BuildStepsIntegrationTest.java | 63 +++++++++---------- .../frontend/JavaDockerContextGenerator.java | 62 ++++++++++++++---- .../JavaDockerContextGeneratorTest.java | 2 + jib-core/src/test/resources/sampleDockerfile | 6 +- jib-gradle-plugin/CHANGELOG.md | 2 + .../jib/gradle/JibPluginIntegrationTest.java | 4 +- .../projects/simple/complex-build.gradle | 1 + .../src/main/java/com/test/HelloWorld.java | 7 +++ .../tools/jib/gradle/ContainerParameters.java | 11 ++++ .../cloud/tools/jib/gradle/JibExtension.java | 6 ++ .../gradle/PluginConfigurationProcessor.java | 1 + .../tools/jib/gradle/JibExtensionTest.java | 6 ++ jib-maven-plugin/CHANGELOG.md | 3 + .../jib/maven/JibPluginConfiguration.java | 6 +- .../maven/BuildImageMojoIntegrationTest.java | 4 +- .../resources/projects/simple/pom-complex.xml | 4 ++ .../src/main/java/com/test/HelloWorld.java | 7 +++ 17 files changed, 141 insertions(+), 54 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index e19795cc0c..bf284da32f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -64,6 +64,34 @@ private static ImmutableList getResourceFilesList(String resourcePath) } } + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerContainerConfig = new Command("docker", "inspect", imageReference).run(); + Assert.assertThat( + dockerContainerConfig, + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/tcp\": {},\n" + + " \"2001/tcp\": {},\n" + + " \"2002/tcp\": {},\n" + + " \"3000/udp\": {}")); + Assert.assertThat( + dockerContainerConfig, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); + String dockerConfigEnv = + new Command("docker", "inspect", "-f", "{{.Config.Env}}", imageReference).run(); + Assert.assertThat( + dockerConfigEnv, CoreMatchers.containsString("env1=envvalue1 env2=envvalue2")); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); + Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); + } + private static final TestJibLogger logger = new TestJibLogger(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -104,18 +132,7 @@ public void testSteps_forBuildToDockerRegistry() String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); - Assert.assertThat( - new Command("docker", "inspect", imageReference).run(), - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/tcp\": {},\n" - + " \"2001/tcp\": {},\n" - + " \"2002/tcp\": {},\n" - + " \"3000/udp\": {}")); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); - Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); + assertDockerInspect(imageReference); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } @@ -152,26 +169,7 @@ public void testSteps_forBuildToDockerDaemon() new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) .run(); - String dockerContainerConfig = new Command("docker", "inspect", imageReference).run(); - Assert.assertThat( - dockerContainerConfig, - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/tcp\": {},\n" - + " \"2001/tcp\": {},\n" - + " \"2002/tcp\": {},\n" - + " \"3000/udp\": {}")); - Assert.assertThat( - dockerContainerConfig, - CoreMatchers.containsString( - " \"Labels\": {\n" - + " \"key1\": \"value1\",\n" - + " \"key2\": \"value2\"\n" - + " }")); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); - Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); + assertDockerInspect(imageReference); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } @@ -213,6 +211,7 @@ private BuildConfiguration getBuildConfiguration( JavaEntrypointConstructor.makeDefaultEntrypoint( Collections.emptyList(), "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) + .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) .setExposedPorts( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index e4d593e0f0..d7b30e6b1c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.StringJoiner; import javax.annotation.Nullable; /** @@ -58,6 +59,8 @@ public class JavaDockerContextGenerator { private static final String CLASSES_LAYER_DIRECTORY = "classes"; private static final String EXTRA_FILES_LAYER_DIRECTORY = "root"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + /** Represents a Dockerfile {@code COPY} directive. */ private static class CopyDirective { @@ -98,11 +101,39 @@ private static void addIfNotEmpty( layerEntry.getSourceFiles(), directoryInContext, layerEntry.getExtractionPath())); } + /** + * Converts a map to a corresponding dockerfile string in the form of: + * + *

{@code
+   * command key1="value1" \
+   *     key2="value2" \
+   *     ...
+   * }
+ * + * @param map the map to convert + * @param command the dockerfile command to prefix the map values with + * @return the new dockerfile command as a string + * @throws JsonProcessingException if getting the json string of a map value fails + */ + private static String mapToDockerfileString(Map map, String command) + throws JsonProcessingException { + if (map.isEmpty()) { + return ""; + } + + StringJoiner joiner = new StringJoiner(" \\\n ", "\n" + command + " ", ""); + for (Entry entry : map.entrySet()) { + joiner.add(entry.getKey() + "=" + objectMapper.writeValueAsString(entry.getValue())); + } + return joiner.toString(); + } + private final ImmutableList copyDirectives; @Nullable private String baseImage; private List entrypoint = Collections.emptyList(); private List javaArguments = Collections.emptyList(); + private Map environment = Collections.emptyMap(); private List exposedPorts = Collections.emptyList(); private Map labels = Collections.emptyMap(); @@ -170,6 +201,17 @@ public JavaDockerContextGenerator setJavaArguments(List javaArguments) { return this; } + /** + * Sets the environment variables + * + * @param environment map from the environment variable name to value + * @return this + */ + public JavaDockerContextGenerator setEnvironment(Map environment) { + this.environment = environment; + return this; + } + /** * Sets the exposed ports. * @@ -240,9 +282,12 @@ public void generate(Path targetDirectory) throws IOException { * * EXPOSE [port] * [More EXPOSE instructions, if necessary] + * ENV [key1]="[value1]" \ + * [key2]="[value2]" \ + * [...] * LABEL [key1]="[value1]" \ - * [key2]="[value2]" \ - * [...] + * [key2]="[value2]" \ + * [...] * ENTRYPOINT java [jvm flags] -cp [classpaths] [main class] * CMD [main class args] * } @@ -251,7 +296,6 @@ public void generate(Path targetDirectory) throws IOException { */ @VisibleForTesting String makeDockerfile() throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); StringBuilder dockerfile = new StringBuilder(); dockerfile.append("FROM ").append(Preconditions.checkNotNull(baseImage)).append("\n"); for (CopyDirective copyDirective : copyDirectives) { @@ -267,16 +311,8 @@ String makeDockerfile() throws JsonProcessingException { dockerfile.append("\nEXPOSE ").append(port); } - boolean firstLabel = true; - for (Entry label : labels.entrySet()) { - dockerfile - .append(firstLabel ? "\nLABEL " : " \\\n ") - .append(label.getKey()) - .append("=") - .append(objectMapper.writeValueAsString(label.getValue())); - firstLabel = false; - } - + dockerfile.append(mapToDockerfileString(environment, "ENV")); + dockerfile.append(mapToDockerfileString(labels, "LABEL")); dockerfile .append("\nENTRYPOINT ") .append(objectMapper.writeValueAsString(entrypoint)) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 6a3f1401ae..8fee632d99 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -125,6 +125,7 @@ public void testMakeDockerfile() throws IOException { List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); String expectedMainClass = "SomeMainClass"; List expectedJavaArguments = Arrays.asList("arg1", "arg2"); + Map expectedEnv = ImmutableMap.of("key1", "value1", "key2", "value2"); List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); Map expectedLabels = ImmutableMap.of( @@ -155,6 +156,7 @@ public void testMakeDockerfile() throws IOException { JavaEntrypointConstructor.makeDefaultEntrypoint( expectedJvmFlags, expectedMainClass)) .setJavaArguments(expectedJavaArguments) + .setEnvironment(expectedEnv) .setExposedPorts(exposedPorts) .setLabels(expectedLabels) .makeDockerfile(); diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 48af0b1114..8d9c114471 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -8,8 +8,10 @@ COPY root / EXPOSE 1000/tcp EXPOSE 2000-2010/udp +ENV key1="value1" \ + key2="value2" LABEL key1="value" \ - key2="value with\\backslashes\"and\\\\\"\"quotes\"\\" \ - key3="value3" + key2="value with\\backslashes\"and\\\\\"\"quotes\"\\" \ + key3="value3" ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources/:/app/classes/:/app/libs/*","SomeMainClass"] CMD ["arg1","arg2"] diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 2d86a39241..f9b6329fb7 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `container.environment` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 9a3ff565da..7209a6baca 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -238,7 +238,7 @@ public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -248,7 +248,7 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index 9976101f72..988b50d5e9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -34,6 +34,7 @@ jib { args = ['An argument.'] mainClass = 'com.test.HelloWorld' jvmFlags = ['-Xms512m', '-Xdebug'] + environment = [env1:'envvalue1', env2:'envvalue2'] ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 2102d6331b..733648707f 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -50,5 +50,12 @@ public static void main(String[] args) throws IOException, URISyntaxException { for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { System.out.println(jvmFlag); } + + if (System.getenv("env1") != null) { + System.out.println(System.getenv("env1")); + } + if (System.getenv("env2") != null) { + System.out.println(System.getenv("env2")); + } } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index b67eff1a1a..cab2623272 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -34,6 +34,7 @@ public class ContainerParameters { private boolean useCurrentTimestamp = false; private List jvmFlags = Collections.emptyList(); + private Map environment = Collections.emptyMap(); private List entrypoint = Collections.emptyList(); @Nullable private String mainClass; private List args = Collections.emptyList(); @@ -71,6 +72,16 @@ public void setJvmFlags(List jvmFlags) { this.jvmFlags = jvmFlags; } + @Input + @Optional + public Map getEnvironment() { + return environment; + } + + public void setEnvironment(Map environment) { + this.environment = environment; + } + @Input @Nullable @Optional diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 00c57cb419..2a2cd969af 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -229,6 +229,12 @@ List getJvmFlags() { return container.getJvmFlags(); } + @Internal + @Optional + Map getEnvironment() { + return container.getEnvironment(); + } + // TODO: Make @Internal (deprecated) @Input @Nullable diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index a9b15776cc..a80e17bc5e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -97,6 +97,7 @@ static PluginConfigurationProcessor processCommonConfiguration( ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() .setEntrypoint(entrypoint) + .setEnvironment(jibExtension.getEnvironment()) .setProgramArguments(jibExtension.getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) .setLabels(jibExtension.getLabels()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index c175fe71cc..67221e9605 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -95,14 +95,17 @@ public void testTo() { @Test public void testContainer() { Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getJvmFlags()); + Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getEnvironment()); Assert.assertNull(testJibExtension.getContainer().getMainClass()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getArgs()); Assert.assertEquals(V22ManifestTemplate.class, testJibExtension.getContainer().getFormat()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); + Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); testJibExtension.container( container -> { container.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); + container.setEnvironment(ImmutableMap.of("var1", "value1", "var2", "value2")); container.setEntrypoint(Arrays.asList("foo", "bar", "baz")); container.setMainClass("mainClass"); container.setArgs(Arrays.asList("arg1", "arg2", "arg3")); @@ -114,6 +117,9 @@ public void testContainer() { Arrays.asList("foo", "bar", "baz"), testJibExtension.getContainer().getEntrypoint()); Assert.assertEquals( Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getContainer().getJvmFlags()); + Assert.assertEquals( + ImmutableMap.of("var1", "value1", "var2", "value2"), + testJibExtension.getContainer().getEnvironment()); Assert.assertEquals("mainClass", testJibExtension.getContainer().getMainClass()); Assert.assertEquals( Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getContainer().getArgs()); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 1d3fe59b1e..b421d61a25 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. ### Added +- `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property) ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) +- `` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 27abed44e4..19e0b37181 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -122,6 +122,8 @@ public static class ContainerParameters { @Parameter private List jvmFlags = Collections.emptyList(); + @Parameter private Map environment = Collections.emptyMap(); + @Nullable @Parameter private String mainClass; @Parameter private List args = Collections.emptyList(); @@ -152,8 +154,6 @@ public static class ContainerParameters { @Deprecated @Parameter private List jvmFlags = Collections.emptyList(); - @Nullable @Parameter private Map environment; - @Deprecated @Nullable @Parameter private String mainClass; @Deprecated @Parameter private List args = Collections.emptyList(); @@ -268,7 +268,7 @@ List getJvmFlags() { @Nullable Map getEnvironment() { - return environment; + return container.environment; } @Nullable diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 70a7d54748..f2b5931522 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -261,7 +261,7 @@ public void testExecute_complex() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); } @@ -270,7 +270,7 @@ public void testExecute_complex_sameFromAndToRegistry() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\n", + "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); } diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index dc98ada0ba..02ebc126ce 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -62,6 +62,10 @@ -Xms512m -Xdebug + + envvalue1 + envvalue2 + 1000/tcp 2000-2003/udp diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 2102d6331b..733648707f 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -50,5 +50,12 @@ public static void main(String[] args) throws IOException, URISyntaxException { for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { System.out.println(jvmFlag); } + + if (System.getenv("env1") != null) { + System.out.println(System.getenv("env1")); + } + if (System.getenv("env2") != null) { + System.out.println(System.getenv("env2")); + } } } From 90d9c10f56b1e232fe3f594c5dfa173013920923 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 5 Sep 2018 15:48:29 -0400 Subject: [PATCH 0184/2020] Improve the error message when pushing to a registry that does not support v2-2 (#932) --- .../tools/jib/registry/ManifestPusher.java | 61 ++++++++++++ .../jib/registry/ManifestPusherTest.java | 96 +++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 47aab078f5..a92d413805 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -17,14 +17,19 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; +import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.List; +import org.apache.http.HttpStatus; /** Pushes an image's manifest. */ class ManifestPusher implements RegistryEndpointProvider { @@ -53,6 +58,62 @@ public List getAccept() { return Collections.emptyList(); } + @Override + public Void handleHttpResponseException(HttpResponseException httpResponseException) + throws HttpResponseException, RegistryErrorException { + // docker registry 2.0 and 2.1 returns: + // 400 Bad Request + // {"errors":[{"code":"TAG_INVALID","message":"manifest tag did not match URI"}]} + // docker registry:2.2 returns: + // 400 Bad Request + // {"errors":[{"code":"MANIFEST_INVALID","message":"manifest invalid","detail":{}}]} + // quay.io returns: + // 415 UNSUPPORTED MEDIA TYPE + // {"errors":[{"code":"MANIFEST_INVALID","detail": + // {"message":"manifest schema version not supported"},"message":"manifest invalid"}]} + + if (httpResponseException.getStatusCode() != HttpStatus.SC_BAD_REQUEST + && httpResponseException.getStatusCode() != HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE) { + throw httpResponseException; + } + + // TODO turn deserializing the error-code into a library method + + // Obtain the error response code. + String errorContent = httpResponseException.getContent(); + if (errorContent == null) { + throw httpResponseException; + } + + try { + ErrorResponseTemplate errorResponse = + JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); + List errors = errorResponse.getErrors(); + if (errors.size() == 1) { + String errorCodeString = errors.get(0).getCode(); + if (errorCodeString == null) { + // Did not get an error code back. + throw httpResponseException; + } + ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); + if (errorCode.equals(ErrorCodes.MANIFEST_INVALID) + || errorCode.equals(ErrorCodes.TAG_INVALID)) { + throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) + .addReason("Registry may not support Image Manifest Version 2, Schema 2") + .build(); + } + } + + } catch (IOException ex) { + throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) + .addReason("Failed to parse registry error response body") + .build(); + } + + // unhandled error response code. + throw httpResponseException; + } + @Override public Void handleResponse(Response response) { return null; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 29a83a1e78..b65e06f8d5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -30,6 +32,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import org.apache.http.HttpStatus; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -101,4 +105,96 @@ public void testGetActionDescription() { public void testGetAccept() { Assert.assertEquals(0, testManifestPusher.getAccept().size()); } + + /** Docker Registry 2.0 and 2.1 return 400 / TAG_INVALID. */ + @Test + public void testHandleHttpResponseException_dockerRegistry_tagInvalid() { + HttpResponseException exception = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent( + "{\"errors\":[{\"code\":\"TAG_INVALID\",\"message\":\"manifest tag did not match URI\"}]}") + .build(); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support Image Manifest Version 2, Schema 2")); + + } catch (HttpResponseException ex) { + Assert.fail("should have been a RegistryErrorException"); + } + } + + /** Docker Registry 2.2 returns a 400 / MANIFEST_INVALID. */ + @Test + public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() { + HttpResponseException exception = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}") + .build(); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support Image Manifest Version 2, Schema 2")); + + } catch (HttpResponseException ex) { + Assert.fail("should have been a RegistryErrorException"); + } + } + + /** Quay.io returns an undocumented 415 / MANIFEST_INVALID. */ + @Test + public void testHandleHttpResponseException_quayIo() { + HttpResponseException exception = + new HttpResponseException.Builder( + HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED MEDIA TYPE", new HttpHeaders()) + .setContent( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"detail\":" + + "{\"message\":\"manifest schema version not supported\"},\"message\":\"manifest invalid\"}]}") + .build(); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Registry may not support Image Manifest Version 2, Schema 2")); + + } catch (HttpResponseException ex) { + Assert.fail("should have been a RegistryErrorException"); + } + } + + @Test + public void testHandleHttpResponseException_otherError() { + HttpResponseException exception = + new HttpResponseException.Builder( + HttpStatus.SC_UNAUTHORIZED, "Unauthorized", new HttpHeaders()) + .setContent("{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"Unauthorized\"]}}") + .build(); + try { + testManifestPusher.handleHttpResponseException(exception); + Assert.fail(); + + } catch (RegistryErrorException ex) { + Assert.fail("should have been a HttpResponseException"); + + } catch (HttpResponseException ex) { + Assert.assertSame(exception, ex); + } + } } From 57822ee6ca85e26ffe6305e7eaf31f5dc03e8e89 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 5 Sep 2018 16:25:24 -0400 Subject: [PATCH 0185/2020] Make JibExtension setters public (#939) --- .../tools/jib/gradle/BuildDockerTask.java | 5 +-- .../tools/jib/gradle/BuildImageTask.java | 5 +-- .../cloud/tools/jib/gradle/BuildTarTask.java | 5 +-- .../tools/jib/gradle/DockerContextTask.java | 5 +-- .../cloud/tools/jib/gradle/JibTask.java | 31 +++++++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index c9e1599cc0..fa85fd966a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -35,7 +35,7 @@ import org.gradle.api.tasks.options.Option; /** Builds a container image and exports to the default Docker daemon. */ -public class BuildDockerTask extends DefaultTask { +public class BuildDockerTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; @@ -119,7 +119,8 @@ public void buildDocker() throws InvalidImageReferenceException { } } - BuildDockerTask setJibExtension(JibExtension jibExtension) { + @Override + public BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 343ba8e262..662cc51dbc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -38,7 +38,7 @@ import org.gradle.api.tasks.options.Option; /** Builds a container image. */ -public class BuildImageTask extends DefaultTask { +public class BuildImageTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build image failed"; @@ -137,7 +137,8 @@ public void buildImage() throws InvalidImageReferenceException { } } - BuildImageTask setJibExtension(JibExtension jibExtension) { + @Override + public BuildImageTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index ecf3fb1a84..5f8fe78f93 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -38,7 +38,7 @@ import org.gradle.api.tasks.options.Option; /** Builds a container image to a tarball. */ -public class BuildTarTask extends DefaultTask { +public class BuildTarTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Building image tarball failed"; @@ -146,7 +146,8 @@ public void buildTar() throws InvalidImageReferenceException { } } - BuildTarTask setJibExtension(JibExtension jibExtension) { + @Override + public BuildTarTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 871463a28c..b1aaae5c83 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -37,7 +37,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; -public class DockerContextTask extends DefaultTask { +public class DockerContextTask extends DefaultTask implements JibTask { @Nullable private String targetDir; @Nullable private JibExtension jibExtension; @@ -153,7 +153,8 @@ public void generateDockerContext() { } } - DockerContextTask setJibExtension(JibExtension jibExtension) { + @Override + public DockerContextTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java new file mode 100644 index 0000000000..bb398da9d6 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import org.gradle.api.Task; + +/** A task with a {@link JibExtension}. */ +public interface JibTask extends Task { + + /** + * Sets the task's {@link JibExtension}. + * + * @param jibExtension the {@link JibExtension} + * @return this + */ + Task setJibExtension(JibExtension jibExtension); +} From f68833ebf3732a680cb2ae966979f8d0c46d7cdd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 5 Sep 2018 17:01:35 -0400 Subject: [PATCH 0186/2020] Create helper for parsing an error code from error response (#938) --- .../cloud/tools/jib/registry/BlobChecker.java | 33 +------ .../tools/jib/registry/ErrorResponseUtil.java | 70 ++++++++++++++ .../tools/jib/registry/ManifestPusher.java | 38 +------- .../jib/registry/ErrorResponseUtilTest.java | 93 +++++++++++++++++++ 4 files changed, 172 insertions(+), 62 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index e439a97c61..427f47dfff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -23,10 +23,6 @@ import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; -import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -71,34 +67,15 @@ public BlobDescriptor handleHttpResponseException(HttpResponseException httpResp } // Finds a BLOB_UNKNOWN error response code. - String errorContent = httpResponseException.getContent(); - if (errorContent == null) { + if (httpResponseException.getContent() == null) { // TODO: The Google HTTP client gives null content for HEAD requests. Make the content never // be null, even for HEAD requests. return null; + } - } else { - try { - ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); - List errors = errorResponse.getErrors(); - if (errors.size() == 1) { - String errorCodeString = errors.get(0).getCode(); - if (errorCodeString == null) { - // Did not get an error code back. - throw httpResponseException; - } - ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); - if (errorCode.equals(ErrorCodes.BLOB_UNKNOWN)) { - return null; - } - } - - } catch (IOException ex) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) - .addReason("Failed to parse registry error response body") - .build(); - } + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); + if (errorCode == ErrorCodes.BLOB_UNKNOWN) { + return null; } // BLOB_UNKNOWN was not found as a error response code. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java new file mode 100644 index 0000000000..f6407235d2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; +import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; +import java.io.IOException; +import java.util.List; + +/** Utility methods for parsing {@link ErrorResponseTemplate JSON-encoded error responses}. */ +public class ErrorResponseUtil { + + /** + * Extract an {@link ErrorCodes} response from the error object encoded in an {@link + * HttpResponseException}. + * + * @param httpResponseException the response exception + * @return the parsed {@link ErrorCodes} if found + * @throws HttpResponseException rethrows the original exception if an error object could not be + * parsed, if there were multiple error objects, or if the error code is unknown. + */ + public static ErrorCodes getErrorCode(HttpResponseException httpResponseException) + throws HttpResponseException { + // Obtain the error response code. + String errorContent = httpResponseException.getContent(); + if (errorContent == null) { + throw httpResponseException; + } + + try { + ErrorResponseTemplate errorResponse = + JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); + List errors = errorResponse.getErrors(); + // There may be multiple error objects + if (errors.size() == 1) { + String errorCodeString = errors.get(0).getCode(); + // May not get an error code back. + if (errorCodeString != null) { + // throws IllegalArgumentException if unknown error code + return ErrorCodes.valueOf(errorCodeString); + } + } + + } catch (IOException | IllegalArgumentException ex) { + // Parse exception: either isn't an error object or unknown error code + } + + // rethrow the original exception + throw httpResponseException; + } + + // not intended to be instantiated + private ErrorResponseUtil() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index a92d413805..7dff52623d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -22,9 +22,6 @@ import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; -import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -77,40 +74,13 @@ public Void handleHttpResponseException(HttpResponseException httpResponseExcept throw httpResponseException; } - // TODO turn deserializing the error-code into a library method - - // Obtain the error response code. - String errorContent = httpResponseException.getContent(); - if (errorContent == null) { - throw httpResponseException; - } - - try { - ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson(errorContent, ErrorResponseTemplate.class); - List errors = errorResponse.getErrors(); - if (errors.size() == 1) { - String errorCodeString = errors.get(0).getCode(); - if (errorCodeString == null) { - // Did not get an error code back. - throw httpResponseException; - } - ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); - if (errorCode.equals(ErrorCodes.MANIFEST_INVALID) - || errorCode.equals(ErrorCodes.TAG_INVALID)) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) - .addReason("Registry may not support Image Manifest Version 2, Schema 2") - .build(); - } - } - - } catch (IOException ex) { + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); + if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.TAG_INVALID) { throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) - .addReason("Failed to parse registry error response body") + .addReason("Registry may not support Image Manifest Version 2, Schema 2") .build(); } - - // unhandled error response code. + // rethrow: unhandled error response code. throw httpResponseException; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java new file mode 100644 index 0000000000..fe15dfdd3a --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +/** Test for {@link ErrorReponseUtil}. */ +public class ErrorResponseUtilTest { + + @Test + public void testGetErrorCode_knownErrorCode() throws HttpResponseException { + HttpResponseException httpResponseException = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}") + .build(); + + Assert.assertSame( + ErrorCodes.MANIFEST_INVALID, ErrorResponseUtil.getErrorCode(httpResponseException)); + } + + /** An unknown {@link ErrorCodes} should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_unknownErrorCode() { + HttpResponseException httpResponseException = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent( + "{\"errors\":[{\"code\":\"INVALID_ERROR_CODE\",\"message\":\"invalid code\",\"detail\":{}}]}") + .build(); + try { + ErrorResponseUtil.getErrorCode(httpResponseException); + Assert.fail(); + } catch (HttpResponseException ex) { + Assert.assertSame(httpResponseException, ex); + } + } + + /** Multiple error objects should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_multipleErrors() { + HttpResponseException httpResponseException = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent( + "{\"errors\":[" + + "{\"code\":\"MANIFEST_INVALID\",\"message\":\"message 1\",\"detail\":{}}," + + "{\"code\":\"TAG_INVALID\",\"message\":\"message 2\",\"detail\":{}}" + + "]}") + .build(); + try { + ErrorResponseUtil.getErrorCode(httpResponseException); + Assert.fail(); + } catch (HttpResponseException ex) { + Assert.assertSame(httpResponseException, ex); + } + } + + /** An non-error object should cause original exception to be rethrown. */ + @Test + public void testGetErrorCode_invalidErrorObject() { + HttpResponseException httpResponseException = + new HttpResponseException.Builder( + HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) + .setContent("{\"type\":\"other\",\"message\":\"some other object\"}") + .build(); + try { + ErrorResponseUtil.getErrorCode(httpResponseException); + Assert.fail(); + } catch (HttpResponseException ex) { + Assert.assertSame(httpResponseException, ex); + } + } +} From 29f4ad8d1b80fc718a674598e4231a6951ede9e1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 5 Sep 2018 17:30:03 -0400 Subject: [PATCH 0187/2020] Fix ManifestPusherTest warnings (#941) --- .../jib/registry/ManifestPusherTest.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index b65e06f8d5..2f9f971325 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -108,7 +108,8 @@ public void testGetAccept() { /** Docker Registry 2.0 and 2.1 return 400 / TAG_INVALID. */ @Test - public void testHandleHttpResponseException_dockerRegistry_tagInvalid() { + public void testHandleHttpResponseException_dockerRegistry_tagInvalid() + throws HttpResponseException { HttpResponseException exception = new HttpResponseException.Builder( HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) @@ -124,15 +125,13 @@ public void testHandleHttpResponseException_dockerRegistry_tagInvalid() { ex.getMessage(), CoreMatchers.containsString( "Registry may not support Image Manifest Version 2, Schema 2")); - - } catch (HttpResponseException ex) { - Assert.fail("should have been a RegistryErrorException"); } } /** Docker Registry 2.2 returns a 400 / MANIFEST_INVALID. */ @Test - public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() { + public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() + throws HttpResponseException { HttpResponseException exception = new HttpResponseException.Builder( HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) @@ -148,15 +147,12 @@ public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() { ex.getMessage(), CoreMatchers.containsString( "Registry may not support Image Manifest Version 2, Schema 2")); - - } catch (HttpResponseException ex) { - Assert.fail("should have been a RegistryErrorException"); } } /** Quay.io returns an undocumented 415 / MANIFEST_INVALID. */ @Test - public void testHandleHttpResponseException_quayIo() { + public void testHandleHttpResponseException_quayIo() throws HttpResponseException { HttpResponseException exception = new HttpResponseException.Builder( HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED MEDIA TYPE", new HttpHeaders()) @@ -173,14 +169,11 @@ public void testHandleHttpResponseException_quayIo() { ex.getMessage(), CoreMatchers.containsString( "Registry may not support Image Manifest Version 2, Schema 2")); - - } catch (HttpResponseException ex) { - Assert.fail("should have been a RegistryErrorException"); } } @Test - public void testHandleHttpResponseException_otherError() { + public void testHandleHttpResponseException_otherError() throws RegistryErrorException { HttpResponseException exception = new HttpResponseException.Builder( HttpStatus.SC_UNAUTHORIZED, "Unauthorized", new HttpHeaders()) @@ -190,9 +183,6 @@ public void testHandleHttpResponseException_otherError() { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); - } catch (RegistryErrorException ex) { - Assert.fail("should have been a HttpResponseException"); - } catch (HttpResponseException ex) { Assert.assertSame(exception, ex); } From 0b70ba2028499c9eeec871c7c9501ca58756b302 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 6 Sep 2018 14:38:21 -0400 Subject: [PATCH 0188/2020] Consolidate tool name usage and refactor User Agent injection (#935) --- .../builder/BuildStepsIntegrationTest.java | 2 +- .../jib/builder/steps/BuildImageStep.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 7 +- .../jib/builder/steps/PullBaseImageStep.java | 7 +- .../tools/jib/builder/steps/PushBlobStep.java | 7 +- .../jib/builder/steps/PushImageStep.java | 7 +- .../jib/configuration/BuildConfiguration.java | 53 ++++++++++--- .../tools/jib/registry/RegistryClient.java | 75 +++++++++---------- .../jib/builder/steps/BuildImageStepTest.java | 2 +- .../configuration/BuildConfigurationTest.java | 6 +- .../jib/registry/RegistryClientTest.java | 20 ++--- .../jib/gradle/GradleProjectProperties.java | 6 ++ .../gradle/PluginConfigurationProcessor.java | 7 +- .../jib/maven/MavenProjectProperties.java | 6 ++ .../maven/PluginConfigurationProcessor.java | 7 +- 15 files changed, 113 insertions(+), 101 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index bf284da32f..838213fc22 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -222,7 +222,7 @@ private BuildConfiguration getBuildConfiguration( .setContainerConfiguration(containerConfiguration) .setAllowInsecureRegistries(true) .setLayerConfigurations(fakeLayerConfigurations) - .setCreatedBy("jib-integration-test") + .setToolName("jib-integration-test") .build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 07ee21fd2c..63e0e9d928 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -138,7 +138,7 @@ private Image afterCachedLayersSteps() HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") - .setCreatedBy(buildConfiguration.getCreatedBy()) + .setCreatedBy(buildConfiguration.getToolName()) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index f5425ba5c6..6033bd10f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -70,11 +70,8 @@ public CachedLayer call() throws IOException, RegistryException { try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), String.format(DESCRIPTION, layerDigest))) { RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBuildLogger(), - buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + buildConfiguration + .newBaseImageRegistryClientFactory() .setAuthorization(pullAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 7833b34630..8c93660be1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -185,11 +185,8 @@ private Image pullBaseImage(@Nullable Authorization registryAuthorization throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBuildLogger(), - buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + buildConfiguration + .newBaseImageRegistryClientFactory() .setAuthorization(registryAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 61a15431a8..49852aec90 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -69,11 +69,8 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBuildLogger(), - buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + buildConfiguration + .newTargetImageRegistryClientFactory() .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); registryClient.setTimer(timer); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 93758461e3..262f2807e6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -117,11 +117,8 @@ private ListenableFuture afterPushSteps() throws ExecutionException { private Void afterAllPushed() throws IOException, RegistryException, ExecutionException { try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { RegistryClient registryClient = - RegistryClient.factory( - buildConfiguration.getBuildLogger(), - buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + buildConfiguration + .newTargetImageRegistryClientFactory() .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 24cc8dfd24..63036fb988 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; @@ -38,7 +39,7 @@ public static class Builder { private boolean allowInsecureRegistries = false; private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = V22ManifestTemplate.class; - private String createdBy = "jib"; + private String toolName = "jib"; private JibLogger buildLogger; @@ -137,14 +138,13 @@ public Builder setLayerConfigurations(List layerConfiguratio } /** - * Sets the command that created the image layers (for the "Created By" field in the container's - * history). + * Sets the name of the tool that is executing the build. * - * @param createdBy the field value + * @param toolName the tool name * @return this */ - public Builder setCreatedBy(String createdBy) { - this.createdBy = createdBy; + public Builder setToolName(String toolName) { + this.toolName = toolName; return this; } @@ -185,7 +185,7 @@ public BuildConfiguration build() { targetFormat, allowInsecureRegistries, layerConfigurations, - createdBy); + toolName); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -213,7 +213,7 @@ public static Builder builder(JibLogger buildLogger) { private Class targetFormat; private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; - private final String createdBy; + private final String toolName; /** Instantiate with {@link Builder#build}. */ private BuildConfiguration( @@ -226,7 +226,7 @@ private BuildConfiguration( Class targetFormat, boolean allowInsecureRegistries, ImmutableList layerConfigurations, - String createdBy) { + String toolName) { this.buildLogger = buildLogger; this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; @@ -236,7 +236,7 @@ private BuildConfiguration( this.targetFormat = targetFormat; this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; - this.createdBy = createdBy; + this.toolName = toolName; } public JibLogger getBuildLogger() { @@ -260,8 +260,8 @@ public Class getTargetFormat() { return targetFormat; } - public String getCreatedBy() { - return createdBy; + public String getToolName() { + return toolName; } /** @@ -302,4 +302,33 @@ public boolean getAllowInsecureRegistries() { public ImmutableList getLayerConfigurations() { return layerConfigurations; } + + /** + * Creates a new {@link RegistryClient.Factory} for the base image with fields from the build + * configuration. + * + * @return a new {@link RegistryClient.Factory} + */ + public RegistryClient.Factory newBaseImageRegistryClientFactory() { + return newRegistryClientFactory(baseImageConfiguration); + } + + /** + * Creates a new {@link RegistryClient.Factory} for the target image with fields from the build + * configuration. + * + * @return a new {@link RegistryClient.Factory} + */ + public RegistryClient.Factory newTargetImageRegistryClientFactory() { + return newRegistryClientFactory(targetImageConfiguration); + } + + private RegistryClient.Factory newRegistryClientFactory(ImageConfiguration imageConfiguration) { + return RegistryClient.factory( + getBuildLogger(), + imageConfiguration.getImageRegistry(), + imageConfiguration.getImageRepository()) + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setUserAgentSuffix(getToolName()); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 568fa6200d..60ee097e49 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -70,6 +70,7 @@ public static class Factory { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; + @Nullable private String userAgentSuffix; @Nullable private Authorization authorization; private Factory( @@ -102,6 +103,17 @@ public Factory setAuthorization(@Nullable Authorization authorization) { return this; } + /** + * Sets a suffix to append to {@code User-Agent} headers. + * + * @param userAgentSuffix the suffix to append + * @return this + */ + public Factory setUserAgentSuffix(@Nullable String userAgentSuffix) { + this.userAgentSuffix = userAgentSuffix; + return this; + } + /** * Creates a new {@link RegistryClient}. * @@ -115,9 +127,32 @@ public RegistryClient newRegistryClient() { allowInsecureRegistries, makeUserAgent()); } - } - @Nullable private static String userAgentSuffix; + /** + * The {@code User-Agent} is in the form of {@code jib }. For example: {@code + * jib 0.9.0 jib-maven-plugin}. + * + * @return the {@code User-Agent} header to send. The {@code User-Agent} can be disabled by + * setting the system property variable {@code _JIB_DISABLE_USER_AGENT} to any non-empty + * string. + */ + private String makeUserAgent() { + if (!Strings.isNullOrEmpty(System.getProperty("_JIB_DISABLE_USER_AGENT"))) { + return ""; + } + + String version = RegistryClient.class.getPackage().getImplementationVersion(); + StringBuilder userAgentBuilder = new StringBuilder(); + userAgentBuilder.append("jib"); + if (version != null) { + userAgentBuilder.append(" ").append(version); + } + if (userAgentSuffix != null) { + userAgentBuilder.append(" ").append(userAgentSuffix); + } + return userAgentBuilder.toString(); + } + } /** * Creates a new {@link Factory} for building a {@link RegistryClient}. @@ -131,42 +166,6 @@ public static Factory factory(JibLogger buildLogger, String serverUrl, String im return new Factory(buildLogger, new RegistryEndpointRequestProperties(serverUrl, imageName)); } - // TODO: Inject via a RegistryClient.Factory. - /** - * Sets a suffix to append to {@code User-Agent} headers. - * - * @param userAgentSuffix the suffix to append - */ - public static void setUserAgentSuffix(@Nullable String userAgentSuffix) { - RegistryClient.userAgentSuffix = userAgentSuffix; - } - - /** - * The {@code User-Agent} is in the form of {@code jib }. For example: {@code jib - * 0.9.0 jib-maven-plugin}. - * - * @return the {@code User-Agent} header to send. The {@code User-Agent} can be disabled by - * setting the system property variable {@code _JIB_DISABLE_USER_AGENT} to any non-empty - * string. - */ - @VisibleForTesting - static String makeUserAgent() { - if (!Strings.isNullOrEmpty(System.getProperty("_JIB_DISABLE_USER_AGENT"))) { - return ""; - } - - String version = RegistryClient.class.getPackage().getImplementationVersion(); - StringBuilder userAgentBuilder = new StringBuilder(); - userAgentBuilder.append("jib"); - if (version != null) { - userAgentBuilder.append(" ").append(version); - } - if (userAgentSuffix != null) { - userAgentBuilder.append(" ").append(userAgentSuffix); - } - return userAgentBuilder.toString(); - } - private final JibLogger buildLogger; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 44575cff90..57360b6855 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -73,7 +73,7 @@ public void setUp() throws DigestException { Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); - Mockito.when(mockBuildConfiguration.getCreatedBy()).thenReturn("jib"); + Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 8db52564e0..5ac72160ce 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -96,7 +96,7 @@ public void testBuilder() throws Exception { .setTargetFormat(OCIManifestTemplate.class) .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) - .setCreatedBy(expectedCreatedBy); + .setToolName(expectedCreatedBy); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -143,7 +143,7 @@ public void testBuilder() throws Exception { Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); Assert.assertEquals( expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertEquals(expectedCreatedBy, buildConfiguration.getCreatedBy()); + Assert.assertEquals(expectedCreatedBy, buildConfiguration.getToolName()); } @Test @@ -179,7 +179,7 @@ public void testBuilder_default() { Assert.assertEquals(buildConfiguration.getTargetFormat(), V22ManifestTemplate.class); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); - Assert.assertEquals("jib", buildConfiguration.getCreatedBy()); + Assert.assertEquals("jib", buildConfiguration.getToolName()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 4d9f8ad09f..425d9ed8fe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -51,10 +51,10 @@ public void testGetUserAgent_null() { .getUserAgent() .startsWith("jib")); - RegistryClient.setUserAgentSuffix(null); Assert.assertTrue( testRegistryClientFactory .setAuthorization(mockAuthorization) + .setUserAgentSuffix(null) .newRegistryClient() .getUserAgent() .startsWith("jib")); @@ -62,20 +62,14 @@ public void testGetUserAgent_null() { @Test public void testGetUserAgent() { - RegistryClient.setUserAgentSuffix("some user agent suffix"); - - Assert.assertTrue( - testRegistryClientFactory - .setAllowInsecureRegistries(true) - .newRegistryClient() - .getUserAgent() - .startsWith("jib ")); - Assert.assertTrue( + RegistryClient registryClient = testRegistryClientFactory .setAllowInsecureRegistries(true) - .newRegistryClient() - .getUserAgent() - .endsWith(" some user agent suffix")); + .setUserAgentSuffix("some user agent suffix") + .newRegistryClient(); + + Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); + Assert.assertTrue(registryClient.getUserAgent().endsWith(" some user agent suffix")); } @Test diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 273932180f..b17413444c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -39,7 +39,13 @@ /** Obtains information about a Gradle {@link Project} that uses Jib. */ class GradleProjectProperties implements ProjectProperties { + /** Used to generate the User-Agent header and history metadata. */ + static final String TOOL_NAME = "jib-gradle-plugin"; + + /** Used for logging during main class inference. */ private static final String PLUGIN_NAME = "jib"; + + /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'jar' task"; /** @return a GradleProjectProperties from the given project and logger. */ diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index a80e17bc5e..9d5b3c9953 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -29,7 +29,6 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; -import com.google.cloud.tools.jib.registry.RegistryClient; import java.time.Instant; import java.util.List; import javax.annotation.Nullable; @@ -38,9 +37,6 @@ /** Configures and provides builders for the image building tasks. */ class PluginConfigurationProcessor { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-gradle-plugin"; - /** * Sets up {@link BuildConfiguration} that is common among the image building tasks. This includes * setting up the base image reference/authorization, container configuration, cache @@ -60,7 +56,6 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided GradleJibLogger.disableHttpLogging(); - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); ImageReference baseImage = ImageReference.parse(jibExtension.getBaseImage()); @@ -109,7 +104,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) - .setCreatedBy("jib-gradle-plugin") + .setToolName(GradleProjectProperties.TOOL_NAME) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index fd2c3d5c4a..35c0fb3c7b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -34,7 +34,13 @@ /** Obtains information about a {@link MavenProject}. */ class MavenProjectProperties implements ProjectProperties { + /** Used to generate the User-Agent header and history metadata. */ + static final String TOOL_NAME = "jib-maven-plugin"; + + /** Used for logging during main class inference. */ private static final String PLUGIN_NAME = "jib-maven-plugin"; + + /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'maven-jar-plugin'"; /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 551f46aa22..177b3b1d6b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; -import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.List; @@ -38,9 +37,6 @@ /** Configures and provides builders for the image building goals. */ class PluginConfigurationProcessor { - /** {@code User-Agent} header suffix to send to the registry. */ - private static final String USER_AGENT_SUFFIX = "jib-maven-plugin"; - /** * Sets up {@link BuildConfiguration} that is common among the image building goals. This includes * setting up the base image reference/authorization, container configuration, cache @@ -63,7 +59,6 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided MavenJibLogger.disableHttpLogging(); - RegistryClient.setUserAgentSuffix(USER_AGENT_SUFFIX); ImageReference baseImage = parseImageReference(jibPluginConfiguration.getBaseImage(), "from"); @@ -128,7 +123,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) - .setCreatedBy("jib-maven-plugin") + .setToolName(MavenProjectProperties.TOOL_NAME) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); From 8b9e3a7d53f725b3f622adc5c74a641b8e6cbd31 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 6 Sep 2018 15:09:42 -0400 Subject: [PATCH 0189/2020] Report registry and image on RegistryAuthenticationFailedException (#944) --- ...RegistryAuthenticationFailedException.java | 28 +++++++++-- .../jib/registry/RegistryAuthenticator.java | 43 +++++++++++++---- ...stryAuthenticationFailedExceptionTest.java | 48 +++++++++++++++++++ .../registry/RegistryAuthenticatorTest.java | 4 +- .../jib/plugins/common/BuildStepsRunner.java | 6 ++- 5 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java index c00785febe..d574d62206 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java @@ -16,16 +16,34 @@ package com.google.cloud.tools.jib.registry; +import java.text.MessageFormat; + /** Thrown because registry authentication failed. */ public class RegistryAuthenticationFailedException extends Exception { - private static final String REASON_PREFIX = "Failed to authenticate with the registry because: "; + private static final String REASON = "Failed to authenticate with registry {0}/{1} because: {2}"; + private final String serverUrl; + private final String imageName; + + RegistryAuthenticationFailedException(String serverUrl, String imageName, Throwable cause) { + super(MessageFormat.format(REASON, serverUrl, imageName, cause.getMessage()), cause); + this.serverUrl = serverUrl; + this.imageName = imageName; + } + + RegistryAuthenticationFailedException(String serverUrl, String imageName, String reason) { + super(MessageFormat.format(REASON, serverUrl, imageName, reason)); + this.serverUrl = serverUrl; + this.imageName = imageName; + } - RegistryAuthenticationFailedException(Throwable cause) { - super(REASON_PREFIX + cause.getMessage(), cause); + /** @return the server being authenticated */ + public String getServerUrl() { + return serverUrl; } - RegistryAuthenticationFailedException(String reason) { - super(REASON_PREFIX + reason); + /** @return the image being authenticated */ + public String getImageName() { + return imageName; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 9785ed8b42..a861a46912 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -87,7 +87,7 @@ public RegistryAuthenticator initialize() .getRegistryAuthenticator(); } catch (MalformedURLException ex) { - throw new RegistryAuthenticationFailedException(ex); + throw new RegistryAuthenticationFailedException(serverUrl, repository, ex); } catch (InsecureRegistryException ex) { // Cannot skip certificate validation or use HTTP, so just return null. @@ -133,13 +133,21 @@ static RegistryAuthenticator fromAuthenticationMethod( // Checks that the authentication method starts with 'bearer ' (case insensitive). if (!authenticationMethod.matches("^(?i)(bearer) .*")) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "Bearer"); + throw newRegistryAuthenticationFailedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), + authenticationMethod, + "Bearer"); } Pattern realmPattern = Pattern.compile("realm=\"(.*?)\""); Matcher realmMatcher = realmPattern.matcher(authenticationMethod); if (!realmMatcher.find()) { - throw newRegistryAuthenticationFailedException(authenticationMethod, "realm"); + throw newRegistryAuthenticationFailedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), + authenticationMethod, + "realm"); } String realm = realmMatcher.group(1); @@ -151,13 +159,14 @@ static RegistryAuthenticator fromAuthenticationMethod( ? serviceMatcher.group(1) : registryEndpointRequestProperties.getServerUrl(); - return new RegistryAuthenticator( - realm, service, registryEndpointRequestProperties.getImageName()); + return new RegistryAuthenticator(realm, service, registryEndpointRequestProperties); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( - String authenticationMethod, String authParam) { + String registry, String repository, String authenticationMethod, String authParam) { return new RegistryAuthenticationFailedException( + registry, + repository, "'" + authParam + "' was not found in the 'WWW-Authenticate' header, tried to parse: " @@ -189,10 +198,21 @@ private String getToken() { } private final String authenticationUrlBase; + private final RegistryEndpointRequestProperties registryEndpointRequestProperties; @Nullable private Authorization authorization; - RegistryAuthenticator(String realm, String service, String repository) { - authenticationUrlBase = realm + "?service=" + service + "&scope=repository:" + repository + ":"; + RegistryAuthenticator( + String realm, + String service, + RegistryEndpointRequestProperties registryEndpointRequestProperties) { + authenticationUrlBase = + realm + + "?service=" + + service + + "&scope=repository:" + + registryEndpointRequestProperties.getImageName() + + ":"; + this.registryEndpointRequestProperties = registryEndpointRequestProperties; } /** @@ -257,13 +277,18 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), "Did not get token in authentication response from " + authenticationUrl); } return Authorizations.withBearerToken(responseJson.getToken()); } } catch (IOException ex) { - throw new RegistryAuthenticationFailedException(ex); + throw new RegistryAuthenticationFailedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), + ex); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java new file mode 100644 index 0000000000..638b2e53cd --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.registry; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RegistryAuthenticationFailedException}. */ +public class RegistryAuthenticationFailedExceptionTest { + + @Test + public void testRegistryAuthenticationFailedException_message() { + RegistryAuthenticationFailedException exception = + new RegistryAuthenticationFailedException("serverUrl", "imageName", "message"); + Assert.assertEquals("serverUrl", exception.getServerUrl()); + Assert.assertEquals("imageName", exception.getImageName()); + Assert.assertEquals( + "Failed to authenticate with registry serverUrl/imageName because: message", + exception.getMessage()); + } + + @Test + public void testRegistryAuthenticationFailedException_exception() { + Throwable cause = new Throwable("message"); + RegistryAuthenticationFailedException exception = + new RegistryAuthenticationFailedException("serverUrl", "imageName", cause); + Assert.assertEquals("serverUrl", exception.getServerUrl()); + Assert.assertEquals("imageName", exception.getImageName()); + Assert.assertSame(cause, exception.getCause()); + Assert.assertEquals( + "Failed to authenticate with registry serverUrl/imageName because: message", + exception.getMessage()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index e13d4ecf71..ec12297c82 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -74,7 +74,7 @@ public void testFromAuthenticationMethod_noBearer() { } catch (RegistryAuthenticationFailedException ex) { Assert.assertEquals( - "Failed to authenticate with the registry because: 'Bearer' was not found in the 'WWW-Authenticate' header, tried to parse: realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + "Failed to authenticate with registry someserver/someimage because: 'Bearer' was not found in the 'WWW-Authenticate' header, tried to parse: realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", ex.getMessage()); } } @@ -88,7 +88,7 @@ public void testFromAuthenticationMethod_noRealm() { } catch (RegistryAuthenticationFailedException ex) { Assert.assertEquals( - "Failed to authenticate with the registry because: 'realm' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer scope=\"somescope\"", + "Failed to authenticate with registry someserver/someimage because: 'realm' was not found in the 'WWW-Authenticate' header, tried to parse: Bearer scope=\"somescope\"", ex.getMessage()); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 33dc62d1b0..3b2f364728 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -199,10 +199,12 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { + RegistryAuthenticationFailedException failureException = + (RegistryAuthenticationFailedException) exceptionDuringBuildSteps; handleRegistryUnauthorizedException( new RegistryUnauthorizedException( - buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getImageRepository(), + failureException.getServerUrl(), + failureException.getImageName(), (HttpResponseException) exceptionDuringBuildSteps.getCause()), helpfulSuggestions); From 0b2e09ae4ab690ac262d3f76f55753a280986f90 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 6 Sep 2018 16:09:28 -0400 Subject: [PATCH 0190/2020] Lower verbosity of insecure registry failover (#945) --- .../tools/jib/registry/RegistryEndpointCaller.java | 4 ++-- .../jib/registry/RegistryEndpointCallerTest.java | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index b24428e441..dafd646011 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -169,7 +169,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } try { - logger.warn( + logger.info( "Cannot verify server at " + url + ". Attempting again with no TLS verification."); return call(url, getInsecureConnectionFactory()); @@ -182,7 +182,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr private T fallBackToHttp(URL url) throws IOException, RegistryException { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); - logger.warn( + logger.info( "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl); return call(httpUrl.toURL(), connectionFactory); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 7c781d59e7..785dc29791 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -163,7 +163,7 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); Mockito.verify(mockBuildLogger) - .warn( + .info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); } @@ -190,10 +190,10 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); Mockito.verify(mockBuildLogger) - .warn( + .info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); Mockito.verify(mockBuildLogger) - .warn( + .info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); } @@ -216,7 +216,7 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); Mockito.verify(mockBuildLogger) - .warn( + .info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); } @@ -338,10 +338,10 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); Mockito.verify(mockBuildLogger) - .warn( + .info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); Mockito.verify(mockBuildLogger) - .warn( + .info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); } From 56d818ac5a61d40da7e3c06ab5bb787923ab067e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 7 Sep 2018 12:39:22 -0400 Subject: [PATCH 0191/2020] Restart Docker for Mac to fix clock skew in VM (#951) --- kokoro/continuous.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index fc32908ce5..415557bddd 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -19,6 +19,16 @@ docker kill $(docker ps --all --quiet) || true # Sets the integration testing project. export JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing +# Restarting Docker for Mac to get around the certificate expiration issue: +# b/112707824 +# https://github.com/GoogleContainerTools/jib/issues/730#issuecomment-413603874 +# https://github.com/moby/moby/issues/11534 +# TODO: remove this temporary fix once b/112707824 is permanently fixed. +if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then + osascript -e 'quit app "Docker"' + open -a Docker +fi + (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd github/jib/jib-plugins-common; ./gradlew clean build --info --stacktrace) (cd github/jib/jib-maven-plugin; ./mvnw clean install -P integration-tests -B -U -X) From 411da83bf9ef735e9aa0731a8d041498273b404b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Sep 2018 16:17:22 -0400 Subject: [PATCH 0192/2020] Adds DefaultCacheStorage#write. (#879) --- .../google/cloud/tools/jib/blob/Blobs.java | 15 +- .../jib/builder/steps/WriteTarFileStep.java | 4 +- .../tools/jib/filesystem/FileOperations.java | 33 +++ .../jib/filesystem/TemporaryDirectory.java | 58 +++++ .../tools/jib/ncache/DefaultCacheStorage.java | 91 +++++++ .../jib/ncache/DefaultCacheStorageFiles.java | 105 ++++++++ .../jib/ncache/DefaultCacheStorageWriter.java | 236 ++++++++++++++++++ .../jib/filesystem/FileOperationsTest.java | 54 ++++ .../filesystem/TemporaryDirectoryTest.java | 68 +++++ .../ncache/DefaultCacheStorageFilesTest.java | 118 +++++++++ .../ncache/DefaultCacheStorageWriterTest.java | 124 +++++++++ 11 files changed, 892 insertions(+), 14 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 21a253001c..2d493d607f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -16,16 +16,13 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.filesystem.FileOperations; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.EnumSet; /** Static methods for {@link Blob}. */ public class Blobs { @@ -85,15 +82,7 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { * @throws IOException if writing the BLOB fails */ public static BlobDescriptor writeToFileWithLock(Blob blob, Path blobFile) throws IOException { - EnumSet createOrTruncate = - EnumSet.of( - StandardOpenOption.CREATE, - StandardOpenOption.WRITE, - StandardOpenOption.TRUNCATE_EXISTING); - // channel is closed by outputStream.close() - FileChannel channel = FileChannel.open(blobFile, createOrTruncate); - channel.lock(); // released when channel is closed - try (OutputStream outputStream = Channels.newOutputStream(channel)) { + try (OutputStream outputStream = FileOperations.newLockingOutputStream(blobFile)) { return blob.writeTo(outputStream); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index ba9eb3a967..cdfa8338c5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -93,7 +94,8 @@ private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, I // Build the image to a tarball buildConfiguration.getBuildLogger().lifecycle("Building image to tar file..."); Files.createDirectories(outputPath.getParent()); - try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outputPath))) { + try (OutputStream outputStream = + new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { new ImageToTarballTranslator(image) .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage()) .writeTo(outputStream); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java index 75f9df0917..d4f6e09839 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java @@ -18,8 +18,15 @@ import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; /** Static methods for operating on the filesystem. */ public class FileOperations { @@ -52,5 +59,31 @@ public static void copy(ImmutableList sourceFiles, Path destDir) throws IO } } + /** + * Acquires an exclusive {@link FileLock} on the {@code file} and opens an {@link OutputStream} to + * write to it. The file will be created if it does not exist, or truncated to length 0 if it does + * exist. The {@link OutputStream} must be closed to release the lock. + * + *

The locking mechanism should not be used as a concurrency management feature. Rather, this + * should be used as a way to prevent concurrent writes to {@code file}. Concurrent attempts to + * lock {@code file} will result in {@link OverlappingFileLockException}s. + * + * @param file the file to write to + * @return an {@link OutputStream} that writes to the file + * @throws IOException if an I/O exception occurs + */ + public static OutputStream newLockingOutputStream(Path file) throws IOException { + EnumSet createOrTruncate = + EnumSet.of( + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + // Channel is closed by outputStream.close(). + FileChannel channel = FileChannel.open(file, createOrTruncate); + // Lock is released when channel is closed. + channel.lock(); + return Channels.newOutputStream(channel); + } + private FileOperations() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java new file mode 100644 index 0000000000..d4db8d84a2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * A temporary directory that tries to delete itself upon close. Note that deletion is NOT + * guaranteed. + */ +public class TemporaryDirectory implements Closeable { + + private final Path temporaryDirectory; + + /** + * Creates a new temporary directory. + * + * @throws IOException if an I/O exception occurs + */ + public TemporaryDirectory() throws IOException { + temporaryDirectory = Files.createTempDirectory(null); + } + + /** + * Gets the temporary directory. + * + * @return the temporary directory. + */ + public Path getDirectory() { + return temporaryDirectory; + } + + @Override + public void close() throws IOException { + if (Files.exists(temporaryDirectory)) { + MoreFiles.deleteRecursively(temporaryDirectory, RecursiveDeleteOption.ALLOW_INSECURE); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java new file mode 100644 index 0000000000..d5c29d0403 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Default implementation of {@link CacheStorage}. This storage engine stores cache entries in a + * directory in the format: + * + *

{@code
+ * layers/
+ *   /
+ *     .layer
+ *     metadata
+ *   ...
+ * selectors/
+ *   
+ *   ...
+ * }
+ * + * Layers entries are stored in their own directories under the {@code layers/} directory. Each + * layer directory is named by the layer digest. Inside each layer directory, the layer contents is + * the {@code .layer} file prefixed with the layer diff ID, and the metadata is the {@code metadata} + * file. + * + *

Selectors are stored in the {@code selectors/} directory. Each selector file is named by the + * selector digest. The contents of a selector file is the digest of the layer it selects. + */ +public class DefaultCacheStorage implements CacheStorage { + + /** + * Instantiates a {@link CacheStorage} backed by this storage engine. + * + * @param cacheDirectory the directory for this cache + * @return a new {@link CacheStorage} + */ + public static CacheStorage withDirectory(Path cacheDirectory) { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(cacheDirectory); + return new DefaultCacheStorage(new DefaultCacheStorageWriter(defaultCacheStorageFiles)); + } + + private final DefaultCacheStorageWriter defaultCacheStorageWriter; + + private DefaultCacheStorage(DefaultCacheStorageWriter defaultCacheStorageWriter) { + this.defaultCacheStorageWriter = defaultCacheStorageWriter; + } + + @Override + public CacheEntry write(CacheWrite cacheWrite) throws IOException { + return defaultCacheStorageWriter.write(cacheWrite); + } + + @Override + public List listDigests() throws IOException { + // TODO: Implement + return Collections.emptyList(); + } + + @Override + public Optional retrieve(DescriptorDigest layerDigest) throws IOException { + // TODO: Implement + return Optional.empty(); + } + + @Override + public Optional select(DescriptorDigest selector) throws IOException { + // TODO: Implement + return Optional.empty(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java new file mode 100644 index 0000000000..d5baff9073 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.nio.file.Path; + +/** Resolves the files used in the default cache storage engine. */ +class DefaultCacheStorageFiles { + + private static final String LAYERS_DIRECTORY = "layers"; + private static final String LAYER_FILENAME_SUFFIX = ".layer"; + private static final String METADATA_FILENAME = "metadata"; + private static final String SELECTORS_DIRECTORY = "selectors"; + + private final Path cacheDirectory; + + DefaultCacheStorageFiles(Path cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + /** + * Resolves the layer contents file. + * + * @param layerDigest the layer digest + * @param layerDiffId the layer diff Id + * @return the layer contents file + */ + Path getLayerFile(DescriptorDigest layerDigest, DescriptorDigest layerDiffId) { + return getLayerDirectory(layerDigest).resolve(getLayerFilename(layerDiffId)); + } + + /** + * Gets the filename for the layer file. The filename is in the form {@code .layer}. + * + * @param layerDiffId the layer's diff ID + * @return the layer filename + */ + String getLayerFilename(DescriptorDigest layerDiffId) { + return layerDiffId.getHash() + LAYER_FILENAME_SUFFIX; + } + + /** + * Resolves the layer metadata file. + * + * @param layerDigest the layer digest + * @return the layer metadata file + */ + Path getMetadataFile(DescriptorDigest layerDigest) { + return getLayerDirectory(layerDigest).resolve(METADATA_FILENAME); + } + + /** + * Gets the filename for the metadata file. + * + * @return the filename for the metadata file + */ + String getMetadataFilename() { + return METADATA_FILENAME; + } + + /** + * Resolves a selector file. + * + * @param selector the selector digest + * @return the selector file + */ + Path getSelectorFile(DescriptorDigest selector) { + return cacheDirectory.resolve(SELECTORS_DIRECTORY).resolve(selector.getHash()); + } + + /** + * Resolves the layers directory. + * + * @return the directory with all the layer directories + */ + Path getLayersDirectory() { + return cacheDirectory.resolve(LAYERS_DIRECTORY); + } + + /** + * Gets the directory for the layer with digest {@code layerDigest}. + * + * @param layerDigest the digest of the layer + * @return the directory for that {@code layerDigest} + */ + Path getLayerDirectory(DescriptorDigest layerDigest) { + return getLayersDirectory().resolve(layerDigest.getHash()); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java new file mode 100644 index 0000000000..495c8c0ef2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java @@ -0,0 +1,236 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; +import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.zip.GZIPOutputStream; + +/** Writes to the default cache storage engine. */ +class DefaultCacheStorageWriter { + + /** Holds information about a layer that was written. */ + private static class WrittenLayer { + + private final DescriptorDigest layerDigest; + private final DescriptorDigest layerDiffId; + private final long layerSize; + + private WrittenLayer( + DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize) { + this.layerDigest = layerDigest; + this.layerDiffId = layerDiffId; + this.layerSize = layerSize; + } + } + + /** + * Attempts to move {@code source} to {@code destination}. If {@code destination} already exists, + * this does nothing. Attempts an atomic move first, and falls back to non-atomic if the + * filesystem does not support atomic moves. + * + * @param source the source path + * @param destination the destination path + * @throws IOException if an I/O exception occurs + */ + private static void moveIfDoesNotExist(Path source, Path destination) throws IOException { + try { + Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE); + + } catch (FileAlreadyExistsException ignored) { + // If the file already exists, we skip renaming and use the existing file. This happens if a + // new layer happens to have the same content as a previously-cached layer. + // + // Do not attempt to remove the try-catch block with the idea of checking file existence + // before moving; there can be concurrent file moves. + + } catch (AtomicMoveNotSupportedException ignored) { + try { + Files.move(source, destination); + + } catch (FileAlreadyExistsException alsoIgnored) { + // Same reasoning + } + } + } + + private final DefaultCacheStorageFiles defaultCacheStorageFiles; + + DefaultCacheStorageWriter(DefaultCacheStorageFiles defaultCacheStorageFiles) { + this.defaultCacheStorageFiles = defaultCacheStorageFiles; + } + + /** + * Writes the {@link CacheWrite}. + * + *

The {@link CacheWrite} is written out to the cache directory in the form: + * + *

    + *
  • The {@link CacheWrite#getLayerBlob} and {@link CacheWrite#getMetadataBlob} are written to + * the layer directory under the layers directory corresponding to the layer blob. + *
  • The {@link CacheWrite#getSelector} is written to the selector file under the selectors + * directory. + *
+ * + * Note that writes that fail to clean up unfinished temporary directories could result in stray + * directories in the layers directory. Cache reads should ignore these stray directories. + * + * @param cacheWrite the {@link CacheWrite} to write out + * @return the {@link CacheEntry} representing the written entry + * @throws IOException if an I/O exception occurs + */ + CacheEntry write(CacheWrite cacheWrite) throws IOException { + // Creates the layers directory if it doesn't exist. + Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); + + // Creates the temporary directory. + try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + + // Writes the layer file to the temporary directory. + WrittenLayer writtenLayer = + writeLayerBlobToDirectory(cacheWrite.getLayerBlob(), temporaryLayerDirectory); + + // Writes the metadata to the temporary directory. + if (cacheWrite.getMetadataBlob().isPresent()) { + writeMetadataBlobToDirectory(cacheWrite.getMetadataBlob().get(), temporaryLayerDirectory); + } + + // Moves the temporary directory to the final location. + moveIfDoesNotExist( + temporaryLayerDirectory, + defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + + // Updates cacheEntry with the blob information. + Path layerFile = + defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + DefaultCacheEntry.Builder cacheEntryBuilder = + DefaultCacheEntry.builder() + .setLayerDigest(writtenLayer.layerDigest) + .setLayerDiffId(writtenLayer.layerDiffId) + .setLayerSize(writtenLayer.layerSize) + .setLayerBlob(Blobs.from(layerFile)); + if (cacheWrite.getMetadataBlob().isPresent()) { + Path metadataFile = defaultCacheStorageFiles.getMetadataFile(writtenLayer.layerDigest); + cacheEntryBuilder.setMetadataBlob(Blobs.from(metadataFile)).build(); + } + + // Write the selector file. + if (cacheWrite.getSelector().isPresent()) { + writeSelector(cacheWrite.getSelector().get(), writtenLayer.layerDigest); + } + + return cacheEntryBuilder.build(); + } + } + + /** + * Writes the {@code layerBlob} to the {@code layerDirectory}. + * + * @param layerBlob the layer {@link Blob} + * @param layerDirectory the directory for the layer + * @return a {@link WrittenLayer} with the written layer information + * @throws IOException if an I/O exception occurs + */ + private WrittenLayer writeLayerBlobToDirectory(Blob layerBlob, Path layerDirectory) + throws IOException { + Path temporaryLayerFile = Files.createTempFile(layerDirectory, null, null); + temporaryLayerFile.toFile().deleteOnExit(); + + try (CountingDigestOutputStream compressedDigestOutputStream = + new CountingDigestOutputStream( + new BufferedOutputStream(FileOperations.newLockingOutputStream(temporaryLayerFile)))) { + // Writes the layer with GZIP compression. The original bytes are captured as the layer's + // diff ID and the bytes outputted from the GZIP compression are captured as the layer's + // content descriptor. + GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream); + DescriptorDigest layerDiffId = layerBlob.writeTo(compressorStream).getDigest(); + + // The GZIPOutputStream must be closed in order to write out the remaining compressed data. + compressorStream.close(); + BlobDescriptor compressedBlobDescriptor = compressedDigestOutputStream.toBlobDescriptor(); + DescriptorDigest layerDigest = compressedBlobDescriptor.getDigest(); + long layerSize = compressedBlobDescriptor.getSize(); + + // Renames the temporary layer file to the correct filename. + Path layerFile = + layerDirectory.resolve(defaultCacheStorageFiles.getLayerFilename(layerDiffId)); + moveIfDoesNotExist(temporaryLayerFile, layerFile); + + return new WrittenLayer(layerDigest, layerDiffId, layerSize); + } + } + + /** + * Writes the {@code metadataBlob} to the {@code layerDirectory}. + * + * @param metadataBlob the metadata {@link Blob} + * @param layerDirectory the directory for the layer the metadata is for + * @throws IOException if an I/O exception occurs + */ + private void writeMetadataBlobToDirectory(Blob metadataBlob, Path layerDirectory) + throws IOException { + Path metadataFile = layerDirectory.resolve(defaultCacheStorageFiles.getMetadataFilename()); + Blobs.writeToFileWithLock(metadataBlob, metadataFile); + } + + /** + * Writes the {@code selector} to a file in the selectors directory, with contents {@code + * layerDigest}. + * + * @param selector the selector + * @param layerDigest the layer digest it selects + * @throws IOException if an I/O exception occurs + */ + private void writeSelector(DescriptorDigest selector, DescriptorDigest layerDigest) + throws IOException { + Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + + // Creates the selectors directory if it doesn't exist. + Files.createDirectories(selectorFile.getParent()); + + // Writes the selector to a temporary file and then moves the file to the intended location. + Path temporarySelectorFile = Files.createTempFile(null, null); + temporarySelectorFile.toFile().deleteOnExit(); + Blobs.writeToFileWithLock(Blobs.from(layerDigest.getHash()), temporarySelectorFile); + + // Attempts an atomic move first, and falls back to non-atomic if the file system does not + // support atomic moves. + try { + Files.move( + temporarySelectorFile, + selectorFile, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + + } catch (AtomicMoveNotSupportedException ignored) { + Files.move(temporarySelectorFile, selectorFile, StandardCopyOption.REPLACE_EXISTING); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java index 25863a610f..3992971c11 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java @@ -17,12 +17,21 @@ package com.google.cloud.tools.jib.filesystem; import com.google.common.collect.ImmutableList; +import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.URISyntaxException; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -31,6 +40,30 @@ /** Tests for {@link FileOperations}. */ public class FileOperationsTest { + private static void verifyWriteWithLock(Path file) throws IOException { + OutputStream fileOutputStream = FileOperations.newLockingOutputStream(file); + + // Checks that the file was locked. + try { + FileChannel.open(file, StandardOpenOption.READ).tryLock(0, Long.MAX_VALUE, true); + Assert.fail("Lock attempt should have failed"); + + } catch (OverlappingFileLockException ex) { + // pass + } + + fileOutputStream.write("jib".getBytes(StandardCharsets.UTF_8)); + fileOutputStream.close(); + + FileChannel channel = FileChannel.open(file, StandardOpenOption.READ); + channel.lock(0, Long.MAX_VALUE, true); + try (InputStream inputStream = Channels.newInputStream(channel); + InputStreamReader inputStreamReader = + new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + Assert.assertEquals("jib", CharStreams.toString(inputStreamReader)); + } + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test @@ -56,6 +89,27 @@ public void testCopy() throws IOException, URISyntaxException { assertFilesEqual(dirLayer.resolve("foo"), destDir.resolve("layer").resolve("foo")); } + @Test + public void testNewLockingOutputStream_newFile() throws IOException { + Path file = Files.createTempFile("", ""); + // Ensures file doesn't exist. + Assert.assertTrue(Files.deleteIfExists(file)); + + verifyWriteWithLock(file); + } + + @Test + public void testNewLockingOutputStream_existingFile() throws IOException { + Path file = Files.createTempFile("", ""); + // Writes out more bytes to ensure proper truncated. + byte[] dataBytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + Files.write(file, dataBytes, StandardOpenOption.WRITE); + Assert.assertTrue(Files.exists(file)); + Assert.assertEquals(10, Files.size(file)); + + verifyWriteWithLock(file); + } + private void assertFilesEqual(Path file1, Path file2) throws IOException { Assert.assertArrayEquals(Files.readAllBytes(file1), Files.readAllBytes(file2)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java new file mode 100644 index 0000000000..338aaa53c1 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link TemporaryDirectory}. */ +public class TemporaryDirectoryTest { + + private static void createFilesInDirectory(Path directory) + throws IOException, URISyntaxException { + Path testFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + new DirectoryWalker(testFilesDirectory) + .filterRoot() + .walk(path -> Files.copy(path, directory.resolve(testFilesDirectory.relativize(path)))); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testClose_directoryDeleted() throws IOException, URISyntaxException { + try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + createFilesInDirectory(temporaryDirectory.getDirectory()); + + temporaryDirectory.close(); + Assert.assertFalse(Files.exists(temporaryDirectory.getDirectory())); + } + } + + @Test + public void testClose_directoryNotDeletedIfMoved() throws IOException, URISyntaxException { + Path destinationParent = temporaryFolder.newFolder().toPath(); + + try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + createFilesInDirectory(temporaryDirectory.getDirectory()); + + Assert.assertFalse(Files.exists(destinationParent.resolve("destination"))); + Files.move(temporaryDirectory.getDirectory(), destinationParent.resolve("destination")); + + temporaryDirectory.close(); + Assert.assertFalse(Files.exists(temporaryDirectory.getDirectory())); + Assert.assertTrue(Files.exists(destinationParent.resolve("destination"))); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java new file mode 100644 index 0000000000..6e3c12d210 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.nio.file.Paths; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DefaultCacheStorageFiles}. */ +public class DefaultCacheStorageFilesTest { + + private static final DefaultCacheStorageFiles testDefaultCacheStorageFiles = + new DefaultCacheStorageFiles(Paths.get("cache/directory")); + + @Test + public void testGetLayerFile() throws DigestException { + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + DescriptorDigest diffId = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "layers", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.layer"), + testDefaultCacheStorageFiles.getLayerFile(layerDigest, diffId)); + } + + @Test + public void testGetLayerFilename() throws DigestException { + DescriptorDigest diffId = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + Assert.assertEquals( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.layer", + testDefaultCacheStorageFiles.getLayerFilename(diffId)); + } + + @Test + public void testGetMetadataFile() throws DigestException { + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "layers", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "metadata"), + testDefaultCacheStorageFiles.getMetadataFile(layerDigest)); + } + + @Test + public void testGetMetadataFilename() { + Assert.assertEquals("metadata", testDefaultCacheStorageFiles.getMetadataFilename()); + } + + @Test + public void testGetSelectorFile() throws DigestException { + DescriptorDigest selector = + DescriptorDigest.fromHash( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "selectors", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + testDefaultCacheStorageFiles.getSelectorFile(selector)); + } + + @Test + public void testGetLayersDirectory() { + Assert.assertEquals( + Paths.get("cache", "directory", "layers"), + testDefaultCacheStorageFiles.getLayersDirectory()); + } + + @Test + public void testGetLayerDirectory() throws DigestException { + DescriptorDigest layerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + Assert.assertEquals( + Paths.get( + "cache", + "directory", + "layers", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + testDefaultCacheStorageFiles.getLayerDirectory(layerDigest)); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java new file mode 100644 index 0000000000..3ea10fd4ec --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link DefaultCacheStorageWriter}. */ +public class DefaultCacheStorageWriterTest { + + private static DescriptorDigest getDigest(Blob blob) throws IOException { + return blob.writeTo(new CountingDigestOutputStream(ByteStreams.nullOutputStream())).getDigest(); + } + + private static BlobDescriptor getCompressedBlobDescriptor(Blob blob) throws IOException { + CountingDigestOutputStream compressedDigestOutputStream = + new CountingDigestOutputStream(ByteStreams.nullOutputStream()); + try (GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream)) { + blob.writeTo(compressorStream); + } + return compressedDigestOutputStream.toBlobDescriptor(); + } + + private static Blob decompress(Blob blob) throws IOException { + return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private DefaultCacheStorageFiles defaultCacheStorageFiles; + + @Before + public void setUp() throws IOException { + defaultCacheStorageFiles = new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + } + + @Test + public void testWrite_layerOnly() throws IOException { + Blob layerBlob = Blobs.from("layerBlob"); + CacheEntry cacheEntry = verifyWrite(DefaultCacheWrite.layerOnly(layerBlob), layerBlob); + Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); + } + + @Test + public void testWrite_withSelectorAndMetadata() throws IOException { + Blob layerBlob = Blobs.from("layerBlob"); + DescriptorDigest layerDigest = getCompressedBlobDescriptor(layerBlob).getDigest(); + DescriptorDigest selector = getDigest(Blobs.from("selector")); + Blob metadataBlob = Blobs.from("metadata"); + + CacheEntry cacheEntry = + verifyWrite( + DefaultCacheWrite.withSelectorAndMetadata(layerBlob, selector, metadataBlob), + layerBlob); + + // Verifies cacheEntry is correct. + Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); + Assert.assertArrayEquals( + Blobs.writeToByteArray(metadataBlob), + Blobs.writeToByteArray(cacheEntry.getMetadataBlob().get())); + + // Verifies that the files are present. + Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Assert.assertTrue(Files.exists(selectorFile)); + Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); + Assert.assertTrue( + Files.exists(defaultCacheStorageFiles.getMetadataFile(cacheEntry.getLayerDigest()))); + } + + private CacheEntry verifyWrite(CacheWrite cacheWriteEntry, Blob expectedLayerBlob) + throws IOException { + BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(expectedLayerBlob); + DescriptorDigest layerDiffId = getDigest(expectedLayerBlob); + + DefaultCacheStorageWriter defaultCacheStorageWriter = + new DefaultCacheStorageWriter(defaultCacheStorageFiles); + CacheEntry cacheEntry = defaultCacheStorageWriter.write(cacheWriteEntry); + + // Verifies cacheEntry is correct. + Assert.assertEquals(layerBlobDescriptor.getDigest(), cacheEntry.getLayerDigest()); + Assert.assertEquals(layerDiffId, cacheEntry.getLayerDiffId()); + Assert.assertEquals(layerBlobDescriptor.getSize(), cacheEntry.getLayerSize()); + Assert.assertArrayEquals( + Blobs.writeToByteArray(expectedLayerBlob), + Blobs.writeToByteArray(decompress(cacheEntry.getLayerBlob()))); + + // Verifies that the files are present. + Assert.assertTrue( + Files.exists( + defaultCacheStorageFiles.getLayerFile( + cacheEntry.getLayerDigest(), cacheEntry.getLayerDiffId()))); + + return cacheEntry; + } +} From 971604210615f34bc5df634512f21156c71f544c Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Sep 2018 16:29:57 -0400 Subject: [PATCH 0193/2020] Adds EventEmitter. (#931) --- .../cloud/tools/jib/event/EventEmitter.java | 45 +++++++++++++ .../cloud/tools/jib/event/EventHandlers.java | 50 +++----------- .../google/cloud/tools/jib/event/Handler.java | 42 ++++++++++++ .../cloud/tools/jib/event/JibEvent.java | 5 +- .../cloud/tools/jib/event/JibEventType.java | 2 +- .../tools/jib/event/EventEmitterTest.java | 67 +++++++++++++++++++ .../tools/jib/event/EventHandlersTest.java | 19 ++---- 7 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java new file mode 100644 index 0000000000..1b669133cb --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.common.collect.ImmutableMultimap; + +/** Emits {@link JibEvent}s to event handlers. */ +public class EventEmitter { + + /** Maps from {@link JibEvent} class to handlers for that event type. */ + private final ImmutableMultimap, Handler> handlers; + + /** + * Creates an instance from {@link Handler}s in an {@link EventHandlers}. + * + * @param eventHandlers the {@link EventHandlers} to get the {@link Handler}s from + */ + public EventEmitter(EventHandlers eventHandlers) { + handlers = eventHandlers.getHandlers(); + } + + /** + * Emits {@code jibEvent} to all the handlers that can handle it. + * + * @param jibEvent the {@link JibEvent} to emit + */ + public void emit(JibEvent jibEvent) { + handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); + handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index 99e8e58e5c..06f9eafc27 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -16,46 +16,17 @@ package com.google.cloud.tools.jib.event; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; import java.util.function.Consumer; /** Builds a set of event handlers to handle {@link JibEvent}s. */ public class EventHandlers { - /** Handles an emitted {@link JibEvent}. */ - public static class Handler { - - private final Class eventClass; - private final Consumer eventConsumer; - - private Handler(Class eventClass, Consumer eventConsumer) { - this.eventClass = eventClass; - this.eventConsumer = eventConsumer; - } - - /** - * Handles a {@link JibEvent}. - * - * @param jibEvent the event to handle - * @return {@code true} if this {@link Handler} handled the event; {@code false} if this {@link - * Handler} does not handle the event - */ - public boolean handle(JibEvent jibEvent) { - if (eventClass.isInstance(jibEvent)) { - eventConsumer.accept(eventClass.cast(jibEvent)); - return true; - } - return false; - } - } - - // Maps from JibEvent class to handlers for that event type. - private final Map, List>> handlers = - new HashMap<>(); + /** Maps from {@link JibEvent} class to handlers for that event type. */ + private final Multimap, Handler> handlers = + ArrayListMultimap.create(); /** * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. @@ -72,10 +43,7 @@ public boolean handle(JibEvent jibEvent) { public EventHandlers add( JibEventType eventType, Consumer eventConsumer) { Class eventClass = eventType.getEventClass(); - if (!handlers.containsKey(eventClass)) { - handlers.put(eventClass, new ArrayList<>()); - } - handlers.get(eventClass).add(new Handler<>(eventClass, eventConsumer)); + handlers.put(eventClass, new Handler<>(eventClass, eventConsumer)); return this; } @@ -95,7 +63,7 @@ public EventHandlers add(Consumer eventConsumer) { * * @return the map from {@link JibEvent} type to a list of {@link Handler}s */ - public Map, List>> getHandlers() { - return Collections.unmodifiableMap(handlers); + ImmutableMultimap, Handler> getHandlers() { + return ImmutableMultimap.copyOf(handlers); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java new file mode 100644 index 0000000000..080671f502 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.common.base.Preconditions; +import java.util.function.Consumer; + +/** Handles an emitted {@link JibEvent}. */ +class Handler { + + private final Class eventClass; + private final Consumer eventConsumer; + + Handler(Class eventClass, Consumer eventConsumer) { + this.eventClass = eventClass; + this.eventConsumer = eventConsumer; + } + + /** + * Handles a {@link JibEvent}. + * + * @param jibEvent the event to handle + */ + void handle(JibEvent jibEvent) { + Preconditions.checkArgument(eventClass.isInstance(jibEvent)); + eventConsumer.accept(eventClass.cast(jibEvent)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java index 412525890a..8ad999a203 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java @@ -16,5 +16,8 @@ package com.google.cloud.tools.jib.event; -/** Type for events emitted by Jib Core. */ +/** + * Type for events emitted by Jib Core. Implementation classes should not inherit from each + * other. + */ public interface JibEvent {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java index 7cc2134fd8..7056cc2ca9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java @@ -21,7 +21,7 @@ /** Holds references to all {@link JibEvent} types. */ public class JibEventType { - /** All event types. */ + /** All event types. Handlers for this will always be called first. */ public static final JibEventType ALL = new JibEventType<>(JibEvent.class); // TODO: Add entries for all JibEvent types. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java new file mode 100644 index 0000000000..a4ac7b63bf --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link EventEmitter}. */ +public class EventEmitterTest { + + /** Test {@link JibEvent}. */ + private static class TestJibEvent1 implements JibEvent {} + + /** Test {@link JibEvent}. */ + private static class TestJibEvent2 implements JibEvent {} + + @Test + public void testEmit() { + List emissions = new ArrayList<>(); + + EventHandlers eventHandlers = + new EventHandlers() + .add( + new JibEventType<>(TestJibEvent1.class), + testJibEvent1 -> emissions.add("handled 1 first")) + .add( + new JibEventType<>(TestJibEvent1.class), + testJibEvent1 -> emissions.add("handled 1 second")) + .add( + new JibEventType<>(TestJibEvent2.class), + testJibEvent2 -> emissions.add("handled 2")) + .add(jibEvent -> emissions.add("handled generic")); + + TestJibEvent1 testJibEvent1 = new TestJibEvent1(); + TestJibEvent2 testJibEvent2 = new TestJibEvent2(); + + EventEmitter eventEmitter = new EventEmitter(eventHandlers); + eventEmitter.emit(testJibEvent1); + eventEmitter.emit(testJibEvent2); + + Assert.assertEquals( + Arrays.asList( + "handled generic", + "handled 1 first", + "handled 1 second", + "handled generic", + "handled 2"), + emissions); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java index 726807ef63..c0754c7ed3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -52,9 +52,7 @@ public void testAdd() { new EventHandlers() .add( new JibEventType<>(TestJibEvent1.class), - testJibEvent1 -> { - Assert.assertEquals("payload", testJibEvent1.getPayload()); - }) + testJibEvent1 -> Assert.assertEquals("payload", testJibEvent1.getPayload())) .add( new JibEventType<>(TestJibEvent2.class), testJibEvent2 -> testJibEvent2.sayHello("Jib")) @@ -71,17 +69,10 @@ public void testAdd() { TestJibEvent2 testJibEvent2 = new TestJibEvent2(); // Checks that the handlers handled their respective event types. - Assert.assertTrue( - eventHandlers.getHandlers().get(JibEvent.class).get(0).handle(mockTestJibEvent1)); - Assert.assertTrue(eventHandlers.getHandlers().get(JibEvent.class).get(0).handle(testJibEvent2)); - Assert.assertTrue( - eventHandlers.getHandlers().get(TestJibEvent1.class).get(0).handle(mockTestJibEvent1)); - Assert.assertTrue( - eventHandlers.getHandlers().get(TestJibEvent2.class).get(0).handle(testJibEvent2)); - Assert.assertFalse( - eventHandlers.getHandlers().get(TestJibEvent1.class).get(0).handle(testJibEvent2)); - Assert.assertFalse( - eventHandlers.getHandlers().get(TestJibEvent2.class).get(0).handle(mockTestJibEvent1)); + eventHandlers.getHandlers().get(JibEvent.class).asList().get(0).handle(mockTestJibEvent1); + eventHandlers.getHandlers().get(JibEvent.class).asList().get(0).handle(testJibEvent2); + eventHandlers.getHandlers().get(TestJibEvent1.class).asList().get(0).handle(mockTestJibEvent1); + eventHandlers.getHandlers().get(TestJibEvent2.class).asList().get(0).handle(testJibEvent2); Assert.assertEquals(2, counter[0]); Mockito.verify(mockTestJibEvent1).getPayload(); From 5aad1dd3401242b79d92003633c115506cd090ab Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 10 Sep 2018 17:03:10 -0400 Subject: [PATCH 0194/2020] Preparation for Gradle WAR support (#917) * Preparation for Gradle WAR support * Update TODO about CMD and ENTRYPOINT * Add TODO to remove exploded war --- .../frontend/JavaDockerContextGenerator.java | 10 +++++ .../frontend/JavaEntrypointConstructor.java | 13 +++++++ .../jib/frontend/JavaLayerConfigurations.java | 16 ++++++++ .../JavaDockerContextGeneratorTest.java | 9 +++++ .../JavaEntrypointConstructorTest.java | 6 +++ .../frontend/JavaLayerConfigurationsTest.java | 14 ++++++- jib-core/src/test/resources/exploded-war/file | 0 jib-core/src/test/resources/sampleDockerfile | 1 + .../jib/gradle/GradleProjectProperties.java | 18 +++++++++ .../gradle/GradleProjectPropertiesTest.java | 37 +++++++++++++++++++ .../jib/maven/MavenProjectProperties.java | 5 +++ .../jib/maven/MavenProjectPropertiesTest.java | 5 +++ .../jib/plugins/common/ProjectProperties.java | 2 + 13 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 jib-core/src/test/resources/exploded-war/file diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index d7b30e6b1c..0f30e7f753 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -57,6 +57,9 @@ public class JavaDockerContextGenerator { private static final String SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY = "snapshot-libs"; private static final String RESOURCES_LAYER_DIRECTORY = "resources"; private static final String CLASSES_LAYER_DIRECTORY = "classes"; + // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, + // snapshot dependencies, resources, and classes layers). Should copy files in the right + private static final String EXPLODED_WAR_LAYER_DIRECTORY = "exploded-war"; private static final String EXTRA_FILES_LAYER_DIRECTORY = "root"; private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -160,6 +163,13 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration copyDirectivesBuilder, javaLayerConfigurations.getClassesLayerEntry(), CLASSES_LAYER_DIRECTORY); + // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, + // snapshot dependencies, resources, and classes layers). Should copy files in the right + // directories. (For example, "resources" will go into the webapp root.) + addIfNotEmpty( + copyDirectivesBuilder, + javaLayerConfigurations.getExplodedWarEntry(), + EXPLODED_WAR_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getExtraFilesLayerEntry(), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 71b3569edd..407b4d2e54 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -26,6 +26,7 @@ public class JavaEntrypointConstructor { public static final String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; public static final String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; public static final String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; + public static final String DEFAULT_JETTY_BASE_ON_IMAGE = "/jetty/webapps/ROOT/"; public static List makeDefaultEntrypoint(List jvmFlags, String mainClass) { return makeEntrypoint( @@ -37,6 +38,18 @@ public static List makeDefaultEntrypoint(List jvmFlags, String m mainClass); } + /** + * Constructs the container entrypoint for the gcr.io/distroless/jetty base image. + * + * @return ["java", "-jar", "/jetty/start.jar"] + * @see
+ * https://github.com/GoogleContainerTools/distroless/blob/master/java/jetty/BUILD + */ + // TODO: inherit CMD and ENTRYPOINT from the base image and remove this. + public static List makeDistrolessJettyEntrypoint() { + return Arrays.asList("java", "-jar", "/jetty/start.jar"); + } + /** * Constructs the container entrypoint. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 82d25488fd..cd9ce2a5be 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -39,6 +39,9 @@ enum LayerType { "snapshot dependencies", JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE), RESOURCES("resources", JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE), CLASSES("classes", JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE), + // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, + // snapshot dependencies, resources, and classes layers). Should copy files in the right + EXPLODED_WAR("exploded war", JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE), EXTRA_FILES("extra files", "/"); private final String label; @@ -97,6 +100,13 @@ public Builder setExtraFiles(List extraFiles) { return this; } + // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot + // dependencies, resources, and classes layers). + public Builder setExplodedWar(List explodedWar) { + layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWar); + return this; + } + public JavaLayerConfigurations build() { ImmutableMap.Builder layerConfigurationsMap = ImmutableMap.builderWithExpectedSize(LayerType.values().length); @@ -148,6 +158,12 @@ public LayerEntry getExtraFilesLayerEntry() { return getLayerEntry(LayerType.EXTRA_FILES); } + // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, + // snapshot dependencies, resources, and classes layers). Should copy files in the right + public LayerEntry getExplodedWarEntry() { + return getLayerEntry(LayerType.EXPLODED_WAR); + } + private LayerEntry getLayerEntry(LayerType layerType) { return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)) .getLayerEntries() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 8fee632d99..116425503b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -49,6 +49,7 @@ public class JavaDockerContextGeneratorTest { private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; + private static final String EXPECTED_EXPLODED_WAR_PATH = "/jetty/webapps/ROOT/"; private static void assertSameFiles(Path directory1, Path directory2) throws IOException { Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); @@ -80,6 +81,7 @@ public void testGenerate() throws IOException, URISyntaxException { Paths.get(Resources.getResource("application/snapshot-dependencies").toURI()); Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); + Path testExplodedWarFiles = Paths.get(Resources.getResource("exploded-war").toURI()); Path testExtraFiles = Paths.get(Resources.getResource("layer").toURI()); ImmutableList expectedDependenciesFiles = listFilesInDirectory(testDependencies); @@ -87,6 +89,7 @@ public void testGenerate() throws IOException, URISyntaxException { listFilesInDirectory(testSnapshotDependencies); ImmutableList expectedResourcesFiles = listFilesInDirectory(testResources); ImmutableList expectedClassesFiles = listFilesInDirectory(testClasses); + ImmutableList expectedExplodedWarFiles = listFilesInDirectory(testExplodedWarFiles); ImmutableList expectedExtraFiles = listFilesInDirectory(testExtraFiles); Path targetDirectory = temporaryFolder.newFolder().toPath(); @@ -105,6 +108,8 @@ public void testGenerate() throws IOException, URISyntaxException { .thenReturn(new LayerEntry(expectedResourcesFiles, EXPECTED_RESOURCES_PATH)); Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) .thenReturn(new LayerEntry(expectedClassesFiles, EXPECTED_CLASSES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntry()) + .thenReturn(new LayerEntry(expectedExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH)); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) .thenReturn(new LayerEntry(expectedExtraFiles, "/")); new JavaDockerContextGenerator(mockJavaLayerConfigurations) @@ -116,6 +121,7 @@ public void testGenerate() throws IOException, URISyntaxException { assertSameFiles(targetDirectory.resolve("snapshot-libs"), testSnapshotDependencies); assertSameFiles(targetDirectory.resolve("resources"), testResources); assertSameFiles(targetDirectory.resolve("classes"), testClasses); + assertSameFiles(targetDirectory.resolve("exploded-war"), testExplodedWarFiles); assertSameFiles(targetDirectory.resolve("root"), testExtraFiles); } @@ -147,6 +153,9 @@ public void testMakeDockerfile() throws IOException { new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_RESOURCES_PATH)); Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_CLASSES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntry()) + .thenReturn( + new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_EXPLODED_WAR_PATH)); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), "/")); String dockerfile = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index 99898a5cca..b612666b8f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -52,4 +52,10 @@ public void testMakeEntrypoint() { JavaEntrypointConstructor.makeDefaultEntrypoint(expectedJvmFlags, expectedMainClass), entrypoint); } + + @Test + public void testMakeDistrolessJettyEntrypoint() { + List expected = Arrays.asList("java", "-jar", "/jetty/start.jar"); + Assert.assertEquals(expected, JavaEntrypointConstructor.makeDistrolessJettyEntrypoint()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index a043c4edad..8f986198d6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -23,6 +23,7 @@ public void testDefault() { javaLayerConfigurations.getSnapshotDependenciesLayerEntry(); LayerEntry resourcesLayerEntry = javaLayerConfigurations.getResourcesLayerEntry(); LayerEntry classesLayerEntry = javaLayerConfigurations.getClassesLayerEntry(); + LayerEntry explodedWarEntry = javaLayerConfigurations.getExplodedWarEntry(); LayerEntry extraFilesLayerEntry = javaLayerConfigurations.getExtraFilesLayerEntry(); Assert.assertEquals( @@ -34,6 +35,9 @@ public void testDefault() { Assert.assertEquals( JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE, resourcesLayerEntry.getExtractionPath()); + Assert.assertEquals( + JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE, + explodedWarEntry.getExtractionPath()); Assert.assertEquals( JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE, classesLayerEntry.getExtractionPath()); @@ -42,6 +46,7 @@ public void testDefault() { Assert.assertTrue(snapshotDependenciesLayerEntry.getSourceFiles().isEmpty()); Assert.assertTrue(resourcesLayerEntry.getSourceFiles().isEmpty()); Assert.assertTrue(classesLayerEntry.getSourceFiles().isEmpty()); + Assert.assertTrue(explodedWarEntry.getSourceFiles().isEmpty()); Assert.assertTrue(extraFilesLayerEntry.getSourceFiles().isEmpty()); List expectedLabels = new ArrayList<>(); @@ -62,6 +67,7 @@ public void testSetFiles() { Collections.singletonList(Paths.get("snapshot dependency")); List resourceFiles = Collections.singletonList(Paths.get("resource")); List classFiles = Collections.singletonList(Paths.get("class")); + List explodedWarFiles = Collections.singletonList(Paths.get("exploded war")); List extraFiles = Collections.singletonList(Paths.get("extra file")); JavaLayerConfigurations javaLayerConfigurations = @@ -70,12 +76,18 @@ public void testSetFiles() { .setSnapshotDependenciesFiles(snapshotDependencyFiles) .setResourcesFiles(resourceFiles) .setClassesFiles(classFiles) + .setExplodedWar(explodedWarFiles) .setExtraFiles(extraFiles) .build(); List> expectedFiles = Arrays.asList( - dependencyFiles, snapshotDependencyFiles, resourceFiles, classFiles, extraFiles); + dependencyFiles, + snapshotDependencyFiles, + resourceFiles, + classFiles, + explodedWarFiles, + extraFiles); List> actualFiles = new ArrayList<>(); for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { actualFiles.add(layerConfiguration.getLayerEntries().get(0).getSourceFiles()); diff --git a/jib-core/src/test/resources/exploded-war/file b/jib-core/src/test/resources/exploded-war/file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 8d9c114471..e90bea1963 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -4,6 +4,7 @@ COPY libs /app/libs/ COPY snapshot-libs /app/libs/ COPY resources /app/resources/ COPY classes /app/classes/ +COPY exploded-war /jetty/webapps/ROOT/ COPY root / EXPOSE 1000/tcp diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index b17413444c..d3c88855c8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -34,6 +34,8 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.WarPluginConvention; +import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ @@ -62,6 +64,16 @@ static GradleProjectProperties getForProject( } } + @Nullable + static War getWarTask(Project project) { + WarPluginConvention warPluginConvention = + project.getConvention().findPlugin(WarPluginConvention.class); + if (warPluginConvention == null) { + return null; + } + return (War) warPluginConvention.getProject().getTasks().findByName("war"); + } + private final Project project; private final GradleJibLogger gradleJibLogger; private final JavaLayerConfigurations javaLayerConfigurations; @@ -111,6 +123,12 @@ public String getJarPluginName() { return JAR_PLUGIN_NAME; } + @Override + public boolean isWarProject() { + // TODO: replace with "getWarTask(project) != null" once ready + return false; + } + /** * Tries to resolve the main class. * diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index ae3ac0b4b9..ef46626f4b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -24,6 +24,10 @@ import org.gradle.api.internal.file.FileResolver; import org.gradle.api.java.archives.Manifest; import org.gradle.api.java.archives.internal.DefaultManifest; +import org.gradle.api.plugins.Convention; +import org.gradle.api.plugins.WarPluginConvention; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; import org.junit.Assert; import org.junit.Before; @@ -41,6 +45,9 @@ public class GradleProjectPropertiesTest { @Mock private Jar mockJar; @Mock private Jar mockJar2; @Mock private Project mockProject; + @Mock private Convention mockConvention; + @Mock private WarPluginConvention mockWarPluginConvection; + @Mock private TaskContainer mockTaskContainer; @Mock private GradleJibLogger mockGradleJibLogger; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @@ -50,6 +57,12 @@ public class GradleProjectPropertiesTest { @Before public void setup() { manifest = new DefaultManifest(mockFileResolver); + Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) + .thenReturn(mockWarPluginConvection); + Mockito.when(mockWarPluginConvection.getProject()).thenReturn(mockProject); + Mockito.when(mockProject.getTasks()).thenReturn(mockTaskContainer); + Mockito.when(mockTaskContainer.findByName("war")).thenReturn(Mockito.mock(War.class)); Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = @@ -76,4 +89,28 @@ public void testGetMainClassFromJar_multiple() { .thenReturn(ImmutableSet.of(mockJar, mockJar2)); Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); } + + @Test + public void testIsWarProject() { + Assert.assertFalse(gradleProjectProperties.isWarProject()); + } + + @Test + public void testGetWar_warProject() { + Assert.assertNotNull(GradleProjectProperties.getWarTask(mockProject)); + } + + @Test + public void testGetWar_noWarPlugin() { + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)).thenReturn(null); + + Assert.assertNull(GradleProjectProperties.getWarTask(mockProject)); + } + + @Test + public void testGetWar_noWarTask() { + Mockito.when(mockTaskContainer.findByName("war")).thenReturn(null); + + Assert.assertNull(GradleProjectProperties.getWarTask(mockProject)); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 35c0fb3c7b..2dd3f670d6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -130,6 +130,11 @@ public String getJarPluginName() { return JAR_PLUGIN_NAME; } + @Override + public boolean isWarProject() { + return false; // TODO: to be implemented. For now, assume false. + } + /** * Tries to resolve the main class. * diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 036fb93133..d98647c571 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -110,4 +110,9 @@ public void testGetMainClassFromJar_missingConfiguration() { public void testGetMainClassFromJar_missingPlugin() { Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } + + @Test + public void testIsWarProject() { + Assert.assertFalse(mavenProjectProperties.isWarProject()); + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index b87b00c116..c0aea242da 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -40,4 +40,6 @@ public interface ProjectProperties { /** @return the name of the main class configured in a jar plugin, or null if none is found. */ @Nullable String getMainClassFromJar(); + + boolean isWarProject(); } From 5e2d2411d9adf6fd9c2889c9aa57b1083e60429b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Sep 2018 17:59:17 -0400 Subject: [PATCH 0195/2020] Adds DefaultCacheStorage#listDigests. (#898) --- .../jib/ncache/CacheCorruptedException.java | 25 +++++++ .../cloud/tools/jib/ncache/CacheStorage.java | 5 +- .../tools/jib/ncache/DefaultCacheStorage.java | 18 ++--- .../jib/ncache/DefaultCacheStorageFiles.java | 4 +- .../jib/ncache/DefaultCacheStorageReader.java | 61 ++++++++++++++++ .../jib/ncache/DefaultCacheStorageWriter.java | 3 +- .../ncache/DefaultCacheStorageReaderTest.java | 69 +++++++++++++++++++ 7 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java new file mode 100644 index 0000000000..80cefa62c3 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +/** Thrown if the the cache was found to be corrupted. */ +public class CacheCorruptedException extends Exception { + + CacheCorruptedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 13740e79b0..29d03f795f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -47,10 +47,11 @@ public interface CacheStorage { /** * Lists all the layer digests stored. * - * @return the list of layer digests (that can be retrieved via {@link #retrieve} + * @return the list of layer digests (that can be retrieved via {@link #retrieve}) + * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - List listDigests() throws IOException; + List listDigests() throws IOException, CacheCorruptedException; /** * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java index d5c29d0403..f9328a041a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -55,15 +54,17 @@ public class DefaultCacheStorage implements CacheStorage { * @return a new {@link CacheStorage} */ public static CacheStorage withDirectory(Path cacheDirectory) { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(cacheDirectory); - return new DefaultCacheStorage(new DefaultCacheStorageWriter(defaultCacheStorageFiles)); + return new DefaultCacheStorage(cacheDirectory); } private final DefaultCacheStorageWriter defaultCacheStorageWriter; + private final DefaultCacheStorageReader defaultCacheStorageReader; - private DefaultCacheStorage(DefaultCacheStorageWriter defaultCacheStorageWriter) { - this.defaultCacheStorageWriter = defaultCacheStorageWriter; + private DefaultCacheStorage(Path cacheDirectory) { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(cacheDirectory); + this.defaultCacheStorageWriter = new DefaultCacheStorageWriter(defaultCacheStorageFiles); + this.defaultCacheStorageReader = new DefaultCacheStorageReader(defaultCacheStorageFiles); } @Override @@ -72,9 +73,8 @@ public CacheEntry write(CacheWrite cacheWrite) throws IOException { } @Override - public List listDigests() throws IOException { - // TODO: Implement - return Collections.emptyList(); + public List listDigests() throws IOException, CacheCorruptedException { + return defaultCacheStorageReader.listDigests(); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java index d5baff9073..7978b8e084 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java @@ -85,9 +85,9 @@ Path getSelectorFile(DescriptorDigest selector) { } /** - * Resolves the layers directory. + * Resolves the {@link #LAYERS_DIRECTORY} in the {@link #cacheDirectory}. * - * @return the directory with all the layer directories + * @return the directory containing all the layer directories */ Path getLayersDirectory() { return cacheDirectory.resolve(LAYERS_DIRECTORY); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java new file mode 100644 index 0000000000..8abedc0939 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** Reads from the default cache storage engine. */ +class DefaultCacheStorageReader { + + private final DefaultCacheStorageFiles defaultCacheStorageFiles; + + DefaultCacheStorageReader(DefaultCacheStorageFiles defaultCacheStorageFiles) { + this.defaultCacheStorageFiles = defaultCacheStorageFiles; + } + + /** + * Lists all the layer digests stored. + * + * @return the list of layer digests + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + List listDigests() throws IOException, CacheCorruptedException { + try (Stream layerDirectories = + Files.list(defaultCacheStorageFiles.getLayersDirectory())) { + List layerDirectoriesList = layerDirectories.collect(Collectors.toList()); + List layerDigests = new ArrayList<>(layerDirectoriesList.size()); + for (Path layerDirectory : layerDirectoriesList) { + try { + layerDigests.add(DescriptorDigest.fromHash(layerDirectory.getFileName().toString())); + + } catch (DigestException ex) { + throw new CacheCorruptedException("Found non-digest file in layers directory", ex); + } + } + return layerDigests; + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java index 495c8c0ef2..91de02cd09 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -165,7 +164,7 @@ private WrittenLayer writeLayerBlobToDirectory(Blob layerBlob, Path layerDirecto try (CountingDigestOutputStream compressedDigestOutputStream = new CountingDigestOutputStream( - new BufferedOutputStream(FileOperations.newLockingOutputStream(temporaryLayerFile)))) { + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile)))) { // Writes the layer with GZIP compression. The original bytes are captured as the layer's // diff ID and the bytes outputted from the GZIP compression are captured as the layer's // content descriptor. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java new file mode 100644 index 0000000000..164df5d91b --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.io.IOException; +import java.nio.file.Files; +import java.security.DigestException; +import java.util.Arrays; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link DefaultCacheStorageReader}. */ +public class DefaultCacheStorageReaderTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testListDigests() throws IOException, DigestException, CacheCorruptedException { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + + DefaultCacheStorageReader defaultCacheStorageReader = + new DefaultCacheStorageReader(defaultCacheStorageFiles); + + // Checks that layer directories created are all listed. + DescriptorDigest layerDigest1 = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + DescriptorDigest layerDigest2 = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + Files.createDirectories( + defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest1.getHash())); + Files.createDirectories( + defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest2.getHash())); + + Assert.assertEquals( + Arrays.asList(layerDigest1, layerDigest2), defaultCacheStorageReader.listDigests()); + + // Checks that non-digest directories means the cache is corrupted. + Files.createDirectory(defaultCacheStorageFiles.getLayersDirectory().resolve("not a hash")); + try { + defaultCacheStorageReader.listDigests(); + Assert.fail("Listing digests should have failed"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals("Found non-digest file in layers directory", ex.getMessage()); + Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); + } + } +} From c479fd35f86bf7b4a24e9148bad551cee9ddd7bd Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 11 Sep 2018 11:24:58 -0400 Subject: [PATCH 0196/2020] Add constants for configuration defaults (#970) --- .../cloud/tools/jib/builder/steps/BuildImageStep.java | 4 +++- .../tools/jib/configuration/BuildConfiguration.java | 11 +++++++++-- .../jib/configuration/ContainerConfiguration.java | 5 ++++- .../jib/configuration/BuildConfigurationTest.java | 1 - 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 63e0e9d928..80d5ef141f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -121,7 +121,9 @@ private Image afterCachedLayersSteps() // Add history elements for non-empty layers that don't have one yet Instant layerCreationTime = - containerConfiguration == null ? Instant.EPOCH : containerConfiguration.getCreationTime(); + containerConfiguration == null + ? ContainerConfiguration.DEFAULT_CREATION_TIME + : containerConfiguration.getCreationTime(); for (int count = 0; count < baseImageLayers.size() - nonEmptyLayerCount; count++) { imageBuilder.addHistory( HistoryEntry.builder() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 63036fb988..91cce649ec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -28,6 +28,13 @@ /** Immutable configuration options for the builder process. */ public class BuildConfiguration { + /** The default target format of the container manifest. */ + private static final Class DEFAULT_TARGET_FORMAT = + V22ManifestTemplate.class; + + /** The default tool identifier. */ + private static final String DEFAULT_TOOL_NAME = "jib"; + public static class Builder { // All the parameters below are set to their default values. @@ -38,8 +45,8 @@ public static class Builder { @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; private boolean allowInsecureRegistries = false; private ImmutableList layerConfigurations = ImmutableList.of(); - private Class targetFormat = V22ManifestTemplate.class; - private String toolName = "jib"; + private Class targetFormat = DEFAULT_TARGET_FORMAT; + private String toolName = DEFAULT_TOOL_NAME; private JibLogger buildLogger; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 5193f92208..4e4d3c3207 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -29,10 +29,13 @@ /** Immutable configuration options for the container. */ public class ContainerConfiguration { + /** The default creation time of the container (constant to ensure reproducibility by default). */ + public static final Instant DEFAULT_CREATION_TIME = Instant.EPOCH; + /** Builder for instantiating a {@link ContainerConfiguration}. */ public static class Builder { - private Instant creationTime = Instant.EPOCH; + private Instant creationTime = DEFAULT_CREATION_TIME; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; @Nullable private ImmutableMap environmentMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 5ac72160ce..786fa5acf2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -176,7 +176,6 @@ public void testBuilder_default() { Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals(buildConfiguration.getTargetFormat(), V22ManifestTemplate.class); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); Assert.assertEquals("jib", buildConfiguration.getToolName()); From c24c798057c3ac46040693d457abc0f8d4c949fb Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 11 Sep 2018 16:10:48 -0400 Subject: [PATCH 0197/2020] Adds additional tags to BuildConfiguration. (#963) --- .../com/google/cloud/tools/jib/JibLogger.java | 1 + .../jib/configuration/BuildConfiguration.java | 36 +++++++++++++++++-- .../configuration/BuildConfigurationTest.java | 6 ++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java index 36ad403c86..9783485b18 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib; +/** Logging interface for logging messages during Jib execution. */ public interface JibLogger { void error(CharSequence message); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 91cce649ec..7a5a77abd1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -21,8 +21,10 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; /** Immutable configuration options for the builder process. */ @@ -35,11 +37,13 @@ public class BuildConfiguration { /** The default tool identifier. */ private static final String DEFAULT_TOOL_NAME = "jib"; + /** Builds an immutable {@link BuildConfiguration}. Instantiate with {@link #builder}. */ public static class Builder { // All the parameters below are set to their default values. @Nullable private ImageConfiguration baseImageConfiguration; @Nullable private ImageConfiguration targetImageConfiguration; + private ImmutableSet additionalTargetImageTags = ImmutableSet.of(); @Nullable private ContainerConfiguration containerConfiguration; @Nullable private CacheConfiguration applicationLayersCacheConfiguration; @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; @@ -76,6 +80,18 @@ public Builder setTargetImageConfiguration(ImageConfiguration imageConfiguration return this; } + /** + * Sets the tags to tag the target image with (in addition to the tag in the target image + * configuration image reference set via {@link #setTargetImageConfiguration}). + * + * @param tags a set of tags + * @return this + */ + public Builder setAdditionalTargetImageTags(Set tags) { + additionalTargetImageTags = ImmutableSet.copyOf(tags); + return this; + } + /** * Sets configuration parameters for the container. * @@ -186,6 +202,7 @@ public BuildConfiguration build() { buildLogger, baseImageConfiguration, targetImageConfiguration, + additionalTargetImageTags, containerConfiguration, applicationLayersCacheConfiguration, baseImageLayersCacheConfiguration, @@ -207,13 +224,20 @@ public BuildConfiguration build() { } } - public static Builder builder(JibLogger buildLogger) { - return new Builder(buildLogger); + /** + * Creates a new {@link Builder} to build a {@link BuildConfiguration}. + * + * @param jibLogger the logger to log messages during build + * @return a new {@link Builder} + */ + public static Builder builder(JibLogger jibLogger) { + return new Builder(jibLogger); } private final JibLogger buildLogger; private final ImageConfiguration baseImageConfiguration; private final ImageConfiguration targetImageConfiguration; + private final ImmutableSet additionalTargetImageTags; @Nullable private final ContainerConfiguration containerConfiguration; @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; @@ -222,11 +246,12 @@ public static Builder builder(JibLogger buildLogger) { private final ImmutableList layerConfigurations; private final String toolName; - /** Instantiate with {@link Builder#build}. */ + /** Instantiate with {@link #builder}. */ private BuildConfiguration( JibLogger buildLogger, ImageConfiguration baseImageConfiguration, ImageConfiguration targetImageConfiguration, + ImmutableSet additionalTargetImageTags, @Nullable ContainerConfiguration containerConfiguration, @Nullable CacheConfiguration applicationLayersCacheConfiguration, @Nullable CacheConfiguration baseImageLayersCacheConfiguration, @@ -237,6 +262,7 @@ private BuildConfiguration( this.buildLogger = buildLogger; this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; + this.additionalTargetImageTags = additionalTargetImageTags; this.containerConfiguration = containerConfiguration; this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; @@ -258,6 +284,10 @@ public ImageConfiguration getTargetImageConfiguration() { return targetImageConfiguration; } + public ImmutableSet getAdditionalTargetImageTags() { + return additionalTargetImageTags; + } + @Nullable public ContainerConfiguration getContainerConfiguration() { return containerConfiguration; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 786fa5acf2..1082bbf15e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -26,12 +26,14 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -47,6 +49,7 @@ public void testBuilder() throws Exception { String expectedTargetServerUrl = "someotherserver"; String expectedTargetImageName = "targetimage"; String expectedTargetTag = "targettag"; + Set expectedTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); List credentialRetrievers = Collections.singletonList(() -> new Credential("username", "password")); Instant expectedCreationTime = Instant.ofEpochSecond(10000); @@ -90,6 +93,7 @@ public void testBuilder() throws Exception { BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) + .setAdditionalTargetImageTags(expectedTargetImageTags) .setContainerConfiguration(containerConfiguration) .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) @@ -117,6 +121,7 @@ public void testBuilder() throws Exception { buildConfiguration.getTargetImageConfiguration().getImageRepository()); Assert.assertEquals( expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); + Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAdditionalTargetImageTags()); Assert.assertEquals( new Credential("username", "password"), buildConfiguration @@ -172,6 +177,7 @@ public void testBuilder_default() { .setTargetImageConfiguration(targetImageConfiguration) .build(); + Assert.assertTrue(buildConfiguration.getAdditionalTargetImageTags().isEmpty()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); From 484b0ff8353c652135781f7ffb00c41cbc02153b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Sep 2018 11:33:42 -0400 Subject: [PATCH 0198/2020] Pushes to the additional tags. (#968) * Adds additional tags to BuildConfiguration. * Pushes to the additional tags. --- .../builder/BuildStepsIntegrationTest.java | 72 +++++++++++++++---- .../jib/builder/steps/PushImageStep.java | 10 ++- .../jib/configuration/BuildConfiguration.java | 8 ++- .../configuration/BuildConfigurationTest.java | 9 +-- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 838213fc22..5d576768b2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -32,6 +32,7 @@ import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -119,9 +120,10 @@ public void testSteps_forBuildToDockerRegistry() CacheDirectoryNotOwnedException, CacheDirectoryCreationException { BuildSteps buildImageSteps = getBuildSteps( - getBuildConfiguration( - ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of("localhost:5000", "testimage", "testtag"))); + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("localhost:5000", "testimage", "testtag")) + .build()); long lastTime = System.nanoTime(); buildImageSteps.run(); @@ -137,15 +139,54 @@ public void testSteps_forBuildToDockerRegistry() "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } + @Test + public void testSteps_forBuildToDockerRegistry_multipleTags() + throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, + CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + BuildSteps buildImageSteps = + getBuildSteps( + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("localhost:5000", "testimage", "testtag")) + .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) + .build()); + + long lastTime = System.nanoTime(); + buildImageSteps.run(); + logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + lastTime = System.nanoTime(); + buildImageSteps.run(); + logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + + String imageReference = "localhost:5000/testimage:testtag"; + localRegistry.pull(imageReference); + assertDockerInspect(imageReference); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + + String imageReference2 = "localhost:5000/testimage:testtag2"; + localRegistry.pull(imageReference2); + assertDockerInspect(imageReference2); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", imageReference2).run()); + + String imageReference3 = "localhost:5000/testimage:testtag3"; + localRegistry.pull(imageReference3); + assertDockerInspect(imageReference3); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", imageReference3).run()); + } + @Test public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, CacheDirectoryCreationException, CacheMetadataCorruptedException, CacheDirectoryNotOwnedException { getBuildSteps( - getBuildConfiguration( - ImageReference.parse("openjdk:8-jre-alpine"), - ImageReference.of("localhost:5000", "testimage", "testtag"))) + getBuildConfigurationBuilder( + ImageReference.parse("openjdk:8-jre-alpine"), + ImageReference.of("localhost:5000", "testimage", "testtag")) + .build()) .run(); String imageReference = "localhost:5000/testimage:testtag"; @@ -160,9 +201,10 @@ public void testSteps_forBuildToDockerDaemon() CacheDirectoryNotOwnedException, CacheDirectoryCreationException { String imageReference = "testdocker"; BuildConfiguration buildConfiguration = - getBuildConfiguration( - ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of(null, imageReference, null)); + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of(null, imageReference, null)) + .build(); Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToDockerDaemon( buildConfiguration, @@ -179,9 +221,10 @@ public void testSteps_forBuildToTarball() throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { BuildConfiguration buildConfiguration = - getBuildConfiguration( - ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of(null, "testtar", null)); + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of(null, "testtar", null)) + .build(); Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); Path cacheDirectory = temporaryFolder.newFolder().toPath(); BuildSteps.forBuildToTar( @@ -201,7 +244,7 @@ private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws I buildConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)); } - private BuildConfiguration getBuildConfiguration( + private BuildConfiguration.Builder getBuildConfigurationBuilder( ImageReference baseImage, ImageReference targetImage) { ImageConfiguration baseImageConfiguration = ImageConfiguration.builder(baseImage).build(); ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); @@ -222,7 +265,6 @@ private BuildConfiguration getBuildConfiguration( .setContainerConfiguration(containerConfiguration) .setAllowInsecureRegistries(true) .setLayerConfigurations(fakeLayerConfigurations) - .setToolName("jib-integration-test") - .build(); + .setToolName("jib-integration-test"); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 262f2807e6..d1a4bc1276 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -126,14 +126,18 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep))); - // Pushes the image manifest. + // Gets the image manifest to push. BuildableManifestTemplate manifestTemplate = imageToJsonTranslator.getManifestTemplate( buildConfiguration.getTargetFormat(), NonBlockingSteps.get( NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)))); - registryClient.pushManifest( - manifestTemplate, buildConfiguration.getTargetImageConfiguration().getImageTag()); + + // Pushes to all target image tags. + // TODO: Parallelize. + for (String tag : buildConfiguration.getAllTargetImageTags()) { + registryClient.pushManifest(manifestTemplate, tag); + } } return null; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 7a5a77abd1..259112f8b3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -284,8 +284,12 @@ public ImageConfiguration getTargetImageConfiguration() { return targetImageConfiguration; } - public ImmutableSet getAdditionalTargetImageTags() { - return additionalTargetImageTags; + public ImmutableSet getAllTargetImageTags() { + ImmutableSet.Builder allTargetImageTags = + ImmutableSet.builderWithExpectedSize(1 + additionalTargetImageTags.size()); + allTargetImageTags.add(targetImageConfiguration.getImageTag()); + allTargetImageTags.addAll(additionalTargetImageTags); + return allTargetImageTags.build(); } @Nullable diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 1082bbf15e..5ba6855509 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -49,7 +49,8 @@ public void testBuilder() throws Exception { String expectedTargetServerUrl = "someotherserver"; String expectedTargetImageName = "targetimage"; String expectedTargetTag = "targettag"; - Set expectedTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); + Set additionalTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); + Set expectedTargetImageTags = ImmutableSet.of("targettag", "tag1", "tag2", "tag3"); List credentialRetrievers = Collections.singletonList(() -> new Credential("username", "password")); Instant expectedCreationTime = Instant.ofEpochSecond(10000); @@ -93,7 +94,7 @@ public void testBuilder() throws Exception { BuildConfiguration.builder(Mockito.mock(JibLogger.class)) .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) - .setAdditionalTargetImageTags(expectedTargetImageTags) + .setAdditionalTargetImageTags(additionalTargetImageTags) .setContainerConfiguration(containerConfiguration) .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) @@ -121,7 +122,7 @@ public void testBuilder() throws Exception { buildConfiguration.getTargetImageConfiguration().getImageRepository()); Assert.assertEquals( expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); - Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAdditionalTargetImageTags()); + Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAllTargetImageTags()); Assert.assertEquals( new Credential("username", "password"), buildConfiguration @@ -177,7 +178,7 @@ public void testBuilder_default() { .setTargetImageConfiguration(targetImageConfiguration) .build(); - Assert.assertTrue(buildConfiguration.getAdditionalTargetImageTags().isEmpty()); + Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); From 4536dbb28b689bdd1e582c13ddb18254114728c2 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Sep 2018 12:11:37 -0400 Subject: [PATCH 0199/2020] Changes LayerEntry to be more like TarArchiveEntry. (#960) --- .../builder/BuildStepsIntegrationTest.java | 28 +-- .../BuildAndCacheApplicationLayerStep.java | 23 +-- .../cloud/tools/jib/cache/CacheFiles.java | 2 +- .../cloud/tools/jib/cache/CacheMetadata.java | 50 ++--- .../jib/cache/CacheMetadataTranslator.java | 33 ++-- .../cloud/tools/jib/cache/CacheReader.java | 28 +-- .../cloud/tools/jib/cache/LayerMetadata.java | 37 ++-- ...MetadataLayerPropertiesObjectTemplate.java | 17 +- .../jib/cache/json/CacheMetadataTemplate.java | 3 +- .../jib/configuration/LayerConfiguration.java | 66 +++++-- .../frontend/JavaDockerContextGenerator.java | 75 +++++--- .../jib/frontend/JavaLayerConfigurations.java | 76 ++++---- .../tools/jib/frontend/MainClassFinder.java | 182 +++++++----------- .../cloud/tools/jib/image/LayerEntry.java | 109 +++++++++-- .../jib/image/ReproducibleLayerBuilder.java | 70 +------ ...BuildAndCacheApplicationLayerStepTest.java | 80 +++++--- .../cloud/tools/jib/cache/CacheFilesTest.java | 2 +- .../tools/jib/cache/CacheMetadataTest.java | 75 ++++---- .../cache/CacheMetadataTranslatorTest.java | 21 +- .../tools/jib/cache/CacheReaderTest.java | 89 ++++++--- .../cloud/tools/jib/cache/CacheTest.java | 39 ++-- .../tools/jib/cache/CacheWriterTest.java | 21 +- .../cache/PlatformSpecificMetadataJson.java | 4 +- .../cache/json/CacheMetadataTemplateTest.java | 12 +- .../configuration/BuildConfigurationTest.java | 4 +- .../configuration/LayerConfigurationTest.java | 57 ++++++ .../JavaDockerContextGeneratorTest.java | 109 ++++++----- .../frontend/JavaLayerConfigurationsTest.java | 82 ++++---- .../jib/frontend/MainClassFinderTest.java | 65 +++---- .../image/ReproducibleLayerBuilderTest.java | 56 ++---- .../src/test/resources/cache/metadata-v3.json | 34 ++++ .../src/test/resources/json/metadata-v3.json | 1 + .../resources/json/metadata_windows-v3.json | 1 + .../jib/gradle/GradleLayerConfigurations.java | 8 +- .../gradle/GradleLayerConfigurationsTest.java | 58 +++--- .../jib/maven/MavenLayerConfigurations.java | 8 +- .../maven/MavenLayerConfigurationsTest.java | 49 +++-- .../jib/plugins/common/BuildStepsRunner.java | 6 +- .../jib/plugins/common/MainClassResolver.java | 23 +-- .../plugins/common/BuildStepsRunnerTest.java | 12 +- .../plugins/common/MainClassResolverTest.java | 57 +++--- 41 files changed, 982 insertions(+), 790 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java create mode 100644 jib-core/src/test/resources/cache/metadata-v3.json create mode 100644 jib-core/src/test/resources/json/metadata-v3.json create mode 100644 jib-core/src/test/resources/json/metadata_windows-v3.json diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 5d576768b2..0fbdc4114c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -56,12 +56,20 @@ public class BuildStepsIntegrationTest { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); - /** Lists the files in the {@code resourcePath} resources directory. */ - private static ImmutableList getResourceFilesList(String resourcePath) - throws URISyntaxException, IOException { + /** + * Lists the files in the {@code resourcePath} resources directory and builds a {@link + * LayerConfiguration} from those files. + */ + private static LayerConfiguration makeLayerConfiguration( + String resourcePath, Path pathInContainer) throws URISyntaxException, IOException { try (Stream fileStream = Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { - return fileStream.collect(ImmutableList.toImmutableList()); + LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder(); + fileStream.forEach( + sourceFile -> + layerConfigurationBuilder.addEntry( + sourceFile, pathInContainer.resolve(sourceFile.getFileName()))); + return layerConfigurationBuilder.build(); } } @@ -103,15 +111,9 @@ private static void assertDockerInspect(String imageReference) public void setUp() throws IOException, URISyntaxException { fakeLayerConfigurations = ImmutableList.of( - LayerConfiguration.builder() - .addEntry(getResourceFilesList("application/dependencies"), "/app/libs/") - .build(), - LayerConfiguration.builder() - .addEntry(getResourceFilesList("application/resources"), "/app/resources/") - .build(), - LayerConfiguration.builder() - .addEntry(getResourceFilesList("application/classes"), "/app/classes/") - .build()); + makeLayerConfiguration("application/dependencies", Paths.get("/app/libs/")), + makeLayerConfiguration("application/resources", Paths.get("/app/resources/")), + makeLayerConfiguration("application/classes", Paths.get("/app/classes/"))); } @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index a3653c2ea7..c7a9570be6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -25,12 +25,12 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; +import java.util.Optional; import java.util.concurrent.Callable; /** Builds and caches application layers. */ @@ -52,10 +52,7 @@ static ImmutableList makeList( ImmutableList.builderWithExpectedSize(buildConfiguration.getLayerConfigurations().size()); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { // Skips the layer if empty. - if (layerConfiguration - .getLayerEntries() - .stream() - .allMatch(layerEntry -> layerEntry.getSourceFiles().isEmpty())) { + if (layerConfiguration.getLayerEntries().isEmpty()) { continue; } @@ -105,20 +102,16 @@ public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorrupted try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { // Don't build the layer if it exists already. - CachedLayerWithMetadata cachedLayer = + Optional optionalCachedLayer = new CacheReader(cache) .getUpToDateLayerByLayerEntries(layerConfiguration.getLayerEntries()); - if (cachedLayer != null) { - return cachedLayer; - } - - ReproducibleLayerBuilder reproducibleLayerBuilder = new ReproducibleLayerBuilder(); - for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - reproducibleLayerBuilder.addFiles( - layerEntry.getSourceFiles(), layerEntry.getExtractionPath()); + if (optionalCachedLayer.isPresent()) { + return optionalCachedLayer.get(); } - cachedLayer = new CacheWriter(cache).writeLayer(reproducibleLayerBuilder); + CachedLayerWithMetadata cachedLayer = + new CacheWriter(cache) + .writeLayer(new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries())); buildConfiguration .getBuildLogger() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java index 10ab5a8f47..49b3e86530 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java @@ -23,7 +23,7 @@ class CacheFiles { /** Increment the version prefix when the metadata format changes. */ - static final String METADATA_FILENAME = "metadata-v2.json"; + static final String METADATA_FILENAME = "metadata-v3.json"; private static final String LAYER_FILE_EXTENSION = ".tar.gz"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java index 08548da129..65f6b37c0d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java @@ -21,11 +21,7 @@ import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; -import java.nio.file.Paths; -import java.util.List; -import java.util.function.BiPredicate; -import java.util.function.Predicate; +import java.util.Iterator; import javax.annotation.Nullable; /** @@ -34,6 +30,7 @@ */ class CacheMetadata { + /** Builds a {@link CacheMetadata}. */ static class Builder { private final ImageLayers.Builder layersBuilder = @@ -64,7 +61,7 @@ CacheMetadata build() { static class LayerFilter { /** - * Checks if the layer entries matches the metadata layer entries + * Checks if the layer entries matches the metadata layer entries. * * @param layerEntries the layer entries to check * @param metadataEntries the metadata entries to match against @@ -78,31 +75,24 @@ static boolean doLayerEntriesMatchMetadataEntries( if (layerEntries.size() != metadataEntries.size()) { return false; } - return pairwiseCompareAllPass( - layerEntries, - metadataEntries, - (layerEntry, metadataEntry) -> { - // Checks extraction path not equal. - if (!layerEntry.getExtractionPath().equals(metadataEntry.getExtractionPath())) { - return false; - } - - // Checks for any source file not equal. - if (layerEntry.getSourceFiles().size() - != metadataEntry.getSourceFilesStrings().size()) { - return false; - } - return pairwiseCompareAllPass( - layerEntry.getSourceFiles(), - metadataEntry.getSourceFilesStrings(), - (sourceFile, sourceFileString) -> sourceFile.equals(Paths.get(sourceFileString))); - }); - } - private static boolean pairwiseCompareAllPass( - List listA, List listB, BiPredicate compare) { - return Streams.zip(listA.stream(), listB.stream(), compare::test) - .allMatch(Predicate.isEqual(true)); + // Pairwise-compares all the layer entries with the metadata layer entries. + Iterator layerEntriesIterator = layerEntries.iterator(); + Iterator metadataEntriesIterator = + metadataEntries.iterator(); + while (layerEntriesIterator.hasNext() && metadataEntriesIterator.hasNext()) { + LayerEntry layerEntry = layerEntriesIterator.next(); + LayerMetadata.LayerMetadataEntry metadataEntry = metadataEntriesIterator.next(); + + boolean areSourceFilesEqual = + layerEntry.getSourceFileString().equals(metadataEntry.getSourceFileString()); + boolean areExtractionPathsEqual = + layerEntry.getExtractionPathString().equals(metadataEntry.getExtractionPathString()); + if (!areSourceFilesEqual || !areExtractionPathsEqual) { + return false; + } + } + return true; } private final ImageLayers layers; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java index 9064c3bac6..6a20813617 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java @@ -25,8 +25,8 @@ import com.google.common.collect.ImmutableList; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** Translates {@link CacheMetadata} to and from {@link CacheMetadataTemplate}. */ public class CacheMetadataTranslator { @@ -58,19 +58,15 @@ static CacheMetadata fromTemplate(CacheMetadataTemplate template, Path cacheDire ImmutableList.builderWithExpectedSize( propertiesObjectTemplate.getLayerEntries().size()); for (LayerEntryTemplate layerEntryTemplate : propertiesObjectTemplate.getLayerEntries()) { - if (layerEntryTemplate.getSourceFiles() == null - || layerEntryTemplate.getExtractionPath() == null) { + if (layerEntryTemplate.getSourceFileString() == null + || layerEntryTemplate.getExtractionPathString() == null) { throw new CacheMetadataCorruptedException( "Cannot translate cache metadata layer entry without source files or extraction path"); } layerEntries.add( new LayerEntry( - layerEntryTemplate - .getSourceFiles() - .stream() - .map(Paths::get) - .collect(ImmutableList.toImmutableList()), - layerEntryTemplate.getExtractionPath())); + Paths.get(layerEntryTemplate.getSourceFileString()), + Paths.get(layerEntryTemplate.getExtractionPathString()))); } layerMetadata = @@ -105,17 +101,14 @@ static CacheMetadataTemplate toTemplate(CacheMetadata cacheMetadata) { if (cachedLayerWithMetadata.getMetadata() != null) { // Constructs the layer entry templates to add to the layer object template. - List layerEntryTemplates = - cachedLayerWithMetadata - .getMetadata() - .getEntries() - .stream() - .map( - layerMetadataEntry -> - new LayerEntryTemplate( - layerMetadataEntry.getSourceFilesStrings(), - layerMetadataEntry.getExtractionPath())) - .collect(Collectors.toList()); + ImmutableList metadataEntries = + cachedLayerWithMetadata.getMetadata().getEntries(); + List layerEntryTemplates = new ArrayList<>(metadataEntries.size()); + for (LayerMetadata.LayerMetadataEntry metadataEntry : metadataEntries) { + layerEntryTemplates.add( + new LayerEntryTemplate( + metadataEntry.getSourceFileString(), metadataEntry.getExtractionPathString())); + } layerObjectTemplate.setProperties( new CacheMetadataLayerPropertiesObjectTemplate() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java index 572d0040ba..76bf7a5044 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java @@ -27,16 +27,19 @@ import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** Reads image content from the cache. */ public class CacheReader { /** - * @param path the file to check. - * @return the last modified time for the file at {@code path}. Recursively finds the most recent - * last modified time for all subfiles if the file is a directory. - * @throws IOException if checking the last modified time fails. + * Gets the last modified time for the file at {@code path}. If {@code path} is a directory, then + * gets the latest modified time of its subfiles. + * + * @param path the file to check + * @return the last modified time + * @throws IOException if checking the last modified time fails */ private static FileTime getLastModifiedTime(Path path) throws IOException { if (Files.isDirectory(path)) { @@ -118,24 +121,21 @@ public Path getLayerFile(ImmutableList layerEntries) * @throws IOException if reading the source files fails. * @throws CacheMetadataCorruptedException if reading the cache metadata fails. */ - @Nullable - public CachedLayerWithMetadata getUpToDateLayerByLayerEntries( + public Optional getUpToDateLayerByLayerEntries( ImmutableList layerEntries) throws IOException, CacheMetadataCorruptedException { // Grabs all the layers that have matching source files. ImageLayers cachedLayersWithSourceFiles = cache.getMetadata().filterLayers().byLayerEntries(layerEntries).filter(); if (cachedLayersWithSourceFiles.isEmpty()) { - return null; + return Optional.empty(); } // Determines the latest modification time for the source files. FileTime sourceFilesLastModifiedTime = FileTime.from(Instant.MIN); for (LayerEntry layerEntry : layerEntries) { - for (Path path : layerEntry.getSourceFiles()) { - FileTime lastModifiedTime = getLastModifiedTime(path); - if (lastModifiedTime.compareTo(sourceFilesLastModifiedTime) > 0) { - sourceFilesLastModifiedTime = lastModifiedTime; - } + FileTime lastModifiedTime = getLastModifiedTime(layerEntry.getSourceFile()); + if (lastModifiedTime.compareTo(sourceFilesLastModifiedTime) > 0) { + sourceFilesLastModifiedTime = lastModifiedTime; } } @@ -148,10 +148,10 @@ public CachedLayerWithMetadata getUpToDateLayerByLayerEntries( if (sourceFilesLastModifiedTime.compareTo(cachedLayer.getMetadata().getLastModifiedTime()) <= 0) { // This layer is an up-to-date layer. - return cachedLayer; + return Optional.of(cachedLayer); } } - return null; + return Optional.empty(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java index e16c467d61..e4f7eb53f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java @@ -19,9 +19,7 @@ import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.util.List; /** * Metadata about an application layer stored in the cache. This is part of the {@link @@ -32,20 +30,24 @@ class LayerMetadata { /** Entry into the layer metadata. */ static class LayerMetadataEntry { - private ImmutableList sourceFilesStrings; - private String extractionPath; + /** The source file path string, in OS-specific format. */ + private final String sourceFileString; - ImmutableList getSourceFilesStrings() { - return sourceFilesStrings; + /** The extraction path string, in OS-specific format. */ + private final String extractionPathString; + + String getSourceFileString() { + return sourceFileString; } - String getExtractionPath() { - return extractionPath; + String getExtractionPathString() { + return extractionPathString; } - private LayerMetadataEntry(ImmutableList sourceFilesStrings, String extractionPath) { - this.sourceFilesStrings = sourceFilesStrings; - this.extractionPath = extractionPath; + @VisibleForTesting + LayerMetadataEntry(String sourceFileString, String extractionPathString) { + this.sourceFileString = sourceFileString; + this.extractionPathString = extractionPathString; } } @@ -54,14 +56,9 @@ static LayerMetadata from(ImmutableList layerEntries, FileTime lastM ImmutableList.builderWithExpectedSize(layerEntries.size()); for (LayerEntry layerEntry : layerEntries) { - List sourceFiles = layerEntry.getSourceFiles(); - ImmutableList.Builder sourceFilesStrings = - ImmutableList.builderWithExpectedSize(sourceFiles.size()); - for (Path sourceFile : sourceFiles) { - sourceFilesStrings.add(sourceFile.toString()); - } entries.add( - new LayerMetadataEntry(sourceFilesStrings.build(), layerEntry.getExtractionPath())); + new LayerMetadataEntry( + layerEntry.getSourceFileString(), layerEntry.getExtractionPathString())); } return new LayerMetadata(entries.build(), lastModifiedTime); @@ -87,7 +84,7 @@ FileTime getLastModifiedTime() { } @VisibleForTesting - void setEntry(ImmutableList sourceFilesStrings, String extractionPath) { - entries = ImmutableList.of(new LayerMetadataEntry(sourceFilesStrings, extractionPath)); + void setEntries(ImmutableList layerMetadataEntries) { + entries = layerMetadataEntries; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java index beecb4d5fa..0e4f0d096e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java @@ -31,27 +31,28 @@ public class CacheMetadataLayerPropertiesObjectTemplate implements JsonTemplate /** Represents a pair of source files and extraction path. */ public static class LayerEntryTemplate implements JsonTemplate { - /** The paths to the source files that the layer was constructed from. */ - @Nullable private List sourceFiles; + /** The path to the source file for this layer entry. */ + @Nullable private String sourceFile; - /** The intended path to extract the source files to in the container. */ + /** The intended path to extract the source file to in the container. */ @Nullable private String extractionPath; @Nullable - public List getSourceFiles() { - return sourceFiles; + public String getSourceFileString() { + return sourceFile; } @Nullable - public String getExtractionPath() { + public String getExtractionPathString() { return extractionPath; } - public LayerEntryTemplate(List sourceFiles, String extractionPath) { - this.sourceFiles = sourceFiles; + public LayerEntryTemplate(String sourceFile, String extractionPath) { + this.sourceFile = sourceFile; this.extractionPath = extractionPath; } + /** For Jackson JSON templating. */ public LayerEntryTemplate() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java index 48f8f7580f..4cee1f87e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java @@ -47,9 +47,10 @@ * "properties": { * "layerEntries": [ * { - * "sourceFiles": ["build/classes"], + * "sourceFile": "build/classes", * "extractionPath": "/app/classes" * }, + * ... * ], * "lastModifiedTime": 255073580723571 * } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index c6555e7fa7..54a7c52ace 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -17,12 +17,14 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; -/** Configures how to build a layer in the container image. */ +/** Configures how to build a layer in the container image. Instantiate with {@link #builder}. */ public class LayerConfiguration { /** Builds a {@link LayerConfiguration}. */ @@ -45,20 +47,51 @@ public Builder setLabel(String label) { } /** - * Adds an entry to the layer. + * Adds an entry to the layer. Only adds the single source file to the exact path in the + * container file system. * - *

The source files are specified as a list instead of a set to define the order in which - * they are added. + *

For example, {@code addEntry(Paths.get("myfile"), Paths.get("/path/in/container"))} adds a + * file {@code myfile} to the container file system at {@code /path/in/container}. * - * @param sourceFiles the source files to build from. Source files that are directories will - * have all subfiles in the directory added (but not the directory itself) - * @param destinationOnImage Unix-style path to add the source files to in the container image - * filesystem + *

For example, {@code addEntry(Paths.get("mydirectory"), Paths.get("/path/in/container"))} + * adds a directory {@code mydirectory/} to the container file system at {@code + * /path/in/container/}. This does not add the contents of {@code mydirectory}. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} (relative to root {@code /}) + * @return this + */ + public Builder addEntry(Path sourceFile, Path pathInContainer) { + layerEntries.add(new LayerEntry(sourceFile, pathInContainer)); + return this; + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + *

For example, {@code addEntryRecursive(Paths.get("mydirectory", + * Paths.get("/path/in/container"))} adds {@code mydirectory} to the container file system at + * {@code /path/in/container} such that {@code mydirectory/subfile} is found at {@code + * /path/in/container/subfile}. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} (relative to root {@code /}) * @return this + * @throws IOException if an exception occurred when recursively listing the directory */ - public Builder addEntry(List sourceFiles, String destinationOnImage) { - Preconditions.checkArgument(!sourceFiles.contains(null)); - this.layerEntries.add(new LayerEntry(ImmutableList.copyOf(sourceFiles), destinationOnImage)); + public Builder addEntryRecursive(Path sourceFile, Path pathInContainer) throws IOException { + if (!Files.isDirectory(sourceFile)) { + return addEntry(sourceFile, pathInContainer); + } + addEntry(sourceFile, pathInContainer); + try (Stream files = Files.list(sourceFile)) { + for (Path file : files.collect(Collectors.toList())) { + addEntryRecursive(file, pathInContainer.resolve(file.getFileName())); + } + } return this; } @@ -72,6 +105,11 @@ public LayerConfiguration build() { } } + /** + * Builds a {@link LayerConfiguration}. + * + * @return a new {@link Builder} + */ public static Builder builder() { return new Builder(); } @@ -80,7 +118,7 @@ public static Builder builder() { private final String label; /** - * Constructs a new layer configuration. + * Use {@link #builder} to instantiate. * * @param label an optional label for the layer * @param layerEntries the list of {@link LayerEntry}s diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 0f30e7f753..30ef1f49f2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -29,6 +28,7 @@ import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -67,8 +67,8 @@ public class JavaDockerContextGenerator { /** Represents a Dockerfile {@code COPY} directive. */ private static class CopyDirective { - /** The source files to put into the context. */ - private final ImmutableList sourceFiles; + /** The layer entries to put into the context. */ + private final ImmutableList layerEntries; /** The directory in the context to put the source files for the layer */ private final String directoryInContext; @@ -77,31 +77,31 @@ private static class CopyDirective { private final String extractionPath; private CopyDirective( - ImmutableList sourceFiles, String directoryInContext, String extractionPath) { - this.sourceFiles = sourceFiles; + ImmutableList layerEntries, String directoryInContext, String extractionPath) { + this.layerEntries = layerEntries; this.directoryInContext = directoryInContext; this.extractionPath = extractionPath; } } /** - * Adds a copy directive for the {@code layerEntry} if it's not empty. + * Adds a copy directive for the {@code layerEntries} if it's not empty. * * @param listBuilder the {@link ImmutableList.Builder} to add to - * @param layerEntry the layer entry + * @param layerEntries the layer entries * @param directoryInContext the directory in the context to put the source files for the layer + * @param extractionPath the extraction path to extract the directory to */ private static void addIfNotEmpty( ImmutableList.Builder listBuilder, - LayerEntry layerEntry, - String directoryInContext) { - if (layerEntry.getSourceFiles().isEmpty()) { + ImmutableList layerEntries, + String directoryInContext, + String extractionPath) { + if (layerEntries.isEmpty()) { return; } - listBuilder.add( - new CopyDirective( - layerEntry.getSourceFiles(), directoryInContext, layerEntry.getExtractionPath())); + listBuilder.add(new CopyDirective(layerEntries, directoryInContext, extractionPath)); } /** @@ -149,31 +149,37 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration ImmutableList.Builder copyDirectivesBuilder = ImmutableList.builder(); addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getDependenciesLayerEntry(), - DEPENDENCIES_LAYER_DIRECTORY); + javaLayerConfigurations.getDependencyLayerEntries(), + DEPENDENCIES_LAYER_DIRECTORY, + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE); addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getSnapshotDependenciesLayerEntry(), - SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY); + javaLayerConfigurations.getSnapshotDependencyLayerEntries(), + SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY, + JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE); addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getResourcesLayerEntry(), - RESOURCES_LAYER_DIRECTORY); + javaLayerConfigurations.getResourceLayerEntries(), + RESOURCES_LAYER_DIRECTORY, + JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE); addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getClassesLayerEntry(), - CLASSES_LAYER_DIRECTORY); + javaLayerConfigurations.getClassLayerEntries(), + CLASSES_LAYER_DIRECTORY, + JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE); // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, // snapshot dependencies, resources, and classes layers). Should copy files in the right // directories. (For example, "resources" will go into the webapp root.) addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getExplodedWarEntry(), - EXPLODED_WAR_LAYER_DIRECTORY); + javaLayerConfigurations.getExplodedWarEntries(), + EXPLODED_WAR_LAYER_DIRECTORY, + JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE); addIfNotEmpty( copyDirectivesBuilder, - javaLayerConfigurations.getExtraFilesLayerEntry(), - EXTRA_FILES_LAYER_DIRECTORY); + javaLayerConfigurations.getExtraFilesLayerEntries(), + EXTRA_FILES_LAYER_DIRECTORY, + "/"); copyDirectives = copyDirectivesBuilder.build(); } @@ -269,8 +275,23 @@ public void generate(Path targetDirectory) throws IOException { Path directoryInContext = targetDirectory.resolve(copyDirective.directoryInContext); Files.createDirectory(directoryInContext); - // Copies dependencies. - FileOperations.copy(copyDirective.sourceFiles, directoryInContext); + // Copies the source files to the directoryInContext. + for (LayerEntry layerEntry : copyDirective.layerEntries) { + // This resolves the path to copy the source file to in the {@code directory}. + // For example, for a 'baseDirectory' of 'target/jib-docker-context/classes', a + // 'baseExtractionPath' of '/app/classes', and an 'actualExtractionPath' of + // '/app/classes/com/test/HelloWorld.class', the resolved destination would be + // 'target/jib-docker-context/classes/com/test/HelloWorld.class'. + Path destination = + directoryInContext.resolve( + Paths.get(copyDirective.extractionPath).relativize(layerEntry.getExtractionPath())); + + if (Files.isDirectory(layerEntry.getSourceFile())) { + Files.createDirectories(destination); + } else { + Files.copy(layerEntry.getSourceFile(), destination); + } + } } // Creates the Dockerfile. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index cd9ce2a5be..bc80febe0a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -22,7 +22,9 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -45,12 +47,12 @@ enum LayerType { EXTRA_FILES("extra files", "/"); private final String label; - private final String extractionPath; + private final Path extractionPath; /** Initializes with a label for the layer and the layer files' default extraction path root. */ LayerType(String label, String extractionPath) { this.label = label; - this.extractionPath = extractionPath; + this.extractionPath = Paths.get(extractionPath); } @VisibleForTesting @@ -59,7 +61,7 @@ String getLabel() { } @VisibleForTesting - String getExtractionPath() { + Path getExtractionPath() { return extractionPath; } } @@ -75,23 +77,23 @@ private Builder() { } } - public Builder setDependenciesFiles(List dependenciesFiles) { - layerFilesMap.put(LayerType.DEPENDENCIES, dependenciesFiles); + public Builder setDependencyFiles(List dependencyFiles) { + layerFilesMap.put(LayerType.DEPENDENCIES, dependencyFiles); return this; } - public Builder setSnapshotDependenciesFiles(List snapshotDependenciesFiles) { - layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependenciesFiles); + public Builder setSnapshotDependencyFiles(List snapshotDependencyFiles) { + layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependencyFiles); return this; } - public Builder setResourcesFiles(List resourcesFiles) { - layerFilesMap.put(LayerType.RESOURCES, resourcesFiles); + public Builder setResourceFiles(List resourceFiles) { + layerFilesMap.put(LayerType.RESOURCES, resourceFiles); return this; } - public Builder setClassesFiles(List classesFiles) { - layerFilesMap.put(LayerType.CLASSES, classesFiles); + public Builder setClassFiles(List classFiles) { + layerFilesMap.put(LayerType.CLASSES, classFiles); return this; } @@ -102,22 +104,26 @@ public Builder setExtraFiles(List extraFiles) { // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot // dependencies, resources, and classes layers). - public Builder setExplodedWar(List explodedWar) { - layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWar); + public Builder setExplodedWarFiles(List explodedWarFiles) { + layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWarFiles); return this; } - public JavaLayerConfigurations build() { + public JavaLayerConfigurations build() throws IOException { ImmutableMap.Builder layerConfigurationsMap = ImmutableMap.builderWithExpectedSize(LayerType.values().length); for (LayerType layerType : LayerType.values()) { + LayerConfiguration.Builder layerConfigurationBuilder = + LayerConfiguration.builder().setLabel(layerType.getLabel()); + + // Adds all the layer files recursively. List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType)); - layerConfigurationsMap.put( - layerType, - LayerConfiguration.builder() - .addEntry(layerFiles, layerType.getExtractionPath()) - .setLabel(layerType.getLabel()) - .build()); + for (Path layerFile : layerFiles) { + layerConfigurationBuilder.addEntryRecursive( + layerFile, layerType.getExtractionPath().resolve(layerFile.getFileName())); + } + + layerConfigurationsMap.put(layerType, layerConfigurationBuilder.build()); } return new JavaLayerConfigurations(layerConfigurationsMap.build()); } @@ -138,35 +144,33 @@ public ImmutableList getLayerConfigurations() { return layerConfigurationMap.values().asList(); } - public LayerEntry getDependenciesLayerEntry() { - return getLayerEntry(LayerType.DEPENDENCIES); + public ImmutableList getDependencyLayerEntries() { + return getLayerEntries(LayerType.DEPENDENCIES); } - public LayerEntry getSnapshotDependenciesLayerEntry() { - return getLayerEntry(LayerType.SNAPSHOT_DEPENDENCIES); + public ImmutableList getSnapshotDependencyLayerEntries() { + return getLayerEntries(LayerType.SNAPSHOT_DEPENDENCIES); } - public LayerEntry getResourcesLayerEntry() { - return getLayerEntry(LayerType.RESOURCES); + public ImmutableList getResourceLayerEntries() { + return getLayerEntries(LayerType.RESOURCES); } - public LayerEntry getClassesLayerEntry() { - return getLayerEntry(LayerType.CLASSES); + public ImmutableList getClassLayerEntries() { + return getLayerEntries(LayerType.CLASSES); } - public LayerEntry getExtraFilesLayerEntry() { - return getLayerEntry(LayerType.EXTRA_FILES); + public ImmutableList getExtraFilesLayerEntries() { + return getLayerEntries(LayerType.EXTRA_FILES); } // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, // snapshot dependencies, resources, and classes layers). Should copy files in the right - public LayerEntry getExplodedWarEntry() { - return getLayerEntry(LayerType.EXPLODED_WAR); + public ImmutableList getExplodedWarEntries() { + return getLayerEntries(LayerType.EXPLODED_WAR); } - private LayerEntry getLayerEntry(LayerType layerType) { - return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)) - .getLayerEntries() - .get(0); + private ImmutableList getLayerEntries(LayerType layerType) { + return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 58f3a71ea2..c57ebe0208 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -27,25 +26,27 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.Optional; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; -import javax.annotation.Nullable; +/** + * Finds main classes in a list of class files. Main classes are classes that define the {@code + * public static void main(String[] args)} method. + */ public class MainClassFinder { /** The result of a call to {@link #find}. */ public static class Result { - /** The type of error. */ - public enum ErrorType { + /** The type of result. */ + public enum Type { - // An IOException occurred. - IO_EXCEPTION, + // Found a single main class. + MAIN_CLASS_FOUND, // Did not find any main class. MAIN_CLASS_NOT_FOUND, @@ -55,74 +56,43 @@ public enum ErrorType { } private static Result success(String foundMainClass) { - return new Result(true, Collections.singletonList(foundMainClass), null, null); + return new Result(Type.MAIN_CLASS_FOUND, Collections.singletonList(foundMainClass)); } private static Result mainClassNotFound() { - return new Result(false, Collections.emptyList(), ErrorType.MAIN_CLASS_NOT_FOUND, null); + return new Result(Type.MAIN_CLASS_NOT_FOUND, Collections.emptyList()); } private static Result multipleMainClasses(List foundMainClasses) { - return new Result(false, foundMainClasses, ErrorType.MULTIPLE_MAIN_CLASSES, null); - } - - private static Result ioException(IOException ioException) { - return new Result(false, Collections.emptyList(), ErrorType.IO_EXCEPTION, ioException); + return new Result(Type.MULTIPLE_MAIN_CLASSES, foundMainClasses); } - private final boolean isSuccess; + private final Type type; private final List foundMainClasses; - @Nullable private final ErrorType errorType; - @Nullable private final Throwable errorCause; - - private Result( - boolean isSuccess, - List foundMainClasses, - @Nullable ErrorType errorType, - @Nullable Throwable errorCause) { - this.isSuccess = isSuccess; - this.foundMainClasses = foundMainClasses; - this.errorType = errorType; - this.errorCause = errorCause; - } - /** - * Gets whether or not this result is a success. - * - * @return {@code true} if successful; {@code false} if not - */ - public boolean isSuccess() { - return isSuccess; + private Result(Type type, List foundMainClasses) { + this.foundMainClasses = foundMainClasses; + this.type = type; } /** - * Gets the found main class. Only call if {@link #isSuccess} is {@code true}. + * Gets the found main class. Only call if {@link #getType} is {@link Type#MAIN_CLASS_FOUND}. * * @return the found main class */ public String getFoundMainClass() { - Preconditions.checkArgument(isSuccess); - Preconditions.checkArgument(foundMainClasses.size() == 1); + Preconditions.checkState(Type.MAIN_CLASS_FOUND == type); + Preconditions.checkState(foundMainClasses.size() == 1); return foundMainClasses.get(0); } /** - * Gets the type of error. Call only if {@link #isSuccess} is {@code false}. - * - * @return the type of error, or {@code null} if successful - */ - public ErrorType getErrorType() { - return Preconditions.checkNotNull(errorType); - } - - /** - * Gets the cause of the error. Call only if {@link #getErrorType} is {@link - * ErrorType#IO_EXCEPTION}. + * Gets the type of the result. * - * @return the cause of the error, or {@code null} if not available + * @return the type of the result */ - public Throwable getErrorCause() { - return Preconditions.checkNotNull(errorCause); + public Type getType() { + return Preconditions.checkNotNull(type); } /** @@ -135,104 +105,88 @@ public List getFoundMainClasses() { } } - private final ImmutableList classesFiles; + private final ImmutableList files; private final JibLogger buildLogger; /** - * Finds a class with {@code psvm} in {@code classesFiles}. + * Finds a class with {@code psvm} (see class javadoc) in {@code files}. * - * @param classesFiles the classes files to check + * @param files the files to check * @param buildLogger used for displaying status messages. */ - public MainClassFinder(ImmutableList classesFiles, JibLogger buildLogger) { - this.classesFiles = classesFiles; + public MainClassFinder(ImmutableList files, JibLogger buildLogger) { + this.files = files; this.buildLogger = buildLogger; } /** - * Tries to find a class with {@code psvm} in {@link #classesFiles}. + * Tries to find classes with {@code psvm} (see class javadoc) in {@link #files}. * * @return the {@link Result} of the main class finding attempt */ public Result find() { - try { - List mainClasses = new ArrayList<>(); - Set roots = new HashSet<>(); - for (Path classPath : classesFiles) { - roots.add(classPath.getParent()); - } - for (Path root : roots) { - mainClasses.addAll(findMainClasses(root)); - } - - if (mainClasses.size() == 1) { - // Valid class found. - return Result.success(mainClasses.get(0)); - } - if (mainClasses.size() == 0) { - // No main class found anywhere. - return Result.mainClassNotFound(); - } - // More than one main class found. - return Result.multipleMainClasses(mainClasses); + List mainClasses = new ArrayList<>(); + for (Path file : files) { + findMainClass(file).ifPresent(mainClasses::add); + } - } catch (IOException ex) { - return Result.ioException(ex); + if (mainClasses.size() == 1) { + // Valid class found. + return Result.success(mainClasses.get(0)); + } + if (mainClasses.size() == 0) { + // No main class found anywhere. + return Result.mainClassNotFound(); } + // More than one main class found. + return Result.multipleMainClasses(mainClasses); } /** - * Finds the classes with {@code public static void main(String[] args)} in {@code rootDirectory}. + * Checks the {@code file} for being a {@code .class} file with {@code public static void + * main(String[] args)}. * - * @param rootDirectory directory containing the {@code .class} files. - * @return a list of class names containing a main method. - * @throws IOException if searching the root directory fails. + * @param file the file + * @return name of the class containing a main method, or {@link Optional#empty} if {@code + * classFile} is not a class */ - private List findMainClasses(Path rootDirectory) throws IOException { - // Makes sure rootDirectory is valid. - if (!Files.exists(rootDirectory) || !Files.isDirectory(rootDirectory)) { - return Collections.emptyList(); + private Optional findMainClass(Path file) { + // Makes sure classFile is valid. + if (!Files.exists(file) || !Files.isRegularFile(file) || !file.toString().endsWith(".class")) { + return Optional.empty(); } - List classNames = new ArrayList<>(); - ClassPool classPool = new ClassPool(); classPool.appendSystemPath(); try { CtClass[] mainMethodParams = new CtClass[] {classPool.get("java.lang.String[]")}; - new DirectoryWalker(rootDirectory) - .filter(Files::isRegularFile) - .filter(path -> path.toString().endsWith(".class")) - .walk( - classFile -> { - try (InputStream classFileInputStream = Files.newInputStream(classFile)) { - CtClass fileClass = classPool.makeClass(classFileInputStream); + try (InputStream classFileInputStream = Files.newInputStream(file)) { + CtClass fileClass = classPool.makeClass(classFileInputStream); - // Check if class contains 'public static void main(String[] args)'. - CtMethod mainMethod = fileClass.getDeclaredMethod("main", mainMethodParams); + // Check if class contains 'psvm' (see class javadoc). + CtMethod mainMethod = fileClass.getDeclaredMethod("main", mainMethodParams); - if (CtClass.voidType.equals(mainMethod.getReturnType()) - && Modifier.isStatic(mainMethod.getModifiers()) - && Modifier.isPublic(mainMethod.getModifiers())) { - classNames.add(fileClass.getName()); - } + if (CtClass.voidType.equals(mainMethod.getReturnType()) + && Modifier.isStatic(mainMethod.getModifiers()) + && Modifier.isPublic(mainMethod.getModifiers())) { + return Optional.of(fileClass.getName()); + } - } catch (NotFoundException ex) { - // Ignores main method not found. + } catch (NotFoundException ex) { + // Ignores main method not found. - } catch (IOException ex) { - // Could not read class file. - buildLogger.warn("Could not read class file: " + classFile); - } - }); - - return classNames; + } catch (IOException ex) { + // Could not read class file. + buildLogger.warn("Could not read file: " + file); + } } catch (NotFoundException ex) { // Thrown if 'java.lang.String' is not found in classPool. throw new RuntimeException(ex); } + + return Optional.empty(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 9fcc743cae..5587a6f62e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -16,37 +16,116 @@ package com.google.cloud.tools.jib.image; -import com.google.common.collect.ImmutableList; +import com.google.common.annotations.VisibleForTesting; import java.nio.file.Path; +import java.util.Objects; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar * archive entries. + * + *

Note that: + * + *

    + *
  • Entry source files can be either files or directories. + *
  • Adding a directory does not include the contents of the directory. Each file under a + * directory must be added as a separate {@link LayerEntry}. + *
*/ public class LayerEntry { + private final Path sourceFile; + private final Path extractionPath; + /** - * The source files to build from. Source files that are directories will have all subfiles in the - * directory added (but not the directory itself). + * Instantiates with a source file and the path to place the source file in the container file + * system. + * + *

For example, {@code new LayerEntry(Paths.get("HelloWorld.class"), + * Paths.get("/app/classes/HelloWorld.class"))} adds a file {@code HelloWorld.class} to the + * container file system at {@code /app/classes/HelloWorld.class}. * - *

The source files are specified as a list instead of a set to define the order in which they - * are added. + *

For example, {@code new LayerEntry(Paths.get("com"), Paths.get("/app/classes/com"))} adds a + * directory to the container file system at {@code /app/classes/com}. This does not add + * the contents of {@code com/}. + * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} (relative to root {@code /}) */ - private final ImmutableList sourceFiles; - - /** The Unix-style path to add the source files to in the container image filesystem. */ - private final String extractionPath; - - public LayerEntry(ImmutableList sourceFiles, String extractionPath) { - this.sourceFiles = sourceFiles; + public LayerEntry(Path sourceFile, Path extractionPath) { + this.sourceFile = sourceFile; this.extractionPath = extractionPath; } - public ImmutableList getSourceFiles() { - return sourceFiles; + /** + * Gets the source file. + * + *

Do not call {@link Path#toString} on this - use {@link #getSourceFileString} instead. + * This path can be relative or absolute, and {@link #getSourceFileString} can also be relative or + * absolute, but callers should rely on {@link #getSourceFileString} for the serialized form since + * the serialization could change independently of the path representation. + * + * @return the source file + */ + public Path getSourceFile() { + return sourceFile; } - public String getExtractionPath() { + /** + * Gets the extraction path. + * + *

Do not call {@link Path#toString} on this - use {@link #getExtractionPathString} + * instead. This path can be relative or absolute, and {@link #getExtractionPathString} can also + * be relative or absolute, but callers should rely on {@link #getExtractionPathString} for the + * serialized form since the serialization could change independently of the path representation. + * + * @return the extraction path + */ + public Path getExtractionPath() { return extractionPath; } + + /** + * Gets the source file path in string form. + * + * @return the source file path + */ + public String getSourceFileString() { + return sourceFile.toString(); + } + + /** + * Gets the extraction path in string form. This does not convert the extraction path to an + * absolute path. + * + * @return the extraction path + */ + public String getExtractionPathString() { + return extractionPath.toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LayerEntry)) { + return false; + } + LayerEntry otherLayerEntry = (LayerEntry) other; + return sourceFile.equals(otherLayerEntry.sourceFile) + && extractionPath.equals(otherLayerEntry.extractionPath); + } + + @Override + public int hashCode() { + return Objects.hash(sourceFile, extractionPath); + } + + @Override + @VisibleForTesting + public String toString() { + return getSourceFileString() + "\t" + getExtractionPathString(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 2349422f21..5eeca54cdc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -16,14 +16,10 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.tar.TarStreamBuilder; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -85,77 +81,27 @@ private List getSortedEntries() { } } - /** - * Builds the {@link TarArchiveEntry}s for adding this {@link LayerEntry} to a tarball archive. - * - * @return the list of {@link TarArchiveEntry} - * @throws IOException if walking a source file that is a directory failed - */ - @VisibleForTesting - static List buildAsTarArchiveEntries(LayerEntry layerEntry) throws IOException { - List tarArchiveEntries = new ArrayList<>(); - - // Adds the files to extract relative to the extraction path. - for (Path sourceFile : layerEntry.getSourceFiles()) { - if (Files.isDirectory(sourceFile)) { - new DirectoryWalker(sourceFile) - .filterRoot() - .filter(path -> !Files.isDirectory(path)) - .walk( - path -> { - Path sourceFileRelativePath = sourceFile.getParent().relativize(path); - Path extractionPath = - Paths.get(layerEntry.getExtractionPath()).resolve(sourceFileRelativePath); - tarArchiveEntries.add( - new TarArchiveEntry(path.toFile(), extractionPath.toString())); - }); - } else { - Path extractionPath = - Paths.get(layerEntry.getExtractionPath()).resolve(sourceFile.getFileName()); - TarArchiveEntry tarArchiveEntry = - new TarArchiveEntry(sourceFile.toFile(), extractionPath.toString()); - tarArchiveEntries.add(tarArchiveEntry); - } - } - - return tarArchiveEntries; - } - - private final ImmutableList.Builder layerEntries = ImmutableList.builder(); - - public ReproducibleLayerBuilder() {} + private final ImmutableList layerEntries; - /** - * Adds the {@code sourceFiles} to be extracted on the image at {@code extractionPath}. The order - * in which files are added matters. - * - * @param sourceFiles the source files to build from - * @param extractionPath the Unix-style path to add the source files to in the container image - * filesystem - * @return this - */ - public ReproducibleLayerBuilder addFiles(List sourceFiles, String extractionPath) { - this.layerEntries.add(new LayerEntry(ImmutableList.copyOf(sourceFiles), extractionPath)); - return this; + public ReproducibleLayerBuilder(ImmutableList layerEntries) { + this.layerEntries = layerEntries; } /** * Builds and returns the layer. * * @return the new layer - * @throws IOException if walking the source files fails */ - public UnwrittenLayer build() throws IOException { + public UnwrittenLayer build() { UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. - List layerEntries = this.layerEntries.build(); for (LayerEntry layerEntry : layerEntries) { - // Converts layerEntry to list of TarArchiveEntrys. - List tarArchiveEntries = buildAsTarArchiveEntries(layerEntry); // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and // adds parent directories for each extraction path. - tarArchiveEntries.forEach(uniqueTarArchiveEntries::add); + uniqueTarArchiveEntries.add( + new TarArchiveEntry( + layerEntry.getSourceFile().toFile(), layerEntry.getExtractionPathString())); } // Gets the entries sorted by extraction path. @@ -183,6 +129,6 @@ public UnwrittenLayer build() throws IOException { } public ImmutableList getLayerEntries() { - return layerEntries.build(); + return layerEntries; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index ae1bc4bfb9..0cee34edc1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -52,16 +52,24 @@ public class BuildAndCacheApplicationLayerStepTest { // TODO: Consolidate with BuildStepsIntegrationTest. - private static final String EXTRACTION_PATH = "/some/extraction/path/"; + private static final Path EXTRACTION_PATH_ROOT = Paths.get("/some/extraction/path/"); - private static final String EXTRA_FILES_LAYER_EXTRACTION_PATH = "/extra"; + private static final Path EXTRA_FILES_LAYER_EXTRACTION_PATH = Paths.get("/extra"); - /** Lists the files in the {@code resourcePath} resources directory. */ - private static ImmutableList getFilesList(String resourcePath) + /** + * Lists the files in the {@code resourcePath} resources directory and creates a {@link + * LayerConfiguration} with entries from those files. + */ + private static LayerConfiguration makeLayerConfiguration(String resourcePath, Path extractionPath) throws URISyntaxException, IOException { try (Stream fileStream = Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { - return fileStream.collect(ImmutableList.toImmutableList()); + LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder(); + fileStream.forEach( + sourceFile -> + layerConfigurationBuilder.addEntry( + sourceFile, extractionPath.resolve(sourceFile.getFileName()))); + return layerConfigurationBuilder.build(); } } @@ -80,28 +88,22 @@ private static ImmutableList getFilesList(String resourcePath) @Before public void setUp() throws IOException, URISyntaxException { fakeDependenciesLayerConfiguration = - LayerConfiguration.builder() - .addEntry(getFilesList("application/dependencies"), EXTRACTION_PATH + "libs/") - .build(); + makeLayerConfiguration("application/dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); fakeSnapshotDependenciesLayerConfiguration = - LayerConfiguration.builder() - .addEntry(getFilesList("application/snapshot-dependencies"), EXTRACTION_PATH + "libs/") - .build(); + makeLayerConfiguration( + "application/snapshot-dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); fakeResourcesLayerConfiguration = - LayerConfiguration.builder() - .addEntry(getFilesList("application/resources"), EXTRACTION_PATH + "resources/") - .build(); + makeLayerConfiguration("application/resources", EXTRACTION_PATH_ROOT.resolve("resources")); fakeClassesLayerConfiguration = - LayerConfiguration.builder() - .addEntry(getFilesList("application/classes"), EXTRACTION_PATH + "classes/") - .build(); + makeLayerConfiguration("application/classes", EXTRACTION_PATH_ROOT.resolve("classes")); fakeExtraFilesLayerConfiguration = LayerConfiguration.builder() .addEntry( - ImmutableList.of( - Paths.get(Resources.getResource("fileA").toURI()), - Paths.get(Resources.getResource("fileB").toURI())), - EXTRA_FILES_LAYER_EXTRACTION_PATH) + Paths.get(Resources.getResource("fileA").toURI()), + EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileA")) + .addEntry( + Paths.get(Resources.getResource("fileB").toURI()), + EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestJibLogger()); @@ -165,21 +167,34 @@ public void testRun() CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(dependenciesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor(), cacheReader .getUpToDateLayerByLayerEntries(snapshotDependenciesLayerEntries) + .orElseThrow(AssertionError::new) .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(2).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(resourcesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(3).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(classesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(4).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(extraFilesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(extraFilesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); // Verifies that the cache reader gets the same layers as the newest application layers. Assert.assertEquals( @@ -228,13 +243,22 @@ public void testRun_emptyLayersIgnored() CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(dependenciesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(dependenciesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(resourcesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(resourcesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); Assert.assertEquals( applicationLayers.get(2).getBlobDescriptor(), - cacheReader.getUpToDateLayerByLayerEntries(classesLayerEntries).getBlobDescriptor()); + cacheReader + .getUpToDateLayerByLayerEntries(classesLayerEntries) + .orElseThrow(AssertionError::new) + .getBlobDescriptor()); // Verifies that the cache reader gets the same layers as the newest application layers. Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java index 0dabe7836b..3df1f2ce55 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java @@ -41,7 +41,7 @@ public void testGetMetadataFile() { Path metadataFile = CacheFiles.getMetadataFile(mockPath); - Assert.assertEquals("metadata-v2.json", fileNameCaptor.getValue()); + Assert.assertEquals("metadata-v3.json", fileNameCaptor.getValue()); Assert.assertEquals(mockPath, metadataFile); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java index fdf3b08670..70fe03aa65 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java @@ -72,37 +72,40 @@ public void testFilter_doLayerEntriesMatchMetadataEntries_mismatchSize() { @Test public void testFilter_doLayerEntriesMatchMetadataEntries_extractionPath() { - ImmutableList layerEntries = - ImmutableList.of(new LayerEntry(ImmutableList.of(), "extractionPath")); ImmutableList metadataEntries = LayerMetadata.from( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "anotherExtractionPath")), + ImmutableList.of( + new LayerEntry( + Paths.get("anotherSourceFile"), Paths.get("anotherExtractionPath"))), FileTime.fromMillis(0)) .getEntries(); Assert.assertFalse( CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - layerEntries, metadataEntries)); - + ImmutableList.of(new LayerEntry(Paths.get("sourceFile"), Paths.get("extractionPath"))), + metadataEntries)); Assert.assertTrue( CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "anotherExtractionPath")), + ImmutableList.of( + new LayerEntry(Paths.get("anotherSourceFile"), Paths.get("anotherExtractionPath"))), metadataEntries)); } @Test public void testFilter_doLayerEntriesMatchMetadataEntries_pass() { - LayerEntry layerEntry1 = - new LayerEntry( - ImmutableList.of(Paths.get("sourceFile1"), Paths.get("sourceFile2")), "extractionPath"); - LayerEntry layerEntry2 = - new LayerEntry( - ImmutableList.of(Paths.get("sourceFile3"), Paths.get("sourceFile4")), - "anotherExtractionPath"); + LayerEntry layerEntry1 = new LayerEntry(Paths.get("sourceFile1"), Paths.get("extractionPath")); + LayerEntry layerEntry2 = new LayerEntry(Paths.get("sourceFile2"), Paths.get("extractionPath")); + LayerEntry layerEntry3 = + new LayerEntry(Paths.get("sourceFile3"), Paths.get("anotherExtractionPath")); + LayerEntry layerEntry4 = + new LayerEntry(Paths.get("sourceFile4"), Paths.get("anotherExtractionPath")); - ImmutableList layerEntries = ImmutableList.of(layerEntry1, layerEntry2); + ImmutableList layerEntries = + ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4); ImmutableList metadataEntries = - LayerMetadata.from(ImmutableList.of(layerEntry1, layerEntry2), FileTime.fromMillis(0)) + LayerMetadata.from( + ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4), + FileTime.fromMillis(0)) .getEntries(); Assert.assertTrue( @@ -115,25 +118,24 @@ public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { List mockLayers = Stream.generate(CacheMetadataTest::mockCachedLayer).limit(6).collect(Collectors.toList()); - LayerEntry fakeLayerEntry = - new LayerEntry( - ImmutableList.of(Paths.get("some/source/file"), Paths.get("some/source/directory")), - "extractionPath"); + LayerEntry fakeLayerEntry1 = + new LayerEntry(Paths.get("some/source/file"), Paths.get("extractionPath")); + LayerEntry fakeLayerEntry2 = + new LayerEntry(Paths.get("some/source/directory"), Paths.get("extractionPath")); LayerMetadata fakeExpectedSourceFilesClassesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); + LayerMetadata.from( + ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2), FileTime.fromMillis(0)); LayerMetadata fakeExpectedSourceFilesResourcesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); + LayerMetadata.from( + ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2), FileTime.fromMillis(0)); LayerMetadata fakeOtherSourceFilesLayerMetadata = LayerMetadata.from( ImmutableList.of( - new LayerEntry( - ImmutableList.of(Paths.get("not/the/same/source/file")), "extractionPath")), + new LayerEntry(Paths.get("not/the/same/source/file"), Paths.get("extractionPath"))), FileTime.fromMillis(0)); LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(new LayerEntry(ImmutableList.of(), "extractionPath")), - FileTime.fromMillis(0)); + LayerMetadata.from(ImmutableList.of(), FileTime.fromMillis(0)); List cachedLayers = Arrays.asList( @@ -154,7 +156,10 @@ public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); ImageLayers filteredLayers = - cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of(fakeLayerEntry)).filter(); + cacheMetadata + .filterLayers() + .byLayerEntries(ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2)) + .filter(); Assert.assertEquals(3, filteredLayers.size()); Assert.assertEquals( @@ -172,8 +177,7 @@ public void testFilter_byNoEntries() throws CacheMetadataCorruptedException { LayerEntry fakeLayerEntry = new LayerEntry( - ImmutableList.of(Paths.get("some/source/file", "some/source/directory")), - "extractionPath"); + Paths.get("some/source/file", "some/source/directory"), Paths.get("extractionPath")); LayerMetadata fakeSourceFilesLayerMetadata = LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); @@ -205,16 +209,12 @@ public void testFilter_byEmptySourceFiles() throws CacheMetadataCorruptedExcepti LayerEntry fakeLayerEntry = new LayerEntry( - ImmutableList.of(Paths.get("some/source/file", "some/source/directory")), - "extractionPath"); - LayerEntry fakeLayerEntryWithNoSourceFiles = - new LayerEntry(ImmutableList.of(), "extractionPath"); + Paths.get("some/source/file", "some/source/directory"), Paths.get("extractionPath")); LayerMetadata fakeSourceFilesLayerMetadata = LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(fakeLayerEntryWithNoSourceFiles), FileTime.fromMillis(0)); + LayerMetadata.from(ImmutableList.of(), FileTime.fromMillis(0)); List cachedLayers = Arrays.asList( @@ -228,10 +228,7 @@ public void testFilter_byEmptySourceFiles() throws CacheMetadataCorruptedExcepti CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); ImageLayers filteredLayers = - cacheMetadata - .filterLayers() - .byLayerEntries(ImmutableList.of(fakeLayerEntryWithNoSourceFiles)) - .filter(); + cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of()).filter(); Assert.assertEquals(1, filteredLayers.size()); Assert.assertEquals(fakeEmptySourceFilesLayerMetadata, filteredLayers.get(0).getMetadata()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java index 1d67705029..0f23ba9047 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java @@ -31,7 +31,6 @@ import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.security.DigestException; -import java.util.Collections; import java.util.List; import org.junit.Assert; import org.junit.Before; @@ -44,9 +43,8 @@ @RunWith(MockitoJUnitRunner.class) public class CacheMetadataTranslatorTest { - private static final List CLASSES_LAYER_SOURCE_FILES = - Collections.singletonList(Paths.get("some", "source", "path").toString()); - private static final String CLASSES_LAYER_EXTRACTION_PATH = "some/extraction/path"; + private static final Path CLASSES_LAYER_SOURCE_FILE = Paths.get("/some/source/path"); + private static final Path CLASSES_LAYER_EXTRACTION_PATH = Paths.get("some/extraction/path"); private static final FileTime CLASSES_LAYER_LAST_MODIFIED_TIME = FileTime.fromMillis(255073580723571L); @@ -110,11 +108,11 @@ public void testFromTemplate() Assert.assertEquals(classesLayerDiffId, classesLayer.getDiffId()); Assert.assertNotNull(classesLayer.getMetadata()); Assert.assertEquals( - CLASSES_LAYER_SOURCE_FILES, - classesLayer.getMetadata().getEntries().get(0).getSourceFilesStrings()); + CLASSES_LAYER_SOURCE_FILE.toString(), + classesLayer.getMetadata().getEntries().get(0).getSourceFileString()); Assert.assertEquals( - CLASSES_LAYER_EXTRACTION_PATH, - classesLayer.getMetadata().getEntries().get(0).getExtractionPath()); + CLASSES_LAYER_EXTRACTION_PATH.toString(), + classesLayer.getMetadata().getEntries().get(0).getExtractionPathString()); Assert.assertEquals( CLASSES_LAYER_LAST_MODIFIED_TIME, classesLayer.getMetadata().getLastModifiedTime()); } @@ -133,12 +131,7 @@ public void testToTemplate() throws URISyntaxException, IOException { LayerMetadata classesLayerMetadata = LayerMetadata.from( ImmutableList.of( - new LayerEntry( - CLASSES_LAYER_SOURCE_FILES - .stream() - .map(Paths::get) - .collect(ImmutableList.toImmutableList()), - CLASSES_LAYER_EXTRACTION_PATH)), + new LayerEntry(CLASSES_LAYER_SOURCE_FILE, CLASSES_LAYER_EXTRACTION_PATH)), CLASSES_LAYER_LAST_MODIFIED_TIME); CachedLayerWithMetadata classesLayer = new CachedLayerWithMetadata(classesCachedLayer, classesLayerMetadata); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java index c657117b4d..3f5643d983 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java @@ -30,6 +30,7 @@ import java.nio.file.attribute.FileTime; import java.security.DigestException; import java.util.Comparator; +import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -97,8 +98,7 @@ public void testGetLayerFile() throws CacheMetadataCorruptedException, IOExcepti cacheReader.getLayerFile( ImmutableList.of( new LayerEntry( - ImmutableList.of(Paths.get("some", "source", "directory")), - "some/extraction/path")))); + Paths.get("/some/source/path"), Paths.get("some/extraction/path"))))); Assert.assertNull(cacheReader.getLayerFile(ImmutableList.of())); } } @@ -112,14 +112,17 @@ public void testGetUpToDateLayerBySourceFiles() FileTime newerLastModifiedTime = FileTime.fromMillis(2000); // Copies test files to a modifiable temporary folder. - Path resourceSourceFiles = Paths.get(Resources.getResource("layer").toURI()); - Path testSourceFiles = temporaryFolder.newFolder().toPath(); - copyDirectory(resourceSourceFiles, testSourceFiles); + Path resourceSourceFilesPath = Paths.get(Resources.getResource("layer").toURI()); + Path testSourceFilesPath = temporaryFolder.newFolder().toPath(); + copyDirectory(resourceSourceFilesPath, testSourceFilesPath); + + // Gets the contents of the test source files. + ImmutableList testSourceFiles = new DirectoryWalker(testSourceFilesPath).walk(); // Walk the files in reverse order so that the subfiles are changed before the parent // directories are. - ImmutableList paths = new DirectoryWalker(testSourceFiles).walk(); - paths = ImmutableList.sortedCopyOf(Comparator.reverseOrder(), paths); + ImmutableList paths = + ImmutableList.sortedCopyOf(Comparator.reverseOrder(), testSourceFiles); for (Path path : paths) { Files.setLastModifiedTime(path, olderLastModifiedTime); } @@ -136,36 +139,74 @@ public void testGetUpToDateLayerBySourceFiles() Assert.assertNotNull(classesCachedLayer.getMetadata()); classesCachedLayer .getMetadata() - .setEntry(ImmutableList.of(testSourceFiles.toString()), "/some/extraction/path"); + .setEntries( + testSourceFiles + .stream() + .map( + testSourceFile -> + new LayerMetadata.LayerMetadataEntry( + testSourceFile.toString(), + Paths.get("some/extraction/path") + .resolve(testSourceFilesPath.relativize(testSourceFile)) + .toString())) + .collect(ImmutableList.toImmutableList())); } try (Cache cache = Cache.init(testCacheFolder)) { CacheReader cacheReader = new CacheReader(cache); ImmutableList upToDateLayerEntries = - ImmutableList.of( - new LayerEntry(ImmutableList.of(testSourceFiles), "/some/extraction/path")); - - CachedLayerWithMetadata upToDateLayer = + testSourceFiles + .stream() + .map( + testSourceFile -> + new LayerEntry( + testSourceFile, + Paths.get("some/extraction/path") + .resolve(testSourceFilesPath.relativize(testSourceFile)))) + .collect(ImmutableList.toImmutableList()); + + Optional optionalUpToDateLayer = cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries); - Assert.assertNotNull(upToDateLayer); Assert.assertEquals( - classesCachedLayer.getBlobDescriptor(), upToDateLayer.getBlobDescriptor()); + classesCachedLayer.getBlobDescriptor(), + optionalUpToDateLayer.orElseThrow(AssertionError::new).getBlobDescriptor()); // Changes a file and checks that the change is detected. Files.setLastModifiedTime( - testSourceFiles.resolve("a").resolve("b").resolve("bar"), newerLastModifiedTime); - Assert.assertNull(cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries)); - Assert.assertNull( - cacheReader.getUpToDateLayerByLayerEntries( - ImmutableList.of( - new LayerEntry(ImmutableList.of(testSourceFiles), "extractionPath")))); + testSourceFilesPath.resolve("a").resolve("b").resolve("bar"), newerLastModifiedTime); + Assert.assertFalse( + cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries).isPresent()); + Assert.assertFalse( + cacheReader + .getUpToDateLayerByLayerEntries( + testSourceFiles + .stream() + .map( + testSourceFile -> + new LayerEntry( + testSourceFile, + Paths.get("extractionPath") + .resolve(testSourceFilesPath.relativize(testSourceFile)))) + .collect(ImmutableList.toImmutableList())) + .isPresent()); // Any non-cached directory should be deemed modified. - Assert.assertNull( - cacheReader.getUpToDateLayerByLayerEntries( - ImmutableList.of( - new LayerEntry(ImmutableList.of(resourceSourceFiles), "/some/extraction/path")))); + Assert.assertFalse( + cacheReader + .getUpToDateLayerByLayerEntries( + new DirectoryWalker(resourceSourceFilesPath) + .walk() + .stream() + .map( + resourceSourceFile -> + new LayerEntry( + resourceSourceFile, + Paths.get("/some/extraction/path") + .resolve( + resourceSourceFilesPath.relativize(resourceSourceFile)))) + .collect(ImmutableList.toImmutableList())) + .isPresent()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index c24f286ad2..6bf5b84caa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.cache.LayerMetadata.LayerMetadataEntry; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; @@ -98,19 +99,21 @@ public void test_saveMetadata_noDuplicates() "6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); LayerEntry layerEntry1 = - new LayerEntry( - ImmutableList.of(Paths.get("some", "file"), Paths.get("some", "other", "file")), - "extractionPath1"); + new LayerEntry(Paths.get("some", "file"), Paths.get("extractionPath1")); LayerEntry layerEntry2 = - new LayerEntry( - ImmutableList.of(Paths.get("another", "file"), Paths.get("yet", "another", "file")), - "extractionPath2"); + new LayerEntry(Paths.get("some", "other", "file"), Paths.get("extractionPath1")); + LayerEntry layerEntry3 = + new LayerEntry(Paths.get("another", "file"), Paths.get("extractionPath2")); + LayerEntry layerEntry4 = + new LayerEntry(Paths.get("yet", "another", "file"), Paths.get("extractionPath2")); LayerMetadata layerMetadata1 = LayerMetadata.from( - ImmutableList.of(layerEntry1, layerEntry2), FileTime.from(Instant.now())); + ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4), + FileTime.from(Instant.now())); LayerMetadata layerMetadata2 = - LayerMetadata.from(ImmutableList.of(layerEntry2), FileTime.from(Instant.EPOCH)); + LayerMetadata.from( + ImmutableList.of(layerEntry3, layerEntry4), FileTime.from(Instant.EPOCH)); DescriptorDigest mockDiffId = DescriptorDigest.fromHash( @@ -152,17 +155,23 @@ public void test_saveMetadata_noDuplicates() Assert.assertNotNull(descriptorDigest1Layer); LayerMetadata layerMetadata = descriptorDigest1Layer.getMetadata(); Assert.assertNotNull(layerMetadata); - Assert.assertEquals(1, layerMetadata.getEntries().size()); + Assert.assertEquals(2, layerMetadata.getEntries().size()); Assert.assertEquals(FileTime.from(Instant.EPOCH), layerMetadata.getLastModifiedTime()); Assert.assertEquals( - layerEntry2 - .getSourceFiles() + ImmutableList.of(layerEntry3.getSourceFileString(), layerEntry4.getSourceFileString()), + layerMetadata + .getEntries() .stream() - .map(Path::toString) - .collect(ImmutableList.toImmutableList()), - layerMetadata.getEntries().get(0).getSourceFilesStrings()); + .map(LayerMetadataEntry::getSourceFileString) + .collect(ImmutableList.toImmutableList())); Assert.assertEquals( - layerEntry2.getExtractionPath(), layerMetadata.getEntries().get(0).getExtractionPath()); + ImmutableList.of( + layerEntry3.getExtractionPathString(), layerEntry4.getExtractionPathString()), + layerMetadata + .getEntries() + .stream() + .map(LayerMetadataEntry::getExtractionPathString) + .collect(ImmutableList.toImmutableList())); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java index a447d08baa..83d2452467 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java @@ -94,8 +94,7 @@ public void testWriteLayer_unwritten() throws IOException { Mockito.when(mockReproducibleLayerBuilder.getLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry( - ImmutableList.of(Paths.get("some/source/file")), "/some/extraction/path"))); + new LayerEntry(Paths.get("some/source/file"), Paths.get("/some/extraction/path")))); CachedLayerWithMetadata cachedLayerWithMetadata = cacheWriter.writeLayer(mockReproducibleLayerBuilder); @@ -106,10 +105,11 @@ public void testWriteLayer_unwritten() throws IOException { Assert.assertNotNull(layerMetadata); Assert.assertEquals(1, layerMetadata.getEntries().size()); Assert.assertEquals( - Collections.singletonList(Paths.get("some/source/file").toString()), - layerMetadata.getEntries().get(0).getSourceFilesStrings()); + Paths.get("some/source/file").toString(), + layerMetadata.getEntries().get(0).getSourceFileString()); Assert.assertEquals( - "/some/extraction/path", layerMetadata.getEntries().get(0).getExtractionPath()); + Paths.get("/some/extraction/path").toString(), + layerMetadata.getEntries().get(0).getExtractionPathString()); verifyCachedLayerIsExpected(getExpectedLayer(), cachedLayerWithMetadata); } @@ -124,7 +124,7 @@ public void testWriteLayer_doesNotOverwriteExistingTarGz() ReproducibleLayerBuilder layerBuilder = Mockito.mock(ReproducibleLayerBuilder.class); Mockito.when(layerBuilder.build()).thenReturn(unwrittenLayer); LayerEntry layerEntry = - new LayerEntry(ImmutableList.of(Paths.get("some/source/file")), "/some/extraction/path"); + new LayerEntry(Paths.get("some/source/file"), Paths.get("/some/extraction/path")); Mockito.when(layerBuilder.getLayerEntries()).thenReturn(ImmutableList.of(layerEntry)); cacheWriter.writeLayer(layerBuilder); @@ -144,8 +144,13 @@ private long getTarGzCountInCache() throws IOException { } private long getTarGzModifiedTimeInCache() throws IOException { - try (Stream stream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { - return stream.filter(CacheWriterTest::isTarGz).findFirst().get().toFile().lastModified(); + try (Stream fileStream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { + return fileStream + .filter(CacheWriterTest::isTarGz) + .findFirst() + .orElseThrow(AssertionError::new) + .toFile() + .lastModified(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java index 2bd35ae14c..f1f9ce68b5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java @@ -29,9 +29,9 @@ public class PlatformSpecificMetadataJson { * the different path separator. */ public static Path getMetadataJsonFile() throws URISyntaxException { - String metadataResourceFilename = "json/metadata-v2.json"; + String metadataResourceFilename = "json/metadata-v3.json"; if (System.getProperty("os.name").toLowerCase().contains("win")) { - metadataResourceFilename = "json/metadata_windows-v2.json"; + metadataResourceFilename = "json/metadata_windows-v3.json"; } // Loads the expected JSON string. return Paths.get(Resources.getResource(metadataResourceFilename).toURI()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java index b5e2c1a1cb..97f49788f7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java @@ -62,8 +62,8 @@ public void testToJson() throws URISyntaxException, IOException, DigestException .setLayerEntries( Collections.singletonList( new LayerEntryTemplate( - Collections.singletonList(Paths.get("some", "source", "path").toString()), - "some/extraction/path"))) + Paths.get("/some/source/path").toString(), + Paths.get("some/extraction/path").toString()))) .setLastModifiedTime(FileTime.fromMillis(255073580723571L)); CacheMetadataLayerObjectTemplate classesLayerTemplate = new CacheMetadataLayerObjectTemplate() @@ -125,11 +125,11 @@ public void testFromJson() throws URISyntaxException, IOException, DigestExcepti Assert.assertNotNull(classesLayerTemplate.getProperties()); Assert.assertEquals(1, classesLayerTemplate.getProperties().getLayerEntries().size()); Assert.assertEquals( - Collections.singletonList(Paths.get("some", "source", "path").toString()), - classesLayerTemplate.getProperties().getLayerEntries().get(0).getSourceFiles()); + Paths.get("/some/source/path").toString(), + classesLayerTemplate.getProperties().getLayerEntries().get(0).getSourceFileString()); Assert.assertEquals( - "some/extraction/path", - classesLayerTemplate.getProperties().getLayerEntries().get(0).getExtractionPath()); + Paths.get("some/extraction/path").toString(), + classesLayerTemplate.getProperties().getLayerEntries().get(0).getExtractionPathString()); Assert.assertEquals( FileTime.fromMillis(255073580723571L), classesLayerTemplate.getProperties().getLastModifiedTime()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 5ba6855509..33aa887d64 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -67,7 +67,9 @@ public void testBuilder() throws Exception { CacheConfiguration.forPath(Paths.get("base/image/layers")); List expectedLayerConfigurations = Collections.singletonList( - LayerConfiguration.builder().addEntry(Collections.emptyList(), "destination").build()); + LayerConfiguration.builder() + .addEntry(Paths.get("sourceFile"), Paths.get("pathInContainer")) + .build()); String expectedCreatedBy = "createdBy"; ImageConfiguration baseImageConfiguration = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java new file mode 100644 index 0000000000..f2c30b81b2 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LayerConfiguration}. */ +public class LayerConfigurationTest { + + @Test + public void testAddEntryRecursive() throws IOException, URISyntaxException { + Path testDirectory = Paths.get(Resources.getResource("layer").toURI()).toAbsolutePath(); + Path testFile = Paths.get(Resources.getResource("fileA").toURI()); + + LayerConfiguration layerConfiguration = + LayerConfiguration.builder() + .addEntryRecursive(testDirectory, Paths.get("/app/layer/")) + .addEntryRecursive(testFile, Paths.get("/app/fileA")) + .build(); + + ImmutableSet expectedLayerEntries = + ImmutableSet.of( + new LayerEntry(testDirectory, Paths.get("/app/layer/")), + new LayerEntry(testDirectory.resolve("a"), Paths.get("/app/layer/a/")), + new LayerEntry(testDirectory.resolve("a/b"), Paths.get("/app/layer/a/b/")), + new LayerEntry(testDirectory.resolve("a/b/bar"), Paths.get("/app/layer/a/b/bar/")), + new LayerEntry(testDirectory.resolve("c/"), Paths.get("/app/layer/c")), + new LayerEntry(testDirectory.resolve("c/cat/"), Paths.get("/app/layer/c/cat")), + new LayerEntry(testDirectory.resolve("foo"), Paths.get("/app/layer/foo")), + new LayerEntry(testFile, Paths.get("/app/fileA"))); + + Assert.assertEquals( + expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 116425503b..f92eb0d97c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -27,12 +27,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayDeque; import java.util.Arrays; -import java.util.Deque; import java.util.List; import java.util.Map; -import java.util.stream.Stream; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -46,34 +43,43 @@ @RunWith(MockitoJUnitRunner.class) public class JavaDockerContextGeneratorTest { - private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; - private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; - private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; - private static final String EXPECTED_EXPLODED_WAR_PATH = "/jetty/webapps/ROOT/"; + private static final Path EXPECTED_DEPENDENCIES_PATH = Paths.get("/app/libs/"); + private static final Path EXPECTED_RESOURCES_PATH = Paths.get("/app/resources/"); + private static final Path EXPECTED_CLASSES_PATH = Paths.get("/app/classes/"); + private static final Path EXPECTED_EXPLODED_WAR_PATH = Paths.get("/jetty/webapps/ROOT/"); private static void assertSameFiles(Path directory1, Path directory2) throws IOException { - Deque directory1Paths = new ArrayDeque<>(new DirectoryWalker(directory1).walk()); - - new DirectoryWalker(directory2) - .walk( - directory2Path -> - Assert.assertEquals( - directory1.relativize(directory1Paths.pop()), - directory2.relativize(directory2Path))); - - Assert.assertEquals(0, directory1Paths.size()); - } - - private static ImmutableList listFilesInDirectory(Path directory) throws IOException { - try (Stream files = Files.list(directory)) { - return files.collect(ImmutableList.toImmutableList()); - } + ImmutableList directory1Files = + new DirectoryWalker(directory1) + .walk() + .stream() + .map(directory1::relativize) + .collect(ImmutableList.toImmutableList()); + ImmutableList directory2Files = + new DirectoryWalker(directory2) + .walk() + .stream() + .map(directory2::relativize) + .collect(ImmutableList.toImmutableList()); + Assert.assertEquals(directory1Files, directory2Files); } @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; + private ImmutableList filesToLayerEntries(Path directory, Path extractionPathRoot) + throws IOException { + return new DirectoryWalker(directory) + .walk() + .stream() + .map( + sourceFile -> + new LayerEntry( + sourceFile, extractionPathRoot.resolve(directory.relativize(sourceFile)))) + .collect(ImmutableList.toImmutableList()); + } + @Test public void testGenerate() throws IOException, URISyntaxException { Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); @@ -84,14 +90,6 @@ public void testGenerate() throws IOException, URISyntaxException { Path testExplodedWarFiles = Paths.get(Resources.getResource("exploded-war").toURI()); Path testExtraFiles = Paths.get(Resources.getResource("layer").toURI()); - ImmutableList expectedDependenciesFiles = listFilesInDirectory(testDependencies); - ImmutableList expectedSnapshotDependenciesFiles = - listFilesInDirectory(testSnapshotDependencies); - ImmutableList expectedResourcesFiles = listFilesInDirectory(testResources); - ImmutableList expectedClassesFiles = listFilesInDirectory(testClasses); - ImmutableList expectedExplodedWarFiles = listFilesInDirectory(testExplodedWarFiles); - ImmutableList expectedExtraFiles = listFilesInDirectory(testExtraFiles); - Path targetDirectory = temporaryFolder.newFolder().toPath(); /* @@ -100,18 +98,19 @@ public void testGenerate() throws IOException, URISyntaxException { */ Files.delete(targetDirectory); - Mockito.when(mockJavaLayerConfigurations.getDependenciesLayerEntry()) - .thenReturn(new LayerEntry(expectedDependenciesFiles, EXPECTED_DEPENDENCIES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getSnapshotDependenciesLayerEntry()) - .thenReturn(new LayerEntry(expectedSnapshotDependenciesFiles, EXPECTED_DEPENDENCIES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getResourcesLayerEntry()) - .thenReturn(new LayerEntry(expectedResourcesFiles, EXPECTED_RESOURCES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) - .thenReturn(new LayerEntry(expectedClassesFiles, EXPECTED_CLASSES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntry()) - .thenReturn(new LayerEntry(expectedExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH)); - Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) - .thenReturn(new LayerEntry(expectedExtraFiles, "/")); + Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) + .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) + .thenReturn(filesToLayerEntries(testSnapshotDependencies, EXPECTED_DEPENDENCIES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) + .thenReturn(filesToLayerEntries(testResources, EXPECTED_RESOURCES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) + .thenReturn(filesToLayerEntries(testClasses, EXPECTED_CLASSES_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) + .thenReturn(filesToLayerEntries(testExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH)); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) + .thenReturn(filesToLayerEntries(testExtraFiles, Paths.get("/"))); + new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage("somebaseimage") .generate(targetDirectory); @@ -142,22 +141,22 @@ public void testMakeDockerfile() throws IOException { "key3", "value3"); - Mockito.when(mockJavaLayerConfigurations.getDependenciesLayerEntry()) + Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) .thenReturn( - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getSnapshotDependenciesLayerEntry()) + ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_DEPENDENCIES_PATH))); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) .thenReturn( - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_DEPENDENCIES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getResourcesLayerEntry()) + ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_DEPENDENCIES_PATH))); + Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) .thenReturn( - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_RESOURCES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_CLASSES_PATH)); - Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntry()) + ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_RESOURCES_PATH))); + Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_CLASSES_PATH))); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) .thenReturn( - new LayerEntry(ImmutableList.of(Paths.get("ignored")), EXPECTED_EXPLODED_WAR_PATH)); - Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(Paths.get("ignored")), "/")); + ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_EXPLODED_WAR_PATH))); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("/")))); String dockerfile = new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage(expectedBaseImage) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 8f986198d6..a53b460df5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -2,12 +2,15 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Test; @@ -15,39 +18,47 @@ public class JavaLayerConfigurationsTest { @Test - public void testDefault() { - JavaLayerConfigurations javaLayerConfigurations = JavaLayerConfigurations.builder().build(); + public void testDefault() throws IOException { + JavaLayerConfigurations javaLayerConfigurations = + JavaLayerConfigurations.builder() + .setDependencyFiles(Collections.singletonList(Paths.get("dependency"))) + .setSnapshotDependencyFiles(Collections.singletonList(Paths.get("snapshotDependency"))) + .setResourceFiles(Collections.singletonList(Paths.get("resource"))) + .setClassFiles(Collections.singletonList(Paths.get("class"))) + .setExplodedWarFiles(Collections.singletonList(Paths.get("exploded-war"))) + .setExtraFiles(Collections.singletonList(Paths.get("extra"))) + .build(); - LayerEntry dependenciesLayerEntry = javaLayerConfigurations.getDependenciesLayerEntry(); - LayerEntry snapshotDependenciesLayerEntry = - javaLayerConfigurations.getSnapshotDependenciesLayerEntry(); - LayerEntry resourcesLayerEntry = javaLayerConfigurations.getResourcesLayerEntry(); - LayerEntry classesLayerEntry = javaLayerConfigurations.getClassesLayerEntry(); - LayerEntry explodedWarEntry = javaLayerConfigurations.getExplodedWarEntry(); - LayerEntry extraFilesLayerEntry = javaLayerConfigurations.getExtraFilesLayerEntry(); + ImmutableList dependencyLayerEntries = + javaLayerConfigurations.getDependencyLayerEntries(); + ImmutableList snapshotDependencyLayerEntries = + javaLayerConfigurations.getSnapshotDependencyLayerEntries(); + ImmutableList resourceLayerEntries = + javaLayerConfigurations.getResourceLayerEntries(); + ImmutableList classLayerEntries = javaLayerConfigurations.getClassLayerEntries(); + ImmutableList explodedWarLayerEntries = + javaLayerConfigurations.getExplodedWarEntries(); + ImmutableList extraFilesLayerEntries = + javaLayerConfigurations.getExtraFilesLayerEntries(); Assert.assertEquals( - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE, - dependenciesLayerEntry.getExtractionPath()); + Paths.get(JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .resolve("dependency"), + dependencyLayerEntries.get(0).getExtractionPath()); Assert.assertEquals( - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE, - snapshotDependenciesLayerEntry.getExtractionPath()); + Paths.get(JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) + .resolve("snapshotDependency"), + snapshotDependencyLayerEntries.get(0).getExtractionPath()); Assert.assertEquals( - JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE, - resourcesLayerEntry.getExtractionPath()); + Paths.get(JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE).resolve("resource"), + resourceLayerEntries.get(0).getExtractionPath()); Assert.assertEquals( - JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE, - explodedWarEntry.getExtractionPath()); + Paths.get(JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE).resolve("class"), + classLayerEntries.get(0).getExtractionPath()); Assert.assertEquals( - JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE, - classesLayerEntry.getExtractionPath()); - Assert.assertEquals("/", extraFilesLayerEntry.getExtractionPath()); - Assert.assertTrue(dependenciesLayerEntry.getSourceFiles().isEmpty()); - Assert.assertTrue(snapshotDependenciesLayerEntry.getSourceFiles().isEmpty()); - Assert.assertTrue(resourcesLayerEntry.getSourceFiles().isEmpty()); - Assert.assertTrue(classesLayerEntry.getSourceFiles().isEmpty()); - Assert.assertTrue(explodedWarEntry.getSourceFiles().isEmpty()); - Assert.assertTrue(extraFilesLayerEntry.getSourceFiles().isEmpty()); + Paths.get(JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE).resolve("exploded-war"), + explodedWarLayerEntries.get(0).getExtractionPath()); + Assert.assertEquals(Paths.get("/extra"), extraFilesLayerEntries.get(0).getExtractionPath()); List expectedLabels = new ArrayList<>(); for (JavaLayerConfigurations.LayerType layerType : JavaLayerConfigurations.LayerType.values()) { @@ -61,7 +72,7 @@ public void testDefault() { } @Test - public void testSetFiles() { + public void testSetFiles() throws IOException { List dependencyFiles = Collections.singletonList(Paths.get("dependency")); List snapshotDependencyFiles = Collections.singletonList(Paths.get("snapshot dependency")); @@ -72,11 +83,11 @@ public void testSetFiles() { JavaLayerConfigurations javaLayerConfigurations = JavaLayerConfigurations.builder() - .setDependenciesFiles(dependencyFiles) - .setSnapshotDependenciesFiles(snapshotDependencyFiles) - .setResourcesFiles(resourceFiles) - .setClassesFiles(classFiles) - .setExplodedWar(explodedWarFiles) + .setDependencyFiles(dependencyFiles) + .setSnapshotDependencyFiles(snapshotDependencyFiles) + .setResourceFiles(resourceFiles) + .setClassFiles(classFiles) + .setExplodedWarFiles(explodedWarFiles) .setExtraFiles(extraFiles) .build(); @@ -90,7 +101,12 @@ public void testSetFiles() { extraFiles); List> actualFiles = new ArrayList<>(); for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { - actualFiles.add(layerConfiguration.getLayerEntries().get(0).getSourceFiles()); + actualFiles.add( + layerConfiguration + .getLayerEntries() + .stream() + .map(LayerEntry::getSourceFile) + .collect(Collectors.toList())); } Assert.assertEquals(expectedFiles, actualFiles); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 0182f3677e..0b3910583c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -17,8 +17,10 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.JibLogger; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.frontend.MainClassFinder.Result.Type; import com.google.common.io.Resources; +import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; @@ -36,51 +38,42 @@ public class MainClassFinderTest { @Mock private JibLogger mockBuildLogger; @Test - public void testFindMainClass_simple() throws URISyntaxException { + public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); } @Test - public void testFindMainClass_subdirectories() throws URISyntaxException { + public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("multi.layered.HelloWorld")); } @Test - public void testFindMainClass_noClass() throws URISyntaxException { + public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertFalse(mainClassFinderResult.isSuccess()); - Assert.assertEquals( - MainClassFinder.Result.ErrorType.MAIN_CLASS_NOT_FOUND, - mainClassFinderResult.getErrorType()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @Test - public void testFindMainClass_multiple() throws URISyntaxException { + public void testFindMainClass_multiple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertFalse(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); Assert.assertEquals( - MainClassFinder.Result.ErrorType.MULTIPLE_MAIN_CLASSES, - mainClassFinderResult.getErrorType()); + MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); Assert.assertTrue( mainClassFinderResult.getFoundMainClasses().contains("multi.layered.HelloMoon")); @@ -88,48 +81,44 @@ public void testFindMainClass_multiple() throws URISyntaxException { } @Test - public void testFindMainClass_extension() throws URISyntaxException { + public void testFindMainClass_extension() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test - public void testFindMainClass_importedMethods() throws URISyntaxException { + public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test - public void testFindMainClass_externalClasses() throws URISyntaxException { + public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); } @Test - public void testFindMainClass_innerClasses() throws URISyntaxException { + public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(ImmutableList.of(rootDirectory.resolve("child")), mockBuildLogger) - .find(); - Assert.assertTrue(mainClassFinderResult.isSuccess()); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld$InnerClass")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 9c86e41f30..6e009cdce9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; @@ -32,9 +33,6 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.hamcrest.CoreMatchers; @@ -91,43 +89,19 @@ private static void verifyNextTarArchiveEntryIsDirectory( @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Test - public void testBuildAsTarArchiveEntries() throws URISyntaxException, IOException { - Path testDirectory = Paths.get(Resources.getResource("layer").toURI()); - Path testFile = Paths.get(Resources.getResource("fileA").toURI()); - - List tarArchiveEntries = - ReproducibleLayerBuilder.buildAsTarArchiveEntries( - new LayerEntry(ImmutableList.of(testDirectory, testFile), "/app/")); - - List expectedTarArchiveEntries = - ImmutableList.of( - new TarArchiveEntry( - testDirectory.resolve("a").resolve("b").resolve("bar").toFile(), - "/app/layer/a/b/bar"), - new TarArchiveEntry( - testDirectory.resolve("c").resolve("cat").toFile(), "/app/layer/c/cat"), - new TarArchiveEntry(testDirectory.resolve("foo").toFile(), "/app/layer/foo"), - new TarArchiveEntry(testFile.toFile(), "/app/fileA")); - - Assert.assertEquals(expectedTarArchiveEntries.size(), tarArchiveEntries.size()); - for (int entryIndex = 0; entryIndex < expectedTarArchiveEntries.size(); entryIndex++) { - TarArchiveEntry expectedTarArchiveEntry = expectedTarArchiveEntries.get(entryIndex); - TarArchiveEntry tarArchiveEntry = tarArchiveEntries.get(entryIndex); - Assert.assertEquals(expectedTarArchiveEntry.getFile(), tarArchiveEntry.getFile()); - Assert.assertEquals(expectedTarArchiveEntry.getName(), tarArchiveEntry.getName()); - } - } - @Test public void testBuild() throws URISyntaxException, IOException { Path layerDirectory = Paths.get(Resources.getResource("layer").toURI()); Path blobA = Paths.get(Resources.getResource("blobA").toURI()); ReproducibleLayerBuilder layerBuilder = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(layerDirectory, blobA), "extract/here/apple") - .addFiles(Collections.singletonList(blobA), "extract/here/banana"); + new ReproducibleLayerBuilder( + LayerConfiguration.builder() + .addEntryRecursive(layerDirectory, Paths.get("extract/here/apple/layer")) + .addEntry(blobA, Paths.get("extract/here/apple/blobA")) + .addEntry(blobA, Paths.get("extract/here/banana/blobA")) + .build() + .getLayerEntries()); // Writes the layer tar to a temporary file. UnwrittenLayer unwrittenLayer = layerBuilder.build(); @@ -171,7 +145,7 @@ public void testToBlob_reproducibility() throws IOException { Path root1 = Files.createDirectories(testRoot.resolve("files1")); Path root2 = Files.createDirectories(testRoot.resolve("files2")); - String extractionPath = "/somewhere"; + Path extractionPath = Paths.get("/somewhere"); // TODO: Currently this test only covers variation in order and modified time, even though // TODO: the code is designed to clean up userid/groupid, this test does not check that yet. @@ -188,13 +162,17 @@ public void testToBlob_reproducibility() throws IOException { // create layers of exact same content but ordered differently and with different timestamps Blob layer = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(fileA1, fileB1), extractionPath) + new ReproducibleLayerBuilder( + ImmutableList.of( + new LayerEntry(fileA1, extractionPath.resolve("fileA")), + new LayerEntry(fileB1, extractionPath.resolve("fileB")))) .build() .getBlob(); Blob reproduced = - new ReproducibleLayerBuilder() - .addFiles(Arrays.asList(fileB2, fileA2), extractionPath) + new ReproducibleLayerBuilder( + ImmutableList.of( + new LayerEntry(fileB2, extractionPath.resolve("fileB")), + new LayerEntry(fileA2, extractionPath.resolve("fileA")))) .build() .getBlob(); diff --git a/jib-core/src/test/resources/cache/metadata-v3.json b/jib-core/src/test/resources/cache/metadata-v3.json new file mode 100644 index 0000000000..1b6d8ef370 --- /dev/null +++ b/jib-core/src/test/resources/cache/metadata-v3.json @@ -0,0 +1,34 @@ +{ + "layers": [ + { + "reference": { + "size": 1000, + "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", + "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" + } + }, + { + "reference": { + "size": 2000, + "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" + } + }, + { + "reference": { + "size": 0, + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "properties": { + "layerEntries": [ + { + "sourceFile": "/some/source/path", + "extractionPath": "some/extraction/path" + } + ], + "lastModifiedTime": 1000 + } + } + ] +} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/metadata-v3.json b/jib-core/src/test/resources/json/metadata-v3.json new file mode 100644 index 0000000000..d2ea276ca5 --- /dev/null +++ b/jib-core/src/test/resources/json/metadata-v3.json @@ -0,0 +1 @@ +{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"/some/source/path","extractionPath":"some/extraction/path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/metadata_windows-v3.json b/jib-core/src/test/resources/json/metadata_windows-v3.json new file mode 100644 index 0000000000..234f3e94da --- /dev/null +++ b/jib-core/src/test/resources/json/metadata_windows-v3.json @@ -0,0 +1 @@ +{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"\\some\\source\\path","extractionPath":"some\\extraction\\path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 299d5bcbbb..57df1b1f8a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -115,10 +115,10 @@ static JavaLayerConfigurations getForProject( Collections.sort(extraFiles); return JavaLayerConfigurations.builder() - .setDependenciesFiles(dependenciesFiles) - .setSnapshotDependenciesFiles(snapshotDependenciesFiles) - .setResourcesFiles(resourcesFiles) - .setClassesFiles(classesFiles) + .setDependencyFiles(dependenciesFiles) + .setSnapshotDependencyFiles(snapshotDependenciesFiles) + .setResourceFiles(resourcesFiles) + .setClassFiles(classesFiles) .setExtraFiles(extraFiles) .build(); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index c06b5bf93e..065839e021 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -67,6 +68,14 @@ public Set getFiles() { } } + private static ImmutableList getSourceFilesFromLayerEntries( + ImmutableList layerEntries) { + return layerEntries + .stream() + .map(LayerEntry::getSourceFile) + .collect(ImmutableList.toImmutableList()); + } + @Mock private Project mockProject; @Mock private Convention mockConvention; @Mock private JavaPluginConvention mockJavaPluginConvention; @@ -112,27 +121,24 @@ public void setUp() throws URISyntaxException { @Test public void test_correctFiles() throws URISyntaxException, IOException { + Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); ImmutableList expectedDependenciesFiles = ImmutableList.of( - Paths.get( - Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()), - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()), - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); + applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/libraryA.jar"), + applicationDirectory.resolve("dependencies/libraryB.jar")); ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( - Paths.get( - Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") - .toURI())); + applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); ImmutableList expectedResourcesFiles = ImmutableList.of( - Paths.get(Resources.getResource("application/resources").toURI()).resolve("resourceA"), - Paths.get(Resources.getResource("application/resources").toURI()).resolve("resourceB"), - Paths.get(Resources.getResource("application/resources").toURI()).resolve("world")); + applicationDirectory.resolve("resources/resourceA"), + applicationDirectory.resolve("resources/resourceB"), + applicationDirectory.resolve("resources/world")); ImmutableList expectedClassesFiles = ImmutableList.of( - Paths.get(Resources.getResource("application/classes").toURI()) - .resolve("HelloWorld.class"), - Paths.get(Resources.getResource("application/classes").toURI()).resolve("some.class")); + applicationDirectory.resolve("classes").resolve("HelloWorld.class"), + applicationDirectory.resolve("classes/some.class")); ImmutableList expectedExtraFiles = ImmutableList.of(); JavaLayerConfigurations javaLayerConfigurations = @@ -140,16 +146,20 @@ public void test_correctFiles() throws URISyntaxException, IOException { mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); Assert.assertEquals( expectedDependenciesFiles, - javaLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); + getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); Assert.assertEquals( expectedSnapshotDependenciesFiles, - javaLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); + getSourceFilesFromLayerEntries( + javaLayerConfigurations.getSnapshotDependencyLayerEntries())); Assert.assertEquals( - expectedResourcesFiles, javaLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + expectedResourcesFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getResourceLayerEntries())); Assert.assertEquals( - expectedClassesFiles, javaLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + expectedClassesFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getClassLayerEntries())); Assert.assertEquals( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); + expectedExtraFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); } @Test @@ -178,11 +188,15 @@ public void test_extraFiles() throws URISyntaxException, IOException { ImmutableList expectedExtraFiles = ImmutableList.of( - Paths.get(Resources.getResource("layer/a").toURI()), - Paths.get(Resources.getResource("layer/c").toURI()), - Paths.get(Resources.getResource("layer/foo").toURI())); + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")); Assert.assertEquals( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); + expectedExtraFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 7cd6fad6f1..75afbf19bf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -104,10 +104,10 @@ static JavaLayerConfigurations getForProject(MavenProject project, Path extraDir Collections.sort(extraFiles); return JavaLayerConfigurations.builder() - .setDependenciesFiles(dependenciesFiles) - .setSnapshotDependenciesFiles(snapshotDependenciesFiles) - .setResourcesFiles(resourcesFiles) - .setClassesFiles(classesFiles) + .setDependencyFiles(dependenciesFiles) + .setSnapshotDependencyFiles(snapshotDependenciesFiles) + .setResourceFiles(resourcesFiles) + .setClassFiles(classesFiles) .setExtraFiles(extraFiles) .build(); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 489c1771b1..3765964eaf 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -41,6 +42,14 @@ @RunWith(MockitoJUnitRunner.class) public class MavenLayerConfigurationsTest { + private static ImmutableList getSourceFilesFromLayerEntries( + ImmutableList layerEntries) { + return layerEntries + .stream() + .map(LayerEntry::getSourceFile) + .collect(ImmutableList.toImmutableList()); + } + @Rule public TestRepository testRepository = new TestRepository(); @Mock private MavenProject mockMavenProject; @@ -78,30 +87,36 @@ public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); + Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); ImmutableList expectedResourcesFiles = ImmutableList.of( - Paths.get(Resources.getResource("application/output/directory").toURI()), - Paths.get(Resources.getResource("application/output/resourceA").toURI()), - Paths.get(Resources.getResource("application/output/resourceB").toURI()), - Paths.get(Resources.getResource("application/output/world").toURI())); + applicationDirectory.resolve("output/directory"), + applicationDirectory.resolve("output/directory/somefile"), + applicationDirectory.resolve("output/resourceA"), + applicationDirectory.resolve("output/resourceB"), + applicationDirectory.resolve("output/world")); ImmutableList expectedClassesFiles = ImmutableList.of( - Paths.get(Resources.getResource("application/output/HelloWorld.class").toURI()), - Paths.get(Resources.getResource("application/output/package").toURI()), - Paths.get(Resources.getResource("application/output/some.class").toURI())); + applicationDirectory.resolve("output/HelloWorld.class"), + applicationDirectory.resolve("output/package"), + applicationDirectory.resolve("output/package/some.class"), + applicationDirectory.resolve("output/some.class")); JavaLayerConfigurations javaLayerConfigurations = MavenLayerConfigurations.getForProject(mockMavenProject, Paths.get("nonexistent/path")); Assert.assertEquals( expectedDependenciesFiles, - javaLayerConfigurations.getDependenciesLayerEntry().getSourceFiles()); + getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); Assert.assertEquals( expectedSnapshotDependenciesFiles, - javaLayerConfigurations.getSnapshotDependenciesLayerEntry().getSourceFiles()); + getSourceFilesFromLayerEntries( + javaLayerConfigurations.getSnapshotDependencyLayerEntries())); Assert.assertEquals( - expectedResourcesFiles, javaLayerConfigurations.getResourcesLayerEntry().getSourceFiles()); + expectedResourcesFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getResourceLayerEntries())); Assert.assertEquals( - expectedClassesFiles, javaLayerConfigurations.getClassesLayerEntry().getSourceFiles()); + expectedClassesFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getClassLayerEntries())); } @Test @@ -113,12 +128,16 @@ public void test_extraFiles() throws URISyntaxException, IOException { ImmutableList expectedExtraFiles = ImmutableList.of( - Paths.get(Resources.getResource("layer/a").toURI()), - Paths.get(Resources.getResource("layer/c").toURI()), - Paths.get(Resources.getResource("layer/foo").toURI())); + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")); Assert.assertEquals( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntry().getSourceFiles()); + expectedExtraFiles, + getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); } private Artifact makeArtifact(Path path) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 3b2f364728..1b32dc974c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -164,9 +164,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getLabel()) + ":"); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - for (Path sourceFile : layerEntry.getSourceFiles()) { - buildLogger.info("\t\t" + sourceFile); - } + buildLogger.info("\t\t" + layerEntry.getSourceFile()); } } @@ -180,8 +178,6 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut helpfulSuggestions.forCacheNeedsClean(), cacheMetadataCorruptedException); } catch (ExecutionException executionException) { - BuildConfiguration buildConfiguration = buildSteps.getBuildConfiguration(); - Throwable exceptionDuringBuildSteps = executionException.getCause(); if (exceptionDuringBuildSteps instanceof HttpHostConnectException) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index a32bbee7b9..64039e2612 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -17,10 +17,10 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.frontend.MainClassFinder; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; -import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import java.nio.file.Path; import javax.annotation.Nullable; @@ -112,17 +112,20 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + "; attempting to infer main class."); ImmutableList classesSourceFiles = - projectProperties.getJavaLayerConfigurations().getClassesLayerEntry().getSourceFiles(); + projectProperties + .getJavaLayerConfigurations() + .getClassLayerEntries() + .stream() + .map(LayerEntry::getSourceFile) + .collect(ImmutableList.toImmutableList()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(classesSourceFiles, projectProperties.getLogger()).find(); - if (mainClassFinderResult.isSuccess()) { - return mainClassFinderResult.getFoundMainClass(); - } + switch (mainClassFinderResult.getType()) { + case MAIN_CLASS_FOUND: + return mainClassFinderResult.getFoundMainClass(); - Verify.verify(mainClassFinderResult.getErrorType() != null); - switch (mainClassFinderResult.getErrorType()) { case MAIN_CLASS_NOT_FOUND: throw new MainClassInferenceException( HelpfulSuggestions.forMainClassNotFound( @@ -135,12 +138,6 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + String.join(", ", mainClassFinderResult.getFoundMainClasses()), projectProperties.getPluginName())); - case IO_EXCEPTION: - throw new MainClassInferenceException( - HelpfulSuggestions.forMainClassNotFound( - "Failed to get main class", projectProperties.getPluginName()), - mainClassFinderResult.getErrorCause()); - default: throw new IllegalStateException("Cannot reach here"); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 2ed1bbefae..d96f7297ad 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -88,9 +88,15 @@ public void setUpMocks() { Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( ImmutableList.of( - LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build(), - LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build(), - LayerConfiguration.builder().addEntry(ImmutableList.of(), "ignored").build())); + LayerConfiguration.builder() + .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .build(), + LayerConfiguration.builder() + .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .build(), + LayerConfiguration.builder() + .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .build())); } @Test diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 6d7c0cd35f..637821f1f1 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -17,10 +17,12 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; +import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; @@ -41,7 +43,7 @@ public class MainClassResolverTest { @Mock private ProjectProperties mockProjectProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; - private final ImmutableList fakeClassesPath = ImmutableList.of(Paths.get("a/b/c")); + private final Path FAKE_CLASSES_PATH = Paths.get("a/b/c"); @Before public void setup() { @@ -64,8 +66,8 @@ public void testResolveMainClass() throws MainClassInferenceException { @Test public void testResolveMainClass_notValid() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) - .thenReturn(new LayerEntry(fakeClassesPath, "ignored")); + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, Paths.get("ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -73,40 +75,33 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { @Test public void testResolveMainClass_multipleInferredWithBackup() - throws MainClassInferenceException, URISyntaxException { + throws MainClassInferenceException, URISyntaxException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( - new LayerEntry( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class") - .toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class") - .toURI())), - "ignored")); + new DirectoryWalker( + Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) + .walk() + .stream() + .map(path -> new LayerEntry(path, Paths.get("ignored"))) + .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); } @Test - public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException { + public void testResolveMainClass_multipleInferredWithoutBackup() + throws URISyntaxException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( - new LayerEntry( - ImmutableList.of( - Paths.get(Resources.getResource("class-finder-tests/multiple/multi").toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/HelloWorld.class") - .toURI()), - Paths.get( - Resources.getResource("class-finder-tests/multiple/NotMain.class") - .toURI())), - "ignored")); + new DirectoryWalker( + Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) + .walk() + .stream() + .map(path -> new LayerEntry(path, Paths.get("ignored"))) + .collect(ImmutableList.toImmutableList())); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); @@ -122,8 +117,8 @@ public void testResolveMainClass_multipleInferredWithoutBackup() throws URISynta @Test public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); + Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -131,8 +126,8 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere @Test public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockJavaLayerConfigurations.getClassesLayerEntry()) - .thenReturn(new LayerEntry(ImmutableList.of(), "ignored")); + Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("ignored")))); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); From 8cdc00f401a9422dded691daf45566ef3f14cefa Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 12 Sep 2018 15:04:15 -0400 Subject: [PATCH 0200/2020] Define system property names in one class (#972) --- .../tools/jib/builder/steps/StepsRunner.java | 3 +- .../tools/jib/global/JibSystemProperties.java | 102 ++++++++++++++++++ .../jib/registry/RegistryAuthenticator.java | 3 +- .../tools/jib/registry/RegistryClient.java | 4 +- .../jib/registry/RegistryEndpointCaller.java | 6 +- .../jib/global/JibSystemPropertiesTest.java | 58 ++++++++++ .../registry/RegistryEndpointCallerTest.java | 21 ++-- .../tools/jib/gradle/DockerContextTask.java | 4 +- .../gradle/PluginConfigurationProcessor.java | 8 +- .../tools/jib/maven/DockerContextMojo.java | 8 +- .../maven/PluginConfigurationProcessor.java | 9 +- .../ConfigurationPropertyValidator.java | 24 ----- .../ConfigurationPropertyValidatorTest.java | 34 ------ 13 files changed, 200 insertions(+), 84 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 67153a0602..642b81964c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; @@ -72,7 +73,7 @@ public StepsRunner( this.applicationLayersCache = applicationLayersCache; ExecutorService executorService = - Boolean.getBoolean("jibSerialize") + JibSystemProperties.isSerializedExecutionEnabled() ? MoreExecutors.newDirectExecutorService() : Executors.newCachedThreadPool(); listeningExecutorService = MoreExecutors.listeningDecorator(executorService); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java new file mode 100644 index 0000000000..26f4f8534f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.global; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; + +/** Names of system properties defined/used by Jib. */ +public class JibSystemProperties { + + @VisibleForTesting public static final String HTTP_TIMEOUT = "jib.httpTimeout"; + + @VisibleForTesting + public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; + + private static final String SERIALIZE = "jibSerialize"; + + private static final String DISABLE_USER_AGENT = "_JIB_DISABLE_USER_AGENT"; + + /** + * Gets the HTTP connection/read timeouts for registry interactions in milliseconds. This is + * defined by the {@code jib.httpTimeout} system property. The default value is 20000 if the + * system property is not set, and 0 indicates an infinite timeout. + * + * @return the HTTP connection/read timeouts for registry interactions in milliseconds + */ + public static int getHttpTimeout() { + if (Integer.getInteger(HTTP_TIMEOUT) == null) { + return 20000; + } + return Integer.getInteger(HTTP_TIMEOUT); + } + + /** + * Gets whether or not to serialize Jib's execution. This is defined by the {@code jibSerialize} + * system property. + * + * @return {@code true} if Jib's execution should be serialized, {@code false} if not + */ + public static boolean isSerializedExecutionEnabled() { + return Boolean.getBoolean(SERIALIZE); + } + + /** + * Gets whether or not to allow sending authentication information over insecure HTTP connections. + * This is defined by the {@code sendCredentialsOverHttp} system property. + * + * @return {@code true} if authentication information is allowed to be sent over insecure + * connections, {@code false} if not + */ + public static boolean isSendCredentialsOverHttpEnabled() { + return Boolean.getBoolean(SEND_CREDENTIALS_OVER_HTTP); + } + + /** + * Gets whether or not to enable the User-Agent header. This is defined by the {@code + * _JIB_DISABLE_USER_AGENT} system property. + * + * @return {@code true} if the User-Agent header is enabled, {@code false} if not + */ + public static boolean isUserAgentEnabled() { + return Strings.isNullOrEmpty(System.getProperty(DISABLE_USER_AGENT)); + } + + /** + * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) + * values. + * + * @throws NumberFormatException if invalid values + */ + public static void checkHttpTimeoutProperty() throws NumberFormatException { + String value = System.getProperty(HTTP_TIMEOUT); + if (value == null) { + return; + } + int parsed; + try { + parsed = Integer.parseInt(value); + } catch (NumberFormatException ex) { + throw new NumberFormatException(HTTP_TIMEOUT + " must be an integer: " + value); + } + if (parsed < 0) { + throw new NumberFormatException(HTTP_TIMEOUT + " cannot be negative: " + value); + } + } + + private JibSystemProperties() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index a861a46912..7de507be14 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.Connection; @@ -266,7 +267,7 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { Request.Builder requestBuilder = - Request.builder().setHttpTimeout(Integer.getInteger("jib.httpTimeout")); + Request.builder().setHttpTimeout(JibSystemProperties.getHttpTimeout()); if (authorization != null) { requestBuilder.setAuthorization(authorization); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 60ee097e49..1d7482e750 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -28,7 +29,6 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.IOException; import java.io.OutputStream; import java.net.URL; @@ -137,7 +137,7 @@ public RegistryClient newRegistryClient() { * string. */ private String makeUserAgent() { - if (!Strings.isNullOrEmpty(System.getProperty("_JIB_DISABLE_USER_AGENT"))) { + if (!JibSystemProperties.isUserAgentEnabled()) { return ""; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index dafd646011..6231bcc3e6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -20,6 +20,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; @@ -211,13 +212,14 @@ private Function getInsecureConnectionFactory() throws Registry private T call(URL url, Function connectionFactory) throws IOException, RegistryException { // Only sends authorization if using HTTPS or explicitly forcing over HTTP. - boolean sendCredentials = isHttpsProtocol(url) || Boolean.getBoolean("sendCredentialsOverHttp"); + boolean sendCredentials = + isHttpsProtocol(url) || JibSystemProperties.isSendCredentialsOverHttpEnabled(); try (Connection connection = connectionFactory.apply(url)) { Request.Builder requestBuilder = Request.builder() .setUserAgent(userAgent) - .setHttpTimeout(Integer.getInteger("jib.httpTimeout")) + .setHttpTimeout(JibSystemProperties.getHttpTimeout()) .setAccept(registryEndpointProvider.getAccept()) .setBody(registryEndpointProvider.getContent()); if (sendCredentials) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java new file mode 100644 index 0000000000..1657156973 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.global; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link JibSystemProperties}. */ +public class JibSystemPropertiesTest { + + @After + public void tearDown() { + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_ok() throws NumberFormatException { + Assert.assertNull(System.getProperty(JibSystemProperties.HTTP_TIMEOUT)); + JibSystemProperties.checkHttpTimeoutProperty(); + } + + @Test + public void testCheckHttpTimeoutSystemProperty_stringValue() { + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); + try { + JibSystemProperties.checkHttpTimeoutProperty(); + Assert.fail("Should error with a non-integer timeout"); + } catch (NumberFormatException ex) { + Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); + } + } + + @Test + public void testCheckHttpTimeoutSystemProperty_negativeValue() { + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-80"); + try { + JibSystemProperties.checkHttpTimeoutProperty(); + Assert.fail("Should error with a negative timeout"); + } catch (NumberFormatException ex) { + Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 785dc29791..bd2533d2ed 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -22,6 +22,7 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; @@ -123,8 +124,8 @@ public void setUp() throws IOException { @After public void tearDown() { - System.clearProperty("jib.httpTimeout"); - System.clearProperty("sendCredentialsOverHttp"); + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + System.clearProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP); } @Test @@ -321,7 +322,7 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - System.setProperty("sendCredentialsOverHttp", "true"); + System.setProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP, "true"); RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); Assert.assertEquals("body", insecureEndpointCaller.call()); @@ -421,12 +422,12 @@ public void testHttpTimeout_propertyNotSet() throws IOException, RegistryExcepti MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Assert.assertNull(System.getProperty("jib.httpTimeout")); + Assert.assertNull(System.getProperty(JibSystemProperties.HTTP_TIMEOUT)); secureEndpointCaller.call(); // We fall back to the default timeout: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 - Assert.assertNull(mockConnection.getRequestedHttpTimeout()); + Assert.assertEquals(20000, mockConnection.getRequestedHttpTimeout().intValue()); } @Test @@ -434,10 +435,10 @@ public void testHttpTimeout_stringValue() throws IOException, RegistryException MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - System.setProperty("jib.httpTimeout", "random string"); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); secureEndpointCaller.call(); - Assert.assertNull(mockConnection.getRequestedHttpTimeout()); + Assert.assertEquals(20000, mockConnection.getRequestedHttpTimeout().intValue()); } @Test @@ -445,7 +446,7 @@ public void testHttpTimeout_negativeValue() throws IOException, RegistryExceptio MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - System.setProperty("jib.httpTimeout", "-1"); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-1"); secureEndpointCaller.call(); // We let the negative value pass through: @@ -455,7 +456,7 @@ public void testHttpTimeout_negativeValue() throws IOException, RegistryExceptio @Test public void testHttpTimeout_0accepted() throws IOException, RegistryException { - System.setProperty("jib.httpTimeout", "0"); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "0"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); @@ -467,7 +468,7 @@ public void testHttpTimeout_0accepted() throws IOException, RegistryException { @Test public void testHttpTimeout() throws IOException, RegistryException { - System.setProperty("jib.httpTimeout", "7593"); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "7593"); MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index b1aaae5c83..e528dbcd36 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -104,7 +104,7 @@ public void generateDockerContext() { GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); jibExtension.handleDeprecatedParameters(gradleJibLogger); - ConfigurationPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + JibSystemProperties.checkHttpTimeoutProperty(); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 9d5b3c9953..fcfc15921b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; @@ -32,7 +33,6 @@ import java.time.Instant; import java.util.List; import javax.annotation.Nullable; -import org.gradle.api.GradleException; /** Configures and provides builders for the image building tasks. */ class PluginConfigurationProcessor { @@ -50,16 +50,16 @@ class PluginConfigurationProcessor { */ static PluginConfigurationProcessor processCommonConfiguration( JibLogger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) - throws InvalidImageReferenceException { + throws InvalidImageReferenceException, NumberFormatException { jibExtension.handleDeprecatedParameters(logger); - ConfigurationPropertyValidator.checkHttpTimeoutProperty(GradleException::new); + JibSystemProperties.checkHttpTimeoutProperty(); // TODO: Instead of disabling logging, have authentication credentials be provided GradleJibLogger.disableHttpLogging(); ImageReference baseImage = ImageReference.parse(jibExtension.getBaseImage()); - if (Boolean.getBoolean("sendCredentialsOverHttp")) { + if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { logger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index c4f4028235..92ce6fa07c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -61,7 +61,11 @@ public void execute() throws MojoExecutionException { MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); handleDeprecatedParameters(mavenJibLogger); - ConfigurationPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); + try { + JibSystemProperties.checkHttpTimeoutProperty(); + } catch (NumberFormatException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } Preconditions.checkNotNull(targetDir); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 177b3b1d6b..4681d6b1e6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; @@ -55,7 +56,11 @@ static PluginConfigurationProcessor processCommonConfiguration( MavenProjectProperties projectProperties) throws MojoExecutionException { jibPluginConfiguration.handleDeprecatedParameters(logger); - ConfigurationPropertyValidator.checkHttpTimeoutProperty(MojoExecutionException::new); + try { + JibSystemProperties.checkHttpTimeoutProperty(); + } catch (NumberFormatException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } // TODO: Instead of disabling logging, have authentication credentials be provided MavenJibLogger.disableHttpLogging(); @@ -63,7 +68,7 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageReference baseImage = parseImageReference(jibPluginConfiguration.getBaseImage(), "from"); // Checks Maven settings for registry credentials. - if (Boolean.getBoolean("sendCredentialsOverHttp")) { + if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { logger.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 26e09f7c46..7f2c6b04de 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -22,35 +22,11 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; -import java.util.function.Function; import javax.annotation.Nullable; /** Validator for plugin configuration parameters and system properties. */ public class ConfigurationPropertyValidator { - /** - * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) - * values. - * - * @param exceptionFactory factory to create an exception with the given description - * @param the exception type to throw if invalid values - * @throws T if invalid values - */ - public static void checkHttpTimeoutProperty( - Function exceptionFactory) throws T { - String value = System.getProperty("jib.httpTimeout"); - if (value == null) { - return; - } - try { - if (Integer.parseInt(value) < 0) { - throw exceptionFactory.apply("jib.httpTimeout cannot be negative: " + value); - } - } catch (NumberFormatException ex) { - throw exceptionFactory.apply("jib.httpTimeout must be an integer: " + value); - } - } - /** * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 08151544ce..63bcb87478 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,39 +35,6 @@ public class ConfigurationPropertyValidatorTest { @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; - @After - public void tearDown() { - System.clearProperty("jib.httpTimeout"); - } - - @Test - public void testCheckHttpTimeoutSystemProperty_ok() throws Exception { - Assert.assertNull(System.getProperty("jib.httpTimeout")); - ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); - } - - @Test - public void testCheckHttpTimeoutSystemProperty_stringValue() { - System.setProperty("jib.httpTimeout", "random string"); - try { - ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); - Assert.fail("Should error with a non-integer timeout"); - } catch (Exception ex) { - Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); - } - } - - @Test - public void testCheckHttpTimeoutSystemProperty_negativeValue() { - System.setProperty("jib.httpTimeout", "-80"); - try { - ConfigurationPropertyValidator.checkHttpTimeoutProperty(Exception::new); - Assert.fail("Should error with a negative timeout"); - } catch (Exception ex) { - Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); - } - } - @Test public void testGetImageAuth() { Mockito.when(mockAuth.getUsernamePropertyDescriptor()).thenReturn("user"); From aaa913f6c34c8bebde04dcdf32312ad42526e57b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Sep 2018 10:52:00 -0400 Subject: [PATCH 0201/2020] Adds DefaultCacheStorage#retrieve. (#901) --- .../tools/jib/image/DescriptorDigest.java | 4 +- .../jib/ncache/CacheCorruptedException.java | 4 + .../cloud/tools/jib/ncache/CacheStorage.java | 4 +- .../tools/jib/ncache/DefaultCacheEntry.java | 8 ++ .../tools/jib/ncache/DefaultCacheStorage.java | 6 +- .../jib/ncache/DefaultCacheStorageFiles.java | 42 +++++++++- .../jib/ncache/DefaultCacheStorageReader.java | 51 +++++++++++ .../jib/ncache/DefaultCacheEntryTest.java | 20 +++-- .../ncache/DefaultCacheStorageFilesTest.java | 67 ++++++++++++++- .../ncache/DefaultCacheStorageReaderTest.java | 84 +++++++++++++++++-- 10 files changed, 265 insertions(+), 25 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java index 974f431fe6..4639cdd83e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java @@ -34,8 +34,10 @@ @JsonDeserialize(using = DescriptorDigestDeserializer.class) public class DescriptorDigest { + public static final int HASH_LENGTH = 64; + /** Pattern matches a SHA-256 hash - 32 bytes in lowercase hexadecimal. */ - private static final String HASH_REGEX = "[a-f0-9]{64}"; + private static final String HASH_REGEX = String.format("[a-f0-9]{%d}", HASH_LENGTH); /** The algorithm prefix for the digest string. */ private static final String DIGEST_PREFIX = "sha256:"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java index 80cefa62c3..4a9b4b2330 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java @@ -22,4 +22,8 @@ public class CacheCorruptedException extends Exception { CacheCorruptedException(String message, Throwable cause) { super(message, cause); } + + CacheCorruptedException(String message) { + super(message); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 29d03f795f..823790f82e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -58,9 +58,11 @@ public interface CacheStorage { * * @param layerDigest the layer digest * @return the {@link CacheEntry} referenced by the layer digest + * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - Optional retrieve(DescriptorDigest layerDigest) throws IOException; + Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException; /** * Queries for layer digests that can be selected with the {@code selector}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java index 0c388fed02..d68d1add13 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java @@ -61,6 +61,14 @@ public Builder setMetadataBlob(@Nullable Blob metadataBlob) { return this; } + public boolean hasLayerBlob() { + return layerBlob != null; + } + + public boolean hasMetadataBlob() { + return metadataBlob != null; + } + public CacheEntry build() { return new DefaultCacheEntry( Preconditions.checkNotNull(layerDigest, "layerDigest required"), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java index f9328a041a..68a6eb0a3b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -78,9 +78,9 @@ public List listDigests() throws IOException, CacheCorruptedEx } @Override - public Optional retrieve(DescriptorDigest layerDigest) throws IOException { - // TODO: Implement - return Optional.empty(); + public Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException { + return defaultCacheStorageReader.retrieve(layerDigest); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java index 7978b8e084..ae901d47d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java @@ -18,15 +18,53 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; +import java.security.DigestException; /** Resolves the files used in the default cache storage engine. */ class DefaultCacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; - private static final String LAYER_FILENAME_SUFFIX = ".layer"; private static final String METADATA_FILENAME = "metadata"; private static final String SELECTORS_DIRECTORY = "selectors"; + /** + * Returns whether or not {@code file} is a layer contents file. + * + * @param file the file to check + * @return {@code true} if {@code file} is a layer contents file; {@code false} otherwise + */ + static boolean isLayerFile(Path file) { + return file.getFileName().toString().length() == DescriptorDigest.HASH_LENGTH; + } + + /** + * Returns whether or not {@code file} is a metadata file. + * + * @param file the file to check + * @return {@code true} if {@code file} is a metadata file; {@code false} otherwise + */ + static boolean isMetadataFile(Path file) { + return METADATA_FILENAME.equals(file.getFileName().toString()); + } + + /** + * Gets the diff ID portion of the layer filename. + * + * @param layerFile the layer file to parse for the diff ID + * @return the diff ID portion of the layer file filename + * @throws CacheCorruptedException if no valid diff ID could be parsed + */ + static DescriptorDigest getDiffId(Path layerFile) throws CacheCorruptedException { + try { + String diffId = layerFile.getFileName().toString(); + return DescriptorDigest.fromHash(diffId); + + } catch (DigestException | IndexOutOfBoundsException ex) { + throw new CacheCorruptedException( + "Layer file did not include valid diff ID: " + layerFile, ex); + } + } + private final Path cacheDirectory; DefaultCacheStorageFiles(Path cacheDirectory) { @@ -52,7 +90,7 @@ Path getLayerFile(DescriptorDigest layerDigest, DescriptorDigest layerDiffId) { * @return the layer filename */ String getLayerFilename(DescriptorDigest layerDiffId) { - return layerDiffId.getHash() + LAYER_FILENAME_SUFFIX; + return layerDiffId.getHash(); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java index 8abedc0939..609799d721 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Files; @@ -23,6 +24,7 @@ import java.security.DigestException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,4 +60,53 @@ List listDigests() throws IOException, CacheCorruptedException return layerDigests; } } + + /** + * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. + * + * @param layerDigest the layer digest + * @return the {@link CacheEntry} referenced by the layer digest + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException { + Path layerDirectory = defaultCacheStorageFiles.getLayerDirectory(layerDigest); + if (!Files.exists(layerDirectory)) { + return Optional.empty(); + } + + DefaultCacheEntry.Builder cacheEntryBuilder = + DefaultCacheEntry.builder().setLayerDigest(layerDigest); + + try (Stream filesInLayerDirectory = Files.list(layerDirectory)) { + for (Path fileInLayerDirectory : filesInLayerDirectory.collect(Collectors.toList())) { + if (DefaultCacheStorageFiles.isLayerFile(fileInLayerDirectory)) { + if (cacheEntryBuilder.hasLayerBlob()) { + throw new CacheCorruptedException( + "Multiple layer files found for layer with digest " + + layerDigest.getHash() + + " in directory: " + + layerDirectory); + } + cacheEntryBuilder + .setLayerBlob(Blobs.from(fileInLayerDirectory)) + .setLayerDiffId(DefaultCacheStorageFiles.getDiffId(fileInLayerDirectory)) + .setLayerSize(Files.size(fileInLayerDirectory)); + + } else if (DefaultCacheStorageFiles.isMetadataFile(fileInLayerDirectory)) { + if (cacheEntryBuilder.hasMetadataBlob()) { + throw new CacheCorruptedException( + "Multiple metadata files found for layer with digest " + + layerDigest.getHash() + + " in directory: " + + layerDirectory); + } + cacheEntryBuilder.setMetadataBlob(Blobs.from(fileInLayerDirectory)); + } + } + } + + return Optional.of(cacheEntryBuilder.build()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java index c189318296..dbd4ea3673 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java @@ -65,13 +65,15 @@ public void testBuilder_fail() { @Test public void testBuilder_noMetadataBlob() throws IOException { - CacheEntry cacheEntry = + DefaultCacheEntry.Builder cacheEntryBuilder = DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) - .setLayerSize(1337) - .setLayerBlob(Blobs.from("layerBlob")) - .build(); + .setLayerSize(1337); + Assert.assertFalse(cacheEntryBuilder.hasLayerBlob()); + cacheEntryBuilder.setLayerBlob(Blobs.from("layerBlob")); + Assert.assertTrue(cacheEntryBuilder.hasLayerBlob()); + CacheEntry cacheEntry = cacheEntryBuilder.build(); Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); Assert.assertEquals(1337, cacheEntry.getLayerSize()); @@ -81,14 +83,16 @@ public void testBuilder_noMetadataBlob() throws IOException { @Test public void testBuilder_withMetadataBlob() throws IOException { - CacheEntry cacheEntry = + DefaultCacheEntry.Builder cacheEntryBuilder = DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .setLayerSize(1337) - .setLayerBlob(Blobs.from("layerBlob")) - .setMetadataBlob(Blobs.from("metadataBlob")) - .build(); + .setLayerBlob(Blobs.from("layerBlob")); + Assert.assertFalse(cacheEntryBuilder.hasMetadataBlob()); + cacheEntryBuilder.setMetadataBlob(Blobs.from("metadataBlob")); + Assert.assertTrue(cacheEntryBuilder.hasMetadataBlob()); + CacheEntry cacheEntry = cacheEntryBuilder.build(); Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); Assert.assertEquals(1337, cacheEntry.getLayerSize()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java index 6e3c12d210..9324c0c91b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Paths; import java.security.DigestException; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -28,6 +29,68 @@ public class DefaultCacheStorageFilesTest { private static final DefaultCacheStorageFiles testDefaultCacheStorageFiles = new DefaultCacheStorageFiles(Paths.get("cache/directory")); + @Test + public void testIsLayerFile() { + Assert.assertTrue( + DefaultCacheStorageFiles.isLayerFile( + Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + Assert.assertTrue( + DefaultCacheStorageFiles.isLayerFile( + Paths.get("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); + Assert.assertFalse(DefaultCacheStorageFiles.isLayerFile(Paths.get("is.not.layer.file"))); + } + + @Test + public void testIsMetadataFile() { + Assert.assertTrue(DefaultCacheStorageFiles.isMetadataFile(Paths.get("metadata"))); + Assert.assertTrue(DefaultCacheStorageFiles.isMetadataFile(Paths.get("is", "metadata"))); + Assert.assertFalse(DefaultCacheStorageFiles.isMetadataFile(Paths.get("not.metadata"))); + } + + @Test + public void testGetDiffId() throws DigestException, CacheCorruptedException { + Assert.assertEquals( + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + DefaultCacheStorageFiles.getDiffId( + Paths.get( + "layer", + "file", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); + Assert.assertEquals( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + DefaultCacheStorageFiles.getDiffId( + Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + } + + @Test + public void testGetDiffId_corrupted() { + try { + DefaultCacheStorageFiles.getDiffId(Paths.get("not long enough")); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Layer file did not include valid diff ID: not long enough", ex.getMessage()); + Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); + } + + try { + DefaultCacheStorageFiles.getDiffId( + Paths.get( + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Layer file did not include valid diff ID: " + + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ex.getMessage()); + Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); + } + } + @Test public void testGetLayerFile() throws DigestException { DescriptorDigest layerDigest = @@ -43,7 +106,7 @@ public void testGetLayerFile() throws DigestException { "directory", "layers", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.layer"), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), testDefaultCacheStorageFiles.getLayerFile(layerDigest, diffId)); } @@ -54,7 +117,7 @@ public void testGetLayerFilename() throws DigestException { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); Assert.assertEquals( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.layer", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", testDefaultCacheStorageFiles.getLayerFilename(diffId)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java index 164df5d91b..0c08d0aaab 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java @@ -16,13 +16,17 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Files; import java.security.DigestException; import java.util.Arrays; +import java.util.Optional; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -32,26 +36,34 @@ public class DefaultCacheStorageReaderTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private DescriptorDigest layerDigest1; + private DescriptorDigest layerDigest2; + + @Before + public void setUp() throws DigestException { + layerDigest1 = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + layerDigest2 = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + } + @Test - public void testListDigests() throws IOException, DigestException, CacheCorruptedException { + public void testListDigests() throws IOException, CacheCorruptedException { DefaultCacheStorageFiles defaultCacheStorageFiles = new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); DefaultCacheStorageReader defaultCacheStorageReader = new DefaultCacheStorageReader(defaultCacheStorageFiles); - // Checks that layer directories created are all listed. - DescriptorDigest layerDigest1 = - DescriptorDigest.fromHash( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - DescriptorDigest layerDigest2 = - DescriptorDigest.fromHash( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + // Creates test layer directories. Files.createDirectories( defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest1.getHash())); Files.createDirectories( defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest2.getHash())); + // Checks that layer directories created are all listed. Assert.assertEquals( Arrays.asList(layerDigest1, layerDigest2), defaultCacheStorageReader.listDigests()); @@ -66,4 +78,60 @@ public void testListDigests() throws IOException, DigestException, CacheCorrupte Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } } + + @Test + public void testRetrieve() throws IOException, CacheCorruptedException { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + + DefaultCacheStorageReader defaultCacheStorageReader = + new DefaultCacheStorageReader(defaultCacheStorageFiles); + + // Creates the test layer directory. + DescriptorDigest layerDigest = layerDigest1; + DescriptorDigest layerDiffId = layerDigest2; + Blob layerBlob = Blobs.from("layerBlob"); + Files.createDirectories(defaultCacheStorageFiles.getLayerDirectory(layerDigest)); + Blobs.writeToFileWithLock( + layerBlob, defaultCacheStorageFiles.getLayerFile(layerDigest, layerDiffId)); + + // Checks that the CacheEntry is retrieved correctly. + Optional optionalCacheEntry = defaultCacheStorageReader.retrieve(layerDigest); + Assert.assertTrue(optionalCacheEntry.isPresent()); + Assert.assertEquals(layerDigest, optionalCacheEntry.get().getLayerDigest()); + Assert.assertEquals(layerDiffId, optionalCacheEntry.get().getLayerDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCacheEntry.get().getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCacheEntry.get().getLayerBlob())); + Assert.assertFalse(optionalCacheEntry.get().getMetadataBlob().isPresent()); + + // Adds a metadata file. + Blobs.writeToFileWithLock( + Blobs.from("metadata"), defaultCacheStorageFiles.getMetadataFile(layerDigest)); + + // Checks that the CacheEntry is retrieved correctly with the metadata. + optionalCacheEntry = defaultCacheStorageReader.retrieve(layerDigest); + Assert.assertTrue(optionalCacheEntry.isPresent()); + Assert.assertEquals(layerDigest, optionalCacheEntry.get().getLayerDigest()); + Assert.assertEquals(layerDiffId, optionalCacheEntry.get().getLayerDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCacheEntry.get().getLayerSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCacheEntry.get().getLayerBlob())); + Assert.assertTrue(optionalCacheEntry.get().getMetadataBlob().isPresent()); + Assert.assertEquals( + "metadata", Blobs.writeToString(optionalCacheEntry.get().getMetadataBlob().get())); + + // Checks that multiple .layer files means the cache is corrupted. + Files.createFile(defaultCacheStorageFiles.getLayerFile(layerDigest, layerDigest)); + try { + defaultCacheStorageReader.retrieve(layerDigest); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Multiple layer files found for layer with digest " + + layerDigest.getHash() + + " in directory: " + + defaultCacheStorageFiles.getLayerDirectory(layerDigest), + ex.getMessage()); + } + } } From 70f5cf546fa01eb9ab1511c28721a5e0a20aa2bc Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Sep 2018 10:53:00 -0400 Subject: [PATCH 0202/2020] Proposal RFC: New cache mechanism (#878) --- proposals/cache_v2.md | 177 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 proposals/cache_v2.md diff --git a/proposals/cache_v2.md b/proposals/cache_v2.md new file mode 100644 index 0000000000..4bc8f3f439 --- /dev/null +++ b/proposals/cache_v2.md @@ -0,0 +1,177 @@ +# Proposal: Improved cache mechanism + +The tracking issue is at [#637](https://github.com/GoogleContainerTools/jib/issues/637). + +## TLDR; + +The current cache design is less-than-ideal and should be revamped before releasing [Jib as a library for Java](https://github.com/GoogleContainerTools/jib/issues/337). + +## Current cache mechanism + +In the current state (as of version `0.9.9`), Jib caches layer data in a directory on disk. Layer tarballs are stored as files in that directory, and metadata for all layers are stored in a single JSON file. + +For example, the cache directory layout may look like: + +``` + 36a2b7401dcddc50a35aeaa81085718b9d5fbce9d607c55a1d79beec2469f9ac.tar.gz + c63484398b097b7e9693ac373ac95630bb8d8ad8ff90a3277e7105bb77e8e986.tar.gz + metadata-v2.json +``` + +The metadata stores a list of metadata regarding each of the layers, including its diff ID (uncompressed digest), last modified time, and layer entries (the layout of the tarball including what files actually went into building the tarball). + +One of the main cache queries to perform during the Jib execution is to check if a layer is up-to-date (recently stored in the cache). Currently, this mechanism works by storing the last modified time of the layer and the layer entries. Jib queries the cache for layers that match some layer entries. If the last modified time of that cached layer precedes the modification times of the files in the layer entries, then that cached layer is used; otherwise, the layer is rebuilt from the newly-modified files. + +### Problems + +The problems with the current cache mechanism include: +- When multiple Jib executions using the same cache run in parallel: + - Concurrent writes to the metadata could result in corruption of the metadata file. See [#848](https://github.com/GoogleContainerTools/jib/issues/848). + - Only the last-finishing execution will have its metadata updates written to the metadata. The other executions will have their metadata updates lost, resulting in less-than-optimal caching. +- The up-to-date check is tightly coupled to the implementation of the cache. This does not allow for easy switching of the strategy to use for up-to-date checks. +- Retrieval of layers by layer entries currently involves checking through the entire metadata and matching against the layer entries stored in the metadata. This could become slow as the cache grows in size. +- The current implementation tightly couples the actual storage implementation with the contents stored in the cache. This does not allow for easily implementing new storage engines with possibly different underlying storage systems. +- Since the metadata is only written out at the end of a Jib execution (successful or failure), interrupted Jib executions would result in lost metadata updates. + +These problems should be resolved before [Jib as a library for Java](https://github.com/GoogleContainerTools/jib/issues/337) is released. + +## Proposal + +### Goals + +- Clearly-defined storage engine interface that decouples the query system from the storage engine. +- A default storage engine implementation that stores layer data independently and can retrieve layers by the layer entries that built that layer in constant time. + +### Solution + +#### Storage engine interface + +There are 4 actions that would be performed against the storage engine: + +- Save a cache entry +- List the entries in the cache +- Retrieve a cache entry by layer digest +- Retrieve a layer digest by a selector (explained below) +- (Optional) Prune the cache + +Cache entry writes are provided with: + +- Layer blob (generates the layer digest, diff ID, and size) +- Optional selector to additionally reference the layer +- Optional metadata blob + +Cache entry reads are provided with: + +- Layer digest +- Layer diff ID +- Layer size +- Layer blob +- Optional metadata blob + +#### Storage engine implementation + +There are two types of layers - application layers and base image layers. Base image layers would only need to store their layer data (layer digest and diff ID). Application layers need to store their layer data along with metadata and a custom selector. + +In the provided storage implementation, the metadata will be just the last modified time of the layer (the latest modification time for the files that go into the layer). **The selector will be a digest of the layer entries that built the layer.** + +For example, the cache directory structure looks like: + +``` +layers/ + 36a2b7401dcddc50a35aeaa81085718b9d5fbce9d607c55a1d79beec2469f9ac/ + 326a609681777ee4ca02b1898579c9e07801ef066a629a3c59fa6df6ab42b7aa + metadata +selectors/ + 65de3b72aaf98e4f300ccdf7d64bf9a3b1e23c8c44a1242265f717db1a0877e9 +``` + +The `layers/` directory consists of directories for each layer, named by the layer digest. +Inside each of the layer directories: +- The layer tarball is named by the layer diff ID +- The metadata is stored in the file `metadata` + +The `selectors/` directory consists of a file for each selector, named by the selector digest. The selector file contents will be the digest of the layer the selector references. In the example, `selectors/65de3b72aaf98e4f300ccdf7d64bf9a3b1e23c8c44a1242265f717db1a0877e9` will have contents `36a2b7401dcddc50a35aeaa81085718b9d5fbce9d607c55a1d79beec2469f9ac`. + +All writes should lock the file they are writing to. + +### Analysis + +*Note: This analysis assumes that directory traversals are constant, but some filesystems have linear time directory traversal. To account for directory traversals, see [directory sharding](#directory-sharding) below.* + +#### Save a cache entry + +Each cache entry (layer data, metadata, selector) is stored independently. This solves all of the concurrent metadata write problems due to a single metadata file. Writes are now *constant time* rather than linear in the size of the cache. + +#### List the entries in the cache + +Listing the entries (by list of layer digests) is a simple call to list the names of the directories in `layers/`. Listing is *linear* in the size of the cache. + +#### Retrieve a cache entry by layer digest + +Retrieving an entry by layer digest is simply finding the file referenced at `layers/` (along with associated files). This operation is *constant* in the size of the cache. + +#### Retrieve a layer digest by a selector + +The higher-level operation is finding a layer that was built by a list of layer entries (the files that built the layer). The operation generates a digest of the layer entries (the selector) and then the storage engine simply finds the file referenced at `selectors/` (from which the layer digest is retrieved). This operation is *constant* in the size of the cache (and linear in the number of layer entries). + +### Directory sharding + +Directory traversal may be at worst a linear time operation (such as ext2 and FAT), and at best a logarithmic time operation (such as with the B-tree directory structure of ext3/4, HFS+, NTFS). The linear time directory traversal could potentially be an issue if the cache grows without bound. To address this issue, the contents of a directory can be sharded into a set number of bins to achieve logarithmic time traversal. For example, given a flat directory of files: + +``` +directory/ + file1 + file2 + ... + file100000 +``` + +The files can be placed in a select number of bins (say 4 in this case): + +``` +directory/ + bin1/ + file1 + ... + bin2/ + file25001 + ... + bin3/ + file50001 + ... + bin4/ + file75001 + ... +``` + +This technique can be applied recursively within each bin as well (with a sentinel to denote recursive termination). + +By choosing a constant number of bins, the traversal at each directory is constant time. The total depth of the sharding is logarithmic to the number of files. *The overall traversal time is logarithmic.* + +In practice, an effective number of bins may be 256, named by the hex codes `00` through `ff`. + +### Locking + +All file writes will obtain an exclusive lock on the file and all file reads will obtain a shared lock on the file. However, obtaining locks on the same file during concurrent executions of Jib within the same JVM would result in an `OverlappingFileLockException`. Therefore, the cache cannot rely on `FileLock`s for concurrent I/O control on files. + +#### Save a cache entry + +Saving a cache entry requires saving the layer blob and metadata to a layer directory under `layers/` and the selector file to the `selectors/` directory. + +The layer directory should be written atomically to avoid reads that may read an incomplete layer directory. + +The selector file can be written atomically anytime after the layer directory is fully written. + +All atomic writes should be done by first writing to a temporary location (outside of the cache) and then moving to the desired location. + +#### List the entries in the cache + +Listing the entries in the cache just requires obtaining a list of all the layer directories under `layers/`. + +#### Retrieve a cache entry by layer digest + +Since layer directories are only moved to their intended location after finishing the atomic write, retrieving by layer digest (via finding the file at the intended location) would always retrieve a completely finished and valid layer directory. + +#### Retrieve a layer digest by a selector + +Since selectors are only written after the layer directory it selects is completely written, retrieving by selector would always retrieve a completely finished and valid layer directory. From 98099c3542fa7de2093207b9379f29126a49c457 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Sep 2018 12:38:25 -0400 Subject: [PATCH 0203/2020] Proposal RFC: Emit events from Jib Core (#909) --- proposals/events.md | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 proposals/events.md diff --git a/proposals/events.md b/proposals/events.md new file mode 100644 index 0000000000..cdeeec807b --- /dev/null +++ b/proposals/events.md @@ -0,0 +1,65 @@ +# Proposal: Emit events from Jib Core + +The tracking issue is at [#714](https://github.com/GoogleContainerTools/jib/issues/714). + +## Motivation + +Currently, Jib logs various log messages via injecting a [`JibLogger`](https://github.com/GoogleContainerTools/jib/blob/02f7f41874223e1e6acf2a40648b5b3695877397/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java) interface into the execution steps in the `builder` package. However, the data is not structured. The user (of Jib Core) needs to parse log messages in order to obtain useful information. The log messages are also catered towards the `jib-maven/gradle-plugin` execution output. + +## Solution + +Emit events with structured information. + +## Goals + +- Flexible to support different event payloads +- Extensible with different event types +- Minimal overhead if not used + +## Proposal + +An `EventHandlers` class holds handlers to pass into the execution steps. This is used by `ExecutionContext`. + +```java +class EventHandlers { + + // Handles `E` event class to with `eventConsumer`. + add(JibEventType eventType, Consumer eventConsumer); + + // Handles all events. + add(Consumer eventConsumer); +} +``` + +Emitted events will be matched to handlers by their exact type. `JibEvent`s should **not** inherit from each other. `JibEventType` defines constants for all the possible event types to add handlers for. + +An example usage could look like this: + +```java +// In Jib Core +class PushingBlobEvent implements JibEvent { + + DescriptorDigest getDigest(); + URL getUploadLocation(); +} + +// Called by user +ExecutionContext executionContext = + ExecutionContext.newContext() + .addEventHandler(JibEventType.PUSHING_BLOB, event -> { + // Do some processing on event, like: + System.out.println( + "Pushing blob " + event.getDigest() + " to " + + event.getUploadLocation()); + }) + .addEventHandler(allJibEvents -> { + System.out.println("Some event happened"); + }) + // Some class that has pre-defined handlers that print log messages. + .addEventHandlers(new LoggingEventHandlers(jibLogger)); +Jib.from(...) + ... + .containerize( + Containerizers.withExecutionContext(executionContext) + .to(RegistryImage.named(targetImage))); +``` From 8f143cfa79b3bf74ec8d82202b1591038e5f8fdb Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Sep 2018 12:53:00 -0400 Subject: [PATCH 0204/2020] Adds Jib and JibContainerBuilder. (#947) --- .../com/google/cloud/tools/jib/api/Jib.java | 51 +++ .../tools/jib/api/JibContainerBuilder.java | 301 ++++++++++++++++++ .../BuildAndCacheApplicationLayerStep.java | 2 +- .../jib/configuration/LayerConfiguration.java | 36 ++- .../cloud/tools/jib/configuration/Port.java | 86 +++-- .../jib/frontend/ExposedPortsParser.java | 5 +- .../jib/frontend/JavaLayerConfigurations.java | 14 +- .../jib/image/json/JsonToImageTranslator.java | 3 +- .../configuration/BuildConfigurationTest.java | 4 +- .../ContainerConfigurationTest.java | 3 +- .../tools/jib/configuration/PortTest.java | 45 +++ .../jib/frontend/ExposedPortsParserTest.java | 23 +- .../frontend/JavaLayerConfigurationsTest.java | 4 +- .../cloud/tools/jib/image/ImageTest.java | 8 +- .../image/json/ImageToJsonTranslatorTest.java | 9 +- .../image/json/JsonToImageTranslatorTest.java | 13 +- .../jib/plugins/common/BuildStepsRunner.java | 2 +- 17 files changed, 506 insertions(+), 103 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java new file mode 100644 index 0000000000..da8e914dba --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; + +/** Build containers with Jib. */ +// TODO: Add tests once JibContainerBuilder#containerize() is added. +public class Jib { + + /** + * Starts building the container from a base image. + * + * @param baseImageReference the base image reference + * @return a new {@link JibContainerBuilder} to continue building the container + * @throws InvalidImageReferenceException if the {@code baseImageReference} is not a valid image + * reference + */ + public static JibContainerBuilder from(String baseImageReference) + throws InvalidImageReferenceException { + return from(ImageReference.parse(baseImageReference)); + } + + /** + * Starts building the container from a base image. + * + * @param baseImageReference the base image reference + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(ImageReference baseImageReference) { + return new JibContainerBuilder(baseImageReference); + } + + private Jib() {} +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java new file mode 100644 index 0000000000..3640e2c656 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -0,0 +1,301 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Builds a container with Jib. + * + *

Example usage: + * + *

{@code
+ * Jib.from(baseImage)
+ *    .addLayer(sourceFiles, extractionPath)
+ *    .setEntrypoint("myprogram", "--flag", "subcommand")
+ *    .setProgramArguments("hello", "world")
+ *    .addEnvironmentVariable("HOME", "/app")
+ *    .addExposedPort(Port.tcp(8080))
+ *    .addLabel("containerizer", "jib")
+ *    .containerize(...);
+ * }
+ */ +// TODO: Add tests once containerize() is added. +public class JibContainerBuilder { + + private final ImageReference baseImageReference; + + private List layerConfigurations = new ArrayList<>(); + private Map environment = new HashMap<>(); + private List ports = new ArrayList<>(); + private Map labels = new HashMap<>(); + @Nullable private ImmutableList entrypoint; + @Nullable private ImmutableList programArguments; + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(ImageReference baseImageReference) { + this.baseImageReference = baseImageReference; + } + + /** + * Adds a new layer to the container with {@code files} as the source files and {@code + * pathInContainer} as the path to copy the source files to in the container file system. + * + *

Source files that are directories will be recursively copied. For example, if the source + * files are: + * + *

    + *
  • {@code fileA} + *
  • {@code fileB} + *
  • {@code directory/} + *
+ * + * and the destination to copy to is {@code /path/in/container}, then the new layer will have the + * following entries for the container file system: + * + *
    + *
  • {@code /path/in/container/fileA} + *
  • {@code /path/in/container/fileB} + *
  • {@code /path/in/container/directory/} + *
  • {@code /path/in/container/directory/...} (all contents of {@code directory/}) + *
+ * + * @param files the source files to copy to a new layer in the container + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} (relative to root {@code /}) + * @return this + * @throws IOException if an exception occurred when recursively listing any directories + */ + public JibContainerBuilder addLayer(List files, Path pathInContainer) throws IOException { + LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder(); + + for (Path file : files) { + layerConfigurationBuilder.addEntryRecursive( + file, pathInContainer.resolve(file.getFileName())); + } + + return addLayer(layerConfigurationBuilder.build()); + } + + /** + * Sets the layers (defined by a list of {@link LayerConfiguration}s). This replaces any + * previously-added layers. + * + * @param layerConfigurations the list of {@link LayerConfiguration}s + * @return this + */ + public JibContainerBuilder setLayers(List layerConfigurations) { + this.layerConfigurations = new ArrayList<>(layerConfigurations); + return this; + } + + /** + * Sets the layers. This replaces any previously-added layers. + * + * @param layerConfigurations the {@link LayerConfiguration}s + * @return this + */ + public JibContainerBuilder setLayers(LayerConfiguration... layerConfigurations) { + return setLayers(Arrays.asList(layerConfigurations)); + } + + /** + * Adds a layer (defined by a {@link LayerConfiguration}). + * + * @param layerConfiguration the {@link LayerConfiguration} + * @return this + */ + public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { + layerConfigurations.add(layerConfiguration); + return this; + } + + /** + * Sets the container entrypoint. This is the beginning of the command that is run when the + * container starts. {@link #setProgramArguments} sets additional tokens. + * + *

This is similar to {@code + * CMD} in Dockerfiles or {@code args} in the Kubernetes + * Container spec. + * + *

For example, if the entrypoint was {@code myprogram --flag subcommand} and program arguments + * were {@code hello world}, then the command that run when the container starts is {@code + * myprogram --flag subcommand hello world}. + * + * @param programArguments a list of program argument tokens + * @return this + */ + public JibContainerBuilder setProgramArguments(List programArguments) { + this.programArguments = ImmutableList.copyOf(programArguments); + return this; + } + + /** + * Sets the container entrypoint program arguments. + * + * @param programArguments program arguments tokens + * @return this + * @see #setProgramArguments(List) for more details + */ + public JibContainerBuilder setProgramArguments(String... programArguments) { + return setProgramArguments(Arrays.asList(programArguments)); + } + + /** + * Sets the container environment. These environment variables are available to the program + * launched by the container entrypoint command. This replaces any previously-set environment + * variables. + * + *

This is similar to {@code + * ENV} in Dockerfiles or {@code env} in the Kubernetes + * Container spec. + * + * @param environmentMap a map of environment variable names to values + * @return this + */ + public JibContainerBuilder setEnvironment(Map environmentMap) { + environment = new HashMap<>(environmentMap); + return this; + } + + /** + * Sets a variable in the container environment. + * + * @param name the environment variable name + * @param value the environment variable value + * @return this + * @see #setEnvironment for more details + */ + public JibContainerBuilder addEnvironmentVariable(String name, String value) { + environment.put(name, value); + return this; + } + + /** + * Sets the ports to expose from the container. Ports exposed will allow ingress traffic. This + * replaces any previously-set exposed ports. + * + *

Use {@link Port#tcp} to expose a port for TCP traffic and {@link Port#udp} to expose a port + * for UDP traffic. + * + *

This is similar to {@code + * EXPOSE} in Dockerfiles or {@code ports} in the Kubernetes + * Container spec. + * + * @param ports the list of ports to expose + * @return this + */ + public JibContainerBuilder setExposedPorts(List ports) { + this.ports = new ArrayList<>(ports); + return this; + } + + /** + * Sets the ports to expose from the container. This replaces any previously-set exposed ports. + * + * @param ports the ports to expose + * @return this + * @see #setExposedPorts(List) for more details + */ + public JibContainerBuilder setExposedPorts(Port... ports) { + return setExposedPorts(Arrays.asList(ports)); + } + + /** + * Adds a port to expose from the container. + * + * @param port the port to expose + * @return this + * @see #setExposedPorts(List) for more details + */ + public JibContainerBuilder addExposedPort(Port port) { + ports.add(port); + return this; + } + + /** + * Sets the labels for the container. This replaces any previously-set labels. + * + *

This is similar to {@code + * LABEL} in Dockerfiles. + * + * @param labelMap a map of label keys to values + * @return this + */ + public JibContainerBuilder setLabels(Map labelMap) { + labels = new HashMap<>(labelMap); + return this; + } + + /** + * Sets a label for the container. + * + * @param key the label key + * @param value the label value + * @return this + */ + public JibContainerBuilder addLabel(String key, String value) { + labels.put(key, value); + return this; + } + + // TODO: Add containerize(...). +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index c7a9570be6..849e4fe1f0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -58,7 +58,7 @@ static ImmutableList makeList( buildAndCacheApplicationLayerSteps.add( new BuildAndCacheApplicationLayerStep( - layerConfiguration.getLabel(), + layerConfiguration.getName(), listeningExecutorService, buildConfiguration, layerConfiguration, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index 54a7c52ace..c309cfa8a7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -31,18 +31,18 @@ public class LayerConfiguration { public static class Builder { private final ImmutableList.Builder layerEntries = ImmutableList.builder(); - private String label = ""; + private String name = ""; private Builder() {} /** - * Sets a label for this layer. + * Sets a name for this layer. This name does not affect the contents of the layer. * - * @param label the label + * @param name the name * @return this */ - public Builder setLabel(String label) { - this.label = label; + public Builder setName(String name) { + this.name = name; return this; } @@ -101,12 +101,12 @@ public Builder addEntryRecursive(Path sourceFile, Path pathInContainer) throws I * @return the built {@link LayerConfiguration} */ public LayerConfiguration build() { - return new LayerConfiguration(label, layerEntries.build()); + return new LayerConfiguration(name, layerEntries.build()); } } /** - * Builds a {@link LayerConfiguration}. + * Gets a new {@link Builder} for {@link LayerConfiguration}. * * @return a new {@link Builder} */ @@ -115,23 +115,33 @@ public static Builder builder() { } private final ImmutableList layerEntries; - private final String label; + private final String name; /** * Use {@link #builder} to instantiate. * - * @param label an optional label for the layer + * @param name an optional name for the layer * @param layerEntries the list of {@link LayerEntry}s */ - private LayerConfiguration(String label, ImmutableList layerEntries) { - this.label = label; + private LayerConfiguration(String name, ImmutableList layerEntries) { + this.name = name; this.layerEntries = layerEntries; } - public String getLabel() { - return label; + /** + * Gets the name. + * + * @return the name + */ + public String getName() { + return name; } + /** + * Gets the list of layer entries. + * + * @return the list of layer entries + */ public ImmutableList getLayerEntries() { return layerEntries; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java index 734743bec3..f0ed4dbab0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java @@ -17,55 +17,69 @@ package com.google.cloud.tools.jib.configuration; import java.util.Objects; -import javax.annotation.Nullable; -/** Holds port number and protocol for an exposed port. */ +/** Represents a port number with a protocol (TCP or UDP). */ public class Port { - /** Represents the protocol portion of the port. */ - public enum Protocol { - TCP("tcp"), - UDP("udp"); + private static final String TCP_PROTOCOL = "tcp"; + private static final String UDP_PROTOCOL = "udp"; - private final String stringRepresentation; - - Protocol(String stringRepresentation) { - this.stringRepresentation = stringRepresentation; - } + /** + * Create a new {@link Port} with TCP protocol. + * + * @param port the port number + * @return the new {@link Port} + */ + public static Port tcp(int port) { + return new Port(port, TCP_PROTOCOL); + } - @Override - public String toString() { - return stringRepresentation; - } + /** + * Create a new {@link Port} with UDP protocol. + * + * @param port the port number + * @return the new {@link Port} + */ + public static Port udp(int port) { + return new Port(port, UDP_PROTOCOL); + } - /** - * Returns the {@link Protocol} given its string representation. - * - * @param protocolString the case insensitive string (e.g. "tcp", "udp") - * @return the {@link Protocol} with the corresponding name, or {@code Protocol#TCP} by default - * if invalid - */ - public static Protocol getFromString(@Nullable String protocolString) { - if (protocolString == null) { - return TCP; - } - return UDP.toString().equalsIgnoreCase(protocolString) ? UDP : TCP; - } + /** + * Gets a {@link Port} with protocol parsed from the string form {@code protocolString}. Unknown + * protocols will default to TCP. + * + * @param port the port number + * @param protocolString the case insensitive string (e.g. "tcp", "udp") + * @return the {@link Port} + */ + public static Port parseProtocol(int port, String protocolString) { + String protocol = UDP_PROTOCOL.equalsIgnoreCase(protocolString) ? UDP_PROTOCOL : TCP_PROTOCOL; + return new Port(port, protocol); } private final int port; - private final Protocol protocol; + private final String protocol; - public Port(int port, Protocol protocol) { + private Port(int port, String protocol) { this.port = port; this.protocol = protocol; } + /** + * Gets the port number. + * + * @return the port number + */ public int getPort() { return port; } - public Protocol getProtocol() { + /** + * Gets the protocol. + * + * @return the protocol + */ + public String getProtocol() { return protocol; } @@ -74,11 +88,11 @@ public boolean equals(Object other) { if (other == this) { return true; } - if (other == null || !(other instanceof Port)) { + if (!(other instanceof Port)) { return false; } Port otherPort = (Port) other; - return port == otherPort.port && protocol == otherPort.protocol; + return port == otherPort.port && protocol.equals(otherPort.protocol); } @Override @@ -86,6 +100,12 @@ public int hashCode() { return Objects.hash(port, protocol); } + /** + * Stringifies the port with protocol, in the form {@code /}. For example: {@code + * 1337/TCP}. + * + * @return the string form of the port with protocol + */ @Override public String toString() { return port + "/" + protocol; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java index 9294f037e9..7e56c4022b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import java.util.List; @@ -66,7 +65,7 @@ public static ImmutableList parse(List ports) throws NumberFormatE if (!Strings.isNullOrEmpty(matcher.group(2))) { max = Integer.parseInt(matcher.group(2)); } - Protocol protocol = Protocol.getFromString(matcher.group(3)); + String protocol = matcher.group(3); // Error if configured as 'max-min' instead of 'min-max' if (min > max) { @@ -81,7 +80,7 @@ public static ImmutableList parse(List ports) throws NumberFormatE } for (int portNumber = min; portNumber <= max; portNumber++) { - result.add(new Port(portNumber, protocol)); + result.add(Port.parseProtocol(portNumber, protocol)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index bc80febe0a..d3e4b2b9b6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -46,18 +46,18 @@ enum LayerType { EXPLODED_WAR("exploded war", JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE), EXTRA_FILES("extra files", "/"); - private final String label; + private final String name; private final Path extractionPath; - /** Initializes with a label for the layer and the layer files' default extraction path root. */ - LayerType(String label, String extractionPath) { - this.label = label; + /** Initializes with a name for the layer and the layer files' default extraction path root. */ + LayerType(String name, String extractionPath) { + this.name = name; this.extractionPath = Paths.get(extractionPath); } @VisibleForTesting - String getLabel() { - return label; + String getName() { + return name; } @VisibleForTesting @@ -114,7 +114,7 @@ public JavaLayerConfigurations build() throws IOException { ImmutableMap.builderWithExpectedSize(LayerType.values().length); for (LayerType layerType : LayerType.values()) { LayerConfiguration.Builder layerConfigurationBuilder = - LayerConfiguration.builder().setLabel(layerType.getLabel()); + LayerConfiguration.builder().setName(layerType.getName()); // Adds all the layer files recursively. List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 7d14fad871..b89447520a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.DigestOnlyLayer; import com.google.cloud.tools.jib.image.Image; @@ -191,7 +190,7 @@ static ImmutableList portMapToList(@Nullable Map> portMa int portNumber = Integer.parseInt(matcher.group("portNum")); String protocol = matcher.group("protocol"); - ports.add(new Port(portNumber, Protocol.getFromString(protocol))); + ports.add(Port.parseProtocol(portNumber, protocol)); } return ports.build(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 33aa887d64..31738d9ebe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.ImageReference; @@ -57,8 +56,7 @@ public void testBuilder() throws Exception { List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedJavaArguments = Arrays.asList("arg1", "arg2"); Map expectedEnvironment = ImmutableMap.of("key", "value"); - ImmutableList expectedExposedPorts = - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP)); + ImmutableList expectedExposedPorts = ImmutableList.of(Port.tcp(1000), Port.tcp(2000)); Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); Class expectedTargetFormat = OCIManifestTemplate.class; CacheConfiguration expectedApplicationLayersCacheConfiguration = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index 3e6494d28e..46d5baee41 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.HashMap; @@ -49,7 +48,7 @@ public void testBuilder_nullValues() { } // Exposed ports element should not be null. - List badPorts = Arrays.asList(new Port(1000, Protocol.TCP), null); + List badPorts = Arrays.asList(Port.tcp(1000), null); try { ContainerConfiguration.builder().setExposedPorts(badPorts); Assert.fail("The IllegalArgumentException should be thrown."); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java new file mode 100644 index 0000000000..c70fe3f9a8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Port}. */ +public class PortTest { + + @Test + public void testTcp() { + Port port = Port.tcp(5555); + Assert.assertEquals(5555, port.getPort()); + Assert.assertEquals("5555/tcp", port.toString()); + } + + @Test + public void testUdp() { + Port port = Port.udp(6666); + Assert.assertEquals(6666, port.getPort()); + Assert.assertEquals("6666/udp", port.toString()); + } + + @Test + public void testParseProtocol() { + Assert.assertEquals(Port.tcp(1111), Port.parseProtocol(1111, "tcp")); + Assert.assertEquals(Port.udp(2222), Port.parseProtocol(2222, "udp")); + Assert.assertEquals(Port.tcp(3333), Port.parseProtocol(3333, "")); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index 8837e25e93..34258099e2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collections; @@ -42,17 +41,17 @@ public void testParse() { ImmutableList expected = new ImmutableList.Builder() .add( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(2001, Protocol.TCP), - new Port(2002, Protocol.TCP), - new Port(2003, Protocol.TCP), - new Port(3000, Protocol.TCP), - new Port(4000, Protocol.TCP), - new Port(5000, Protocol.UDP), - new Port(6000, Protocol.UDP), - new Port(6001, Protocol.UDP), - new Port(6002, Protocol.UDP)) + Port.tcp(1000), + Port.tcp(2000), + Port.tcp(2001), + Port.tcp(2002), + Port.tcp(2003), + Port.tcp(3000), + Port.tcp(4000), + Port.udp(5000), + Port.udp(6000), + Port.udp(6001), + Port.udp(6002)) .build(); ImmutableList result = ExposedPortsParser.parse(goodInputs); Assert.assertEquals(expected, result); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index a53b460df5..4605510aa8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -62,11 +62,11 @@ public void testDefault() throws IOException { List expectedLabels = new ArrayList<>(); for (JavaLayerConfigurations.LayerType layerType : JavaLayerConfigurations.LayerType.values()) { - expectedLabels.add(layerType.getLabel()); + expectedLabels.add(layerType.getName()); } List actualLabels = new ArrayList<>(); for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { - actualLabels.add(layerConfiguration.getLabel()); + actualLabels.add(layerConfiguration.getName()); } Assert.assertEquals(expectedLabels, actualLabels); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index c0f4245d0c..543d068a48 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.time.Instant; @@ -53,8 +52,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) .setJavaArguments(Arrays.asList("arg1", "arg2")) - .setExposedPorts( - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP))) + .setExposedPorts(ImmutableList.of(Port.tcp(1000), Port.tcp(2000))) .addLayer(mockLayer) .build(); @@ -65,8 +63,6 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments()); - Assert.assertEquals( - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.TCP)), - image.getExposedPorts()); + Assert.assertEquals(ImmutableList.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index ea7c5d0b48..d3ec7777d1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -61,10 +60,7 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); testImageBuilder.setJavaArguments(Arrays.asList("arg1", "arg2")); testImageBuilder.setExposedPorts( - ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP))); + ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); testImageBuilder.setWorkingDirectory("/some/workspace"); @@ -118,8 +114,7 @@ public void testGetManifest_oci() throws URISyntaxException, IOException { @Test public void testPortListToMap() { - ImmutableList input = - ImmutableList.of(new Port(1000, Protocol.TCP), new Port(2000, Protocol.UDP)); + ImmutableList input = ImmutableList.of(Port.tcp(1000), Port.udp(2000)); ImmutableSortedMap> expected = ImmutableSortedMap.of("1000/tcp", ImmutableMap.of(), "2000/udp", ImmutableMap.of()); Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 50faad39b8..ffa8e036ad 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.configuration.Port.Protocol; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -89,11 +88,7 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException ImmutableMap.of(), "3000/udp", ImmutableMap.of()); - ImmutableList expected = - ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP)); + ImmutableList expected = ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)); Assert.assertEquals(expected, JsonToImageTranslator.portMapToList(input)); ImmutableList>> badInputs = @@ -187,10 +182,6 @@ private void testToImage_buildable( Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); Assert.assertEquals( - ImmutableList.of( - new Port(1000, Protocol.TCP), - new Port(2000, Protocol.TCP), - new Port(3000, Protocol.UDP)), - image.getExposedPorts()); + ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 1b32dc974c..2914794d7e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -161,7 +161,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut for (LayerConfiguration layerConfiguration : buildSteps.getBuildConfiguration().getLayerConfigurations()) { - buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getLabel()) + ":"); + buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":"); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { buildLogger.info("\t\t" + layerEntry.getSourceFile()); From 35df6375a747eefd107e3b25784896551797b5b6 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Sep 2018 15:41:59 -0400 Subject: [PATCH 0205/2020] Builds to Docker daemon with additional tags. (#983) --- .../builder/BuildStepsIntegrationTest.java | 30 +++++++++++++ .../jib/builder/steps/LoadDockerStep.java | 18 ++++++-- .../cloud/tools/jib/docker/DockerClient.java | 27 ++++++++++++ .../cloud/tools/jib/image/ImageReference.java | 10 +++++ .../tools/jib/docker/DockerClientTest.java | 44 ++++++++++++++++++- 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 0fbdc4114c..5d98266a91 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -218,6 +218,36 @@ public void testSteps_forBuildToDockerDaemon() "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); } + @Test + public void testSteps_forBuildToDockerDaemon_multipleTags() + throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, + CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + String imageReference = "testdocker"; + BuildConfiguration buildConfiguration = + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of(null, imageReference, null)) + .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) + .build(); + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + BuildSteps.forBuildToDockerDaemon( + buildConfiguration, + new Caches.Initializer(cacheDirectory, false, cacheDirectory, false)) + .run(); + + assertDockerInspect(imageReference); + Assert.assertEquals( + "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + assertDockerInspect(imageReference + ":testtag2"); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", imageReference + ":testtag2").run()); + assertDockerInspect(imageReference + ":testtag3"); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", imageReference + ":testtag3").run()); + } + @Test public void testSteps_forBuildToTarball() throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 29ab413c0f..c9b8ab6ca9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -85,13 +86,22 @@ public Void call() throws ExecutionException, InterruptedException { private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + ImageReference targetImageReference = + buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. buildConfiguration.getBuildLogger().lifecycle("Loading to Docker daemon..."); - new DockerClient() - .load( - new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage())); + DockerClient dockerClient = new DockerClient(); + dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); + + // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. + for (String tag : buildConfiguration.getAllTargetImageTags()) { + if (tag.equals(targetImageReference.getTag())) { + continue; + } + + dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); + } return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 8420aeb8ff..77c6f06019 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.docker; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.CharStreams; import java.io.IOException; @@ -114,6 +115,32 @@ public String load(Blob imageTarballBlob) throws InterruptedException, IOExcepti } } + /** + * Tags the image referenced by {@code originalImageReference} with a new image reference {@code + * newImageReference}. + * + * @param originalImageReference the existing image reference on the Docker daemon + * @param newImageReference the new image reference + * @see https://docs.docker.com/engine/reference/commandline/tag/ + * @throws InterruptedException if the 'docker tag' process is interrupted. + * @throws IOException if an I/O exception occurs or {@code docker tag} failed + */ + public void tag(ImageReference originalImageReference, ImageReference newImageReference) + throws IOException, InterruptedException { + // Runs 'docker tag'. + Process dockerProcess = + docker("tag", originalImageReference.toString(), newImageReference.toString()); + + if (dockerProcess.waitFor() != 0) { + try (InputStreamReader stderr = + new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { + throw new IOException( + "'docker tag' command failed with error: " + CharStreams.toString(stderr)); + } + } + } + /** Runs a {@code docker} command. */ private Process docker(String... subCommand) throws IOException { return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index 14407866e4..33b0208b95 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -201,6 +201,16 @@ public boolean usesDefaultTag() { return DEFAULT_TAG.equals(tag); } + /** + * Gets an {@link ImageReference} with the same registry and repository, but a different tag. + * + * @param newTag the new tag + * @return an {@link ImageReference} with the same registry/repository and the new tag + */ + public ImageReference withTag(String newTag) { + return ImageReference.of(registry, repository, newTag); + } + /** @return the image reference in Docker-readable format (inverse of {@link #parse}) */ @Override public String toString() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index cf24b94078..52a709a3a6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.docker; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -25,6 +27,8 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -66,7 +70,7 @@ public void testLoad() throws IOException, InterruptedException { DockerClient testDockerClient = new DockerClient( subcommand -> { - Assert.assertEquals("load", subcommand.get(0)); + Assert.assertEquals(Collections.singletonList("load"), subcommand); return mockProcessBuilder; }); Mockito.when(mockProcess.waitFor()).thenReturn(0); @@ -140,7 +144,7 @@ public int read() throws IOException { Assert.fail("Write should have failed"); } catch (IOException ex) { - Assert.assertTrue(expectedIOException == ex); + Assert.assertSame(expectedIOException, ex); } } @@ -161,4 +165,40 @@ public void testLoad_stdoutFail() throws InterruptedException { Assert.assertEquals("'docker load' command failed with output: failed", ex.getMessage()); } } + + @Test + public void testTag() throws InterruptedException, IOException, InvalidImageReferenceException { + DockerClient testDockerClient = + new DockerClient( + subcommand -> { + Assert.assertEquals(Arrays.asList("tag", "original", "new"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + + testDockerClient.tag(ImageReference.of(null, "original", null), ImageReference.parse("new")); + } + + @Test + public void testTag_fail() + throws InterruptedException, IOException, InvalidImageReferenceException { + DockerClient testDockerClient = + new DockerClient( + subcommand -> { + Assert.assertEquals(Arrays.asList("tag", "original", "new"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.tag(ImageReference.of(null, "original", null), ImageReference.parse("new")); + Assert.fail("docker tag should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker tag' command failed with error: error", ex.getMessage()); + } + } } From 9061b9c4d4c0fcff162f599436c4cd7ccb179800 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 13 Sep 2018 16:37:55 -0400 Subject: [PATCH 0206/2020] Add in _skaffold-files _skaffold-package-goals (#959) * Add in _skaffold-files _skaffold-package-goals Built in skaffold support goals: 1. _skaffold-files: a list of files to watch 2. _skaffold-package-goals: a list of jib goals bound to the package phase. * Fix path resolution for extraDirectory --- .../jib/maven/JibPluginConfiguration.java | 9 +- .../jib/maven/MavenProjectProperties.java | 11 +- .../tools/jib/maven/skaffold/FilesMojo.java | 154 ++++++++++++++++++ .../jib/maven/skaffold/PackageGoalsMojo.java | 80 +++++++++ .../maven/BuildImageMojoIntegrationTest.java | 2 +- .../jib/maven/JibPluginConfigurationTest.java | 3 +- .../cloud/tools/jib/maven/TestPlugin.java | 4 +- .../cloud/tools/jib/maven/TestProject.java | 9 +- .../jib/maven/skaffold/FilesMojoTest.java | 124 ++++++++++++++ .../maven/skaffold/PackageGoalsMojoTest.java | 86 ++++++++++ .../projects/multi/complex-service/pom.xml | 92 +++++++++++ .../src/main/extra-resources-1/resource1.txt | 0 .../src/main/extra-resources-2/resource2.txt | 0 .../src/main/java/com/test/HelloWorld.java | 23 +++ .../test/resources/projects/multi/lib/pom.xml | 33 ++++ .../multi/lib/src/main/java/com/lib/Lib.java | 25 +++ .../lib/src/test/java/com/lib/LibTest.java | 33 ++++ .../src/test/resources/projects/multi/pom.xml | 45 +++++ .../projects/multi/simple-service/pom.xml | 25 +++ .../src/main/java/com/test/HelloWorld.java | 23 +++ 20 files changed, 766 insertions(+), 15 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 19e0b37181..28bf0d49ff 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -21,8 +21,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import java.io.File; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -166,9 +166,10 @@ public static class ContainerParameters { @Parameter(defaultValue = "false", required = true) private boolean allowInsecureRegistries; + // this parameter is cloned in FilesMojo @Nullable @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) - private String extraDirectory; + private File extraDirectory; @Parameter(defaultValue = "false", property = "jib.skip") private boolean skip; @@ -302,7 +303,7 @@ boolean getAllowInsecureRegistries() { Path getExtraDirectory() { // TODO: Should inform user about nonexistent directory if using custom directory. - return Paths.get(Preconditions.checkNotNull(extraDirectory)); + return Preconditions.checkNotNull(extraDirectory).toPath(); } boolean isSkipped() { @@ -339,7 +340,7 @@ void setProject(MavenProject project) { } @VisibleForTesting - void setExtraDirectory(String extraDirectory) { + void setExtraDirectory(File extraDirectory) { this.extraDirectory = extraDirectory; } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 2dd3f670d6..389e68f395 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -32,14 +32,17 @@ import org.codehaus.plexus.util.xml.Xpp3Dom; /** Obtains information about a {@link MavenProject}. */ -class MavenProjectProperties implements ProjectProperties { +public class MavenProjectProperties implements ProjectProperties { + + /** Used for logging during main class inference and analysis of user configuration. */ + public static final String PLUGIN_NAME = "jib-maven-plugin"; + + /** Used to identify this plugin when interacting with the maven system. */ + public static final String PLUGIN_KEY = "com.google.cloud.tools:" + PLUGIN_NAME; /** Used to generate the User-Agent header and history metadata. */ static final String TOOL_NAME = "jib-maven-plugin"; - /** Used for logging during main class inference. */ - private static final String PLUGIN_NAME = "jib-maven-plugin"; - /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'maven-jar-plugin'"; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java new file mode 100644 index 0000000000..5148f7e648 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -0,0 +1,154 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.FileSet; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.DefaultDependencyResolutionRequest; +import org.apache.maven.project.DependencyResolutionException; +import org.apache.maven.project.DependencyResolutionResult; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectDependenciesResolver; +import org.eclipse.aether.graph.DependencyFilter; + +/** + * Print out changing source dependencies on a module. In multimodule applications it should be run + * by activating a single module and its dependent modules. Dependency collection will ignore + * project level snapshots (sub-modules) unless the user has explicitly installed them (by only + * requiring dependencyCollection). For use only within skaffold. + * + *

Expected use: "./mvnw jib:_skaffold-files -q" or "./mvnw jib:_skaffold-files -pl module -am + * -q" + */ +@Mojo( + name = FilesMojo.GOAL_NAME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class FilesMojo extends AbstractMojo { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-files"; + + @Nullable + @Parameter(defaultValue = "${session}", required = true, readonly = true) + private MavenSession session; + + @Nullable + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + + @Nullable + @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) + private List projects; + + // TODO: This is internal maven, we should find a better way to do this + @Nullable @Component private ProjectDependenciesResolver projectDependenciesResolver; + + // This parameter is cloned from JibPluginConfiguration + @Nullable + @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) + private File extraDirectory; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + Preconditions.checkNotNull(project); + Preconditions.checkNotNull(projects); + Preconditions.checkNotNull(session); + Preconditions.checkNotNull(extraDirectory); + Preconditions.checkNotNull(projectDependenciesResolver); + + // print out pom configuration files + System.out.println(project.getFile()); + if ("pom".equals(project.getPackaging())) { + // done if pom + return; + } + + // print out sources directory (resolved by maven to be an absolute path) + System.out.println(project.getBuild().getSourceDirectory()); + + // print out resources directory (resolved by maven to be an absolute path) + ImmutableSet.copyOf(project.getBuild().getResources()) + .stream() + .map(FileSet::getDirectory) + .forEach(System.out::println); + + // this seems weird, but we will only print out the jib "extraFiles" directory on projects where + // the plugin is explicitly configured (even though _skaffold-files is a jib-maven-plugin goal + // and is expected to run on all projects irrespective of their configuring of the jib plugin). + if (project.getPlugin(MavenProjectProperties.PLUGIN_KEY) != null) { + // print out extra directory + System.out.println(extraDirectory.getAbsoluteFile().toPath()); + } + + // Grab non-project SNAPSHOT dependencies for this project + // TODO: this whole sections relies on internal maven API, it could break. We need to explore + // TODO: better ways to resolve dependencies using the public maven API. + Set projectArtifacts = + projects + .stream() + .map(MavenProject::getArtifact) + .map(Artifact::toString) + .collect(Collectors.toSet()); + + DependencyFilter ignoreProjectDependenciesFilter = + (node, parents) -> { + if (node == null || node.getDependency() == null) { + // if nothing, then ignore + return false; + } + if (projectArtifacts.contains(node.getArtifact().toString())) { + // ignore project dependency artifacts + return false; + } + // we only want compile/runtime deps + return Artifact.SCOPE_COMPILE_PLUS_RUNTIME.contains(node.getDependency().getScope()); + }; + + try { + DependencyResolutionResult resolutionResult = + projectDependenciesResolver.resolve( + new DefaultDependencyResolutionRequest(project, session.getRepositorySession()) + .setResolutionFilter(ignoreProjectDependenciesFilter)); + resolutionResult + .getDependencies() + .stream() + .map(org.eclipse.aether.graph.Dependency::getArtifact) + .filter(org.eclipse.aether.artifact.Artifact::isSnapshot) + .map(org.eclipse.aether.artifact.Artifact::getFile) + .forEach(System.out::println); + + } catch (DependencyResolutionException ex) { + throw new MojoExecutionException("Failed to resolve dependencies", ex); + } + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java new file mode 100644 index 0000000000..d50daa37ba --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.lifecycle.LifecycleExecutor; +import org.apache.maven.lifecycle.MavenExecutionPlan; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * Print out all jib goals tied to the package phase. Useful in multimodule situations to determine + * if the correct jib goals are configured when running skaffold. For use only within skaffold. + * + *

It is intended to be used from the root project and only in multimodule situations: + * + *

./mvnw jib:_skaffold-package-goals -q -pl module [-Pprofile] + */ +@Mojo( + name = PackageGoalsMojo.GOAL_NAME, + requiresDirectInvocation = true, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class PackageGoalsMojo extends AbstractMojo { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-package-goals"; + + @Nullable @Component private LifecycleExecutor lifecycleExecutor; + + @Nullable + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession session; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + Preconditions.checkNotNull(lifecycleExecutor); + Preconditions.checkNotNull(session); + + MavenExecutionPlan mavenExecutionPlan; + try { + mavenExecutionPlan = lifecycleExecutor.calculateExecutionPlan(session, "package"); + } catch (Exception ex) { + throw new MojoExecutionException("failed to calculate execution plan", ex); + } + + mavenExecutionPlan + .getMojoExecutions() + .stream() + .filter(mojoExecution -> "package".equals(mojoExecution.getLifecyclePhase())) + .filter( + mojoExecution -> + MavenProjectProperties.PLUGIN_NAME.equals( + mojoExecution.getPlugin().getArtifactId())) + .map(MojoExecution::getGoal) + .forEach(System.out::println); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index f2b5931522..c8871e668f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -238,7 +238,7 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi } @Test - public void testExecute_defaultTarget() { + public void testExecute_defaultTarget() throws IOException { // Test error when 'to' is missing try { Verifier verifier = new Verifier(defaultTargetTestProject.getProjectRoot().toString()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 935f896f6d..9737986189 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; +import java.io.File; import java.nio.file.Paths; import java.util.Arrays; import org.junit.Assert; @@ -69,7 +70,7 @@ public void testHandleDeprecatedParameters() { testPluginConfiguration.setMainClass("mainClass"); testPluginConfiguration.setArgs(Arrays.asList("arg1", "arg2", "arg3")); testPluginConfiguration.setFormat("OCI"); - testPluginConfiguration.setExtraDirectory("some/path"); + testPluginConfiguration.setExtraDirectory(new File("some/path")); testPluginConfiguration.handleDeprecatedParameters(mockLogger); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java index 0455a90f51..59396ec0bd 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java @@ -28,7 +28,7 @@ import org.junit.rules.ExternalResource; /** Sets up the plugin for testing. */ -class TestPlugin extends ExternalResource { +public class TestPlugin extends ExternalResource { private String pluginVersion; @@ -42,6 +42,8 @@ protected void before() throws IOException, XmlPullParserException, Verification Verifier verifier = new Verifier(".", true); verifier.setAutoclean(false); verifier.addCliOption("-DskipTests"); + verifier.addCliOption("-Dfmt.skip"); + verifier.addCliOption("-Dcheckstyle.skip"); verifier.executeGoal("install"); // Reads the project version. diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index 232b3acbf2..c75e429dc8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -25,7 +25,7 @@ import org.junit.rules.TemporaryFolder; /** Works with the test Maven projects in the {@code resources/projects} directory. */ -class TestProject extends TemporaryFolder implements Closeable { +public class TestProject extends TemporaryFolder implements Closeable { private static final String PROJECTS_PATH_IN_RESOURCES = "/projects/"; @@ -36,7 +36,7 @@ class TestProject extends TemporaryFolder implements Closeable { private Path projectRoot; /** Initialize to a specific project directory. */ - TestProject(TestPlugin testPlugin, String projectDir) { + public TestProject(TestPlugin testPlugin, String projectDir) { this(testPlugin, projectDir, "pom.xml"); } @@ -47,8 +47,9 @@ class TestProject extends TemporaryFolder implements Closeable { this.pomFilename = pomFilename; } - Path getProjectRoot() { - return projectRoot; + /** Get the project root resolved as a real path */ + public Path getProjectRoot() throws IOException { + return projectRoot.toRealPath(); } @Override diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java new file mode 100644 index 0000000000..ecdb98df5b --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.TestPlugin; +import com.google.cloud.tools.jib.maven.TestProject; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link FilesMojo}. */ +public class FilesMojoTest { + + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + + @ClassRule + public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + + private void verifyFiles(Path projectRoot, String module, List files) + throws VerificationException, IOException { + + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-q"); + if (!Strings.isNullOrEmpty(module)) { + verifier.addCliOption("-pl"); + verifier.addCliOption(module); + verifier.addCliOption("-am"); + } + verifier.executeGoal("jib:" + FilesMojo.GOAL_NAME); + + verifier.verifyErrorFreeLog(); + Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); + List log = Files.readAllLines(logFile, StandardCharsets.UTF_8); + + List expectedResult = + files.stream().map(Path::toAbsolutePath).map(Path::toString).collect(Collectors.toList()); + + Assert.assertEquals(expectedResult, log); + } + + @Test + public void testFilesMojo_singleModule() throws VerificationException, IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + + verifyFiles( + projectRoot, + null, + ImmutableList.of( + projectRoot.resolve("pom.xml"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/jib"))); + } + + @Test + public void testFilesMojo_multiModuleSimpleService() throws VerificationException, IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path simpleServiceRoot = projectRoot.resolve("simple-service"); + + verifyFiles( + projectRoot, + "simple-service", + ImmutableList.of( + projectRoot.resolve("pom.xml"), + simpleServiceRoot.resolve("pom.xml"), + simpleServiceRoot.resolve("src/main/java"), + simpleServiceRoot.resolve("src/main/resources"), + simpleServiceRoot.resolve("src/main/jib"))); + } + + @Test + public void testFilesMojo_multiModuleComplexService() throws VerificationException, IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + + verifyFiles( + projectRoot, + "complex-service", + ImmutableList.of( + projectRoot.resolve("pom.xml"), + libRoot.resolve("pom.xml"), + libRoot.resolve("src/main/java"), + libRoot.resolve("src/main/resources"), + complexServiceRoot.resolve("pom.xml"), + complexServiceRoot.resolve("src/main/java"), + complexServiceRoot.resolve("src/main/resources1"), + complexServiceRoot.resolve("src/main/resources2"), + complexServiceRoot.resolve("src/main/other-jib"), + // this test expects standard .m2 locations + Paths.get(System.getProperty("user.home")) + .resolve( + ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar"))); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java new file mode 100644 index 0000000000..71d685b6e3 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.TestPlugin; +import com.google.cloud.tools.jib.maven.TestProject; +import com.google.common.base.Strings; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link PackageGoalsMojo}. */ +public class PackageGoalsMojoTest { + + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + + private void verifyGoals(Path projectRoot, String profilesString, String... expectedGoals) + throws VerificationException, IOException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-q"); + verifier.addCliOption("-pl"); + verifier.addCliOption("complex-service"); + verifier.addCliOption("-am"); + if (!Strings.isNullOrEmpty(profilesString)) { + verifier.addCliOption("-P" + profilesString); + } + verifier.executeGoal("jib:" + PackageGoalsMojo.GOAL_NAME); + + verifier.verifyErrorFreeLog(); + Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); + List log = Files.readAllLines(logFile, StandardCharsets.UTF_8); + + Assert.assertEquals(Arrays.asList(expectedGoals), log); + } + + @Test + public void testPackageGoalsMojo_complexServiceDefault() + throws VerificationException, IOException { + verifyGoals(multiTestProject.getProjectRoot(), null); + } + + @Test + public void testPackageGoalsMojo_complexServiceLocalProfile() + throws VerificationException, IOException { + verifyGoals(multiTestProject.getProjectRoot(), "localJib", "dockerBuild"); + } + + @Test + public void testPackageGoalsMojo_complexServiceRemoteProfile() + throws VerificationException, IOException { + verifyGoals(multiTestProject.getProjectRoot(), "remoteJib", "build"); + } + + @Test + public void testPackageGoalsMojo_complexServiceMultipleProfiles() + throws VerificationException, IOException { + verifyGoals(multiTestProject.getProjectRoot(), "localJib,remoteJib", "dockerBuild", "build"); + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml new file mode 100644 index 0000000000..eaaa5cecc6 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + service + 1.0.0.TEST-SNAPSHOT + + + com.example + multimodule + 1.0.0.TEST-SNAPSHOT + + + + + + com.google.cloud.tools + jib-maven-plugin + + src/main/other-jib + + + + + + src/main/resources1 + + + src/main/resources2 + + + + + + + localJib + + + + com.google.cloud.tools + jib-maven-plugin + + + package + dockerBuild + + + + + + + + remoteJib + + + + com.google.cloud.tools + jib-maven-plugin + + + package + build + + + + + + + + + + + com.jib.test + lib + 1.0.0.TEST-SNAPSHOT + compile + + + org.apache.commons + commons-io + 1.3.2 + compile + + + com.google.guava + guava + HEAD-jre-SNAPSHOT + compile + + + diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml new file mode 100644 index 0000000000..4f0786ce65 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml @@ -0,0 +1,33 @@ + + + + 4.0.0 + + com.jib.test + lib + 1.0.0.TEST-SNAPSHOT + lib + + + com.example + multimodule + 1.0.0.TEST-SNAPSHOT + + + + UTF-8 + 1.8 + 1.8 + + + + + junit + junit + 4.12 + test + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java b/jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java new file mode 100644 index 0000000000..0873623756 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +/** Shared Code! */ +public class Lib { + + public String getThing() { + return "thing"; + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java b/jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java new file mode 100644 index 0000000000..41fee95d40 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit test for simple App. + */ +public class LibTest { + /** Rigorous Test :-) */ + @Test + public void testGetThing() { + Assert.assertEquals("thing", new Lib().getThing()); + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/multi/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/pom.xml new file mode 100644 index 0000000000..f1a68eba56 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + + com.example + multimodule + pom + 1.0.0.TEST-SNAPSHOT + multimodule + + + + simple-service + complex-service + lib + + + + + + + com.google.cloud.tools + jib-maven-plugin + @@PluginVersion@@ + + + + + + + + snapshots-repo + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml new file mode 100644 index 0000000000..5a96aec3af --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + name-service + 1.0.0.TEST-SNAPSHOT + + + + com.example + multimodule + 1.0.0.TEST-SNAPSHOT + + + + + + com.google.cloud.tools + jib-maven-plugin + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} From 8f14bd545339e372aae61522f35b783a9e2da855 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 14 Sep 2018 12:34:54 -0400 Subject: [PATCH 0207/2020] Serializes LayerEntry in unix path form. (#976) --- .../cloud/tools/jib/cache/CacheMetadata.java | 8 ++- .../jib/cache/CacheMetadataTranslator.java | 3 +- .../cloud/tools/jib/cache/LayerMetadata.java | 25 ++++----- ...MetadataLayerPropertiesObjectTemplate.java | 6 +-- .../cloud/tools/jib/image/LayerEntry.java | 54 +++++++++++++------ .../jib/image/ReproducibleLayerBuilder.java | 2 +- .../cache/CacheMetadataTranslatorTest.java | 21 ++++---- .../cloud/tools/jib/cache/CacheTest.java | 17 +++--- .../tools/jib/cache/CacheWriterTest.java | 10 ++-- .../cache/PlatformSpecificMetadataJson.java | 41 -------------- .../cache/json/CacheMetadataTemplateTest.java | 14 +++-- .../frontend/JavaLayerConfigurationsTest.java | 12 ++--- .../src/test/resources/cache/metadata-v3.json | 2 +- .../src/test/resources/json/metadata-v3.json | 2 +- .../resources/json/metadata_windows-v3.json | 1 - 15 files changed, 105 insertions(+), 113 deletions(-) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java delete mode 100644 jib-core/src/test/resources/json/metadata_windows-v3.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java index 65f6b37c0d..535a5be1f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java @@ -85,9 +85,13 @@ static boolean doLayerEntriesMatchMetadataEntries( LayerMetadata.LayerMetadataEntry metadataEntry = metadataEntriesIterator.next(); boolean areSourceFilesEqual = - layerEntry.getSourceFileString().equals(metadataEntry.getSourceFileString()); + layerEntry + .getAbsoluteSourceFileString() + .equals(metadataEntry.getAbsoluteSourceFileString()); boolean areExtractionPathsEqual = - layerEntry.getExtractionPathString().equals(metadataEntry.getExtractionPathString()); + layerEntry + .getAbsoluteExtractionPathString() + .equals(metadataEntry.getAbsoluteExtractionPathString()); if (!areSourceFilesEqual || !areExtractionPathsEqual) { return false; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java index 6a20813617..537b8b8d4f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java @@ -107,7 +107,8 @@ static CacheMetadataTemplate toTemplate(CacheMetadata cacheMetadata) { for (LayerMetadata.LayerMetadataEntry metadataEntry : metadataEntries) { layerEntryTemplates.add( new LayerEntryTemplate( - metadataEntry.getSourceFileString(), metadataEntry.getExtractionPathString())); + metadataEntry.getAbsoluteSourceFileString(), + metadataEntry.getAbsoluteExtractionPathString())); } layerObjectTemplate.setProperties( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java index e4f7eb53f6..d629fa7d70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java @@ -30,24 +30,24 @@ class LayerMetadata { /** Entry into the layer metadata. */ static class LayerMetadataEntry { - /** The source file path string, in OS-specific format. */ - private final String sourceFileString; + /** The source file path string, in Unix form. The path should be an absolute path. */ + private final String absoluteSourceFileString; - /** The extraction path string, in OS-specific format. */ - private final String extractionPathString; + /** The extraction path string, in Unix form. The path should be an absolute path. */ + private final String absoluteExtractionPathString; - String getSourceFileString() { - return sourceFileString; + String getAbsoluteSourceFileString() { + return absoluteSourceFileString; } - String getExtractionPathString() { - return extractionPathString; + String getAbsoluteExtractionPathString() { + return absoluteExtractionPathString; } @VisibleForTesting - LayerMetadataEntry(String sourceFileString, String extractionPathString) { - this.sourceFileString = sourceFileString; - this.extractionPathString = extractionPathString; + LayerMetadataEntry(String absoluteSourceFileString, String absoluteExtractionPathString) { + this.absoluteSourceFileString = absoluteSourceFileString; + this.absoluteExtractionPathString = absoluteExtractionPathString; } } @@ -58,7 +58,8 @@ static LayerMetadata from(ImmutableList layerEntries, FileTime lastM for (LayerEntry layerEntry : layerEntries) { entries.add( new LayerMetadataEntry( - layerEntry.getSourceFileString(), layerEntry.getExtractionPathString())); + layerEntry.getAbsoluteSourceFileString(), + layerEntry.getAbsoluteExtractionPathString())); } return new LayerMetadata(entries.build(), lastModifiedTime); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java index 0e4f0d096e..f6de5a10a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java @@ -47,9 +47,9 @@ public String getExtractionPathString() { return extractionPath; } - public LayerEntryTemplate(String sourceFile, String extractionPath) { - this.sourceFile = sourceFile; - this.extractionPath = extractionPath; + public LayerEntryTemplate(String absoluteSourceFile, String absoluteExtractionPath) { + sourceFile = absoluteSourceFile; + extractionPath = absoluteExtractionPath; } /** For Jackson JSON templating. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 5587a6f62e..c28b021a70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -17,8 +17,11 @@ package com.google.cloud.tools.jib.image; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Objects; +import java.util.StringJoiner; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar @@ -34,6 +37,23 @@ */ public class LayerEntry { + /** + * Stringifies {@code path} in Unix form. The path must be absolute. + * + * @param path the path + * @return the string form of the absolute path + */ + private static String toUnixPath(Path path) { + Preconditions.checkArgument( + path.getRoot() != null, "Tried to stringify a non-absolute path: %s", path); + + StringJoiner pathJoiner = new StringJoiner("/", "/", ""); + for (Path pathComponent : path) { + pathJoiner.add(pathComponent.toString()); + } + return pathJoiner.toString(); + } + private final Path sourceFile; private final Path extractionPath; @@ -61,10 +81,10 @@ public LayerEntry(Path sourceFile, Path extractionPath) { /** * Gets the source file. * - *

Do not call {@link Path#toString} on this - use {@link #getSourceFileString} instead. - * This path can be relative or absolute, and {@link #getSourceFileString} can also be relative or - * absolute, but callers should rely on {@link #getSourceFileString} for the serialized form since - * the serialization could change independently of the path representation. + *

Do not call {@link Path#toString} on this - use {@link #getAbsoluteSourceFileString} + * instead. This path can be relative or absolute, but {@link #getAbsoluteSourceFileString} can + * only be absolute. Callers should rely on {@link #getAbsoluteSourceFileString} for the + * serialized form since the serialization could change independently of the path representation. * * @return the source file */ @@ -75,10 +95,11 @@ public Path getSourceFile() { /** * Gets the extraction path. * - *

Do not call {@link Path#toString} on this - use {@link #getExtractionPathString} - * instead. This path can be relative or absolute, and {@link #getExtractionPathString} can also - * be relative or absolute, but callers should rely on {@link #getExtractionPathString} for the - * serialized form since the serialization could change independently of the path representation. + *

Do not call {@link Path#toString} on this - use {@link + * #getAbsoluteExtractionPathString} instead. This path can be relative or absolute, but {@link + * #getAbsoluteExtractionPathString} can only be absolute. Callers should rely on {@link + * #getAbsoluteExtractionPathString} for the serialized form since the serialization could change + * independently of the path representation. * * @return the extraction path */ @@ -87,22 +108,23 @@ public Path getExtractionPath() { } /** - * Gets the source file path in string form. + * Get the source file as an absolute path in Unix form. The path is made absolute first, if not + * already absolute. * * @return the source file path */ - public String getSourceFileString() { - return sourceFile.toString(); + public String getAbsoluteSourceFileString() { + return toUnixPath(sourceFile.toAbsolutePath()); } /** - * Gets the extraction path in string form. This does not convert the extraction path to an - * absolute path. + * Gets the extraction path as an absolute path in Unix form. The path is made absolute first, if + * not already absolute. * * @return the extraction path */ - public String getExtractionPathString() { - return extractionPath.toString(); + public String getAbsoluteExtractionPathString() { + return toUnixPath(Paths.get("/").resolve(extractionPath)); } @Override @@ -126,6 +148,6 @@ public int hashCode() { @Override @VisibleForTesting public String toString() { - return getSourceFileString() + "\t" + getExtractionPathString(); + return getAbsoluteSourceFileString() + "\t" + getAbsoluteExtractionPathString(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 5eeca54cdc..505228f739 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -101,7 +101,7 @@ public UnwrittenLayer build() { // adds parent directories for each extraction path. uniqueTarArchiveEntries.add( new TarArchiveEntry( - layerEntry.getSourceFile().toFile(), layerEntry.getExtractionPathString())); + layerEntry.getSourceFile().toFile(), layerEntry.getAbsoluteExtractionPathString())); } // Gets the entries sorted by extraction path. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java index 0f23ba9047..3b97aa863e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -43,8 +44,8 @@ @RunWith(MockitoJUnitRunner.class) public class CacheMetadataTranslatorTest { - private static final Path CLASSES_LAYER_SOURCE_FILE = Paths.get("/some/source/path"); - private static final Path CLASSES_LAYER_EXTRACTION_PATH = Paths.get("some/extraction/path"); + private static final String CLASSES_LAYER_SOURCE_FILE = "/some/source/path"; + private static final String CLASSES_LAYER_EXTRACTION_PATH = "/some/extraction/path"; private static final FileTime CLASSES_LAYER_LAST_MODIFIED_TIME = FileTime.fromMillis(255073580723571L); @@ -81,7 +82,7 @@ public void testFromTemplate() Path fakePath = Paths.get("fake/path"); // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); // Deserializes into a metadata JSON object. CacheMetadataTemplate metadataTemplate = @@ -108,18 +109,18 @@ public void testFromTemplate() Assert.assertEquals(classesLayerDiffId, classesLayer.getDiffId()); Assert.assertNotNull(classesLayer.getMetadata()); Assert.assertEquals( - CLASSES_LAYER_SOURCE_FILE.toString(), - classesLayer.getMetadata().getEntries().get(0).getSourceFileString()); + CLASSES_LAYER_SOURCE_FILE, + classesLayer.getMetadata().getEntries().get(0).getAbsoluteSourceFileString()); Assert.assertEquals( - CLASSES_LAYER_EXTRACTION_PATH.toString(), - classesLayer.getMetadata().getEntries().get(0).getExtractionPathString()); + CLASSES_LAYER_EXTRACTION_PATH, + classesLayer.getMetadata().getEntries().get(0).getAbsoluteExtractionPathString()); Assert.assertEquals( CLASSES_LAYER_LAST_MODIFIED_TIME, classesLayer.getMetadata().getLastModifiedTime()); } @Test public void testToTemplate() throws URISyntaxException, IOException { - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); CachedLayer baseCachedLayer = @@ -131,7 +132,9 @@ public void testToTemplate() throws URISyntaxException, IOException { LayerMetadata classesLayerMetadata = LayerMetadata.from( ImmutableList.of( - new LayerEntry(CLASSES_LAYER_SOURCE_FILE, CLASSES_LAYER_EXTRACTION_PATH)), + new LayerEntry( + Paths.get(CLASSES_LAYER_SOURCE_FILE), + Paths.get(CLASSES_LAYER_EXTRACTION_PATH))), CLASSES_LAYER_LAST_MODIFIED_TIME); CachedLayerWithMetadata classesLayer = new CachedLayerWithMetadata(classesCachedLayer, classesLayerMetadata); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 6bf5b84caa..44cf25e142 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -70,7 +71,8 @@ public void testInit_withMetadata() throws URISyntaxException, IOException, CacheMetadataCorruptedException { Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - Path resourceMetadataJsonPath = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path resourceMetadataJsonPath = + Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); @@ -87,7 +89,8 @@ public void test_saveMetadata_noDuplicates() throws IOException, CacheMetadataCorruptedException, DigestException, URISyntaxException { Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - Path resourceMetadataJsonPath = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path resourceMetadataJsonPath = + Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); @@ -158,19 +161,21 @@ public void test_saveMetadata_noDuplicates() Assert.assertEquals(2, layerMetadata.getEntries().size()); Assert.assertEquals(FileTime.from(Instant.EPOCH), layerMetadata.getLastModifiedTime()); Assert.assertEquals( - ImmutableList.of(layerEntry3.getSourceFileString(), layerEntry4.getSourceFileString()), + ImmutableList.of( + layerEntry3.getAbsoluteSourceFileString(), layerEntry4.getAbsoluteSourceFileString()), layerMetadata .getEntries() .stream() - .map(LayerMetadataEntry::getSourceFileString) + .map(LayerMetadataEntry::getAbsoluteSourceFileString) .collect(ImmutableList.toImmutableList())); Assert.assertEquals( ImmutableList.of( - layerEntry3.getExtractionPathString(), layerEntry4.getExtractionPathString()), + layerEntry3.getAbsoluteExtractionPathString(), + layerEntry4.getAbsoluteExtractionPathString()), layerMetadata .getEntries() .stream() - .map(LayerMetadataEntry::getExtractionPathString) + .map(LayerMetadataEntry::getAbsoluteExtractionPathString) .collect(ImmutableList.toImmutableList())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java index 83d2452467..c98faf43c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java @@ -94,7 +94,8 @@ public void testWriteLayer_unwritten() throws IOException { Mockito.when(mockReproducibleLayerBuilder.getLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("some/source/file"), Paths.get("/some/extraction/path")))); + new LayerEntry( + Paths.get("/some/source/file"), Paths.get("/some/extraction/path")))); CachedLayerWithMetadata cachedLayerWithMetadata = cacheWriter.writeLayer(mockReproducibleLayerBuilder); @@ -105,11 +106,10 @@ public void testWriteLayer_unwritten() throws IOException { Assert.assertNotNull(layerMetadata); Assert.assertEquals(1, layerMetadata.getEntries().size()); Assert.assertEquals( - Paths.get("some/source/file").toString(), - layerMetadata.getEntries().get(0).getSourceFileString()); + "/some/source/file", layerMetadata.getEntries().get(0).getAbsoluteSourceFileString()); Assert.assertEquals( - Paths.get("/some/extraction/path").toString(), - layerMetadata.getEntries().get(0).getExtractionPathString()); + "/some/extraction/path", + layerMetadata.getEntries().get(0).getAbsoluteExtractionPathString()); verifyCachedLayerIsExpected(getExpectedLayer(), cachedLayerWithMetadata); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java deleted file mode 100644 index f1f9ce68b5..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/PlatformSpecificMetadataJson.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.common.io.Resources; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** Test utility for using platform-specific test resources. */ -public class PlatformSpecificMetadataJson { - - /** - * Retrieves a sample metadata JSON file. A separate version for Windows is used to account for - * the different path separator. - */ - public static Path getMetadataJsonFile() throws URISyntaxException { - String metadataResourceFilename = "json/metadata-v3.json"; - if (System.getProperty("os.name").toLowerCase().contains("win")) { - metadataResourceFilename = "json/metadata_windows-v3.json"; - } - // Loads the expected JSON string. - return Paths.get(Resources.getResource(metadataResourceFilename).toURI()); - } - - private PlatformSpecificMetadataJson() {} -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java index 97f49788f7..6458b1962d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.cache.json; -import com.google.cloud.tools.jib.cache.PlatformSpecificMetadataJson; import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -40,7 +40,7 @@ public class CacheMetadataTemplateTest { @Test public void testToJson() throws URISyntaxException, IOException, DigestException { // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); CacheMetadataTemplate cacheMetadataTemplate = new CacheMetadataTemplate(); @@ -61,9 +61,7 @@ public void testToJson() throws URISyntaxException, IOException, DigestException new CacheMetadataLayerPropertiesObjectTemplate() .setLayerEntries( Collections.singletonList( - new LayerEntryTemplate( - Paths.get("/some/source/path").toString(), - Paths.get("some/extraction/path").toString()))) + new LayerEntryTemplate("/some/source/path", "/some/extraction/path"))) .setLastModifiedTime(FileTime.fromMillis(255073580723571L)); CacheMetadataLayerObjectTemplate classesLayerTemplate = new CacheMetadataLayerObjectTemplate() @@ -88,7 +86,7 @@ public void testToJson() throws URISyntaxException, IOException, DigestException @Test public void testFromJson() throws URISyntaxException, IOException, DigestException { // Loads the expected JSON string. - Path jsonFile = PlatformSpecificMetadataJson.getMetadataJsonFile(); + Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); // Deserializes into a metadata JSON object. CacheMetadataTemplate metadataTemplate = @@ -125,10 +123,10 @@ public void testFromJson() throws URISyntaxException, IOException, DigestExcepti Assert.assertNotNull(classesLayerTemplate.getProperties()); Assert.assertEquals(1, classesLayerTemplate.getProperties().getLayerEntries().size()); Assert.assertEquals( - Paths.get("/some/source/path").toString(), + "/some/source/path", classesLayerTemplate.getProperties().getLayerEntries().get(0).getSourceFileString()); Assert.assertEquals( - Paths.get("some/extraction/path").toString(), + "/some/extraction/path", classesLayerTemplate.getProperties().getLayerEntries().get(0).getExtractionPathString()); Assert.assertEquals( FileTime.fromMillis(255073580723571L), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 4605510aa8..fd097e727f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -73,13 +73,13 @@ public void testDefault() throws IOException { @Test public void testSetFiles() throws IOException { - List dependencyFiles = Collections.singletonList(Paths.get("dependency")); + List dependencyFiles = Collections.singletonList(Paths.get("/dependency")); List snapshotDependencyFiles = - Collections.singletonList(Paths.get("snapshot dependency")); - List resourceFiles = Collections.singletonList(Paths.get("resource")); - List classFiles = Collections.singletonList(Paths.get("class")); - List explodedWarFiles = Collections.singletonList(Paths.get("exploded war")); - List extraFiles = Collections.singletonList(Paths.get("extra file")); + Collections.singletonList(Paths.get("/snapshot dependency")); + List resourceFiles = Collections.singletonList(Paths.get("/resource")); + List classFiles = Collections.singletonList(Paths.get("/class")); + List explodedWarFiles = Collections.singletonList(Paths.get("/exploded war")); + List extraFiles = Collections.singletonList(Paths.get("/extra file")); JavaLayerConfigurations javaLayerConfigurations = JavaLayerConfigurations.builder() diff --git a/jib-core/src/test/resources/cache/metadata-v3.json b/jib-core/src/test/resources/cache/metadata-v3.json index 1b6d8ef370..03985f307c 100644 --- a/jib-core/src/test/resources/cache/metadata-v3.json +++ b/jib-core/src/test/resources/cache/metadata-v3.json @@ -24,7 +24,7 @@ "layerEntries": [ { "sourceFile": "/some/source/path", - "extractionPath": "some/extraction/path" + "extractionPath": "/some/extraction/path" } ], "lastModifiedTime": 1000 diff --git a/jib-core/src/test/resources/json/metadata-v3.json b/jib-core/src/test/resources/json/metadata-v3.json index d2ea276ca5..84411ba067 100644 --- a/jib-core/src/test/resources/json/metadata-v3.json +++ b/jib-core/src/test/resources/json/metadata-v3.json @@ -1 +1 @@ -{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"/some/source/path","extractionPath":"some/extraction/path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file +{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"/some/source/path","extractionPath":"/some/extraction/path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/metadata_windows-v3.json b/jib-core/src/test/resources/json/metadata_windows-v3.json deleted file mode 100644 index 234f3e94da..0000000000 --- a/jib-core/src/test/resources/json/metadata_windows-v3.json +++ /dev/null @@ -1 +0,0 @@ -{"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFile":"\\some\\source\\path","extractionPath":"some\\extraction\\path"}],"lastModifiedTime":255073580723571}}]} \ No newline at end of file From 2419a62b5f73d6357742aae800a369168ae493db Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 14 Sep 2018 12:51:07 -0400 Subject: [PATCH 0208/2020] List cached layer digests as Set (#986) * List digests as Set * listDigests --> fetchDigests --- .../com/google/cloud/tools/jib/ncache/CacheStorage.java | 8 ++++---- .../cloud/tools/jib/ncache/DefaultCacheStorage.java | 6 +++--- .../cloud/tools/jib/ncache/DefaultCacheStorageReader.java | 7 ++++--- .../tools/jib/ncache/DefaultCacheStorageReaderTest.java | 6 ++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 823790f82e..c4e9a4d065 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; -import java.util.List; import java.util.Optional; +import java.util.Set; /** * Interface for queries to a cache storage engine. @@ -45,13 +45,13 @@ public interface CacheStorage { CacheEntry write(CacheWrite cacheWrite) throws IOException; /** - * Lists all the layer digests stored. + * Fetches all the layer digests stored. * - * @return the list of layer digests (that can be retrieved via {@link #retrieve}) + * @return the set of layer digests (that can be retrieved via {@link #retrieve}) * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - List listDigests() throws IOException, CacheCorruptedException; + Set fetchDigests() throws IOException, CacheCorruptedException; /** * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java index 68a6eb0a3b..4e8a6bc31f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Path; -import java.util.List; import java.util.Optional; +import java.util.Set; /** * Default implementation of {@link CacheStorage}. This storage engine stores cache entries in a @@ -73,8 +73,8 @@ public CacheEntry write(CacheWrite cacheWrite) throws IOException { } @Override - public List listDigests() throws IOException, CacheCorruptedException { - return defaultCacheStorageReader.listDigests(); + public Set fetchDigests() throws IOException, CacheCorruptedException { + return defaultCacheStorageReader.fetchDigests(); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java index 609799d721..f67590675c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java @@ -22,9 +22,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.DigestException; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,11 +45,11 @@ class DefaultCacheStorageReader { * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - List listDigests() throws IOException, CacheCorruptedException { + Set fetchDigests() throws IOException, CacheCorruptedException { try (Stream layerDirectories = Files.list(defaultCacheStorageFiles.getLayersDirectory())) { List layerDirectoriesList = layerDirectories.collect(Collectors.toList()); - List layerDigests = new ArrayList<>(layerDirectoriesList.size()); + Set layerDigests = new HashSet<>(layerDirectoriesList.size()); for (Path layerDirectory : layerDirectoriesList) { try { layerDigests.add(DescriptorDigest.fromHash(layerDirectory.getFileName().toString())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java index 0c08d0aaab..3ac33cf779 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.security.DigestException; import java.util.Arrays; +import java.util.HashSet; import java.util.Optional; import org.hamcrest.CoreMatchers; import org.junit.Assert; @@ -65,12 +66,13 @@ public void testListDigests() throws IOException, CacheCorruptedException { // Checks that layer directories created are all listed. Assert.assertEquals( - Arrays.asList(layerDigest1, layerDigest2), defaultCacheStorageReader.listDigests()); + new HashSet<>(Arrays.asList(layerDigest1, layerDigest2)), + defaultCacheStorageReader.fetchDigests()); // Checks that non-digest directories means the cache is corrupted. Files.createDirectory(defaultCacheStorageFiles.getLayersDirectory().resolve("not a hash")); try { - defaultCacheStorageReader.listDigests(); + defaultCacheStorageReader.fetchDigests(); Assert.fail("Listing digests should have failed"); } catch (CacheCorruptedException ex) { From 536488033c4c7f692de57356b09c4818128cd98a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 14 Sep 2018 13:50:01 -0400 Subject: [PATCH 0209/2020] Logs additional tags. (#982) --- .../cloud/tools/jib/builder/BuildSteps.java | 57 ++------------ .../cloud/tools/jib/image/ImageReference.java | 75 +++++++++++++++++-- .../jib/plugins/common/BuildStepsRunner.java | 60 +++++++++++++-- .../plugins/common/BuildStepsRunnerTest.java | 2 +- 4 files changed, 129 insertions(+), 65 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index d57612f27c..c02263352f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -31,6 +31,10 @@ /** Steps for building an image. */ public class BuildSteps { + private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; + private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; + private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; + /** Accepts {@link StepsRunner} by running the appropriate steps. */ @FunctionalInterface private interface StepsRunnerConsumer { @@ -38,27 +42,6 @@ private interface StepsRunnerConsumer { void accept(StepsRunner stepsRunner) throws ExecutionException, InterruptedException; } - private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; - private static final String STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY = - "Containerizing application to %s..."; - // String parameter (target image reference) in cyan. - private static final String SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY = - "Built and pushed image as \u001B[36m%s\u001B[0m"; - - private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; - private static final String STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON = - "Containerizing application to Docker daemon as %s..."; - // String parameter (target image reference) in cyan. - private static final String SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON = - "Built image to Docker daemon as \u001B[36m%s\u001B[0m"; - - private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; - private static final String STARTUP_MESSAGE_FORMAT_FOR_TARBALL = - "Containerizing application to file at '%s'..."; - // String parameter (target file) in cyan. - private static final String SUCCESS_MESSAGE_FORMAT_FOR_TARBALL = - "Built image tarball at \u001B[36m%s\u001B[0m"; - /** * All the steps to build an image to a Docker registry. * @@ -72,12 +55,6 @@ public static BuildSteps forBuildToDockerRegistry( DESCRIPTION_FOR_DOCKER_REGISTRY, buildConfiguration, cachesInitializer, - String.format( - STARTUP_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageConfiguration().getImage()), - String.format( - SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_REGISTRY, - buildConfiguration.getTargetImageConfiguration().getImage()), stepsRunner -> stepsRunner .runRetrieveTargetRegistryCredentialsStep() @@ -107,12 +84,6 @@ public static BuildSteps forBuildToDockerDaemon( DESCRIPTION_FOR_DOCKER_DAEMON, buildConfiguration, cachesInitializer, - String.format( - STARTUP_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, - buildConfiguration.getTargetImageConfiguration().getImage()), - String.format( - SUCCESS_MESSAGE_FORMAT_FOR_DOCKER_DAEMON, - buildConfiguration.getTargetImageConfiguration().getImage()), stepsRunner -> stepsRunner .runPullBaseImageStep() @@ -140,8 +111,6 @@ public static BuildSteps forBuildToTar( DESCRIPTION_FOR_TARBALL, buildConfiguration, cachesInitializer, - String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), - String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), stepsRunner -> stepsRunner .runPullBaseImageStep() @@ -156,28 +125,20 @@ public static BuildSteps forBuildToTar( private final String description; private final BuildConfiguration buildConfiguration; private final Caches.Initializer cachesInitializer; - private final String startupMessage; - private final String successMessage; private final StepsRunnerConsumer stepsRunnerConsumer; /** * @param description a description of what the steps do - * @param startupMessage shown when the steps start running - * @param successMessage shown when the steps finish successfully * @param stepsRunnerConsumer accepts a {@link StepsRunner} by running the necessary steps */ private BuildSteps( String description, BuildConfiguration buildConfiguration, Caches.Initializer cachesInitializer, - String startupMessage, - String successMessage, StepsRunnerConsumer stepsRunnerConsumer) { this.description = description; this.buildConfiguration = buildConfiguration; this.cachesInitializer = cachesInitializer; - this.startupMessage = startupMessage; - this.successMessage = successMessage; this.stepsRunnerConsumer = stepsRunnerConsumer; } @@ -185,20 +146,12 @@ public BuildConfiguration getBuildConfiguration() { return buildConfiguration; } - public String getStartupMessage() { - return startupMessage; - } - - public String getSuccessMessage() { - return successMessage; - } - public void run() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { buildConfiguration.getBuildLogger().lifecycle(""); - try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), description)) { + try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { try (Caches caches = cachesInitializer.init()) { Cache baseImageLayersCache = caches.getBaseCache(); Cache applicationLayersCache = caches.getApplicationCache(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index 33b0208b95..a3e71f8fc4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -75,6 +75,16 @@ public class ImageReference { private static final Pattern REFERENCE_PATTERN = Pattern.compile(REFERENCE_REGEX); /** + * Parses a string {@code reference} into an {@link ImageReference}. + * + *

Image references should generally be in the form: {@code /:} For + * example, an image reference could be {@code gcr.io/distroless/java:debug}. + * + *

See https://docs.docker.com/engine/reference/commandline/tag/#extended-description + * for a description of valid image reference format. Note, however, that the image reference is + * referred confusingly as {@code tag} on that page. + * * @param reference the string to parse * @return an {@link ImageReference} parsed from the string * @throws InvalidImageReferenceException if {@code reference} is formatted incorrectly @@ -134,9 +144,12 @@ public static ImageReference parse(String reference) throws InvalidImageReferenc } /** - * @param registry the image registry + * Constructs an {@link ImageReference} from the image reference components, consisting of an + * optional registry, a repository, and an optional tag. + * + * @param registry the image registry, or {@code null} to use the default registry (Docker Hub) * @param repository the image repository - * @param tag the image tag + * @param tag the image tag, or {@code null} to use the default tag ({@code latest}) * @return an {@link ImageReference} built from the given registry, repository, and tag */ public static ImageReference of( @@ -151,6 +164,9 @@ public static ImageReference of( } /** + * Returns {@code true} if {@code registry} is a valid registry string. For example, a valid + * registry could be {@code gcr.io} or {@code localhost:5000}. + * * @param registry the registry to check * @return {@code true} if is a valid registry; {@code false} otherwise */ @@ -159,6 +175,9 @@ public static boolean isValidRegistry(String registry) { } /** + * Returns {@code true} if {@code repository} is a valid repository string. For example, a valid + * repository could be {@code distroless} or {@code my/container-image/repository}. + * * @param repository the repository to check * @return {@code true} if is a valid repository; {@code false} otherwise */ @@ -167,6 +186,9 @@ public static boolean isValidRepository(String repository) { } /** + * Returns {@code true} if {@code tag} is a valid tag string. For example, a valid tag could be + * {@code v120.5-release}. + * * @param tag the tag to check * @return {@code true} if is a valid tag; {@code false} otherwise */ @@ -174,31 +196,64 @@ public static boolean isValidTag(String tag) { return tag.matches(TAG_REGEX); } + /** + * Returns {@code true} if {@code tag} is the default tag ((@code latest} or empty); {@code false} + * if not. + * + * @param tag the tag to check + * @return {@code true} if {@code tag} is the default tag ((@code latest} or empty); {@code false} + * if not + */ + public static boolean isDefaultTag(String tag) { + return tag.isEmpty() || DEFAULT_TAG.equals(tag); + } + private final String registry; private final String repository; private final String tag; - /** Use {@link #parse} to construct. */ + /** Construct with {@link #parse}. */ private ImageReference(String registry, String repository, String tag) { this.registry = registry; this.repository = repository; this.tag = tag; } + /** + * Gets the registry portion of the {@link ImageReference}. + * + * @return the registry + */ public String getRegistry() { return registry; } + /** + * Gets the repository portion of the {@link ImageReference}. + * + * @return the repository + */ public String getRepository() { return repository; } + /** + * Gets the tag portion of the {@link ImageReference}. + * + * @return the tag + */ public String getTag() { return tag; } + /** + * Returns {@code true} if the {@link ImageReference} uses the default tag ((@code latest} or + * empty); {@code false} if not + * + * @return {@code true} if uses the default tag; {@code false} if not + */ public boolean usesDefaultTag() { - return DEFAULT_TAG.equals(tag); + return isDefaultTag(tag); } /** @@ -211,7 +266,11 @@ public ImageReference withTag(String newTag) { return ImageReference.of(registry, repository, newTag); } - /** @return the image reference in Docker-readable format (inverse of {@link #parse}) */ + /** + * Stringifies the {@link ImageReference}. + * + * @return the image reference in Docker-readable format (inverse of {@link #parse}) + */ @Override public String toString() { StringBuilder referenceString = new StringBuilder(); @@ -237,7 +296,11 @@ public String toString() { return referenceString.toString(); } - /** @return the image reference in Docker-readable format, without hiding the tag. */ + /** + * Stringifies the {@link ImageReference}, without hiding the tag. + * + * @return the image reference in Docker-readable format, without hiding the tag + */ public String toStringWithTag() { return this + (usesDefaultTag() ? ":" + DEFAULT_TAG : ""); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 2914794d7e..9ccd05ac9e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; @@ -38,12 +39,45 @@ import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.Path; +import java.util.StringJoiner; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; /** Runs a {@link BuildSteps} and builds helpful error messages. */ public class BuildStepsRunner { + private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = + "Containerizing application to "; + private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = + "Built and pushed image as "; + + private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = + "Containerizing application to Docker daemon as "; + private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = + "Built image to Docker daemon as "; + + private static final String STARTUP_MESSAGE_FORMAT_FOR_TARBALL = + "Containerizing application to file at '%s'..."; + private static final String SUCCESS_MESSAGE_FORMAT_FOR_TARBALL = + "Built image tarball at \u001B[36m%s\u001B[0m"; + + private static CharSequence colorCyan(CharSequence innerText) { + return new StringBuilder().append("\u001B[36m").append(innerText).append("\u001B[0m"); + } + + private static String buildMessageWithTargetImageReferences( + BuildConfiguration buildConfiguration, String prefix, String suffix) { + String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + String targetRepository = buildConfiguration.getTargetImageConfiguration().getImageRepository(); + + StringJoiner successMessageBuilder = new StringJoiner(", ", prefix, suffix); + for (String tag : buildConfiguration.getAllTargetImageTags()) { + successMessageBuilder.add( + colorCyan(ImageReference.of(targetRegistry, targetRepository, tag).toString())); + } + return successMessageBuilder.toString(); + } + /** * Creates a runner to build an image. Creates a directory for the cache, if needed. * @@ -55,7 +89,11 @@ public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfigurati throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToDockerRegistry( - buildConfiguration, getCacheInitializer(buildConfiguration))); + buildConfiguration, getCacheInitializer(buildConfiguration)), + buildMessageWithTargetImageReferences( + buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "..."), + buildMessageWithTargetImageReferences( + buildConfiguration, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "")); } /** @@ -69,7 +107,11 @@ public static BuildStepsRunner forBuildToDockerDaemon(BuildConfiguration buildCo throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToDockerDaemon( - buildConfiguration, getCacheInitializer(buildConfiguration))); + buildConfiguration, getCacheInitializer(buildConfiguration)), + buildMessageWithTargetImageReferences( + buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), + buildMessageWithTargetImageReferences( + buildConfiguration, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "")); } /** @@ -84,7 +126,9 @@ public static BuildStepsRunner forBuildTar(Path outputPath, BuildConfiguration b throws CacheDirectoryCreationException { return new BuildStepsRunner( BuildSteps.forBuildToTar( - outputPath, buildConfiguration, getCacheInitializer(buildConfiguration))); + outputPath, buildConfiguration, getCacheInitializer(buildConfiguration)), + String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), + String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); } // TODO: Move this up to somewhere where defaults for cache location are provided and ownership is @@ -136,10 +180,14 @@ private static String capitalizeFirstLetter(String string) { } private final BuildSteps buildSteps; + private final String startupMessage; + private final String successMessage; @VisibleForTesting - BuildStepsRunner(BuildSteps buildSteps) { + BuildStepsRunner(BuildSteps buildSteps, String startupMessage, String successMessage) { this.buildSteps = buildSteps; + this.startupMessage = startupMessage; + this.successMessage = successMessage; } /** @@ -154,7 +202,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut JibLogger buildLogger = buildSteps.getBuildConfiguration().getBuildLogger(); buildLogger.lifecycle(""); - buildLogger.lifecycle(buildSteps.getStartupMessage()); + buildLogger.lifecycle(startupMessage); // Logs the different source files used. buildLogger.info("Containerizing application with the following files:"); @@ -171,7 +219,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut buildSteps.run(); buildLogger.lifecycle(""); - buildLogger.lifecycle(buildSteps.getSuccessMessage()); + buildLogger.lifecycle(successMessage); } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { throw new BuildStepsExecutionException( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index d96f7297ad..58ccc86995 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -81,7 +81,7 @@ public class BuildStepsRunnerTest { @Before public void setUpMocks() { - testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps); + testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps, "ignored", "ignored"); Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); From 95e374ea46cd9f56c530dd25cd78395ea31fd9b7 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 14 Sep 2018 20:50:25 -0600 Subject: [PATCH 0210/2020] Fixed incorrect reference to Gradle (#989) --- jib-maven-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 9ffe79d5a7..4ab1a81d87 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -202,7 +202,7 @@ Field | Type | Default | Description `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. -`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Gradle projects. +`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `from` is an object with the following properties: From 3728d3ea6b055c91f26ebee5aacc6445ed41f780 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 17 Sep 2018 15:08:47 -0400 Subject: [PATCH 0211/2020] Adds RegistryImage, DockerDaemonImage, and TarImage. (#952) --- .../tools/jib/api/DockerDaemonImage.java | 79 +++++++++++ .../cloud/tools/jib/api/RegistryImage.java | 124 ++++++++++++++++++ .../google/cloud/tools/jib/api/TarImage.java | 95 ++++++++++++++ .../RetrieveRegistryCredentialsStep.java | 7 +- .../configuration/credentials/Credential.java | 23 +++- .../credentials/CredentialRetriever.java | 15 +-- .../frontend/CredentialRetrieverFactory.java | 24 ++-- .../DockerConfigCredentialRetriever.java | 20 ++- .../credentials/DockerCredentialHelper.java | 2 +- .../RetrieveRegistryCredentialsStepTest.java | 16 ++- .../configuration/BuildConfigurationTest.java | 8 +- .../credentials/CredentialTest.java | 8 +- .../CredentialRetrieverFactoryTest.java | 22 +++- .../DockerConfigCredentialRetrieverTest.java | 51 ++++--- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 13 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../gradle/PluginConfigurationProcessor.java | 23 ++-- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 25 ++-- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../maven/MavenSettingsServerCredentials.java | 15 ++- .../maven/PluginConfigurationProcessor.java | 33 +++-- .../MavenSettingsServerCredentialsTest.java | 26 ++-- .../ConfigurationPropertyValidator.java | 16 +-- .../ConfigurationPropertyValidatorTest.java | 21 +-- .../DefaultCredentialRetrieversTest.java | 4 +- 27 files changed, 519 insertions(+), 159 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java new file mode 100644 index 0000000000..09a370b0df --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** Builds to the Docker daemon. */ +// TODO: Add tests once JibContainerBuilder#containerize() is added. +public class DockerDaemonImage { + + /** + * Instantiate with the image reference to tag the built image with. This is the name that shows + * up on the Docker daemon. + * + * @param imageReference the image reference + * @return a new {@link DockerDaemonImage} + */ + public static DockerDaemonImage named(ImageReference imageReference) { + return new DockerDaemonImage(imageReference); + } + + /** + * Instantiate with the image reference to tag the built image with. This is the name that shows + * up on the Docker daemon. + * + * @param imageReference the image reference + * @return a new {@link DockerDaemonImage} + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public static DockerDaemonImage named(String imageReference) + throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + private final ImageReference imageReference; + private Path dockerExecutable = Paths.get("docker"); + + /** Instantiate with {@link #named}. */ + private DockerDaemonImage(ImageReference imageReference) { + this.imageReference = imageReference; + } + + /** + * Sets the path to the {@code docker} CLI. This is {@code docker} by default. + * + * @param dockerExecutable the path to the {@code docker} CLI + * @return this + */ + public DockerDaemonImage setDockerExecutable(Path dockerExecutable) { + this.dockerExecutable = dockerExecutable; + return this; + } + + ImageReference getImageReference() { + return imageReference; + } + + Path getDockerExecutable() { + return dockerExecutable; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java new file mode 100644 index 0000000000..b81bc3f4c2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Builds to a container registry. + * + *

The registry portion of the image reference determines which registry to push the image to. + * The repository portion is the namespace to push the image to. The tag is a label to easily + * identify an image among all the images in the repository. See {@link ImageReference} for more + * details. + * + *

When configuring credentials (via {@link #addCredential} for example), make sure the + * credentials are valid push credentials for the repository specified via the image reference. + */ +// TODO: Add tests once JibContainerBuilder#containerize() is added. +public class RegistryImage { + + /** + * Instantiate with the image reference to push to. + * + * @param imageReference the image reference + * @return a new {@link RegistryImage} + */ + public static RegistryImage named(ImageReference imageReference) { + return new RegistryImage(imageReference); + } + + /** + * Instantiate with the image reference to push to. + * + * @param imageReference the image reference + * @return a new {@link RegistryImage} + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public static RegistryImage named(String imageReference) throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + private final ImageReference imageReference; + private List credentialRetrievers = new ArrayList<>(); + + /** Instantiate with {@link #named}. */ + private RegistryImage(ImageReference imageReference) { + this.imageReference = imageReference; + } + + /** + * Adds a username-password credential to use to push the image. This is a shorthand for {@code + * addCredentialRetriever(() -> Optional.of(Credential.basic(username, password)))}. + * + * @param username the username + * @param password the password + * @return this + */ + public RegistryImage addCredential(String username, String password) { + addCredentialRetriever(() -> Optional.of(Credential.basic(username, password))); + return this; + } + + /** + * Adds {@link CredentialRetriever} to fetch push credentials for the image. Credential retrievers + * are attempted in the order in which they are specified until credentials are successfully + * retrieved. + * + *

Example usage: + * + *

{@code
+   * .addCredentialRetriever(() -> {
+   *   if (!Files.exists("secret.txt") {
+   *     return Optional.empty();
+   *   }
+   *   try {
+   *     String password = fetchPasswordFromFile("secret.txt");
+   *     return Credential.basic("myaccount", password);
+   *
+   *   } catch (IOException ex) {
+   *     throw new CredentialRetrievalException("Failed to load password", ex);
+   *   }
+   * })
+   * }
+ * + * @param credentialRetriever the {@link CredentialRetriever} to add + * @return this + * @see CredentialRetrieverFactory for useful pre-defined {@link CredentialRetriever}s + */ + public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetriever) { + credentialRetrievers.add(credentialRetriever); + return this; + } + + ImageReference getImageReference() { + return imageReference; + } + + List getCredentialRetrievers() { + return Collections.unmodifiableList(credentialRetrievers); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java new file mode 100644 index 0000000000..82ea282335 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.nio.file.Path; + +/** + * Builds to a tarball archive. + * + *

Usage example: + * + *

{@code
+ * TarImage tarImage = TarImage.named("myimage")
+ *                             .saveTo(Paths.get("image.tar"));
+ * }
+ */ +// TODO: Add tests once JibContainerBuilder#containerize() is added. +public class TarImage { + + /** Finishes constructing a {@link TarImage}. */ + public static class Builder { + + private final ImageReference imageReference; + + private Builder(ImageReference imageReference) { + this.imageReference = imageReference; + } + + /** + * Sets the output file to save the tarball archive to. + * + * @param outputFile the output file + * @return a new {@link TarImage} + */ + public TarImage saveTo(Path outputFile) { + return new TarImage(imageReference, outputFile); + } + } + + /** + * Configures the output tarball archive with an image reference. This image reference will be the + * name of the image if loaded into the Docker daemon. + * + * @param imageReference the image reference + * @return a {@link Builder} to finish constructing a new {@link TarImage} + */ + public static Builder named(ImageReference imageReference) { + return new Builder(imageReference); + } + + /** + * Configures the output tarball archive with an image reference to set as its tag. + * + * @param imageReference the image reference + * @return a {@link Builder} to finish constructing a new {@link TarImage} + * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference + */ + public static Builder named(String imageReference) throws InvalidImageReferenceException { + return named(ImageReference.parse(imageReference)); + } + + private final ImageReference imageReference; + private final Path outputFile; + + /** Instantiate with {@link #named}. */ + private TarImage(ImageReference imageReference, Path outputFile) { + this.imageReference = imageReference; + this.outputFile = outputFile; + } + + ImageReference getImageReference() { + return imageReference; + } + + Path getOutputFile() { + return outputFile; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 3c2b02c3b7..3d06844201 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; @@ -86,9 +87,9 @@ public Credential call() throws CredentialRetrievalException { try (Timer ignored = new Timer(buildLogger, String.format(DESCRIPTION, registry))) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { - Credential credential = credentialRetriever.retrieve(); - if (credential != null) { - return credential; + Optional optionalCredential = credentialRetriever.retrieve(); + if (optionalCredential.isPresent()) { + return optionalCredential.get(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index a516a2c2d8..b77a0978e6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -22,18 +22,39 @@ /** Holds credentials (username and password). */ public class Credential { + /** + * Gets a {@link Credential} configured with a username and password. + * + * @param username the username + * @param password the password + * @return a new {@link Credential} + */ + public static Credential basic(String username, String password) { + return new Credential(username, password); + } + private final String username; private final String password; - public Credential(String username, String password) { + private Credential(String username, String password) { this.username = username; this.password = password; } + /** + * Gets the username. + * + * @return the username + */ public String getUsername() { return username; } + /** + * Gets the password. + * + * @return the password + */ public String getPassword() { return password; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java index a607c817f2..20def4297b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.configuration.credentials; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import javax.annotation.Nullable; +import java.util.Optional; /** Retrieves credentials for a registry. */ @FunctionalInterface @@ -26,14 +26,13 @@ public interface CredentialRetriever { /** * Fetches the credentials. Implementations must be thread-safe. * - *

Implementations should return {@code null} if no credentials could be fetched with this - * {@link CredentialRetriever} (and so other credential retrieval methods may be tried), or throw - * an exception something went wrong when fetching the credentials. + *

Implementations should return {@link Optional#empty} if no credentials could be fetched with + * this {@link CredentialRetriever} (and so other credential retrieval methods may be tried), or + * throw an exception something went wrong when fetching the credentials. * - * @return the fetched credentials or {@code null} if no credentials could be fetched with this - * provider + * @return the fetched credentials or {@link Optional#empty} if no credentials could be fetched + * with this provider * @throws CredentialRetrievalException if the credential retrieval encountered an exception */ - @Nullable - Credential retrieve() throws CredentialRetrievalException; + Optional retrieve() throws CredentialRetrievalException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 1c2b537002..182ee6d614 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -32,6 +32,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** Static factories for various {@link CredentialRetriever}s. */ public class CredentialRetrieverFactory { @@ -90,7 +91,7 @@ private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logg public CredentialRetriever known(Credential credential, String credentialSource) { return () -> { logGotCredentialsFrom(credentialSource); - return credential; + return Optional.of(credential); }; } @@ -119,12 +120,12 @@ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { logger.info("Checking credentials from " + credentialHelper); try { - return retrieveFromDockerCredentialHelper(credentialHelper); + return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { logger.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); - return null; + return Optional.empty(); } catch (IOException ex) { throw new CredentialRetrievalException(ex); @@ -155,10 +156,11 @@ public CredentialRetriever inferCredentialHelper() { return () -> { for (String inferredCredentialHelperSuffix : inferredCredentialHelperSuffixes) { try { - return retrieveFromDockerCredentialHelper( - Paths.get( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX - + inferredCredentialHelperSuffix)); + return Optional.of( + retrieveFromDockerCredentialHelper( + Paths.get( + DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + + inferredCredentialHelperSuffix))); } catch (CredentialHelperNotFoundException | CredentialHelperUnhandledServerUrlException ex) { @@ -174,7 +176,7 @@ public CredentialRetriever inferCredentialHelper() { throw new CredentialRetrievalException(ex); } } - return null; + return Optional.empty(); }; } @@ -207,8 +209,8 @@ CredentialRetriever dockerConfig( DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { return () -> { try { - Credential dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); - if (dockerConfigCredentials != null) { + Optional dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); + if (dockerConfigCredentials.isPresent()) { logger.info("Using credentials from Docker config for " + imageReference.getRegistry()); return dockerConfigCredentials; } @@ -216,7 +218,7 @@ CredentialRetriever dockerConfig( } catch (IOException ex) { logger.info("Unable to parse Docker config"); } - return null; + return Optional.empty(); }; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 05027c641c..bb29ff3d9f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -27,7 +27,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import javax.annotation.Nullable; +import java.util.Optional; /** * Retrieves registry credentials from the Docker config. @@ -68,13 +68,12 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { /** * Retrieves credentials for a registry. Tries all possible known aliases. * - * @return {@link Credential} found for {@code registry}, or {@code null} if not found + * @return {@link Credential} found for {@code registry}, or {@link Optional#empty} if not found * @throws IOException if failed to parse the config JSON */ - @Nullable - public Credential retrieve() throws IOException { + public Optional retrieve() throws IOException { if (!Files.exists(dockerConfigFile)) { - return null; + return Optional.empty(); } DockerConfig dockerConfig = new DockerConfig( @@ -86,11 +85,10 @@ public Credential retrieve() throws IOException { * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @param dockerConfig the {@link DockerConfig} to retrieve from - * @return the retrieved credentials, or {@code null} if none are found + * @return the retrieved credentials, or {@code Optional#empty} if none are found */ @VisibleForTesting - @Nullable - Credential retrieve(DockerConfig dockerConfig) { + Optional retrieve(DockerConfig dockerConfig) { for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { // First, tries to find defined auth. String auth = dockerConfig.getAuthFor(registryAlias); @@ -100,7 +98,7 @@ Credential retrieve(DockerConfig dockerConfig) { new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); - return new Credential(username, password); + return Optional.of(Credential.basic(username, password)); } // Then, tries to use a defined credHelpers credential helper. @@ -109,7 +107,7 @@ Credential retrieve(DockerConfig dockerConfig) { if (dockerCredentialHelper != null) { try { // Tries with the given registry alias (may be the original registry). - return dockerCredentialHelper.retrieve(); + return Optional.of(dockerCredentialHelper.retrieve()); } catch (IOException | CredentialHelperUnhandledServerUrlException @@ -118,6 +116,6 @@ Credential retrieve(DockerConfig dockerConfig) { } } } - return null; + return Optional.empty(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index d837db1d6a..010cc7b30b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -119,7 +119,7 @@ public Credential retrieve() credentialHelper, serverUrl, output); } - return new Credential(dockerCredentials.Username, dockerCredentials.Secret); + return Credential.basic(dockerCredentials.Username, dockerCredentials.Secret); } catch (JsonProcessingException ex) { throw new CredentialHelperUnhandledServerUrlException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 29120a2e8f..4b31cdc0f4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,18 +46,20 @@ public class RetrieveRegistryCredentialsStepTest { public void testCall_retrieved() throws CredentialRetrievalException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( - Arrays.asList(() -> null, () -> new Credential("baseusername", "basepassword")), Arrays.asList( - () -> new Credential("targetusername", "targetpassword"), - () -> new Credential("ignored", "ignored"))); + Optional::empty, + () -> Optional.of(Credential.basic("baseusername", "basepassword"))), + Arrays.asList( + () -> Optional.of(Credential.basic("targetusername", "targetpassword")), + () -> Optional.of(Credential.basic("ignored", "ignored")))); Assert.assertEquals( - new Credential("baseusername", "basepassword"), + Credential.basic("baseusername", "basepassword"), RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration) .call()); Assert.assertEquals( - new Credential("targetusername", "targetpassword"), + Credential.basic("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration) .call()); @@ -65,7 +68,8 @@ public void testCall_retrieved() throws CredentialRetrievalException { @Test public void testCall_none() throws CredentialRetrievalException { BuildConfiguration buildConfiguration = - makeFakeBuildConfiguration(Arrays.asList(() -> null, () -> null), Collections.emptyList()); + makeFakeBuildConfiguration( + Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertNull( RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 31738d9ebe..33f10bd1aa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.junit.Assert; import org.junit.Test; @@ -51,7 +52,7 @@ public void testBuilder() throws Exception { Set additionalTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); Set expectedTargetImageTags = ImmutableSet.of("targettag", "tag1", "tag2", "tag3"); List credentialRetrievers = - Collections.singletonList(() -> new Credential("username", "password")); + Collections.singletonList(() -> Optional.of(Credential.basic("username", "password"))); Instant expectedCreationTime = Instant.ofEpochSecond(10000); List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedJavaArguments = Arrays.asList("arg1", "arg2"); @@ -124,12 +125,13 @@ public void testBuilder() throws Exception { expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAllTargetImageTags()); Assert.assertEquals( - new Credential("username", "password"), + Credential.basic("username", "password"), buildConfiguration .getTargetImageConfiguration() .getCredentialRetrievers() .get(0) - .retrieve()); + .retrieve() + .orElseThrow(AssertionError::new)); Assert.assertEquals( expectedJavaArguments, buildConfiguration.getContainerConfiguration().getProgramArguments()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index 7d8b345f52..3ddab3ccd2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -27,10 +27,10 @@ public class CredentialTest { @Test public void testCredentialsHash() { - Credential credentialA1 = new Credential("username", "password"); - Credential credentialA2 = new Credential("username", "password"); - Credential credentialB1 = new Credential("", ""); - Credential credentialB2 = new Credential("", ""); + Credential credentialA1 = Credential.basic("username", "password"); + Credential credentialA2 = Credential.basic("username", "password"); + Credential credentialB1 = Credential.basic("", ""); + Credential credentialB2 = Credential.basic("", ""); Assert.assertEquals(credentialA1, credentialA2); Assert.assertEquals(credentialB1, credentialB2); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 9ad70785d5..b30d495f0f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -40,7 +41,7 @@ @RunWith(MockitoJUnitRunner.class) public class CredentialRetrieverFactoryTest { - private static final Credential FAKE_CREDENTIALS = new Credential("username", "password"); + private static final Credential FAKE_CREDENTIALS = Credential.basic("username", "password"); /** * Returns a {@link DockerCredentialHelperFactory} that checks given parameters upon creating a @@ -93,7 +94,8 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { FAKE_CREDENTIALS, credentialRetrieverFactory .dockerCredentialHelper(Paths.get("docker-credential-helper")) - .retrieve()); + .retrieve() + .orElseThrow(AssertionError::new)); Mockito.verify(mockJibLogger).info("Using docker-credential-helper for registry"); } @@ -110,7 +112,11 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { mockDockerCredentialHelper)); Assert.assertEquals( - FAKE_CREDENTIALS, credentialRetrieverFactory.inferCredentialHelper().retrieve()); + FAKE_CREDENTIALS, + credentialRetrieverFactory + .inferCredentialHelper() + .retrieve() + .orElseThrow(AssertionError::new)); Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); } @@ -128,7 +134,7 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); - Assert.assertNull(credentialRetrieverFactory.inferCredentialHelper().retrieve()); + Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); Mockito.verify(mockJibLogger).warn("warning"); Mockito.verify(mockJibLogger).info(" Caused by: the root cause"); } @@ -139,11 +145,15 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException CredentialRetrieverFactory.forImage( ImageReference.of("registry", null, null), mockJibLogger); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve()).thenReturn(FAKE_CREDENTIALS); + Mockito.when(mockDockerConfigCredentialRetriever.retrieve()) + .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( FAKE_CREDENTIALS, - credentialRetrieverFactory.dockerConfig(mockDockerConfigCredentialRetriever).retrieve()); + credentialRetrieverFactory + .dockerConfig(mockDockerConfigCredentialRetriever) + .retrieve() + .orElseThrow(AssertionError::new)); Mockito.verify(mockJibLogger).info("Using credentials from Docker config for registry"); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index a5e9ce58f8..3d3d0103ae 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -22,6 +22,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -34,7 +35,7 @@ @RunWith(MockitoJUnitRunner.class) public class DockerConfigCredentialRetrieverTest { - private static final Credential FAKE_CREDENTIAL = new Credential("username", "password"); + private static final Credential FAKE_CREDENTIAL = Credential.basic("username", "password"); @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; @@ -54,7 +55,7 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); - Assert.assertNull(dockerConfigCredentialRetriever.retrieve()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve().isPresent()); } @Test @@ -62,10 +63,10 @@ public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - Credential credentials = dockerConfigCredentialRetriever.retrieve(); - Assert.assertNotNull(credentials); - Assert.assertEquals("some", credentials.getUsername()); - Assert.assertEquals("auth", credentials.getPassword()); + Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("some", credentials.get().getUsername()); + Assert.assertEquals("auth", credentials.get().getPassword()); } @Test @@ -76,7 +77,10 @@ public void testRetrieve_useCredsStore() { new DockerConfigCredentialRetriever("just registry", dockerConfigFile); Assert.assertEquals( - FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); + FAKE_CREDENTIAL, + dockerConfigCredentialRetriever + .retrieve(mockDockerConfig) + .orElseThrow(AssertionError::new)); } @Test @@ -87,7 +91,10 @@ public void testRetrieve_useCredsStore_withProtocol() { new DockerConfigCredentialRetriever("with.protocol", dockerConfigFile); Assert.assertEquals( - FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); + FAKE_CREDENTIAL, + dockerConfigCredentialRetriever + .retrieve(mockDockerConfig) + .orElseThrow(AssertionError::new)); } @Test @@ -98,7 +105,10 @@ public void testRetrieve_useCredHelper() { new DockerConfigCredentialRetriever("another registry", dockerConfigFile); Assert.assertEquals( - FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); + FAKE_CREDENTIAL, + dockerConfigCredentialRetriever + .retrieve(mockDockerConfig) + .orElseThrow(AssertionError::new)); } @Test @@ -106,7 +116,7 @@ public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); - Assert.assertNull(dockerConfigCredentialRetriever.retrieve()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve().isPresent()); } @Test @@ -117,7 +127,10 @@ public void testRetrieve_credentialFromAlias() { new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); Assert.assertEquals( - FAKE_CREDENTIAL, dockerConfigCredentialRetriever.retrieve(mockDockerConfig)); + FAKE_CREDENTIAL, + dockerConfigCredentialRetriever + .retrieve(mockDockerConfig) + .orElseThrow(AssertionError::new)); } @Test @@ -128,10 +141,10 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); - Credential credentials = dockerConfigCredentialRetriever.retrieve(); - Assert.assertNotNull(credentials); - Assert.assertEquals("token for", credentials.getUsername()); - Assert.assertEquals(" index.docker.io/v1/", credentials.getPassword()); + Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("token for", credentials.get().getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); } @Test @@ -142,9 +155,9 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); - Credential credentials = dockerConfigCredentialRetriever.retrieve(); - Assert.assertNotNull(credentials); - Assert.assertEquals("token for", credentials.getUsername()); - Assert.assertEquals(" index.docker.io/v1/", credentials.getPassword()); + Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Assert.assertTrue(credentials.isPresent()); + Assert.assertEquals("token for", credentials.get().getUsername()); + Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index fa85fd966a..73bf211deb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -106,7 +106,7 @@ public void buildDocker() throws InvalidImageReferenceException { gradleHelpfulSuggestionsBuilder .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 662cc51dbc..c1d7b1dd2f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -89,15 +90,15 @@ public void buildImage() throws InvalidImageReferenceException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage(targetImage, gradleJibLogger)); - Credential toCredential = + Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( gradleJibLogger, "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); - if (toCredential != null) { - defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth"); - } + optionalToCredential.ifPresent( + toCredential -> + defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getTo().getCredHelper()); ImageConfiguration targetImageConfiguration = @@ -124,9 +125,9 @@ public void buildImage() throws InvalidImageReferenceException { new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) - .setTargetImageHasConfiguredCredentials(toCredential != null) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); try { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5f8fe78f93..874c2f4aba 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -132,7 +132,7 @@ public void buildTar() throws InvalidImageReferenceException { gradleHelpfulSuggestionsBuilder .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index fcfc15921b..6efc3a94a4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -32,7 +32,7 @@ import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import java.time.Instant; import java.util.List; -import javax.annotation.Nullable; +import java.util.Optional; /** Configures and provides builders for the image building tasks. */ class PluginConfigurationProcessor { @@ -66,15 +66,15 @@ static PluginConfigurationProcessor processCommonConfiguration( } DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); - Credential fromCredential = + Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); - if (fromCredential != null) { - defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth"); - } + optionalFromCredential.ifPresent( + fromCredential -> + defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth")); defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getFrom().getCredHelper()); ImageConfiguration.Builder baseImageConfigurationBuilder = @@ -121,23 +121,23 @@ static PluginConfigurationProcessor processCommonConfiguration( buildConfigurationBuilder, baseImageConfigurationBuilder, containerConfigurationBuilder, - fromCredential); + optionalFromCredential.isPresent()); } private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; - @Nullable private final Credential baseImageCredential; + private final boolean isBaseImageCredentialPresent; private PluginConfigurationProcessor( BuildConfiguration.Builder buildConfigurationBuilder, ImageConfiguration.Builder baseImageConfigurationBuilder, ContainerConfiguration.Builder containerConfigurationBuilder, - @Nullable Credential baseImageCredential) { + boolean isBaseImageCredentialPresent) { this.buildConfigurationBuilder = buildConfigurationBuilder; this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; this.containerConfigurationBuilder = containerConfigurationBuilder; - this.baseImageCredential = baseImageCredential; + this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; } BuildConfiguration.Builder getBuildConfigurationBuilder() { @@ -152,8 +152,7 @@ ContainerConfiguration.Builder getContainerConfigurationBuilder() { return containerConfigurationBuilder; } - @Nullable - Credential getBaseImageCredential() { - return baseImageCredential; + boolean isBaseImageCredentialPresent() { + return isBaseImageCredentialPresent; } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 429ec83d94..59b9a3b57f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -91,7 +91,7 @@ public void execute() throws MojoExecutionException { mavenHelpfulSuggestionsBuilder .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 61745ff779..e2871ea3d0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -31,6 +31,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.util.Arrays; +import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -93,21 +94,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage(targetImage, mavenJibLogger)); - Credential toCredential = + Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); - if (toCredential == null) { - toCredential = + if (optionalToCredential.isPresent()) { + defaultCredentialRetrievers.setKnownCredential( + optionalToCredential.get(), "jib-maven-plugin configuration"); + } else { + optionalToCredential = pluginConfigurationProcessor .getMavenSettingsServerCredentials() .retrieve(targetImage.getRegistry()); - if (toCredential != null) { - defaultCredentialRetrievers.setInferredCredential( - toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); - } - } else { - defaultCredentialRetrievers.setKnownCredential( - toCredential, "jib-maven-plugin configuration"); + optionalToCredential.ifPresent( + toCredential -> + defaultCredentialRetrievers.setInferredCredential( + toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); } defaultCredentialRetrievers.setCredentialHelperSuffix(getTargetImageCredentialHelperName()); @@ -131,9 +132,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) - .setTargetImageHasConfiguredCredentials(toCredential != null) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); try { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index cf31bf9301..b2638c395d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -88,7 +88,7 @@ public void execute() throws MojoExecutionException { mavenHelpfulSuggestionsBuilder .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.getBaseImageCredential() != null) + pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 88fa4b5795..1bbc8cd15e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.common.annotations.VisibleForTesting; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -36,7 +37,7 @@ */ class MavenSettingsServerCredentials { - public static final String CREDENTIAL_SOURCE = "Maven settings"; + static final String CREDENTIAL_SOURCE = "Maven settings"; // pattern cribbed directly from // https://github.com/sonatype/plexus-cipher/blob/master/src/main/java/org/sonatype/plexus/components/cipher/DefaultPlexusCipher.java @@ -78,18 +79,17 @@ static boolean isEncrypted(String password) { * Attempts to retrieve credentials for {@code registry} from Maven settings. * * @param registry the registry - * @return the credentials for the registry + * @return the credentials for the registry, or {@link Optional#empty} if none could be retrieved * @throws MojoExecutionException if the credentials could not be retrieved */ - @Nullable - Credential retrieve(@Nullable String registry) throws MojoExecutionException { + Optional retrieve(@Nullable String registry) throws MojoExecutionException { if (registry == null) { - return null; + return Optional.empty(); } Server registryServer = settings.getServer(registry); if (registryServer == null) { - return null; + return Optional.empty(); } if (settingsDecrypter != null) { @@ -118,6 +118,7 @@ Credential retrieve(@Nullable String registry) throws MojoExecutionException { + " appears to be encrypted, but there is no decrypter available"); } - return new Credential(registryServer.getUsername(), registryServer.getPassword()); + return Optional.of( + Credential.basic(registryServer.getUsername(), registryServer.getPassword())); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 4681d6b1e6..d3d2ef9a1f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -32,7 +32,7 @@ import com.google.common.base.Preconditions; import java.time.Instant; import java.util.List; -import javax.annotation.Nullable; +import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; /** Configures and provides builders for the image building goals. */ @@ -80,21 +80,21 @@ static PluginConfigurationProcessor processCommonConfiguration( logger); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); - Credential fromCredential = + Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( logger, "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); - if (fromCredential == null) { - fromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); - if (fromCredential != null) { - defaultCredentialRetrievers.setInferredCredential( - fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE); - } - } else { + if (optionalFromCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( - fromCredential, "jib-maven-plugin configuration"); + optionalFromCredential.get(), "jib-maven-plugin configuration"); + } else { + optionalFromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + optionalFromCredential.ifPresent( + fromCredential -> + defaultCredentialRetrievers.setInferredCredential( + fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); } defaultCredentialRetrievers.setCredentialHelperSuffix( jibPluginConfiguration.getBaseImageCredentialHelperName()); @@ -146,7 +146,7 @@ static PluginConfigurationProcessor processCommonConfiguration( baseImageConfiguration, containerConfigurationBuilder, mavenSettingsServerCredentials, - fromCredential); + optionalFromCredential.isPresent()); } /** @@ -166,19 +166,19 @@ static ImageReference parseImageReference(String image, String type) { private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; private final MavenSettingsServerCredentials mavenSettingsServerCredentials; - @Nullable private final Credential baseImageCredential; + private final boolean isBaseImageCredentialPresent; private PluginConfigurationProcessor( BuildConfiguration.Builder buildConfigurationBuilder, ImageConfiguration.Builder baseImageConfigurationBuilder, ContainerConfiguration.Builder containerConfigurationBuilder, MavenSettingsServerCredentials mavenSettingsServerCredentials, - @Nullable Credential baseImageCredential) { + boolean isBaseImageCredentialPresent) { this.buildConfigurationBuilder = buildConfigurationBuilder; this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; this.containerConfigurationBuilder = containerConfigurationBuilder; this.mavenSettingsServerCredentials = mavenSettingsServerCredentials; - this.baseImageCredential = baseImageCredential; + this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; } BuildConfiguration.Builder getBuildConfigurationBuilder() { @@ -197,8 +197,7 @@ MavenSettingsServerCredentials getMavenSettingsServerCredentials() { return mavenSettingsServerCredentials; } - @Nullable - Credential getBaseImageCredential() { - return baseImageCredential; + boolean isBaseImageCredentialPresent() { + return isBaseImageCredentialPresent; } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index f95725fe06..cd2a4f0a58 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import java.util.Collections; +import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; @@ -55,20 +56,23 @@ public void testRetrieve_found() throws MojoExecutionException { Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertEquals(new Credential("server1 username", "server1 password"), credential); + Optional optionalCredential = + testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(optionalCredential.isPresent()); + Assert.assertEquals( + Credential.basic("server1 username", "server1 password"), optionalCredential.get()); Mockito.verifyZeroInteractions(mockLogger); } @Test public void testRetrieve_notFound() throws MojoExecutionException { - Assert.assertNull(testMavenSettingsServerCredentials.retrieve("serverUnknown")); + Assert.assertFalse(testMavenSettingsServerCredentials.retrieve("serverUnknown").isPresent()); } @Test public void testRetrieve_withNullServer() throws MojoExecutionException { - Assert.assertNull(testMavenSettingsServerCredentials.retrieve(null)); + Assert.assertFalse(testMavenSettingsServerCredentials.retrieve(null).isPresent()); } @Test @@ -77,8 +81,11 @@ public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionExcep Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); - Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertEquals(new Credential("server1 username", "{COQLCE6DU6GtcS5P=}"), credential); + Optional optionalCredential = + testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(optionalCredential.isPresent()); + Assert.assertEquals( + Credential.basic("server1 username", "{COQLCE6DU6GtcS5P=}"), optionalCredential.get()); Mockito.verify(mockLogger) .warn( "Server password for registry server1 appears to be encrypted, " @@ -102,8 +109,11 @@ public void testRetrieve_withDecrypter_success() throws MojoExecutionException { Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Credential credential = testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertEquals(new Credential("server1 username", "server1 password"), credential); + Optional optionalCredential = + testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(optionalCredential.isPresent()); + Assert.assertEquals( + Credential.basic("server1 username", "server1 password"), optionalCredential.get()); Mockito.verify(mockDecrypter).decrypt(Mockito.any()); Mockito.verify(mockResult).getProblems(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 7f2c6b04de..490a2c55ba 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; +import java.util.Optional; import javax.annotation.Nullable; /** Validator for plugin configuration parameters and system properties. */ @@ -36,17 +37,16 @@ public class ConfigurationPropertyValidator { * @param passwordProperty the name of the password system property * @param auth the configured credentials * @return a new {@link Authorization} from the system properties or build configuration, or - * {@code null} if neither is configured. + * {@link Optional#empty} if neither is configured. */ - @Nullable - public static Credential getImageCredential( + public static Optional getImageCredential( JibLogger logger, String usernameProperty, String passwordProperty, AuthProperty auth) { // System property takes priority over build configuration String commandlineUsername = System.getProperty(usernameProperty); String commandlinePassword = System.getProperty(passwordProperty); if (!Strings.isNullOrEmpty(commandlineUsername) && !Strings.isNullOrEmpty(commandlinePassword)) { - return new Credential(commandlineUsername, commandlinePassword); + return Optional.of(Credential.basic(commandlineUsername, commandlinePassword)); } // Warn if a system property is missing @@ -67,22 +67,22 @@ public static Credential getImageCredential( // Check auth configuration next; warn if they aren't both set if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return null; + return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getUsername())) { logger.warn( auth.getUsernamePropertyDescriptor() + " is missing from build configuration; ignoring auth section."); - return null; + return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { logger.warn( auth.getPasswordPropertyDescriptor() + " is missing from build configuration; ignoring auth section."); - return null; + return Optional.empty(); } - return new Credential(auth.getUsername(), auth.getPassword()); + return Optional.of(Credential.basic(auth.getUsername(), auth.getPassword())); } /** diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 63bcb87478..2cb08817e9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,22 +46,22 @@ public void testGetImageAuth() { // System properties set System.setProperty("jib.test.auth.user", "abcde"); System.setProperty("jib.test.auth.pass", "12345"); - Credential expected = new Credential("abcde", "12345"); - Credential actual = + Credential expected = Credential.basic("abcde", "12345"); + Optional actual = ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); + Assert.assertTrue(actual.isPresent()); + Assert.assertEquals(expected.toString(), actual.get().toString()); // Auth set in configuration System.clearProperty("jib.test.auth.user"); System.clearProperty("jib.test.auth.pass"); - expected = new Credential("vwxyz", "98765"); + expected = Credential.basic("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); - Assert.assertNotNull(actual); - Assert.assertEquals(expected.toString(), actual.toString()); + Assert.assertTrue(actual.isPresent()); + Assert.assertEquals(expected.toString(), actual.get().toString()); Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); // Auth completely missing @@ -69,7 +70,7 @@ public void testGetImageAuth() { actual = ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); - Assert.assertNull(actual); + Assert.assertFalse(actual.isPresent()); // Password missing Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); @@ -77,7 +78,7 @@ public void testGetImageAuth() { actual = ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); - Assert.assertNull(actual); + Assert.assertFalse(actual.isPresent()); Mockito.verify(mockLogger) .warn("pass is missing from build configuration; ignoring auth section."); @@ -87,7 +88,7 @@ public void testGetImageAuth() { actual = ConfigurationPropertyValidator.getImageCredential( mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); - Assert.assertNull(actual); + Assert.assertFalse(actual.isPresent()); Mockito.verify(mockLogger) .warn("user is missing from build configuration; ignoring auth section."); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 6a93e4e95c..b90f095ead 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -40,8 +40,8 @@ public class DefaultCredentialRetrieversTest { @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; - private final Credential knownCredential = new Credential("username", "password"); - private final Credential inferredCredential = new Credential("username2", "password2"); + private final Credential knownCredential = Credential.basic("username", "password"); + private final Credential inferredCredential = Credential.basic("username2", "password2"); @Before public void setUp() { From fe0bd5c2cf4728a82a8668edb564bc862d0ffd5e Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 18 Sep 2018 09:51:36 -0400 Subject: [PATCH 0212/2020] Adds Containerizer. (#955) --- .../cloud/tools/jib/api/Containerizer.java | 116 ++++++++++++++++++ .../tools/jib/api/DockerDaemonImage.java | 2 +- .../cloud/tools/jib/api/RegistryImage.java | 2 +- .../google/cloud/tools/jib/api/TarImage.java | 2 +- .../cloud/tools/jib/api/TargetImage.java | 21 ++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java new file mode 100644 index 0000000000..dc638aa834 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.annotation.Nullable; + +/** Configures how to containerize. */ +// TODO: Add tests once JibContainerBuilder#containerize() is added. +public class Containerizer { + + /** + * Gets a new {@link Containerizer} that containerizes to a container registry. + * + * @param registryImage the {@link RegistryImage} that defines target container registry and + * credentials + * @return a new {@link Containerizer} + */ + public static Containerizer to(RegistryImage registryImage) { + return new Containerizer(registryImage); + } + + /** + * Gets a new {@link Containerizer} that containerizes to a Docker daemon. + * + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines target Docker daemon + * @return a new {@link Containerizer} + */ + public static Containerizer to(DockerDaemonImage dockerDaemonImage) { + return new Containerizer(dockerDaemonImage); + } + + /** + * Gets a new {@link Containerizer} that containerizes to a tarball archive. + * + * @param tarImage the {@link TarImage} that defines target output file + * @return a new {@link Containerizer} + */ + public static Containerizer to(TarImage tarImage) { + return new Containerizer(tarImage); + } + + private final TargetImage targetImage; + @Nullable private ExecutorService executorService; + @Nullable private CacheConfiguration cacheConfiguration; + @Nullable private EventHandlers eventHandlers; + + /** Instantiate with {@link #to}. */ + private Containerizer(TargetImage targetImage) { + this.targetImage = targetImage; + } + + /** + * Sets the {@link ExecutorService} Jib executes on. Jib, by default, uses {@link + * Executors#newCachedThreadPool}. + * + * @param executorService the {@link ExecutorService} + * @return this + */ + public Containerizer setExecutorService(@Nullable ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + // TODO: Rethink this method. + public Containerizer setCacheConfiguration(CacheConfiguration cacheConfiguration) { + this.cacheConfiguration = cacheConfiguration; + return this; + } + + /** + * Sets the {@link EventHandlers} to handle events emitted during Jib's execution. + * + * @param eventHandlers the {@link EventHandlers} + * @return this + */ + public Containerizer setEventHandlers(EventHandlers eventHandlers) { + this.eventHandlers = eventHandlers; + return this; + } + + TargetImage getTargetImage() { + return targetImage; + } + + Optional getExecutorService() { + return Optional.ofNullable(executorService); + } + + Optional getCacheConfiguration() { + return Optional.ofNullable(cacheConfiguration); + } + + Optional getEventHandlers() { + return Optional.ofNullable(eventHandlers); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index 09a370b0df..f214ad766d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -24,7 +24,7 @@ /** Builds to the Docker daemon. */ // TODO: Add tests once JibContainerBuilder#containerize() is added. -public class DockerDaemonImage { +public class DockerDaemonImage implements TargetImage { /** * Instantiate with the image reference to tag the built image with. This is the name that shows diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index b81bc3f4c2..36de11acdb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -39,7 +39,7 @@ * credentials are valid push credentials for the repository specified via the image reference. */ // TODO: Add tests once JibContainerBuilder#containerize() is added. -public class RegistryImage { +public class RegistryImage implements TargetImage { /** * Instantiate with the image reference to push to. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index 82ea282335..d8c08d655f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -32,7 +32,7 @@ * } */ // TODO: Add tests once JibContainerBuilder#containerize() is added. -public class TarImage { +public class TarImage implements TargetImage { /** Finishes constructing a {@link TarImage}. */ public static class Builder { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java new file mode 100644 index 0000000000..2f75631d10 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java @@ -0,0 +1,21 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +/** Represents a destination for the Jib-built image. */ +interface TargetImage {} From 2329a2b790ed00368222b6628c967b22a8abe4be Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 18 Sep 2018 23:08:25 -0400 Subject: [PATCH 0213/2020] Adds SourceImage and has Jib#from also take RegistryImage. (#996) --- .../com/google/cloud/tools/jib/api/Jib.java | 21 +++++++++++--- .../tools/jib/api/JibContainerBuilder.java | 7 ++--- .../cloud/tools/jib/api/RegistryImage.java | 29 ++++++++++--------- .../cloud/tools/jib/api/SourceImage.java | 21 ++++++++++++++ 4 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index da8e914dba..46e6e19160 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -25,7 +25,8 @@ public class Jib { /** - * Starts building the container from a base image. + * Starts building the container from a base image. The base image should be publicly-available. + * For a base image that requires credentials, use {@link #from(RegistryImage)}. * * @param baseImageReference the base image reference * @return a new {@link JibContainerBuilder} to continue building the container @@ -34,17 +35,29 @@ public class Jib { */ public static JibContainerBuilder from(String baseImageReference) throws InvalidImageReferenceException { - return from(ImageReference.parse(baseImageReference)); + return from(RegistryImage.named(baseImageReference)); } /** - * Starts building the container from a base image. + * Starts building the container from a base image. The base image should be publicly-available. + * For a base image that requires credentials, use {@link #from(RegistryImage)}. * * @param baseImageReference the base image reference * @return a new {@link JibContainerBuilder} to continue building the container */ public static JibContainerBuilder from(ImageReference baseImageReference) { - return new JibContainerBuilder(baseImageReference); + return new JibContainerBuilder(RegistryImage.named(baseImageReference)); + } + + /** + * Starts building the container from a base image. + * + * @param registryImage the {@link RegistryImage} that defines base container registry and + * credentials + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(RegistryImage registryImage) { + return new JibContainerBuilder(registryImage); } private Jib() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 3640e2c656..e1d1218c02 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; @@ -49,7 +48,7 @@ // TODO: Add tests once containerize() is added. public class JibContainerBuilder { - private final ImageReference baseImageReference; + private final SourceImage baseImage; private List layerConfigurations = new ArrayList<>(); private Map environment = new HashMap<>(); @@ -59,8 +58,8 @@ public class JibContainerBuilder { @Nullable private ImmutableList programArguments; /** Instantiate with {@link Jib#from}. */ - JibContainerBuilder(ImageReference baseImageReference) { - this.baseImageReference = baseImageReference; + JibContainerBuilder(SourceImage baseImage) { + this.baseImage = baseImage; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 36de11acdb..bde4cb636e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -28,21 +28,22 @@ import java.util.Optional; /** - * Builds to a container registry. + * Defines an image on a container registry that can be used as either a source or target image. * - *

The registry portion of the image reference determines which registry to push the image to. - * The repository portion is the namespace to push the image to. The tag is a label to easily - * identify an image among all the images in the repository. See {@link ImageReference} for more - * details. + *

The registry portion of the image reference determines which registry to the image lives (or + * should live) on. The repository portion is the namespace within the registry. The tag is a label + * to easily identify an image among all the images in the repository. See {@link ImageReference} + * for more details. * *

When configuring credentials (via {@link #addCredential} for example), make sure the - * credentials are valid push credentials for the repository specified via the image reference. + * credentials are valid push (for using this as a target image) or pull (for using this as a source + * image) credentials for the repository specified via the image reference. */ // TODO: Add tests once JibContainerBuilder#containerize() is added. -public class RegistryImage implements TargetImage { +public class RegistryImage implements SourceImage, TargetImage { /** - * Instantiate with the image reference to push to. + * Instantiate with the image reference to use. * * @param imageReference the image reference * @return a new {@link RegistryImage} @@ -52,7 +53,7 @@ public static RegistryImage named(ImageReference imageReference) { } /** - * Instantiate with the image reference to push to. + * Instantiate with the image reference to use. * * @param imageReference the image reference * @return a new {@link RegistryImage} @@ -71,8 +72,8 @@ private RegistryImage(ImageReference imageReference) { } /** - * Adds a username-password credential to use to push the image. This is a shorthand for {@code - * addCredentialRetriever(() -> Optional.of(Credential.basic(username, password)))}. + * Adds a username-password credential to use to push/pull the image. This is a shorthand for + * {@code addCredentialRetriever(() -> Optional.of(Credential.basic(username, password)))}. * * @param username the username * @param password the password @@ -84,9 +85,9 @@ public RegistryImage addCredential(String username, String password) { } /** - * Adds {@link CredentialRetriever} to fetch push credentials for the image. Credential retrievers - * are attempted in the order in which they are specified until credentials are successfully - * retrieved. + * Adds {@link CredentialRetriever} to fetch push/pull credentials for the image. Credential + * retrievers are attempted in the order in which they are specified until credentials are + * successfully retrieved. * *

Example usage: * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java new file mode 100644 index 0000000000..ea730bfdc9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java @@ -0,0 +1,21 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; +// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. + +/** Represents a source image for Jib to use (for example, as a base image). */ +interface SourceImage {} From a5e4c9d917a79b3c3b46288be4f01e3f1114f93f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 19 Sep 2018 10:18:34 -0400 Subject: [PATCH 0214/2020] Wait until Docker daemon is ready (#1003) --- kokoro/continuous.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 415557bddd..b6e580e0c1 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -27,6 +27,7 @@ export JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then osascript -e 'quit app "Docker"' open -a Docker + while ! docker info > /dev/null 2>&1; do sleep 1; done fi (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) From 3a934d90bb2e1e53b53b6620f1d2c46e38ee85e2 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 19 Sep 2018 13:46:01 -0400 Subject: [PATCH 0215/2020] Adds AbsoluteUnixPath/RelativeUnixPath to represent Unix paths. (#995) --- .../jib/filesystem/AbsoluteUnixPath.java | 127 ++++++++++++++++++ .../jib/filesystem/RelativeUnixPath.java | 60 +++++++++ .../tools/jib/filesystem/UnixPathParser.java | 44 ++++++ .../jib/filesystem/AbsoluteUnixPathTest.java | 80 +++++++++++ .../jib/filesystem/RelativeUnixPathTest.java | 43 ++++++ .../jib/filesystem/UnixPathParserTest.java | 38 ++++++ 6 files changed, 392 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UnixPathParserTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java new file mode 100644 index 0000000000..72ecad8f00 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.nio.file.Path; +import java.util.StringJoiner; +import javax.annotation.concurrent.Immutable; + +/** + * Represents a Unix-style path in absolute form (containing all path components relative to the + * file system root {@code /}). + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class AbsoluteUnixPath { + + /** + * Gets a new {@link AbsoluteUnixPath} from a Unix-style path string. The path must begin with a + * forward slash ({@code /}). + * + * @param unixPath the Unix-style path string in absolute form + * @return a new {@link AbsoluteUnixPath} + */ + public static AbsoluteUnixPath get(String unixPath) { + Preconditions.checkArgument( + unixPath.startsWith("/"), "Path does not start with forward slash (/): " + unixPath); + + return new AbsoluteUnixPath(UnixPathParser.parse(unixPath)); + } + + /** + * Gets a new {@link AbsoluteUnixPath} from a {@link Path}. The {@code path} must be absolute + * (indicated by a non-null {@link Path#getRoot}). + * + * @param path the absolute {@link Path} to convert to an {@link AbsoluteUnixPath}. + * @return a new {@link AbsoluteUnixPath} + */ + public static AbsoluteUnixPath fromPath(Path path) { + Preconditions.checkArgument( + path.getRoot() != null, "Cannot create AbsoluteUnixPath from non-absolute Path: " + path); + + ImmutableList.Builder pathComponents = + ImmutableList.builderWithExpectedSize(path.getNameCount()); + for (Path pathComponent : path) { + pathComponents.add(pathComponent.toString()); + } + return new AbsoluteUnixPath(pathComponents.build()); + } + + /** Path components after the file system root. This should always match {@link #unixPath}. */ + private final ImmutableList pathComponents; + + /** + * Unix-style path, in absolute form. Does not end with trailing slash, except for the file system + * root ({@code /}). This should always match {@link #pathComponents}. + */ + private final String unixPath; + + private AbsoluteUnixPath(ImmutableList pathComponents) { + this.pathComponents = pathComponents; + + StringJoiner pathJoiner = new StringJoiner("/", "/", ""); + for (String pathComponent : pathComponents) { + pathJoiner.add(pathComponent); + } + unixPath = pathJoiner.toString(); + } + + /** + * Resolves this path against another relative path. + * + * @param relativeUnixPath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(RelativeUnixPath relativeUnixPath) { + ImmutableList.Builder newPathComponents = + ImmutableList.builderWithExpectedSize( + pathComponents.size() + relativeUnixPath.getRelativePathComponents().size()); + newPathComponents.addAll(pathComponents); + newPathComponents.addAll(relativeUnixPath.getRelativePathComponents()); + return new AbsoluteUnixPath(newPathComponents.build()); + } + + /** + * Returns the string form of the absolute Unix-style path. + * + * @return the string form + */ + @Override + public String toString() { + return unixPath; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AbsoluteUnixPath)) { + return false; + } + AbsoluteUnixPath otherAbsoluteUnixPath = (AbsoluteUnixPath) other; + return unixPath.equals(otherAbsoluteUnixPath.unixPath); + } + + @Override + public int hashCode() { + return unixPath.hashCode(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java new file mode 100644 index 0000000000..0dfc23febb --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import javax.annotation.concurrent.Immutable; + +/** + * Represents a Unix-style path in relative form (does not start at the file system root {@code /}). + * + *

This class is immutable and thread-safe. + */ +@Immutable +public class RelativeUnixPath { + + /** + * Gets a new {@link RelativeUnixPath} from a Unix-style path in relative form. The {@code path} + * must be relative (does not begin with a leading slash {@code /}). + * + * @param relativePath the relative path + * @return a new {@link RelativeUnixPath} + */ + public static RelativeUnixPath get(String relativePath) { + Preconditions.checkArgument( + !relativePath.startsWith("/"), "Path starts with forward slash (/): " + relativePath); + + return new RelativeUnixPath(UnixPathParser.parse(relativePath)); + } + + private final ImmutableList pathComponents; + + /** Instantiate with {@link #get}. */ + private RelativeUnixPath(ImmutableList pathComponents) { + this.pathComponents = pathComponents; + } + + /** + * Gets the relative Unix path this represents, in a list of components. + * + * @return the relative path this represents, in a list of components + */ + ImmutableList getRelativePathComponents() { + return pathComponents; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java new file mode 100644 index 0000000000..f4f9a98fda --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +/** Parses Unix-style paths. */ +class UnixPathParser { + + /** + * Parses a Unix-style path into a list of path components. + * + * @param unixPath the Unix-style path + * @return a list of path components + */ + static ImmutableList parse(String unixPath) { + ImmutableList.Builder pathComponents = ImmutableList.builder(); + for (String pathComponent : Splitter.on('/').split(unixPath)) { + if (pathComponent.isEmpty()) { + // Skips empty components. + continue; + } + pathComponents.add(pathComponent); + } + return pathComponents.build(); + } + + private UnixPathParser() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java new file mode 100644 index 0000000000..2a737faa03 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +/** Test for {@link AbsoluteUnixPath}. */ +public class AbsoluteUnixPathTest { + + @Test + public void testGet_notAbsolute() { + try { + AbsoluteUnixPath.get("not/absolute"); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "Path does not start with forward slash (/): not/absolute", ex.getMessage()); + } + } + + @Test + public void testFromPath() { + Assert.assertEquals( + "/absolute/path", AbsoluteUnixPath.fromPath(Paths.get("/absolute/path")).toString()); + } + + @Test + public void testFromPath_windows() { + Assume.assumeTrue(System.getProperty("os.name").startsWith("Windows")); + + Assert.assertEquals( + "/absolute/path", AbsoluteUnixPath.fromPath(Paths.get("T:\\absolute\\path")).toString()); + } + + @Test + public void testEquals() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/absolute/path"); + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/absolute/path/"); + AbsoluteUnixPath absoluteUnixPath3 = AbsoluteUnixPath.get("/another/path"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath2); + Assert.assertNotEquals(absoluteUnixPath1, absoluteUnixPath3); + } + + @Test + public void testResolve() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve(RelativeUnixPath.get(""))); + Assert.assertEquals( + "/file", absoluteUnixPath1.resolve(RelativeUnixPath.get("file")).toString()); + Assert.assertEquals( + "/relative/path", + absoluteUnixPath1.resolve(RelativeUnixPath.get("relative/path")).toString()); + + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/some/path"); + Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve(RelativeUnixPath.get(""))); + Assert.assertEquals( + "/some/path/file", absoluteUnixPath2.resolve(RelativeUnixPath.get("file")).toString()); + Assert.assertEquals( + "/some/path/relative/path", + absoluteUnixPath2.resolve(RelativeUnixPath.get("relative/path")).toString()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java new file mode 100644 index 0000000000..39dbdc2eef --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.collect.ImmutableList; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link RelativeUnixPath}. */ +public class RelativeUnixPathTest { + + @Test + public void testGet_absolute() { + try { + RelativeUnixPath.get("/absolute"); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals("Path starts with forward slash (/): /absolute", ex.getMessage()); + } + } + + @Test + public void testGet() { + Assert.assertEquals( + ImmutableList.of("some", "relative", "path"), + RelativeUnixPath.get("some/relative///path").getRelativePathComponents()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UnixPathParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UnixPathParserTest.java new file mode 100644 index 0000000000..ec54f3e4df --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UnixPathParserTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.collect.ImmutableList; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link UnixPathParser}. */ +public class UnixPathParserTest { + + @Test + public void testParse() { + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("/some/path")); + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("some/path/")); + Assert.assertEquals(ImmutableList.of("some", "path"), UnixPathParser.parse("some///path///")); + // Windows-style paths are resolved in Unix semantics. + Assert.assertEquals( + ImmutableList.of("\\windows\\path"), UnixPathParser.parse("\\windows\\path")); + Assert.assertEquals(ImmutableList.of("T:\\dir"), UnixPathParser.parse("T:\\dir")); + Assert.assertEquals( + ImmutableList.of("T:\\dir", "real", "path"), UnixPathParser.parse("T:\\dir/real/path")); + } +} From 34c4e49c5f43cf3d37338fe3e69a2cfce07224b3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 19 Sep 2018 15:17:45 -0400 Subject: [PATCH 0216/2020] Add container.appRoot config parameter (#984) --- .../builder/BuildStepsIntegrationTest.java | 2 +- .../frontend/JavaDockerContextGenerator.java | 12 +- .../frontend/JavaEntrypointConstructor.java | 21 ++- .../jib/frontend/JavaLayerConfigurations.java | 103 ++++++++---- .../JavaDockerContextGeneratorTest.java | 63 ++++++-- .../JavaEntrypointConstructorTest.java | 65 +++++++- .../frontend/JavaLayerConfigurationsTest.java | 137 ++++++++-------- jib-core/src/test/resources/sampleDockerfile | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-gradle-plugin/README.md | 8 +- .../tools/jib/gradle/BuildDockerTask.java | 3 +- .../tools/jib/gradle/BuildImageTask.java | 3 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 3 +- .../tools/jib/gradle/ContainerParameters.java | 12 ++ .../tools/jib/gradle/DockerContextTask.java | 6 +- .../jib/gradle/GradleLayerConfigurations.java | 24 ++- .../jib/gradle/GradleProjectProperties.java | 5 +- .../cloud/tools/jib/gradle/JibExtension.java | 1 + .../gradle/PluginConfigurationProcessor.java | 21 ++- .../jib/gradle/DockerContextTaskTest.java | 129 ++++++++++++++++ .../gradle/GradleLayerConfigurationsTest.java | 48 +++++- .../tools/jib/gradle/JibExtensionTest.java | 38 ++--- .../PluginConfigurationProcessorTest.java | 64 ++++++++ jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 4 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 4 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 4 +- .../tools/jib/maven/DockerContextMojo.java | 16 +- .../jib/maven/JibPluginConfiguration.java | 11 +- .../jib/maven/MavenLayerConfigurations.java | 25 ++- .../jib/maven/MavenProjectProperties.java | 7 +- .../maven/PluginConfigurationProcessor.java | 20 ++- .../jib/maven/DockerContextMojoTest.java | 146 ++++++++++++++++++ .../jib/maven/JibPluginConfigurationTest.java | 1 + .../maven/MavenLayerConfigurationsTest.java | 54 ++++++- .../PluginConfigurationProcessorTest.java | 64 ++++++++ .../ConfigurationPropertyValidator.java | 17 ++ .../ConfigurationPropertyValidatorTest.java | 30 ++++ 38 files changed, 975 insertions(+), 200 deletions(-) create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 5d98266a91..d4db5cbc01 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -284,7 +284,7 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( ContainerConfiguration.builder() .setEntrypoint( JavaEntrypointConstructor.makeDefaultEntrypoint( - Collections.emptyList(), "HelloWorld")) + "/app", Collections.emptyList(), "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) .setExposedPorts( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 30ef1f49f2..027b651d78 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -151,22 +151,22 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration copyDirectivesBuilder, javaLayerConfigurations.getDependencyLayerEntries(), DEPENDENCIES_LAYER_DIRECTORY, - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE); + javaLayerConfigurations.getDependencyExtractionPath()); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getSnapshotDependencyLayerEntries(), SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY, - JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE); + javaLayerConfigurations.getSnapshotDependencyExtractionPath()); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getResourceLayerEntries(), RESOURCES_LAYER_DIRECTORY, - JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE); + javaLayerConfigurations.getResourceExtractionPath()); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getClassLayerEntries(), CLASSES_LAYER_DIRECTORY, - JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE); + javaLayerConfigurations.getClassExtractionPath()); // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, // snapshot dependencies, resources, and classes layers). Should copy files in the right // directories. (For example, "resources" will go into the webapp root.) @@ -174,12 +174,12 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration copyDirectivesBuilder, javaLayerConfigurations.getExplodedWarEntries(), EXPLODED_WAR_LAYER_DIRECTORY, - JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE); + javaLayerConfigurations.getExplodedWarExtractionPath()); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getExtraFilesLayerEntries(), EXTRA_FILES_LAYER_DIRECTORY, - "/"); + javaLayerConfigurations.getExtraFilesExtractionPath()); copyDirectives = copyDirectivesBuilder.build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 407b4d2e54..0592d2c869 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.frontend; +import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -23,17 +24,21 @@ /** Constructs an image entrypoint for the Java application. */ public class JavaEntrypointConstructor { - public static final String DEFAULT_RESOURCES_PATH_ON_IMAGE = "/app/resources/"; - public static final String DEFAULT_CLASSES_PATH_ON_IMAGE = "/app/classes/"; - public static final String DEFAULT_DEPENDENCIES_PATH_ON_IMAGE = "/app/libs/"; - public static final String DEFAULT_JETTY_BASE_ON_IMAGE = "/jetty/webapps/ROOT/"; + public static final String DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE = "resources/"; + public static final String DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE = "classes/"; + public static final String DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE = "libs/"; + + public static List makeDefaultEntrypoint( + String appRoot, List jvmFlags, String mainClass) { + Preconditions.checkArgument( + appRoot.startsWith("/"), "appRoot should be an absolute path in Unix-style: " + appRoot); + appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/'; - public static List makeDefaultEntrypoint(List jvmFlags, String mainClass) { return makeEntrypoint( Arrays.asList( - DEFAULT_RESOURCES_PATH_ON_IMAGE, - DEFAULT_CLASSES_PATH_ON_IMAGE, - DEFAULT_DEPENDENCIES_PATH_ON_IMAGE + "*"), + appRoot + DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE, + appRoot + DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE, + appRoot + DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE + "*"), jvmFlags, mainClass); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index d3e4b2b9b6..cff962aa8d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -36,76 +36,81 @@ public class JavaLayerConfigurations { /** Represents the different types of layers for a Java application. */ @VisibleForTesting enum LayerType { - DEPENDENCIES("dependencies", JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE), - SNAPSHOT_DEPENDENCIES( - "snapshot dependencies", JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE), - RESOURCES("resources", JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE), - CLASSES("classes", JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE), + DEPENDENCIES("dependencies"), + SNAPSHOT_DEPENDENCIES("snapshot dependencies"), + RESOURCES("resources"), + CLASSES("classes"), // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, // snapshot dependencies, resources, and classes layers). Should copy files in the right - EXPLODED_WAR("exploded war", JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE), - EXTRA_FILES("extra files", "/"); + EXPLODED_WAR("exploded war"), + EXTRA_FILES("extra files"); private final String name; - private final Path extractionPath; - /** Initializes with a name for the layer and the layer files' default extraction path root. */ - LayerType(String name, String extractionPath) { + /** + * Initializes with a name for the layer. + * + * @param name name to set for the layer; does not affect the contents of the layer + */ + LayerType(String name) { this.name = name; - this.extractionPath = Paths.get(extractionPath); } @VisibleForTesting String getName() { return name; } - - @VisibleForTesting - Path getExtractionPath() { - return extractionPath; - } } /** Builds with each layer's files. */ public static class Builder { private final Map> layerFilesMap = new EnumMap<>(LayerType.class); + private final Map extractionPathMap = new EnumMap<>(LayerType.class); private Builder() { for (LayerType layerType : LayerType.values()) { layerFilesMap.put(layerType, new ArrayList<>()); + extractionPathMap.put(layerType, "/"); } } - public Builder setDependencyFiles(List dependencyFiles) { + public Builder setDependencyFiles(List dependencyFiles, String extractionPath) { layerFilesMap.put(LayerType.DEPENDENCIES, dependencyFiles); + extractionPathMap.put(LayerType.DEPENDENCIES, extractionPath); return this; } - public Builder setSnapshotDependencyFiles(List snapshotDependencyFiles) { + public Builder setSnapshotDependencyFiles( + List snapshotDependencyFiles, String extractionPath) { layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependencyFiles); + extractionPathMap.put(LayerType.SNAPSHOT_DEPENDENCIES, extractionPath); return this; } - public Builder setResourceFiles(List resourceFiles) { + public Builder setResourceFiles(List resourceFiles, String extractionPath) { layerFilesMap.put(LayerType.RESOURCES, resourceFiles); + extractionPathMap.put(LayerType.RESOURCES, extractionPath); return this; } - public Builder setClassFiles(List classFiles) { + public Builder setClassFiles(List classFiles, String extractionPath) { layerFilesMap.put(LayerType.CLASSES, classFiles); + extractionPathMap.put(LayerType.CLASSES, extractionPath); return this; } - public Builder setExtraFiles(List extraFiles) { + public Builder setExtraFiles(List extraFiles, String extractionPath) { layerFilesMap.put(LayerType.EXTRA_FILES, extraFiles); + extractionPathMap.put(LayerType.EXTRA_FILES, extractionPath); return this; } // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot // dependencies, resources, and classes layers). - public Builder setExplodedWarFiles(List explodedWarFiles) { + public Builder setExplodedWarFiles(List explodedWarFiles, String extractionPath) { layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWarFiles); + extractionPathMap.put(LayerType.EXPLODED_WAR, extractionPath); return this; } @@ -113,19 +118,26 @@ public JavaLayerConfigurations build() throws IOException { ImmutableMap.Builder layerConfigurationsMap = ImmutableMap.builderWithExpectedSize(LayerType.values().length); for (LayerType layerType : LayerType.values()) { + String extractionPath = Preconditions.checkNotNull(extractionPathMap.get(layerType)); + // Windows filenames cannot have "/", so this also blocks Windows-style path. + Preconditions.checkState( + extractionPath.startsWith("/"), + "extractionPath should be an absolute path in Unix-style: " + extractionPath); + LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder().setName(layerType.getName()); // Adds all the layer files recursively. List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType)); for (Path layerFile : layerFiles) { - layerConfigurationBuilder.addEntryRecursive( - layerFile, layerType.getExtractionPath().resolve(layerFile.getFileName())); + Path pathInContainer = Paths.get(extractionPath).resolve(layerFile.getFileName()); + layerConfigurationBuilder.addEntryRecursive(layerFile, pathInContainer); } layerConfigurationsMap.put(layerType, layerConfigurationBuilder.build()); } - return new JavaLayerConfigurations(layerConfigurationsMap.build()); + return new JavaLayerConfigurations( + layerConfigurationsMap.build(), ImmutableMap.copyOf(extractionPathMap)); } } @@ -133,11 +145,20 @@ public static Builder builder() { return new Builder(); } + /** + * The default app root in the image. For example, if this is set to {@code "/app"}, dependency + * JARs will be in {@code "/app/libs"}. + */ + public static final String DEFAULT_APP_ROOT = "/app"; + private final ImmutableMap layerConfigurationMap; + private final ImmutableMap extractionPathMap; private JavaLayerConfigurations( - ImmutableMap layerConfigurationsMap) { - this.layerConfigurationMap = layerConfigurationsMap; + ImmutableMap layerConfigurationMap, + ImmutableMap extractionPathMap) { + this.layerConfigurationMap = layerConfigurationMap; + this.extractionPathMap = extractionPathMap; } public ImmutableList getLayerConfigurations() { @@ -170,7 +191,35 @@ public ImmutableList getExplodedWarEntries() { return getLayerEntries(LayerType.EXPLODED_WAR); } + public String getDependencyExtractionPath() { + return getExtractionPath(LayerType.DEPENDENCIES); + } + + public String getSnapshotDependencyExtractionPath() { + return getExtractionPath(LayerType.SNAPSHOT_DEPENDENCIES); + } + + public String getResourceExtractionPath() { + return getExtractionPath(LayerType.RESOURCES); + } + + public String getClassExtractionPath() { + return getExtractionPath(LayerType.CLASSES); + } + + public String getExtraFilesExtractionPath() { + return getExtractionPath(LayerType.EXTRA_FILES); + } + + public String getExplodedWarExtractionPath() { + return getExtractionPath(LayerType.EXPLODED_WAR); + } + private ImmutableList getLayerEntries(LayerType layerType) { return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries(); } + + private String getExtractionPath(LayerType layerType) { + return Preconditions.checkNotNull(extractionPathMap.get(layerType)); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index f92eb0d97c..036c4b7dd9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -43,10 +43,10 @@ @RunWith(MockitoJUnitRunner.class) public class JavaDockerContextGeneratorTest { - private static final Path EXPECTED_DEPENDENCIES_PATH = Paths.get("/app/libs/"); - private static final Path EXPECTED_RESOURCES_PATH = Paths.get("/app/resources/"); - private static final Path EXPECTED_CLASSES_PATH = Paths.get("/app/classes/"); - private static final Path EXPECTED_EXPLODED_WAR_PATH = Paths.get("/jetty/webapps/ROOT/"); + private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; + private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; + private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; + private static final String EXPECTED_EXPLODED_WAR_PATH = "/exploded/war/"; private static void assertSameFiles(Path directory1, Path directory2) throws IOException { ImmutableList directory1Files = @@ -99,17 +99,33 @@ public void testGenerate() throws IOException, URISyntaxException { Files.delete(targetDirectory); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH)); + .thenReturn(filesToLayerEntries(testDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testSnapshotDependencies, EXPECTED_DEPENDENCIES_PATH)); + .thenReturn( + filesToLayerEntries(testSnapshotDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(filesToLayerEntries(testResources, EXPECTED_RESOURCES_PATH)); + .thenReturn(filesToLayerEntries(testResources, Paths.get(EXPECTED_RESOURCES_PATH))); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(filesToLayerEntries(testClasses, EXPECTED_CLASSES_PATH)); + .thenReturn(filesToLayerEntries(testClasses, Paths.get(EXPECTED_CLASSES_PATH))); Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) - .thenReturn(filesToLayerEntries(testExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH)); + .thenReturn( + filesToLayerEntries(testExplodedWarFiles, Paths.get(EXPECTED_EXPLODED_WAR_PATH))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) .thenReturn(filesToLayerEntries(testExtraFiles, Paths.get("/"))); + Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) + .thenReturn(filesToLayerEntries(testDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); + + Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath()) + .thenReturn(EXPECTED_DEPENDENCIES_PATH); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath()) + .thenReturn(EXPECTED_DEPENDENCIES_PATH); + Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath()) + .thenReturn(EXPECTED_RESOURCES_PATH); + Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath()) + .thenReturn(EXPECTED_CLASSES_PATH); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath()) + .thenReturn(EXPECTED_EXPLODED_WAR_PATH); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()).thenReturn("/"); new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage("somebaseimage") @@ -141,28 +157,41 @@ public void testMakeDockerfile() throws IOException { "key3", "value3"); + Path ignored = Paths.get("ignored"); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_DEPENDENCIES_PATH))); + ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_DEPENDENCIES_PATH)))); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_DEPENDENCIES_PATH))); + ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_DEPENDENCIES_PATH)))); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_RESOURCES_PATH))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_RESOURCES_PATH)))); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_CLASSES_PATH))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_CLASSES_PATH)))); Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(Paths.get("ignored"), EXPECTED_EXPLODED_WAR_PATH))); + ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_EXPLODED_WAR_PATH)))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("/")))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get("/")))); + + Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath()) + .thenReturn(EXPECTED_DEPENDENCIES_PATH); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath()) + .thenReturn(EXPECTED_DEPENDENCIES_PATH); + Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath()) + .thenReturn(EXPECTED_RESOURCES_PATH); + Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath()) + .thenReturn(EXPECTED_CLASSES_PATH); + Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath()) + .thenReturn(EXPECTED_EXPLODED_WAR_PATH); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()).thenReturn("/"); + String dockerfile = new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage(expectedBaseImage) .setEntrypoint( JavaEntrypointConstructor.makeDefaultEntrypoint( - expectedJvmFlags, expectedMainClass)) + "/app", expectedJvmFlags, expectedMainClass)) .setJavaArguments(expectedJavaArguments) .setEnvironment(expectedEnv) .setExposedPorts(exposedPorts) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index b612666b8f..2d14e11bfe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.frontend; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -49,10 +50,72 @@ public void testMakeEntrypoint() { // Checks that this is also the default entrypoint. Assert.assertEquals( - JavaEntrypointConstructor.makeDefaultEntrypoint(expectedJvmFlags, expectedMainClass), + JavaEntrypointConstructor.makeDefaultEntrypoint( + "/app", expectedJvmFlags, expectedMainClass), entrypoint); } + @Test + public void testMakeDefaultEntrypoint_classpathString() { + // Checks that this is also the default entrypoint. + List entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint("/app", Collections.emptyList(), "MyMain"); + Assert.assertEquals("/app/resources/:/app/classes/:/app/libs/*", entrypoint.get(2)); + } + + @Test + public void testMakeDefaultEntrypoint_classpathStringWithNonDefaultAppRoot() { + // Checks that this is also the default entrypoint. + List entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint("/my/app", Collections.emptyList(), "Main"); + Assert.assertEquals("/my/app/resources/:/my/app/classes/:/my/app/libs/*", entrypoint.get(2)); + } + + @Test + public void testMakeDefaultEntrypoint_appRootWithTrailingSlash() { + // Checks that this is also the default entrypoint. + List entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint( + "/my/root/", Collections.emptyList(), "SomeMainClass"); + Assert.assertEquals("/my/root/resources/:/my/root/classes/:/my/root/libs/*", entrypoint.get(2)); + } + + @Test + public void testMakeDefaultEntrypoint_nonAbsoluteAppRoot() { + try { + JavaEntrypointConstructor.makeDefaultEntrypoint( + "relative/path", Collections.emptyList(), "MainClass"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "appRoot should be an absolute path in Unix-style: relative/path", ex.getMessage()); + } + } + + @Test + public void testMakeDefaultEntrypoint_windowsAppRootPath() { + try { + JavaEntrypointConstructor.makeDefaultEntrypoint( + "\\windows\\path", Collections.emptyList(), "MyMain"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "appRoot should be an absolute path in Unix-style: \\windows\\path", ex.getMessage()); + } + } + + @Test + public void testMakeDefaultEntrypoint_windowsPathWithDriveLetter() { + try { + JavaEntrypointConstructor.makeDefaultEntrypoint( + "D:\\windows\\path", Collections.emptyList(), "MyMain"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "appRoot should be an absolute path in Unix-style: D:\\windows\\path", ex.getMessage()); + } + } + @Test public void testMakeDistrolessJettyEntrypoint() { List expected = Arrays.asList("java", "-jar", "/jetty/start.jar"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index fd097e727f..7ec53cc918 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -2,7 +2,6 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -17,48 +16,25 @@ /** Tests for {@link JavaLayerConfigurations}. */ public class JavaLayerConfigurationsTest { - @Test - public void testDefault() throws IOException { - JavaLayerConfigurations javaLayerConfigurations = - JavaLayerConfigurations.builder() - .setDependencyFiles(Collections.singletonList(Paths.get("dependency"))) - .setSnapshotDependencyFiles(Collections.singletonList(Paths.get("snapshotDependency"))) - .setResourceFiles(Collections.singletonList(Paths.get("resource"))) - .setClassFiles(Collections.singletonList(Paths.get("class"))) - .setExplodedWarFiles(Collections.singletonList(Paths.get("exploded-war"))) - .setExtraFiles(Collections.singletonList(Paths.get("extra"))) - .build(); + private static JavaLayerConfigurations createFakeConfigurations() throws IOException { + return JavaLayerConfigurations.builder() + .setDependencyFiles(Collections.singletonList(Paths.get("dependency")), "/dependency/path") + .setSnapshotDependencyFiles( + Collections.singletonList(Paths.get("snapshot dependency")), "/snapshots") + .setResourceFiles(Collections.singletonList(Paths.get("resource")), "/resources/here") + .setClassFiles(Collections.singletonList(Paths.get("class")), "/classes/go/here") + .setExplodedWarFiles(Collections.singletonList(Paths.get("exploded war")), "/for/war") + .setExtraFiles(Collections.singletonList(Paths.get("extra file")), "/some/extras") + .build(); + } - ImmutableList dependencyLayerEntries = - javaLayerConfigurations.getDependencyLayerEntries(); - ImmutableList snapshotDependencyLayerEntries = - javaLayerConfigurations.getSnapshotDependencyLayerEntries(); - ImmutableList resourceLayerEntries = - javaLayerConfigurations.getResourceLayerEntries(); - ImmutableList classLayerEntries = javaLayerConfigurations.getClassLayerEntries(); - ImmutableList explodedWarLayerEntries = - javaLayerConfigurations.getExplodedWarEntries(); - ImmutableList extraFilesLayerEntries = - javaLayerConfigurations.getExtraFilesLayerEntries(); + private static List layerEntriesToSourceFiles(List entries) { + return entries.stream().map(LayerEntry::getSourceFile).collect(Collectors.toList()); + } - Assert.assertEquals( - Paths.get(JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .resolve("dependency"), - dependencyLayerEntries.get(0).getExtractionPath()); - Assert.assertEquals( - Paths.get(JavaEntrypointConstructor.DEFAULT_DEPENDENCIES_PATH_ON_IMAGE) - .resolve("snapshotDependency"), - snapshotDependencyLayerEntries.get(0).getExtractionPath()); - Assert.assertEquals( - Paths.get(JavaEntrypointConstructor.DEFAULT_RESOURCES_PATH_ON_IMAGE).resolve("resource"), - resourceLayerEntries.get(0).getExtractionPath()); - Assert.assertEquals( - Paths.get(JavaEntrypointConstructor.DEFAULT_CLASSES_PATH_ON_IMAGE).resolve("class"), - classLayerEntries.get(0).getExtractionPath()); - Assert.assertEquals( - Paths.get(JavaEntrypointConstructor.DEFAULT_JETTY_BASE_ON_IMAGE).resolve("exploded-war"), - explodedWarLayerEntries.get(0).getExtractionPath()); - Assert.assertEquals(Paths.get("/extra"), extraFilesLayerEntries.get(0).getExtractionPath()); + @Test + public void testLabels() throws IOException { + JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); List expectedLabels = new ArrayList<>(); for (JavaLayerConfigurations.LayerType layerType : JavaLayerConfigurations.LayerType.values()) { @@ -72,42 +48,55 @@ public void testDefault() throws IOException { } @Test - public void testSetFiles() throws IOException { - List dependencyFiles = Collections.singletonList(Paths.get("/dependency")); - List snapshotDependencyFiles = - Collections.singletonList(Paths.get("/snapshot dependency")); - List resourceFiles = Collections.singletonList(Paths.get("/resource")); - List classFiles = Collections.singletonList(Paths.get("/class")); - List explodedWarFiles = Collections.singletonList(Paths.get("/exploded war")); - List extraFiles = Collections.singletonList(Paths.get("/extra file")); - - JavaLayerConfigurations javaLayerConfigurations = - JavaLayerConfigurations.builder() - .setDependencyFiles(dependencyFiles) - .setSnapshotDependencyFiles(snapshotDependencyFiles) - .setResourceFiles(resourceFiles) - .setClassFiles(classFiles) - .setExplodedWarFiles(explodedWarFiles) - .setExtraFiles(extraFiles) - .build(); + public void testSetFiles_files() throws IOException { + JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); List> expectedFiles = Arrays.asList( - dependencyFiles, - snapshotDependencyFiles, - resourceFiles, - classFiles, - explodedWarFiles, - extraFiles); - List> actualFiles = new ArrayList<>(); - for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { - actualFiles.add( - layerConfiguration - .getLayerEntries() - .stream() - .map(LayerEntry::getSourceFile) - .collect(Collectors.toList())); - } + Collections.singletonList(Paths.get("dependency")), + Collections.singletonList(Paths.get("snapshot dependency")), + Collections.singletonList(Paths.get("resource")), + Collections.singletonList(Paths.get("class")), + Collections.singletonList(Paths.get("exploded war")), + Collections.singletonList(Paths.get("extra file"))); + List> actualFiles = + javaLayerConfigurations + .getLayerConfigurations() + .stream() + .map(LayerConfiguration::getLayerEntries) + .map(JavaLayerConfigurationsTest::layerEntriesToSourceFiles) + .collect(Collectors.toList()); Assert.assertEquals(expectedFiles, actualFiles); } + + @Test + public void testSetFiles_extractionPaths() throws IOException { + JavaLayerConfigurations configurations = createFakeConfigurations(); + + Assert.assertEquals("/dependency/path", configurations.getDependencyExtractionPath()); + Assert.assertEquals("/snapshots", configurations.getSnapshotDependencyExtractionPath()); + Assert.assertEquals("/resources/here", configurations.getResourceExtractionPath()); + Assert.assertEquals("/classes/go/here", configurations.getClassExtractionPath()); + Assert.assertEquals("/for/war", configurations.getExplodedWarExtractionPath()); + Assert.assertEquals("/some/extras", configurations.getExtraFilesExtractionPath()); + + Assert.assertEquals( + Paths.get("/dependency/path/dependency"), + configurations.getDependencyLayerEntries().get(0).getExtractionPath()); + Assert.assertEquals( + Paths.get("/snapshots/snapshot dependency"), + configurations.getSnapshotDependencyLayerEntries().get(0).getExtractionPath()); + Assert.assertEquals( + Paths.get("/resources/here/resource"), + configurations.getResourceLayerEntries().get(0).getExtractionPath()); + Assert.assertEquals( + Paths.get("/classes/go/here/class"), + configurations.getClassLayerEntries().get(0).getExtractionPath()); + Assert.assertEquals( + Paths.get("/for/war/exploded war"), + configurations.getExplodedWarEntries().get(0).getExtractionPath()); + Assert.assertEquals( + Paths.get("/some/extras/extra file"), + configurations.getExtraFilesLayerEntries().get(0).getExtractionPath()); + } } diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index e90bea1963..c1de9b2f4b 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -4,7 +4,7 @@ COPY libs /app/libs/ COPY snapshot-libs /app/libs/ COPY resources /app/resources/ COPY classes /app/classes/ -COPY exploded-war /jetty/webapps/ROOT/ +COPY exploded-war /exploded/war/ COPY root / EXPOSE 1000/tcp diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index f9b6329fb7..126b1bf107 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `container.environment` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) ### Changed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 247f0b974e..046ca4f927 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -89,7 +89,7 @@ Build your container image with: gradle jib ``` -Subsequent builds are much faster than the initial build. +Subsequent builds are much faster than the initial build. *Having trouble? Let us know by [submitting an issue](/../../issues/new), contacting us on [Gitter](https://gitter.im/google/jib), or posting to the [Jib users forum](https://groups.google.com/forum/#!forum/jib-users).* @@ -150,7 +150,7 @@ You can then build your image with Docker: ```shell docker build -t myimage my/docker/context/ -``` +``` ## Extended Usage @@ -273,7 +273,7 @@ Some common credential helpers include: Configure credential helpers to use by specifying them as a `credHelper` for their respective image in the `jib` extension. -*Example configuration:* +*Example configuration:* ```groovy jib { from { @@ -310,7 +310,7 @@ jib { } ``` -These credentials can be stored in `gradle.properties`, retrieved from a command (like `gcloud auth print-access-token`), or read in from a file. +These credentials can be stored in `gradle.properties`, retrieved from a command (like `gcloud auth print-access-token`), or read in from a file. For example, you can use a key file for authentication (for GCR, see [Using a JSON key file](https://cloud.google.com/container-registry/docs/advanced-authentication#using_a_json_key_file)): diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 73bf211deb..75949da525 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -73,9 +73,10 @@ public void buildDocker() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index c1d7b1dd2f..4de1ab3ebe 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -72,9 +72,10 @@ public void buildImage() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 874c2f4aba..2f2e081d9b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -100,9 +100,10 @@ public void buildTar() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index cab2623272..8bdf32339f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.common.base.Preconditions; @@ -41,6 +42,7 @@ public class ContainerParameters { private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); private Map labels = Collections.emptyMap(); + private String appRoot = JavaLayerConfigurations.DEFAULT_APP_ROOT; @Input @Optional @@ -132,4 +134,14 @@ public Map getLabels() { public void setLabels(Map labels) { this.labels = labels; } + + @Input + @Optional + public String getAppRoot() { + return appRoot; + } + + public void setAppRoot(String appRoot) { + this.appRoot = appRoot; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index e528dbcd36..635607119b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -106,16 +106,18 @@ public void generateDockerContext() { jibExtension.handleDeprecatedParameters(gradleJibLogger); JibSystemProperties.checkHttpTimeoutProperty(); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath()); + getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); String targetDir = getTargetDir(); List entrypoint = jibExtension.getContainer().getEntrypoint(); if (entrypoint.isEmpty()) { String mainClass = gradleProjectProperties.getMainClass(jibExtension); entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint(jibExtension.getJvmFlags(), mainClass); + JavaEntrypointConstructor.makeDefaultEntrypoint( + appRoot, jibExtension.getJvmFlags(), mainClass); } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { gradleJibLogger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 57df1b1f8a..ed74dae264 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.io.File; import java.io.IOException; @@ -43,11 +44,13 @@ class GradleLayerConfigurations { * @param project the Gradle {@link Project} * @param gradleJibLogger the build logger for providing feedback about the resolution * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ static JavaLayerConfigurations getForProject( - Project project, GradleJibLogger gradleJibLogger, Path extraDirectory) throws IOException { + Project project, GradleJibLogger gradleJibLogger, Path extraDirectory, String appRoot) + throws IOException { JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); @@ -114,12 +117,21 @@ static JavaLayerConfigurations getForProject( Collections.sort(classesFiles); Collections.sort(extraFiles); + appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/'; return JavaLayerConfigurations.builder() - .setDependencyFiles(dependenciesFiles) - .setSnapshotDependencyFiles(snapshotDependenciesFiles) - .setResourceFiles(resourcesFiles) - .setClassFiles(classesFiles) - .setExtraFiles(extraFiles) + .setDependencyFiles( + dependenciesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .setSnapshotDependencyFiles( + snapshotDependenciesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .setResourceFiles( + resourcesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE) + .setClassFiles( + classesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE) + .setExtraFiles(extraFiles, "/") .build(); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index d3c88855c8..ee41f375f2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -52,12 +52,13 @@ class GradleProjectProperties implements ProjectProperties { /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, GradleJibLogger gradleJibLogger, Path extraDirectory) { + Project project, GradleJibLogger gradleJibLogger, Path extraDirectory, String appRoot) { try { return new GradleProjectProperties( project, gradleJibLogger, - GradleLayerConfigurations.getForProject(project, gradleJibLogger, extraDirectory)); + GradleLayerConfigurations.getForProject( + project, gradleJibLogger, extraDirectory, appRoot)); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 2a2cd969af..fe9b7df018 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -58,6 +58,7 @@ * args = ['arg1', 'arg2'] * exposedPorts = ['1000', '2000-2010', '3000'] * format = OCI + * appRoot = "/app"; * } * } * } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 6efc3a94a4..efb2acc9f7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -33,10 +33,28 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import org.gradle.api.GradleException; /** Configures and provides builders for the image building tasks. */ class PluginConfigurationProcessor { + /** + * Gets the value of the {@code container.appRoot} parameter. Throws {@link GradleException} if it + * is not an absolute path in Unix-style. + * + * @param jibExtension the Jib plugin extension + * @return the app root value + * @throws GradleException if the app root is not an absolute path in Unix-style + */ + static String getAppRootChecked(JibExtension jibExtension) { + String appRoot = jibExtension.getContainer().getAppRoot(); + if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(appRoot)) { + throw new GradleException( + "container.appRoot (" + appRoot + ") is not an absolute Unix-style path"); + } + return appRoot; + } + /** * Sets up {@link BuildConfiguration} that is common among the image building tasks. This includes * setting up the base image reference/authorization, container configuration, cache @@ -85,7 +103,8 @@ static PluginConfigurationProcessor processCommonConfiguration( if (entrypoint.isEmpty()) { String mainClass = projectProperties.getMainClass(jibExtension); entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint(jibExtension.getJvmFlags(), mainClass); + JavaEntrypointConstructor.makeDefaultEntrypoint( + jibExtension.getContainer().getAppRoot(), jibExtension.getJvmFlags(), mainClass); } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java new file mode 100644 index 0000000000..c304bb726f --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.Assert; +import org.junit.Before; +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Test for {@link DockerContextTask}. */ +@RunWith(MockitoJUnitRunner.class) +public class DockerContextTaskTest { + + @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); + + @Mock private ContainerParameters ContainerParameters; + + private DockerContextTask task; + + @Before + public void setUp() throws IOException { + projectRoot.newFolder("build", "jib-docker-context"); + + JibExtension jibExtension = Mockito.mock(JibExtension.class); + Mockito.when(jibExtension.getContainer()).thenReturn(ContainerParameters); + Mockito.when(jibExtension.getExtraDirectoryPath()).thenReturn(projectRoot.getRoot().toPath()); + Mockito.when(jibExtension.getMainClass()).thenReturn("MainClass"); + Mockito.when(jibExtension.getBaseImage()).thenReturn("base image"); + Mockito.when(ContainerParameters.getAppRoot()).thenReturn("/app"); + + Project project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); + project.getPluginManager().apply("java"); + + task = project.getTasks().create("jibExportDockerContext", DockerContextTask.class); + task.setJibExtension(jibExtension); + } + + @Test + public void testEntrypoint() throws IOException { + task.generateDockerContext(); + + Assert.assertEquals( + "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources/:/app/classes/:/app/libs/*\",\"MainClass\"]", + getEntrypoint()); + } + + @Test + public void testEntrypoint_nonDefaultAppRoot() throws IOException { + Mockito.when(ContainerParameters.getAppRoot()).thenReturn("/"); + task.generateDockerContext(); + + Assert.assertEquals( + "ENTRYPOINT [\"java\",\"-cp\",\"/resources/:/classes/:/libs/*\",\"MainClass\"]", + getEntrypoint()); + } + + @Test + public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { + Mockito.when(ContainerParameters.getAppRoot()).thenReturn("relative/path"); + + try { + task.generateDockerContext(); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (relative/path) is not an absolute Unix-style path", ex.getMessage()); + } + } + + @Test + public void testGenerateDockerContext_errorOnWindowsAppRoot() { + Mockito.when(ContainerParameters.getAppRoot()).thenReturn("\\windows\\path"); + + try { + task.generateDockerContext(); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { + Mockito.when(ContainerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); + + try { + task.generateDockerContext(); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (C:\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + private String getEntrypoint() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 065839e021..2153647a3b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -26,8 +26,12 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; @@ -76,6 +80,11 @@ private static ImmutableList getSourceFilesFromLayerEntries( .collect(ImmutableList.toImmutableList()); } + private static List getExtractionPathFromLayerEntries(List layerEntries) { + Stream stream = layerEntries.stream(); + return stream.map(LayerEntry::getAbsoluteExtractionPathString).collect(Collectors.toList()); + } + @Mock private Project mockProject; @Mock private Convention mockConvention; @Mock private JavaPluginConvention mockJavaPluginConvention; @@ -137,13 +146,13 @@ public void test_correctFiles() throws URISyntaxException, IOException { applicationDirectory.resolve("resources/world")); ImmutableList expectedClassesFiles = ImmutableList.of( - applicationDirectory.resolve("classes").resolve("HelloWorld.class"), + applicationDirectory.resolve("classes/HelloWorld.class"), applicationDirectory.resolve("classes/some.class")); ImmutableList expectedExtraFiles = ImmutableList.of(); JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), "/app"); Assert.assertEquals( expectedDependenciesFiles, getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); @@ -169,7 +178,7 @@ public void test_noClassesFiles() throws IOException { .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path")); + mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), "/app"); Mockito.verify(mockGradleJibLogger) .info("Adding corresponding output directories of source sets to image"); @@ -184,7 +193,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, extraFilesDirectory); + mockProject, mockGradleJibLogger, extraFilesDirectory, "/app"); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -199,4 +208,35 @@ public void test_extraFiles() throws URISyntaxException, IOException { expectedExtraFiles, getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); } + + @Test + public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + GradleLayerConfigurations.getForProject( + mockProject, mockGradleJibLogger, extraFilesDirectory, "/my/app"); + + Assert.assertEquals( + Arrays.asList( + "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar"), + getExtractionPathFromLayerEntries(configuration.getDependencyLayerEntries())); + Assert.assertEquals( + Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + getExtractionPathFromLayerEntries(configuration.getSnapshotDependencyLayerEntries())); + Assert.assertEquals( + Arrays.asList( + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"), + getExtractionPathFromLayerEntries(configuration.getResourceLayerEntries())); + Assert.assertEquals( + Arrays.asList("/my/app/classes/HelloWorld.class", "/my/app/classes/some.class"), + getExtractionPathFromLayerEntries(configuration.getClassLayerEntries())); + Assert.assertEquals( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + getExtractionPathFromLayerEntries(configuration.getExtraFilesLayerEntries())); + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 67221e9605..08054b9d64 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -59,11 +59,8 @@ public void testFrom() { from -> { from.setImage("some image"); from.setCredHelper("some cred helper"); - from.auth( - auth -> { - auth.setUsername("some username"); - auth.setPassword("some password"); - }); + from.auth(auth -> auth.setUsername("some username")); + from.auth(auth -> auth.setPassword("some password")); }); Assert.assertEquals("some image", testJibExtension.getFrom().getImage()); Assert.assertEquals("some cred helper", testJibExtension.getFrom().getCredHelper()); @@ -80,11 +77,8 @@ public void testTo() { to -> { to.setImage("some image"); to.setCredHelper("some cred helper"); - to.auth( - auth -> { - auth.setUsername("some username"); - auth.setPassword("some password"); - }); + to.auth(auth -> auth.setUsername("some username")); + to.auth(auth -> auth.setPassword("some password")); }); Assert.assertEquals("some image", testJibExtension.getTo().getImage()); Assert.assertEquals("some cred helper", testJibExtension.getTo().getCredHelper()); @@ -101,6 +95,7 @@ public void testContainer() { Assert.assertEquals(V22ManifestTemplate.class, testJibExtension.getContainer().getFormat()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); + Assert.assertEquals("/app", testJibExtension.getContainer().getAppRoot()); testJibExtension.container( container -> { @@ -112,23 +107,20 @@ public void testContainer() { container.setPorts(Arrays.asList("1000", "2000-2010", "3000")); container.setLabels(ImmutableMap.of("label1", "value1", "label2", "value2")); container.setFormat(ImageFormat.OCI); + container.setAppRoot("some invalid appRoot value"); }); + ContainerParameters container = testJibExtension.getContainer(); + Assert.assertEquals(Arrays.asList("foo", "bar", "baz"), container.getEntrypoint()); + Assert.assertEquals(Arrays.asList("jvmFlag1", "jvmFlag2"), container.getJvmFlags()); Assert.assertEquals( - Arrays.asList("foo", "bar", "baz"), testJibExtension.getContainer().getEntrypoint()); - Assert.assertEquals( - Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getContainer().getJvmFlags()); - Assert.assertEquals( - ImmutableMap.of("var1", "value1", "var2", "value2"), - testJibExtension.getContainer().getEnvironment()); + ImmutableMap.of("var1", "value1", "var2", "value2"), container.getEnvironment()); Assert.assertEquals("mainClass", testJibExtension.getContainer().getMainClass()); + Assert.assertEquals(Arrays.asList("arg1", "arg2", "arg3"), container.getArgs()); + Assert.assertEquals(Arrays.asList("1000", "2000-2010", "3000"), container.getPorts()); Assert.assertEquals( - Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getContainer().getArgs()); - Assert.assertEquals( - Arrays.asList("1000", "2000-2010", "3000"), testJibExtension.getContainer().getPorts()); - Assert.assertEquals( - ImmutableMap.of("label1", "value1", "label2", "value2"), - testJibExtension.getContainer().getLabels()); - Assert.assertEquals(OCIManifestTemplate.class, testJibExtension.getContainer().getFormat()); + ImmutableMap.of("label1", "value1", "label2", "value2"), container.getLabels()); + Assert.assertEquals(OCIManifestTemplate.class, container.getFormat()); + Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 26294b0253..d7c68194c6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.util.Arrays; import java.util.Collections; +import org.gradle.api.GradleException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -46,6 +47,7 @@ public void setUp() throws Exception { Mockito.doReturn(new AuthParameters("mock")).when(mockImageParameters).getAuth(); Mockito.doReturn(mockContainerParameters).when(mockJibExtension).getContainer(); Mockito.doReturn(Collections.emptyList()).when(mockContainerParameters).getEntrypoint(); + Mockito.doReturn("/app").when(mockContainerParameters).getAppRoot(); Mockito.doReturn(JavaLayerConfigurations.builder().build()) .when(mockProjectProperties) @@ -116,5 +118,67 @@ public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceExce .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } + @Test + public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException { + Mockito.doReturn("/my/app").when(mockContainerParameters).getAppRoot(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockGradleJibLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals( + "/my/app/resources/:/my/app/classes/:/my/app/libs/*", configuration.getEntrypoint().get(2)); + } + + @Test + public void testGetAppRootChecked() { + Mockito.doReturn("/some/root").when(mockContainerParameters).getAppRoot(); + + Assert.assertEquals( + "/some/root", PluginConfigurationProcessor.getAppRootChecked(mockJibExtension)); + } + + @Test + public void testGetAppRootChecked_errorOnNonAbsolutePath() { + Mockito.doReturn("relative/path").when(mockContainerParameters).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (relative/path) is not an absolute Unix-style path", ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPath() { + Mockito.doReturn("\\windows\\path").when(mockContainerParameters).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { + Mockito.doReturn("C:\\windows\\path").when(mockContainerParameters).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "container.appRoot (C:\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + // TODO should test other behaviours } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b421d61a25..d61c54e7de 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property) ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) - `` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 59b9a3b57f..a06e133978 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -58,8 +58,10 @@ public void execute() throws MojoExecutionException { } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); + MavenProjectProperties.getForProject( + getProject(), mavenJibLogger, getExtraDirectory(), appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index e2871ea3d0..df3611e6b2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -81,8 +81,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); + MavenProjectProperties.getForProject( + getProject(), mavenJibLogger, getExtraDirectory(), appRoot); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index b2638c395d..0d1c8f98f2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -55,8 +55,10 @@ public void execute() throws MojoExecutionException { } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); + MavenProjectProperties.getForProject( + getProject(), mavenJibLogger, getExtraDirectory(), appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 92ce6fa07c..58bc0cad7b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -46,7 +47,8 @@ public class DockerContextMojo extends JibPluginConfiguration { property = "jibTargetDir", defaultValue = "${project.build.directory}/jib-docker-context", required = true) - private String targetDir; + @VisibleForTesting + String targetDir; @Override public void execute() throws MojoExecutionException { @@ -67,15 +69,23 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException(ex.getMessage(), ex); } + if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(getAppRoot())) { + throw new MojoExecutionException( + " (" + getAppRoot() + ") is not an absolute Unix-style path"); + } + Preconditions.checkNotNull(targetDir); + String appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), mavenJibLogger, getExtraDirectory()); + MavenProjectProperties.getForProject( + getProject(), mavenJibLogger, getExtraDirectory(), appRoot); List entrypoint = getEntrypoint(); if (entrypoint.isEmpty()) { String mainClass = mavenProjectProperties.getMainClass(this); - entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint(getJvmFlags(), mainClass); + entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint(getAppRoot(), getJvmFlags(), mainClass); } else if (getMainClass() != null || !getJvmFlags().isEmpty()) { mavenJibLogger.warn(" and are ignored when is specified"); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 28bf0d49ff..e44830b973 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -79,8 +80,8 @@ void setPassword(String password) { } private void setPropertyDescriptors(String descriptorPrefix) { - this.usernameDescriptor = descriptorPrefix + ""; - this.passwordDescriptor = descriptorPrefix + ""; + usernameDescriptor = descriptorPrefix + ""; + passwordDescriptor = descriptorPrefix + ""; } } @@ -135,6 +136,8 @@ public static class ContainerParameters { @Parameter private List ports = Collections.emptyList(); @Parameter private Map labels = Collections.emptyMap(); + + @Parameter private String appRoot = JavaLayerConfigurations.DEFAULT_APP_ROOT; } @Nullable @@ -289,6 +292,10 @@ Map getLabels() { return container.labels; } + String getAppRoot() { + return container.appRoot; + } + String getFormat() { return Preconditions.checkNotNull(container.format); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 75afbf19bf..c5b1ccc4d2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.io.IOException; import java.nio.file.FileSystems; @@ -38,11 +39,12 @@ class MavenLayerConfigurations { * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the project * @throws IOException if collecting the project files fails */ - static JavaLayerConfigurations getForProject(MavenProject project, Path extraDirectory) - throws IOException { + static JavaLayerConfigurations getForProject( + MavenProject project, Path extraDirectory, String appRoot) throws IOException { Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory()); Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); @@ -103,12 +105,21 @@ static JavaLayerConfigurations getForProject(MavenProject project, Path extraDir Collections.sort(classesFiles); Collections.sort(extraFiles); + appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/'; return JavaLayerConfigurations.builder() - .setDependencyFiles(dependenciesFiles) - .setSnapshotDependencyFiles(snapshotDependenciesFiles) - .setResourceFiles(resourcesFiles) - .setClassFiles(classesFiles) - .setExtraFiles(extraFiles) + .setDependencyFiles( + dependenciesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .setSnapshotDependencyFiles( + snapshotDependenciesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .setResourceFiles( + resourcesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE) + .setClassFiles( + classesFiles, + appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE) + .setExtraFiles(extraFiles, "/") .build(); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 389e68f395..f0cd99e4ea 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -50,15 +50,18 @@ public class MavenProjectProperties implements ProjectProperties { * @param project the {@link MavenProject} for the plugin. * @param mavenJibLogger the logger used for printing status messages. * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed * @return a MavenProjectProperties from the given project and logger. * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, MavenJibLogger mavenJibLogger, Path extraDirectory) + MavenProject project, MavenJibLogger mavenJibLogger, Path extraDirectory, String appRoot) throws MojoExecutionException { try { return new MavenProjectProperties( - project, mavenJibLogger, MavenLayerConfigurations.getForProject(project, extraDirectory)); + project, + mavenJibLogger, + MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); } catch (IOException ex) { throw new MojoExecutionException( "Obtaining project build output files failed; make sure you have compiled your project " diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index d3d2ef9a1f..669e83403e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -38,6 +38,24 @@ /** Configures and provides builders for the image building goals. */ class PluginConfigurationProcessor { + /** + * Gets the value of the {@code } parameter. Throws {@link + * MojoExecutionException} if it is not an absolute path in Unix-style. + * + * @param jibPluginConfiguration the Jib plugin configuration + * @return the app root value + * @throws MojoExecutionException if the app root is not an absolute path in Unix-style + */ + static String getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) + throws MojoExecutionException { + String appRoot = jibPluginConfiguration.getAppRoot(); + if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(appRoot)) { + throw new MojoExecutionException( + " (" + appRoot + ") is not an absolute Unix-style path"); + } + return appRoot; + } + /** * Sets up {@link BuildConfiguration} that is common among the image building goals. This includes * setting up the base image reference/authorization, container configuration, cache @@ -108,7 +126,7 @@ static PluginConfigurationProcessor processCommonConfiguration( String mainClass = projectProperties.getMainClass(jibPluginConfiguration); entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint( - jibPluginConfiguration.getJvmFlags(), mainClass); + jibPluginConfiguration.getAppRoot(), jibPluginConfiguration.getJvmFlags(), mainClass); } else if (jibPluginConfiguration.getMainClass() != null || !jibPluginConfiguration.getJvmFlags().isEmpty()) { logger.warn(" and are ignored when is specified"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java new file mode 100644 index 0000000000..3d3d68fd9a --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.maven.model.Build; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DockerContextMojo}. */ +@RunWith(MockitoJUnitRunner.class) +public class DockerContextMojoTest { + + @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); + + private DockerContextMojo mojo; + private String appRoot = "/app"; + + @Before + public void setUp() throws IOException { + File outputFolder = projectRoot.newFolder("target"); + + MavenProject project = Mockito.mock(MavenProject.class); + Build build = Mockito.mock(Build.class); + Mockito.when(project.getBuild()).thenReturn(build); + Mockito.when(build.getSourceDirectory()).thenReturn(projectRoot.newFolder("src").toString()); + Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); + + mojo = + new DockerContextMojo() { + @Override + MavenProject getProject() { + return project; + } + + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } + + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + String getAppRoot() { + return appRoot; + } + }; + mojo.targetDir = outputFolder.toString(); + } + + @Test + public void testEntrypoint() throws MojoExecutionException, IOException { + mojo.execute(); + + Assert.assertEquals( + "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources/:/app/classes/:/app/libs/*\",\"MainClass\"]", + getEntrypoint()); + } + + @Test + public void testEntrypoint_nonDefaultAppRoot() throws MojoExecutionException, IOException { + appRoot = "/"; + mojo.execute(); + + Assert.assertEquals( + "ENTRYPOINT [\"java\",\"-cp\",\"/resources/:/classes/:/libs/*\",\"MainClass\"]", + getEntrypoint()); + } + + @Test + public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { + appRoot = "relative/path"; + + try { + mojo.execute(); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (relative/path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGenerateDockerContext_errorOnWindowsAppRoot() { + appRoot = "\\windows\\path"; + + try { + mojo.execute(); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { + appRoot = "C:\\windows\\path"; + + try { + mojo.execute(); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (C:\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + private String getEntrypoint() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 9737986189..ae17c93689 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -59,6 +59,7 @@ public void testAuthDefaults() { Assert.assertEquals( "", testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); + Assert.assertEquals("/app", testPluginConfiguration.getAppRoot()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 3765964eaf..08a0159f9c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -25,7 +25,12 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; @@ -50,6 +55,11 @@ private static ImmutableList getSourceFilesFromLayerEntries( .collect(ImmutableList.toImmutableList()); } + private static List getExtractionPathFromLayerEntries(List layerEntries) { + Stream stream = layerEntries.stream(); + return stream.map(LayerEntry::getAbsoluteExtractionPathString).collect(Collectors.toList()); + } + @Rule public TestRepository testRepository = new TestRepository(); @Mock private MavenProject mockMavenProject; @@ -103,7 +113,8 @@ public void test_correctFiles() throws URISyntaxException, IOException { applicationDirectory.resolve("output/some.class")); JavaLayerConfigurations javaLayerConfigurations = - MavenLayerConfigurations.getForProject(mockMavenProject, Paths.get("nonexistent/path")); + MavenLayerConfigurations.getForProject( + mockMavenProject, Paths.get("nonexistent/path"), "/app"); Assert.assertEquals( expectedDependenciesFiles, getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); @@ -124,7 +135,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); JavaLayerConfigurations javaLayerConfigurations = - MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory); + MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, "/app"); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -140,6 +151,45 @@ public void test_extraFiles() throws URISyntaxException, IOException { getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); } + @Test + public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, "/my/app"); + + Assert.assertEquals( + // on windows, these files may be in a different order, so use Set + new HashSet<>( + Arrays.asList( + "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar")), + new HashSet<>( + getExtractionPathFromLayerEntries(configuration.getDependencyLayerEntries()))); + Assert.assertEquals( + Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + getExtractionPathFromLayerEntries(configuration.getSnapshotDependencyLayerEntries())); + Assert.assertEquals( + Arrays.asList( + "/my/app/resources/directory", + "/my/app/resources/directory/somefile", + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"), + getExtractionPathFromLayerEntries(configuration.getResourceLayerEntries())); + Assert.assertEquals( + Arrays.asList( + "/my/app/classes/HelloWorld.class", + "/my/app/classes/package", + "/my/app/classes/package/some.class", + "/my/app/classes/some.class"), + getExtractionPathFromLayerEntries(configuration.getClassLayerEntries())); + Assert.assertEquals( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + getExtractionPathFromLayerEntries(configuration.getExtraFilesLayerEntries())); + } + private Artifact makeArtifact(Path path) { Artifact artifact = Mockito.mock(Artifact.class); Mockito.when(artifact.getFile()).thenReturn(path.toFile()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index 0cd2f5e7e9..e1fb9ed48e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -53,6 +53,7 @@ public void setUp() throws Exception { Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getJvmFlags(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getArgs(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getExposedPorts(); + Mockito.doReturn("/app").when(mockJibPluginConfiguration).getAppRoot(); Mockito.doReturn(JavaLayerConfigurations.builder().build()) .when(mockProjectProperties) @@ -125,5 +126,68 @@ public void testEntrypoint_warningOnMainclass() throws MojoExecutionException { .warn(" and are ignored when is specified"); } + @Test + public void testEntrypointClasspath_nonDefaultAppRoot() throws MojoExecutionException { + Mockito.doReturn("/my/app").when(mockJibPluginConfiguration).getAppRoot(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals( + "/my/app/resources/:/my/app/classes/:/my/app/libs/*", configuration.getEntrypoint().get(2)); + } + + @Test + public void testGetAppRootChecked() throws MojoExecutionException { + Mockito.doReturn("/some/root").when(mockJibPluginConfiguration).getAppRoot(); + + Assert.assertEquals( + "/some/root", PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); + } + + @Test + public void testGetAppRootChecked_errorOnNonAbsolutePath() { + Mockito.doReturn("relative/path").when(mockJibPluginConfiguration).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (relative/path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPath() { + Mockito.doReturn("\\windows\\path").when(mockJibPluginConfiguration).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { + Mockito.doReturn("C:\\windows\\path").when(mockJibPluginConfiguration).getAppRoot(); + + try { + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals( + " (C:\\windows\\path) is not an absolute Unix-style path", + ex.getMessage()); + } + } + // TODO should test other behaviours } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 490a2c55ba..9c1eb34431 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; import java.util.Optional; import javax.annotation.Nullable; @@ -119,5 +121,20 @@ public static ImageReference getGeneratedTargetDockerTag( } } + /** + * Checks if the path string is absolute in Unix-style (using forward slashes). + * + * @param path path to test + * @return {@code true} if absolute in Unix-style; {@code false} otherwise + */ + public static boolean isAbsoluteUnixPath(String path) { + try { + Paths.get(path); + } catch (InvalidPathException ex) { + return false; + } + return path.startsWith("/"); + } + private ConfigurationPropertyValidator() {} } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 2cb08817e9..a26bd19e5f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -139,4 +139,34 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept } catch (InvalidImageReferenceException ignored) { } } + + @Test + public void testIsAbsoluteUnixPath_emptyString() { + Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("")); + } + + @Test + public void testIsAbsoluteUnixPath_root() { + Assert.assertTrue(ConfigurationPropertyValidator.isAbsoluteUnixPath("/")); + } + + @Test + public void testIsAbsoluteUnixPath() { + Assert.assertTrue(ConfigurationPropertyValidator.isAbsoluteUnixPath("/some/path")); + } + + @Test + public void testIsAbsoluteUnixPath_nonAbsolute() { + Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("relative/path")); + } + + @Test + public void testIsAbsoluteUnixPath_windowsPath() { + Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("\\windows\\path")); + } + + @Test + public void testIsAbsoluteUnixPath_windowsPathWithDriveLetter() { + Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("C:\\windows\\path")); + } } From 9109122f076618c82c792705176d28d58de0c67f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Sep 2018 08:57:17 -0400 Subject: [PATCH 0217/2020] Wait until local registry is ready when setting it up in integration tests (#1006) --- .../tools/jib/registry/LocalRegistry.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 04f2bb1920..e5c0306603 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -18,6 +18,9 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -53,20 +56,14 @@ protected void before() throws IOException, InterruptedException { ArrayList dockerTokens = new ArrayList<>( Arrays.asList( - "docker", - "run", - "-d", - "-p", - port + ":5000", - "--restart=always", - "--name", - containerName)); + "docker", "run", "--rm", "-d", "-p", port + ":5000", "--name", containerName)); if (username != null && password != null) { // Generate the htpasswd file to store credentials String credentialString = new Command( "docker", "run", + "--rm", "--entrypoint", "htpasswd", "registry:2", @@ -96,6 +93,7 @@ protected void before() throws IOException, InterruptedException { } dockerTokens.add("registry:2"); new Command(dockerTokens).run(); + waitUntilReady(); } @Override @@ -103,7 +101,6 @@ protected void after() { try { logout(); new Command("docker", "stop", containerName).run(); - new Command("docker", "rm", "-v", containerName).run(); } catch (InterruptedException | IOException ex) { throw new RuntimeException("Could not stop local registry fully: " + containerName, ex); @@ -151,4 +148,20 @@ private void logout() throws IOException, InterruptedException { new Command("docker", "logout", "localhost:" + port).run(); } } + + private void waitUntilReady() throws InterruptedException, MalformedURLException { + URL queryUrl = new URL("http://localhost:" + port + "/v2/_catalog"); + + for (int i = 0; i < 40; i++) { + try { + HttpURLConnection connection = (HttpURLConnection) queryUrl.openConnection(); + int code = connection.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK || code == HttpURLConnection.HTTP_UNAUTHORIZED) { + return; + } + } catch (IOException ex) { + } + Thread.sleep(250); + } + } } From 4b950979c5368f7b71af0b806ec2ac3b7c576b18 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Sep 2018 10:41:24 -0400 Subject: [PATCH 0218/2020] Delete container after run in integration tests (#1016) --- .../builder/BuildStepsIntegrationTest.java | 22 ++++++++++--------- .../jib/gradle/JibPluginIntegrationTest.java | 12 +++++----- .../maven/BuildDockerMojoIntegrationTest.java | 2 +- .../maven/BuildImageMojoIntegrationTest.java | 4 ++-- .../maven/BuildTarMojoIntegrationTest.java | 3 ++- .../DockerContextMojoIntegrationTest.java | 3 ++- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index d4db5cbc01..1b5299c949 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -138,7 +138,7 @@ public void testSteps_forBuildToDockerRegistry() localRegistry.pull(imageReference); assertDockerInspect(imageReference); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); } @Test @@ -164,19 +164,21 @@ public void testSteps_forBuildToDockerRegistry_multipleTags() localRegistry.pull(imageReference); assertDockerInspect(imageReference); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); String imageReference2 = "localhost:5000/testimage:testtag2"; localRegistry.pull(imageReference2); assertDockerInspect(imageReference2); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference2).run()); + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReference2).run()); String imageReference3 = "localhost:5000/testimage:testtag3"; localRegistry.pull(imageReference3); assertDockerInspect(imageReference3); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference3).run()); + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReference3).run()); } @Test @@ -194,7 +196,7 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() String imageReference = "localhost:5000/testimage:testtag"; new Command("docker", "pull", imageReference).run(); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); } @Test @@ -215,7 +217,7 @@ public void testSteps_forBuildToDockerDaemon() assertDockerInspect(imageReference); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); } @Test @@ -237,15 +239,15 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() assertDockerInspect(imageReference); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); assertDockerInspect(imageReference + ":testtag2"); Assert.assertEquals( "Hello, world. An argument.\n", - new Command("docker", "run", imageReference + ":testtag2").run()); + new Command("docker", "run", "--rm", imageReference + ":testtag2").run()); assertDockerInspect(imageReference + ":testtag3"); Assert.assertEquals( "Hello, world. An argument.\n", - new Command("docker", "run", imageReference + ":testtag3").run()); + new Command("docker", "run", "--rm", imageReference + ":testtag3").run()); } @Test @@ -267,7 +269,7 @@ public void testSteps_forBuildToTarball() new Command("docker", "load", "--input", outputPath.toString()).run(); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", "testtar").run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); } private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws IOException { diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 7209a6baca..8c578b69d9 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -65,7 +65,7 @@ private static String buildAndRun(TestProject testProject, String imageReference assertDockerInspect(imageReference); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } private static String buildAndRunComplex( @@ -88,7 +88,7 @@ private static String buildAndRunComplex( assertDockerInspect(imageReference); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } private static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) @@ -103,7 +103,7 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String assertDockerInspect(imageReference); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } /** @@ -296,7 +296,8 @@ public void testBuildTar_simple() throws IOException, InterruptedException { new Command("docker", "load", "--input", outputPath).run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "--rm", targetImage).run()); assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -324,7 +325,8 @@ public void testDockerContext() throws IOException, InterruptedException { assertDockerInspect(imageName); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "--rm", imageName).run()); // Checks that generating the Docker context again is skipped. BuildTask upToDateJibDockerContextTask = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 7f6860e8c4..69cd1ee18f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -76,7 +76,7 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe + " \"key1\": \"value1\",\n" + " \"key2\": \"value2\"\n" + " }")); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index c8871e668f..b9c784639e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -100,7 +100,7 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole new Command("docker", "pull", imageReference).run(); assertDockerInspectParameters(imageReference); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } private static String buildAndRunComplex( @@ -125,7 +125,7 @@ private static String buildAndRunComplex( Instant.parse( new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); - return new Command("docker", "run", imageReference).run(); + return new Command("docker", "run", "--rm", imageReference).run(); } private static void assertDockerInspectParameters(String imageReference) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index b04bf8fb76..25288e905b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -63,7 +63,8 @@ public void testExecute_simple() throws VerificationException, IOException, Inte .toString()) .run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", targetImage).run()); + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "--rm", targetImage).run()); Instant buildTime = Instant.parse( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index b8346e8de6..4003444432 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -73,7 +73,8 @@ public void testExecute() throws VerificationException, IOException, Interrupted + " }")); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", new Command("docker", "run", imageName).run()); + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "--rm", imageName).run()); } public void testExecute_skipJibGoal() throws VerificationException, IOException { From 6825cd287123b6e54519edd6eba447a61a7a71e2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Sep 2018 10:42:35 -0400 Subject: [PATCH 0219/2020] Add "format" target (#1015) --- build.sh | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/build.sh b/build.sh index eca20c54b6..00fee12d02 100755 --- a/build.sh +++ b/build.sh @@ -9,8 +9,8 @@ usage() { eval 1>&2 echo "Simple builder for Jib for jib-core, jib-plugins-common, jib-maven-plugin, and jib-gradle-plugin" - echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | all | it]" - echo " 'all' is the same as 'core plugins gradle maven'" + echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | it | format]" + echo " providing no target is the same as 'core plugins gradle maven'" echo " -q quick mode: skip tests, formatting" echo " -e show error information (mvn: -e, gradle: --stacktrace --info)" exit 1 @@ -32,13 +32,13 @@ while getopts qe c; do \?) usage;; esac done -shift `expr $OPTIND - 1` +shift $( expr $OPTIND - 1 ) if [ $# -eq 0 ]; then set -- core plugins gradle maven fi -set -e # exit on error +set -o errexit # exit on error for target in "$@"; do case "$target" in clean) @@ -50,9 +50,9 @@ for target in "$@"; do core) if [ "$quickMode" = false ]; then - doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build + doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build else - doBuild jib-core ./gradlew $gradleOptions build \ + doBuild jib-core ./gradlew $gradleOptions build \ --exclude-task test --exclude-task check fi ;; @@ -68,27 +68,34 @@ for target in "$@"; do maven) if [ "$quickMode" = false ]; then - doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U + doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U else # jib-maven-plugin pulls in jib-core directly - doBuild jib-maven-plugin ./mvnw $mavenOptions -Dcheckstyle.skip -Dfmt.skip -DskipTests install -U + doBuild jib-maven-plugin ./mvnw $mavenOptions -Dcheckstyle.skip -Dfmt.skip -DskipTests install -U fi ;; gradle) if [ "$quickMode" = false ]; then - doBuild jib-gradle-plugin ./gradlew $gradleOptions googleJavaFormat build install + doBuild jib-gradle-plugin ./gradlew $gradleOptions googleJavaFormat build install else # jib-gradle-plugin pulls in jib-core directly - doBuild jib-gradle-plugin ./gradlew $gradleOptions build install \ + doBuild jib-gradle-plugin ./gradlew $gradleOptions build install \ --exclude-task test --exclude-task check fi ;; it) - doBuild jib-core ./gradlew $gradleOptions integrationTest - doBuild jib-maven-plugin ./mvnw $mavenOptions -Pintegration-tests verify -U - doBuild jib-gradle-plugin ./gradlew $gradleOptions integrationTest + doBuild jib-core ./gradlew $gradleOptions integrationTest + doBuild jib-maven-plugin ./mvnw $mavenOptions -Pintegration-tests verify -U + doBuild jib-gradle-plugin ./gradlew $gradleOptions integrationTest + ;; + + format) + doBuild jib-core ./gradlew googleJavaFormat + doBuild jib-plugins-common ./gradlew googleJavaFormat + doBuild jib-gradle-plugin ./gradlew googleJavaFormat + doBuild jib-maven-plugin ./mvnw fmt:format ;; esac done From fd891637cad19d5c162d1e0ef8bea16e9fb69082 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Sep 2018 11:44:57 -0400 Subject: [PATCH 0220/2020] Use AbsoluteUnixPath for container.appRoot propagation (#1012) --- .../builder/BuildStepsIntegrationTest.java | 3 +- .../frontend/JavaDockerContextGenerator.java | 29 +++++---- .../frontend/JavaEntrypointConstructor.java | 27 ++++---- .../jib/frontend/JavaLayerConfigurations.java | 47 +++++++------- .../JavaDockerContextGeneratorTest.java | 29 +++++---- .../JavaEntrypointConstructorTest.java | 62 +++---------------- .../frontend/JavaLayerConfigurationsTest.java | 40 ++++++++---- jib-core/src/test/resources/sampleDockerfile | 2 +- .../tools/jib/gradle/BuildDockerTask.java | 3 +- .../tools/jib/gradle/BuildImageTask.java | 3 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 3 +- .../tools/jib/gradle/DockerContextTask.java | 3 +- .../jib/gradle/GradleLayerConfigurations.java | 17 ++--- .../jib/gradle/GradleProjectProperties.java | 8 +-- .../gradle/PluginConfigurationProcessor.java | 13 ++-- .../jib/gradle/DockerContextTaskTest.java | 11 ++-- .../gradle/GradleLayerConfigurationsTest.java | 11 ++-- .../PluginConfigurationProcessorTest.java | 16 ++--- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 3 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../tools/jib/maven/DockerContextMojo.java | 11 +--- .../jib/maven/MavenLayerConfigurations.java | 14 ++--- .../jib/maven/MavenProjectProperties.java | 7 ++- .../maven/PluginConfigurationProcessor.java | 14 +++-- .../jib/maven/DockerContextMojoTest.java | 13 ++-- .../maven/MavenLayerConfigurationsTest.java | 9 ++- .../PluginConfigurationProcessorTest.java | 15 ++--- .../ConfigurationPropertyValidator.java | 17 ----- .../ConfigurationPropertyValidatorTest.java | 30 --------- 30 files changed, 210 insertions(+), 256 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 1b5299c949..2e31c4ae70 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.image.ImageReference; @@ -286,7 +287,7 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( ContainerConfiguration.builder() .setEntrypoint( JavaEntrypointConstructor.makeDefaultEntrypoint( - "/app", Collections.emptyList(), "HelloWorld")) + AbsoluteUnixPath.get("/app"), Collections.emptyList(), "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) .setExposedPorts( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 027b651d78..7b08194955 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -74,10 +75,12 @@ private static class CopyDirective { private final String directoryInContext; /** The extraction path in the image. */ - private final String extractionPath; + private final AbsoluteUnixPath extractionPath; private CopyDirective( - ImmutableList layerEntries, String directoryInContext, String extractionPath) { + ImmutableList layerEntries, + String directoryInContext, + AbsoluteUnixPath extractionPath) { this.layerEntries = layerEntries; this.directoryInContext = directoryInContext; this.extractionPath = extractionPath; @@ -96,7 +99,7 @@ private static void addIfNotEmpty( ImmutableList.Builder listBuilder, ImmutableList layerEntries, String directoryInContext, - String extractionPath) { + AbsoluteUnixPath extractionPath) { if (layerEntries.isEmpty()) { return; } @@ -282,9 +285,9 @@ public void generate(Path targetDirectory) throws IOException { // 'baseExtractionPath' of '/app/classes', and an 'actualExtractionPath' of // '/app/classes/com/test/HelloWorld.class', the resolved destination would be // 'target/jib-docker-context/classes/com/test/HelloWorld.class'. - Path destination = - directoryInContext.resolve( - Paths.get(copyDirective.extractionPath).relativize(layerEntry.getExtractionPath())); + Path baseExtractionPath = Paths.get(copyDirective.extractionPath.toString()); + Path relativeEntryPath = baseExtractionPath.relativize(layerEntry.getExtractionPath()); + Path destination = directoryInContext.resolve(relativeEntryPath); if (Files.isDirectory(layerEntry.getSourceFile())) { Files.createDirectories(destination); @@ -305,11 +308,11 @@ public void generate(Path targetDirectory) throws IOException { *

{@code
    * FROM [base image]
    *
-   * COPY libs [path/to/dependencies]
-   * COPY snapshot-libs [path/to/dependencies]
-   * COPY resources [path/to/resources]
-   * COPY classes [path/to/classes]
-   * COPY root [path/to/classes]
+   * COPY libs [path/to/dependencies/]
+   * COPY snapshot-libs [path/to/dependencies/]
+   * COPY resources [path/to/resources/]
+   * COPY classes [path/to/classes/]
+   * COPY root [path/to/root/]
    *
    * EXPOSE [port]
    * [More EXPOSE instructions, if necessary]
@@ -330,11 +333,13 @@ String makeDockerfile() throws JsonProcessingException {
     StringBuilder dockerfile = new StringBuilder();
     dockerfile.append("FROM ").append(Preconditions.checkNotNull(baseImage)).append("\n");
     for (CopyDirective copyDirective : copyDirectives) {
+      boolean hasTrailingSlash = copyDirective.extractionPath.toString().endsWith("/");
       dockerfile
           .append("\nCOPY ")
           .append(copyDirective.directoryInContext)
           .append(" ")
-          .append(copyDirective.extractionPath);
+          .append(copyDirective.extractionPath)
+          .append(hasTrailingSlash ? "" : "/");
     }
 
     dockerfile.append("\n");
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java
index 0592d2c869..740520e77e 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java
@@ -16,7 +16,8 @@
 
 package com.google.cloud.tools.jib.frontend;
 
-import com.google.common.base.Preconditions;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
+import com.google.cloud.tools.jib.filesystem.RelativeUnixPath;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -24,21 +25,23 @@
 /** Constructs an image entrypoint for the Java application. */
 public class JavaEntrypointConstructor {
 
-  public static final String DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE = "resources/";
-  public static final String DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE = "classes/";
-  public static final String DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE = "libs/";
+  public static final RelativeUnixPath DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE =
+      RelativeUnixPath.get("resources");
+  public static final RelativeUnixPath DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE =
+      RelativeUnixPath.get("classes");
+  public static final RelativeUnixPath DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE =
+      RelativeUnixPath.get("libs");
 
   public static List makeDefaultEntrypoint(
-      String appRoot, List jvmFlags, String mainClass) {
-    Preconditions.checkArgument(
-        appRoot.startsWith("/"), "appRoot should be an absolute path in Unix-style: " + appRoot);
-    appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/';
-
+      AbsoluteUnixPath appRoot, List jvmFlags, String mainClass) {
     return makeEntrypoint(
         Arrays.asList(
-            appRoot + DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE,
-            appRoot + DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE,
-            appRoot + DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE + "*"),
+            appRoot.resolve(DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE).toString(),
+            appRoot.resolve(DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE).toString(),
+            appRoot
+                .resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE)
+                .resolve(RelativeUnixPath.get("*"))
+                .toString()),
         jvmFlags,
         mainClass);
   }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
index cff962aa8d..8351458fc4 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
@@ -17,6 +17,7 @@
 package com.google.cloud.tools.jib.frontend;
 
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -66,41 +67,42 @@ String getName() {
   public static class Builder {
 
     private final Map> layerFilesMap = new EnumMap<>(LayerType.class);
-    private final Map extractionPathMap = new EnumMap<>(LayerType.class);
+    private final Map extractionPathMap =
+        new EnumMap<>(LayerType.class);
 
     private Builder() {
       for (LayerType layerType : LayerType.values()) {
         layerFilesMap.put(layerType, new ArrayList<>());
-        extractionPathMap.put(layerType, "/");
+        extractionPathMap.put(layerType, AbsoluteUnixPath.get("/"));
       }
     }
 
-    public Builder setDependencyFiles(List dependencyFiles, String extractionPath) {
+    public Builder setDependencyFiles(List dependencyFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.DEPENDENCIES, dependencyFiles);
       extractionPathMap.put(LayerType.DEPENDENCIES, extractionPath);
       return this;
     }
 
     public Builder setSnapshotDependencyFiles(
-        List snapshotDependencyFiles, String extractionPath) {
+        List snapshotDependencyFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependencyFiles);
       extractionPathMap.put(LayerType.SNAPSHOT_DEPENDENCIES, extractionPath);
       return this;
     }
 
-    public Builder setResourceFiles(List resourceFiles, String extractionPath) {
+    public Builder setResourceFiles(List resourceFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.RESOURCES, resourceFiles);
       extractionPathMap.put(LayerType.RESOURCES, extractionPath);
       return this;
     }
 
-    public Builder setClassFiles(List classFiles, String extractionPath) {
+    public Builder setClassFiles(List classFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.CLASSES, classFiles);
       extractionPathMap.put(LayerType.CLASSES, extractionPath);
       return this;
     }
 
-    public Builder setExtraFiles(List extraFiles, String extractionPath) {
+    public Builder setExtraFiles(List extraFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.EXTRA_FILES, extraFiles);
       extractionPathMap.put(LayerType.EXTRA_FILES, extractionPath);
       return this;
@@ -108,7 +110,8 @@ public Builder setExtraFiles(List extraFiles, String extractionPath) {
 
     // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot
     // dependencies, resources, and classes layers).
-    public Builder setExplodedWarFiles(List explodedWarFiles, String extractionPath) {
+    public Builder setExplodedWarFiles(
+        List explodedWarFiles, AbsoluteUnixPath extractionPath) {
       layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWarFiles);
       extractionPathMap.put(LayerType.EXPLODED_WAR, extractionPath);
       return this;
@@ -118,11 +121,8 @@ public JavaLayerConfigurations build() throws IOException {
       ImmutableMap.Builder layerConfigurationsMap =
           ImmutableMap.builderWithExpectedSize(LayerType.values().length);
       for (LayerType layerType : LayerType.values()) {
-        String extractionPath = Preconditions.checkNotNull(extractionPathMap.get(layerType));
-        // Windows filenames cannot have "/", so this also blocks Windows-style path.
-        Preconditions.checkState(
-            extractionPath.startsWith("/"),
-            "extractionPath should be an absolute path in Unix-style: " + extractionPath);
+        AbsoluteUnixPath extractionPath =
+            Preconditions.checkNotNull(extractionPathMap.get(layerType));
 
         LayerConfiguration.Builder layerConfigurationBuilder =
             LayerConfiguration.builder().setName(layerType.getName());
@@ -130,7 +130,8 @@ public JavaLayerConfigurations build() throws IOException {
         // Adds all the layer files recursively.
         List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType));
         for (Path layerFile : layerFiles) {
-          Path pathInContainer = Paths.get(extractionPath).resolve(layerFile.getFileName());
+          Path pathInContainer =
+              Paths.get(extractionPath.toString()).resolve(layerFile.getFileName());
           layerConfigurationBuilder.addEntryRecursive(layerFile, pathInContainer);
         }
 
@@ -152,11 +153,11 @@ public static Builder builder() {
   public static final String DEFAULT_APP_ROOT = "/app";
 
   private final ImmutableMap layerConfigurationMap;
-  private final ImmutableMap extractionPathMap;
+  private final ImmutableMap extractionPathMap;
 
   private JavaLayerConfigurations(
       ImmutableMap layerConfigurationMap,
-      ImmutableMap extractionPathMap) {
+      ImmutableMap extractionPathMap) {
     this.layerConfigurationMap = layerConfigurationMap;
     this.extractionPathMap = extractionPathMap;
   }
@@ -191,27 +192,27 @@ public ImmutableList getExplodedWarEntries() {
     return getLayerEntries(LayerType.EXPLODED_WAR);
   }
 
-  public String getDependencyExtractionPath() {
+  public AbsoluteUnixPath getDependencyExtractionPath() {
     return getExtractionPath(LayerType.DEPENDENCIES);
   }
 
-  public String getSnapshotDependencyExtractionPath() {
+  public AbsoluteUnixPath getSnapshotDependencyExtractionPath() {
     return getExtractionPath(LayerType.SNAPSHOT_DEPENDENCIES);
   }
 
-  public String getResourceExtractionPath() {
+  public AbsoluteUnixPath getResourceExtractionPath() {
     return getExtractionPath(LayerType.RESOURCES);
   }
 
-  public String getClassExtractionPath() {
+  public AbsoluteUnixPath getClassExtractionPath() {
     return getExtractionPath(LayerType.CLASSES);
   }
 
-  public String getExtraFilesExtractionPath() {
+  public AbsoluteUnixPath getExtraFilesExtractionPath() {
     return getExtractionPath(LayerType.EXTRA_FILES);
   }
 
-  public String getExplodedWarExtractionPath() {
+  public AbsoluteUnixPath getExplodedWarExtractionPath() {
     return getExtractionPath(LayerType.EXPLODED_WAR);
   }
 
@@ -219,7 +220,7 @@ private ImmutableList getLayerEntries(LayerType layerType) {
     return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries();
   }
 
-  private String getExtractionPath(LayerType layerType) {
+  private AbsoluteUnixPath getExtractionPath(LayerType layerType) {
     return Preconditions.checkNotNull(extractionPathMap.get(layerType));
   }
 }
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
index 036c4b7dd9..ca69fb5827 100644
--- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
+++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.frontend;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.filesystem.DirectoryWalker;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.collect.ImmutableList;
@@ -116,16 +117,17 @@ public void testGenerate() throws IOException, URISyntaxException {
         .thenReturn(filesToLayerEntries(testDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH)));
 
     Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath())
-        .thenReturn(EXPECTED_RESOURCES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_RESOURCES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath())
-        .thenReturn(EXPECTED_CLASSES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_CLASSES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath())
-        .thenReturn(EXPECTED_EXPLODED_WAR_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()).thenReturn("/");
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_EXPLODED_WAR_PATH));
+    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath())
+        .thenReturn(AbsoluteUnixPath.get("/"));
 
     new JavaDockerContextGenerator(mockJavaLayerConfigurations)
         .setBaseImage("somebaseimage")
@@ -175,23 +177,24 @@ public void testMakeDockerfile() throws IOException {
         .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get("/"))));
 
     Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath())
-        .thenReturn(EXPECTED_RESOURCES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_RESOURCES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath())
-        .thenReturn(EXPECTED_CLASSES_PATH);
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_CLASSES_PATH));
     Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath())
-        .thenReturn(EXPECTED_EXPLODED_WAR_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()).thenReturn("/");
+        .thenReturn(AbsoluteUnixPath.get(EXPECTED_EXPLODED_WAR_PATH));
+    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath())
+        .thenReturn(AbsoluteUnixPath.get("/"));
 
     String dockerfile =
         new JavaDockerContextGenerator(mockJavaLayerConfigurations)
             .setBaseImage(expectedBaseImage)
             .setEntrypoint(
                 JavaEntrypointConstructor.makeDefaultEntrypoint(
-                    "/app", expectedJvmFlags, expectedMainClass))
+                    AbsoluteUnixPath.get("/app"), expectedJvmFlags, expectedMainClass))
             .setJavaArguments(expectedJavaArguments)
             .setEnvironment(expectedEnv)
             .setExposedPorts(exposedPorts)
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java
index 2d14e11bfe..e97c0f9558 100644
--- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java
+++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.frontend;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -27,8 +28,8 @@ public class JavaEntrypointConstructorTest {
 
   @Test
   public void testMakeEntrypoint() {
-    String expectedResourcesPath = "/app/resources/";
-    String expectedClassesPath = "/app/classes/";
+    String expectedResourcesPath = "/app/resources";
+    String expectedClassesPath = "/app/classes";
     String expectedDependenciesPath = "/app/libs/*";
     List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag");
     String expectedMainClass = "SomeMainClass";
@@ -44,14 +45,14 @@ public void testMakeEntrypoint() {
             "-flag",
             "anotherFlag",
             "-cp",
-            "/app/resources/:/app/classes/:/app/libs/*",
+            "/app/resources:/app/classes:/app/libs/*",
             "SomeMainClass"),
         entrypoint);
 
     // Checks that this is also the default entrypoint.
     Assert.assertEquals(
         JavaEntrypointConstructor.makeDefaultEntrypoint(
-            "/app", expectedJvmFlags, expectedMainClass),
+            AbsoluteUnixPath.get("/app"), expectedJvmFlags, expectedMainClass),
         entrypoint);
   }
 
@@ -59,61 +60,18 @@ public void testMakeEntrypoint() {
   public void testMakeDefaultEntrypoint_classpathString() {
     // Checks that this is also the default entrypoint.
     List entrypoint =
-        JavaEntrypointConstructor.makeDefaultEntrypoint("/app", Collections.emptyList(), "MyMain");
-    Assert.assertEquals("/app/resources/:/app/classes/:/app/libs/*", entrypoint.get(2));
+        JavaEntrypointConstructor.makeDefaultEntrypoint(
+            AbsoluteUnixPath.get("/app"), Collections.emptyList(), "MyMain");
+    Assert.assertEquals("/app/resources:/app/classes:/app/libs/*", entrypoint.get(2));
   }
 
   @Test
   public void testMakeDefaultEntrypoint_classpathStringWithNonDefaultAppRoot() {
-    // Checks that this is also the default entrypoint.
-    List entrypoint =
-        JavaEntrypointConstructor.makeDefaultEntrypoint("/my/app", Collections.emptyList(), "Main");
-    Assert.assertEquals("/my/app/resources/:/my/app/classes/:/my/app/libs/*", entrypoint.get(2));
-  }
-
-  @Test
-  public void testMakeDefaultEntrypoint_appRootWithTrailingSlash() {
     // Checks that this is also the default entrypoint.
     List entrypoint =
         JavaEntrypointConstructor.makeDefaultEntrypoint(
-            "/my/root/", Collections.emptyList(), "SomeMainClass");
-    Assert.assertEquals("/my/root/resources/:/my/root/classes/:/my/root/libs/*", entrypoint.get(2));
-  }
-
-  @Test
-  public void testMakeDefaultEntrypoint_nonAbsoluteAppRoot() {
-    try {
-      JavaEntrypointConstructor.makeDefaultEntrypoint(
-          "relative/path", Collections.emptyList(), "MainClass");
-      Assert.fail();
-    } catch (IllegalArgumentException ex) {
-      Assert.assertEquals(
-          "appRoot should be an absolute path in Unix-style: relative/path", ex.getMessage());
-    }
-  }
-
-  @Test
-  public void testMakeDefaultEntrypoint_windowsAppRootPath() {
-    try {
-      JavaEntrypointConstructor.makeDefaultEntrypoint(
-          "\\windows\\path", Collections.emptyList(), "MyMain");
-      Assert.fail();
-    } catch (IllegalArgumentException ex) {
-      Assert.assertEquals(
-          "appRoot should be an absolute path in Unix-style: \\windows\\path", ex.getMessage());
-    }
-  }
-
-  @Test
-  public void testMakeDefaultEntrypoint_windowsPathWithDriveLetter() {
-    try {
-      JavaEntrypointConstructor.makeDefaultEntrypoint(
-          "D:\\windows\\path", Collections.emptyList(), "MyMain");
-      Assert.fail();
-    } catch (IllegalArgumentException ex) {
-      Assert.assertEquals(
-          "appRoot should be an absolute path in Unix-style: D:\\windows\\path", ex.getMessage());
-    }
+            AbsoluteUnixPath.get("/my/app"), Collections.emptyList(), "Main");
+    Assert.assertEquals("/my/app/resources:/my/app/classes:/my/app/libs/*", entrypoint.get(2));
   }
 
   @Test
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
index 7ec53cc918..c01d9d2f7c 100644
--- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
+++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
@@ -1,6 +1,7 @@
 package com.google.cloud.tools.jib.frontend;
 
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -18,13 +19,22 @@ public class JavaLayerConfigurationsTest {
 
   private static JavaLayerConfigurations createFakeConfigurations() throws IOException {
     return JavaLayerConfigurations.builder()
-        .setDependencyFiles(Collections.singletonList(Paths.get("dependency")), "/dependency/path")
+        .setDependencyFiles(
+            Collections.singletonList(Paths.get("dependency")),
+            AbsoluteUnixPath.get("/dependency/path"))
         .setSnapshotDependencyFiles(
-            Collections.singletonList(Paths.get("snapshot dependency")), "/snapshots")
-        .setResourceFiles(Collections.singletonList(Paths.get("resource")), "/resources/here")
-        .setClassFiles(Collections.singletonList(Paths.get("class")), "/classes/go/here")
-        .setExplodedWarFiles(Collections.singletonList(Paths.get("exploded war")), "/for/war")
-        .setExtraFiles(Collections.singletonList(Paths.get("extra file")), "/some/extras")
+            Collections.singletonList(Paths.get("snapshot dependency")),
+            AbsoluteUnixPath.get("/snapshots"))
+        .setResourceFiles(
+            Collections.singletonList(Paths.get("resource")),
+            AbsoluteUnixPath.get("/resources/here"))
+        .setClassFiles(
+            Collections.singletonList(Paths.get("class")), AbsoluteUnixPath.get("/classes/go/here"))
+        .setExplodedWarFiles(
+            Collections.singletonList(Paths.get("exploded war")), AbsoluteUnixPath.get("/for/war"))
+        .setExtraFiles(
+            Collections.singletonList(Paths.get("extra file")),
+            AbsoluteUnixPath.get("/some/extras"))
         .build();
   }
 
@@ -73,12 +83,18 @@ public void testSetFiles_files() throws IOException {
   public void testSetFiles_extractionPaths() throws IOException {
     JavaLayerConfigurations configurations = createFakeConfigurations();
 
-    Assert.assertEquals("/dependency/path", configurations.getDependencyExtractionPath());
-    Assert.assertEquals("/snapshots", configurations.getSnapshotDependencyExtractionPath());
-    Assert.assertEquals("/resources/here", configurations.getResourceExtractionPath());
-    Assert.assertEquals("/classes/go/here", configurations.getClassExtractionPath());
-    Assert.assertEquals("/for/war", configurations.getExplodedWarExtractionPath());
-    Assert.assertEquals("/some/extras", configurations.getExtraFilesExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/dependency/path"), configurations.getDependencyExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/snapshots"), configurations.getSnapshotDependencyExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/resources/here"), configurations.getResourceExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/classes/go/here"), configurations.getClassExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/for/war"), configurations.getExplodedWarExtractionPath());
+    Assert.assertEquals(
+        AbsoluteUnixPath.get("/some/extras"), configurations.getExtraFilesExtractionPath());
 
     Assert.assertEquals(
         Paths.get("/dependency/path/dependency"),
diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile
index c1de9b2f4b..65c8f0a015 100644
--- a/jib-core/src/test/resources/sampleDockerfile
+++ b/jib-core/src/test/resources/sampleDockerfile
@@ -14,5 +14,5 @@ ENV key1="value1" \
 LABEL key1="value" \
     key2="value with\\backslashes\"and\\\\\"\"quotes\"\\" \
     key3="value3"
-ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources/:/app/classes/:/app/libs/*","SomeMainClass"]
+ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources:/app/classes:/app/libs/*","SomeMainClass"]
 CMD ["arg1","arg2"]
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java
index 75949da525..755593c375 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java
@@ -20,6 +20,7 @@
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.docker.DockerClient;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException;
@@ -73,7 +74,7 @@ public void buildDocker() throws InvalidImageReferenceException {
     // Asserts required @Input parameters are not null.
     Preconditions.checkNotNull(jibExtension);
     GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
     GradleProjectProperties gradleProjectProperties =
         GradleProjectProperties.getForProject(
             getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot);
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java
index 4de1ab3ebe..182509d286 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java
@@ -20,6 +20,7 @@
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.configuration.credentials.Credential;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory;
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
@@ -72,7 +73,7 @@ public void buildImage() throws InvalidImageReferenceException {
     // Asserts required @Input parameters are not null.
     Preconditions.checkNotNull(jibExtension);
     GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
     GradleProjectProperties gradleProjectProperties =
         GradleProjectProperties.getForProject(
             getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot);
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java
index 2f2e081d9b..a343084757 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java
@@ -19,6 +19,7 @@
 import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException;
@@ -100,7 +101,7 @@ public void buildTar() throws InvalidImageReferenceException {
     // Asserts required @Input parameters are not null.
     Preconditions.checkNotNull(jibExtension);
     GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
     GradleProjectProperties gradleProjectProperties =
         GradleProjectProperties.getForProject(
             getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot);
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java
index 635607119b..16c5ace1b8 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.gradle;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.ExposedPortsParser;
 import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
@@ -106,7 +107,7 @@ public void generateDockerContext() {
     jibExtension.handleDeprecatedParameters(gradleJibLogger);
     JibSystemProperties.checkHttpTimeoutProperty();
 
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension);
     GradleProjectProperties gradleProjectProperties =
         GradleProjectProperties.getForProject(
             getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot);
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
index ed74dae264..45fba4442b 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.gradle;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import java.io.File;
@@ -49,7 +50,10 @@ class GradleLayerConfigurations {
    * @throws IOException if an I/O exception occurred during resolution
    */
   static JavaLayerConfigurations getForProject(
-      Project project, GradleJibLogger gradleJibLogger, Path extraDirectory, String appRoot)
+      Project project,
+      GradleJibLogger gradleJibLogger,
+      Path extraDirectory,
+      AbsoluteUnixPath appRoot)
       throws IOException {
     JavaPluginConvention javaPluginConvention =
         project.getConvention().getPlugin(JavaPluginConvention.class);
@@ -117,21 +121,20 @@ static JavaLayerConfigurations getForProject(
     Collections.sort(classesFiles);
     Collections.sort(extraFiles);
 
-    appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/';
     return JavaLayerConfigurations.builder()
         .setDependencyFiles(
             dependenciesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
         .setSnapshotDependencyFiles(
             snapshotDependenciesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
         .setResourceFiles(
             resourcesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE))
         .setClassFiles(
             classesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE)
-        .setExtraFiles(extraFiles, "/")
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE))
+        .setExtraFiles(extraFiles, AbsoluteUnixPath.get("/"))
         .build();
   }
 
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java
index ee41f375f2..78f4f56de0 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java
@@ -17,6 +17,7 @@
 package com.google.cloud.tools.jib.gradle;
 
 import com.google.cloud.tools.jib.JibLogger;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException;
 import com.google.cloud.tools.jib.plugins.common.MainClassResolver;
@@ -52,13 +53,12 @@ class GradleProjectProperties implements ProjectProperties {
 
   /** @return a GradleProjectProperties from the given project and logger. */
   static GradleProjectProperties getForProject(
-      Project project, GradleJibLogger gradleJibLogger, Path extraDirectory, String appRoot) {
+      Project project, GradleJibLogger logger, Path extraDirectory, AbsoluteUnixPath appRoot) {
     try {
       return new GradleProjectProperties(
           project,
-          gradleJibLogger,
-          GradleLayerConfigurations.getForProject(
-              project, gradleJibLogger, extraDirectory, appRoot));
+          logger,
+          GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot));
 
     } catch (IOException ex) {
       throw new GradleException("Obtaining project build output files failed", ex);
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java
index efb2acc9f7..2d83b14dcd 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java
@@ -22,6 +22,7 @@
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.configuration.credentials.Credential;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory;
 import com.google.cloud.tools.jib.frontend.ExposedPortsParser;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
@@ -46,13 +47,13 @@ class PluginConfigurationProcessor {
    * @return the app root value
    * @throws GradleException if the app root is not an absolute path in Unix-style
    */
-  static String getAppRootChecked(JibExtension jibExtension) {
+  static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension) {
     String appRoot = jibExtension.getContainer().getAppRoot();
-    if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(appRoot)) {
-      throw new GradleException(
-          "container.appRoot (" + appRoot + ") is not an absolute Unix-style path");
+    try {
+      return AbsoluteUnixPath.get(appRoot);
+    } catch (IllegalArgumentException ex) {
+      throw new GradleException("container.appRoot is not an absolute Unix-style path: " + appRoot);
     }
-    return appRoot;
   }
 
   /**
@@ -104,7 +105,7 @@ static PluginConfigurationProcessor processCommonConfiguration(
       String mainClass = projectProperties.getMainClass(jibExtension);
       entrypoint =
           JavaEntrypointConstructor.makeDefaultEntrypoint(
-              jibExtension.getContainer().getAppRoot(), jibExtension.getJvmFlags(), mainClass);
+              getAppRootChecked(jibExtension), jibExtension.getJvmFlags(), mainClass);
     } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) {
       logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified");
     }
diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java
index c304bb726f..0411598a5b 100644
--- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java
+++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java
@@ -66,7 +66,7 @@ public void testEntrypoint() throws IOException {
     task.generateDockerContext();
 
     Assert.assertEquals(
-        "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources/:/app/classes/:/app/libs/*\",\"MainClass\"]",
+        "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]",
         getEntrypoint());
   }
 
@@ -76,7 +76,7 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException {
     task.generateDockerContext();
 
     Assert.assertEquals(
-        "ENTRYPOINT [\"java\",\"-cp\",\"/resources/:/classes/:/libs/*\",\"MainClass\"]",
+        "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]",
         getEntrypoint());
   }
 
@@ -89,7 +89,7 @@ public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (relative/path) is not an absolute Unix-style path", ex.getMessage());
+          "container.appRoot is not an absolute Unix-style path: relative/path", ex.getMessage());
     }
   }
 
@@ -102,8 +102,7 @@ public void testGenerateDockerContext_errorOnWindowsAppRoot() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (\\windows\\path) is not an absolute Unix-style path",
-          ex.getMessage());
+          "container.appRoot is not an absolute Unix-style path: \\windows\\path", ex.getMessage());
     }
   }
 
@@ -116,7 +115,7 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (C:\\windows\\path) is not an absolute Unix-style path",
+          "container.appRoot is not an absolute Unix-style path: C:\\windows\\path",
           ex.getMessage());
     }
   }
diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java
index 2153647a3b..89739b095c 100644
--- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java
+++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.gradle;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.collect.ImmutableList;
@@ -150,9 +151,10 @@ public void test_correctFiles() throws URISyntaxException, IOException {
             applicationDirectory.resolve("classes/some.class"));
     ImmutableList expectedExtraFiles = ImmutableList.of();
 
+    AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app");
     JavaLayerConfigurations javaLayerConfigurations =
         GradleLayerConfigurations.getForProject(
-            mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), "/app");
+            mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), appRoot);
     Assert.assertEquals(
         expectedDependenciesFiles,
         getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries()));
@@ -177,8 +179,9 @@ public void test_noClassesFiles() throws IOException {
     Mockito.when(mockMainSourceSetOutput.getClassesDirs())
         .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile)));
 
+    AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app");
     GradleLayerConfigurations.getForProject(
-        mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), "/app");
+        mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), appRoot);
 
     Mockito.verify(mockGradleJibLogger)
         .info("Adding corresponding output directories of source sets to image");
@@ -193,7 +196,7 @@ public void test_extraFiles() throws URISyntaxException, IOException {
 
     JavaLayerConfigurations javaLayerConfigurations =
         GradleLayerConfigurations.getForProject(
-            mockProject, mockGradleJibLogger, extraFilesDirectory, "/app");
+            mockProject, mockGradleJibLogger, extraFilesDirectory, AbsoluteUnixPath.get("/app"));
 
     ImmutableList expectedExtraFiles =
         ImmutableList.of(
@@ -215,7 +218,7 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxE
 
     JavaLayerConfigurations configuration =
         GradleLayerConfigurations.getForProject(
-            mockProject, mockGradleJibLogger, extraFilesDirectory, "/my/app");
+            mockProject, mockGradleJibLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app"));
 
     Assert.assertEquals(
         Arrays.asList(
diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java
index d7c68194c6..eb895db441 100644
--- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java
+++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java
@@ -17,6 +17,7 @@
 package com.google.cloud.tools.jib.gradle;
 
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import java.util.Arrays;
@@ -63,8 +64,7 @@ public void testPluginConfigurationProcessor_defaults() throws InvalidImageRefer
             mockGradleJibLogger, mockJibExtension, mockProjectProperties);
     ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build();
     Assert.assertEquals(
-        Arrays.asList(
-            "java", "-cp", "/app/resources/:/app/classes/:/app/libs/*", "java.lang.Object"),
+        Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"),
         configuration.getEntrypoint());
     Mockito.verifyZeroInteractions(mockGradleJibLogger);
   }
@@ -128,7 +128,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageRefer
     ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build();
 
     Assert.assertEquals(
-        "/my/app/resources/:/my/app/classes/:/my/app/libs/*", configuration.getEntrypoint().get(2));
+        "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2));
   }
 
   @Test
@@ -136,7 +136,8 @@ public void testGetAppRootChecked() {
     Mockito.doReturn("/some/root").when(mockContainerParameters).getAppRoot();
 
     Assert.assertEquals(
-        "/some/root", PluginConfigurationProcessor.getAppRootChecked(mockJibExtension));
+        AbsoluteUnixPath.get("/some/root"),
+        PluginConfigurationProcessor.getAppRootChecked(mockJibExtension));
   }
 
   @Test
@@ -148,7 +149,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (relative/path) is not an absolute Unix-style path", ex.getMessage());
+          "container.appRoot is not an absolute Unix-style path: relative/path", ex.getMessage());
     }
   }
 
@@ -161,8 +162,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (\\windows\\path) is not an absolute Unix-style path",
-          ex.getMessage());
+          "container.appRoot is not an absolute Unix-style path: \\windows\\path", ex.getMessage());
     }
   }
 
@@ -175,7 +175,7 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() {
       Assert.fail();
     } catch (GradleException ex) {
       Assert.assertEquals(
-          "container.appRoot (C:\\windows\\path) is not an absolute Unix-style path",
+          "container.appRoot is not an absolute Unix-style path: C:\\windows\\path",
           ex.getMessage());
     }
   }
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java
index a06e133978..5ff5f7fe04 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java
@@ -20,6 +20,7 @@
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.docker.DockerClient;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException;
@@ -58,7 +59,7 @@ public void execute() throws MojoExecutionException {
     }
 
     MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
     MavenProjectProperties mavenProjectProperties =
         MavenProjectProperties.getForProject(
             getProject(), mavenJibLogger, getExtraDirectory(), appRoot);
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java
index df3611e6b2..1c9ebdd4d7 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java
@@ -20,6 +20,7 @@
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.configuration.credentials.Credential;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory;
 import com.google.cloud.tools.jib.image.ImageFormat;
 import com.google.cloud.tools.jib.image.ImageReference;
@@ -81,7 +82,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
     }
 
     MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
     MavenProjectProperties mavenProjectProperties =
         MavenProjectProperties.getForProject(
             getProject(), mavenJibLogger, getExtraDirectory(), appRoot);
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java
index 0d1c8f98f2..8003168a31 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java
@@ -19,6 +19,7 @@
 import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException;
@@ -55,7 +56,7 @@ public void execute() throws MojoExecutionException {
     }
 
     MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog());
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
     MavenProjectProperties mavenProjectProperties =
         MavenProjectProperties.getForProject(
             getProject(), mavenJibLogger, getExtraDirectory(), appRoot);
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java
index 58bc0cad7b..a130d8eab5 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java
@@ -16,11 +16,11 @@
 
 package com.google.cloud.tools.jib.maven;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.ExposedPortsParser;
 import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
 import com.google.cloud.tools.jib.global.JibSystemProperties;
-import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator;
 import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -69,14 +69,9 @@ public void execute() throws MojoExecutionException {
       throw new MojoExecutionException(ex.getMessage(), ex);
     }
 
-    if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(getAppRoot())) {
-      throw new MojoExecutionException(
-          " (" + getAppRoot() + ") is not an absolute Unix-style path");
-    }
-
     Preconditions.checkNotNull(targetDir);
 
-    String appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
+    AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this);
     MavenProjectProperties mavenProjectProperties =
         MavenProjectProperties.getForProject(
             getProject(), mavenJibLogger, getExtraDirectory(), appRoot);
@@ -85,7 +80,7 @@ public void execute() throws MojoExecutionException {
     if (entrypoint.isEmpty()) {
       String mainClass = mavenProjectProperties.getMainClass(this);
       entrypoint =
-          JavaEntrypointConstructor.makeDefaultEntrypoint(getAppRoot(), getJvmFlags(), mainClass);
+          JavaEntrypointConstructor.makeDefaultEntrypoint(appRoot, getJvmFlags(), mainClass);
     } else if (getMainClass() != null || !getJvmFlags().isEmpty()) {
       mavenJibLogger.warn(" and  are ignored when  is specified");
     }
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
index c5b1ccc4d2..56c90c05b9 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.maven;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import java.io.IOException;
@@ -44,7 +45,7 @@ class MavenLayerConfigurations {
    * @throws IOException if collecting the project files fails
    */
   static JavaLayerConfigurations getForProject(
-      MavenProject project, Path extraDirectory, String appRoot) throws IOException {
+      MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException {
     Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory());
     Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory());
 
@@ -105,21 +106,20 @@ static JavaLayerConfigurations getForProject(
     Collections.sort(classesFiles);
     Collections.sort(extraFiles);
 
-    appRoot = appRoot.endsWith("/") ? appRoot : appRoot + '/';
     return JavaLayerConfigurations.builder()
         .setDependencyFiles(
             dependenciesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
         .setSnapshotDependencyFiles(
             snapshotDependenciesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
         .setResourceFiles(
             resourcesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE)
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE))
         .setClassFiles(
             classesFiles,
-            appRoot + JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE)
-        .setExtraFiles(extraFiles, "/")
+            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE))
+        .setExtraFiles(extraFiles, AbsoluteUnixPath.get("/"))
         .build();
   }
 
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java
index f0cd99e4ea..f51d8d75cb 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java
@@ -17,6 +17,7 @@
 package com.google.cloud.tools.jib.maven;
 
 import com.google.cloud.tools.jib.JibLogger;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException;
 import com.google.cloud.tools.jib.plugins.common.MainClassResolver;
@@ -48,19 +49,19 @@ public class MavenProjectProperties implements ProjectProperties {
 
   /**
    * @param project the {@link MavenProject} for the plugin.
-   * @param mavenJibLogger the logger used for printing status messages.
+   * @param logger the logger used for printing status messages.
    * @param extraDirectory path to the directory for the extra files layer
    * @param appRoot root directory in the image where the app will be placed
    * @return a MavenProjectProperties from the given project and logger.
    * @throws MojoExecutionException if no class files are found in the output directory.
    */
   static MavenProjectProperties getForProject(
-      MavenProject project, MavenJibLogger mavenJibLogger, Path extraDirectory, String appRoot)
+      MavenProject project, MavenJibLogger logger, Path extraDirectory, AbsoluteUnixPath appRoot)
       throws MojoExecutionException {
     try {
       return new MavenProjectProperties(
           project,
-          mavenJibLogger,
+          logger,
           MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot));
     } catch (IOException ex) {
       throw new MojoExecutionException(
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java
index 669e83403e..920e4b3c12 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java
@@ -21,6 +21,7 @@
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.configuration.credentials.Credential;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory;
 import com.google.cloud.tools.jib.frontend.ExposedPortsParser;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
@@ -46,14 +47,15 @@ class PluginConfigurationProcessor {
    * @return the app root value
    * @throws MojoExecutionException if the app root is not an absolute path in Unix-style
    */
-  static String getAppRootChecked(JibPluginConfiguration jibPluginConfiguration)
+  static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration)
       throws MojoExecutionException {
     String appRoot = jibPluginConfiguration.getAppRoot();
-    if (!ConfigurationPropertyValidator.isAbsoluteUnixPath(appRoot)) {
+    try {
+      return AbsoluteUnixPath.get(appRoot);
+    } catch (IllegalArgumentException ex) {
       throw new MojoExecutionException(
-          " (" + appRoot + ") is not an absolute Unix-style path");
+          " is not an absolute Unix-style path: " + appRoot);
     }
-    return appRoot;
   }
 
   /**
@@ -126,7 +128,9 @@ static PluginConfigurationProcessor processCommonConfiguration(
       String mainClass = projectProperties.getMainClass(jibPluginConfiguration);
       entrypoint =
           JavaEntrypointConstructor.makeDefaultEntrypoint(
-              jibPluginConfiguration.getAppRoot(), jibPluginConfiguration.getJvmFlags(), mainClass);
+              getAppRootChecked(jibPluginConfiguration),
+              jibPluginConfiguration.getJvmFlags(),
+              mainClass);
     } else if (jibPluginConfiguration.getMainClass() != null
         || !jibPluginConfiguration.getJvmFlags().isEmpty()) {
       logger.warn(" and  are ignored when  is specified");
diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java
index 3d3d68fd9a..3437c1eb3a 100644
--- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java
+++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java
@@ -82,7 +82,7 @@ public void testEntrypoint() throws MojoExecutionException, IOException {
     mojo.execute();
 
     Assert.assertEquals(
-        "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources/:/app/classes/:/app/libs/*\",\"MainClass\"]",
+        "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]",
         getEntrypoint());
   }
 
@@ -92,20 +92,19 @@ public void testEntrypoint_nonDefaultAppRoot() throws MojoExecutionException, IO
     mojo.execute();
 
     Assert.assertEquals(
-        "ENTRYPOINT [\"java\",\"-cp\",\"/resources/:/classes/:/libs/*\",\"MainClass\"]",
+        "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]",
         getEntrypoint());
   }
 
   @Test
   public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() {
     appRoot = "relative/path";
-
     try {
       mojo.execute();
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (relative/path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: relative/path",
           ex.getMessage());
     }
   }
@@ -113,13 +112,12 @@ public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() {
   @Test
   public void testGenerateDockerContext_errorOnWindowsAppRoot() {
     appRoot = "\\windows\\path";
-
     try {
       mojo.execute();
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (\\windows\\path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: \\windows\\path",
           ex.getMessage());
     }
   }
@@ -127,13 +125,12 @@ public void testGenerateDockerContext_errorOnWindowsAppRoot() {
   @Test
   public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() {
     appRoot = "C:\\windows\\path";
-
     try {
       mojo.execute();
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (C:\\windows\\path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: C:\\windows\\path",
           ex.getMessage());
     }
   }
diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java
index 08a0159f9c..f591096176 100644
--- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java
+++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.maven;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.collect.ImmutableList;
@@ -114,7 +115,7 @@ public void test_correctFiles() throws URISyntaxException, IOException {
 
     JavaLayerConfigurations javaLayerConfigurations =
         MavenLayerConfigurations.getForProject(
-            mockMavenProject, Paths.get("nonexistent/path"), "/app");
+            mockMavenProject, Paths.get("nonexistent/path"), AbsoluteUnixPath.get("/app"));
     Assert.assertEquals(
         expectedDependenciesFiles,
         getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries()));
@@ -134,8 +135,9 @@ public void test_correctFiles() throws URISyntaxException, IOException {
   public void test_extraFiles() throws URISyntaxException, IOException {
     Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI());
 
+    AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app");
     JavaLayerConfigurations javaLayerConfigurations =
-        MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, "/app");
+        MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot);
 
     ImmutableList expectedExtraFiles =
         ImmutableList.of(
@@ -155,8 +157,9 @@ public void test_extraFiles() throws URISyntaxException, IOException {
   public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOException {
     Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI());
 
+    AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app");
     JavaLayerConfigurations configuration =
-        MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, "/my/app");
+        MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot);
 
     Assert.assertEquals(
         // on windows, these files may be in a different order, so use Set
diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java
index e1fb9ed48e..f8410ed79f 100644
--- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java
+++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java
@@ -17,6 +17,7 @@
 package com.google.cloud.tools.jib.maven;
 
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
 import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration;
 import java.util.Arrays;
@@ -71,8 +72,7 @@ public void testPluginConfigurationProcessor_defaults() throws MojoExecutionExce
             mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties);
     ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build();
     Assert.assertEquals(
-        Arrays.asList(
-            "java", "-cp", "/app/resources/:/app/classes/:/app/libs/*", "java.lang.Object"),
+        Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"),
         configuration.getEntrypoint());
     Mockito.verifyZeroInteractions(mockMavenJibLogger);
   }
@@ -136,7 +136,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws MojoExecutionExce
     ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build();
 
     Assert.assertEquals(
-        "/my/app/resources/:/my/app/classes/:/my/app/libs/*", configuration.getEntrypoint().get(2));
+        "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2));
   }
 
   @Test
@@ -144,7 +144,8 @@ public void testGetAppRootChecked() throws MojoExecutionException {
     Mockito.doReturn("/some/root").when(mockJibPluginConfiguration).getAppRoot();
 
     Assert.assertEquals(
-        "/some/root", PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration));
+        AbsoluteUnixPath.get("/some/root"),
+        PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration));
   }
 
   @Test
@@ -156,7 +157,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() {
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (relative/path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: relative/path",
           ex.getMessage());
     }
   }
@@ -170,7 +171,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() {
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (\\windows\\path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: \\windows\\path",
           ex.getMessage());
     }
   }
@@ -184,7 +185,7 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() {
       Assert.fail();
     } catch (MojoExecutionException ex) {
       Assert.assertEquals(
-          " (C:\\windows\\path) is not an absolute Unix-style path",
+          " is not an absolute Unix-style path: C:\\windows\\path",
           ex.getMessage());
     }
   }
diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java
index 9c1eb34431..490a2c55ba 100644
--- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java
+++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java
@@ -22,8 +22,6 @@
 import com.google.cloud.tools.jib.image.ImageReference;
 import com.google.cloud.tools.jib.image.InvalidImageReferenceException;
 import com.google.common.base.Strings;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Paths;
 import java.util.Optional;
 import javax.annotation.Nullable;
 
@@ -121,20 +119,5 @@ public static ImageReference getGeneratedTargetDockerTag(
     }
   }
 
-  /**
-   * Checks if the path string is absolute in Unix-style (using forward slashes).
-   *
-   * @param path path to test
-   * @return {@code true} if absolute in Unix-style; {@code false} otherwise
-   */
-  public static boolean isAbsoluteUnixPath(String path) {
-    try {
-      Paths.get(path);
-    } catch (InvalidPathException ex) {
-      return false;
-    }
-    return path.startsWith("/");
-  }
-
   private ConfigurationPropertyValidator() {}
 }
diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java
index a26bd19e5f..2cb08817e9 100644
--- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java
+++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java
@@ -139,34 +139,4 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept
     } catch (InvalidImageReferenceException ignored) {
     }
   }
-
-  @Test
-  public void testIsAbsoluteUnixPath_emptyString() {
-    Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath(""));
-  }
-
-  @Test
-  public void testIsAbsoluteUnixPath_root() {
-    Assert.assertTrue(ConfigurationPropertyValidator.isAbsoluteUnixPath("/"));
-  }
-
-  @Test
-  public void testIsAbsoluteUnixPath() {
-    Assert.assertTrue(ConfigurationPropertyValidator.isAbsoluteUnixPath("/some/path"));
-  }
-
-  @Test
-  public void testIsAbsoluteUnixPath_nonAbsolute() {
-    Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("relative/path"));
-  }
-
-  @Test
-  public void testIsAbsoluteUnixPath_windowsPath() {
-    Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("\\windows\\path"));
-  }
-
-  @Test
-  public void testIsAbsoluteUnixPath_windowsPathWithDriveLetter() {
-    Assert.assertFalse(ConfigurationPropertyValidator.isAbsoluteUnixPath("C:\\windows\\path"));
-  }
 }

From 6dcda920afec59fc5a9190d157bda177bbe01086 Mon Sep 17 00:00:00 2001
From: Q Chen 
Date: Thu, 20 Sep 2018 14:50:18 -0400
Subject: [PATCH 0221/2020]  Changes LayerEntry#extractionPath to use
 AbsoluteUnixPath. (#1014)

---
 .../builder/BuildStepsIntegrationTest.java    | 10 +--
 .../tools/jib/api/JibContainerBuilder.java    |  6 +-
 .../jib/cache/CacheMetadataTranslator.java    |  3 +-
 .../jib/configuration/LayerConfiguration.java | 26 ++++---
 .../jib/filesystem/AbsoluteUnixPath.java      | 25 +++++++
 .../frontend/JavaDockerContextGenerator.java  |  3 +-
 .../frontend/JavaEntrypointConstructor.java   |  5 +-
 .../jib/frontend/JavaLayerConfigurations.java |  6 +-
 .../cloud/tools/jib/image/LayerEntry.java     | 39 ++++-------
 ...BuildAndCacheApplicationLayerStepTest.java | 11 +--
 .../tools/jib/cache/CacheMetadataTest.java    | 36 ++++++----
 .../cache/CacheMetadataTranslatorTest.java    |  9 +--
 .../tools/jib/cache/CacheReaderTest.java      | 12 ++--
 .../cloud/tools/jib/cache/CacheTest.java      | 11 +--
 .../tools/jib/cache/CacheWriterTest.java      |  7 +-
 .../configuration/BuildConfigurationTest.java |  3 +-
 .../configuration/LayerConfigurationTest.java | 23 ++++---
 .../jib/filesystem/AbsoluteUnixPathTest.java  | 40 ++++++++---
 .../JavaDockerContextGeneratorTest.java       | 67 +++++++++----------
 .../frontend/JavaLayerConfigurationsTest.java | 12 ++--
 .../image/ReproducibleLayerBuilderTest.java   | 18 ++---
 .../plugins/common/BuildStepsRunnerTest.java  |  7 +-
 .../plugins/common/MainClassResolverTest.java | 16 +++--
 23 files changed, 233 insertions(+), 162 deletions(-)

diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
index 2e31c4ae70..24949eb961 100644
--- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
+++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
@@ -62,7 +62,8 @@ public class BuildStepsIntegrationTest {
    * LayerConfiguration} from those files.
    */
   private static LayerConfiguration makeLayerConfiguration(
-      String resourcePath, Path pathInContainer) throws URISyntaxException, IOException {
+      String resourcePath, AbsoluteUnixPath pathInContainer)
+      throws URISyntaxException, IOException {
     try (Stream fileStream =
         Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) {
       LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder();
@@ -112,9 +113,10 @@ private static void assertDockerInspect(String imageReference)
   public void setUp() throws IOException, URISyntaxException {
     fakeLayerConfigurations =
         ImmutableList.of(
-            makeLayerConfiguration("application/dependencies", Paths.get("/app/libs/")),
-            makeLayerConfiguration("application/resources", Paths.get("/app/resources/")),
-            makeLayerConfiguration("application/classes", Paths.get("/app/classes/")));
+            makeLayerConfiguration("application/dependencies", AbsoluteUnixPath.get("/app/libs/")),
+            makeLayerConfiguration(
+                "application/resources", AbsoluteUnixPath.get("/app/resources/")),
+            makeLayerConfiguration("application/classes", AbsoluteUnixPath.get("/app/classes/")));
   }
 
   @Test
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
index e1d1218c02..44b6889131 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
@@ -19,6 +19,7 @@
 
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
 import com.google.cloud.tools.jib.configuration.Port;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -87,11 +88,12 @@ public class JibContainerBuilder {
    *
    * @param files the source files to copy to a new layer in the container
    * @param pathInContainer the path in the container file system corresponding to the {@code
-   *     sourceFile} (relative to root {@code /})
+   *     sourceFile}
    * @return this
    * @throws IOException if an exception occurred when recursively listing any directories
    */
-  public JibContainerBuilder addLayer(List files, Path pathInContainer) throws IOException {
+  public JibContainerBuilder addLayer(List files, AbsoluteUnixPath pathInContainer)
+      throws IOException {
     LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder();
 
     for (Path file : files) {
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
index 537b8b8d4f..dd0cf93f84 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
@@ -21,6 +21,7 @@
 import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate;
 import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate;
 import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate;
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
@@ -66,7 +67,7 @@ static CacheMetadata fromTemplate(CacheMetadataTemplate template, Path cacheDire
           layerEntries.add(
               new LayerEntry(
                   Paths.get(layerEntryTemplate.getSourceFileString()),
-                  Paths.get(layerEntryTemplate.getExtractionPathString())));
+                  AbsoluteUnixPath.get(layerEntryTemplate.getExtractionPathString())));
         }
 
         layerMetadata =
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java
index c309cfa8a7..0f8d33ed5c 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.tools.jib.configuration;
 
+import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.LayerEntry;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -50,19 +51,21 @@ public Builder setName(String name) {
      * Adds an entry to the layer. Only adds the single source file to the exact path in the
      * container file system.
      *
-     * 

For example, {@code addEntry(Paths.get("myfile"), Paths.get("/path/in/container"))} adds a - * file {@code myfile} to the container file system at {@code /path/in/container}. + *

For example, {@code addEntry(Paths.get("myfile"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a file {@code myfile} to the container file + * system at {@code /path/in/container}. * - *

For example, {@code addEntry(Paths.get("mydirectory"), Paths.get("/path/in/container"))} - * adds a directory {@code mydirectory/} to the container file system at {@code - * /path/in/container/}. This does not add the contents of {@code mydirectory}. + *

For example, {@code addEntry(Paths.get("mydirectory"), + * AbsoluteUnixPath.get("/path/in/container"))} adds a directory {@code mydirectory/} to the + * container file system at {@code /path/in/container/}. This does not add the contents + * of {@code mydirectory}. * * @param sourceFile the source file to add to the layer * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} (relative to root {@code /}) + * sourceFile} * @return this */ - public Builder addEntry(Path sourceFile, Path pathInContainer) { + public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { layerEntries.add(new LayerEntry(sourceFile, pathInContainer)); return this; } @@ -72,17 +75,18 @@ public Builder addEntry(Path sourceFile, Path pathInContainer) { * will be added recursively. * *

For example, {@code addEntryRecursive(Paths.get("mydirectory", - * Paths.get("/path/in/container"))} adds {@code mydirectory} to the container file system at - * {@code /path/in/container} such that {@code mydirectory/subfile} is found at {@code + * AbsoluteUnixPath.get("/path/in/container"))} adds {@code mydirectory} to the container file + * system at {@code /path/in/container} such that {@code mydirectory/subfile} is found at {@code * /path/in/container/subfile}. * * @param sourceFile the source file to add to the layer recursively * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} (relative to root {@code /}) + * sourceFile} * @return this * @throws IOException if an exception occurred when recursively listing the directory */ - public Builder addEntryRecursive(Path sourceFile, Path pathInContainer) throws IOException { + public Builder addEntryRecursive(Path sourceFile, AbsoluteUnixPath pathInContainer) + throws IOException { if (!Files.isDirectory(sourceFile)) { return addEntry(sourceFile, pathInContainer); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java index 72ecad8f00..7cba249d7f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java @@ -19,6 +19,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.StringJoiner; import javax.annotation.concurrent.Immutable; @@ -98,6 +99,30 @@ public AbsoluteUnixPath resolve(RelativeUnixPath relativeUnixPath) { return new AbsoluteUnixPath(newPathComponents.build()); } + /** + * Resolves this path against another relative path (by the name elements of {@code + * relativePath}). + * + * @param relativePath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(Path relativePath) { + Preconditions.checkArgument( + relativePath.getRoot() == null, "Cannot resolve against absolute Path: " + relativePath); + + return AbsoluteUnixPath.fromPath(Paths.get(unixPath).resolve(relativePath)); + } + + /** + * Resolves this path against another relative Unix path in string form. + * + * @param relativeUnixPath the relative path to resolve against + * @return a new {@link AbsoluteUnixPath} representing the resolved path + */ + public AbsoluteUnixPath resolve(String relativeUnixPath) { + return resolve(RelativeUnixPath.get(relativeUnixPath)); + } + /** * Returns the string form of the absolute Unix-style path. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 7b08194955..ebdb4a7476 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -286,7 +286,8 @@ public void generate(Path targetDirectory) throws IOException { // '/app/classes/com/test/HelloWorld.class', the resolved destination would be // 'target/jib-docker-context/classes/com/test/HelloWorld.class'. Path baseExtractionPath = Paths.get(copyDirective.extractionPath.toString()); - Path relativeEntryPath = baseExtractionPath.relativize(layerEntry.getExtractionPath()); + Path relativeEntryPath = + baseExtractionPath.relativize(Paths.get(layerEntry.getExtractionPath().toString())); Path destination = directoryInContext.resolve(relativeEntryPath); if (Files.isDirectory(layerEntry.getSourceFile())) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 740520e77e..6b0dea0b70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -38,10 +38,7 @@ public static List makeDefaultEntrypoint( Arrays.asList( appRoot.resolve(DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE).toString(), appRoot.resolve(DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE).toString(), - appRoot - .resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) - .resolve(RelativeUnixPath.get("*")) - .toString()), + appRoot.resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE).resolve("*").toString()), jvmFlags, mainClass); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 8351458fc4..7796fc3620 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -25,7 +25,6 @@ import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -130,9 +129,8 @@ public JavaLayerConfigurations build() throws IOException { // Adds all the layer files recursively. List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType)); for (Path layerFile : layerFiles) { - Path pathInContainer = - Paths.get(extractionPath.toString()).resolve(layerFile.getFileName()); - layerConfigurationBuilder.addEntryRecursive(layerFile, pathInContainer); + layerConfigurationBuilder.addEntryRecursive( + layerFile, extractionPath.resolve(layerFile.getFileName())); } layerConfigurationsMap.put(layerType, layerConfigurationBuilder.build()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index c28b021a70..b2c21bcf76 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -16,10 +16,9 @@ package com.google.cloud.tools.jib.image; -import com.google.common.annotations.VisibleForTesting; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.base.Preconditions; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Objects; import java.util.StringJoiner; @@ -55,25 +54,25 @@ private static String toUnixPath(Path path) { } private final Path sourceFile; - private final Path extractionPath; + private final AbsoluteUnixPath extractionPath; /** * Instantiates with a source file and the path to place the source file in the container file * system. * *

For example, {@code new LayerEntry(Paths.get("HelloWorld.class"), - * Paths.get("/app/classes/HelloWorld.class"))} adds a file {@code HelloWorld.class} to the - * container file system at {@code /app/classes/HelloWorld.class}. + * AbsoluteUnixPath.get("/app/classes/HelloWorld.class"))} adds a file {@code HelloWorld.class} to + * the container file system at {@code /app/classes/HelloWorld.class}. * - *

For example, {@code new LayerEntry(Paths.get("com"), Paths.get("/app/classes/com"))} adds a - * directory to the container file system at {@code /app/classes/com}. This does not add - * the contents of {@code com/}. + *

For example, {@code new LayerEntry(Paths.get("com"), + * AbsoluteUnixPath.get("/app/classes/com"))} adds a directory to the container file system at + * {@code /app/classes/com}. This does not add the contents of {@code com/}. * * @param sourceFile the source file to add to the layer * @param extractionPath the path in the container file system corresponding to the {@code - * sourceFile} (relative to root {@code /}) + * sourceFile} */ - public LayerEntry(Path sourceFile, Path extractionPath) { + public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath) { this.sourceFile = sourceFile; this.extractionPath = extractionPath; } @@ -95,18 +94,13 @@ public Path getSourceFile() { /** * Gets the extraction path. * - *

Do not call {@link Path#toString} on this - use {@link - * #getAbsoluteExtractionPathString} instead. This path can be relative or absolute, but {@link - * #getAbsoluteExtractionPathString} can only be absolute. Callers should rely on {@link - * #getAbsoluteExtractionPathString} for the serialized form since the serialization could change - * independently of the path representation. - * * @return the extraction path */ - public Path getExtractionPath() { + public AbsoluteUnixPath getExtractionPath() { return extractionPath; } + // TODO: Remove these get...String methods. /** * Get the source file as an absolute path in Unix form. The path is made absolute first, if not * already absolute. @@ -118,13 +112,12 @@ public String getAbsoluteSourceFileString() { } /** - * Gets the extraction path as an absolute path in Unix form. The path is made absolute first, if - * not already absolute. + * Gets the extraction path as an absolute path in Unix form. * * @return the extraction path */ public String getAbsoluteExtractionPathString() { - return toUnixPath(Paths.get("/").resolve(extractionPath)); + return extractionPath.toString(); } @Override @@ -144,10 +137,4 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(sourceFile, extractionPath); } - - @Override - @VisibleForTesting - public String toString() { - return getAbsoluteSourceFileString() + "\t" + getAbsoluteExtractionPathString(); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 0cee34edc1..3ac405e252 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -52,16 +53,18 @@ public class BuildAndCacheApplicationLayerStepTest { // TODO: Consolidate with BuildStepsIntegrationTest. - private static final Path EXTRACTION_PATH_ROOT = Paths.get("/some/extraction/path/"); + private static final AbsoluteUnixPath EXTRACTION_PATH_ROOT = + AbsoluteUnixPath.get("/some/extraction/path/"); - private static final Path EXTRA_FILES_LAYER_EXTRACTION_PATH = Paths.get("/extra"); + private static final AbsoluteUnixPath EXTRA_FILES_LAYER_EXTRACTION_PATH = + AbsoluteUnixPath.get("/extra"); /** * Lists the files in the {@code resourcePath} resources directory and creates a {@link * LayerConfiguration} with entries from those files. */ - private static LayerConfiguration makeLayerConfiguration(String resourcePath, Path extractionPath) - throws URISyntaxException, IOException { + private static LayerConfiguration makeLayerConfiguration( + String resourcePath, AbsoluteUnixPath extractionPath) throws URISyntaxException, IOException { try (Stream fileStream = Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java index 70fe03aa65..f93292bb8c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; @@ -76,29 +77,35 @@ public void testFilter_doLayerEntriesMatchMetadataEntries_extractionPath() { LayerMetadata.from( ImmutableList.of( new LayerEntry( - Paths.get("anotherSourceFile"), Paths.get("anotherExtractionPath"))), + Paths.get("anotherSourceFile"), + AbsoluteUnixPath.get("/another/extraction/path"))), FileTime.fromMillis(0)) .getEntries(); Assert.assertFalse( CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of(new LayerEntry(Paths.get("sourceFile"), Paths.get("extractionPath"))), + ImmutableList.of( + new LayerEntry(Paths.get("sourceFile"), AbsoluteUnixPath.get("/extraction/path"))), metadataEntries)); Assert.assertTrue( CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( ImmutableList.of( - new LayerEntry(Paths.get("anotherSourceFile"), Paths.get("anotherExtractionPath"))), + new LayerEntry( + Paths.get("anotherSourceFile"), + AbsoluteUnixPath.get("/another/extraction/path"))), metadataEntries)); } @Test public void testFilter_doLayerEntriesMatchMetadataEntries_pass() { - LayerEntry layerEntry1 = new LayerEntry(Paths.get("sourceFile1"), Paths.get("extractionPath")); - LayerEntry layerEntry2 = new LayerEntry(Paths.get("sourceFile2"), Paths.get("extractionPath")); + LayerEntry layerEntry1 = + new LayerEntry(Paths.get("sourceFile1"), AbsoluteUnixPath.get("/extraction/path")); + LayerEntry layerEntry2 = + new LayerEntry(Paths.get("sourceFile2"), AbsoluteUnixPath.get("/extraction/path")); LayerEntry layerEntry3 = - new LayerEntry(Paths.get("sourceFile3"), Paths.get("anotherExtractionPath")); + new LayerEntry(Paths.get("sourceFile3"), AbsoluteUnixPath.get("/another/extraction/path")); LayerEntry layerEntry4 = - new LayerEntry(Paths.get("sourceFile4"), Paths.get("anotherExtractionPath")); + new LayerEntry(Paths.get("sourceFile4"), AbsoluteUnixPath.get("/another/extraction/path")); ImmutableList layerEntries = ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4); @@ -119,9 +126,10 @@ public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { Stream.generate(CacheMetadataTest::mockCachedLayer).limit(6).collect(Collectors.toList()); LayerEntry fakeLayerEntry1 = - new LayerEntry(Paths.get("some/source/file"), Paths.get("extractionPath")); + new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); LayerEntry fakeLayerEntry2 = - new LayerEntry(Paths.get("some/source/directory"), Paths.get("extractionPath")); + new LayerEntry( + Paths.get("some/source/directory"), AbsoluteUnixPath.get("/extraction/path")); LayerMetadata fakeExpectedSourceFilesClassesLayerMetadata = LayerMetadata.from( @@ -132,7 +140,9 @@ public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { LayerMetadata fakeOtherSourceFilesLayerMetadata = LayerMetadata.from( ImmutableList.of( - new LayerEntry(Paths.get("not/the/same/source/file"), Paths.get("extractionPath"))), + new LayerEntry( + Paths.get("not/the/same/source/file"), + AbsoluteUnixPath.get("/extraction/path"))), FileTime.fromMillis(0)); LayerMetadata fakeEmptySourceFilesLayerMetadata = LayerMetadata.from(ImmutableList.of(), FileTime.fromMillis(0)); @@ -176,8 +186,7 @@ public void testFilter_byNoEntries() throws CacheMetadataCorruptedException { Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); LayerEntry fakeLayerEntry = - new LayerEntry( - Paths.get("some/source/file", "some/source/directory"), Paths.get("extractionPath")); + new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); LayerMetadata fakeSourceFilesLayerMetadata = LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); @@ -208,8 +217,7 @@ public void testFilter_byEmptySourceFiles() throws CacheMetadataCorruptedExcepti Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); LayerEntry fakeLayerEntry = - new LayerEntry( - Paths.get("some/source/file", "some/source/directory"), Paths.get("extractionPath")); + new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); LayerMetadata fakeSourceFilesLayerMetadata = LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java index 3b97aa863e..50548f1bde 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -45,7 +46,8 @@ public class CacheMetadataTranslatorTest { private static final String CLASSES_LAYER_SOURCE_FILE = "/some/source/path"; - private static final String CLASSES_LAYER_EXTRACTION_PATH = "/some/extraction/path"; + private static final AbsoluteUnixPath CLASSES_LAYER_EXTRACTION_PATH = + AbsoluteUnixPath.get("/some/extraction/path"); private static final FileTime CLASSES_LAYER_LAST_MODIFIED_TIME = FileTime.fromMillis(255073580723571L); @@ -112,7 +114,7 @@ public void testFromTemplate() CLASSES_LAYER_SOURCE_FILE, classesLayer.getMetadata().getEntries().get(0).getAbsoluteSourceFileString()); Assert.assertEquals( - CLASSES_LAYER_EXTRACTION_PATH, + CLASSES_LAYER_EXTRACTION_PATH.toString(), classesLayer.getMetadata().getEntries().get(0).getAbsoluteExtractionPathString()); Assert.assertEquals( CLASSES_LAYER_LAST_MODIFIED_TIME, classesLayer.getMetadata().getLastModifiedTime()); @@ -133,8 +135,7 @@ public void testToTemplate() throws URISyntaxException, IOException { LayerMetadata.from( ImmutableList.of( new LayerEntry( - Paths.get(CLASSES_LAYER_SOURCE_FILE), - Paths.get(CLASSES_LAYER_EXTRACTION_PATH))), + Paths.get(CLASSES_LAYER_SOURCE_FILE), CLASSES_LAYER_EXTRACTION_PATH)), CLASSES_LAYER_LAST_MODIFIED_TIME); CachedLayerWithMetadata classesLayer = new CachedLayerWithMetadata(classesCachedLayer, classesLayerMetadata); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java index 3f5643d983..9157dcc343 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageLayers; @@ -98,7 +99,8 @@ public void testGetLayerFile() throws CacheMetadataCorruptedException, IOExcepti cacheReader.getLayerFile( ImmutableList.of( new LayerEntry( - Paths.get("/some/source/path"), Paths.get("some/extraction/path"))))); + Paths.get("/some/source/path"), + AbsoluteUnixPath.get("/some/extraction/path"))))); Assert.assertNull(cacheReader.getLayerFile(ImmutableList.of())); } } @@ -146,7 +148,7 @@ public void testGetUpToDateLayerBySourceFiles() testSourceFile -> new LayerMetadata.LayerMetadataEntry( testSourceFile.toString(), - Paths.get("some/extraction/path") + AbsoluteUnixPath.get("/some/extraction/path") .resolve(testSourceFilesPath.relativize(testSourceFile)) .toString())) .collect(ImmutableList.toImmutableList())); @@ -162,7 +164,7 @@ public void testGetUpToDateLayerBySourceFiles() testSourceFile -> new LayerEntry( testSourceFile, - Paths.get("some/extraction/path") + AbsoluteUnixPath.get("/some/extraction/path") .resolve(testSourceFilesPath.relativize(testSourceFile)))) .collect(ImmutableList.toImmutableList()); @@ -186,7 +188,7 @@ public void testGetUpToDateLayerBySourceFiles() testSourceFile -> new LayerEntry( testSourceFile, - Paths.get("extractionPath") + AbsoluteUnixPath.get("/another/extraction/path") .resolve(testSourceFilesPath.relativize(testSourceFile)))) .collect(ImmutableList.toImmutableList())) .isPresent()); @@ -202,7 +204,7 @@ public void testGetUpToDateLayerBySourceFiles() resourceSourceFile -> new LayerEntry( resourceSourceFile, - Paths.get("/some/extraction/path") + AbsoluteUnixPath.get("/some/extraction/path") .resolve( resourceSourceFilesPath.relativize(resourceSourceFile)))) .collect(ImmutableList.toImmutableList())) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 44cf25e142..8d6888a7d0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.cache.LayerMetadata.LayerMetadataEntry; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; @@ -102,13 +103,15 @@ public void test_saveMetadata_noDuplicates() "6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); LayerEntry layerEntry1 = - new LayerEntry(Paths.get("some", "file"), Paths.get("extractionPath1")); + new LayerEntry(Paths.get("some", "file"), AbsoluteUnixPath.get("/extraction/path/1")); LayerEntry layerEntry2 = - new LayerEntry(Paths.get("some", "other", "file"), Paths.get("extractionPath1")); + new LayerEntry( + Paths.get("some", "other", "file"), AbsoluteUnixPath.get("/extraction/path/1")); LayerEntry layerEntry3 = - new LayerEntry(Paths.get("another", "file"), Paths.get("extractionPath2")); + new LayerEntry(Paths.get("another", "file"), AbsoluteUnixPath.get("/extraction/path/2")); LayerEntry layerEntry4 = - new LayerEntry(Paths.get("yet", "another", "file"), Paths.get("extractionPath2")); + new LayerEntry( + Paths.get("yet", "another", "file"), AbsoluteUnixPath.get("/extraction/path/2")); LayerMetadata layerMetadata1 = LayerMetadata.from( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java index c98faf43c1..ced3a601ce 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; @@ -95,7 +96,8 @@ public void testWriteLayer_unwritten() throws IOException { .thenReturn( ImmutableList.of( new LayerEntry( - Paths.get("/some/source/file"), Paths.get("/some/extraction/path")))); + Paths.get("/some/source/file"), + AbsoluteUnixPath.get("/some/extraction/path")))); CachedLayerWithMetadata cachedLayerWithMetadata = cacheWriter.writeLayer(mockReproducibleLayerBuilder); @@ -124,7 +126,8 @@ public void testWriteLayer_doesNotOverwriteExistingTarGz() ReproducibleLayerBuilder layerBuilder = Mockito.mock(ReproducibleLayerBuilder.class); Mockito.when(layerBuilder.build()).thenReturn(unwrittenLayer); LayerEntry layerEntry = - new LayerEntry(Paths.get("some/source/file"), Paths.get("/some/extraction/path")); + new LayerEntry( + Paths.get("some/source/file"), AbsoluteUnixPath.get("/some/extraction/path")); Mockito.when(layerBuilder.getLayerEntries()).thenReturn(ImmutableList.of(layerEntry)); cacheWriter.writeLayer(layerBuilder); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 33f10bd1aa..b269f4dfa1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -67,7 +68,7 @@ public void testBuilder() throws Exception { List expectedLayerConfigurations = Collections.singletonList( LayerConfiguration.builder() - .addEntry(Paths.get("sourceFile"), Paths.get("pathInContainer")) + .addEntry(Paths.get("sourceFile"), AbsoluteUnixPath.get("/path/in/container")) .build()); String expectedCreatedBy = "createdBy"; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java index f2c30b81b2..11f804cd8b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -36,20 +37,22 @@ public void testAddEntryRecursive() throws IOException, URISyntaxException { LayerConfiguration layerConfiguration = LayerConfiguration.builder() - .addEntryRecursive(testDirectory, Paths.get("/app/layer/")) - .addEntryRecursive(testFile, Paths.get("/app/fileA")) + .addEntryRecursive(testDirectory, AbsoluteUnixPath.get("/app/layer/")) + .addEntryRecursive(testFile, AbsoluteUnixPath.get("/app/fileA")) .build(); ImmutableSet expectedLayerEntries = ImmutableSet.of( - new LayerEntry(testDirectory, Paths.get("/app/layer/")), - new LayerEntry(testDirectory.resolve("a"), Paths.get("/app/layer/a/")), - new LayerEntry(testDirectory.resolve("a/b"), Paths.get("/app/layer/a/b/")), - new LayerEntry(testDirectory.resolve("a/b/bar"), Paths.get("/app/layer/a/b/bar/")), - new LayerEntry(testDirectory.resolve("c/"), Paths.get("/app/layer/c")), - new LayerEntry(testDirectory.resolve("c/cat/"), Paths.get("/app/layer/c/cat")), - new LayerEntry(testDirectory.resolve("foo"), Paths.get("/app/layer/foo")), - new LayerEntry(testFile, Paths.get("/app/fileA"))); + new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), + new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), + new LayerEntry(testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), + new LayerEntry( + testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/")), + new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), + new LayerEntry( + testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat")), + new LayerEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), + new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); Assert.assertEquals( expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java index 2a737faa03..1392dae791 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java @@ -60,21 +60,45 @@ public void testEquals() { } @Test - public void testResolve() { + public void testResolve_relativeUnixPath() { AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/"); - Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve(RelativeUnixPath.get(""))); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve("")); + Assert.assertEquals("/file", absoluteUnixPath1.resolve("file").toString()); + Assert.assertEquals("/relative/path", absoluteUnixPath1.resolve("relative/path").toString()); + + AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/some/path"); + Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve("")); + Assert.assertEquals("/some/path/file", absoluteUnixPath2.resolve("file").toString()); Assert.assertEquals( - "/file", absoluteUnixPath1.resolve(RelativeUnixPath.get("file")).toString()); + "/some/path/relative/path", absoluteUnixPath2.resolve("relative/path").toString()); + } + + @Test + public void testResolve_Path_notRelative() { + try { + AbsoluteUnixPath.get("/").resolve(Paths.get("/not/relative")); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "Cannot resolve against absolute Path: " + Paths.get("/not/relative"), ex.getMessage()); + } + } + + @Test + public void testResolve_Path() { + AbsoluteUnixPath absoluteUnixPath1 = AbsoluteUnixPath.get("/"); + Assert.assertEquals(absoluteUnixPath1, absoluteUnixPath1.resolve(Paths.get(""))); + Assert.assertEquals("/file", absoluteUnixPath1.resolve(Paths.get("file")).toString()); Assert.assertEquals( - "/relative/path", - absoluteUnixPath1.resolve(RelativeUnixPath.get("relative/path")).toString()); + "/relative/path", absoluteUnixPath1.resolve(Paths.get("relative/path")).toString()); AbsoluteUnixPath absoluteUnixPath2 = AbsoluteUnixPath.get("/some/path"); - Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve(RelativeUnixPath.get(""))); + Assert.assertEquals(absoluteUnixPath2, absoluteUnixPath2.resolve(Paths.get(""))); Assert.assertEquals( - "/some/path/file", absoluteUnixPath2.resolve(RelativeUnixPath.get("file")).toString()); + "/some/path/file", absoluteUnixPath2.resolve(Paths.get("file///")).toString()); Assert.assertEquals( "/some/path/relative/path", - absoluteUnixPath2.resolve(RelativeUnixPath.get("relative/path")).toString()); + absoluteUnixPath2.resolve(Paths.get("relative//path/")).toString()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index ca69fb5827..86feb505c5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -44,10 +44,14 @@ @RunWith(MockitoJUnitRunner.class) public class JavaDockerContextGeneratorTest { - private static final String EXPECTED_DEPENDENCIES_PATH = "/app/libs/"; - private static final String EXPECTED_RESOURCES_PATH = "/app/resources/"; - private static final String EXPECTED_CLASSES_PATH = "/app/classes/"; - private static final String EXPECTED_EXPLODED_WAR_PATH = "/exploded/war/"; + private static final AbsoluteUnixPath EXPECTED_DEPENDENCIES_PATH = + AbsoluteUnixPath.get("/app/libs/"); + private static final AbsoluteUnixPath EXPECTED_RESOURCES_PATH = + AbsoluteUnixPath.get("/app/resources/"); + private static final AbsoluteUnixPath EXPECTED_CLASSES_PATH = + AbsoluteUnixPath.get("/app/classes/"); + private static final AbsoluteUnixPath EXPECTED_EXPLODED_WAR_PATH = + AbsoluteUnixPath.get("/exploded/war/"); private static void assertSameFiles(Path directory1, Path directory2) throws IOException { ImmutableList directory1Files = @@ -69,8 +73,8 @@ private static void assertSameFiles(Path directory1, Path directory2) throws IOE @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; - private ImmutableList filesToLayerEntries(Path directory, Path extractionPathRoot) - throws IOException { + private ImmutableList filesToLayerEntries( + Path directory, AbsoluteUnixPath extractionPathRoot) throws IOException { return new DirectoryWalker(directory) .walk() .stream() @@ -100,32 +104,30 @@ public void testGenerate() throws IOException, URISyntaxException { Files.delete(targetDirectory); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); + .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH)); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn( - filesToLayerEntries(testSnapshotDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); + .thenReturn(filesToLayerEntries(testSnapshotDependencies, EXPECTED_DEPENDENCIES_PATH)); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(filesToLayerEntries(testResources, Paths.get(EXPECTED_RESOURCES_PATH))); + .thenReturn(filesToLayerEntries(testResources, EXPECTED_RESOURCES_PATH)); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(filesToLayerEntries(testClasses, Paths.get(EXPECTED_CLASSES_PATH))); + .thenReturn(filesToLayerEntries(testClasses, EXPECTED_CLASSES_PATH)); Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) - .thenReturn( - filesToLayerEntries(testExplodedWarFiles, Paths.get(EXPECTED_EXPLODED_WAR_PATH))); + .thenReturn(filesToLayerEntries(testExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH)); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(filesToLayerEntries(testExtraFiles, Paths.get("/"))); + .thenReturn(filesToLayerEntries(testExtraFiles, AbsoluteUnixPath.get("/"))); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testDependencies, Paths.get(EXPECTED_DEPENDENCIES_PATH))); + .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH)); Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH)); + .thenReturn(EXPECTED_DEPENDENCIES_PATH); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH)); + .thenReturn(EXPECTED_DEPENDENCIES_PATH); Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_RESOURCES_PATH)); + .thenReturn(EXPECTED_RESOURCES_PATH); Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_CLASSES_PATH)); + .thenReturn(EXPECTED_CLASSES_PATH); Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_EXPLODED_WAR_PATH)); + .thenReturn(EXPECTED_EXPLODED_WAR_PATH); Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()) .thenReturn(AbsoluteUnixPath.get("/")); @@ -161,31 +163,28 @@ public void testMakeDockerfile() throws IOException { Path ignored = Paths.get("ignored"); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_DEPENDENCIES_PATH)))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_DEPENDENCIES_PATH))); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_DEPENDENCIES_PATH)))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_DEPENDENCIES_PATH))); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_RESOURCES_PATH)))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_RESOURCES_PATH))); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_CLASSES_PATH)))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_CLASSES_PATH))); Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, Paths.get(EXPECTED_EXPLODED_WAR_PATH)))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_EXPLODED_WAR_PATH))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, Paths.get("/")))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/")))); Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH)); + .thenReturn(EXPECTED_DEPENDENCIES_PATH); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_DEPENDENCIES_PATH)); + .thenReturn(EXPECTED_DEPENDENCIES_PATH); Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_RESOURCES_PATH)); + .thenReturn(EXPECTED_RESOURCES_PATH); Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_CLASSES_PATH)); + .thenReturn(EXPECTED_CLASSES_PATH); Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath()) - .thenReturn(AbsoluteUnixPath.get(EXPECTED_EXPLODED_WAR_PATH)); + .thenReturn(EXPECTED_EXPLODED_WAR_PATH); Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath()) .thenReturn(AbsoluteUnixPath.get("/")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index c01d9d2f7c..d126b5a003 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -97,22 +97,22 @@ public void testSetFiles_extractionPaths() throws IOException { AbsoluteUnixPath.get("/some/extras"), configurations.getExtraFilesExtractionPath()); Assert.assertEquals( - Paths.get("/dependency/path/dependency"), + AbsoluteUnixPath.get("/dependency/path/dependency"), configurations.getDependencyLayerEntries().get(0).getExtractionPath()); Assert.assertEquals( - Paths.get("/snapshots/snapshot dependency"), + AbsoluteUnixPath.get("/snapshots/snapshot dependency"), configurations.getSnapshotDependencyLayerEntries().get(0).getExtractionPath()); Assert.assertEquals( - Paths.get("/resources/here/resource"), + AbsoluteUnixPath.get("/resources/here/resource"), configurations.getResourceLayerEntries().get(0).getExtractionPath()); Assert.assertEquals( - Paths.get("/classes/go/here/class"), + AbsoluteUnixPath.get("/classes/go/here/class"), configurations.getClassLayerEntries().get(0).getExtractionPath()); Assert.assertEquals( - Paths.get("/for/war/exploded war"), + AbsoluteUnixPath.get("/for/war/exploded war"), configurations.getExplodedWarEntries().get(0).getExtractionPath()); Assert.assertEquals( - Paths.get("/some/extras/extra file"), + AbsoluteUnixPath.get("/some/extras/extra file"), configurations.getExtraFilesLayerEntries().get(0).getExtractionPath()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 6e009cdce9..cccc80491a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; @@ -97,9 +98,10 @@ public void testBuild() throws URISyntaxException, IOException { ReproducibleLayerBuilder layerBuilder = new ReproducibleLayerBuilder( LayerConfiguration.builder() - .addEntryRecursive(layerDirectory, Paths.get("extract/here/apple/layer")) - .addEntry(blobA, Paths.get("extract/here/apple/blobA")) - .addEntry(blobA, Paths.get("extract/here/banana/blobA")) + .addEntryRecursive( + layerDirectory, AbsoluteUnixPath.get("/extract/here/apple/layer")) + .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/apple/blobA")) + .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/banana/blobA")) .build() .getLayerEntries()); @@ -145,8 +147,6 @@ public void testToBlob_reproducibility() throws IOException { Path root1 = Files.createDirectories(testRoot.resolve("files1")); Path root2 = Files.createDirectories(testRoot.resolve("files2")); - Path extractionPath = Paths.get("/somewhere"); - // TODO: Currently this test only covers variation in order and modified time, even though // TODO: the code is designed to clean up userid/groupid, this test does not check that yet. String contentA = "abcabc"; @@ -164,15 +164,15 @@ public void testToBlob_reproducibility() throws IOException { Blob layer = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA1, extractionPath.resolve("fileA")), - new LayerEntry(fileB1, extractionPath.resolve("fileB")))) + new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), + new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) .build() .getBlob(); Blob reproduced = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileB2, extractionPath.resolve("fileB")), - new LayerEntry(fileA2, extractionPath.resolve("fileA")))) + new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), + new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) .build() .getBlob(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 58ccc86995..9f0c7e105e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; @@ -89,13 +90,13 @@ public void setUpMocks() { .thenReturn( ImmutableList.of( LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) .build(), LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) .build(), LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), Paths.get("ignored")) + .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) .build())); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 637821f1f1..3dae8b30c5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.LayerEntry; @@ -67,7 +68,8 @@ public void testResolveMainClass() throws MainClassInferenceException { public void testResolveMainClass_notValid() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, Paths.get("ignored")))); + .thenReturn( + ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -83,7 +85,7 @@ public void testResolveMainClass_multipleInferredWithBackup() Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) .walk() .stream() - .map(path -> new LayerEntry(path, Paths.get("ignored"))) + .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"))) .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); @@ -100,7 +102,7 @@ public void testResolveMainClass_multipleInferredWithoutBackup() Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) .walk() .stream() - .map(path -> new LayerEntry(path, Paths.get("ignored"))) + .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"))) .collect(ImmutableList.toImmutableList())); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); @@ -118,7 +120,9 @@ public void testResolveMainClass_multipleInferredWithoutBackup() public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("ignored")))); + .thenReturn( + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); @@ -127,7 +131,9 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere @Test public void testResolveMainClass_noneInferredWithoutBackup() { Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), Paths.get("ignored")))); + .thenReturn( + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); From 24afb5baa784f1959e9bd8f8a597edd8d86b29f8 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 20 Sep 2018 16:46:00 -0400 Subject: [PATCH 0222/2020] Adds Cache with initial method for fetching a cache entry by layer entries. (#975) --- .../cloud/tools/jib/cache/CacheReader.java | 2 +- .../tools/jib/json/JsonTemplateMapper.java | 2 +- .../tools/jib/json/ListOfJsonTemplate.java | 4 +- .../google/cloud/tools/jib/ncache/Cache.java | 65 +++++++++ .../jib/ncache/LayerEntriesSelector.java | 137 ++++++++++++++++++ .../jib/ncache/LayerEntriesSelectorTest.java | 91 ++++++++++++ 6 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java index 76bf7a5044..3eb00cb623 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java @@ -110,7 +110,7 @@ public Path getLayerFile(ImmutableList layerEntries) } /** - * Gets an up-to-date layer that is built from the {@code sourceFiles}. + * Gets an up-to-date layer that is built from the {@code layerEntries}. * *

The method returns the first up-to-date layer found. This is safe because the source files * will not have been modified since creation of any up-to-date layer (ie. all up-to-date layers diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index 82042ef3dc..cce699eeca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -109,7 +109,7 @@ public static T readJson(String jsonString, Class te * @return the template filled with the values parsed from {@code jsonFile} * @throws IOException if an error occurred during parsing the JSON */ - public static List readListOfJson( + public static > List readListOfJson( String jsonString, Class templateClass) throws IOException { return objectMapper.readValue( jsonString, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java index e1d58de333..50d200b846 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java @@ -24,8 +24,8 @@ * *

Json fields should be private fields and fields that are {@code null} will not be serialized. */ -public interface ListOfJsonTemplate extends JsonTemplate { +public interface ListOfJsonTemplate extends JsonTemplate { /** @return the JsonTemplate wrapped as a list. e.g.: [{"property":"value"}] */ - List getList(); + List getList(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java new file mode 100644 index 0000000000..1d7d58d180 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.Optional; + +/** Cache for storing data to be shared between Jib executions. */ +public class Cache { + + private final CacheStorage cacheStorage; + + // TODO: Add way to instantiate after #retrieveCacheEntry and #write are implemented. + private Cache(CacheStorage cacheStorage) { + this.cacheStorage = cacheStorage; + } + + /** + * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. + * + * @param layerEntries the layer entries to match against + * @return a {@link CacheEntry} that was built from {@code layerEntries}, or {@link + * Optional#empty} if none found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + // TODO: Add test once other unimplemented methods are implemented. + public Optional retrieveCacheEntry(ImmutableList layerEntries) + throws IOException, CacheCorruptedException { + Optional optionalSelectedLayerDigest = + cacheStorage.select(LayerEntriesSelector.generateSelector(layerEntries)); + if (!optionalSelectedLayerDigest.isPresent()) { + return Optional.empty(); + } + + return cacheStorage.retrieve(optionalSelectedLayerDigest.get()); + } + + public Optional retrieveCacheEntry(DescriptorDigest layerDigest) { + // TODO: Implement + return Optional.empty(); + } + + public Optional write(CacheWrite cacheWrite) { + // TODO: Implement + return Optional.empty(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java new file mode 100644 index 0000000000..bc1ca678dc --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.json.ListOfJsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Generates a selector based on {@link LayerEntry}s for a layer. Selectors are secondary references + * for a cache entries. + * + *

The selector is the SHA256 hash of the list of layer entries serialized in the following form: + * + *

{@code
+ * [
+ *   {
+ *     "sourceFile": "source/file/for/layer/entry/1",
+ *     "extractionPath": "extraction/path/for/layer/entry/1"
+ *   },
+ *   {
+ *     "sourceFile": "source/file/for/layer/entry/2",
+ *     "extractionPath": "extraction/path/for/layer/entry/2"
+ *   }
+ * ]
+ * }
+ */ +class LayerEntriesSelector { + + /** Serialized form of a {@link LayerEntry}. */ + @VisibleForTesting + static class LayerEntryTemplate implements JsonTemplate, Comparable { + + private final String sourceFile; + private final String extractionPath; + + @VisibleForTesting + LayerEntryTemplate(LayerEntry layerEntry) { + sourceFile = layerEntry.getAbsoluteSourceFileString(); + extractionPath = layerEntry.getAbsoluteExtractionPathString(); + } + + @Override + public int compareTo(LayerEntryTemplate otherLayerEntryTemplate) { + int sourceFileComparison = sourceFile.compareTo(otherLayerEntryTemplate.sourceFile); + if (sourceFileComparison != 0) { + return sourceFileComparison; + } + return extractionPath.compareTo(otherLayerEntryTemplate.extractionPath); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LayerEntryTemplate)) { + return false; + } + LayerEntryTemplate otherLayerEntryTemplate = (LayerEntryTemplate) other; + return sourceFile.equals(otherLayerEntryTemplate.sourceFile) + && extractionPath.equals(otherLayerEntryTemplate.extractionPath); + } + + @Override + public int hashCode() { + return Objects.hash(sourceFile, extractionPath); + } + } + + /** Serialized form of a list of {@link LayerEntry}s. */ + @VisibleForTesting + static class LayerEntriesTemplate implements ListOfJsonTemplate { + + private final List layerEntryTemplates = new ArrayList<>(); + + @Override + public List getList() { + return layerEntryTemplates; + } + + /** + * Serializes a list of {@link LayerEntry}s into a new {@link LayerEntriesTemplate}. The list is + * sorted by source file first, then extraction path (see {@link LayerEntryTemplate#compareTo}). + * + * @param layerEntries the list of {@link LayerEntry}s + */ + @VisibleForTesting + LayerEntriesTemplate(ImmutableList layerEntries) { + for (LayerEntry layerEntry : layerEntries) { + layerEntryTemplates.add(new LayerEntryTemplate(layerEntry)); + } + Collections.sort(layerEntryTemplates); + } + } + + /** + * Generates a selector for the list of {@link LayerEntry}s. The selector is unique to each unique + * set of layer entries, regardless of order. TODO: Should we care about order? + * + * @param layerEntries the layer entries + * @return the selector + * @throws IOException if an I/O exception occurs + */ + static DescriptorDigest generateSelector(ImmutableList layerEntries) + throws IOException { + return JsonTemplateMapper.toBlob(new LayerEntriesTemplate(layerEntries)) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + } + + private LayerEntriesSelector() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java new file mode 100644 index 0000000000..f515fe2b6f --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.ncache.LayerEntriesSelector.LayerEntriesTemplate; +import com.google.cloud.tools.jib.ncache.LayerEntriesSelector.LayerEntryTemplate; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LayerEntriesSelector}. */ +public class LayerEntriesSelectorTest { + + private static final LayerEntry TEST_LAYER_ENTRY_1 = + new LayerEntry(Paths.get("source", "file"), Paths.get("extraction", "path")); + private static final LayerEntry TEST_LAYER_ENTRY_2 = + new LayerEntry(Paths.get("source", "file", "two"), Paths.get("extraction", "path")); + private static final LayerEntry TEST_LAYER_ENTRY_3 = + new LayerEntry(Paths.get("source", "gile"), Paths.get("extraction", "path")); + private static final LayerEntry TEST_LAYER_ENTRY_4 = + new LayerEntry(Paths.get("source", "gile"), Paths.get("extraction", "patha")); + + private static final ImmutableList OUT_OF_ORDER_LAYER_ENTRIES = + ImmutableList.of( + TEST_LAYER_ENTRY_4, TEST_LAYER_ENTRY_2, TEST_LAYER_ENTRY_3, TEST_LAYER_ENTRY_1); + private static final ImmutableList IN_ORDER_LAYER_ENTRIES = + ImmutableList.of( + TEST_LAYER_ENTRY_1, TEST_LAYER_ENTRY_2, TEST_LAYER_ENTRY_3, TEST_LAYER_ENTRY_4); + + private static ImmutableList toLayerEntryTemplates( + ImmutableList layerEntries) { + return layerEntries + .stream() + .map(LayerEntryTemplate::new) + .collect(ImmutableList.toImmutableList()); + } + + @Test + public void testLayerEntryTemplate_compareTo() { + Assert.assertEquals( + toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES), + ImmutableList.sortedCopyOf(toLayerEntryTemplates(OUT_OF_ORDER_LAYER_ENTRIES))); + } + + @Test + public void testLayerEntriesTemplate_getList() { + Assert.assertEquals( + toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES), + new LayerEntriesTemplate(OUT_OF_ORDER_LAYER_ENTRIES).getList()); + } + + @Test + public void testGenerateSelector_empty() throws IOException { + DescriptorDigest expectedSelector = + JsonTemplateMapper.toBlob(new LayerEntriesTemplate(ImmutableList.of())) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of())); + } + + @Test + public void testGenerateSelector() throws IOException { + DescriptorDigest expectedSelector = + JsonTemplateMapper.toBlob(new LayerEntriesTemplate(IN_ORDER_LAYER_ENTRIES)) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(OUT_OF_ORDER_LAYER_ENTRIES)); + } +} From 3e89e15dcfb897648badea1f92b0e68a5292e70c Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 20 Sep 2018 16:47:46 -0400 Subject: [PATCH 0223/2020] Keep duplicate layers to match history (#1017) --- .../cloud/tools/jib/cache/CacheMetadata.java | 2 +- .../cloud/tools/jib/image/ImageLayers.java | 33 +++++++++++++++---- .../tools/jib/image/ImageLayersTest.java | 27 ++++++++++++++- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java index 535a5be1f4..f502e6369d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java @@ -34,7 +34,7 @@ class CacheMetadata { static class Builder { private final ImageLayers.Builder layersBuilder = - ImageLayers.builder(); + ImageLayers.builder().removeDuplicates(); private Builder(ImageLayers initialLayers) { layersBuilder.addAll(initialLayers); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java index bd92686353..18c165b9d2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java @@ -18,18 +18,23 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import javax.annotation.Nullable; -/** Holds the layers for an image. Makes sure that there are no duplicate layers. */ +/** Holds the layers for an image. */ public class ImageLayers implements Iterable { public static class Builder { - private final LinkedHashSet layers = new LinkedHashSet<>(); + private final List layers = new ArrayList<>(); private final ImmutableSet.Builder layerDigestsBuilder = ImmutableSet.builder(); + private boolean removeDuplicates = false; /** * Adds a layer. Removes any prior occurrences of the same layer. @@ -43,11 +48,7 @@ public static class Builder { */ public Builder add(T layer) throws LayerPropertyNotFoundException { layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); - - // Remove necessary to move layer to the end of the LinkedHashSet. - layers.remove(layer); layers.add(layer); - return this; } @@ -67,8 +68,26 @@ public Builder addAll(ImageLayers layers) return this; } + /** + * Remove any duplicate layers, keeping the last occurrence of the layer. + * + * @return this + */ + public Builder removeDuplicates() { + removeDuplicates = true; + return this; + } + public ImageLayers build() { - return new ImageLayers<>(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); + if (!removeDuplicates) { + return new ImageLayers<>(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); + } + + // LinkedHashSet maintains the order but keeps the first occurrence. Keep last occurrence by + // adding elements in reverse, and then reversing the result + Set dedupedButReversed = new LinkedHashSet(Lists.reverse(this.layers)); + ImmutableList deduped = ImmutableList.copyOf(dedupedButReversed).reverse(); + return new ImageLayers<>(deduped, layerDigestsBuilder.build()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index 497745e110..3346f786ba 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -74,12 +74,37 @@ public void testAddLayer_success() throws LayerPropertyNotFoundException { } @Test - public void testAddLayer_sameAsLastLayer() throws LayerPropertyNotFoundException { + public void testAddLayer_maintainDuplicates() throws LayerPropertyNotFoundException { + // must maintain duplicate + List expectedLayers = + Arrays.asList( + mockCachedLayer, + mockReferenceLayer, + mockDigestOnlyLayer, + mockUnwrittenLayer, + mockCachedLayer); + + ImageLayers imageLayers = + ImageLayers.builder() + .add(mockCachedLayer) + .add(mockReferenceLayer) + .add(mockDigestOnlyLayer) + .add(mockUnwrittenLayer) + .add(mockCachedLayer) + .build(); + + Assert.assertEquals(expectedLayers, imageLayers.getLayers()); + } + + @Test + public void testAddLayer_removeDuplicates() throws LayerPropertyNotFoundException { + // remove duplicates: last layer should be kept List expectedLayers = Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockUnwrittenLayer, mockCachedLayer); ImageLayers imageLayers = ImageLayers.builder() + .removeDuplicates() .add(mockCachedLayer) .add(mockReferenceLayer) .add(mockDigestOnlyLayer) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 126b1bf107..b34c342a41 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) + ## 0.9.10 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index d61c54e7de..1e6fbcc127 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) + ## 0.9.10 ### Added From 6af6257f130f4a02abfed02af25031c52c98bad1 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 20 Sep 2018 19:09:52 -0400 Subject: [PATCH 0224/2020] Fixes broken compilation caused by merging #1014 and #975. (#1019) --- .../cloud/tools/jib/ncache/LayerEntriesSelector.java | 4 ++-- .../cloud/tools/jib/ncache/LayerEntriesSelectorTest.java | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java index bc1ca678dc..586fc50bdd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java @@ -40,11 +40,11 @@ * [ * { * "sourceFile": "source/file/for/layer/entry/1", - * "extractionPath": "extraction/path/for/layer/entry/1" + * "extractionPath": "/extraction/path/for/layer/entry/1" * }, * { * "sourceFile": "source/file/for/layer/entry/2", - * "extractionPath": "extraction/path/for/layer/entry/2" + * "extractionPath": "/extraction/path/for/layer/entry/2" * } * ] * }
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java index f515fe2b6f..aef40e925a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -32,13 +33,13 @@ public class LayerEntriesSelectorTest { private static final LayerEntry TEST_LAYER_ENTRY_1 = - new LayerEntry(Paths.get("source", "file"), Paths.get("extraction", "path")); + new LayerEntry(Paths.get("source", "file"), AbsoluteUnixPath.get("/extraction/path")); private static final LayerEntry TEST_LAYER_ENTRY_2 = - new LayerEntry(Paths.get("source", "file", "two"), Paths.get("extraction", "path")); + new LayerEntry(Paths.get("source", "file", "two"), AbsoluteUnixPath.get("/extraction/path")); private static final LayerEntry TEST_LAYER_ENTRY_3 = - new LayerEntry(Paths.get("source", "gile"), Paths.get("extraction", "path")); + new LayerEntry(Paths.get("source", "gile"), AbsoluteUnixPath.get("/extraction/path")); private static final LayerEntry TEST_LAYER_ENTRY_4 = - new LayerEntry(Paths.get("source", "gile"), Paths.get("extraction", "patha")); + new LayerEntry(Paths.get("source", "gile"), AbsoluteUnixPath.get("/extraction/patha")); private static final ImmutableList OUT_OF_ORDER_LAYER_ENTRIES = ImmutableList.of( From 0998f2452acc9b3886eb9aaf51088522e48d392a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 21 Sep 2018 16:41:23 -0400 Subject: [PATCH 0225/2020] Adds to.tags configuration for Gradle. (#978) --- .../jib/builder/steps/PushImageStep.java | 1 + jib-gradle-plugin/CHANGELOG.md | 1 + .../jib/gradle/JibPluginIntegrationTest.java | 72 ++++++++++++++-- .../resources/projects/empty/build.gradle | 5 ++ .../tools/jib/gradle/BaseImageParameters.java | 68 +++++++++++++++ .../tools/jib/gradle/BuildImageTask.java | 1 + .../tools/jib/gradle/ImageParameters.java | 38 ++------- .../cloud/tools/jib/gradle/JibExtension.java | 17 ++-- .../jib/gradle/TargetImageParameters.java | 83 +++++++++++++++++++ .../PluginConfigurationProcessorTest.java | 6 +- 10 files changed, 240 insertions(+), 52 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index d1a4bc1276..5b0166c157 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -136,6 +136,7 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx // Pushes to all target image tags. // TODO: Parallelize. for (String tag : buildConfiguration.getAllTargetImageTags()) { + buildConfiguration.getBuildLogger().info("Tagging with " + tag + "..."); registryClient.pushManifest(manifestTemplate, tag); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index b34c342a41..aaf2a9cd5c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `container.environment` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) - `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `jib.to.tags` (list) defines additional tags to push to ([#978](https://github.com/GoogleContainerTools/jib/pull/978)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 8c578b69d9..7b17a8cf29 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.file.Files; @@ -61,11 +63,30 @@ private static String buildAndRun(TestProject testProject, String imageReference assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - new Command("docker", "pull", imageReference).run(); - assertDockerInspect(imageReference); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", "--rm", imageReference).run(); + return pullAndRunBuiltImage(imageReference); + } + + private static void buildAndRunAdditionalTag( + TestProject testProject, String imageReference, String additionalTag, String expectedOutput) + throws InvalidImageReferenceException, IOException, InterruptedException { + BuildResult buildResult = + testProject.build( + "clean", + JibPlugin.BUILD_IMAGE_TASK_NAME, + "-D_TARGET_IMAGE=" + imageReference, + "-D_ADDITIONAL_TAG=" + additionalTag); + assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + String additionalImageReference = + ImageReference.parse(imageReference).withTag(additionalTag).toString(); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString(additionalImageReference)); + + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(imageReference)); + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(additionalImageReference)); + assertCreationTimeEpoch(imageReference); + assertCreationTimeEpoch(additionalImageReference); } private static String buildAndRunComplex( @@ -106,6 +127,25 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String return new Command("docker", "run", "--rm", imageReference).run(); } + /** + * Pulls a built image and attemps to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @return the container output + * @throws IOException if an I/O exceptio occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + return new Command("docker", "run", "--rm", imageReference).run(); + } + /** * Asserts that the test project build output indicates a success. * @@ -171,6 +211,13 @@ private static void assertDockerInspect(String imageReference) + " }")); } + private static void assertCreationTimeEpoch(String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); + } + @Before public void setup() throws IOException, InterruptedException { // Pull distroless and push to local registry so we can test 'from' credentials @@ -185,9 +232,18 @@ public void testBuild_empty() throws IOException, InterruptedException { + "/emptyimage:gradle" + System.nanoTime(); Assert.assertEquals("", buildAndRun(emptyTestProject, targetImage)); - Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + assertCreationTimeEpoch(targetImage); + } + + @Test + public void testBuild_multipleTags() + throws IOException, InterruptedException, InvalidImageReferenceException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/multitag-image:gradle" + + System.nanoTime(); + buildAndRunAdditionalTag(emptyTestProject, targetImage, "gradle-2", ""); } @Test diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index 7f20ccc2d9..c1bce5d2a2 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -24,3 +24,8 @@ jib { // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true } + +def additionalTag = System.getProperty("_ADDITIONAL_TAG") +if (additionalTag != null && !additionalTag.isEmpty()) { + jib.to.tags = [System.getProperty("_ADDITIONAL_TAG")] +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java new file mode 100644 index 0000000000..6956633a1f --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; + +/** {@link ImageParameters} that configure the base image. */ +public class BaseImageParameters implements ImageParameters { + + private final AuthParameters auth; + + @Nullable private String image; + @Nullable private String credHelper; + + @Inject + public BaseImageParameters(ObjectFactory objectFactory, String imageDescriptor) { + auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); + } + + @Nullable + @Override + public String getImage() { + return image; + } + + @Override + public void setImage(String image) { + this.image = image; + } + + @Nullable + @Override + public String getCredHelper() { + return credHelper; + } + + @Override + public void setCredHelper(String credHelper) { + this.credHelper = credHelper; + } + + @Override + public AuthParameters getAuth() { + return auth; + } + + @Override + public void auth(Action action) { + action.execute(auth); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 182509d286..49f1968497 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -118,6 +118,7 @@ public void buildImage() throws InvalidImageReferenceException { .setBaseImageConfiguration( pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(targetImageConfiguration) + .setAdditionalTargetImageTags(jibExtension.getTo().getTags()) .setContainerConfiguration( pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .setTargetFormat(jibExtension.getFormat()) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java index 9616762248..1f97ef5d7a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java @@ -17,9 +17,7 @@ package com.google.cloud.tools.jib.gradle; import javax.annotation.Nullable; -import javax.inject.Inject; import org.gradle.api.Action; -import org.gradle.api.model.ObjectFactory; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; @@ -31,47 +29,25 @@ *

{@code image} (required) is the image reference and {@code credHelper} (optional) is the name * (after {@code docker-credential} of the credential helper for accessing the {@code image}. */ -public class ImageParameters { - - private AuthParameters auth; - - @Nullable private String image; - @Nullable private String credHelper; - - @Inject - public ImageParameters(ObjectFactory objectFactory, String imageDescriptor) { - auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); - } +interface ImageParameters { @Input @Nullable @Optional - public String getImage() { - return image; - } + String getImage(); - public void setImage(String image) { - this.image = image; - } + void setImage(String image); @Input @Nullable @Optional - public String getCredHelper() { - return credHelper; - } + String getCredHelper(); - public void setCredHelper(String credHelper) { - this.credHelper = credHelper; - } + void setCredHelper(String credHelper); @Nested @Optional - AuthParameters getAuth() { - return auth; - } + AuthParameters getAuth(); - public void auth(Action action) { - action.execute(auth); - } + void auth(Action action); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index fe9b7df018..b83afea98a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -74,15 +74,13 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { return projectDirectory.resolve("src").resolve("main").resolve("jib"); } - private final ImageParameters from; - private final ImageParameters to; + private final BaseImageParameters from; + private final TargetImageParameters to; private final ContainerParameters container; private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; private final Property extraDirectory; - private final Path projectDir; - // TODO: Deprecated parameters; remove these 4 private final ListProperty jvmFlags; private final Property mainClass; @@ -90,11 +88,10 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { private final Property format; public JibExtension(Project project) { - projectDir = project.getProjectDir().toPath(); ObjectFactory objectFactory = project.getObjects(); - from = objectFactory.newInstance(ImageParameters.class, "jib.from"); - to = objectFactory.newInstance(ImageParameters.class, "jib.to"); + from = objectFactory.newInstance(BaseImageParameters.class, "jib.from"); + to = objectFactory.newInstance(TargetImageParameters.class, "jib.to"); container = objectFactory.newInstance(ContainerParameters.class); jvmFlags = objectFactory.listProperty(String.class); @@ -112,7 +109,7 @@ public JibExtension(Project project) { args.set(Collections.emptyList()); useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); - extraDirectory.set(resolveDefaultExtraDirectory(projectDir)); + extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath())); } /** @@ -207,13 +204,13 @@ String getTargetImage() { @Nested @Optional - public ImageParameters getFrom() { + public BaseImageParameters getFrom() { return from; } @Nested @Optional - public ImageParameters getTo() { + public TargetImageParameters getTo() { return to; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java new file mode 100644 index 0000000000..cf3f3e91b6 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import java.util.Collections; +import java.util.Set; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +/** {@link ImageParameters} that configure the target image. */ +public class TargetImageParameters implements ImageParameters { + + private final AuthParameters auth; + + @Nullable private String image; + private Set tags = Collections.emptySet(); + @Nullable private String credHelper; + + @Inject + public TargetImageParameters(ObjectFactory objectFactory, String imageDescriptor) { + auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); + } + + @Nullable + @Override + public String getImage() { + return image; + } + + @Override + public void setImage(String image) { + this.image = image; + } + + @Input + @Optional + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + @Nullable + @Override + public String getCredHelper() { + return credHelper; + } + + @Override + public void setCredHelper(String credHelper) { + this.credHelper = credHelper; + } + + @Override + public AuthParameters getAuth() { + return auth; + } + + @Override + public void auth(Action action) { + action.execute(auth); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index eb895db441..3e8bcc2737 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -37,15 +37,15 @@ public class PluginConfigurationProcessorTest { @Mock GradleJibLogger mockGradleJibLogger; @Mock JibExtension mockJibExtension; - @Mock ImageParameters mockImageParameters; + @Mock BaseImageParameters mockBaseImageParameters; @Mock ContainerParameters mockContainerParameters; @Mock GradleProjectProperties mockProjectProperties; @Before public void setUp() throws Exception { Mockito.doReturn("gcr.io/distroless/java").when(mockJibExtension).getBaseImage(); - Mockito.doReturn(mockImageParameters).when(mockJibExtension).getFrom(); - Mockito.doReturn(new AuthParameters("mock")).when(mockImageParameters).getAuth(); + Mockito.doReturn(mockBaseImageParameters).when(mockJibExtension).getFrom(); + Mockito.doReturn(new AuthParameters("mock")).when(mockBaseImageParameters).getAuth(); Mockito.doReturn(mockContainerParameters).when(mockJibExtension).getContainer(); Mockito.doReturn(Collections.emptyList()).when(mockContainerParameters).getEntrypoint(); Mockito.doReturn("/app").when(mockContainerParameters).getAppRoot(); From 6875a05ab0bbbf6a7769bedf4ac720da09e0a07c Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 24 Sep 2018 11:36:33 -0700 Subject: [PATCH 0226/2020] Adds to.tags configuration for Maven. (#1026) --- .../jib/gradle/JibPluginIntegrationTest.java | 6 +- .../tools/jib/gradle/BuildDockerTask.java | 1 + jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 1 + .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../jib/maven/JibPluginConfiguration.java | 8 ++ .../maven/BuildImageMojoIntegrationTest.java | 73 +++++++++++++++++-- .../src/test/resources/projects/empty/pom.xml | 1 + 8 files changed, 84 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 7b17a8cf29..67e0477b67 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -128,12 +128,12 @@ private static String buildToDockerDaemonAndRun(TestProject testProject, String } /** - * Pulls a built image and attemps to run it. Also verifies the container configuration and + * Pulls a built image and attempts to run it. Also verifies the container configuration and * history of the built image. * * @param imageReference the image reference of the built image * @return the container output - * @throws IOException if an I/O exceptio occurs + * @throws IOException if an I/O exception occurs * @throws InterruptedException if the process was interrupted */ private static String pullAndRunBuiltImage(String imageReference) @@ -243,7 +243,7 @@ public void testBuild_multipleTags() + IntegrationTestingConfiguration.getGCPProject() + "/multitag-image:gradle" + System.nanoTime(); - buildAndRunAdditionalTag(emptyTestProject, targetImage, "gradle-2", ""); + buildAndRunAdditionalTag(emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); } @Test diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 755593c375..ea562eaa9c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -100,6 +100,7 @@ public void buildDocker() throws InvalidImageReferenceException { .setBaseImageConfiguration( pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setAdditionalTargetImageTags(jibExtension.getTo().getTags()) .setContainerConfiguration( pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 1e6fbcc127..8413df52ac 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property) ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) - `` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) - `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `` (list) defines additional tags to push to ([#1026](https://github.com/GoogleContainerTools/jib/pull/1026)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 5ff5f7fe04..c73ed2e0d3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -86,6 +86,7 @@ public void execute() throws MojoExecutionException { .setBaseImageConfiguration( pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) + .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) .setContainerConfiguration( pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .build(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 1c9ebdd4d7..b1155d4fc1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -126,6 +126,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setBaseImageConfiguration( pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) .setTargetImageConfiguration(targetImageConfiguration) + .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) .setContainerConfiguration( pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index e44830b973..d91ce7e276 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -25,8 +25,10 @@ import java.io.File; import java.nio.file.Path; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; @@ -105,6 +107,8 @@ public static class ToConfiguration { @Nullable @Parameter private String image; + @Parameter private List tags = Collections.emptyList(); + @Nullable @Parameter private String credHelper; @Parameter private AuthConfiguration auth = new AuthConfiguration(); @@ -249,6 +253,10 @@ String getTargetImage() { return to.image; } + Set getTargetImageAdditionalTags() { + return new HashSet<>(to.tags); + } + @Nullable String getTargetImageCredentialHelperName() { return Preconditions.checkNotNull(to).credHelper; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index b9c784639e..840165e362 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -98,9 +100,34 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole verifier.verifyErrorFreeLog(); - new Command("docker", "pull", imageReference).run(); - assertDockerInspectParameters(imageReference); - return new Command("docker", "run", "--rm", imageReference).run(); + return pullAndRunBuiltImage(imageReference); + } + + private static String buildAndRunAdditionalTag( + Path projectRoot, String imageReference, String additionalTag) + throws VerificationException, InvalidImageReferenceException, IOException, + InterruptedException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.executeGoals(Arrays.asList("clean", "compile")); + + verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); + verifier.verifyErrorFreeLog(); + + String additionalImageReference = + ImageReference.parse(imageReference).withTag(additionalTag).toString(); + + String output = pullAndRunBuiltImage(imageReference); + String additionalOutput = pullAndRunBuiltImage(additionalImageReference); + Assert.assertEquals(output, additionalOutput); + + assertCreationTimeEpoch(imageReference); + assertCreationTimeEpoch(additionalImageReference); + + return output; } private static String buildAndRunComplex( @@ -128,6 +155,22 @@ private static String buildAndRunComplex( return new Command("docker", "run", "--rm", imageReference).run(); } + /** + * Pulls a built image and attempts to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + assertDockerInspectParameters(imageReference); + return new Command("docker", "run", "--rm", imageReference).run(); + } + private static void assertDockerInspectParameters(String imageReference) throws IOException, InterruptedException { String dockerInspect = new Command("docker", "inspect", imageReference).run(); @@ -167,6 +210,13 @@ private static float getBuildTimeFromVerifierLog(Verifier verifier) throws IOExc return -1; } + private static void assertCreationTimeEpoch(String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); + } + @Before public void setup() throws IOException, InterruptedException { // Pull distroless to local registry so we can test 'from' credentials @@ -232,9 +282,22 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi + System.nanoTime(); Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); + assertCreationTimeEpoch(targetImage); + } + + @Test + public void testExecute_multipleTags() + throws IOException, InterruptedException, InvalidImageReferenceException, + VerificationException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/multitag-image:maven" + + System.nanoTime(); Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + "", + buildAndRunAdditionalTag( + emptyTestProject.getProjectRoot(), targetImage, "maven-2" + System.nanoTime())); } @Test diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index b91a48c8ee..37b971bb13 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -30,6 +30,7 @@ ${_TARGET_IMAGE} + ${_ADDITIONAL_TAG} From be6c283eb0947fffbb42c639062b62788c47300a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 24 Sep 2018 13:09:24 -0700 Subject: [PATCH 0227/2020] Parallelizes pushing to multiple tags. (#1028) --- .../jib/builder/steps/PushImageStep.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 5b0166c157..5a3fb7ea49 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -23,12 +23,10 @@ import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -96,10 +94,11 @@ public Void call() throws ExecutionException, InterruptedException { return Futures.whenAllSucceed(dependenciesBuilder.build()) .call(this::afterPushSteps, listeningExecutorService) .get() + .get() .get(); } - private ListenableFuture afterPushSteps() throws ExecutionException { + private ListenableFuture> afterPushSteps() throws ExecutionException { List> dependencies = new ArrayList<>(); for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); @@ -114,7 +113,7 @@ private ListenableFuture afterPushSteps() throws ExecutionException { .call(this::afterAllPushed, listeningExecutorService); } - private Void afterAllPushed() throws IOException, RegistryException, ExecutionException { + private ListenableFuture afterAllPushed() throws ExecutionException { try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { RegistryClient registryClient = buildConfiguration @@ -134,13 +133,17 @@ private Void afterAllPushed() throws IOException, RegistryException, ExecutionEx NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)))); // Pushes to all target image tags. - // TODO: Parallelize. + List> pushAllTagsFutures = new ArrayList<>(); for (String tag : buildConfiguration.getAllTargetImageTags()) { - buildConfiguration.getBuildLogger().info("Tagging with " + tag + "..."); - registryClient.pushManifest(manifestTemplate, tag); + pushAllTagsFutures.add( + listeningExecutorService.submit( + () -> { + buildConfiguration.getBuildLogger().info("Tagging with " + tag + "..."); + registryClient.pushManifest(manifestTemplate, tag); + return null; + })); } + return Futures.whenAllSucceed(pushAllTagsFutures).call(() -> null, listeningExecutorService); } - - return null; } } From 71f2e44c4adf25658abd9dbe62f1dc1fb72c9df3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 25 Sep 2018 08:08:35 -0700 Subject: [PATCH 0228/2020] Replaces JibLogger with EventEmitter in RetrieveRegistryCredentialsStep. (#971) --- .../google/cloud/tools/jib/builder/Timer.java | 69 ++++++ .../tools/jib/builder/TimerEventEmitter.java | 80 +++++++ .../RetrieveRegistryCredentialsStep.java | 33 +-- .../jib/configuration/BuildConfiguration.java | 28 ++- .../tools/jib/event/DefaultEventEmitter.java | 46 ++++ .../cloud/tools/jib/event/EventEmitter.java | 28 +-- .../cloud/tools/jib/event/JibEventType.java | 11 + .../tools/jib/event/events/LogEvent.java | 92 ++++++++ .../tools/jib/event/events/TimerEvent.java | 133 +++++++++++ .../jib/builder/TimerEventEmitterTest.java | 211 ++++++++++++++++++ .../cloud/tools/jib/builder/TimerTest.java | 56 +++++ .../RetrieveRegistryCredentialsStepTest.java | 29 ++- ...Test.java => DefaultEventEmitterTest.java} | 6 +- .../tools/jib/event/events/LogEventTest.java | 64 ++++++ .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 2 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../tools/jib/gradle/DockerContextTask.java | 2 +- .../tools/jib/gradle/GradleJibLogger.java | 1 + .../jib/gradle/GradleLayerConfigurations.java | 16 +- .../jib/gradle/GradleProjectProperties.java | 27 ++- .../tools/jib/gradle/LogEventHandler.java | 59 +++++ .../gradle/PluginConfigurationProcessor.java | 1 + .../gradle/GradleLayerConfigurationsTest.java | 18 +- .../gradle/GradleProjectPropertiesTest.java | 5 +- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 3 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../tools/jib/maven/DockerContextMojo.java | 3 +- .../tools/jib/maven/LogEventHandler.java | 60 +++++ .../jib/maven/MavenProjectProperties.java | 30 ++- .../maven/PluginConfigurationProcessor.java | 1 + .../jib/maven/MavenProjectPropertiesTest.java | 4 +- .../jib/plugins/common/BuildStepsRunner.java | 4 + .../jib/plugins/common/ProjectProperties.java | 4 + .../jib/plugins/common/TimerEventHandler.java | 81 +++++++ .../plugins/common/TimerEventHandlerTest.java | 73 ++++++ 37 files changed, 1204 insertions(+), 86 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java rename jib-core/src/test/java/com/google/cloud/tools/jib/event/{EventEmitterTest.java => DefaultEventEmitterTest.java} (92%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandler.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java new file mode 100644 index 0000000000..85be3e13c0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.events.TimerEvent; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Times code execution intervals. Call {@link #lap} at the end of each interval. */ +// TODO: Replace com.google.cloud.tools.jib.Timer with this. +class Timer implements TimerEvent.Timer { + + private final Clock clock; + @Nullable private final Timer parentTimer; + + private final Instant startTime; + private Instant lapStartTime; + + Timer(Clock clock, @Nullable Timer parentTimer) { + this.clock = clock; + this.parentTimer = parentTimer; + + startTime = clock.instant(); + lapStartTime = startTime; + } + + @Override + public Optional getParent() { + return Optional.ofNullable(parentTimer); + } + + /** + * Captures the time since last lap or creation, and resets the start time. + * + * @return the duration of the last lap, or since creation + */ + Duration lap() { + Instant now = clock.instant(); + Duration duration = Duration.between(lapStartTime, now); + lapStartTime = now; + return duration; + } + + /** + * Gets the total elapsed time since creation. + * + * @return the total elapsed time + */ + Duration getElapsedTime() { + return Duration.between(startTime, clock.instant()); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java new file mode 100644 index 0000000000..756b27478f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import com.google.common.annotations.VisibleForTesting; +import java.io.Closeable; +import java.time.Clock; +import java.time.Duration; +import javax.annotation.Nullable; + +/** Handles {@link Timer}s to emit {@link TimerEvent}s. */ +public class TimerEventEmitter implements Closeable { + + private static final Clock DEFAULT_CLOCK = Clock.systemUTC(); + + private final EventEmitter eventEmitter; + private final String description; + + private final Clock clock; + private final Timer timer; + + public TimerEventEmitter(EventEmitter eventEmitter, String description) { + this(eventEmitter, description, DEFAULT_CLOCK, null); + } + + @VisibleForTesting + TimerEventEmitter( + EventEmitter eventEmitter, String description, Clock clock, @Nullable Timer parentTimer) { + this.eventEmitter = eventEmitter; + this.description = description; + this.clock = clock; + this.timer = new Timer(clock, parentTimer); + + emitTimerEvent(State.START, Duration.ZERO); + } + + /** + * Creates a new {@link TimerEventEmitter} with its parent timer as this. + * + * @param description a new description + * @return the new {@link TimerEventEmitter} + */ + public TimerEventEmitter subTimer(String description) { + return new TimerEventEmitter(eventEmitter, description, clock, timer); + } + + /** + * Captures the time since last lap or creation and emits an {@link State#LAP} {@link TimerEvent}. + */ + public void lap() { + emitTimerEvent(State.LAP, timer.lap()); + } + + /** Laps and emits an {@link State#FINISHED} {@link TimerEvent} upon close. */ + @Override + public void close() { + emitTimerEvent(State.FINISHED, timer.lap()); + } + + private void emitTimerEvent(State state, Duration duration) { + eventEmitter.emit(new TimerEvent(state, timer, duration, timer.getElapsedTime(), description)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 3d06844201..acbd40015b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -16,12 +16,13 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -34,14 +35,16 @@ /** Attempts to retrieve registry credentials. */ class RetrieveRegistryCredentialsStep implements AsyncStep, Callable { - private static final String DESCRIPTION = "Retrieving registry credentials for %s"; + private static String makeDescription(String registry) { + return "Retrieving registry credentials for " + registry; + } /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getBuildLogger(), + buildConfiguration.getEventEmitter(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } @@ -51,12 +54,12 @@ static RetrieveRegistryCredentialsStep forTargetImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getBuildLogger(), + buildConfiguration.getEventEmitter(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } - private final JibLogger buildLogger; + private final EventEmitter eventEmitter; private final String registry; private final ImmutableList credentialRetrievers; @@ -65,10 +68,10 @@ static RetrieveRegistryCredentialsStep forTargetImage( @VisibleForTesting RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, - JibLogger buildLogger, + EventEmitter eventEmitter, String registry, ImmutableList credentialRetrievers) { - this.buildLogger = buildLogger; + this.eventEmitter = eventEmitter; this.registry = registry; this.credentialRetrievers = credentialRetrievers; @@ -83,9 +86,10 @@ public ListenableFuture getFuture() { @Override @Nullable public Credential call() throws CredentialRetrievalException { - buildLogger.lifecycle(String.format(DESCRIPTION, registry) + "..."); + String description = makeDescription(registry); + eventEmitter.emit(LogEvent.lifecycle(description + "...")); - try (Timer ignored = new Timer(buildLogger, String.format(DESCRIPTION, registry))) { + try (TimerEventEmitter ignored = new TimerEventEmitter(eventEmitter, description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { @@ -93,11 +97,10 @@ public Credential call() throws CredentialRetrievalException { } } - /* - * If no credentials found, give an info (not warning because in most cases, the base image is - * public and does not need extra credentials) and return null. - */ - buildLogger.info("No credentials could be retrieved for registry " + registry); + // If no credentials found, give an info (not warning because in most cases, the base image is + // public and does not need extra credentials) and return null. + eventEmitter.emit( + LogEvent.info("No credentials could be retrieved for registry " + registry)); return null; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 259112f8b3..79919f71e5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -51,6 +52,10 @@ public static class Builder { private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; private String toolName = DEFAULT_TOOL_NAME; + private EventEmitter eventEmitter = + jibEvent -> { + /* No-op EventEmitter. */ + }; private JibLogger buildLogger; @@ -171,6 +176,17 @@ public Builder setToolName(String toolName) { return this; } + /** + * Sets the {@link EventEmitter} to emit events with. + * + * @param eventEmitter the {@link EventEmitter} + * @return this + */ + public Builder setEventEmitter(EventEmitter eventEmitter) { + this.eventEmitter = eventEmitter; + return this; + } + /** * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. * @@ -209,7 +225,8 @@ public BuildConfiguration build() { targetFormat, allowInsecureRegistries, layerConfigurations, - toolName); + toolName, + eventEmitter); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -245,6 +262,7 @@ public static Builder builder(JibLogger jibLogger) { private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; private final String toolName; + private final EventEmitter eventEmitter; /** Instantiate with {@link #builder}. */ private BuildConfiguration( @@ -258,7 +276,8 @@ private BuildConfiguration( Class targetFormat, boolean allowInsecureRegistries, ImmutableList layerConfigurations, - String toolName) { + String toolName, + EventEmitter eventEmitter) { this.buildLogger = buildLogger; this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; @@ -270,6 +289,7 @@ private BuildConfiguration( this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; this.toolName = toolName; + this.eventEmitter = eventEmitter; } public JibLogger getBuildLogger() { @@ -305,6 +325,10 @@ public String getToolName() { return toolName; } + public EventEmitter getEventEmitter() { + return eventEmitter; + } + /** * Gets the location of the cache for storing application layers. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java new file mode 100644 index 0000000000..1faa1d8f3b --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event; + +import com.google.common.collect.ImmutableMultimap; + +/** Emits {@link JibEvent}s to event handlers. */ +public class DefaultEventEmitter implements EventEmitter { + + /** Maps from {@link JibEvent} class to handlers for that event type. */ + private final ImmutableMultimap, Handler> handlers; + + /** + * Creates an instance from {@link Handler}s in an {@link EventHandlers}. + * + * @param eventHandlers the {@link EventHandlers} to get the {@link Handler}s from + */ + public DefaultEventEmitter(EventHandlers eventHandlers) { + handlers = eventHandlers.getHandlers(); + } + + /** + * Emits {@code jibEvent} to all the handlers that can handle it. + * + * @param jibEvent the {@link JibEvent} to emit + */ + @Override + public void emit(JibEvent jibEvent) { + handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); + handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java index 1b669133cb..c3b068fa6c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java @@ -16,30 +16,8 @@ package com.google.cloud.tools.jib.event; -import com.google.common.collect.ImmutableMultimap; +/** Emits {@link JibEvent}s. */ +public interface EventEmitter { -/** Emits {@link JibEvent}s to event handlers. */ -public class EventEmitter { - - /** Maps from {@link JibEvent} class to handlers for that event type. */ - private final ImmutableMultimap, Handler> handlers; - - /** - * Creates an instance from {@link Handler}s in an {@link EventHandlers}. - * - * @param eventHandlers the {@link EventHandlers} to get the {@link Handler}s from - */ - public EventEmitter(EventHandlers eventHandlers) { - handlers = eventHandlers.getHandlers(); - } - - /** - * Emits {@code jibEvent} to all the handlers that can handle it. - * - * @param jibEvent the {@link JibEvent} to emit - */ - public void emit(JibEvent jibEvent) { - handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); - handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); - } + void emit(JibEvent jibEvent); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java index 7056cc2ca9..cae441e903 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.event; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.common.annotations.VisibleForTesting; /** Holds references to all {@link JibEvent} types. */ @@ -24,6 +26,15 @@ public class JibEventType { /** All event types. Handlers for this will always be called first. */ public static final JibEventType ALL = new JibEventType<>(JibEvent.class); + /** + * Event with a message to use for logging purposes. Do not attempt to parse for information as + * the format can change. + */ + public static final JibEventType LOGGING = new JibEventType<>(LogEvent.class); + + /** Timer event for timing various part of Jib's execution. */ + public static final JibEventType TIMING = new JibEventType<>(TimerEvent.class); + // TODO: Add entries for all JibEvent types. private final Class eventClass; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java new file mode 100644 index 0000000000..fede8e7d7c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.JibEvent; + +/** Log message event. */ +public class LogEvent implements JibEvent { + + /** Log levels, in order of verbosity. */ + public enum Level { + + /** Something went wrong. */ + ERROR, + + /** Something might not work as intended. */ + WARN, + + /** Default. */ + LIFECYCLE, + + /** + * Details that can be ignored. + * + *

Use {@link #LIFECYCLE} for progress-indicating messages. + */ + INFO, + + /** Useful for debugging. */ + DEBUG + } + + public static LogEvent error(String message) { + return new LogEvent(Level.ERROR, message); + } + + public static LogEvent lifecycle(String message) { + return new LogEvent(Level.LIFECYCLE, message); + } + + public static LogEvent warn(String message) { + return new LogEvent(Level.WARN, message); + } + + public static LogEvent info(String message) { + return new LogEvent(Level.INFO, message); + } + + public static LogEvent debug(String message) { + return new LogEvent(Level.DEBUG, message); + } + + private final Level level; + private final String message; + + private LogEvent(Level level, String message) { + this.level = level; + this.message = message; + } + + /** + * Gets the log level to log at. + * + * @return the log level + */ + public Level getLevel() { + return level; + } + + /** + * Gets the log message. + * + * @return the log message + */ + public String getMessage() { + return message; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java new file mode 100644 index 0000000000..9f687e4c0f --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.JibEvent; +import java.time.Duration; +import java.util.Optional; + +/** + * Timer event for timing various part of Jib's execution. + * + *

Timer events follow a specific {@link Timer} through a {@link State#START}, {@link State#LAP}, + * and {@link State#FINISHED} states. The duration indicates the duration since the last {@link + * TimerEvent} emitted for the {@link Timer}. + * + *

Timers can also define a hierarchy. + */ +public class TimerEvent implements JibEvent { + + /** The state of the timing. */ + public enum State { + + /** The timer has started timing. {@link #getDuration} is 0. {@link #getElapsed} is 0. */ + START, + + /** + * The timer timed a lap. {@link #getDuration} is the time since the last event. {@link + * #getElapsed} is the total elapsed time. + */ + LAP, + + /** + * The timer has finished timing. {@link #getDuration} is the time since the last event. {@link + * #getElapsed} is the total elapsed time. + */ + FINISHED + } + + /** Defines a timer hierarchy. */ + public interface Timer { + + /** + * Gets the parent of this {@link Timer}. + * + * @return the parent of this {@link Timer} + */ + Optional getParent(); + } + + private final State state; + private final Timer timer; + private final Duration duration; + private final Duration elapsed; + private final String description; + + /** + * Creates a new {@link TimerEvent}. For internal use only. + * + * @param state the state of the {@link Timer} + * @param timer the {@link Timer} + * @param duration the lap duration + * @param elapsed the total elapsed time since the timer was created + * @param description the description of this event + */ + public TimerEvent( + State state, Timer timer, Duration duration, Duration elapsed, String description) { + this.state = state; + this.timer = timer; + this.duration = duration; + this.elapsed = elapsed; + this.description = description; + } + + /** + * Gets the state of the timer. + * + * @return the state of the timer + * @see State + */ + public State getState() { + return state; + } + + /** + * Gets the timer this event is for. + * + * @return the timer + */ + public Timer getTimer() { + return timer; + } + + /** + * Gets the duration since the last {@link TimerEvent} for this timer. + * + * @return the duration since the last {@link TimerEvent} for this timer. + */ + public Duration getDuration() { + return duration; + } + + /** + * Gets the total elapsed duration since this timer was created. + * + * @return the duration since this timer was created + */ + public Duration getElapsed() { + return elapsed; + } + + /** + * Gets the description associated with this event. + * + * @return the description + */ + public String getDescription() { + return description; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java new file mode 100644 index 0000000000..d27c3f3e29 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.DefaultEventEmitter; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Deque; +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link TimerEventEmitter}. */ +@RunWith(MockitoJUnitRunner.class) +public class TimerEventEmitterTest { + + private final Deque timerEventQueue = new ArrayDeque<>(); + + @Mock private Clock mockClock; + + @Test + public void testLogging() { + EventEmitter eventEmitter = + new DefaultEventEmitter(new EventHandlers().add(JibEventType.TIMING, timerEventQueue::add)); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); + try (TimerEventEmitter parentTimerEventEmitter = + new TimerEventEmitter(eventEmitter, "description", mockClock, null)) { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1)); + parentTimerEventEmitter.lap(); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1).plusNanos(1)); + try (TimerEventEmitter ignored = parentTimerEventEmitter.subTimer("child description")) { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(2)); + // Laps on close. + } + } + + TimerEvent timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStartState(timerEvent); + verifyDescription(timerEvent, "description"); + + TimerEvent.Timer parentTimer = timerEvent.getTimer(); + + timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStateFirstLap(timerEvent, State.LAP); + verifyDescription(timerEvent, "description"); + + timerEvent = getNextTimerEvent(); + verifyParent(timerEvent, parentTimer); + verifyStartState(timerEvent); + verifyDescription(timerEvent, "child description"); + + timerEvent = getNextTimerEvent(); + verifyParent(timerEvent, parentTimer); + verifyStateFirstLap(timerEvent, State.FINISHED); + verifyDescription(timerEvent, "child description"); + + timerEvent = getNextTimerEvent(); + verifyNoParent(timerEvent); + verifyStateNotFirstLap(timerEvent, State.FINISHED); + verifyDescription(timerEvent, "description"); + + Assert.assertTrue(timerEventQueue.isEmpty()); + } + + /** + * Verifies the next {@link TimerEvent} on the {@link #timerEventQueue}. + * + * @param expectedParentTimer the expected parent {@link TimerEvent.Timer}, or {@code null} if + * none expected + * @param expectedState the expected {@link TimerEvent.State} + * @param expectedDescription the expected description + * @param hasLapped {@code true} if the timer has already lapped; {@code false} if it has not + * @return the verified {@link TimerEvent} + */ + private TimerEvent.Timer verifyNextTimerEvent( + @Nullable TimerEvent.Timer expectedParentTimer, + State expectedState, + String expectedDescription, + boolean hasLapped) { + TimerEvent timerEvent = timerEventQueue.poll(); + Assert.assertNotNull(timerEvent); + + if (expectedParentTimer == null) { + Assert.assertFalse(timerEvent.getTimer().getParent().isPresent()); + } else { + Assert.assertTrue(timerEvent.getTimer().getParent().isPresent()); + Assert.assertSame(expectedParentTimer, timerEvent.getTimer().getParent().get()); + } + Assert.assertEquals(expectedState, timerEvent.getState()); + if (expectedState == State.START) { + Assert.assertEquals(Duration.ZERO, timerEvent.getDuration()); + Assert.assertEquals(Duration.ZERO, timerEvent.getElapsed()); + } else { + Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); + if (hasLapped) { + Assert.assertTrue(timerEvent.getElapsed().compareTo(timerEvent.getDuration()) > 0); + } else { + Assert.assertEquals(0, timerEvent.getElapsed().compareTo(timerEvent.getDuration())); + } + } + Assert.assertEquals(expectedDescription, timerEvent.getDescription()); + + return timerEvent.getTimer(); + } + + /** + * Verifies that the {@code timerEvent}'s timer has no parent. + * + * @param timerEvent the {@link TimerEvent} to verify + */ + private void verifyNoParent(TimerEvent timerEvent) { + Assert.assertFalse(timerEvent.getTimer().getParent().isPresent()); + } + + /** + * Verifies that the {@code timerEvent}'s timer has parent {@code expectedParentTimer}. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedParentTimer the expected parent timer + */ + private void verifyParent(TimerEvent timerEvent, TimerEvent.Timer expectedParentTimer) { + Assert.assertTrue(timerEvent.getTimer().getParent().isPresent()); + Assert.assertSame(expectedParentTimer, timerEvent.getTimer().getParent().get()); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@link State#START}. + * + * @param timerEvent the {@link TimerEvent} to verify + */ + private void verifyStartState(TimerEvent timerEvent) { + Assert.assertEquals(State.START, timerEvent.getState()); + Assert.assertEquals(Duration.ZERO, timerEvent.getDuration()); + Assert.assertEquals(Duration.ZERO, timerEvent.getElapsed()); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@code expectedState} and that this is the + * first lap for the timer. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedState the expected {@link State} + */ + private void verifyStateFirstLap(TimerEvent timerEvent, State expectedState) { + Assert.assertEquals(expectedState, timerEvent.getState()); + Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); + Assert.assertEquals(0, timerEvent.getElapsed().compareTo(timerEvent.getDuration())); + } + + /** + * Verifies that the {@code timerEvent}'s state is {@code expectedState} and that this is not the + * first lap for the timer. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedState the expected {@link State} + */ + private void verifyStateNotFirstLap(TimerEvent timerEvent, State expectedState) { + Assert.assertEquals(expectedState, timerEvent.getState()); + Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); + Assert.assertTrue(timerEvent.getElapsed().compareTo(timerEvent.getDuration()) > 0); + } + + /** + * Verifies that the {@code timerEvent}'s description is {@code expectedDescription}. + * + * @param timerEvent the {@link TimerEvent} to verify + * @param expectedDescription the expected description + */ + private void verifyDescription(TimerEvent timerEvent, String expectedDescription) { + Assert.assertEquals(expectedDescription, timerEvent.getDescription()); + } + + /** + * Gets the next {@link TimerEvent} on the {@link #timerEventQueue}. + * + * @return the next {@link TimerEvent} + */ + private TimerEvent getNextTimerEvent() { + TimerEvent timerEvent = timerEventQueue.poll(); + Assert.assertNotNull(timerEvent); + return timerEvent; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java new file mode 100644 index 0000000000..46afc78da8 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link Timer}. */ +@RunWith(MockitoJUnitRunner.class) +public class TimerTest { + + @Mock private Clock mockClock; + + @Test + public void testLap() { + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); + Timer parentTimer = new Timer(mockClock, null); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(5)); + Duration parentDuration1 = parentTimer.lap(); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(15)); + Duration parentDuration2 = parentTimer.lap(); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16)); + Timer childTimer = new Timer(mockClock, parentTimer); + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16).plusNanos(1)); + Duration childDuration = childTimer.lap(); + + Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(16).plusNanos(2)); + Duration parentDuration3 = parentTimer.lap(); + + Assert.assertTrue(parentDuration2.compareTo(parentDuration1) > 0); + Assert.assertTrue(parentDuration1.compareTo(parentDuration3) > 0); + Assert.assertTrue(parentDuration3.compareTo(childDuration) > 0); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 4b31cdc0f4..ed1c0c3464 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -21,6 +21,10 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.event.DefaultEventEmitter; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; @@ -28,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +44,17 @@ @RunWith(MockitoJUnitRunner.class) public class RetrieveRegistryCredentialsStepTest { + private final StringBuilder logMessages = new StringBuilder(); + + // Note that in actual code, the event handler should NOT perform thread unsafe operations like + // here. + private final EventEmitter eventEmitter = + new DefaultEventEmitter( + new EventHandlers() + .add(JibEventType.LOGGING, logEvent -> logMessages.append(logEvent.getMessage()))); + @Mock private ListeningExecutorService mockListeningExecutorService; + // TODO: Remove once JibLogger is all replaced by EventEmitter. @Mock private JibLogger mockJibLogger; @Test @@ -74,14 +89,19 @@ public void testCall_none() throws CredentialRetrievalException { RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration) .call()); - Mockito.verify(mockJibLogger) - .info("No credentials could be retrieved for registry baseregistry"); + + Assert.assertThat( + logMessages.toString(), + CoreMatchers.containsString("No credentials could be retrieved for registry baseregistry")); + Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration) .call()); - Mockito.verify(mockJibLogger) - .info("No credentials could be retrieved for registry targetregistry"); + + Assert.assertThat( + logMessages.toString(), + CoreMatchers.containsString("No credentials could be retrieved for registry baseregistry")); } @Test @@ -111,6 +131,7 @@ private BuildConfiguration makeFakeBuildConfiguration( ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder(mockJibLogger) + .setEventEmitter(eventEmitter) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(baseCredentialRetrievers) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java similarity index 92% rename from jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java index a4ac7b63bf..578689fbc2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventEmitterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java @@ -22,8 +22,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link EventEmitter}. */ -public class EventEmitterTest { +/** Tests for {@link DefaultEventEmitter}. */ +public class DefaultEventEmitterTest { /** Test {@link JibEvent}. */ private static class TestJibEvent1 implements JibEvent {} @@ -51,7 +51,7 @@ public void testEmit() { TestJibEvent1 testJibEvent1 = new TestJibEvent1(); TestJibEvent2 testJibEvent2 = new TestJibEvent2(); - EventEmitter eventEmitter = new EventEmitter(eventHandlers); + EventEmitter eventEmitter = new DefaultEventEmitter(eventHandlers); eventEmitter.emit(testJibEvent1); eventEmitter.emit(testJibEvent2); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java new file mode 100644 index 0000000000..72b951e672 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.DefaultEventEmitter; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import java.util.ArrayDeque; +import java.util.Deque; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link LogEvent}. */ +public class LogEventTest { + + private final Deque receivedLogEvents = new ArrayDeque<>(); + + // Note that in actual code, the event handler should NOT perform thread unsafe operations like + // here. + private final EventEmitter eventEmitter = + new DefaultEventEmitter( + new EventHandlers().add(JibEventType.LOGGING, receivedLogEvents::offer)); + + @Test + public void testFactories() { + eventEmitter.emit(LogEvent.error("error")); + eventEmitter.emit(LogEvent.lifecycle("lifecycle")); + eventEmitter.emit(LogEvent.warn("warn")); + eventEmitter.emit(LogEvent.info("info")); + eventEmitter.emit(LogEvent.debug("debug")); + + verifyNextLogEvent(Level.ERROR, "error"); + verifyNextLogEvent(Level.LIFECYCLE, "lifecycle"); + verifyNextLogEvent(Level.WARN, "warn"); + verifyNextLogEvent(Level.INFO, "info"); + verifyNextLogEvent(Level.DEBUG, "debug"); + Assert.assertTrue(receivedLogEvents.isEmpty()); + } + + private void verifyNextLogEvent(Level level, String message) { + Assert.assertFalse(receivedLogEvents.isEmpty()); + + LogEvent logEvent = receivedLogEvents.poll(); + + Assert.assertEquals(level, logEvent.getLevel()); + Assert.assertEquals(message, logEvent.getMessage()); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index ea562eaa9c..2507479740 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -77,7 +77,7 @@ public void buildDocker() throws InvalidImageReferenceException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 49f1968497..36e37ff236 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -76,7 +76,7 @@ public void buildImage() throws InvalidImageReferenceException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index a343084757..a4b478be1a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -104,7 +104,7 @@ public void buildTar() throws InvalidImageReferenceException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 16c5ace1b8..e922d3774b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -110,7 +110,7 @@ public void generateDockerContext() { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), gradleJibLogger, jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); String targetDir = getTargetDir(); List entrypoint = jibExtension.getContainer().getEntrypoint(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java index be4e166909..20e6055c8a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java @@ -30,6 +30,7 @@ /** Implementation of {@link JibLogger} for Gradle plugins. */ // We don't care about the return values of the logging futures. @SuppressWarnings("FutureReturnValueIgnored") +// TODO: Replace use with LogEventHandler. class GradleJibLogger implements JibLogger { /** This executor keeps all log messages in order. */ diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 45fba4442b..6da4247f33 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -30,6 +30,7 @@ import java.util.stream.Stream; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; @@ -43,17 +44,14 @@ class GradleLayerConfigurations { * Resolves the source files configuration for a Gradle {@link Project}. * * @param project the Gradle {@link Project} - * @param gradleJibLogger the build logger for providing feedback about the resolution + * @param logger the logger for providing feedback about the resolution * @param extraDirectory path to the directory for the extra files layer * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ static JavaLayerConfigurations getForProject( - Project project, - GradleJibLogger gradleJibLogger, - Path extraDirectory, - AbsoluteUnixPath appRoot) + Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); @@ -68,19 +66,19 @@ static JavaLayerConfigurations getForProject( // Adds each file in each classes output directory to the classes files list. FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); - gradleJibLogger.info("Adding corresponding output directories of source sets to image"); + logger.info("Adding corresponding output directories of source sets to image"); for (File classesOutputDirectory : classesOutputDirectories) { if (Files.notExists(classesOutputDirectory.toPath())) { - gradleJibLogger.info("\t'" + classesOutputDirectory + "' (not found, skipped)"); + logger.info("\t'" + classesOutputDirectory + "' (not found, skipped)"); continue; } - gradleJibLogger.info("\t'" + classesOutputDirectory + "'"); + logger.info("\t'" + classesOutputDirectory + "'"); try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { classFileStream.forEach(classesFiles::add); } } if (classesFiles.isEmpty()) { - gradleJibLogger.warn("No classes files were found - did you compile your project?"); + logger.warn("No classes files were found - did you compile your project?"); } // Adds each file in the resources output directory to the resources files list. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 78f4f56de0..43ab239dd5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -17,11 +17,16 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.DefaultEventEmitter; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; @@ -35,6 +40,7 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; @@ -53,11 +59,13 @@ class GradleProjectProperties implements ProjectProperties { /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, GradleJibLogger logger, Path extraDirectory, AbsoluteUnixPath appRoot) { + Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) { try { return new GradleProjectProperties( project, - logger, + makeEventEmitter(logger), + // TODO: Remove + new GradleJibLogger(logger), GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot)); } catch (IOException ex) { @@ -65,6 +73,13 @@ static GradleProjectProperties getForProject( } } + private static EventEmitter makeEventEmitter(Logger logger) { + return new DefaultEventEmitter( + new EventHandlers() + .add(JibEventType.LOGGING, new LogEventHandler(logger)) + .add(JibEventType.TIMING, new TimerEventHandler(logger::debug))); + } + @Nullable static War getWarTask(Project project) { WarPluginConvention warPluginConvention = @@ -76,15 +91,18 @@ static War getWarTask(Project project) { } private final Project project; + private final EventEmitter eventEmitter; private final GradleJibLogger gradleJibLogger; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( Project project, + EventEmitter eventEmitter, GradleJibLogger gradleJibLogger, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; + this.eventEmitter = eventEmitter; this.gradleJibLogger = gradleJibLogger; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -94,6 +112,11 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } + @Override + public EventEmitter getEventEmitter() { + return eventEmitter; + } + @Override public JibLogger getLogger() { return gradleJibLogger; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java new file mode 100644 index 0000000000..e3be7eb72a --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.event.events.LogEvent; +import java.util.function.Consumer; +import org.gradle.api.logging.Logger; + +/** Handles {@link LogEvent}s by passing to the Gradle {@link Logger}. */ +class LogEventHandler implements Consumer { + + private final Logger logger; + + LogEventHandler(Logger logger) { + this.logger = logger; + } + + @Override + public void accept(LogEvent logEvent) { + switch (logEvent.getLevel()) { + case LIFECYCLE: + logger.lifecycle(logEvent.getMessage()); + break; + + case DEBUG: + logger.debug(logEvent.getMessage()); + break; + + case ERROR: + logger.error(logEvent.getMessage()); + break; + + case INFO: + logger.info(logEvent.getMessage()); + break; + + case WARN: + logger.warn("warning: " + logEvent.getMessage()); + break; + + default: + throw new IllegalStateException("Unknown LogEvent.Level: " + logEvent.getLevel()); + } + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 2d83b14dcd..a1b6159513 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -125,6 +125,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) .setToolName(GradleProjectProperties.TOOL_NAME) + .setEventEmitter(projectProperties.getEventEmitter()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 89739b095c..bd97b0fc77 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -36,6 +36,7 @@ import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; @@ -92,7 +93,7 @@ private static List getExtractionPathFromLayerEntries(List l @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private GradleJibLogger mockGradleJibLogger; + @Mock private Logger mockLogger; @Before public void setUp() throws URISyntaxException { @@ -154,7 +155,7 @@ public void test_correctFiles() throws URISyntaxException, IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), appRoot); + mockProject, mockLogger, Paths.get("nonexistent/path"), appRoot); Assert.assertEquals( expectedDependenciesFiles, getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); @@ -181,13 +182,12 @@ public void test_noClassesFiles() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, Paths.get("nonexistent/path"), appRoot); + mockProject, mockLogger, Paths.get("nonexistent/path"), appRoot); - Mockito.verify(mockGradleJibLogger) + Mockito.verify(mockLogger) .info("Adding corresponding output directories of source sets to image"); - Mockito.verify(mockGradleJibLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); - Mockito.verify(mockGradleJibLogger) - .warn("No classes files were found - did you compile your project?"); + Mockito.verify(mockLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); + Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } @Test @@ -196,7 +196,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, extraFilesDirectory, AbsoluteUnixPath.get("/app")); + mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/app")); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -218,7 +218,7 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxE JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( - mockProject, mockGradleJibLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); + mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); Assert.assertEquals( Arrays.asList( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index ef46626f4b..7666c1eca6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -48,6 +49,7 @@ public class GradleProjectPropertiesTest { @Mock private Convention mockConvention; @Mock private WarPluginConvention mockWarPluginConvection; @Mock private TaskContainer mockTaskContainer; + @Mock private EventEmitter mockEventEmitter; @Mock private GradleJibLogger mockGradleJibLogger; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @@ -66,7 +68,8 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties(mockProject, mockGradleJibLogger, mockJavaLayerConfigurations); + new GradleProjectProperties( + mockProject, mockEventEmitter, mockGradleJibLogger, mockJavaLayerConfigurations); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index c73ed2e0d3..9cf87d17f4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -61,8 +61,7 @@ public void execute() throws MojoExecutionException { MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), mavenJibLogger, getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index b1155d4fc1..7efcaf7c0e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -84,8 +84,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), mavenJibLogger, getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 8003168a31..362f074f19 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -58,8 +58,7 @@ public void execute() throws MojoExecutionException { MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), mavenJibLogger, getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index a130d8eab5..2ca26d1b17 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -73,8 +73,7 @@ public void execute() throws MojoExecutionException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), mavenJibLogger, getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); List entrypoint = getEntrypoint(); if (entrypoint.isEmpty()) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java new file mode 100644 index 0000000000..2c63939e23 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.event.events.LogEvent; +import java.util.function.Consumer; +import org.apache.maven.plugin.logging.Log; + +/** Handles {@link LogEvent}s by passing to the Maven {@link Log}. */ +class LogEventHandler implements Consumer { + + private final Log log; + + LogEventHandler(Log log) { + this.log = log; + } + + @Override + public void accept(LogEvent logEvent) { + switch (logEvent.getLevel()) { + case LIFECYCLE: + log.info(logEvent.getMessage()); + break; + + case DEBUG: + log.debug(logEvent.getMessage()); + break; + + case ERROR: + log.error(logEvent.getMessage()); + break; + + case INFO: + // Use lifecycle for progress-indicating messages. + log.debug(logEvent.getMessage()); + break; + + case WARN: + log.warn(logEvent.getMessage()); + break; + + default: + throw new IllegalStateException("Unknown LogEvent.Level: " + logEvent.getLevel()); + } + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index f51d8d75cb..58669f9b7c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -17,11 +17,16 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.DefaultEventEmitter; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -29,6 +34,7 @@ import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; @@ -49,20 +55,22 @@ public class MavenProjectProperties implements ProjectProperties { /** * @param project the {@link MavenProject} for the plugin. - * @param logger the logger used for printing status messages. + * @param log the Maven {@link Log} to log messages during Jib execution * @param extraDirectory path to the directory for the extra files layer * @param appRoot root directory in the image where the app will be placed * @return a MavenProjectProperties from the given project and logger. * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, MavenJibLogger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + MavenProject project, Log log, Path extraDirectory, AbsoluteUnixPath appRoot) throws MojoExecutionException { try { return new MavenProjectProperties( project, - logger, + makeEventEmitter(log), + new MavenJibLogger(log), MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); + } catch (IOException ex) { throw new MojoExecutionException( "Obtaining project build output files failed; make sure you have compiled your project " @@ -72,16 +80,27 @@ static MavenProjectProperties getForProject( } } + private static EventEmitter makeEventEmitter(Log log) { + return new DefaultEventEmitter( + new EventHandlers() + .add(JibEventType.LOGGING, new LogEventHandler(log)) + .add(JibEventType.TIMING, new TimerEventHandler(log::debug))); + } + private final MavenProject project; + private final EventEmitter eventEmitter; + // TODO: Remove private final MavenJibLogger mavenJibLogger; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, + EventEmitter eventEmitter, MavenJibLogger mavenJibLogger, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; + this.eventEmitter = eventEmitter; this.mavenJibLogger = mavenJibLogger; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -91,6 +110,11 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } + @Override + public EventEmitter getEventEmitter() { + return eventEmitter; + } + @Override public JibLogger getLogger() { return mavenJibLogger; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 920e4b3c12..d1f66487a0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -151,6 +151,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(logger) .setToolName(MavenProjectProperties.TOOL_NAME) + .setEventEmitter(projectProperties.getEventEmitter()) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index d98647c571..14ffb1aa86 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; @@ -33,6 +34,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; + @Mock private EventEmitter mockEventEmitter; @Mock private MavenJibLogger mockMavenJibLogger; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @@ -48,7 +50,7 @@ public class MavenProjectPropertiesTest { public void setup() { mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockMavenJibLogger, mockJavaLayerConfigurations); + mockMavenProject, mockEventEmitter, mockMavenJibLogger, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 9ccd05ac9e..ec67b1af61 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -209,6 +209,10 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut for (LayerConfiguration layerConfiguration : buildSteps.getBuildConfiguration().getLayerConfigurations()) { + if (layerConfiguration.getLayerEntries().isEmpty()) { + continue; + } + buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":"); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index c0aea242da..7d8e7b8f84 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.nio.file.Path; import javax.annotation.Nullable; @@ -27,6 +28,9 @@ public interface ProjectProperties { /** Directory name for the cache. The directory will be relative to the build output directory. */ String CACHE_DIRECTORY_NAME = "jib-cache"; + EventEmitter getEventEmitter(); + + // TODO: Remove JibLogger getLogger(); String getPluginName(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandler.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandler.java new file mode 100644 index 0000000000..a71d9baf7b --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import java.util.function.Consumer; + +/** Handles {@link TimerEvent}s by producing log messages. */ +public class TimerEventHandler implements Consumer { + + /** + * Gets a {@link StringBuilder} prepended with tabulation based on the number of parents {@code + * timer} has. + * + * @param timer the {@link TimerEvent.Timer} to base the tabulation on + * @return a new {@link StringBuilder} + */ + private static StringBuilder getTabs(TimerEvent.Timer timer) { + StringBuilder tabs = new StringBuilder(); + while (timer.getParent().isPresent()) { + tabs.append("\t"); + timer = timer.getParent().get(); + } + return tabs; + } + + /** + * Builds the log message for a {@link TimerEvent}, based on its state and duration. + * + * @param timerEvent the {@link TimerEvent} + * @return the built log message + */ + private static String buildLogMessage(TimerEvent timerEvent) { + StringBuilder logMessageBuilder = getTabs(timerEvent.getTimer()); + + if (timerEvent.getState() == State.START) { + return logMessageBuilder.append("TIMING\t").append(timerEvent.getDescription()).toString(); + + } else { + return logMessageBuilder + .append("TIMED\t") + .append(timerEvent.getDescription()) + .append(" : ") + .append(timerEvent.getDuration().toNanos() / 1000 / 1000.0) + .append(" ms") + .toString(); + } + } + + private final Consumer logMessageConsumer; + + /** + * Creates a new {@link TimerEventHandler} with a consumer for log messages generated by handling + * {@link TimerEvent}s. + * + * @param logMessageConsumer the log message consumer + */ + public TimerEventHandler(Consumer logMessageConsumer) { + this.logMessageConsumer = logMessageConsumer; + } + + @Override + public void accept(TimerEvent timerEvent) { + logMessageConsumer.accept(buildLogMessage(timerEvent)); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java new file mode 100644 index 0000000000..a242252214 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.event.events.TimerEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent.State; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link com.google.cloud.tools.jib.plugins.common.TimerEventHandler}. */ +public class TimerEventHandlerTest { + + private final Deque logMessageQueue = new ArrayDeque<>(); + + private final TimerEvent.Timer ROOT_TIMER = Optional::empty; + + @Test + public void testAccept() { + TimerEventHandler timerEventHandler = new TimerEventHandler(logMessageQueue::add); + + timerEventHandler.accept( + new TimerEvent(State.START, ROOT_TIMER, Duration.ZERO, Duration.ZERO, "description")); + timerEventHandler.accept( + new TimerEvent(State.LAP, ROOT_TIMER, Duration.ofMillis(10), Duration.ZERO, "description")); + timerEventHandler.accept( + new TimerEvent( + State.FINISHED, ROOT_TIMER, Duration.ofMillis(100), Duration.ZERO, "description")); + + timerEventHandler.accept( + new TimerEvent( + State.LAP, + () -> Optional.of(ROOT_TIMER), + Duration.ZERO, + Duration.ZERO, + "child description")); + + String rootStartMessage = logMessageQueue.poll(); + Assert.assertNotNull(rootStartMessage); + Assert.assertEquals("TIMING\tdescription", rootStartMessage); + + String rootInProgressMessage = logMessageQueue.poll(); + Assert.assertNotNull(rootInProgressMessage); + Assert.assertEquals("TIMED\tdescription : 10.0 ms", rootInProgressMessage); + + String rootFinishedMessage = logMessageQueue.poll(); + Assert.assertNotNull(rootFinishedMessage); + Assert.assertEquals("TIMED\tdescription : 100.0 ms", rootFinishedMessage); + + String childMessage = logMessageQueue.poll(); + Assert.assertNotNull(childMessage); + Assert.assertEquals("\tTIMED\tchild description : 0.0 ms", childMessage); + + Assert.assertTrue(logMessageQueue.isEmpty()); + } +} From 8b7b11a2fe9961fb1ae690e392eda812e7574666 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 25 Sep 2018 09:06:27 -0700 Subject: [PATCH 0229/2020] Finishes implementing new Cache. (#1022) --- .../google/cloud/tools/jib/ncache/Cache.java | 81 +++++- .../cloud/tools/jib/ncache/CacheStorage.java | 9 +- .../cloud/tools/jib/ncache/CacheWrite.java | 56 +++- .../tools/jib/ncache/DefaultCacheEntry.java | 22 +- .../tools/jib/ncache/DefaultCacheStorage.java | 23 +- .../jib/ncache/DefaultCacheStorageReader.java | 34 ++- .../tools/jib/ncache/DefaultCacheWrite.java | 76 ------ .../cloud/tools/jib/ncache/CacheTest.java | 247 ++++++++++++++++++ ...acheWriteTest.java => CacheWriteTest.java} | 8 +- .../ncache/DefaultCacheStorageReaderTest.java | 45 ++++ .../ncache/DefaultCacheStorageWriterTest.java | 5 +- 11 files changed, 475 insertions(+), 131 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java rename jib-core/src/test/java/com/google/cloud/tools/jib/ncache/{DefaultCacheWriteTest.java => CacheWriteTest.java} (89%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java index 1d7d58d180..2a1177aa29 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java @@ -16,33 +16,83 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; +import javax.annotation.concurrent.Immutable; -/** Cache for storing data to be shared between Jib executions. */ +/** + * Cache for storing data to be shared between Jib executions. + * + *

Uses the default cache storage engine ({@link DefaultCacheStorage}) and layer entries as the + * selector ({@link LayerEntriesSelector}). + * + *

This class is immutable and safe to use across threads. + */ +@Immutable public class Cache { + /** + * Initializes the cache using {@code cacheDirectory} for storage. + * + * @param cacheDirectory the directory for the cache. Creates the directory if it does not exist. + * @return a new {@link Cache} + * @throws IOException if an I/O exception occurs + */ + public static Cache withDirectory(Path cacheDirectory) throws IOException { + Files.createDirectories(cacheDirectory); + return new Cache(DefaultCacheStorage.withDirectory(cacheDirectory)); + } + private final CacheStorage cacheStorage; - // TODO: Add way to instantiate after #retrieveCacheEntry and #write are implemented. private Cache(CacheStorage cacheStorage) { this.cacheStorage = cacheStorage; } + /** + * Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList, + * Blob)} to include a selector and metadata. + * + * @param layerBlob the layer {@link Blob} + * @return the {@link CacheEntry} for the written layer + * @throws IOException if an I/O exception occurs + */ + public CacheEntry write(Blob layerBlob) throws IOException { + return cacheStorage.write(CacheWrite.layerOnly(layerBlob)); + } + + /** + * Saves a cache entry with a layer {@link Blob}, an additional selector digest, and a metadata + * {@link Blob}. Use {@link #write(Blob)} to save only a layer {@link Blob}. + * + * @param layerBlob the layer {@link Blob} + * @param layerEntries the layer entries that make up the layer + * @param metadataBlob the metadata {@link Blob} + * @return the {@link CacheEntry} for the written layer and metadata + * @throws IOException if an I/O exception occurs + */ + public CacheEntry write(Blob layerBlob, ImmutableList layerEntries, Blob metadataBlob) + throws IOException { + return cacheStorage.write( + CacheWrite.withSelectorAndMetadata( + layerBlob, LayerEntriesSelector.generateSelector(layerEntries), metadataBlob)); + } + /** * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. * * @param layerEntries the layer entries to match against - * @return a {@link CacheEntry} that was built from {@code layerEntries}, or {@link - * Optional#empty} if none found + * @return a {@link CacheEntry} that was built from {@code layerEntries}, if found * @throws IOException if an I/O exception occurs * @throws CacheCorruptedException if the cache is corrupted */ - // TODO: Add test once other unimplemented methods are implemented. - public Optional retrieveCacheEntry(ImmutableList layerEntries) + public Optional retrieve(ImmutableList layerEntries) throws IOException, CacheCorruptedException { Optional optionalSelectedLayerDigest = cacheStorage.select(LayerEntriesSelector.generateSelector(layerEntries)); @@ -53,13 +103,16 @@ public Optional retrieveCacheEntry(ImmutableList layerEn return cacheStorage.retrieve(optionalSelectedLayerDigest.get()); } - public Optional retrieveCacheEntry(DescriptorDigest layerDigest) { - // TODO: Implement - return Optional.empty(); - } - - public Optional write(CacheWrite cacheWrite) { - // TODO: Implement - return Optional.empty(); + /** + * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. + * + * @param layerDigest the layer digest + * @return the {@link CacheEntry} referenced by the layer digest, if found + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + public Optional retrieve(DescriptorDigest layerDigest) + throws IOException, CacheCorruptedException { + return cacheStorage.retrieve(layerDigest); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index c4e9a4d065..0262cd2df6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -33,7 +33,7 @@ * *

Implementations must be thread-safe and should be immutable. */ -public interface CacheStorage { +interface CacheStorage { /** * Saves the {@link CacheWrite}. @@ -57,7 +57,7 @@ public interface CacheStorage { * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheEntry} referenced by the layer digest + * @return the {@link CacheEntry} referenced by the layer digest, if found * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ @@ -68,8 +68,9 @@ Optional retrieve(DescriptorDigest layerDigest) * Queries for layer digests that can be selected with the {@code selector}. * * @param selector the selector to query with - * @return the layer digest selected, or {@link Optional#empty} if none found + * @return the layer digest selected, if found * @throws IOException if an I/O exception occurs */ - Optional select(DescriptorDigest selector) throws IOException; + Optional select(DescriptorDigest selector) + throws IOException, CacheCorruptedException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java index 4273c3458d..b735c28504 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java @@ -19,16 +19,58 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.util.Optional; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; -/** Represents layer data to write to the cache. Implementations must be immutable. */ -public interface CacheWrite { +/** + * Represents layer data to write to the cache. Implementation is immutable and thread-safe. + */ +@Immutable +class CacheWrite { + + /** + * Constructs a {@link CacheWrite} with only the layer {@link Blob}. + * + * @param layerBlob the layer {@link Blob} + * @return the new {@link CacheWrite} + */ + static CacheWrite layerOnly(Blob layerBlob) { + return new CacheWrite(layerBlob, null, null); + } + + /** + * Constructs a {@link CacheWrite} with a layer {@link Blob}, an additional selector digest, and a + * metadata {@link Blob}. + * + * @param layerBlob the layer {@link Blob} + * @param selector the selector digest + * @param metadataBlob the metadata {@link Blob} + * @return the new {@link CacheWrite} + */ + static CacheWrite withSelectorAndMetadata( + Blob layerBlob, DescriptorDigest selector, Blob metadataBlob) { + return new CacheWrite(layerBlob, selector, metadataBlob); + } + + private final Blob layerBlob; + @Nullable private final DescriptorDigest selector; + @Nullable private final Blob metadataBlob; + + private CacheWrite( + Blob layerBlob, @Nullable DescriptorDigest selector, @Nullable Blob metadataBlob) { + this.layerBlob = layerBlob; + this.selector = selector; + this.metadataBlob = metadataBlob; + } /** * Gets the {@link Blob} to write as the layer contents. * * @return the layer {@link Blob} */ - Blob getLayerBlob(); + Blob getLayerBlob() { + return layerBlob; + } /** * Gets the optional selector digest to also reference this layer data. A selector digest may be a @@ -39,7 +81,9 @@ public interface CacheWrite { * * @return the selector digest */ - Optional getSelector(); + Optional getSelector() { + return Optional.ofNullable(selector); + } /** * Gets the optional {@link Blob} to write as the arbitrary layer metadata. @@ -48,5 +92,7 @@ public interface CacheWrite { * * @return the metadata {@link Blob} */ - Optional getMetadataBlob(); + Optional getMetadataBlob() { + return Optional.ofNullable(metadataBlob); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java index d68d1add13..6853e465cd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java @@ -23,10 +23,10 @@ import javax.annotation.Nullable; /** Default implementation of {@link CacheEntry}. */ -public class DefaultCacheEntry implements CacheEntry { +class DefaultCacheEntry implements CacheEntry { /** Builds a {@link CacheEntry}. */ - public static class Builder { + static class Builder { @Nullable private DescriptorDigest layerDigest; @Nullable private DescriptorDigest layerDiffId; @@ -36,40 +36,40 @@ public static class Builder { private Builder() {} - public Builder setLayerDigest(DescriptorDigest layerDigest) { + Builder setLayerDigest(DescriptorDigest layerDigest) { this.layerDigest = layerDigest; return this; } - public Builder setLayerDiffId(DescriptorDigest layerDiffId) { + Builder setLayerDiffId(DescriptorDigest layerDiffId) { this.layerDiffId = layerDiffId; return this; } - public Builder setLayerSize(long layerSize) { + Builder setLayerSize(long layerSize) { this.layerSize = layerSize; return this; } - public Builder setLayerBlob(Blob layerBlob) { + Builder setLayerBlob(Blob layerBlob) { this.layerBlob = layerBlob; return this; } - public Builder setMetadataBlob(@Nullable Blob metadataBlob) { + Builder setMetadataBlob(@Nullable Blob metadataBlob) { this.metadataBlob = metadataBlob; return this; } - public boolean hasLayerBlob() { + boolean hasLayerBlob() { return layerBlob != null; } - public boolean hasMetadataBlob() { + boolean hasMetadataBlob() { return metadataBlob != null; } - public CacheEntry build() { + CacheEntry build() { return new DefaultCacheEntry( Preconditions.checkNotNull(layerDigest, "layerDigest required"), Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), @@ -84,7 +84,7 @@ public CacheEntry build() { * * @return the new {@link Builder} */ - public static Builder builder() { + static Builder builder() { return new Builder(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java index 4e8a6bc31f..0a826c4191 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -29,7 +29,7 @@ *

{@code
  * layers/
  *   /
- *     .layer
+ *     
  *     metadata
  *   ...
  * selectors/
@@ -38,14 +38,13 @@
  * }
* * Layers entries are stored in their own directories under the {@code layers/} directory. Each - * layer directory is named by the layer digest. Inside each layer directory, the layer contents is - * the {@code .layer} file prefixed with the layer diff ID, and the metadata is the {@code metadata} - * file. + * layer directory is named by the layer digest. Inside each layer directory, the layer contents + * file is named by the layer diff ID, and the metadata is the {@code metadata} file. * *

Selectors are stored in the {@code selectors/} directory. Each selector file is named by the * selector digest. The contents of a selector file is the digest of the layer it selects. */ -public class DefaultCacheStorage implements CacheStorage { +class DefaultCacheStorage implements CacheStorage { /** * Instantiates a {@link CacheStorage} backed by this storage engine. @@ -53,16 +52,14 @@ public class DefaultCacheStorage implements CacheStorage { * @param cacheDirectory the directory for this cache * @return a new {@link CacheStorage} */ - public static CacheStorage withDirectory(Path cacheDirectory) { - return new DefaultCacheStorage(cacheDirectory); + static CacheStorage withDirectory(Path cacheDirectory) { + return new DefaultCacheStorage(new DefaultCacheStorageFiles(cacheDirectory)); } private final DefaultCacheStorageWriter defaultCacheStorageWriter; private final DefaultCacheStorageReader defaultCacheStorageReader; - private DefaultCacheStorage(Path cacheDirectory) { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(cacheDirectory); + private DefaultCacheStorage(DefaultCacheStorageFiles defaultCacheStorageFiles) { this.defaultCacheStorageWriter = new DefaultCacheStorageWriter(defaultCacheStorageFiles); this.defaultCacheStorageReader = new DefaultCacheStorageReader(defaultCacheStorageFiles); } @@ -84,8 +81,8 @@ public Optional retrieve(DescriptorDigest layerDigest) } @Override - public Optional select(DescriptorDigest selector) throws IOException { - // TODO: Implement - return Optional.empty(); + public Optional select(DescriptorDigest selector) + throws IOException, CacheCorruptedException { + return defaultCacheStorageReader.select(selector); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java index f67590675c..77b9fa3d17 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.DigestException; @@ -66,7 +67,7 @@ Set fetchDigests() throws IOException, CacheCorruptedException * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheEntry} referenced by the layer digest + * @return the {@link CacheEntry} referenced by the layer digest, if found * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ @@ -110,4 +111,35 @@ Optional retrieve(DescriptorDigest layerDigest) return Optional.of(cacheEntryBuilder.build()); } + + /** + * Retrieves the layer digest selected by the {@code selector}. + * + * @param selector the selector + * @return the layer digest {@code selector} selects, if found + * @throws CacheCorruptedException if the selector file contents was not a valid layer digest + * @throws IOException if an I/O exception occurs + */ + Optional select(DescriptorDigest selector) + throws CacheCorruptedException, IOException { + Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + if (!Files.exists(selectorFile)) { + return Optional.empty(); + } + + String selectorFileContents = + new String(Files.readAllBytes(selectorFile), StandardCharsets.UTF_8); + try { + return Optional.of(DescriptorDigest.fromHash(selectorFileContents)); + + } catch (DigestException ex) { + throw new CacheCorruptedException( + "Expected valid layer digest as contents of selector file `" + + selectorFile + + "` for selector `" + + selector.getHash() + + "`, but got: " + + selectorFileContents); + } + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java deleted file mode 100644 index e77017fc29..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheWrite.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.ncache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.util.Optional; -import javax.annotation.Nullable; - -/** A default implementation of {@link CacheWrite}. */ -public class DefaultCacheWrite implements CacheWrite { - - /** - * Constructs a {@link CacheWrite} with only the layer {@link Blob}. - * - * @param layerBlob the layer {@link Blob} - * @return the new {@link CacheWrite} - */ - public static CacheWrite layerOnly(Blob layerBlob) { - return new DefaultCacheWrite(layerBlob, null, null); - } - - /** - * Constructs a {@link CacheWrite} with a layer {@link Blob}, an additional selector digest, and a - * metadata {@link Blob}. - * - * @param layerBlob the layer {@link Blob} - * @param selector the selector digest - * @param metadataBlob the metadata {@link Blob} - * @return the new {@link CacheWrite} - */ - public static CacheWrite withSelectorAndMetadata( - Blob layerBlob, DescriptorDigest selector, Blob metadataBlob) { - return new DefaultCacheWrite(layerBlob, selector, metadataBlob); - } - - private final Blob layerBlob; - @Nullable private final DescriptorDigest selector; - @Nullable private final Blob metadataBlob; - - private DefaultCacheWrite( - Blob layerBlob, @Nullable DescriptorDigest selector, @Nullable Blob metadataBlob) { - this.layerBlob = layerBlob; - this.selector = selector; - this.metadataBlob = metadataBlob; - } - - @Override - public Blob getLayerBlob() { - return layerBlob; - } - - @Override - public Optional getSelector() { - return Optional.ofNullable(selector); - } - - @Override - public Optional getMetadataBlob() { - return Optional.ofNullable(metadataBlob); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java new file mode 100644 index 0000000000..04b2344fea --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java @@ -0,0 +1,247 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import com.google.common.io.CountingOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link Cache}. */ +public class CacheTest { + + /** + * Gets a {@link Blob} that is {@code blob} compressed. + * + * @param blob the {@link Blob} to compress + * @return the compressed {@link Blob} + */ + private static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }); + } + + /** + * Gets a {@link Blob} that is {@code blob} decompressed. + * + * @param blob the {@link Blob} to decompress + * @return the decompressed {@link Blob} + */ + private static Blob decompress(Blob blob) { + return Blobs.from( + outputStream -> { + ByteArrayInputStream compressedInputStream = + new ByteArrayInputStream(Blobs.writeToByteArray(blob)); + try (GZIPInputStream decompressorStream = new GZIPInputStream(compressedInputStream)) { + ByteStreams.copy(decompressorStream, outputStream); + } + }); + } + + /** + * Gets the digest of {@code blob}. + * + * @param blob the {@link Blob} + * @return the {@link DescriptorDigest} of {@code blob} + * @throws IOException if an I/O exception occurs + */ + private static DescriptorDigest digestOf(Blob blob) throws IOException { + return blob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + } + + /** + * Gets the size of {@code blob}. + * + * @param blob the {@link Blob} + * @return the size (in bytes) of {@code blob} + * @throws IOException if an I/O exception occurs + */ + private static long sizeOf(Blob blob) throws IOException { + CountingOutputStream countingOutputStream = + new CountingOutputStream(ByteStreams.nullOutputStream()); + blob.writeTo(countingOutputStream); + return countingOutputStream.getCount(); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Blob layerBlob1; + private DescriptorDigest layerDigest1; + private DescriptorDigest layerDiffId1; + private long layerSize1; + private ImmutableList layerEntries1; + private Blob metadataBlob1; + + private Blob layerBlob2; + private DescriptorDigest layerDigest2; + private DescriptorDigest layerDiffId2; + private long layerSize2; + private ImmutableList layerEntries2; + private Blob metadataBlob2; + + @Before + public void setUp() throws IOException { + layerBlob1 = Blobs.from("layerBlob1"); + layerDigest1 = digestOf(compress(layerBlob1)); + layerDiffId1 = digestOf(layerBlob1); + layerSize1 = sizeOf(compress(layerBlob1)); + layerEntries1 = + ImmutableList.of( + new LayerEntry(Paths.get("source/file"), AbsoluteUnixPath.get("/extraction/path")), + new LayerEntry( + Paths.get("another/source/file"), + AbsoluteUnixPath.get("/another/extraction/path"))); + metadataBlob1 = Blobs.from("metadata"); + + layerBlob2 = Blobs.from("layerBlob2"); + layerDigest2 = digestOf(compress(layerBlob2)); + layerDiffId2 = digestOf(layerBlob2); + layerSize2 = sizeOf(compress(layerBlob2)); + layerEntries2 = ImmutableList.of(); + metadataBlob2 = Blobs.from("metadata"); + } + + @Test + public void testWithDirectory_existsButNotDirectory() throws IOException { + Path file = temporaryFolder.newFile().toPath(); + + try { + Cache.withDirectory(file); + Assert.fail(); + + } catch (FileAlreadyExistsException ex) { + // pass + } + } + + @Test + public void testWriteLayerOnly_retrieveByLayerDigest() + throws IOException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1NoMetadata(cache.write(layerBlob1)); + verifyIsLayer1NoMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + } + + @Test + public void testWriteWithSelectorAndMetadata_retrieveByLayerDigest() + throws IOException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); + verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + } + + @Test + public void testWriteWithSelectorAndMetadata_retrieveByLayerEntries() + throws IOException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); + verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + } + + @Test + public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorruptedException { + Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); + verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2, metadataBlob2)); + verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + verifyIsLayer2WithMetadata(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); + verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + verifyIsLayer2WithMetadata(cache.retrieve(layerEntries2).orElseThrow(AssertionError::new)); + } + + /** + * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}, and + * that no metadata was written. + * + * @param cacheEntry the {@link CacheEntry} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer1NoMetadata(CacheEntry cacheEntry) throws IOException { + verifyIsLayer1(cacheEntry); + Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); + } + + /** + * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}, and + * that its metadata was written. + * + * @param cacheEntry the {@link CacheEntry} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer1WithMetadata(CacheEntry cacheEntry) throws IOException { + verifyIsLayer1(cacheEntry); + Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); + Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get())); + } + + /** + * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}. Does + * not check the metadata. + * + * @param cacheEntry the {@link CacheEntry} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer1(CacheEntry cacheEntry) throws IOException { + Assert.assertEquals("layerBlob1", Blobs.writeToString(decompress(cacheEntry.getLayerBlob()))); + Assert.assertEquals(layerDigest1, cacheEntry.getLayerDigest()); + Assert.assertEquals(layerDiffId1, cacheEntry.getLayerDiffId()); + Assert.assertEquals(layerSize1, cacheEntry.getLayerSize()); + } + + /** + * Verifies that {@code cacheEntry} corresponds to the second fake layer in {@link #setUp}, and + * that its metadata was written. + * + * @param cacheEntry the {@link CacheEntry} to verify + * @throws IOException if an I/O exception occurs + */ + private void verifyIsLayer2WithMetadata(CacheEntry cacheEntry) throws IOException { + Assert.assertEquals("layerBlob2", Blobs.writeToString(decompress(cacheEntry.getLayerBlob()))); + Assert.assertEquals(layerDigest2, cacheEntry.getLayerDigest()); + Assert.assertEquals(layerDiffId2, cacheEntry.getLayerDiffId()); + Assert.assertEquals(layerSize2, cacheEntry.getLayerSize()); + Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); + Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get())); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java similarity index 89% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java index bb4853a36f..0f0b938033 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheWriteTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java @@ -25,15 +25,15 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DefaultCacheWrite}. */ +/** Tests for {@link CacheWrite}. */ @RunWith(MockitoJUnitRunner.class) -public class DefaultCacheWriteTest { +public class CacheWriteTest { @Mock private DescriptorDigest mockSelector; @Test public void testLayerOnly() throws IOException { - CacheWrite cacheWrite = DefaultCacheWrite.layerOnly(Blobs.from("layerBlob")); + CacheWrite cacheWrite = CacheWrite.layerOnly(Blobs.from("layerBlob")); Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); Assert.assertFalse(cacheWrite.getSelector().isPresent()); Assert.assertFalse(cacheWrite.getMetadataBlob().isPresent()); @@ -42,7 +42,7 @@ public void testLayerOnly() throws IOException { @Test public void testWithSelectorAndMetadata() throws IOException { CacheWrite cacheWrite = - DefaultCacheWrite.withSelectorAndMetadata( + CacheWrite.withSelectorAndMetadata( Blobs.from("layerBlob"), mockSelector, Blobs.from("metadataBlob")); Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); Assert.assertEquals(mockSelector, cacheWrite.getSelector().orElse(null)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java index 3ac33cf779..b3d269dbeb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.security.DigestException; import java.util.Arrays; import java.util.HashSet; @@ -136,4 +137,48 @@ public void testRetrieve() throws IOException, CacheCorruptedException { ex.getMessage()); } } + + @Test + public void testSelect_invalidLayerDigest() throws IOException { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + + DefaultCacheStorageReader defaultCacheStorageReader = + new DefaultCacheStorageReader(defaultCacheStorageFiles); + + DescriptorDigest selector = layerDigest1; + Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Files.createDirectories(selectorFile.getParent()); + Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from("not a valid layer digest"))); + + try { + defaultCacheStorageReader.select(selector); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertEquals( + "Expected valid layer digest as contents of selector file `" + + selectorFile + + "` for selector `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, but got: not a valid layer digest", + ex.getMessage()); + } + } + + @Test + public void testSelect() throws IOException, CacheCorruptedException { + DefaultCacheStorageFiles defaultCacheStorageFiles = + new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + + DefaultCacheStorageReader defaultCacheStorageReader = + new DefaultCacheStorageReader(defaultCacheStorageFiles); + + DescriptorDigest selector = layerDigest1; + Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Files.createDirectories(selectorFile.getParent()); + Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from(layerDigest2.getHash()))); + + Optional selectedLayerDigest = defaultCacheStorageReader.select(selector); + Assert.assertTrue(selectedLayerDigest.isPresent()); + Assert.assertEquals(layerDigest2, selectedLayerDigest.get()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java index 3ea10fd4ec..9e5cbddfc1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java @@ -66,7 +66,7 @@ public void setUp() throws IOException { @Test public void testWrite_layerOnly() throws IOException { Blob layerBlob = Blobs.from("layerBlob"); - CacheEntry cacheEntry = verifyWrite(DefaultCacheWrite.layerOnly(layerBlob), layerBlob); + CacheEntry cacheEntry = verifyWrite(CacheWrite.layerOnly(layerBlob), layerBlob); Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); } @@ -79,8 +79,7 @@ public void testWrite_withSelectorAndMetadata() throws IOException { CacheEntry cacheEntry = verifyWrite( - DefaultCacheWrite.withSelectorAndMetadata(layerBlob, selector, metadataBlob), - layerBlob); + CacheWrite.withSelectorAndMetadata(layerBlob, selector, metadataBlob), layerBlob); // Verifies cacheEntry is correct. Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); From 84678d94eefe1906881c6c9b5d253bcfb8b175a0 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 25 Sep 2018 15:05:57 -0400 Subject: [PATCH 0230/2020] Remove ListOfJsonTemplate class (#1023) * Remove ListOfJsonTemplate class * ManifestTemplate --> ManifestEntryTemplate --- .../jib/docker/ImageToTarballTranslator.java | 8 ++-- ...a => DockerLoadManifestEntryTemplate.java} | 21 ++++------ .../tools/jib/json/JsonTemplateMapper.java | 21 +++++----- .../tools/jib/json/ListOfJsonTemplate.java | 31 -------------- .../jib/ncache/LayerEntriesSelector.java | 38 +++++++---------- .../docker/ImageToTarballTranslatorTest.java | 4 +- .../json/DockerLoadManifestTemplateTest.java | 9 ++-- .../jib/json/JsonTemplateMapperTest.java | 41 +++++++++++++++++++ .../jib/ncache/LayerEntriesSelectorTest.java | 9 ++-- .../src/test/resources/json/basic_list.json | 1 + 10 files changed, 93 insertions(+), 90 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/{DockerLoadManifestTemplate.java => DockerLoadManifestEntryTemplate.java} (78%) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java create mode 100644 jib-core/src/test/resources/json/basic_list.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java index 81ec90606f..c5e407dd82 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestTemplate; +import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.tar.TarStreamBuilder; import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** Translates an {@link Image} to a tarball that can be loaded into Docker. */ @@ -50,7 +51,7 @@ public ImageToTarballTranslator(Image image) { public Blob toTarballBlob(ImageReference imageReference) throws IOException { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - DockerLoadManifestTemplate manifestTemplate = new DockerLoadManifestTemplate(); + DockerLoadManifestEntryTemplate manifestTemplate = new DockerLoadManifestEntryTemplate(); // Adds all the layers to the tarball and manifest. for (CachedLayer layer : image.getLayers()) { @@ -71,7 +72,8 @@ public Blob toTarballBlob(ImageReference imageReference) throws IOException { // Adds the manifest to tarball. manifestTemplate.setRepoTags(imageReference.toStringWithTag()); tarStreamBuilder.addByteEntry( - Blobs.writeToByteArray(JsonTemplateMapper.toBlob(manifestTemplate)), + Blobs.writeToByteArray( + JsonTemplateMapper.toBlob(Collections.singletonList(manifestTemplate))), MANIFEST_JSON_FILE_NAME); return tarStreamBuilder.toBlob(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java similarity index 78% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java index 07ad7d0230..e2c311a1e4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java @@ -17,19 +17,21 @@ package com.google.cloud.tools.jib.docker.json; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.cloud.tools.jib.json.ListOfJsonTemplate; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * JSON Template for loadable Docker Manifest. The repoTags property requires a tag; i.e. if a tag - * is missing, it explicitly should use "latest". + * JSON Template for a loadable Docker Manifest entry. The repoTags property requires a tag; i.e. if + * a tag is missing, it explicitly should use "latest". * - *

Example manifest JSON: + *

Note that this is a template for a single Manifest entry, while the entire Docker Manifest + * should be {@code List}. + * + *

Example manifest entry JSON: * *

{@code
- * [{
+ * {
  *   "Config":"config.json",
  *   "RepoTags":["repository:tag"]
  *   "Layers": [
@@ -37,13 +39,13 @@
  *     "ba7c544469e514f1a9a4dec59ab640540d50992b288adbb34a1a63c45bf19a24.tar.gz",
  *     "15705ab016593987662839b40f5a22fd1032996c90808d4a1371eb46974017d5.tar.gz"
  *   ]
- * }]
+ * }
  * }
* * @see Docker load * source */ -public class DockerLoadManifestTemplate implements ListOfJsonTemplate { +public class DockerLoadManifestEntryTemplate implements JsonTemplate { private final String config = "config.json"; private List repoTags = Collections.singletonList(null); @@ -56,9 +58,4 @@ public void setRepoTags(String repoTags) { public void addLayerFile(String layer) { layers.add(layer); } - - @Override - public List getList() { - return Collections.singletonList(this); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index cce699eeca..2ff9e4dbe5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.json; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionType; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; @@ -103,17 +104,17 @@ public static T readJson(String jsonString, Class te /** * Deserializes a JSON object list from a JSON string. * - * @param child type of {@link ListOfJsonTemplate} + * @param child type of {@link JsonTemplate} * @param jsonString a JSON string * @param templateClass the template to deserialize the string to - * @return the template filled with the values parsed from {@code jsonFile} + * @return the template filled with the values parsed from {@code jsonString} * @throws IOException if an error occurred during parsing the JSON */ - public static > List readListOfJson( + public static List readListOfJson( String jsonString, Class templateClass) throws IOException { - return objectMapper.readValue( - jsonString, - objectMapper.getTypeFactory().constructCollectionType(List.class, templateClass)); + CollectionType listType = + objectMapper.getTypeFactory().constructCollectionType(List.class, templateClass); + return objectMapper.readValue(jsonString, listType); } /** @@ -127,13 +128,13 @@ public static Blob toBlob(JsonTemplate template) { } /** - * Convert a {@link ListOfJsonTemplate} to a {@link Blob} of the JSON string. + * Convert a list of {@link JsonTemplate} to a {@link Blob} of the JSON string. * - * @param template the list of JSON templates to convert + * @param templates the list of JSON templates to convert * @return a {@link Blob} of the JSON string */ - public static Blob toBlob(ListOfJsonTemplate template) { - return toBlob(template.getList()); + public static Blob toBlob(List templates) { + return toBlob((Object) templates); } private static Blob toBlob(Object template) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java deleted file mode 100644 index 50d200b846..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/ListOfJsonTemplate.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.json; - -import java.util.List; - -/** - * All JSON templates to be used with {@link JsonTemplateMapper} that need to be wrapped in a list - * must extend this class. - * - *

Json fields should be private fields and fields that are {@code null} will not be serialized. - */ -public interface ListOfJsonTemplate extends JsonTemplate { - - /** @return the JsonTemplate wrapped as a list. e.g.: [{"property":"value"}] */ - List getList(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java index 586fc50bdd..1950687c15 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.json.ListOfJsonTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -92,30 +91,21 @@ public int hashCode() { } } - /** Serialized form of a list of {@link LayerEntry}s. */ + /** + * Converts a list of {@link LayerEntry}s into a list of {@link LayerEntriesTemplate}. The list is + * sorted by source file first, then extraction path (see {@link LayerEntryTemplate#compareTo}). + * + * @param layerEntries + * @return list of {@link LayerEntryTemplate} after sorting + */ @VisibleForTesting - static class LayerEntriesTemplate implements ListOfJsonTemplate { - - private final List layerEntryTemplates = new ArrayList<>(); - - @Override - public List getList() { - return layerEntryTemplates; - } - - /** - * Serializes a list of {@link LayerEntry}s into a new {@link LayerEntriesTemplate}. The list is - * sorted by source file first, then extraction path (see {@link LayerEntryTemplate#compareTo}). - * - * @param layerEntries the list of {@link LayerEntry}s - */ - @VisibleForTesting - LayerEntriesTemplate(ImmutableList layerEntries) { - for (LayerEntry layerEntry : layerEntries) { - layerEntryTemplates.add(new LayerEntryTemplate(layerEntry)); - } - Collections.sort(layerEntryTemplates); + static List toSortedJsonTemplates(List layerEntries) { + List jsonTemplates = new ArrayList<>(); + for (LayerEntry entry : layerEntries) { + jsonTemplates.add(new LayerEntryTemplate(entry)); } + Collections.sort(jsonTemplates); + return jsonTemplates; } /** @@ -128,7 +118,7 @@ public List getList() { */ static DescriptorDigest generateSelector(ImmutableList layerEntries) throws IOException { - return JsonTemplateMapper.toBlob(new LayerEntriesTemplate(layerEntries)) + return JsonTemplateMapper.toBlob(toSortedJsonTemplates(layerEntries)) .writeTo(ByteStreams.nullOutputStream()) .getDigest(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java index 3eeaac9009..73e10168af 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestTemplate; +import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; @@ -103,7 +103,7 @@ public void testToTarballBlob() String manifestJson = CharStreams.toString( new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - JsonTemplateMapper.readListOfJson(manifestJson, DockerLoadManifestTemplate.class); + JsonTemplateMapper.readListOfJson(manifestJson, DockerLoadManifestEntryTemplate.class); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java index 735574e30b..74e22cfbe6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java @@ -26,10 +26,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; import org.junit.Assert; import org.junit.Test; -/** Tests for {@link DockerLoadManifestTemplate}. */ +/** Tests for {@link DockerLoadManifestEntryTemplate}. */ public class DockerLoadManifestTemplateTest { @Test @@ -38,13 +40,14 @@ public void testToJson() throws URISyntaxException, IOException { Path jsonFile = Paths.get(Resources.getResource("json/loadmanifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - DockerLoadManifestTemplate template = new DockerLoadManifestTemplate(); + DockerLoadManifestEntryTemplate template = new DockerLoadManifestEntryTemplate(); template.setRepoTags( ImageReference.of("testregistry", "testrepo", "testtag").toStringWithTag()); template.addLayerFile("layer1.tar.gz"); template.addLayerFile("layer2.tar.gz"); template.addLayerFile("layer3.tar.gz"); - Assert.assertEquals(expectedJson, Blobs.writeToString(JsonTemplateMapper.toBlob(template))); + List loadManifest = Collections.singletonList(template); + Assert.assertEquals(expectedJson, Blobs.writeToString(JsonTemplateMapper.toBlob(loadManifest))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index a942f4fa45..a123370729 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -119,4 +119,45 @@ public void testReadJsonWithLock() throws IOException, URISyntaxException, Diges "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")))); // ignore testJson.list } + + @Test + public void testReadListOfJson() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("json/basic_list.json").toURI()); + + String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + List listofJsons = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); + TestJson json1 = listofJsons.get(0); + TestJson json2 = listofJsons.get(1); + + DescriptorDigest digest1 = + DescriptorDigest.fromDigest( + "sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"); + DescriptorDigest digest2 = + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); + + Assert.assertEquals(1, json1.number); + Assert.assertEquals(2, json2.number); + Assert.assertEquals("text1", json1.text); + Assert.assertEquals("text2", json2.text); + Assert.assertEquals(digest1, json1.digest); + Assert.assertEquals(digest2, json2.digest); + Assert.assertEquals(10, json1.innerObject.number); + Assert.assertEquals(20, json2.innerObject.number); + Assert.assertEquals(2, json1.list.size()); + Assert.assertTrue(json2.list.isEmpty()); + } + + @Test + public void testToBlob_listOfJson() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("json/basic_list.json").toURI()); + + String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + List listOfJson = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); + + ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); + JsonTemplateMapper.toBlob(listOfJson).writeTo(jsonStream); + + Assert.assertEquals(jsonString, jsonStream.toString()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java index aef40e925a..68adbc0f0b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.ncache.LayerEntriesSelector.LayerEntriesTemplate; import com.google.cloud.tools.jib.ncache.LayerEntriesSelector.LayerEntryTemplate; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -64,16 +63,16 @@ public void testLayerEntryTemplate_compareTo() { } @Test - public void testLayerEntriesTemplate_getList() { + public void testToSortedJsonTemplates() { Assert.assertEquals( toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES), - new LayerEntriesTemplate(OUT_OF_ORDER_LAYER_ENTRIES).getList()); + LayerEntriesSelector.toSortedJsonTemplates(OUT_OF_ORDER_LAYER_ENTRIES)); } @Test public void testGenerateSelector_empty() throws IOException { DescriptorDigest expectedSelector = - JsonTemplateMapper.toBlob(new LayerEntriesTemplate(ImmutableList.of())) + JsonTemplateMapper.toBlob(ImmutableList.of()) .writeTo(ByteStreams.nullOutputStream()) .getDigest(); Assert.assertEquals( @@ -83,7 +82,7 @@ public void testGenerateSelector_empty() throws IOException { @Test public void testGenerateSelector() throws IOException { DescriptorDigest expectedSelector = - JsonTemplateMapper.toBlob(new LayerEntriesTemplate(IN_ORDER_LAYER_ENTRIES)) + JsonTemplateMapper.toBlob(toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES)) .writeTo(ByteStreams.nullOutputStream()) .getDigest(); Assert.assertEquals( diff --git a/jib-core/src/test/resources/json/basic_list.json b/jib-core/src/test/resources/json/basic_list.json new file mode 100644 index 0000000000..d140c8738b --- /dev/null +++ b/jib-core/src/test/resources/json/basic_list.json @@ -0,0 +1 @@ +[{"number":1,"text":"text1","digest":"sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10","innerObject":{"number":10},"list":[{"number":11},{"number":12}]},{"number":2,"text":"text2","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","innerObject":{"number":20},"list":[]}] \ No newline at end of file From 5446d45e87623b5894cef59a9f55c9a6932d57c5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 25 Sep 2018 16:43:45 -0400 Subject: [PATCH 0231/2020] Add _jibSkaffoldFiles task to gradle plugin (#1021) --- .../cloud/tools/jib/gradle/FilesTask.java | 174 ++++++++++++++++++ .../cloud/tools/jib/gradle/JibPlugin.java | 5 + .../cloud/tools/jib/gradle/FilesTaskTest.java | 136 ++++++++++++++ .../cloud/tools/jib/gradle/TestProject.java | 0 .../projects/multi-service/build.gradle | 17 ++ .../complex-service/build.gradle | 35 ++++ .../src/main/extra-resources-1/resource1.txt | 0 .../src/main/extra-resources-2/resource2.txt | 0 .../src/main/java/com/test/HelloWorld.java | 23 +++ .../src/main/other-jib/extra-file | 0 .../projects/multi-service/gradle.properties | 0 .../projects/multi-service/lib/build.gradle | 10 + .../lib/src/main/java/com/lib/Lib.java | 25 +++ .../lib/src/main/resources/hi.txt | 1 + .../lib/src/test/java/com/lib/LibTest.java | 29 +++ .../projects/multi-service/settings.gradle | 3 + .../multi-service/simple-service/build.gradle | 17 ++ .../src/main/java/com/test/HelloWorld.java | 23 +++ .../resources/projects/simple/build.gradle | 22 +++ .../projects/simple/libs/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../simple/src/main/custom-extra-dir/bar/cat | 1 + .../simple/src/main/custom-extra-dir/foo | 1 + .../src/main/java/com/test/HelloWorld.java | 41 +++++ .../projects/simple/src/main/resources/world | 1 + .../jib/maven/skaffold/FilesMojoTest.java | 2 +- 25 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java rename jib-gradle-plugin/src/{integration-test => test}/java/com/google/cloud/tools/jib/gradle/TestProject.java (100%) create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/other-jib/extra-file create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/gradle.properties create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/bar/cat create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/foo create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/projects/simple/src/main/resources/world diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java new file mode 100644 index 0000000000..d414879cbd --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java @@ -0,0 +1,174 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.common.base.Preconditions; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.PublishArtifact; +import org.gradle.api.initialization.Settings; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; + +/** + * Print out changing source dependencies on a project. + * + *

Expected use: {@code ./gradlew _jibSkaffoldFiles -q} or {@code ./gradlew + * ::_jibSkaffoldFiles -q} + */ +public class FilesTask extends DefaultTask { + + /** + * Prints the locations of a project's build.gradle, settings.gradle, and gradle.properties. + * + * @param project the project + */ + private static void printGradleFiles(Project project) { + Path projectPath = project.getProjectDir().toPath(); + + // Print build.gradle + System.out.println(project.getBuildFile()); + + // Print settings.gradle + if (project.getGradle().getStartParameter().getSettingsFile() != null) { + System.out.println(project.getGradle().getStartParameter().getSettingsFile()); + } else if (Files.exists(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE))) { + System.out.println(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE)); + } + + // Print gradle.properties + if (Files.exists(projectPath.resolve("gradle.properties"))) { + System.out.println(projectPath.resolve("gradle.properties")); + } + } + + /** + * Prints build files, sources, and resources associated with a project. + * + * @param project the project + */ + private static void printProjectFiles(Project project) { + JavaPluginConvention javaConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = + javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet == null) { + return; + } + + // Print build config, settings, etc. + printGradleFiles(project); + + // Print sources + resources + mainSourceSet + .getAllSource() + .getSourceDirectories() + .forEach( + sourceDirectory -> { + if (sourceDirectory.exists()) { + System.out.println(sourceDirectory); + } + }); + } + + /** + * Recursive function for printing out a project's artifacts. Calls itself when it encounters a + * project dependency. + * + * @param project the project to list the artifacts for + * @param projectDependenciesResult the set of project dependencies encountered. When a project + * dependency is encountered, it is added to this set. + */ + private static void findProjectDependencies( + Project project, Set projectDependenciesResult) { + for (Configuration configuration : + project.getConfigurations().getByName("runtime").getHierarchy()) { + for (Dependency dependency : configuration.getDependencies()) { + if (dependency instanceof ProjectDependency) { + projectDependenciesResult.add((ProjectDependency) dependency); + findProjectDependencies( + ((ProjectDependency) dependency).getDependencyProject(), projectDependenciesResult); + } + } + } + } + + @Nullable private JibExtension jibExtension; + + public FilesTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + @TaskAction + public void listFiles() { + Preconditions.checkNotNull(jibExtension); + Project project = getProject(); + + // If this is not the root project, print the root project's build.gradle and settings.gradle + if (project != project.getRootProject()) { + printGradleFiles(project.getRootProject()); + } + + printProjectFiles(project); + + // Print extra layer + if (Files.exists(jibExtension.getExtraDirectoryPath())) { + System.out.println(jibExtension.getExtraDirectoryPath()); + } + + // Find project dependencies + Set projectDependencies = new HashSet<>(); + findProjectDependencies(project, projectDependencies); + + Set projectDependencyJars = new HashSet<>(); + for (ProjectDependency projectDependency : projectDependencies) { + printProjectFiles(projectDependency.getDependencyProject()); + + // Keep track of project dependency jars for filtering out later + String configurationName = projectDependency.getTargetConfiguration(); + if (configurationName == null) { + configurationName = "default"; + } + Project dependencyProject = projectDependency.getDependencyProject(); + for (Configuration targetConfiguration : + dependencyProject.getConfigurations().getByName(configurationName).getHierarchy()) { + for (PublishArtifact artifact : targetConfiguration.getArtifacts()) { + projectDependencyJars.add(artifact.getFile()); + } + } + } + + // Print out SNAPSHOT, non-project dependency jars + for (File file : project.getConfigurations().getByName("runtime")) { + if (!projectDependencyJars.contains(file) && file.toString().contains("SNAPSHOT")) { + System.out.println(file); + projectDependencyJars.add(file); // Add to set to avoid printing the same files twice + } + } + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index cb94d3ad33..65f7acbb56 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -39,6 +39,7 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; + @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of @@ -102,6 +103,8 @@ public void apply(Project project) { .getTasks() .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) .setJibExtension(jibExtension); + Task filesTask = + project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); project.afterEvaluate( projectAfterEvaluation -> { @@ -112,6 +115,7 @@ public void apply(Project project) { dockerContextTask.dependsOn(classesTask); buildDockerTask.dependsOn(classesTask); buildTarTask.dependsOn(classesTask); + filesTask.dependsOn(classesTask); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble @@ -129,6 +133,7 @@ public void apply(Project project) { dockerContextTask.dependsOn(assembleTask); buildDockerTask.dependsOn(assembleTask); buildTarTask.dependsOn(assembleTask); + filesTask.dependsOn(assembleTask); }); } } catch (UnknownTaskException ex) { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java new file mode 100644 index 0000000000..678b1136d5 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link FilesTask}. */ +public class FilesTaskTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + + /** + * Verifies that the files task succeeded and returns the list of paths it prints out. + * + * @param project the project to run the task on + * @param moduleName the name of the sub-project, or {@code null} if no sub-project + * @return the list of paths printed by the task + */ + private static List verifyTaskSuccess(TestProject project, @Nullable String moduleName) { + String taskName = + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.FILES_TASK_NAME; + BuildResult buildResult = project.build(taskName, "-q"); + BuildTask jibTask = buildResult.task(taskName); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + + return Splitter.on(System.lineSeparator()) + .omitEmptyStrings() + .splitToList(buildResult.getOutput()) + .stream() + .map(Paths::get) + .collect(Collectors.toList()); + } + + /** + * Asserts that two lists contain the same paths. Required to avoid Mac's /var/ vs. /private/var/ + * symlink issue. + * + * @param expected the expected list of paths + * @param actual the actual list of paths + * @throws IOException if checking if two files are the same fails + */ + private static void assertPathListsAreEqual(List expected, List actual) + throws IOException { + Assert.assertEquals(expected.size(), actual.size()); + for (int index = 0; index < expected.size(); index++) { + Assert.assertEquals(expected.get(index).toRealPath(), actual.get(index).toRealPath()); + } + } + + @Test + public void testFilesTask_singleProject() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + List result = verifyTaskSuccess(simpleTestProject, null); + List expected = + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/custom-extra-dir")); + assertPathListsAreEqual(expected, result); + } + + @Test + public void testFilesTask_multiProjectSimpleService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path simpleServiceRoot = projectRoot.resolve("simple-service"); + List result = verifyTaskSuccess(multiTestProject, "simple-service"); + List expected = + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + simpleServiceRoot.resolve("build.gradle"), + simpleServiceRoot.resolve("src/main/java")); + assertPathListsAreEqual(expected, result); + } + + @Test + public void testFilesTask_multiProjectComplexService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + List result = verifyTaskSuccess(multiTestProject, "complex-service"); + List expected = + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + complexServiceRoot.resolve("build.gradle"), + complexServiceRoot.resolve("src/main/extra-resources-1"), + complexServiceRoot.resolve("src/main/extra-resources-2"), + complexServiceRoot.resolve("src/main/java"), + complexServiceRoot.resolve("src/main/other-jib"), + libRoot.resolve("build.gradle"), + libRoot.resolve("src/main/resources"), + libRoot.resolve("src/main/java")); + assertPathListsAreEqual(expected, result.subList(0, 11)); + + // guava jar is in a temporary-looking directory, so don't do a full match here + Assert.assertThat( + result.get(result.size() - 1).toString(), + CoreMatchers.endsWith("guava-HEAD-jre-SNAPSHOT.jar")); + Assert.assertEquals(12, result.size()); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/TestProject.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle new file mode 100644 index 0000000000..9b4757ea5a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle new file mode 100644 index 0000000000..28762e824c --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } + extraDirectory = file('src/main/other-jib') +} + +sourceSets { + main { + resources { + srcDirs "src/main/extra-resources-1", "src/main/extra-resources-2" + } + } +} + +dependencies { + compile project(':lib') + compile 'org.apache.commons:commons-io:1.3.2' + compile 'com.google.guava:guava:HEAD-jre-SNAPSHOT' +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/other-jib/extra-file b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/other-jib/extra-file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/gradle.properties b/jib-gradle-plugin/src/test/resources/projects/multi-service/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle new file mode 100644 index 0000000000..313ac21fc2 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java new file mode 100644 index 0000000000..0873623756 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +/** Shared Code! */ +public class Lib { + + public String getThing() { + return "thing"; + } +} diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt new file mode 100644 index 0000000000..32f95c0d12 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java new file mode 100644 index 0000000000..ab7425f896 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +import org.junit.Assert; +import org.junit.Test; + +/** Unit test for simple App. */ +public class LibTest { + /** Rigorous Test :-) */ + @Test + public void testGetThing() { + Assert.assertEquals("thing", new Lib().getThing()); + } +} diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle new file mode 100644 index 0000000000..748c7dbf3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle @@ -0,0 +1,3 @@ +include ':simple-service' +include ':complex-service' +include ':lib' \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle new file mode 100644 index 0000000000..9b4757ea5a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle new file mode 100644 index 0000000000..adb59af775 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } + extraDirectory = file('src/main/custom-extra-dir') +} diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..333ceb39b26dfc6384b092d18cee5733295b5b12 GIT binary patch literal 770 zcmWIWW@h1HVBp|jkd4;%VE_Uq5CH_7KE;?7qUY=O+4sz8A8%c~i@e^tTIbH3-yCFc#rVO~M^BlM3_Bc!YAZL; zu$0t-)V!3`yyQw0dqGN<#;Em5Gcqu&0Ai5cNJ`y{Qd3JZ^V0Q_a}tY-a|2HMF*ypX zNvpJf(%U(2Pb2&HL%xq&6=n(ev=qoss0r}MCP2FnzB>!p0NB=U#CXy43^H?yh|;Ft?_0= z>YJFKg2J23Z)H?_7wp<=sc6eF?NQkATOU-aA`2{nx#vILbRhU&Osmy3YvbJcU)wIo z>HnO1c2e*6cYRK2yYGfZZM^I4wEDL7w*`+(I-L67m&8QeJih)>ie#k-$64jKEoW?h z*Ey_^wUQUl{-&chVUj;1NBRi`?K#3zo0Quhc^zD2vCe=qU2ng`e9t8+GknijooafT z%OE<3Lp;DW*Su!l9R|O{KRwE1-K?9wE?M`HKfs%jNrV}9qyvK$2m~13I)Z3;5 files) + private static void verifyFiles(Path projectRoot, String module, List files) throws VerificationException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); From 5b531fce4d1fb89bd63f88bcfe10a6061a3a5e58 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 25 Sep 2018 18:52:00 -0400 Subject: [PATCH 0232/2020] Use AbsoluteUnixPath for LayerEntry (#1032) --- .../cloud/tools/jib/image/LayerEntry.java | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index b2c21bcf76..b251065d27 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -17,10 +17,8 @@ package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.common.base.Preconditions; import java.nio.file.Path; import java.util.Objects; -import java.util.StringJoiner; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar @@ -36,23 +34,6 @@ */ public class LayerEntry { - /** - * Stringifies {@code path} in Unix form. The path must be absolute. - * - * @param path the path - * @return the string form of the absolute path - */ - private static String toUnixPath(Path path) { - Preconditions.checkArgument( - path.getRoot() != null, "Tried to stringify a non-absolute path: %s", path); - - StringJoiner pathJoiner = new StringJoiner("/", "/", ""); - for (Path pathComponent : path) { - pathJoiner.add(pathComponent.toString()); - } - return pathJoiner.toString(); - } - private final Path sourceFile; private final AbsoluteUnixPath extractionPath; @@ -108,7 +89,7 @@ public AbsoluteUnixPath getExtractionPath() { * @return the source file path */ public String getAbsoluteSourceFileString() { - return toUnixPath(sourceFile.toAbsolutePath()); + return AbsoluteUnixPath.fromPath(sourceFile.toAbsolutePath()).toString(); } /** From 370501b0ebd628655680f44c3ef356e6da175ccf Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 26 Sep 2018 09:26:07 -0700 Subject: [PATCH 0233/2020] Has Cache use last modified time as metadata. (#1031) --- .../google/cloud/tools/jib/ncache/Cache.java | 43 ++++-- .../jib/ncache/LastModifiedTimeMetadata.java | 88 ++++++++++++ .../cloud/tools/jib/ncache/CacheTest.java | 44 +++--- .../ncache/LastModifiedTimeMetadataTest.java | 126 ++++++++++++++++++ 4 files changed, 277 insertions(+), 24 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java index 2a1177aa29..80eeeacdbe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java @@ -23,14 +23,15 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.util.Optional; import javax.annotation.concurrent.Immutable; /** * Cache for storing data to be shared between Jib executions. * - *

Uses the default cache storage engine ({@link DefaultCacheStorage}) and layer entries as the - * selector ({@link LayerEntriesSelector}). + *

Uses the default cache storage engine ({@link DefaultCacheStorage}), layer entries as the + * selector ({@link LayerEntriesSelector}), and last modified time as the metadata. * *

This class is immutable and safe to use across threads. */ @@ -56,8 +57,8 @@ private Cache(CacheStorage cacheStorage) { } /** - * Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList, - * Blob)} to include a selector and metadata. + * Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList)} to + * include a selector and metadata. * * @param layerBlob the layer {@link Blob} * @return the {@link CacheEntry} for the written layer @@ -73,19 +74,22 @@ public CacheEntry write(Blob layerBlob) throws IOException { * * @param layerBlob the layer {@link Blob} * @param layerEntries the layer entries that make up the layer - * @param metadataBlob the metadata {@link Blob} * @return the {@link CacheEntry} for the written layer and metadata * @throws IOException if an I/O exception occurs */ - public CacheEntry write(Blob layerBlob, ImmutableList layerEntries, Blob metadataBlob) + public CacheEntry write(Blob layerBlob, ImmutableList layerEntries) throws IOException { return cacheStorage.write( CacheWrite.withSelectorAndMetadata( - layerBlob, LayerEntriesSelector.generateSelector(layerEntries), metadataBlob)); + layerBlob, + LayerEntriesSelector.generateSelector(layerEntries), + LastModifiedTimeMetadata.generateMetadata(layerEntries))); } /** - * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. + * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. The last + * modified time of the {@code layerEntries} must match the last modified time as stored by the + * metadata of the {@link CacheEntry}. * * @param layerEntries the layer entries to match against * @return a {@link CacheEntry} that was built from {@code layerEntries}, if found @@ -100,7 +104,28 @@ public Optional retrieve(ImmutableList layerEntries) return Optional.empty(); } - return cacheStorage.retrieve(optionalSelectedLayerDigest.get()); + Optional optionalCacheEntry = + cacheStorage.retrieve(optionalSelectedLayerDigest.get()); + if (!optionalCacheEntry.isPresent()) { + return Optional.empty(); + } + + CacheEntry cacheEntry = optionalCacheEntry.get(); + + Optional optionalRetrievedLastModifiedTime = + LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry); + if (!optionalRetrievedLastModifiedTime.isPresent()) { + return Optional.empty(); + } + + FileTime retrievedLastModifiedTime = optionalRetrievedLastModifiedTime.get(); + FileTime expectedLastModifiedTime = LastModifiedTimeMetadata.getLastModifiedTime(layerEntries); + + if (!expectedLastModifiedTime.equals(retrievedLastModifiedTime)) { + return Optional.empty(); + } + + return Optional.of(cacheEntry); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java new file mode 100644 index 0000000000..6204bc11fb --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Optional; + +/** + * Serializes/deserializes metadata storing the latest last modified time of all the source files in + * {@link LayerEntry}s for a layer. + * + *

Use {@link #generateMetadata} to serialize the latest last modified time of all the source + * files in {@link LayerEntry}s for a layer into a {@link Blob} containing the serialized last + * modified time. Use {@link #getLastModifiedTime(CacheEntry)} to deserialize the metadata in a + * {@link CacheEntry} into a last modified time. + */ +class LastModifiedTimeMetadata { + + /** + * Generates the metadata {@link Blob} for the list of {@link LayerEntry}s. The metadata is the + * latest last modified time of all the source files in the list of {@link LayerEntry}s serialized + * using {@link Instant#toString}. + * + * @param layerEntries the list of {@link LayerEntry}s + * @return the generated metadata + */ + static Blob generateMetadata(ImmutableList layerEntries) throws IOException { + return Blobs.from(getLastModifiedTime(layerEntries).toInstant().toString()); + } + + /** + * Gets the latest last modified time of all the source files in the list of {@link LayerEntry}s. + * + * @param layerEntries the list of {@link LayerEntry}s + * @return the last modified time + */ + static FileTime getLastModifiedTime(ImmutableList layerEntries) throws IOException { + FileTime maxLastModifiedTime = FileTime.from(Instant.MIN); + + for (LayerEntry layerEntry : layerEntries) { + FileTime lastModifiedTime = Files.getLastModifiedTime(layerEntry.getSourceFile()); + if (lastModifiedTime.compareTo(maxLastModifiedTime) > 0) { + maxLastModifiedTime = lastModifiedTime; + } + } + + return maxLastModifiedTime; + } + + /** + * Gets the last modified time from the metadata of a {@link CacheEntry}. + * + * @param cacheEntry the {@link CacheEntry} + * @return the last modified time, if the metadata is present + * @throws IOException if deserialization of the metadata failed + */ + static Optional getLastModifiedTime(CacheEntry cacheEntry) throws IOException { + if (!cacheEntry.getMetadataBlob().isPresent()) { + return Optional.empty(); + } + + Blob metadataBlob = cacheEntry.getMetadataBlob().get(); + return Optional.of(FileTime.from(Instant.parse(Blobs.writeToString(metadataBlob)))); + } + + private LastModifiedTimeMetadata() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java index 04b2344fea..31e901d4bb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java @@ -27,8 +27,10 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.junit.Assert; @@ -104,35 +106,38 @@ private static long sizeOf(Blob blob) throws IOException { private DescriptorDigest layerDiffId1; private long layerSize1; private ImmutableList layerEntries1; - private Blob metadataBlob1; private Blob layerBlob2; private DescriptorDigest layerDigest2; private DescriptorDigest layerDiffId2; private long layerSize2; private ImmutableList layerEntries2; - private Blob metadataBlob2; @Before public void setUp() throws IOException { + Path directory = temporaryFolder.newFolder().toPath(); + Files.createDirectory(directory.resolve("source")); + Files.createFile(directory.resolve("source/file")); + Files.createDirectories(directory.resolve("another/source")); + Files.createFile(directory.resolve("another/source/file")); + layerBlob1 = Blobs.from("layerBlob1"); layerDigest1 = digestOf(compress(layerBlob1)); layerDiffId1 = digestOf(layerBlob1); layerSize1 = sizeOf(compress(layerBlob1)); layerEntries1 = ImmutableList.of( - new LayerEntry(Paths.get("source/file"), AbsoluteUnixPath.get("/extraction/path")), new LayerEntry( - Paths.get("another/source/file"), + directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")), + new LayerEntry( + directory.resolve("another/source/file"), AbsoluteUnixPath.get("/another/extraction/path"))); - metadataBlob1 = Blobs.from("metadata"); layerBlob2 = Blobs.from("layerBlob2"); layerDigest2 = digestOf(compress(layerBlob2)); layerDiffId2 = digestOf(layerBlob2); layerSize2 = sizeOf(compress(layerBlob2)); layerEntries2 = ImmutableList.of(); - metadataBlob2 = Blobs.from("metadata"); } @Test @@ -159,31 +164,36 @@ public void testWriteLayerOnly_retrieveByLayerDigest() } @Test - public void testWriteWithSelectorAndMetadata_retrieveByLayerDigest() + public void testWriteWithLayerEntries_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } @Test - public void testWriteWithSelectorAndMetadata_retrieveByLayerEntries() + public void testWriteWithLayerEntries_retrieveByLayerEntries() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); + + // A source file modification results in the cached layer to be out-of-date and not retrieved. + Files.setLastModifiedTime( + layerEntries1.get(0).getSourceFile(), FileTime.from(Instant.now().plusSeconds(1))); + Assert.assertFalse(cache.retrieve(layerEntries1).isPresent()); } @Test public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1)); - verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2, metadataBlob2)); + verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); + verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2)); verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); verifyIsLayer2WithMetadata(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); @@ -212,7 +222,9 @@ private void verifyIsLayer1NoMetadata(CacheEntry cacheEntry) throws IOException private void verifyIsLayer1WithMetadata(CacheEntry cacheEntry) throws IOException { verifyIsLayer1(cacheEntry); Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); - Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get())); + Assert.assertEquals( + Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries1)), + Blobs.writeToString(cacheEntry.getMetadataBlob().get())); } /** @@ -242,6 +254,8 @@ private void verifyIsLayer2WithMetadata(CacheEntry cacheEntry) throws IOExceptio Assert.assertEquals(layerDiffId2, cacheEntry.getLayerDiffId()); Assert.assertEquals(layerSize2, cacheEntry.getLayerSize()); Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); - Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get())); + Assert.assertEquals( + Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries2)), + Blobs.writeToString(cacheEntry.getMetadataBlob().get())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java new file mode 100644 index 0000000000..7d73dce04f --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.ncache; + +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +/** Tests for {@link LastModifiedTimeMetadata}. */ +public class LastModifiedTimeMetadataTest { + + private static LayerEntry copyFile(Path source, Path destination, FileTime newLastModifiedTime) + throws IOException { + Files.createDirectories(destination.getParent()); + Files.copy(source, destination); + Files.setLastModifiedTime(destination, newLastModifiedTime); + return new LayerEntry(destination, AbsoluteUnixPath.get("/ignored")); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private List layerEntries = new ArrayList<>(); + + @Before + public void setUp() throws IOException, URISyntaxException { + Path originalDirectory = Paths.get(Resources.getResource("layer").toURI()); + Path directory = temporaryFolder.newFolder().toPath(); + + layerEntries.add( + copyFile( + originalDirectory.resolve("a/b/bar"), + directory.resolve("a/b/bar"), + FileTime.fromMillis(1000))); + layerEntries.add( + copyFile( + originalDirectory.resolve("c/cat"), + directory.resolve("c/cat"), + FileTime.fromMillis(2000))); + layerEntries.add( + copyFile( + originalDirectory.resolve("foo"), directory.resolve("foo"), FileTime.fromMillis(1500))); + } + + @Test + public void testGetLastModifiedTime_layerEntries() throws IOException { + Assert.assertEquals( + FileTime.fromMillis(2000), + LastModifiedTimeMetadata.getLastModifiedTime(ImmutableList.copyOf(layerEntries))); + } + + @Test + public void testGetLastModifiedTime_noEntries() throws IOException { + Assert.assertEquals( + FileTime.from(Instant.MIN), + LastModifiedTimeMetadata.getLastModifiedTime(ImmutableList.of())); + } + + @Test + public void testGetLastModifiedTime_cacheEntry() throws IOException { + DescriptorDigest ignored = Mockito.mock(DescriptorDigest.class); + CacheEntry cacheEntry = + DefaultCacheEntry.builder() + .setLayerDigest(ignored) + .setLayerDiffId(ignored) + .setLayerSize(0) + .setLayerBlob(Blobs.from("ignored")) + .setMetadataBlob(Blobs.from(Instant.ofEpochMilli(1000).toString())) + .build(); + Assert.assertEquals( + FileTime.from(Instant.ofEpochMilli(1000)), + LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry).orElseThrow(AssertionError::new)); + } + + @Test + public void testGetLastModifiedTime_cacheEntry_noMetadata() throws IOException { + DescriptorDigest ignored = Mockito.mock(DescriptorDigest.class); + CacheEntry cacheEntry = + DefaultCacheEntry.builder() + .setLayerDigest(ignored) + .setLayerDiffId(ignored) + .setLayerSize(0) + .setLayerBlob(Blobs.from("ignored")) + .build(); + Assert.assertFalse(LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry).isPresent()); + } + + @Test + public void testGenerateMetadata() throws IOException { + Assert.assertEquals( + Instant.ofEpochMilli(2000).toString(), + Blobs.writeToString( + LastModifiedTimeMetadata.generateMetadata(ImmutableList.copyOf(layerEntries)))); + } +} From 3a581b5607be5fee96013d55154bedcb18f03e4e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Sep 2018 13:49:40 -0400 Subject: [PATCH 0234/2020] Gradle release v0.9.11 (#1039) * [Gradle Release Plugin] - pre tag commit: 'v0.9.11-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.9.12-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 44f017a1d2..7210bfc265 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.11-SNAPSHOT +version = 0.9.12-SNAPSHOT From 09844e9950e3073daf15bed8cc4ee0e4c1401b62 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 26 Sep 2018 10:58:04 -0700 Subject: [PATCH 0235/2020] Maven release v0.9.11 (#1040) * preparing release 0.9.11 * 0.9.12-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 38e8c26a25..a1194392a9 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.11-SNAPSHOT + 0.9.12-SNAPSHOT maven-plugin Jib From 2ef1e075a0d03d99da12ddbe82bd1c44436249b7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Sep 2018 14:13:45 -0400 Subject: [PATCH 0236/2020] Start replacing JibLogger calls with Events (#1034) * Start replacing JibLogger calls with Events * Remove JibLogger from ProjectProperties * Fix gradle log messages being out of order --- .../cloud/tools/jib/EmptyJibLogger.java | 35 ------------ ...icationMethodRetrieverIntegrationTest.java | 8 +-- .../registry/BlobCheckerIntegrationTest.java | 8 +-- .../registry/BlobPullerIntegrationTest.java | 8 +-- .../registry/BlobPusherIntegrationTest.java | 6 +-- .../ManifestPullerIntegrationTest.java | 10 ++-- .../ManifestPusherIntegrationTest.java | 10 ++-- .../RegistryAuthenticatorIntegrationTest.java | 6 +-- .../cloud/tools/jib/builder/BuildSteps.java | 14 ++--- .../builder/steps/AuthenticatePushStep.java | 3 +- .../BuildAndCacheApplicationLayerStep.java | 9 ++-- .../jib/builder/steps/FinalizingStep.java | 3 +- .../jib/builder/steps/LoadDockerStep.java | 3 +- .../jib/builder/steps/PullBaseImageStep.java | 32 ++++++----- .../tools/jib/builder/steps/PushBlobStep.java | 5 +- .../jib/builder/steps/PushImageStep.java | 5 +- .../jib/builder/steps/WriteTarFileStep.java | 3 +- .../jib/configuration/BuildConfiguration.java | 12 +++-- .../tools/jib/event/events/LogEvent.java | 22 ++++++++ .../frontend/CredentialRetrieverFactory.java | 39 ++++++++------ .../tools/jib/frontend/MainClassFinder.java | 13 ++--- .../jib/registry/RegistryAuthenticator.java | 18 +++---- .../tools/jib/registry/RegistryClient.java | 25 ++++----- .../jib/registry/RegistryEndpointCaller.java | 25 +++++---- ...BuildAndCacheApplicationLayerStepTest.java | 5 +- .../jib/builder/steps/BuildImageStepTest.java | 3 -- .../RetrieveRegistryCredentialsStepTest.java | 27 +++------- .../CredentialRetrieverFactoryTest.java | 28 +++++----- .../jib/frontend/ExposedPortsParserTest.java | 7 --- .../jib/frontend/MainClassFinderTest.java | 20 +++---- .../jib/registry/RegistryClientTest.java | 6 +-- .../registry/RegistryEndpointCallerTest.java | 49 +++++++++-------- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 5 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../jib/gradle/GradleProjectProperties.java | 15 +----- .../tools/jib/gradle/LogEventHandler.java | 17 ++++-- .../gradle/PluginConfigurationProcessor.java | 5 +- .../gradle/GradleProjectPropertiesTest.java | 4 +- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 ++- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../jib/maven/MavenProjectProperties.java | 11 ---- .../maven/PluginConfigurationProcessor.java | 5 +- .../jib/maven/MavenProjectPropertiesTest.java | 4 +- .../jib/plugins/common/BuildStepsRunner.java | 21 ++++---- .../ConfigurationPropertyValidator.java | 53 +++++++++++-------- .../jib/plugins/common/MainClassResolver.java | 29 +++++----- .../jib/plugins/common/ProjectProperties.java | 4 -- .../plugins/common/BuildStepsRunnerTest.java | 6 +-- .../ConfigurationPropertyValidatorTest.java | 44 +++++++-------- .../plugins/common/MainClassResolverTest.java | 16 +++--- 52 files changed, 363 insertions(+), 359 deletions(-) delete mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java deleted file mode 100644 index 760c7034b2..0000000000 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/EmptyJibLogger.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib; - -public class EmptyJibLogger implements JibLogger { - - @Override - public void error(CharSequence message) {} - - @Override - public void lifecycle(CharSequence message) {} - - @Override - public void warn(CharSequence message) {} - - @Override - public void info(CharSequence message) {} - - @Override - public void debug(CharSequence message) {} -} diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index e4d3441b5d..375a14b1e6 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -25,20 +25,20 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testGetRegistryAuthenticator() throws RegistryAuthenticationFailedException, IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_EMITTER, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(); RegistryClient authorizedRegistryClient = - RegistryClient.factory(BUILD_LOGGER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_EMITTER, "registry.hub.docker.com", "library/busybox") .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index fbe5ee14df..5d38cc8a14 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -29,13 +29,13 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyJibLogger buildLogger = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testCheck_exists() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(buildLogger, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = @@ -50,7 +50,7 @@ public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(buildLogger, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); DescriptorDigest fakeBlobDigest = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 703bcc0d83..162ec06241 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -36,7 +36,7 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -45,7 +45,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls the busybox image. localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -71,7 +71,7 @@ public void testPull_unknownBlob() try { RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index c9334887e5..43e47e110a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.security.DigestException; @@ -30,7 +30,7 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testPush() @@ -43,7 +43,7 @@ public void testPush() "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 8ea84df34a..7339a40cf9 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -30,13 +30,13 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testPull_v21() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -50,7 +50,7 @@ public void testPull_v21() throws IOException, RegistryException, InterruptedExc public void testPull_v22() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_EMITTER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -64,7 +64,7 @@ public void testPull_unknownManifest() localRegistry.pullAndPushToLocal("busybox", "busybox"); try { RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 33e752d21a..3d1d157133 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -18,9 +18,9 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.EmptyJibLogger; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -34,18 +34,18 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_EMITTER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); try { @@ -81,7 +81,7 @@ public void testPush() // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory(BUILD_LOGGER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 2f9502a916..4f332d1e41 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.EmptyJibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -27,7 +27,7 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { - private static final EmptyJibLogger BUILD_LOGGER = new EmptyJibLogger(); + private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; @Test public void testAuthenticate() @@ -36,7 +36,7 @@ public void testAuthenticate() ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - BUILD_LOGGER, + EVENT_EMITTER, dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) .initialize(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index c02263352f..e0b94290b5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.ExecutionException; @@ -149,7 +150,7 @@ public BuildConfiguration getBuildConfiguration() { public void run() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - buildConfiguration.getBuildLogger().lifecycle(""); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("")); try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { try (Caches caches = cachesInitializer.init()) { @@ -168,12 +169,13 @@ public void run() } if (buildConfiguration.getContainerConfiguration() != null) { - buildConfiguration.getBuildLogger().lifecycle(""); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("")); buildConfiguration - .getBuildLogger() - .lifecycle( - "Container entrypoint set to " - + buildConfiguration.getContainerConfiguration().getEntrypoint()); + .getEventEmitter() + .emit( + LogEvent.lifecycle( + "Container entrypoint set to " + + buildConfiguration.getContainerConfiguration().getEntrypoint())); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index e5f86f69cf..744f89ffdc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -34,7 +34,6 @@ import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; -// TODO: This is probably not necessary anymore either. /** * Authenticates push to a target registry using Docker Token Authentication. * @@ -87,7 +86,7 @@ public Authorization call() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getBuildLogger(), + buildConfiguration.getEventEmitter(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 849e4fe1f0..84ceaa83af 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; @@ -98,7 +99,7 @@ public ListenableFuture getFuture() { public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorruptedException { String description = "Building " + layerType + " layer"; - buildConfiguration.getBuildLogger().lifecycle(description + "..."); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle(description + "...")); try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { // Don't build the layer if it exists already. @@ -114,8 +115,10 @@ public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorrupted .writeLayer(new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries())); buildConfiguration - .getBuildLogger() - .debug(description + " built " + cachedLayer.getBlobDescriptor().getDigest()); + .getEventEmitter() + .emit( + LogEvent.debug( + description + " built " + cachedLayer.getBlobDescriptor().getDigest())); return cachedLayer; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index 00a2715ba6..b94de65c76 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -83,7 +84,7 @@ public Void call() throws ExecutionException { Futures.whenAllSucceed(unwrappedDependencies) .call( () -> { - buildConfiguration.getBuildLogger().lifecycle("Finalizing..."); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Finalizing...")); return null; }, listeningExecutorService); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index c9b8ab6ca9..a8507f1d1f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.collect.ImmutableList; @@ -90,7 +91,7 @@ private Void afterPushBaseImageLayerFuturesFuture() buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. - buildConfiguration.getBuildLogger().lifecycle("Loading to Docker daemon..."); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Loading to Docker daemon...")); DockerClient dockerClient = new DockerClient(); dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 8c93660be1..08ee2b0eb8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.Image; @@ -104,11 +105,12 @@ public BaseImageWithAuthorization call() LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, RegistryAuthenticationFailedException { buildConfiguration - .getBuildLogger() - .lifecycle( - "Getting base image " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "..."); + .getEventEmitter() + .emit( + LogEvent.lifecycle( + "Getting base image " + + buildConfiguration.getBaseImageConfiguration().getImage() + + "...")); try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { // First, try with no credentials. @@ -117,11 +119,12 @@ public BaseImageWithAuthorization call() } catch (RegistryUnauthorizedException ex) { buildConfiguration - .getBuildLogger() - .lifecycle( - "The base image requires auth. Trying again for " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "..."); + .getEventEmitter() + .emit( + LogEvent.lifecycle( + "The base image requires auth. Trying again for " + + buildConfiguration.getBaseImageConfiguration().getImage() + + "...")); // If failed, then, retrieve base registry credentials and try with retrieved credentials. // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to @@ -146,16 +149,17 @@ public BaseImageWithAuthorization call() // See https://docs.docker.com/registry/spec/auth/token RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getBuildLogger(), + buildConfiguration.getEventEmitter(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { buildConfiguration - .getBuildLogger() - .error( - "Failed to retrieve authentication challenge for registry that required token authentication"); + .getEventEmitter() + .emit( + LogEvent.error( + "Failed to retrieve authentication challenge for registry that required token authentication")); throw registryUnauthorizedException; } registryAuthorization = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 49852aec90..aa48943c14 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.Futures; @@ -78,8 +79,8 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc // check if the BLOB is available if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { buildConfiguration - .getBuildLogger() - .info("BLOB : " + blobDescriptor + " already exists on registry"); + .getEventEmitter() + .emit(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 5a3fb7ea49..c6b9a7c0bd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -138,7 +139,9 @@ private ListenableFuture afterAllPushed() throws ExecutionException { pushAllTagsFutures.add( listeningExecutorService.submit( () -> { - buildConfiguration.getBuildLogger().info("Tagging with " + tag + "..."); + buildConfiguration + .getEventEmitter() + .emit(LogEvent.info("Tagging with " + tag + "...")); registryClient.pushManifest(manifestTemplate, tag); return null; })); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index cdfa8338c5..9d43bc5fc9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; @@ -92,7 +93,7 @@ private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, I Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Build the image to a tarball - buildConfiguration.getBuildLogger().lifecycle("Building image to tar file..."); + buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Building image to tar file...")); Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 79919f71e5..46fd98f406 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -208,10 +209,11 @@ public BuildConfiguration build() { throw new IllegalStateException("Required fields should not be null"); } if (baseImageConfiguration.getImage().usesDefaultTag()) { - buildLogger.warn( - "Base image '" - + baseImageConfiguration.getImage() - + "' does not use a specific image digest - build may not be reproducible"); + eventEmitter.emit( + LogEvent.warn( + "Base image '" + + baseImageConfiguration.getImage() + + "' does not use a specific image digest - build may not be reproducible")); } return new BuildConfiguration( @@ -390,7 +392,7 @@ public RegistryClient.Factory newTargetImageRegistryClientFactory() { private RegistryClient.Factory newRegistryClientFactory(ImageConfiguration imageConfiguration) { return RegistryClient.factory( - getBuildLogger(), + getEventEmitter(), imageConfiguration.getImageRegistry(), imageConfiguration.getImageRepository()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java index fede8e7d7c..48733f3c8e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.event.events; import com.google.cloud.tools.jib.event.JibEvent; +import com.google.common.annotations.VisibleForTesting; +import java.util.Objects; /** Log message event. */ public class LogEvent implements JibEvent { @@ -89,4 +91,24 @@ public Level getLevel() { public String getMessage() { return message; } + + @VisibleForTesting + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof LogEvent)) { + return false; + } + + LogEvent otherLogEvent = (LogEvent) other; + return level == otherLogEvent.level && message.equals(otherLogEvent.message); + } + + @VisibleForTesting + @Override + public int hashCode() { + return Objects.hash(level, message); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 182ee6d614..43bb5d4d8d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -16,9 +16,10 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; @@ -55,29 +56,29 @@ interface DockerCredentialHelperFactory { * Creates a new {@link CredentialRetrieverFactory} for an image. * * @param imageReference the image the credential are for - * @param logger a logger for logging + * @param eventEmitter an event emitter for emitting log events * @return a new {@link CredentialRetrieverFactory} */ public static CredentialRetrieverFactory forImage( - ImageReference imageReference, JibLogger logger) { - return new CredentialRetrieverFactory(imageReference, logger); + ImageReference imageReference, EventEmitter eventEmitter) { + return new CredentialRetrieverFactory(imageReference, eventEmitter); } - private final JibLogger logger; + private final EventEmitter eventEmitter; private final ImageReference imageReference; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; - private CredentialRetrieverFactory(ImageReference imageReference, JibLogger logger) { - this(imageReference, logger, DockerCredentialHelper::new); + private CredentialRetrieverFactory(ImageReference imageReference, EventEmitter eventEmitter) { + this(imageReference, eventEmitter, DockerCredentialHelper::new); } @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, - JibLogger logger, + EventEmitter eventEmitter, DockerCredentialHelperFactory dockerCredentialHelperFactory) { this.imageReference = imageReference; - this.logger = logger; + this.eventEmitter = eventEmitter; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } @@ -117,14 +118,15 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - logger.info("Checking credentials from " + credentialHelper); + eventEmitter.emit(LogEvent.info("Checking credentials from " + credentialHelper)); try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { - logger.info( - "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper); + eventEmitter.emit( + LogEvent.info( + "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); return Optional.empty(); } catch (IOException ex) { @@ -166,9 +168,9 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - logger.warn(ex.getMessage()); + eventEmitter.emit(LogEvent.warn(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - logger.info(" Caused by: " + ex.getCause().getMessage()); + eventEmitter.emit(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } } @@ -211,12 +213,14 @@ CredentialRetriever dockerConfig( try { Optional dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); if (dockerConfigCredentials.isPresent()) { - logger.info("Using credentials from Docker config for " + imageReference.getRegistry()); + eventEmitter.emit( + LogEvent.info( + "Using credentials from Docker config for " + imageReference.getRegistry())); return dockerConfigCredentials; } } catch (IOException ex) { - logger.info("Unable to parse Docker config"); + eventEmitter.emit(LogEvent.info("Unable to parse Docker config")); } return Optional.empty(); }; @@ -234,6 +238,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) } private void logGotCredentialsFrom(String credentialSource) { - logger.info("Using " + credentialSource + " for " + imageReference.getRegistry()); + eventEmitter.emit( + LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index c57ebe0208..f03113bf9e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -106,17 +107,17 @@ public List getFoundMainClasses() { } private final ImmutableList files; - private final JibLogger buildLogger; + private final EventEmitter eventEmitter; /** * Finds a class with {@code psvm} (see class javadoc) in {@code files}. * * @param files the files to check - * @param buildLogger used for displaying status messages. + * @param eventEmitter used for emitting log events. */ - public MainClassFinder(ImmutableList files, JibLogger buildLogger) { + public MainClassFinder(ImmutableList files, EventEmitter eventEmitter) { this.files = files; - this.buildLogger = buildLogger; + this.eventEmitter = eventEmitter; } /** @@ -179,7 +180,7 @@ private Optional findMainClass(Path file) { } catch (IOException ex) { // Could not read class file. - buildLogger.warn("Could not read file: " + file); + eventEmitter.emit(LogEvent.warn("Could not read file: " + file)); } } catch (NotFoundException ex) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 7de507be14..a45e3bde9c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; @@ -46,7 +46,7 @@ public class RegistryAuthenticator { /** Initializer for {@link RegistryAuthenticator}. */ public static class Initializer { - private final JibLogger buildLogger; + private final EventEmitter eventEmitter; private final String serverUrl; private final String repository; private boolean allowInsecureRegistries = false; @@ -54,12 +54,12 @@ public static class Initializer { /** * Instantiates a new initializer for {@link RegistryAuthenticator}. * - * @param buildLogger the build logger used for printing messages + * @param eventEmitter the event emitter used for emitting log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) */ - private Initializer(JibLogger buildLogger, String serverUrl, String repository) { - this.buildLogger = buildLogger; + private Initializer(EventEmitter eventEmitter, String serverUrl, String repository) { + this.eventEmitter = eventEmitter; this.serverUrl = serverUrl; this.repository = repository; } @@ -82,7 +82,7 @@ public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { public RegistryAuthenticator initialize() throws RegistryAuthenticationFailedException, IOException, RegistryException { try { - return RegistryClient.factory(buildLogger, serverUrl, repository) + return RegistryClient.factory(eventEmitter, serverUrl, repository) .setAllowInsecureRegistries(allowInsecureRegistries) .newRegistryClient() .getRegistryAuthenticator(); @@ -100,14 +100,14 @@ public RegistryAuthenticator initialize() /** * Gets a new initializer for {@link RegistryAuthenticator}. * - * @param buildLogger the build logger used for printing messages + * @param eventEmitter the event emitter used for emitting log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) * @return the new {@link Initializer} */ public static Initializer initializer( - JibLogger buildLogger, String serverUrl, String repository) { - return new Initializer(buildLogger, serverUrl, repository); + EventEmitter eventEmitter, String serverUrl, String repository) { + return new Initializer(eventEmitter, serverUrl, repository); } // TODO: Replace with a WWW-Authenticate header parser. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 1d7482e750..f340301e60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -66,7 +67,7 @@ public RegistryClient setTimer(Timer parentTimer) { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { - private final JibLogger buildLogger; + private final EventEmitter eventEmitter; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; @@ -74,9 +75,9 @@ public static class Factory { @Nullable private Authorization authorization; private Factory( - JibLogger buildLogger, + EventEmitter eventEmitter, RegistryEndpointRequestProperties registryEndpointRequestProperties) { - this.buildLogger = buildLogger; + this.eventEmitter = eventEmitter; this.registryEndpointRequestProperties = registryEndpointRequestProperties; } @@ -121,7 +122,7 @@ public Factory setUserAgentSuffix(@Nullable String userAgentSuffix) { */ public RegistryClient newRegistryClient() { return new RegistryClient( - buildLogger, + eventEmitter, authorization, registryEndpointRequestProperties, allowInsecureRegistries, @@ -157,16 +158,16 @@ private String makeUserAgent() { /** * Creates a new {@link Factory} for building a {@link RegistryClient}. * - * @param buildLogger the build logger used for printing messages + * @param eventEmitter the event emitter used for emitting log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) * @return the new {@link Factory} */ - public static Factory factory(JibLogger buildLogger, String serverUrl, String imageName) { - return new Factory(buildLogger, new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory(EventEmitter eventEmitter, String serverUrl, String imageName) { + return new Factory(eventEmitter, new RegistryEndpointRequestProperties(serverUrl, imageName)); } - private final JibLogger buildLogger; + private final EventEmitter eventEmitter; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -175,18 +176,18 @@ public static Factory factory(JibLogger buildLogger, String serverUrl, String im /** * Instantiate with {@link #factory}. * - * @param buildLogger the build logger used for printing messages + * @param eventEmitter the event emitter used for emitting log events * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( - JibLogger buildLogger, + EventEmitter eventEmitter, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, String userAgent) { - this.buildLogger = buildLogger; + this.eventEmitter = eventEmitter; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; @@ -345,7 +346,7 @@ String getUserAgent() { private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { return new RegistryEndpointCaller<>( - buildLogger, + eventEmitter, userAgent, getApiRouteBase(), registryEndpointProvider, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 6231bcc3e6..5877bc3625 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -19,7 +19,8 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Connection; @@ -58,7 +59,7 @@ private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); } - private final JibLogger logger; + private final EventEmitter eventEmitter; private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -75,7 +76,7 @@ private static boolean isHttpsProtocol(URL url) { /** * Constructs with parameters for making the request. * - * @param logger the build logger used for printing messages + * @param eventEmitter the event emitter used for emitting log events * @param userAgent {@code User-Agent} header to send with the request * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint @@ -85,7 +86,7 @@ private static boolean isHttpsProtocol(URL url) { * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( - JibLogger logger, + EventEmitter eventEmitter, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -94,7 +95,7 @@ private static boolean isHttpsProtocol(URL url) { boolean allowInsecureRegistries) throws MalformedURLException { this( - logger, + eventEmitter, userAgent, apiRouteBase, registryEndpointProvider, @@ -107,7 +108,7 @@ private static boolean isHttpsProtocol(URL url) { @VisibleForTesting RegistryEndpointCaller( - JibLogger logger, + EventEmitter eventEmitter, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -117,7 +118,7 @@ private static boolean isHttpsProtocol(URL url) { Function connectionFactory, @Nullable Function insecureConnectionFactory) throws MalformedURLException { - this.logger = logger; + this.eventEmitter = eventEmitter; this.initialRequestUrl = registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; @@ -170,8 +171,9 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } try { - logger.info( - "Cannot verify server at " + url + ". Attempting again with no TLS verification."); + eventEmitter.emit( + LogEvent.info( + "Cannot verify server at " + url + ". Attempting again with no TLS verification.")); return call(url, getInsecureConnectionFactory()); } catch (SSLPeerUnverifiedException ex) { @@ -183,8 +185,9 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr private T fallBackToHttp(URL url) throws IOException, RegistryException { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); - logger.info( - "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl); + eventEmitter.emit( + LogEvent.info( + "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl)); return call(httpUrl.toURL(), connectionFactory); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 3ac405e252..91856f42a8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -17,13 +17,13 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TestJibLogger; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; @@ -79,6 +79,7 @@ private static LayerConfiguration makeLayerConfiguration( @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private EventEmitter mockEventEmitter; private Path temporaryCacheDirectory; private LayerConfiguration fakeDependenciesLayerConfiguration; @@ -109,7 +110,7 @@ public void setUp() throws IOException, URISyntaxException { EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(new TestJibLogger()); + Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 57360b6855..5134e43331 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; @@ -49,7 +48,6 @@ public class BuildImageStepTest { @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; - @Mock private JibLogger mockBuildLogger; @Mock private PullBaseImageStep mockPullBaseImageStep; @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; @@ -70,7 +68,6 @@ public void setUp() throws DigestException { Paths.get(""), new BlobDescriptor(testDescriptorDigest), testDescriptorDigest), null); - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index ed1c0c3464..a2b92c1974 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -21,10 +21,8 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.DefaultEventEmitter; import com.google.cloud.tools.jib.event.EventEmitter; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; @@ -32,7 +30,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,15 +41,7 @@ @RunWith(MockitoJUnitRunner.class) public class RetrieveRegistryCredentialsStepTest { - private final StringBuilder logMessages = new StringBuilder(); - - // Note that in actual code, the event handler should NOT perform thread unsafe operations like - // here. - private final EventEmitter eventEmitter = - new DefaultEventEmitter( - new EventHandlers() - .add(JibEventType.LOGGING, logEvent -> logMessages.append(logEvent.getMessage()))); - + @Mock private EventEmitter mockEventEmitter; @Mock private ListeningExecutorService mockListeningExecutorService; // TODO: Remove once JibLogger is all replaced by EventEmitter. @Mock private JibLogger mockJibLogger; @@ -90,18 +79,16 @@ public void testCall_none() throws CredentialRetrievalException { mockListeningExecutorService, buildConfiguration) .call()); - Assert.assertThat( - logMessages.toString(), - CoreMatchers.containsString("No credentials could be retrieved for registry baseregistry")); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.info("No credentials could be retrieved for registry baseregistry")); Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration) .call()); - Assert.assertThat( - logMessages.toString(), - CoreMatchers.containsString("No credentials could be retrieved for registry baseregistry")); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.info("No credentials could be retrieved for registry baseregistry")); } @Test @@ -131,7 +118,7 @@ private BuildConfiguration makeFakeBuildConfiguration( ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder(mockJibLogger) - .setEventEmitter(eventEmitter) + .setEventEmitter(mockEventEmitter) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(baseCredentialRetrievers) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index b30d495f0f..e01882ee44 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; @@ -63,7 +64,7 @@ private static DockerCredentialHelperFactory getTestFactory( }; } - @Mock private JibLogger mockJibLogger; + @Mock private EventEmitter mockEventEmitter; @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; @@ -86,7 +87,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("registry", null, null), - mockJibLogger, + mockEventEmitter, getTestFactory( "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); @@ -96,8 +97,8 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { .dockerCredentialHelper(Paths.get("docker-credential-helper")) .retrieve() .orElseThrow(AssertionError::new)); - - Mockito.verify(mockJibLogger).info("Using docker-credential-helper for registry"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.info("Using docker-credential-helper for registry")); } @Test @@ -105,7 +106,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.gcr.io", null, null), - mockJibLogger, + mockEventEmitter, getTestFactory( "something.gcr.io", Paths.get("docker-credential-gcr"), @@ -117,7 +118,8 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { .inferCredentialHelper() .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockJibLogger).info("Using docker-credential-gcr for something.gcr.io"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); } @Test @@ -125,7 +127,7 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.amazonaws.com", null, null), - mockJibLogger, + mockEventEmitter, getTestFactory( "something.amazonaws.com", Paths.get("docker-credential-ecr-login"), @@ -135,15 +137,15 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); - Mockito.verify(mockJibLogger).warn("warning"); - Mockito.verify(mockJibLogger).info(" Caused by: the root cause"); + Mockito.verify(mockEventEmitter).emit(LogEvent.warn("warning")); + Mockito.verify(mockEventEmitter).emit(LogEvent.info(" Caused by: the root cause")); } @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( - ImageReference.of("registry", null, null), mockJibLogger); + ImageReference.of("registry", null, null), mockEventEmitter); Mockito.when(mockDockerConfigCredentialRetriever.retrieve()) .thenReturn(Optional.of(FAKE_CREDENTIALS)); @@ -154,7 +156,7 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException .dockerConfig(mockDockerConfigCredentialRetriever) .retrieve() .orElseThrow(AssertionError::new)); - - Mockito.verify(mockJibLogger).info("Using credentials from Docker config for registry"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.info("Using credentials from Docker config for registry")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index 34258099e2..c3e2b48274 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.Port; import com.google.common.collect.ImmutableList; import java.util.Arrays; @@ -24,16 +23,10 @@ import java.util.List; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link ExposedPortsParser}. */ -@RunWith(MockitoJUnitRunner.class) public class ExposedPortsParserTest { - @Mock private JibLogger mockLogger; - @Test public void testParse() { List goodInputs = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 0b3910583c..0cdc20f878 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.MainClassFinder.Result.Type; import com.google.common.io.Resources; @@ -35,13 +35,13 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { - @Mock private JibLogger mockBuildLogger; + @Mock private EventEmitter mockEventEmitter; @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); @@ -52,7 +52,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -63,7 +63,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @@ -71,7 +71,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { public void testFindMainClass_multiple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertEquals( MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); @@ -84,7 +84,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException public void testFindMainClass_extension() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -95,7 +95,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -106,7 +106,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -117,7 +117,7 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockBuildLogger).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 425d9ed8fe..7365002ccb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; @@ -31,7 +31,7 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { - @Mock private JibLogger buildLogger; + @Mock private EventEmitter eventEmitter; @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; @@ -39,7 +39,7 @@ public class RegistryClientTest { @Before public void setUp() { testRegistryClientFactory = - RegistryClient.factory(buildLogger, "some.server.url", "some image name"); + RegistryClient.factory(eventEmitter, "some.server.url", "some image name"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index bd2533d2ed..c93f1e7486 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,8 +20,9 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -103,7 +104,7 @@ private static HttpResponse mockRedirectHttpResponse(String redirectLocation) th return mockHttpResponse(code307, new HttpHeaders().setLocation(redirectLocation)); } - @Mock private JibLogger mockBuildLogger; + @Mock private EventEmitter mockEventEmitter; @Mock private Connection mockConnection; @Mock private Connection mockInsecureConnection; @Mock private Response mockResponse; @@ -163,9 +164,10 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockBuildLogger) - .info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); } @Test @@ -190,12 +192,14 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockBuildLogger) - .info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); - Mockito.verify(mockBuildLogger) - .info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @Test @@ -216,9 +220,10 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockBuildLogger) - .info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @Test @@ -338,12 +343,14 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockBuildLogger) - .info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification."); - Mockito.verify(mockBuildLogger) - .info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api"); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.info( + "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @Test @@ -559,7 +566,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) private RegistryEndpointCaller createRegistryEndpointCaller( boolean allowInsecure, int port) throws MalformedURLException { return new RegistryEndpointCaller<>( - mockBuildLogger, + mockEventEmitter, "userAgent", (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2507479740..f91126c2c8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -85,7 +85,7 @@ public void buildDocker() throws InvalidImageReferenceException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), - gradleJibLogger, + gradleProjectProperties.getEventEmitter(), getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 36e37ff236..44f7aeabef 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -91,10 +91,11 @@ public void buildImage() throws InvalidImageReferenceException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImage, gradleJibLogger)); + CredentialRetrieverFactory.forImage( + targetImage, gradleProjectProperties.getEventEmitter())); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - gradleJibLogger, + gradleProjectProperties.getEventEmitter(), "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index a4b478be1a..5395db5153 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -111,7 +111,7 @@ public void buildTar() throws InvalidImageReferenceException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), - gradleJibLogger, + gradleProjectProperties.getEventEmitter(), getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 43ab239dd5..59f0523d9a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.event.DefaultEventEmitter; import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.event.EventHandlers; @@ -64,8 +63,6 @@ static GradleProjectProperties getForProject( return new GradleProjectProperties( project, makeEventEmitter(logger), - // TODO: Remove - new GradleJibLogger(logger), GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot)); } catch (IOException ex) { @@ -92,18 +89,13 @@ static War getWarTask(Project project) { private final Project project; private final EventEmitter eventEmitter; - private final GradleJibLogger gradleJibLogger; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( - Project project, - EventEmitter eventEmitter, - GradleJibLogger gradleJibLogger, - JavaLayerConfigurations javaLayerConfigurations) { + Project project, EventEmitter eventEmitter, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; this.eventEmitter = eventEmitter; - this.gradleJibLogger = gradleJibLogger; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -117,11 +109,6 @@ public EventEmitter getEventEmitter() { return eventEmitter; } - @Override - public JibLogger getLogger() { - return gradleJibLogger; - } - @Override public String getPluginName() { return PLUGIN_NAME; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java index e3be7eb72a..6084b6ae2d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java @@ -17,12 +17,19 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.event.events.LogEvent; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Consumer; import org.gradle.api.logging.Logger; /** Handles {@link LogEvent}s by passing to the Gradle {@link Logger}. */ +// We don't care about the return values of the logging futures. +@SuppressWarnings("FutureReturnValueIgnored") class LogEventHandler implements Consumer { + /** This executor keeps all log messages in order. */ + private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final Logger logger; LogEventHandler(Logger logger) { @@ -33,23 +40,23 @@ class LogEventHandler implements Consumer { public void accept(LogEvent logEvent) { switch (logEvent.getLevel()) { case LIFECYCLE: - logger.lifecycle(logEvent.getMessage()); + executorService.submit(() -> logger.lifecycle(logEvent.getMessage())); break; case DEBUG: - logger.debug(logEvent.getMessage()); + executorService.submit(() -> logger.debug(logEvent.getMessage())); break; case ERROR: - logger.error(logEvent.getMessage()); + executorService.submit(() -> logger.error(logEvent.getMessage())); break; case INFO: - logger.info(logEvent.getMessage()); + executorService.submit(() -> logger.info(logEvent.getMessage())); break; case WARN: - logger.warn("warning: " + logEvent.getMessage()); + executorService.submit(() -> logger.warn("warning: " + logEvent.getMessage())); break; default: diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index a1b6159513..e9b2574b8b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -84,10 +84,11 @@ static PluginConfigurationProcessor processCommonConfiguration( + "this on a public network!"); } DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventEmitter())); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - logger, + projectProperties.getEventEmitter(), "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 7666c1eca6..155bd607eb 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -50,7 +50,6 @@ public class GradleProjectPropertiesTest { @Mock private WarPluginConvention mockWarPluginConvection; @Mock private TaskContainer mockTaskContainer; @Mock private EventEmitter mockEventEmitter; - @Mock private GradleJibLogger mockGradleJibLogger; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private Manifest manifest; @@ -68,8 +67,7 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties( - mockProject, mockEventEmitter, mockGradleJibLogger, mockJavaLayerConfigurations); + new GradleProjectProperties(mockProject, mockEventEmitter, mockJavaLayerConfigurations); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 9cf87d17f4..7749ce1868 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -70,7 +70,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenJibLogger, + mavenProjectProperties.getEventEmitter(), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 7efcaf7c0e..697ce26693 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -95,10 +95,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImage, mavenJibLogger)); + CredentialRetrieverFactory.forImage( + targetImage, mavenProjectProperties.getEventEmitter())); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - mavenJibLogger, "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); + mavenProjectProperties.getEventEmitter(), + "jib.to.auth.username", + "jib.to.auth.password", + getTargetImageAuth()); if (optionalToCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( optionalToCredential.get(), "jib-maven-plugin configuration"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 362f074f19..73623355a8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -67,7 +67,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenJibLogger, + mavenProjectProperties.getEventEmitter(), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 58669f9b7c..da805d3924 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.event.DefaultEventEmitter; import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.event.EventHandlers; @@ -68,7 +67,6 @@ static MavenProjectProperties getForProject( return new MavenProjectProperties( project, makeEventEmitter(log), - new MavenJibLogger(log), MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); } catch (IOException ex) { @@ -89,19 +87,15 @@ private static EventEmitter makeEventEmitter(Log log) { private final MavenProject project; private final EventEmitter eventEmitter; - // TODO: Remove - private final MavenJibLogger mavenJibLogger; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, EventEmitter eventEmitter, - MavenJibLogger mavenJibLogger, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; this.eventEmitter = eventEmitter; - this.mavenJibLogger = mavenJibLogger; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -115,11 +109,6 @@ public EventEmitter getEventEmitter() { return eventEmitter; } - @Override - public JibLogger getLogger() { - return mavenJibLogger; - } - @Override public String getPluginName() { return PLUGIN_NAME; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index d1f66487a0..dbcb5bf886 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -99,10 +99,11 @@ static PluginConfigurationProcessor processCommonConfiguration( jibPluginConfiguration.getSettingsDecrypter(), logger); DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init(CredentialRetrieverFactory.forImage(baseImage, logger)); + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventEmitter())); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - logger, + projectProperties.getEventEmitter(), "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 14ffb1aa86..ccb57b7552 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -35,7 +35,6 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; @Mock private EventEmitter mockEventEmitter; - @Mock private MavenJibLogger mockMavenJibLogger; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @@ -49,8 +48,7 @@ public class MavenProjectPropertiesTest { @Before public void setup() { mavenProjectProperties = - new MavenProjectProperties( - mockMavenProject, mockEventEmitter, mockMavenJibLogger, mockJavaLayerConfigurations); + new MavenProjectProperties(mockMavenProject, mockEventEmitter, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index ec67b1af61..a7da47512a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -18,7 +18,6 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; @@ -27,6 +26,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.registry.InsecureRegistryException; @@ -198,14 +199,13 @@ private static String capitalizeFirstLetter(String string) { */ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { try { - // TODO: This logging should be injected via another logging class. - JibLogger buildLogger = buildSteps.getBuildConfiguration().getBuildLogger(); + EventEmitter eventEmitter = buildSteps.getBuildConfiguration().getEventEmitter(); - buildLogger.lifecycle(""); - buildLogger.lifecycle(startupMessage); + eventEmitter.emit(LogEvent.lifecycle("")); + eventEmitter.emit(LogEvent.lifecycle(startupMessage)); // Logs the different source files used. - buildLogger.info("Containerizing application with the following files:"); + eventEmitter.emit(LogEvent.info("Containerizing application with the following files:")); for (LayerConfiguration layerConfiguration : buildSteps.getBuildConfiguration().getLayerConfigurations()) { @@ -213,17 +213,18 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut continue; } - buildLogger.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":"); + eventEmitter.emit( + LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - buildLogger.info("\t\t" + layerEntry.getSourceFile()); + eventEmitter.emit(LogEvent.info("\t\t" + layerEntry.getSourceFile())); } } buildSteps.run(); - buildLogger.lifecycle(""); - buildLogger.lifecycle(successMessage); + eventEmitter.emit(LogEvent.lifecycle("")); + eventEmitter.emit(LogEvent.lifecycle(successMessage)); } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { throw new BuildStepsExecutionException( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 490a2c55ba..8e9e47b626 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -32,7 +33,7 @@ public class ConfigurationPropertyValidator { * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. * - * @param logger the {@link JibLogger} used to print warnings messages + * @param eventEmitter the {@link EventEmitter} used to emit log events * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials @@ -40,7 +41,10 @@ public class ConfigurationPropertyValidator { * {@link Optional#empty} if neither is configured. */ public static Optional getImageCredential( - JibLogger logger, String usernameProperty, String passwordProperty, AuthProperty auth) { + EventEmitter eventEmitter, + String usernameProperty, + String passwordProperty, + AuthProperty auth) { // System property takes priority over build configuration String commandlineUsername = System.getProperty(usernameProperty); String commandlinePassword = System.getProperty(passwordProperty); @@ -51,18 +55,20 @@ public static Optional getImageCredential( // Warn if a system property is missing if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - logger.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods."); + eventEmitter.emit( + LogEvent.warn( + passwordProperty + + " system property is set, but " + + usernameProperty + + " is not; attempting other authentication methods.")); } if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - logger.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods."); + eventEmitter.emit( + LogEvent.warn( + usernameProperty + + " system property is set, but " + + passwordProperty + + " is not; attempting other authentication methods.")); } // Check auth configuration next; warn if they aren't both set @@ -70,15 +76,17 @@ public static Optional getImageCredential( return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.warn( - auth.getUsernamePropertyDescriptor() - + " is missing from build configuration; ignoring auth section."); + eventEmitter.emit( + LogEvent.warn( + auth.getUsernamePropertyDescriptor() + + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.warn( - auth.getPasswordPropertyDescriptor() - + " is missing from build configuration; ignoring auth section."); + eventEmitter.emit( + LogEvent.warn( + auth.getPasswordPropertyDescriptor() + + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } @@ -90,7 +98,7 @@ public static Optional getImageCredential( * {@code project-name:project-version} if target image is not configured * * @param targetImage the configured target image reference - * @param logger the {@link JibLogger} used to show messages + * @param eventEmitter the {@link EventEmitter} used to emit log events * @param projectName the project name, as determined by the plugin * @param projectVersion the project version, as determined by the plugin * @param helpfulSuggestions used for generating the message notifying the user of the generated @@ -102,13 +110,14 @@ public static Optional getImageCredential( */ public static ImageReference getGeneratedTargetDockerTag( @Nullable String targetImage, - JibLogger logger, + EventEmitter eventEmitter, String projectName, String projectVersion, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { if (Strings.isNullOrEmpty(targetImage)) { - logger.lifecycle(helpfulSuggestions.forGeneratedTag(projectName, projectVersion)); + eventEmitter.emit( + LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(projectName, projectVersion))); // Try to parse generated tag to verify that project name and version are valid (throws an // exception if parse fails) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 64039e2612..5f2ec2184b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; @@ -71,7 +72,9 @@ public static String resolveMainClass( Preconditions.checkNotNull(mainClass); if (!isValidJavaClass(mainClass)) { - projectProperties.getLogger().warn("'mainClass' is not a valid Java class : " + mainClass); + projectProperties + .getEventEmitter() + .emit(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); } return mainClass; @@ -94,22 +97,24 @@ static boolean isValidJavaClass(String className) { @Nullable private static String getMainClassFromJar(ProjectProperties projectProperties) { projectProperties - .getLogger() - .info( - "Searching for main class... Add a 'mainClass' configuration to '" - + projectProperties.getPluginName() - + "' to improve build speed."); + .getEventEmitter() + .emit( + LogEvent.info( + "Searching for main class... Add a 'mainClass' configuration to '" + + projectProperties.getPluginName() + + "' to improve build speed.")); return projectProperties.getMainClassFromJar(); } private static String findMainClassInClassFiles(ProjectProperties projectProperties) throws MainClassInferenceException { projectProperties - .getLogger() - .debug( - "Could not find a valid main class specified in " - + projectProperties.getJarPluginName() - + "; attempting to infer main class."); + .getEventEmitter() + .emit( + LogEvent.debug( + "Could not find a valid main class specified in " + + projectProperties.getJarPluginName() + + "; attempting to infer main class.")); ImmutableList classesSourceFiles = projectProperties @@ -120,7 +125,7 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert .collect(ImmutableList.toImmutableList()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(classesSourceFiles, projectProperties.getLogger()).find(); + new MainClassFinder(classesSourceFiles, projectProperties.getEventEmitter()).find(); switch (mainClassFinderResult.getType()) { case MAIN_CLASS_FOUND: diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 7d8e7b8f84..808349efd8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.nio.file.Path; @@ -30,9 +29,6 @@ public interface ProjectProperties { EventEmitter getEventEmitter(); - // TODO: Remove - JibLogger getLogger(); - String getPluginName(); JavaLayerConfigurations getJavaLayerConfigurations(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 9f0c7e105e..e12b573e9f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -18,7 +18,6 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; @@ -26,6 +25,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; @@ -71,7 +71,7 @@ public class BuildStepsRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildSteps mockBuildSteps; - @Mock private JibLogger mockBuildLogger; + @Mock private EventEmitter mockEventEmitter; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @@ -85,7 +85,7 @@ public void setUpMocks() { testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps, "ignored", "ignored"); Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); - Mockito.when(mockBuildConfiguration.getBuildLogger()).thenReturn(mockBuildLogger); + Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( ImmutableList.of( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 2cb08817e9..18e341ec44 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.util.Optional; @@ -32,7 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class ConfigurationPropertyValidatorTest { - @Mock private JibLogger mockLogger; + @Mock private EventEmitter mockEventEmitter; @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; @@ -49,7 +50,7 @@ public void testGetImageAuth() { Credential expected = Credential.basic("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( - mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); @@ -59,17 +60,17 @@ public void testGetImageAuth() { expected = Credential.basic("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); + Mockito.verify(mockEventEmitter, Mockito.never()).emit(LogEvent.warn(Mockito.any())); // Auth completely missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); // Password missing @@ -77,20 +78,20 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockLogger) - .warn("pass is missing from build configuration; ignoring auth section."); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); // Username missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockLogger) - .warn("user is missing from build configuration; ignoring auth section."); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.warn("user is missing from build configuration; ignoring auth section.")); } @Test @@ -114,27 +115,28 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept // Target configured ImageReference result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - "a/b:c", mockLogger, "project-name", "project-version", helpfulSuggestions); + "a/b:c", mockEventEmitter, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockLogger, Mockito.never()).lifecycle(Mockito.any()); + Mockito.verify(mockEventEmitter, Mockito.never()).emit(LogEvent.lifecycle(Mockito.any())); // Target not configured result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockLogger, "project-name", "project-version", helpfulSuggestions); + null, mockEventEmitter, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockLogger) - .lifecycle( - "Tagging image with generated image reference project-name:project-version. If you'd " - + "like to specify a different tag, you can set the to parameter in your " - + "build.txt, or use the --to= commandline flag."); + Mockito.verify(mockEventEmitter) + .emit( + LogEvent.lifecycle( + "Tagging image with generated image reference project-name:project-version. If you'd " + + "like to specify a different tag, you can set the to parameter in your " + + "build.txt, or use the --to= commandline flag.")); // Generated tag invalid try { ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockLogger, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); + null, mockEventEmitter, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); Assert.fail(); } catch (InvalidImageReferenceException ignored) { } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 3dae8b30c5..ac7af2777c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.JibLogger; +import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; @@ -40,7 +41,7 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassResolverTest { - @Mock private JibLogger mockBuildLogger; + @Mock private EventEmitter mockEventEmitter; @Mock private ProjectProperties mockProjectProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @@ -48,7 +49,7 @@ public class MainClassResolverTest { @Before public void setup() { - Mockito.when(mockProjectProperties.getLogger()).thenReturn(mockBuildLogger); + Mockito.when(mockProjectProperties.getEventEmitter()).thenReturn(mockEventEmitter); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) @@ -72,7 +73,8 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -89,7 +91,8 @@ public void testResolveMainClass_multipleInferredWithBackup() .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -125,7 +128,8 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockBuildLogger).warn("'mainClass' is not a valid Java class : ${start-class}"); + Mockito.verify(mockEventEmitter) + .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test From e0064a8c15e2a45b88059f3742e8a57439988933 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Sep 2018 17:30:42 -0400 Subject: [PATCH 0237/2020] Update documentation for 0.9.11 (#936) * Update documentation for 0.9.11 * Update version numbers --- CONTRIBUTING.md | 4 +-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++-- jib-gradle-plugin/README.md | 24 +++++++++------ jib-maven-plugin/CHANGELOG.md | 10 ++++-- jib-maven-plugin/README.md | 32 ++++++++++++-------- 14 files changed, 61 insertions(+), 37 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f66b6308f..305c0755d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,12 +88,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.11-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.12-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.10' + // id 'com.google.cloud.tools.jib' version '0.9.11' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index b54d8353e4..1147bbab0f 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.10' + id 'com.google.cloud.tools.jib' version '0.9.11' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 7b1281e182..b6807d99cc 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.9.10 + 0.9.11 3.8.0 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 629870624b..e21d9ea7c2 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.9.10' + id 'com.google.cloud.tools.jib' version '0.9.11' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 0cf1eef852..805d21b1ec 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.10' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.11' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 58ab252374..cd426ab2ca 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -39,7 +39,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.10 + 0.9.11 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 583a10596b..27d05d5915 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.10' + id 'com.google.cloud.tools.jib' version '0.9.11' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 4e02ce963b..2102950542 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.9.10 + 0.9.11 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index 9d892f8026..f6cf949683 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '0.9.10' + id 'com.google.cloud.tools.jib' version '0.9.11' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index 78537f18bf..e31680e6bf 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.10 + 0.9.11 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index aaf2a9cd5c..9eb71c0ff3 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,12 +5,18 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.11 + +### Added + - `container.environment` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) - `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) - `jib.to.tags` (list) defines additional tags to push to ([#978](https://github.com/GoogleContainerTools/jib/pull/978)) -### Changed - ### Fixed - Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 046ca4f927..b99706709c 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.10' + id 'com.google.cloud.tools.jib' version '0.9.11' } ``` @@ -158,27 +158,28 @@ The plugin provides the `jib` extension for configuration with the following opt Field | Type | Default | Description --- | --- | --- | --- -`from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. +`from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. -`useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. +`useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. `from` is a closure with the following properties: Property | Type | Default | Description --- | --- | --- | --- `image` | `String` | `gcr.io/distroless/java` | The image reference for the base image. -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). +`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `to` is a closure with the following properties: Property | Type | Default | Description --- | --- | --- | --- `image` | `String` | *Required* | The image reference for the target image. This can also be specified via the `--image` command line option. -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). +`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`tags` | `List` | *None* | Additional tags to push to. `auth` is a closure with the following properties (see [Using Specific Credentials](#using-specific-credentials)): @@ -191,14 +192,16 @@ Property | Type Property | Type | Default | Description --- | --- | --- | --- +`appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. +`args` | `List` | *None* | Default main method arguments to run your application with. +`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. +`environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. +`labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `mainClass` | `String` | *Inferred\** | The main class to launch your application from. -`args` | `List` | *None* | Default main method arguments to run your application with. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). -`labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). -`format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. -`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -212,7 +215,7 @@ gradle jib -Djib.httpTimeout=3000 In this configuration, the image: * Is built from a base of `openjdk:alpine` (pulled from Docker Hub) -* Is pushed to `localhost:5000/my-image:built-with-jib` +* Is pushed to `localhost:5000/my-image:built-with-jib`, `localhost:5000/my-image:tag2`, and `localhost:5000/my-image:latest` * Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp * Has two labels (key1:value1 and key2:value2) @@ -226,6 +229,7 @@ jib { to { image = 'localhost:5000/my-image/built-with-jib' credHelper = 'osxkeychain' + tags = ['tag2', 'latest'] } container { jvmFlags = ['-Xms512m', '-Xdebug', '-Xmy:flag=jib-rules'] diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 8413df52ac..c42fe49421 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,13 +5,19 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.11 + +### Added + - `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property) ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) - `` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) - `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) - `` (list) defines additional tags to push to ([#1026](https://github.com/GoogleContainerTools/jib/pull/1026)) -### Changed - ### Fixed - Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 4ab1a81d87..11c161ef2b 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.10:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.11:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.10:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.11:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.10 + 0.9.11 myimage @@ -199,27 +199,29 @@ Extended configuration options provide additional options for customizing the im Field | Type | Default | Description --- | --- | --- | --- -`from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. +`from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. -`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. +`skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. +`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. `from` is an object with the following properties: Property | Type | Default | Description --- | --- | --- | --- `image` | string | `gcr.io/distroless/java` | The image reference for the base image. -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). +`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `to` is an object with the following properties: Property | Type | Default | Description --- | --- | --- | --- `image` | string | *Required* | The image reference for the target image. This can also be specified via the `-Dimage` command line option. -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). `auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). +`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`tags` | list | *None* | Additional tags to push to. `auth` is an object with the following properties (see [Using Specific Credentials](#using-specific-credentials)): @@ -232,14 +234,16 @@ Property | Type Property | Type | Default | Description --- | --- | --- | --- +`appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. +`args` | list | *None* | Default main method arguments to run your application with. +`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. +`environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. +`labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `mainClass` | string | *Inferred\** | The main class to launch the application from. -`args` | list | *None* | Default main method arguments to run your application with. `ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). -`labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). -`format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. -`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -253,7 +257,7 @@ mvn compile jib:build -Djib.httpTimeout=3000 In this configuration, the image: * Is built from a base of `openjdk:alpine` (pulled from Docker Hub) -* Is pushed to `localhost:5000/my-image:built-with-jib` +* Is pushed to `localhost:5000/my-image:built-with-jib`, `localhost:5000/my-image:tag2`, and `localhost:5000/my-image:latest` * Runs by calling `java -Xms512m -Xdebug -Xmy:flag=jib-rules -cp app/libs/*:app/resources:app/classes mypackage.MyApp some args` * Exposes port 1000 for tcp (default), and ports 2000, 2001, 2002, and 2003 for udp * Has two labels (key1:value1 and key2:value2) @@ -267,6 +271,10 @@ In this configuration, the image: localhost:5000/my-image:built-with-jib osxkeychain + + tag2 + latest + From 5316bffb64f51824a3fe3c93ba0ab9b7fd14c787 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Sep 2018 18:54:43 -0400 Subject: [PATCH 0238/2020] Remove deprecated parameters (#1045) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../tools/jib/gradle/BuildImageTask.java | 2 +- .../tools/jib/gradle/DockerContextTask.java | 8 +- .../jib/gradle/GradleProjectProperties.java | 2 +- .../cloud/tools/jib/gradle/JibExtension.java | 73 ------------------- .../gradle/PluginConfigurationProcessor.java | 10 ++- .../jib/gradle/DockerContextTaskTest.java | 2 +- .../tools/jib/gradle/JibExtensionTest.java | 35 --------- .../PluginConfigurationProcessorTest.java | 14 ++-- jib-maven-plugin/CHANGELOG.md | 2 + .../tools/jib/maven/DockerContextMojo.java | 1 - .../jib/maven/JibPluginConfiguration.java | 68 ----------------- .../maven/PluginConfigurationProcessor.java | 1 - .../jib/maven/JibPluginConfigurationTest.java | 40 ---------- 14 files changed, 24 insertions(+), 236 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 9eb71c0ff3..046a3f6e5f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) + ### Fixed ## 0.9.11 diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 44f7aeabef..ef176a5721 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -122,7 +122,7 @@ public void buildImage() throws InvalidImageReferenceException { .setAdditionalTargetImageTags(jibExtension.getTo().getTags()) .setContainerConfiguration( pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .setTargetFormat(jibExtension.getFormat()) + .setTargetFormat(jibExtension.getContainer().getFormat()) .build(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index e922d3774b..8456a571af 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -104,7 +104,6 @@ public void generateDockerContext() { Preconditions.checkNotNull(jibExtension); GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); - jibExtension.handleDeprecatedParameters(gradleJibLogger); JibSystemProperties.checkHttpTimeoutProperty(); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); @@ -118,8 +117,9 @@ public void generateDockerContext() { String mainClass = gradleProjectProperties.getMainClass(jibExtension); entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint( - appRoot, jibExtension.getJvmFlags(), mainClass); - } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { + appRoot, jibExtension.getContainer().getJvmFlags(), mainClass); + } else if (jibExtension.getContainer().getMainClass() != null + || !jibExtension.getContainer().getJvmFlags().isEmpty()) { gradleJibLogger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } @@ -131,7 +131,7 @@ public void generateDockerContext() { new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) .setBaseImage(jibExtension.getBaseImage()) .setEntrypoint(entrypoint) - .setJavaArguments(jibExtension.getArgs()) + .setJavaArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(jibExtension.getExposedPorts()) .setLabels(jibExtension.getLabels()) .generate(Paths.get(targetDir)); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 59f0523d9a..96c2133825 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -147,7 +147,7 @@ public boolean isWarProject() { */ String getMainClass(JibExtension jibExtension) { try { - return MainClassResolver.resolveMainClass(jibExtension.getMainClass(), this); + return MainClassResolver.resolveMainClass(jibExtension.getContainer().getMainClass(), this); } catch (MainClassInferenceException ex) { throw new GradleException(ex.getMessage(), ex); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index b83afea98a..1d4ba4adb0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -16,11 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.File; import java.nio.file.Path; import java.util.Collections; @@ -80,8 +77,6 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; private final Property extraDirectory; - - // TODO: Deprecated parameters; remove these 4 private final ListProperty jvmFlags; private final Property mainClass; private final ListProperty args; @@ -112,45 +107,6 @@ public JibExtension(Project project) { extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath())); } - /** - * Warns about deprecated parameters in use. - * - * @param logger The logger used to print the warnings - */ - void handleDeprecatedParameters(JibLogger logger) { - StringBuilder deprecatedParams = new StringBuilder(); - if (!jvmFlags.get().isEmpty()) { - deprecatedParams.append(" jvmFlags -> container.jvmFlags\n"); - if (container.getJvmFlags().isEmpty()) { - container.setJvmFlags(jvmFlags.get()); - } - } - if (!Strings.isNullOrEmpty(mainClass.getOrNull())) { - deprecatedParams.append(" mainClass -> container.mainClass\n"); - if (Strings.isNullOrEmpty(container.getMainClass())) { - container.setMainClass(mainClass.getOrNull()); - } - } - if (!args.get().isEmpty()) { - deprecatedParams.append(" args -> container.args\n"); - if (container.getArgs().isEmpty()) { - container.setArgs(args.get()); - } - } - if (format.getOrNull() != null) { - deprecatedParams.append(" format -> container.format\n"); - container.setFormat(format.get()); - } - - if (deprecatedParams.length() > 0) { - logger.warn( - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your build.gradle to avoid issues in the future:\n" - + deprecatedParams - + "You may also wrap the parameters in a container{} block."); - } - } - public void from(Action action) { action.execute(from); } @@ -220,41 +176,12 @@ public ContainerParameters getContainer() { return container; } - // TODO: Make @Internal (deprecated) - @Input - @Optional - List getJvmFlags() { - return container.getJvmFlags(); - } - @Internal @Optional Map getEnvironment() { return container.getEnvironment(); } - // TODO: Make @Internal (deprecated) - @Input - @Nullable - @Optional - String getMainClass() { - return container.getMainClass(); - } - - // TODO: Make @Internal (deprecated) - @Input - @Optional - List getArgs() { - return container.getArgs(); - } - - // TODO: Make @Internal (deprecated) - @Input - @Optional - Class getFormat() { - return container.getFormat(); - } - @Internal @Optional List getExposedPorts() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index e9b2574b8b..fb1b98ddbf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -70,7 +70,6 @@ static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension) { static PluginConfigurationProcessor processCommonConfiguration( JibLogger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) throws InvalidImageReferenceException, NumberFormatException { - jibExtension.handleDeprecatedParameters(logger); JibSystemProperties.checkHttpTimeoutProperty(); // TODO: Instead of disabling logging, have authentication credentials be provided @@ -106,15 +105,18 @@ static PluginConfigurationProcessor processCommonConfiguration( String mainClass = projectProperties.getMainClass(jibExtension); entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint( - getAppRootChecked(jibExtension), jibExtension.getJvmFlags(), mainClass); - } else if (jibExtension.getMainClass() != null || !jibExtension.getJvmFlags().isEmpty()) { + getAppRootChecked(jibExtension), + jibExtension.getContainer().getJvmFlags(), + mainClass); + } else if (jibExtension.getContainer().getMainClass() != null + || !jibExtension.getContainer().getJvmFlags().isEmpty()) { logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() .setEntrypoint(entrypoint) .setEnvironment(jibExtension.getEnvironment()) - .setProgramArguments(jibExtension.getArgs()) + .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) .setLabels(jibExtension.getLabels()); if (jibExtension.getUseCurrentTimestamp()) { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 0411598a5b..0577e9021c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -50,7 +50,7 @@ public void setUp() throws IOException { JibExtension jibExtension = Mockito.mock(JibExtension.class); Mockito.when(jibExtension.getContainer()).thenReturn(ContainerParameters); Mockito.when(jibExtension.getExtraDirectoryPath()).thenReturn(projectRoot.getRoot().toPath()); - Mockito.when(jibExtension.getMainClass()).thenReturn("MainClass"); + Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); Mockito.when(jibExtension.getBaseImage()).thenReturn("base image"); Mockito.when(ContainerParameters.getAppRoot()).thenReturn("/app"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 08054b9d64..bfccc9ff51 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -28,17 +27,10 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link JibExtension}. */ -@RunWith(MockitoJUnitRunner.class) public class JibExtensionTest { - @Mock private JibLogger mockLogger; - private JibExtension testJibExtension; @Before @@ -130,31 +122,4 @@ public void testUseOnlyProjectCache() { testJibExtension.setUseOnlyProjectCache(true); Assert.assertTrue(testJibExtension.getUseOnlyProjectCache()); } - - @Test - public void testHandleDeprecatedParameters() { - testJibExtension.handleDeprecatedParameters(mockLogger); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); - - testJibExtension.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); - testJibExtension.setMainClass("mainClass"); - testJibExtension.setArgs(Arrays.asList("arg1", "arg2", "arg3")); - testJibExtension.setFormat(ImageFormat.OCI); - - testJibExtension.handleDeprecatedParameters(mockLogger); - - String expectedOutput = - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your build.gradle to avoid issues in the future:\n" - + " jvmFlags -> container.jvmFlags\n" - + " mainClass -> container.mainClass\n" - + " args -> container.args\n" - + " format -> container.format\n" - + "You may also wrap the parameters in a container{} block."; - Mockito.verify(mockLogger).warn(expectedOutput); - Assert.assertEquals(Arrays.asList("jvmFlag1", "jvmFlag2"), testJibExtension.getJvmFlags()); - Assert.assertEquals("mainClass", testJibExtension.getMainClass()); - Assert.assertEquals(Arrays.asList("arg1", "arg2", "arg3"), testJibExtension.getArgs()); - Assert.assertEquals(OCIManifestTemplate.class, testJibExtension.getFormat()); - } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 3e8bcc2737..24637fe6b7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -35,11 +35,11 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { - @Mock GradleJibLogger mockGradleJibLogger; - @Mock JibExtension mockJibExtension; - @Mock BaseImageParameters mockBaseImageParameters; - @Mock ContainerParameters mockContainerParameters; - @Mock GradleProjectProperties mockProjectProperties; + @Mock private GradleJibLogger mockGradleJibLogger; + @Mock private JibExtension mockJibExtension; + @Mock private BaseImageParameters mockBaseImageParameters; + @Mock private ContainerParameters mockContainerParameters; + @Mock private GradleProjectProperties mockProjectProperties; @Before public void setUp() throws Exception { @@ -89,7 +89,7 @@ public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceExcep Mockito.doReturn(Arrays.asList("custom", "entrypoint")) .when(mockContainerParameters) .getEntrypoint(); - Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockJibExtension).getJvmFlags(); + Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockContainerParameters).getJvmFlags(); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( @@ -106,7 +106,7 @@ public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceExce Mockito.doReturn(Arrays.asList("custom", "entrypoint")) .when(mockContainerParameters) .getEntrypoint(); - Mockito.doReturn("java.util.Object").when(mockJibExtension).getMainClass(); + Mockito.doReturn("java.util.Object").when(mockContainerParameters).getMainClass(); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index c42fe49421..6d5cd24dd0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) + ### Fixed ## 0.9.11 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 2ca26d1b17..ad7038d05a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -62,7 +62,6 @@ public void execute() throws MojoExecutionException { } MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); - handleDeprecatedParameters(mavenJibLogger); try { JibSystemProperties.checkHttpTimeoutProperty(); } catch (NumberFormatException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index d91ce7e276..b82391884e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -16,12 +16,10 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import java.io.File; import java.nio.file.Path; import java.util.Collections; @@ -159,14 +157,6 @@ public static class ContainerParameters { @Parameter private ContainerParameters container = new ContainerParameters(); - @Deprecated @Parameter private List jvmFlags = Collections.emptyList(); - - @Deprecated @Nullable @Parameter private String mainClass; - - @Deprecated @Parameter private List args = Collections.emptyList(); - - @Deprecated @Nullable @Parameter private String format; - @Parameter(defaultValue = "false", required = true) private boolean useOnlyProjectCache; @@ -189,44 +179,6 @@ public static class ContainerParameters { from.auth.setPropertyDescriptors(""); } - /** - * Warns about deprecated parameters in use. - * - * @param logger The logger used to print the warnings - */ - void handleDeprecatedParameters(JibLogger logger) { - StringBuilder deprecatedParams = new StringBuilder(); - if (!jvmFlags.isEmpty()) { - deprecatedParams.append(" -> \n"); - if (container.jvmFlags.isEmpty()) { - container.jvmFlags = jvmFlags; - } - } - if (!Strings.isNullOrEmpty(mainClass)) { - deprecatedParams.append(" -> \n"); - if (Strings.isNullOrEmpty(container.mainClass)) { - container.mainClass = mainClass; - } - } - if (!args.isEmpty()) { - deprecatedParams.append(" -> \n"); - if (container.args.isEmpty()) { - container.args = args; - } - } - if (!Strings.isNullOrEmpty(format)) { - deprecatedParams.append(" -> \n"); - container.format = format; - } - - if (deprecatedParams.length() > 0) { - logger.warn( - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your pom.xml to avoid issues in the future:\n" - + deprecatedParams); - } - } - MavenSession getSession() { return Preconditions.checkNotNull(session); } @@ -329,26 +281,6 @@ SettingsDecrypter getSettingsDecrypter() { return Preconditions.checkNotNull(settingsDecrypter); } - @VisibleForTesting - void setJvmFlags(List jvmFlags) { - this.jvmFlags = jvmFlags; - } - - @VisibleForTesting - void setMainClass(String mainClass) { - this.mainClass = mainClass; - } - - @VisibleForTesting - void setArgs(List args) { - this.args = args; - } - - @VisibleForTesting - void setFormat(String format) { - this.format = format; - } - @VisibleForTesting void setProject(MavenProject project) { this.project = project; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index dbcb5bf886..1303e7491b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -75,7 +75,6 @@ static PluginConfigurationProcessor processCommonConfiguration( JibPluginConfiguration jibPluginConfiguration, MavenProjectProperties projectProperties) throws MojoExecutionException { - jibPluginConfiguration.handleDeprecatedParameters(logger); try { JibSystemProperties.checkHttpTimeoutProperty(); } catch (NumberFormatException ex) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index ae17c93689..b7d00fc71e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -16,24 +16,13 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.JibLogger; -import java.io.File; -import java.nio.file.Paths; -import java.util.Arrays; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link JibPluginConfiguration}. */ -@RunWith(MockitoJUnitRunner.class) public class JibPluginConfigurationTest { - @Mock private JibLogger mockLogger; - private JibPluginConfiguration testPluginConfiguration; @Before @@ -61,33 +50,4 @@ public void testAuthDefaults() { testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); Assert.assertEquals("/app", testPluginConfiguration.getAppRoot()); } - - @Test - public void testHandleDeprecatedParameters() { - testPluginConfiguration.handleDeprecatedParameters(mockLogger); - Mockito.verify(mockLogger, Mockito.never()).warn(Mockito.any()); - - testPluginConfiguration.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); - testPluginConfiguration.setMainClass("mainClass"); - testPluginConfiguration.setArgs(Arrays.asList("arg1", "arg2", "arg3")); - testPluginConfiguration.setFormat("OCI"); - testPluginConfiguration.setExtraDirectory(new File("some/path")); - - testPluginConfiguration.handleDeprecatedParameters(mockLogger); - - String expectedOutput = - "There are deprecated parameters used in the build configuration. Please make the " - + "following changes to your pom.xml to avoid issues in the future:\n" - + " -> \n" - + " -> \n" - + " -> \n" - + " -> \n"; - Mockito.verify(mockLogger).warn(expectedOutput); - Assert.assertEquals( - Arrays.asList("jvmFlag1", "jvmFlag2"), testPluginConfiguration.getJvmFlags()); - Assert.assertEquals("mainClass", testPluginConfiguration.getMainClass()); - Assert.assertEquals(Arrays.asList("arg1", "arg2", "arg3"), testPluginConfiguration.getArgs()); - Assert.assertEquals("OCI", testPluginConfiguration.getFormat()); - Assert.assertEquals(Paths.get("some/path"), testPluginConfiguration.getExtraDirectory()); - } } From b2bd6b4d6ffe5590798cfa93a4e2518cf6d37b76 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 26 Sep 2018 16:31:52 -0700 Subject: [PATCH 0239/2020] Stores temporary cache files under cache directory. (#1037) --- .../jib/filesystem/TemporaryDirectory.java | 7 +++--- .../jib/ncache/DefaultCacheStorageFiles.java | 10 ++++++++ .../jib/ncache/DefaultCacheStorageWriter.java | 23 +++++++++++++++++-- .../filesystem/TemporaryDirectoryTest.java | 6 +++-- .../ncache/DefaultCacheStorageFilesTest.java | 6 +++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java index d4db8d84a2..b55d573fdb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java @@ -32,12 +32,13 @@ public class TemporaryDirectory implements Closeable { private final Path temporaryDirectory; /** - * Creates a new temporary directory. + * Creates a new temporary directory under an existing {@code parentDirectory}. * + * @param parentDirectory the directory to create the temporary directory within * @throws IOException if an I/O exception occurs */ - public TemporaryDirectory() throws IOException { - temporaryDirectory = Files.createTempDirectory(null); + public TemporaryDirectory(Path parentDirectory) throws IOException { + temporaryDirectory = Files.createTempDirectory(parentDirectory, null); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java index ae901d47d9..670718eb1e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java @@ -26,6 +26,7 @@ class DefaultCacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; private static final String METADATA_FILENAME = "metadata"; private static final String SELECTORS_DIRECTORY = "selectors"; + private static final String TEMPORARY_DIRECTORY = "tmp"; /** * Returns whether or not {@code file} is a layer contents file. @@ -140,4 +141,13 @@ Path getLayersDirectory() { Path getLayerDirectory(DescriptorDigest layerDigest) { return getLayersDirectory().resolve(layerDigest.getHash()); } + + /** + * Gets the directory to store temporary files. + * + * @return the directory for temporary files + */ + Path getTemporaryDirectory() { + return cacheDirectory.resolve(TEMPORARY_DIRECTORY); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java index 91de02cd09..2fca700768 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java @@ -25,10 +25,13 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; /** Writes to the default cache storage engine. */ @@ -59,6 +62,10 @@ private WrittenLayer( * @throws IOException if an I/O exception occurs */ private static void moveIfDoesNotExist(Path source, Path destination) throws IOException { + if (Files.exists(destination)) { + return; + } + try { Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE); @@ -75,6 +82,15 @@ private static void moveIfDoesNotExist(Path source, Path destination) throws IOE } catch (FileAlreadyExistsException alsoIgnored) { // Same reasoning + + } catch (DirectoryNotEmptyException ex) { + // The file system cannot rename directories, so we must resort to copying the directory. + Files.createDirectory(destination); + try (Stream sourceFiles = Files.list(source)) { + for (Path sourceFile : sourceFiles.collect(Collectors.toList())) { + Files.copy(sourceFile, destination.resolve(sourceFile.getFileName())); + } + } } } } @@ -108,8 +124,11 @@ CacheEntry write(CacheWrite cacheWrite) throws IOException { // Creates the layers directory if it doesn't exist. Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); - // Creates the temporary directory. - try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + // Creates the temporary directory. The temporary directory must be in the same FileStore as the + // final location for Files.move to work. + Files.createDirectories(defaultCacheStorageFiles.getTemporaryDirectory()); + try (TemporaryDirectory temporaryDirectory = + new TemporaryDirectory(defaultCacheStorageFiles.getTemporaryDirectory())) { Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); // Writes the layer file to the temporary directory. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java index 338aaa53c1..1d41843e04 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java @@ -42,7 +42,8 @@ private static void createFilesInDirectory(Path directory) @Test public void testClose_directoryDeleted() throws IOException, URISyntaxException { - try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + try (TemporaryDirectory temporaryDirectory = + new TemporaryDirectory(temporaryFolder.newFolder().toPath())) { createFilesInDirectory(temporaryDirectory.getDirectory()); temporaryDirectory.close(); @@ -54,7 +55,8 @@ public void testClose_directoryDeleted() throws IOException, URISyntaxException public void testClose_directoryNotDeletedIfMoved() throws IOException, URISyntaxException { Path destinationParent = temporaryFolder.newFolder().toPath(); - try (TemporaryDirectory temporaryDirectory = new TemporaryDirectory()) { + try (TemporaryDirectory temporaryDirectory = + new TemporaryDirectory(temporaryFolder.newFolder().toPath())) { createFilesInDirectory(temporaryDirectory.getDirectory()); Assert.assertFalse(Files.exists(destinationParent.resolve("destination"))); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java index 9324c0c91b..250c50baaa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java @@ -178,4 +178,10 @@ public void testGetLayerDirectory() throws DigestException { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), testDefaultCacheStorageFiles.getLayerDirectory(layerDigest)); } + + @Test + public void testGetTemporaryDirectory() { + Assert.assertEquals( + Paths.get("cache/directory/tmp"), testDefaultCacheStorageFiles.getTemporaryDirectory()); + } } From e77ff6bd7c5d068ca8bc796ca42d143a51c230d1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 27 Sep 2018 10:25:37 -0400 Subject: [PATCH 0240/2020] Replace Timer with timer events (#1043) --- .../com/google/cloud/tools/jib/Timer.java | 88 ------------------- .../cloud/tools/jib/builder/BuildSteps.java | 4 +- .../google/cloud/tools/jib/builder/Timer.java | 1 - .../tools/jib/builder/TimerEventEmitter.java | 29 ++++-- .../builder/steps/AuthenticatePushStep.java | 8 +- .../BuildAndCacheApplicationLayerStep.java | 8 +- .../jib/builder/steps/BuildImageStep.java | 5 +- .../steps/PullAndCacheBaseImageLayerStep.java | 7 +- .../PullAndCacheBaseImageLayersStep.java | 5 +- .../jib/builder/steps/PullBaseImageStep.java | 5 +- .../tools/jib/builder/steps/PushBlobStep.java | 7 +- .../steps/PushContainerConfigurationStep.java | 5 +- .../jib/builder/steps/PushImageStep.java | 5 +- .../jib/builder/steps/PushLayersStep.java | 5 +- .../tools/jib/registry/RegistryClient.java | 38 ++------ .../jib/builder/steps/BuildImageStepTest.java | 3 + .../jib/gradle/GradleProjectProperties.java | 8 +- 17 files changed, 75 insertions(+), 156 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java deleted file mode 100644 index 2b7f7fd0ad..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/Timer.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib; - -import java.io.Closeable; -import javax.annotation.Nullable; - -/** - * Times execution intervals. This is only for testing purposes and will be removed before the first - * release. - */ -public class Timer implements Closeable { - - private final JibLogger buildLogger; - private final int depth; - - @Nullable private String label; - private long startTime = System.nanoTime(); - - public Timer(JibLogger buildLogger, String label) { - this(buildLogger, label, 0); - } - - private Timer(JibLogger buildLogger, String label, int depth) { - this.buildLogger = buildLogger; - this.label = label; - this.depth = depth; - - if (buildLogger != null) { - buildLogger.debug(getTabs().append("TIMING\t").append(label)); - if (depth == 0) { - buildLogger.info("RUNNING\t" + label); - } - } - } - - public Timer subTimer(String label) { - return new Timer(buildLogger, label, depth + 1); - } - - public void lap(@Nullable String label) { - if (this.label == null) { - throw new IllegalStateException("Tried to lap Timer after closing"); - } - if (buildLogger != null) { - double timeInMillis = (System.nanoTime() - startTime) / 1000 / 1000.0; - buildLogger.debug( - getTabs() - .append("TIMED\t") - .append(this.label) - .append(" : ") - .append(timeInMillis) - .append(" ms")); - if (depth == 0) { - buildLogger.info(this.label + " : " + timeInMillis + " ms"); - } - } - this.label = label; - startTime = System.nanoTime(); - } - - private StringBuilder getTabs() { - StringBuilder tabs = new StringBuilder(); - for (int i = 0; i < depth; i++) { - tabs.append("\t"); - } - return tabs; - } - - @Override - public void close() { - lap(null); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index e0b94290b5..b4f373b571 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; @@ -152,7 +151,8 @@ public void run() CacheDirectoryNotOwnedException, CacheDirectoryCreationException { buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("")); - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), description)) { try (Caches caches = cachesInitializer.init()) { Cache baseImageLayersCache = caches.getBaseCache(); Cache applicationLayersCache = caches.getApplicationCache(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java index 85be3e13c0..d3443fdd43 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java @@ -24,7 +24,6 @@ import javax.annotation.Nullable; /** Times code execution intervals. Call {@link #lap} at the end of each interval. */ -// TODO: Replace com.google.cloud.tools.jib.Timer with this. class Timer implements TimerEvent.Timer { private final Clock clock; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java index 756b27478f..0ab3973228 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java @@ -36,6 +36,12 @@ public class TimerEventEmitter implements Closeable { private final Clock clock; private final Timer timer; + /** + * Creates a new {@link TimerEventEmitter}. + * + * @param eventEmitter the {@link EventEmitter} used to emit the {@link TimerEvent}s + * @param description the default description for the {@link TimerEvent}s + */ public TimerEventEmitter(EventEmitter eventEmitter, String description) { this(eventEmitter, description, DEFAULT_CLOCK, null); } @@ -48,7 +54,7 @@ public TimerEventEmitter(EventEmitter eventEmitter, String description) { this.clock = clock; this.timer = new Timer(clock, parentTimer); - emitTimerEvent(State.START, Duration.ZERO); + emitTimerEvent(State.START, Duration.ZERO, description); } /** @@ -63,18 +69,31 @@ public TimerEventEmitter subTimer(String description) { /** * Captures the time since last lap or creation and emits an {@link State#LAP} {@link TimerEvent}. + * + * @see #lap(String) for using a different description */ public void lap() { - emitTimerEvent(State.LAP, timer.lap()); + emitTimerEvent(State.LAP, timer.lap(), description); + } + + /** + * Captures the time since last lap or creation and emits an {@link State#LAP} {@link TimerEvent}. + * + * @param newDescription the description to use instead of the {@link TimerEventEmitter}'s + * description + */ + public void lap(String newDescription) { + emitTimerEvent(State.LAP, timer.lap(), newDescription); } /** Laps and emits an {@link State#FINISHED} {@link TimerEvent} upon close. */ @Override public void close() { - emitTimerEvent(State.FINISHED, timer.lap()); + emitTimerEvent(State.FINISHED, timer.lap(), description); } - private void emitTimerEvent(State state, Duration duration) { - eventEmitter.emit(new TimerEvent(state, timer, duration, timer.getElapsedTime(), description)); + private void emitTimerEvent(State state, Duration duration, String eventDescription) { + eventEmitter.emit( + new TimerEvent(state, timer, duration, timer.getElapsedTime(), eventDescription)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 744f89ffdc..238eb0e983 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; @@ -71,9 +71,9 @@ public ListenableFuture getFuture() { public Authorization call() throws ExecutionException, RegistryAuthenticationFailedException, IOException, RegistryException { - try (Timer ignored = - new Timer( - buildConfiguration.getBuildLogger(), + try (TimerEventEmitter ignored = + new TimerEventEmitter( + buildConfiguration.getEventEmitter(), String.format( DESCRIPTION, buildConfiguration.getTargetImageConfiguration().getImageRegistry()))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 84ceaa83af..78177e7858 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; @@ -48,7 +48,8 @@ static ImmutableList makeList( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, Cache cache) { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = ImmutableList.builderWithExpectedSize(buildConfiguration.getLayerConfigurations().size()); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { @@ -101,7 +102,8 @@ public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorrupted buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle(description + "...")); - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), description)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), description)) { // Don't build the layer if it exists already. Optional optionalCachedLayer = new CacheReader(cache) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 80d5ef141f..810ac220ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -93,7 +93,8 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayersSteps() throws ExecutionException, LayerPropertyNotFoundException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(); Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 6033bd10f4..878b27238d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CacheWriter; @@ -67,8 +67,9 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, RegistryException { - try (Timer ignored = - new Timer(buildConfiguration.getBuildLogger(), String.format(DESCRIPTION, layerDigest))) { + try (TimerEventEmitter ignored = + new TimerEventEmitter( + buildConfiguration.getEventEmitter(), String.format(DESCRIPTION, layerDigest))) { RegistryClient registryClient = buildConfiguration .newBaseImageRegistryClientFactory() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index aeabfd4bc1..029dce2946 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -67,7 +67,8 @@ public ListenableFuture> getFuture @Override public ImmutableList call() throws ExecutionException, LayerPropertyNotFoundException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 08ee2b0eb8..33c8dac7e7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -112,7 +112,8 @@ public BaseImageWithAuthorization call() + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { // First, try with no credentials. try { return new BaseImageWithAuthorization(pullBaseImage(null), null); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index aa48943c14..4448fc28e0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -67,14 +67,13 @@ public ListenableFuture getFuture() { @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { - try (Timer timer = - new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION + blobDescriptor)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); - registryClient.setTimer(timer); // check if the BLOB is available if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index f20b410c4d..c4f0495d10 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; @@ -77,7 +77,8 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { - try (Timer timer = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); Blob containerConfigurationBlob = new ImageToJsonTranslator(image).getContainerConfigurationBlob(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index c6b9a7c0bd..a13e36c9a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -115,7 +115,8 @@ private ListenableFuture> afterPushSteps() throws Executi } private ListenableFuture afterAllPushed() throws ExecutionException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 9ebcb31e52..a9a302b45f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.collect.ImmutableList; @@ -65,7 +65,8 @@ public ListenableFuture>> getFuture() { @Override public ImmutableList> call() throws ExecutionException { - try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { + try (TimerEventEmitter ignored = + new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { ImmutableList> cachedLayerSteps = NonBlockingSteps.get(cachedLayerStepsStep); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index f340301e60..d4ee4151cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,10 +16,9 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.JibLogger; -import com.google.cloud.tools.jib.Timer; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.TimerEventEmitter; import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; @@ -38,32 +37,6 @@ /** Interfaces with a registry. */ public class RegistryClient { - // TODO: Remove - private Timer parentTimer = - new Timer( - new JibLogger() { - @Override - public void debug(CharSequence message) {} - - @Override - public void info(CharSequence message) {} - - @Override - public void warn(CharSequence message) {} - - @Override - public void error(CharSequence message) {} - - @Override - public void lifecycle(CharSequence message) {} - }, - "NULL TIMER"); - - public RegistryClient setTimer(Timer parentTimer) { - this.parentTimer = parentTimer; - return this; - } - /** Factory for creating {@link RegistryClient}s. */ public static class Factory { @@ -297,8 +270,9 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); - try (Timer t = parentTimer.subTimer("pushBlob")) { - try (Timer t2 = t.subTimer("pushBlob POST " + blobDigest)) { + try (TimerEventEmitter timerEventEmitter = new TimerEventEmitter(eventEmitter, "pushBlob")) { + try (TimerEventEmitter timerEventEmitter2 = + timerEventEmitter.subTimer("pushBlob POST " + blobDigest)) { // POST /v2//blobs/uploads/ OR // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} @@ -308,13 +282,13 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String return true; } - t2.lap("pushBlob PATCH " + blobDigest); + timerEventEmitter2.lap("pushBlob PATCH " + blobDigest); // PATCH with BLOB URL putLocation = callRegistryEndpoint(blobPusher.writer(patchLocation)); Preconditions.checkNotNull(putLocation); - t2.lap("pushBlob PUT " + blobDigest); + timerEventEmitter2.lap("pushBlob PUT " + blobDigest); // PUT ?digest={blob.digest} callRegistryEndpoint(blobPusher.committer(putLocation)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 5134e43331..7a1640f5b3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -46,6 +47,7 @@ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { + @Mock private EventEmitter mockEventEmitter; @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; @Mock private PullBaseImageStep mockPullBaseImageStep; @@ -68,6 +70,7 @@ public void setUp() throws DigestException { Paths.get(""), new BlobDescriptor(testDescriptorDigest), testDescriptorDigest), null); + Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 96c2133825..2f1416fe51 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -71,10 +72,13 @@ static GradleProjectProperties getForProject( } private static EventEmitter makeEventEmitter(Logger logger) { + LogEventHandler logEventHandler = new LogEventHandler(logger); return new DefaultEventEmitter( new EventHandlers() - .add(JibEventType.LOGGING, new LogEventHandler(logger)) - .add(JibEventType.TIMING, new TimerEventHandler(logger::debug))); + .add(JibEventType.LOGGING, logEventHandler) + .add( + JibEventType.TIMING, + new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))))); } @Nullable From fd1236759327ad8d04c3127fc330bcdbd87d9ff3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 27 Sep 2018 08:24:02 -0700 Subject: [PATCH 0241/2020] Adds JibContainerBuilder#toBuildConfiguration. (#1000) --- .../tools/jib/api/DockerDaemonImage.java | 16 +- .../tools/jib/api/JibContainerBuilder.java | 30 +++- .../cloud/tools/jib/api/RegistryImage.java | 18 ++- .../cloud/tools/jib/api/SourceImage.java | 7 +- .../google/cloud/tools/jib/api/TarImage.java | 16 +- .../cloud/tools/jib/api/TargetImage.java | 7 +- .../jib/configuration/BuildConfiguration.java | 26 ++++ .../configuration/ContainerConfiguration.java | 26 ++++ .../tools/jib/api/DockerDaemonImageTest.java | 42 ++++++ .../jib/api/JibContainerBuilderTest.java | 139 ++++++++++++++++++ .../tools/jib/api/RegistryImageTest.java | 59 ++++++++ .../cloud/tools/jib/api/TarImageTest.java | 39 +++++ 12 files changed, 411 insertions(+), 14 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index f214ad766d..6070268543 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; @@ -69,10 +70,21 @@ public DockerDaemonImage setDockerExecutable(Path dockerExecutable) { return this; } - ImageReference getImageReference() { - return imageReference; + /** + * Converts into an {@link ImageConfiguration}. For internal use only. + * + * @return an {@link ImageConfiguration} + */ + @Override + public ImageConfiguration toImageConfiguration() { + return ImageConfiguration.builder(imageReference).build(); } + /** + * Gets the path to the {@code docker} CLI. + * + * @return the path to the {@code docker} CLI + */ Path getDockerExecutable() { return dockerExecutable; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 44b6889131..796a7f0b8c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -17,9 +17,12 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; @@ -298,5 +301,30 @@ public JibContainerBuilder addLabel(String key, String value) { return this; } - // TODO: Add containerize(...). + @VisibleForTesting + BuildConfiguration toBuildConfiguration(TargetImage targetImage) { + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(); + + buildConfigurationBuilder + .setBaseImageConfiguration(baseImage.toImageConfiguration()) + .setTargetImageConfiguration(targetImage.toImageConfiguration()) + .setContainerConfiguration(toContainerConfiguration()) + .setLayerConfigurations(layerConfigurations); + + // TODO: Allow users to configure this. + buildConfigurationBuilder.setToolName("jib-core"); + + return buildConfigurationBuilder.build(); + } + + @VisibleForTesting + ContainerConfiguration toContainerConfiguration() { + return ContainerConfiguration.builder() + .setEntrypoint(entrypoint) + .setProgramArguments(programArguments) + .setEnvironment(environment) + .setExposedPorts(ports) + .setLabels(labels) + .build(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index bde4cb636e..45a2c792ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -17,13 +17,13 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -115,11 +115,15 @@ public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetrie return this; } - ImageReference getImageReference() { - return imageReference; - } - - List getCredentialRetrievers() { - return Collections.unmodifiableList(credentialRetrievers); + /** + * Converts into an {@link ImageConfiguration}. For internal use only. + * + * @return an {@link ImageConfiguration} + */ + @Override + public ImageConfiguration toImageConfiguration() { + return ImageConfiguration.builder(imageReference) + .setCredentialRetrievers(credentialRetrievers) + .build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java index ea730bfdc9..af8ec474b0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java @@ -17,5 +17,10 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.ImageConfiguration; + /** Represents a source image for Jib to use (for example, as a base image). */ -interface SourceImage {} +interface SourceImage { + + ImageConfiguration toImageConfiguration(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index d8c08d655f..124abfa61e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; @@ -85,10 +86,21 @@ private TarImage(ImageReference imageReference, Path outputFile) { this.outputFile = outputFile; } - ImageReference getImageReference() { - return imageReference; + /** + * Converts into an {@link ImageConfiguration}. For internal use only. + * + * @return an {@link ImageConfiguration} + */ + @Override + public ImageConfiguration toImageConfiguration() { + return ImageConfiguration.builder(imageReference).build(); } + /** + * Gets the output file to save the tarball archive to. + * + * @return the output file + */ Path getOutputFile() { return outputFile; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java index 2f75631d10..4f52152268 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java @@ -17,5 +17,10 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.configuration.ImageConfiguration; + /** Represents a destination for the Jib-built image. */ -interface TargetImage {} +interface TargetImage { + + ImageConfiguration toImageConfiguration(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 46fd98f406..a998e01476 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -253,6 +253,32 @@ public static Builder builder(JibLogger jibLogger) { return new Builder(jibLogger); } + /** + * Creates a new {@link Builder} to build a {@link BuildConfiguration}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + // TODO: Remove JibLogger and #builder(JibLogger). + return new Builder( + new JibLogger() { + @Override + public void error(CharSequence message) {} + + @Override + public void lifecycle(CharSequence message) {} + + @Override + public void warn(CharSequence message) {} + + @Override + public void info(CharSequence message) {} + + @Override + public void debug(CharSequence message) {} + }); + } + private final JibLogger buildLogger; private final ImageConfiguration baseImageConfiguration; private final ImageConfiguration targetImageConfiguration; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 4e4d3c3207..00cdf3926e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.configuration; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -207,4 +208,29 @@ public ImmutableList getExposedPorts() { public ImmutableMap getLabels() { return labels; } + + @Override + @VisibleForTesting + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ContainerConfiguration)) { + return false; + } + ContainerConfiguration otherContainerConfiguration = (ContainerConfiguration) other; + return creationTime.equals(otherContainerConfiguration.creationTime) + && Objects.equals(entrypoint, otherContainerConfiguration.entrypoint) + && Objects.equals(programArguments, otherContainerConfiguration.programArguments) + && Objects.equals(environmentMap, otherContainerConfiguration.environmentMap) + && Objects.equals(exposedPorts, otherContainerConfiguration.exposedPorts) + && Objects.equals(labels, otherContainerConfiguration.labels); + } + + @Override + @VisibleForTesting + public int hashCode() { + return Objects.hash( + creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java new file mode 100644 index 0000000000..cdf704eef7 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerDaemonImage}. */ +public class DockerDaemonImageTest { + + @Test + public void testToImageConfiguration() throws InvalidImageReferenceException { + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named("docker/daemon/image") + .setDockerExecutable(Paths.get("docker", "executable")); + ImageConfiguration imageConfiguration = dockerDaemonImage.toImageConfiguration(); + + Assert.assertEquals( + ImageReference.parse("docker/daemon/image").toString(), + imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + Assert.assertEquals(Paths.get("docker", "executable"), dockerDaemonImage.getDockerExecutable()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java new file mode 100644 index 0000000000..f6f5554bf1 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link JibContainerBuilder}. */ +@RunWith(MockitoJUnitRunner.class) +public class JibContainerBuilderTest { + + @Mock private LayerConfiguration mockLayerConfiguration1; + @Mock private LayerConfiguration mockLayerConfiguration2; + @Mock private CredentialRetriever mockCredentialRetriever; + + @Test + public void testToContainerConfiguration_set() throws InvalidImageReferenceException { + JibContainerBuilder jibContainerBuilder = + Jib.from("base/image/reference") + .setEntrypoint(Arrays.asList("entry", "point")) + .setEnvironment(ImmutableMap.of("name", "value")) + .setExposedPorts(Arrays.asList(Port.tcp(1234), Port.udp(5678))) + .setLabels(ImmutableMap.of("key", "value")) + .setProgramArguments(Arrays.asList("program", "arguments")); + + ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); + Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); + Assert.assertEquals( + ImmutableMap.of("name", "value"), containerConfiguration.getEnvironmentMap()); + Assert.assertEquals( + Arrays.asList(Port.tcp(1234), Port.udp(5678)), containerConfiguration.getExposedPorts()); + Assert.assertEquals(ImmutableMap.of("key", "value"), containerConfiguration.getLabels()); + Assert.assertEquals( + Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + } + + @Test + public void testToContainerConfiguration_add() { + JibContainerBuilder jibContainerBuilder = + Jib.from(ImageReference.of("base", "image", "reference")) + .setEntrypoint("entry", "point") + .setEnvironment(ImmutableMap.of("name", "value")) + .addEnvironmentVariable("environment", "variable") + .setExposedPorts(Port.tcp(1234), Port.udp(5678)) + .addExposedPort(Port.tcp(1337)) + .setLabels(ImmutableMap.of("key", "value")) + .addLabel("added", "label") + .setProgramArguments("program", "arguments"); + + ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); + Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); + Assert.assertEquals( + ImmutableMap.of("name", "value", "environment", "variable"), + containerConfiguration.getEnvironmentMap()); + Assert.assertEquals( + Arrays.asList(Port.tcp(1234), Port.udp(5678), Port.tcp(1337)), + containerConfiguration.getExposedPorts()); + Assert.assertEquals( + ImmutableMap.of("key", "value", "added", "label"), containerConfiguration.getLabels()); + Assert.assertEquals( + Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + } + + @Test + public void testToBuildConfiguration() + throws InvalidImageReferenceException, CredentialRetrievalException { + RegistryImage baseImage = + RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); + RegistryImage targetImage = + RegistryImage.named(ImageReference.of("gcr.io", "my-project/my-app", null)) + .addCredential("username", "password"); + + JibContainerBuilder jibContainerBuilder = + Jib.from(baseImage) + .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); + BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(targetImage); + + Assert.assertEquals( + jibContainerBuilder.toContainerConfiguration(), + buildConfiguration.getContainerConfiguration()); + + Assert.assertEquals( + baseImage.toImageConfiguration().getImage().toString(), + buildConfiguration.getBaseImageConfiguration().getImage().toString()); + Assert.assertEquals( + 1, buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers().size()); + Assert.assertSame( + mockCredentialRetriever, + buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers().get(0)); + + Assert.assertEquals( + targetImage.toImageConfiguration().getImage().toString(), + buildConfiguration.getTargetImageConfiguration().getImage().toString()); + Assert.assertEquals( + 1, buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers().size()); + Assert.assertEquals( + Credential.basic("username", "password"), + buildConfiguration + .getTargetImageConfiguration() + .getCredentialRetrievers() + .get(0) + .retrieve() + .orElseThrow(AssertionError::new)); + + Assert.assertEquals( + Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2), + buildConfiguration.getLayerConfigurations()); + + Assert.assertEquals("jib-core", buildConfiguration.getToolName()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java new file mode 100644 index 0000000000..745d1a35e9 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link RegistryImage}. */ +@RunWith(MockitoJUnitRunner.class) +public class RegistryImageTest { + + @Mock private CredentialRetriever mockCredentialRetriever; + + @Test + public void testToImageConfiguration() + throws InvalidImageReferenceException, CredentialRetrievalException { + ImageConfiguration imageConfiguration = + RegistryImage.named("registry/image") + .addCredentialRetriever(mockCredentialRetriever) + .addCredential("username", "password") + .toImageConfiguration(); + + Assert.assertEquals( + ImageReference.parse("registry/image").toString(), + imageConfiguration.getImage().toString()); + Assert.assertEquals(2, imageConfiguration.getCredentialRetrievers().size()); + Assert.assertSame(mockCredentialRetriever, imageConfiguration.getCredentialRetrievers().get(0)); + Assert.assertEquals( + Credential.basic("username", "password"), + imageConfiguration + .getCredentialRetrievers() + .get(1) + .retrieve() + .orElseThrow(AssertionError::new)); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java new file mode 100644 index 0000000000..adaa3cecb3 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link TarImage}. */ +public class TarImageTest { + + @Test + public void testToImageConfiguration() throws InvalidImageReferenceException { + TarImage tarImage = TarImage.named("tar/image").saveTo(Paths.get("output/file")); + ImageConfiguration imageConfiguration = tarImage.toImageConfiguration(); + + Assert.assertEquals( + ImageReference.parse("tar/image").toString(), imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + Assert.assertEquals(Paths.get("output/file"), tarImage.getOutputFile()); + } +} From 6f69b50bb28521f706c9a2ffe7011e77d142c81a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 27 Sep 2018 13:09:47 -0400 Subject: [PATCH 0242/2020] Remove JibLogger (#1048) --- .../builder/BuildStepsIntegrationTest.java | 6 +- .../com/google/cloud/tools/jib/JibLogger.java | 36 -------- .../jib/configuration/BuildConfiguration.java | 44 +-------- .../tools/jib/builder/TestJibLogger.java | 52 ----------- .../RetrieveRegistryCredentialsStepTest.java | 5 +- .../configuration/BuildConfigurationTest.java | 9 +- .../tools/jib/gradle/BuildDockerTask.java | 3 +- .../tools/jib/gradle/BuildImageTask.java | 3 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 3 +- .../tools/jib/gradle/DockerContextTask.java | 9 +- .../tools/jib/gradle/GradleJibLogger.java | 90 ------------------- .../gradle/PluginConfigurationProcessor.java | 33 +++++-- .../PluginConfigurationProcessorTest.java | 21 ++--- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 3 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../tools/jib/maven/DockerContextMojo.java | 8 +- .../cloud/tools/jib/maven/MavenJibLogger.java | 63 ------------- .../maven/MavenSettingsServerCredentials.java | 13 ++- .../maven/PluginConfigurationProcessor.java | 15 +++- .../MavenSettingsServerCredentialsTest.java | 13 +-- .../PluginConfigurationProcessorTest.java | 29 +++--- 22 files changed, 105 insertions(+), 359 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 24949eb961..e5a6685e0e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -51,6 +51,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Integration tests for {@link BuildSteps}. */ public class BuildStepsIntegrationTest { @@ -103,7 +105,7 @@ private static void assertDockerInspect(String imageReference) Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); } - private static final TestJibLogger logger = new TestJibLogger(); + private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -296,7 +298,7 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) .build(); - return BuildConfiguration.builder(logger) + return BuildConfiguration.builder() .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfiguration) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java b/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java deleted file mode 100644 index 9783485b18..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/JibLogger.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib; - -/** Logging interface for logging messages during Jib execution. */ -public interface JibLogger { - - void error(CharSequence message); - - /** - * Logs messages as part of normal execution (default log level). - * - * @param message the message to log - */ - void lifecycle(CharSequence message); - - void warn(CharSequence message); - - void info(CharSequence message); - - void debug(CharSequence message); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index a998e01476..2c0576ab8b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.event.EventEmitter; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -58,11 +57,7 @@ public static class Builder { /* No-op EventEmitter. */ }; - private JibLogger buildLogger; - - private Builder(JibLogger buildLogger) { - this.buildLogger = buildLogger; - } + private Builder() {} /** * Sets the base image configuration. @@ -217,7 +212,6 @@ public BuildConfiguration build() { } return new BuildConfiguration( - buildLogger, baseImageConfiguration, targetImageConfiguration, additionalTargetImageTags, @@ -243,43 +237,15 @@ public BuildConfiguration build() { } } - /** - * Creates a new {@link Builder} to build a {@link BuildConfiguration}. - * - * @param jibLogger the logger to log messages during build - * @return a new {@link Builder} - */ - public static Builder builder(JibLogger jibLogger) { - return new Builder(jibLogger); - } - /** * Creates a new {@link Builder} to build a {@link BuildConfiguration}. * * @return a new {@link Builder} */ public static Builder builder() { - // TODO: Remove JibLogger and #builder(JibLogger). - return new Builder( - new JibLogger() { - @Override - public void error(CharSequence message) {} - - @Override - public void lifecycle(CharSequence message) {} - - @Override - public void warn(CharSequence message) {} - - @Override - public void info(CharSequence message) {} - - @Override - public void debug(CharSequence message) {} - }); + return new Builder(); } - private final JibLogger buildLogger; private final ImageConfiguration baseImageConfiguration; private final ImageConfiguration targetImageConfiguration; private final ImmutableSet additionalTargetImageTags; @@ -294,7 +260,6 @@ public void debug(CharSequence message) {} /** Instantiate with {@link #builder}. */ private BuildConfiguration( - JibLogger buildLogger, ImageConfiguration baseImageConfiguration, ImageConfiguration targetImageConfiguration, ImmutableSet additionalTargetImageTags, @@ -306,7 +271,6 @@ private BuildConfiguration( ImmutableList layerConfigurations, String toolName, EventEmitter eventEmitter) { - this.buildLogger = buildLogger; this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; this.additionalTargetImageTags = additionalTargetImageTags; @@ -320,10 +284,6 @@ private BuildConfiguration( this.eventEmitter = eventEmitter; } - public JibLogger getBuildLogger() { - return buildLogger; - } - public ImageConfiguration getBaseImageConfiguration() { return baseImageConfiguration; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java deleted file mode 100644 index f535b58e56..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TestJibLogger.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.JibLogger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Implementation of {@link JibLogger} for testing purposes. */ -public class TestJibLogger implements JibLogger { - - private static final Logger LOGGER = LoggerFactory.getLogger(TestJibLogger.class); - - @Override - public void debug(CharSequence message) { - LOGGER.debug(message.toString()); - } - - @Override - public void info(CharSequence message) { - LOGGER.info(message.toString()); - } - - @Override - public void warn(CharSequence message) { - LOGGER.warn(message.toString()); - } - - @Override - public void error(CharSequence message) { - LOGGER.error(message.toString()); - } - - @Override - public void lifecycle(CharSequence message) { - info(message); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index a2b92c1974..c467906f10 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -43,8 +42,6 @@ public class RetrieveRegistryCredentialsStepTest { @Mock private EventEmitter mockEventEmitter; @Mock private ListeningExecutorService mockListeningExecutorService; - // TODO: Remove once JibLogger is all replaced by EventEmitter. - @Mock private JibLogger mockJibLogger; @Test public void testCall_retrieved() throws CredentialRetrievalException { @@ -117,7 +114,7 @@ private BuildConfiguration makeFakeBuildConfiguration( List targetCredentialRetrievers) { ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); - return BuildConfiguration.builder(mockJibLogger) + return BuildConfiguration.builder() .setEventEmitter(mockEventEmitter) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index b269f4dfa1..2966837231 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.JibLogger; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -93,7 +92,7 @@ public void testBuilder() throws Exception { .setLabels(expectedLabels) .build(); BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(Mockito.mock(JibLogger.class)) + BuildConfiguration.builder() .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setAdditionalTargetImageTags(additionalTargetImageTags) @@ -176,7 +175,7 @@ public void testBuilder_default() { expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .build(); BuildConfiguration buildConfiguration = - BuildConfiguration.builder(Mockito.mock(JibLogger.class)) + BuildConfiguration.builder() .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .build(); @@ -195,7 +194,7 @@ public void testBuilder_default() { public void testBuilder_missingValues() { // Target image is missing try { - BuildConfiguration.builder(Mockito.mock(JibLogger.class)) + BuildConfiguration.builder() .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .build(); @@ -207,7 +206,7 @@ public void testBuilder_missingValues() { // All required fields missing try { - BuildConfiguration.builder(Mockito.mock(JibLogger.class)).build(); + BuildConfiguration.builder().build(); Assert.fail("Build configuration should not be built with missing values"); } catch (IllegalStateException ex) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index f91126c2c8..139feac5bc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -73,7 +73,6 @@ public void buildDocker() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( @@ -92,7 +91,7 @@ public void buildDocker() throws InvalidImageReferenceException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - gradleJibLogger, jibExtension, gradleProjectProperties); + getLogger(), jibExtension, gradleProjectProperties); BuildConfiguration buildConfiguration = pluginConfigurationProcessor diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index ef176a5721..6abe51205e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -72,7 +72,6 @@ public void setTargetImage(String targetImage) { public void buildImage() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( @@ -111,7 +110,7 @@ public void buildImage() throws InvalidImageReferenceException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - gradleJibLogger, jibExtension, gradleProjectProperties); + getLogger(), jibExtension, gradleProjectProperties); BuildConfiguration buildConfiguration = pluginConfigurationProcessor diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5395db5153..1f3ec51431 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -100,7 +100,6 @@ private String getTargetPath() { public void buildTar() throws InvalidImageReferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( @@ -118,7 +117,7 @@ public void buildTar() throws InvalidImageReferenceException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - gradleJibLogger, jibExtension, gradleProjectProperties); + getLogger(), jibExtension, gradleProjectProperties); BuildConfiguration buildConfiguration = pluginConfigurationProcessor diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 8456a571af..33f5e5df09 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -102,10 +102,11 @@ public void setTargetDir(String targetDir) { @TaskAction public void generateDockerContext() { Preconditions.checkNotNull(jibExtension); - - GradleJibLogger gradleJibLogger = new GradleJibLogger(getLogger()); JibSystemProperties.checkHttpTimeoutProperty(); + // TODO: Instead of disabling logging, have authentication credentials be provided + PluginConfigurationProcessor.disableHttpLogging(); + AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( @@ -120,7 +121,7 @@ public void generateDockerContext() { appRoot, jibExtension.getContainer().getJvmFlags(), mainClass); } else if (jibExtension.getContainer().getMainClass() != null || !jibExtension.getContainer().getJvmFlags().isEmpty()) { - gradleJibLogger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + getLogger().warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } try { @@ -136,7 +137,7 @@ public void generateDockerContext() { .setLabels(jibExtension.getLabels()) .generate(Paths.get(targetDir)); - gradleJibLogger.lifecycle("Created Docker context at " + targetDir); + getLogger().lifecycle("Created Docker context at " + targetDir); } catch (InsecureRecursiveDeleteException ex) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java deleted file mode 100644 index 20e6055c8a..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleJibLogger.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.JibLogger; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import org.gradle.api.logging.Logger; -import org.gradle.internal.logging.events.LogEvent; -import org.gradle.internal.logging.events.OutputEventListener; -import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; -import org.slf4j.LoggerFactory; - -/** Implementation of {@link JibLogger} for Gradle plugins. */ -// We don't care about the return values of the logging futures. -@SuppressWarnings("FutureReturnValueIgnored") -// TODO: Replace use with LogEventHandler. -class GradleJibLogger implements JibLogger { - - /** This executor keeps all log messages in order. */ - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - /** - * Disables annoying Apache/Google HTTP client logging. Note that this is a hack and depends on - * internal Gradle classes. - */ - static void disableHttpLogging() { - // Disables Apache HTTP client logging. - OutputEventListenerBackedLoggerContext context = - (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); - OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); - context.setOutputEventListener( - event -> { - LogEvent logEvent = (LogEvent) event; - if (!logEvent.getCategory().contains("org.apache")) { - defaultOutputEventListener.onOutput(event); - } - }); - - // Disables Google HTTP client logging. - java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); - } - - private final Logger logger; - - GradleJibLogger(Logger logger) { - this.logger = logger; - } - - @Override - public void lifecycle(CharSequence message) { - executorService.submit(() -> logger.lifecycle(message.toString())); - } - - @Override - public void debug(CharSequence message) { - executorService.submit(() -> logger.debug(message.toString())); - } - - @Override - public void info(CharSequence message) { - executorService.submit(() -> logger.info(message.toString())); - } - - @Override - public void warn(CharSequence message) { - executorService.submit(() -> logger.warn("warning: " + message)); - } - - @Override - public void error(CharSequence message) { - executorService.submit(() -> logger.error(message.toString())); - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index fb1b98ddbf..cdcee18f6b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.JibLogger; +import com.google.api.client.http.HttpTransport; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -34,7 +34,13 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.logging.Level; import org.gradle.api.GradleException; +import org.gradle.api.logging.Logger; +import org.gradle.internal.logging.events.LogEvent; +import org.gradle.internal.logging.events.OutputEventListener; +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; +import org.slf4j.LoggerFactory; /** Configures and provides builders for the image building tasks. */ class PluginConfigurationProcessor { @@ -56,6 +62,24 @@ static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension) { } } + /** Disables annoying Apache HTTP client logging. */ + static void disableHttpLogging() { + // Disables Apache HTTP client logging. + OutputEventListenerBackedLoggerContext context = + (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); + OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); + context.setOutputEventListener( + event -> { + LogEvent logEvent = (LogEvent) event; + if (!logEvent.getCategory().contains("org.apache")) { + defaultOutputEventListener.onOutput(event); + } + }); + + // Disables Google HTTP client logging. + java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); + } + /** * Sets up {@link BuildConfiguration} that is common among the image building tasks. This includes * setting up the base image reference/authorization, container configuration, cache @@ -68,13 +92,12 @@ static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension) { * @throws InvalidImageReferenceException if parsing the base image configuration fails */ static PluginConfigurationProcessor processCommonConfiguration( - JibLogger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) + Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) throws InvalidImageReferenceException, NumberFormatException { JibSystemProperties.checkHttpTimeoutProperty(); // TODO: Instead of disabling logging, have authentication credentials be provided - GradleJibLogger.disableHttpLogging(); - + disableHttpLogging(); ImageReference baseImage = ImageReference.parse(jibExtension.getBaseImage()); if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { @@ -126,7 +149,7 @@ static PluginConfigurationProcessor processCommonConfiguration( } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(logger) + BuildConfiguration.builder() .setToolName(GradleProjectProperties.TOOL_NAME) .setEventEmitter(projectProperties.getEventEmitter()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 24637fe6b7..a38d7fc99f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import org.gradle.api.GradleException; +import org.gradle.api.logging.Logger; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -35,7 +36,7 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { - @Mock private GradleJibLogger mockGradleJibLogger; + @Mock private Logger mockLogger; @Mock private JibExtension mockJibExtension; @Mock private BaseImageParameters mockBaseImageParameters; @Mock private ContainerParameters mockContainerParameters; @@ -61,12 +62,12 @@ public void setUp() throws Exception { public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockGradleJibLogger, mockJibExtension, mockProjectProperties); + mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), configuration.getEntrypoint()); - Mockito.verifyZeroInteractions(mockGradleJibLogger); + Mockito.verifyZeroInteractions(mockLogger); } @Test @@ -77,11 +78,11 @@ public void testEntrypoint() throws InvalidImageReferenceException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockGradleJibLogger, mockJibExtension, mockProjectProperties); + mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verifyZeroInteractions(mockGradleJibLogger); + Mockito.verifyZeroInteractions(mockLogger); } @Test @@ -93,11 +94,11 @@ public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceExcep PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockGradleJibLogger, mockJibExtension, mockProjectProperties); + mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verify(mockGradleJibLogger) + Mockito.verify(mockLogger) .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } @@ -110,11 +111,11 @@ public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceExce PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockGradleJibLogger, mockJibExtension, mockProjectProperties); + mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verify(mockGradleJibLogger) + Mockito.verify(mockLogger) .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } @@ -124,7 +125,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageRefer PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockGradleJibLogger, mockJibExtension, mockProjectProperties); + mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 7749ce1868..0d4a850de1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -58,7 +58,6 @@ public void execute() throws MojoExecutionException { HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); @@ -77,7 +76,7 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - mavenJibLogger, this, mavenProjectProperties); + getLog(), this, mavenProjectProperties); BuildConfiguration buildConfiguration = pluginConfigurationProcessor diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 697ce26693..2090b3ffb4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -81,14 +81,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { "mvn compile jib:build -Dimage=")); } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - mavenJibLogger, this, mavenProjectProperties); + getLog(), this, mavenProjectProperties); ImageReference targetImage = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 73623355a8..65ae04f9ad 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -55,7 +55,6 @@ public void execute() throws MojoExecutionException { return; } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); @@ -74,7 +73,7 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - mavenJibLogger, this, mavenProjectProperties); + getLog(), this, mavenProjectProperties); BuildConfiguration buildConfiguration = pluginConfigurationProcessor diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index ad7038d05a..a5374d2690 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -61,13 +61,15 @@ public void execute() throws MojoExecutionException { return; } - MavenJibLogger mavenJibLogger = new MavenJibLogger(getLog()); try { JibSystemProperties.checkHttpTimeoutProperty(); } catch (NumberFormatException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } + // TODO: Instead of disabling logging, have authentication credentials be provided + PluginConfigurationProcessor.disableHttpLogging(); + Preconditions.checkNotNull(targetDir); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); @@ -80,7 +82,7 @@ public void execute() throws MojoExecutionException { entrypoint = JavaEntrypointConstructor.makeDefaultEntrypoint(appRoot, getJvmFlags(), mainClass); } else if (getMainClass() != null || !getJvmFlags().isEmpty()) { - mavenJibLogger.warn(" and are ignored when is specified"); + getLog().warn(" and are ignored when is specified"); } try { @@ -96,7 +98,7 @@ public void execute() throws MojoExecutionException { .setLabels(getLabels()) .generate(Paths.get(targetDir)); - mavenJibLogger.lifecycle("Created Docker context at " + targetDir); + getLog().info("Created Docker context at " + targetDir); } catch (InsecureRecursiveDeleteException ex) { throw new MojoExecutionException( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java deleted file mode 100644 index 47eced95ab..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenJibLogger.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.JibLogger; -import org.apache.maven.plugin.logging.Log; - -/** Implementation of {@link JibLogger} for Maven plugins. */ -class MavenJibLogger implements JibLogger { - - /** Disables annoying Apache HTTP client logging. */ - static void disableHttpLogging() { - System.setProperty( - "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); - } - - private final Log log; - - MavenJibLogger(Log log) { - this.log = log; - } - - @Override - public void debug(CharSequence charSequence) { - log.debug(charSequence); - } - - @Override - public void info(CharSequence charSequence) { - // Use lifecycle for progress-indicating messages. - debug(charSequence); - } - - @Override - public void warn(CharSequence charSequence) { - log.warn(charSequence); - } - - @Override - public void error(CharSequence charSequence) { - log.error(charSequence); - } - - @Override - public void lifecycle(CharSequence message) { - log.info(message); - } -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 1bbc8cd15e..b60dd85080 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -23,6 +23,7 @@ import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -57,22 +58,20 @@ static boolean isEncrypted(String password) { private final Settings settings; @Nullable private final SettingsDecrypter settingsDecrypter; - private final MavenJibLogger mavenJibLogger; + private final Log log; /** * Create new instance. * * @param settings the Maven settings object * @param settingsDecrypter the Maven decrypter component - * @param mavenJibLogger the Maven build log + * @param log the Maven build logger */ MavenSettingsServerCredentials( - Settings settings, - @Nullable SettingsDecrypter settingsDecrypter, - MavenJibLogger mavenJibLogger) { + Settings settings, @Nullable SettingsDecrypter settingsDecrypter, Log log) { this.settings = settings; this.settingsDecrypter = settingsDecrypter; - this.mavenJibLogger = mavenJibLogger; + this.log = log; } /** @@ -112,7 +111,7 @@ Optional retrieve(@Nullable String registry) throws MojoExecutionExc registryServer = result.getServer(); } } else if (isEncrypted(registryServer.getPassword())) { - mavenJibLogger.warn( + log.warn( "Server password for registry " + registry + " appears to be encrypted, but there is no decrypter available"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 1303e7491b..bd13957b7f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; /** Configures and provides builders for the image building goals. */ class PluginConfigurationProcessor { @@ -58,6 +59,13 @@ static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfig } } + /** Disables annoying Apache HTTP client logging. */ + static void disableHttpLogging() { + System.setProperty( + "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); + } + /** * Sets up {@link BuildConfiguration} that is common among the image building goals. This includes * setting up the base image reference/authorization, container configuration, cache @@ -71,7 +79,7 @@ static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfig * @throws MojoExecutionException if the http timeout system property is misconfigured */ static PluginConfigurationProcessor processCommonConfiguration( - MavenJibLogger logger, + Log logger, JibPluginConfiguration jibPluginConfiguration, MavenProjectProperties projectProperties) throws MojoExecutionException { @@ -82,8 +90,7 @@ static PluginConfigurationProcessor processCommonConfiguration( } // TODO: Instead of disabling logging, have authentication credentials be provided - MavenJibLogger.disableHttpLogging(); - + disableHttpLogging(); ImageReference baseImage = parseImageReference(jibPluginConfiguration.getBaseImage(), "from"); // Checks Maven settings for registry credentials. @@ -149,7 +156,7 @@ static PluginConfigurationProcessor processCommonConfiguration( } BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder(logger) + BuildConfiguration.builder() .setToolName(MavenProjectProperties.TOOL_NAME) .setEventEmitter(projectProperties.getEventEmitter()) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index cd2a4f0a58..54eb88dd92 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -39,14 +40,14 @@ public class MavenSettingsServerCredentialsTest { @Mock private Settings mockSettings; @Mock private Server mockServer1; - @Mock private MavenJibLogger mockLogger; + @Mock private Log mockLog; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; @Before public void setUp() { testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, null, mockLogger); + new MavenSettingsServerCredentials(mockSettings, null, mockLog); } @Test @@ -62,7 +63,7 @@ public void testRetrieve_found() throws MojoExecutionException { Assert.assertEquals( Credential.basic("server1 username", "server1 password"), optionalCredential.get()); - Mockito.verifyZeroInteractions(mockLogger); + Mockito.verifyZeroInteractions(mockLog); } @Test @@ -86,7 +87,7 @@ public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionExcep Assert.assertTrue(optionalCredential.isPresent()); Assert.assertEquals( Credential.basic("server1 username", "{COQLCE6DU6GtcS5P=}"), optionalCredential.get()); - Mockito.verify(mockLogger) + Mockito.verify(mockLog) .warn( "Server password for registry server1 appears to be encrypted, " + "but there is no decrypter available"); @@ -102,7 +103,7 @@ public void testRetrieve_withDecrypter_success() throws MojoExecutionException { SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger); + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLog); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); @@ -135,7 +136,7 @@ public void testRetrieve_withDecrypter_failure() { SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger); + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLog); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index f8410ed79f..ee3b33e266 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -24,6 +24,7 @@ import java.util.Collections; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.settings.Settings; import org.junit.Assert; import org.junit.Before; @@ -37,11 +38,11 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { - @Mock MavenJibLogger mockMavenJibLogger; - @Mock JibPluginConfiguration mockJibPluginConfiguration; - @Mock MavenProjectProperties mockProjectProperties; - @Mock MavenSession mockMavenSession; - @Mock Settings mockMavenSettings; + @Mock private Log mockLog; + @Mock private JibPluginConfiguration mockJibPluginConfiguration; + @Mock private MavenProjectProperties mockProjectProperties; + @Mock private MavenSession mockMavenSession; + @Mock private Settings mockMavenSettings; @Before public void setUp() throws Exception { @@ -69,12 +70,12 @@ public void setUp() throws Exception { public void testPluginConfigurationProcessor_defaults() throws MojoExecutionException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + mockLog, mockJibPluginConfiguration, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), configuration.getEntrypoint()); - Mockito.verifyZeroInteractions(mockMavenJibLogger); + Mockito.verifyZeroInteractions(mockLog); } @Test @@ -85,11 +86,11 @@ public void testEntrypoint() throws MojoExecutionException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + mockLog, mockJibPluginConfiguration, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verifyZeroInteractions(mockMavenJibLogger); + Mockito.verifyZeroInteractions(mockLog); } @Test @@ -101,11 +102,11 @@ public void testEntrypoint_warningOnJvmFlags() throws MojoExecutionException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + mockLog, mockJibPluginConfiguration, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verify(mockMavenJibLogger) + Mockito.verify(mockLog) .warn(" and are ignored when is specified"); } @@ -118,11 +119,11 @@ public void testEntrypoint_warningOnMainclass() throws MojoExecutionException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + mockLog, mockJibPluginConfiguration, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); - Mockito.verify(mockMavenJibLogger) + Mockito.verify(mockLog) .warn(" and are ignored when is specified"); } @@ -132,7 +133,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws MojoExecutionExce PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - mockMavenJibLogger, mockJibPluginConfiguration, mockProjectProperties); + mockLog, mockJibPluginConfiguration, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); Assert.assertEquals( From e60c485e43513090d968a8f87a3ec528f3f3c1ce Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 27 Sep 2018 13:53:30 -0400 Subject: [PATCH 0243/2020] Remove recursion from files task (#1052) --- .../cloud/tools/jib/gradle/FilesTask.java | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java index d414879cbd..9a4e5347c1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java @@ -20,6 +20,8 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashSet; import java.util.Set; import javax.annotation.Nullable; @@ -96,25 +98,35 @@ private static void printProjectFiles(Project project) { } /** - * Recursive function for printing out a project's artifacts. Calls itself when it encounters a - * project dependency. + * Collects a project's project dependencies, including all transitive project dependencies. * - * @param project the project to list the artifacts for - * @param projectDependenciesResult the set of project dependencies encountered. When a project - * dependency is encountered, it is added to this set. + * @param project the project to find the project dependencies for + * @return the set of project dependencies */ - private static void findProjectDependencies( - Project project, Set projectDependenciesResult) { - for (Configuration configuration : - project.getConfigurations().getByName("runtime").getHierarchy()) { - for (Dependency dependency : configuration.getDependencies()) { - if (dependency instanceof ProjectDependency) { - projectDependenciesResult.add((ProjectDependency) dependency); - findProjectDependencies( - ((ProjectDependency) dependency).getDependencyProject(), projectDependenciesResult); + private static Set findProjectDependencies(Project project) { + Set projectDependencies = new HashSet<>(); + Deque projects = new ArrayDeque<>(); + projects.push(project); + + while (!projects.isEmpty()) { + Project currentProject = projects.pop(); + + // Search through all dependencies + for (Configuration configuration : + currentProject.getConfigurations().getByName("runtime").getHierarchy()) { + for (Dependency dependency : configuration.getDependencies()) { + if (dependency instanceof ProjectDependency) { + // If this is a project dependency, save it + ProjectDependency projectDependency = (ProjectDependency) dependency; + if (!projectDependencies.contains(projectDependency)) { + projects.push(projectDependency.getDependencyProject()); + projectDependencies.add(projectDependency); + } + } } } } + return projectDependencies; } @Nullable private JibExtension jibExtension; @@ -142,8 +154,7 @@ public void listFiles() { } // Find project dependencies - Set projectDependencies = new HashSet<>(); - findProjectDependencies(project, projectDependencies); + Set projectDependencies = findProjectDependencies(project); Set projectDependencyJars = new HashSet<>(); for (ProjectDependency projectDependency : projectDependencies) { From f2be28e15eecb9249a3182a005087a37dce01dd9 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 27 Sep 2018 11:36:21 -0700 Subject: [PATCH 0244/2020] Changes CacheWrite into UncompressedCacheWrite and adds ability to write compressed layer blob. (#1046) --- .../google/cloud/tools/jib/ncache/Cache.java | 26 ++-- .../cloud/tools/jib/ncache/CacheEntry.java | 4 +- .../cloud/tools/jib/ncache/CacheStorage.java | 23 ++- .../tools/jib/ncache/DefaultCacheStorage.java | 10 +- .../jib/ncache/DefaultCacheStorageFiles.java | 13 ++ .../jib/ncache/DefaultCacheStorageWriter.java | 143 +++++++++++++++--- ...Write.java => UncompressedCacheWrite.java} | 49 ++---- .../cloud/tools/jib/ncache/CacheTest.java | 16 +- .../tools/jib/ncache/CacheWriteTest.java | 52 ------- .../ncache/DefaultCacheStorageWriterTest.java | 50 +++--- 10 files changed, 223 insertions(+), 163 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/ncache/{CacheWrite.java => UncompressedCacheWrite.java} (57%) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java index 80eeeacdbe..9274f730ba 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java @@ -57,31 +57,33 @@ private Cache(CacheStorage cacheStorage) { } /** - * Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList)} to - * include a selector and metadata. + * Saves a cache entry with a compressed layer {@link Blob}. Use {@link + * #writeUncompressedLayer(Blob, ImmutableList)} to save a cache entry with an uncompressed layer + * {@link Blob} and include a selector and metadata. * - * @param layerBlob the layer {@link Blob} + * @param compressedLayerBlob the compressed layer {@link Blob} * @return the {@link CacheEntry} for the written layer * @throws IOException if an I/O exception occurs */ - public CacheEntry write(Blob layerBlob) throws IOException { - return cacheStorage.write(CacheWrite.layerOnly(layerBlob)); + public CacheEntry writeCompressedLayer(Blob compressedLayerBlob) throws IOException { + return cacheStorage.write(compressedLayerBlob); } /** - * Saves a cache entry with a layer {@link Blob}, an additional selector digest, and a metadata - * {@link Blob}. Use {@link #write(Blob)} to save only a layer {@link Blob}. + * Saves a cache entry with an uncompressed layer {@link Blob}, an additional selector digest, and + * a metadata {@link Blob}. Use {@link #writeCompressedLayer(Blob)} to save a compressed layer + * {@link Blob}. * - * @param layerBlob the layer {@link Blob} + * @param uncompressedLayerBlob the layer {@link Blob} * @param layerEntries the layer entries that make up the layer * @return the {@link CacheEntry} for the written layer and metadata * @throws IOException if an I/O exception occurs */ - public CacheEntry write(Blob layerBlob, ImmutableList layerEntries) - throws IOException { + public CacheEntry writeUncompressedLayer( + Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { return cacheStorage.write( - CacheWrite.withSelectorAndMetadata( - layerBlob, + new UncompressedCacheWrite( + uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries), LastModifiedTimeMetadata.generateMetadata(layerEntries))); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java index 816e866457..c30469c780 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java @@ -58,8 +58,8 @@ public interface CacheEntry { /** * Gets the optional metadata blob for the layer. The metadata is in the same format as supplied - * when writing to the cache with {@link CacheWrite}. This {@link Blob} should be able to be used - * multiple times. + * when writing to the cache with {@link UncompressedCacheWrite}. This {@link Blob} should be able + * to be used multiple times. * * @return the metadata {@link Blob} */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java index 0262cd2df6..7bb0286870 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.util.Optional; @@ -24,8 +25,9 @@ /** * Interface for queries to a cache storage engine. * - *

The cache storage engine stores layer data as {@link CacheWrite}s. These entries are read out - * as {@link CacheEntry}s. Cache entries can be retrieved by the layer digest. + *

The cache storage engine stores layer data in compressed form, along with optional metadata. + * These entries are read out as {@link CacheEntry}s. Cache entries can be retrieved by the layer + * digest. * *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). * The selectors do not need to be unique. An example of a selector could be the digest of the list @@ -36,13 +38,22 @@ interface CacheStorage { /** - * Saves the {@link CacheWrite}. + * Saves the {@link UncompressedCacheWrite}. * - * @param cacheWrite the {@link CacheWrite} - * @return the {@link CacheEntry} for the written {@link CacheWrite} + * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} + * @return the {@link CacheEntry} for the written {@link UncompressedCacheWrite} * @throws IOException if an I/O exception occurs */ - CacheEntry write(CacheWrite cacheWrite) throws IOException; + CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException; + + /** + * Saves a compressed layer {@link Blob}. + * + * @param compressedLayerBlob the compressed layer {@link Blob} + * @return the {@link CacheEntry} for the written layer + * @throws IOException if an I/O exception occurs + */ + CacheEntry write(Blob compressedLayerBlob) throws IOException; /** * Fetches all the layer digests stored. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java index 0a826c4191..9a0ecdc3e1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.ncache; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.nio.file.Path; @@ -65,8 +66,13 @@ private DefaultCacheStorage(DefaultCacheStorageFiles defaultCacheStorageFiles) { } @Override - public CacheEntry write(CacheWrite cacheWrite) throws IOException { - return defaultCacheStorageWriter.write(cacheWrite); + public CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { + return defaultCacheStorageWriter.write(uncompressedCacheWrite); + } + + @Override + public CacheEntry write(Blob compressedLayerBlob) throws IOException { + return defaultCacheStorageWriter.write(compressedLayerBlob); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java index 670718eb1e..af12046bc2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java @@ -27,6 +27,7 @@ class DefaultCacheStorageFiles { private static final String METADATA_FILENAME = "metadata"; private static final String SELECTORS_DIRECTORY = "selectors"; private static final String TEMPORARY_DIRECTORY = "tmp"; + private static final String TEMPORARY_LAYER_FILE_NAME = ".tmp.layer"; /** * Returns whether or not {@code file} is a layer contents file. @@ -150,4 +151,16 @@ Path getLayerDirectory(DescriptorDigest layerDigest) { Path getTemporaryDirectory() { return cacheDirectory.resolve(TEMPORARY_DIRECTORY); } + + /** + * Resolves a file to use as a temporary file to write layer contents to. + * + * @param layerDirectory the directory in which to resolve the temporary layer file + * @return the temporary layer file + */ + Path getTemporaryLayerFile(Path layerDirectory) { + Path temporaryLayerFile = layerDirectory.resolve(TEMPORARY_LAYER_FILE_NAME); + temporaryLayerFile.toFile().deleteOnExit(); + return temporaryLayerFile; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java index 2fca700768..3c9c4899f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java @@ -22,8 +22,12 @@ import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.io.ByteStreams; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; @@ -32,6 +36,7 @@ import java.nio.file.StandardCopyOption; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** Writes to the default cache storage engine. */ @@ -95,6 +100,26 @@ private static void moveIfDoesNotExist(Path source, Path destination) throws IOE } } + /** + * Decompresses the file to obtain the diff ID. + * + * @param compressedFile the file containing the compressed contents + * @return the digest of the decompressed file + * @throws IOException if an I/O exception occurs + */ + private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile) + throws IOException { + try (CountingDigestOutputStream diffIdCaptureOutputStream = + new CountingDigestOutputStream(ByteStreams.nullOutputStream())) { + try (InputStream fileInputStream = + new BufferedInputStream(Files.newInputStream(compressedFile)); + GZIPInputStream decompressorStream = new GZIPInputStream(fileInputStream)) { + ByteStreams.copy(decompressorStream, diffIdCaptureOutputStream); + } + return diffIdCaptureOutputStream.toBlobDescriptor().getDigest(); + } + } + private final DefaultCacheStorageFiles defaultCacheStorageFiles; DefaultCacheStorageWriter(DefaultCacheStorageFiles defaultCacheStorageFiles) { @@ -102,25 +127,64 @@ private static void moveIfDoesNotExist(Path source, Path destination) throws IOE } /** - * Writes the {@link CacheWrite}. + * Writes a compressed layer {@link Blob}. * - *

The {@link CacheWrite} is written out to the cache directory in the form: + *

The {@code compressedLayerBlob} is written to the layer directory under the layers directory + * corresponding to the layer blob. + * + * @param compressedLayerBlob the compressed layer {@link Blob} to write out + * @return the {@link CacheEntry} representing the written entry + * @throws IOException if an I/O exception occurs + */ + CacheEntry write(Blob compressedLayerBlob) throws IOException { + // Creates the layers directory if it doesn't exist. + Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); + + // Creates the temporary directory. + Files.createDirectories(defaultCacheStorageFiles.getTemporaryDirectory()); + try (TemporaryDirectory temporaryDirectory = + new TemporaryDirectory(defaultCacheStorageFiles.getTemporaryDirectory())) { + Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + + // Writes the layer file to the temporary directory. + WrittenLayer writtenLayer = + writeCompressedLayerBlobToDirectory(compressedLayerBlob, temporaryLayerDirectory); + + // Moves the temporary directory to the final location. + moveIfDoesNotExist( + temporaryLayerDirectory, + defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + + // Updates cacheEntry with the blob information. + Path layerFile = + defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + return DefaultCacheEntry.builder() + .setLayerDigest(writtenLayer.layerDigest) + .setLayerDiffId(writtenLayer.layerDiffId) + .setLayerSize(writtenLayer.layerSize) + .setLayerBlob(Blobs.from(layerFile)) + .build(); + } + } + + /** + * Writes the {@link UncompressedCacheWrite}. + * + *

The {@link UncompressedCacheWrite} is written out to the cache directory in the form: * *

    - *
  • The {@link CacheWrite#getLayerBlob} and {@link CacheWrite#getMetadataBlob} are written to - * the layer directory under the layers directory corresponding to the layer blob. - *
  • The {@link CacheWrite#getSelector} is written to the selector file under the selectors - * directory. + *
  • The {@link UncompressedCacheWrite#getUncompressedLayerBlob} and {@link + * UncompressedCacheWrite#getMetadataBlob} are written to the layer directory under the + * layers directory corresponding to the layer blob. + *
  • The {@link UncompressedCacheWrite#getSelector} is written to the selector file under the + * selectors directory. *
* - * Note that writes that fail to clean up unfinished temporary directories could result in stray - * directories in the layers directory. Cache reads should ignore these stray directories. - * - * @param cacheWrite the {@link CacheWrite} to write out + * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} to write out * @return the {@link CacheEntry} representing the written entry * @throws IOException if an I/O exception occurs */ - CacheEntry write(CacheWrite cacheWrite) throws IOException { + CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { // Creates the layers directory if it doesn't exist. Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); @@ -133,11 +197,13 @@ CacheEntry write(CacheWrite cacheWrite) throws IOException { // Writes the layer file to the temporary directory. WrittenLayer writtenLayer = - writeLayerBlobToDirectory(cacheWrite.getLayerBlob(), temporaryLayerDirectory); + writeUncompressedLayerBlobToDirectory( + uncompressedCacheWrite.getUncompressedLayerBlob(), temporaryLayerDirectory); // Writes the metadata to the temporary directory. - if (cacheWrite.getMetadataBlob().isPresent()) { - writeMetadataBlobToDirectory(cacheWrite.getMetadataBlob().get(), temporaryLayerDirectory); + if (uncompressedCacheWrite.getMetadataBlob().isPresent()) { + writeMetadataBlobToDirectory( + uncompressedCacheWrite.getMetadataBlob().get(), temporaryLayerDirectory); } // Moves the temporary directory to the final location. @@ -154,14 +220,14 @@ CacheEntry write(CacheWrite cacheWrite) throws IOException { .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) .setLayerBlob(Blobs.from(layerFile)); - if (cacheWrite.getMetadataBlob().isPresent()) { + if (uncompressedCacheWrite.getMetadataBlob().isPresent()) { Path metadataFile = defaultCacheStorageFiles.getMetadataFile(writtenLayer.layerDigest); cacheEntryBuilder.setMetadataBlob(Blobs.from(metadataFile)).build(); } // Write the selector file. - if (cacheWrite.getSelector().isPresent()) { - writeSelector(cacheWrite.getSelector().get(), writtenLayer.layerDigest); + if (uncompressedCacheWrite.getSelector().isPresent()) { + writeSelector(uncompressedCacheWrite.getSelector().get(), writtenLayer.layerDigest); } return cacheEntryBuilder.build(); @@ -169,17 +235,46 @@ CacheEntry write(CacheWrite cacheWrite) throws IOException { } /** - * Writes the {@code layerBlob} to the {@code layerDirectory}. + * Writes a compressed {@code layerBlob} to the {@code layerDirectory}. * - * @param layerBlob the layer {@link Blob} + * @param compressedLayerBlob the compressed layer {@link Blob} * @param layerDirectory the directory for the layer * @return a {@link WrittenLayer} with the written layer information * @throws IOException if an I/O exception occurs */ - private WrittenLayer writeLayerBlobToDirectory(Blob layerBlob, Path layerDirectory) - throws IOException { - Path temporaryLayerFile = Files.createTempFile(layerDirectory, null, null); - temporaryLayerFile.toFile().deleteOnExit(); + private WrittenLayer writeCompressedLayerBlobToDirectory( + Blob compressedLayerBlob, Path layerDirectory) throws IOException { + // Writes the layer file to the temporary directory. + Path temporaryLayerFile = defaultCacheStorageFiles.getTemporaryLayerFile(layerDirectory); + + BlobDescriptor layerBlobDescriptor; + try (OutputStream fileOutputStream = + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile))) { + layerBlobDescriptor = compressedLayerBlob.writeTo(fileOutputStream); + } + + // Gets the diff ID. + DescriptorDigest layerDiffId = getDiffIdByDecompressingFile(temporaryLayerFile); + + // Renames the temporary layer file to the correct filename. + Path layerFile = layerDirectory.resolve(defaultCacheStorageFiles.getLayerFilename(layerDiffId)); + moveIfDoesNotExist(temporaryLayerFile, layerFile); + + return new WrittenLayer( + layerBlobDescriptor.getDigest(), layerDiffId, layerBlobDescriptor.getSize()); + } + + /** + * Writes an uncompressed {@code layerBlob} to the {@code layerDirectory}. + * + * @param uncompressedLayerBlob the uncompressed layer {@link Blob} + * @param layerDirectory the directory for the layer + * @return a {@link WrittenLayer} with the written layer information + * @throws IOException if an I/O exception occurs + */ + private WrittenLayer writeUncompressedLayerBlobToDirectory( + Blob uncompressedLayerBlob, Path layerDirectory) throws IOException { + Path temporaryLayerFile = defaultCacheStorageFiles.getTemporaryLayerFile(layerDirectory); try (CountingDigestOutputStream compressedDigestOutputStream = new CountingDigestOutputStream( @@ -188,7 +283,7 @@ private WrittenLayer writeLayerBlobToDirectory(Blob layerBlob, Path layerDirecto // diff ID and the bytes outputted from the GZIP compression are captured as the layer's // content descriptor. GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream); - DescriptorDigest layerDiffId = layerBlob.writeTo(compressorStream).getDigest(); + DescriptorDigest layerDiffId = uncompressedLayerBlob.writeTo(compressorStream).getDigest(); // The GZIPOutputStream must be closed in order to write out the remaining compressed data. compressorStream.close(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java similarity index 57% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java index b735c28504..9a777c715e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java @@ -19,57 +19,34 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.util.Optional; -import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** - * Represents layer data to write to the cache. Implementation is immutable and thread-safe. + * Represents uncompressed layer data to write to the cache, along with an additional selector + * digest and metadata. + * + *

Implementation is immutable and thread-safe. */ @Immutable -class CacheWrite { - - /** - * Constructs a {@link CacheWrite} with only the layer {@link Blob}. - * - * @param layerBlob the layer {@link Blob} - * @return the new {@link CacheWrite} - */ - static CacheWrite layerOnly(Blob layerBlob) { - return new CacheWrite(layerBlob, null, null); - } - - /** - * Constructs a {@link CacheWrite} with a layer {@link Blob}, an additional selector digest, and a - * metadata {@link Blob}. - * - * @param layerBlob the layer {@link Blob} - * @param selector the selector digest - * @param metadataBlob the metadata {@link Blob} - * @return the new {@link CacheWrite} - */ - static CacheWrite withSelectorAndMetadata( - Blob layerBlob, DescriptorDigest selector, Blob metadataBlob) { - return new CacheWrite(layerBlob, selector, metadataBlob); - } +class UncompressedCacheWrite { - private final Blob layerBlob; - @Nullable private final DescriptorDigest selector; - @Nullable private final Blob metadataBlob; + private final Blob uncompressedLayerBlob; + private final DescriptorDigest selector; + private final Blob metadataBlob; - private CacheWrite( - Blob layerBlob, @Nullable DescriptorDigest selector, @Nullable Blob metadataBlob) { - this.layerBlob = layerBlob; + UncompressedCacheWrite(Blob uncompressedLayerBlob, DescriptorDigest selector, Blob metadataBlob) { + this.uncompressedLayerBlob = uncompressedLayerBlob; this.selector = selector; this.metadataBlob = metadataBlob; } /** - * Gets the {@link Blob} to write as the layer contents. + * Gets the {@link Blob} containing the uncompressed layer contents. * * @return the layer {@link Blob} */ - Blob getLayerBlob() { - return layerBlob; + Blob getUncompressedLayerBlob() { + return uncompressedLayerBlob; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java index 31e901d4bb..2b2a1c63f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java @@ -154,31 +154,31 @@ public void testWithDirectory_existsButNotDirectory() throws IOException { } @Test - public void testWriteLayerOnly_retrieveByLayerDigest() + public void testWriteCompressed_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1NoMetadata(cache.write(layerBlob1)); + verifyIsLayer1NoMetadata(cache.writeCompressedLayer(compress(layerBlob1))); verifyIsLayer1NoMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } @Test - public void testWriteWithLayerEntries_retrieveByLayerDigest() + public void testWriteUncompressedWithLayerEntries_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); + verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } @Test - public void testWriteWithLayerEntries_retrieveByLayerEntries() + public void testWriteUncompressedWithLayerEntries_retrieveByLayerEntries() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); + verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); @@ -192,8 +192,8 @@ public void testWriteWithLayerEntries_retrieveByLayerEntries() public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1)); - verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2)); + verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer2WithMetadata(cache.writeUncompressedLayer(layerBlob2, layerEntries2)); verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); verifyIsLayer2WithMetadata(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java deleted file mode 100644 index 0f0b938033..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheWriteTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.ncache; - -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheWrite}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheWriteTest { - - @Mock private DescriptorDigest mockSelector; - - @Test - public void testLayerOnly() throws IOException { - CacheWrite cacheWrite = CacheWrite.layerOnly(Blobs.from("layerBlob")); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); - Assert.assertFalse(cacheWrite.getSelector().isPresent()); - Assert.assertFalse(cacheWrite.getMetadataBlob().isPresent()); - } - - @Test - public void testWithSelectorAndMetadata() throws IOException { - CacheWrite cacheWrite = - CacheWrite.withSelectorAndMetadata( - Blobs.from("layerBlob"), mockSelector, Blobs.from("metadataBlob")); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheWrite.getLayerBlob())); - Assert.assertEquals(mockSelector, cacheWrite.getSelector().orElse(null)); - Assert.assertEquals( - "metadataBlob", Blobs.writeToString(cacheWrite.getMetadataBlob().orElse(null))); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java index 9e5cbddfc1..e5f367fce9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java @@ -44,12 +44,19 @@ private static DescriptorDigest getDigest(Blob blob) throws IOException { private static BlobDescriptor getCompressedBlobDescriptor(Blob blob) throws IOException { CountingDigestOutputStream compressedDigestOutputStream = new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - try (GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream)) { - blob.writeTo(compressorStream); - } + compress(blob).writeTo(compressedDigestOutputStream); return compressedDigestOutputStream.toBlobDescriptor(); } + private static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }); + } + private static Blob decompress(Blob blob) throws IOException { return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); } @@ -64,22 +71,29 @@ public void setUp() throws IOException { } @Test - public void testWrite_layerOnly() throws IOException { - Blob layerBlob = Blobs.from("layerBlob"); - CacheEntry cacheEntry = verifyWrite(CacheWrite.layerOnly(layerBlob), layerBlob); + public void testWrite_compressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + + CacheEntry cacheEntry = + new DefaultCacheStorageWriter(defaultCacheStorageFiles) + .write(compress(uncompressedLayerBlob)); + + verifyCacheEntry(cacheEntry, uncompressedLayerBlob); Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); } @Test - public void testWrite_withSelectorAndMetadata() throws IOException { - Blob layerBlob = Blobs.from("layerBlob"); - DescriptorDigest layerDigest = getCompressedBlobDescriptor(layerBlob).getDigest(); + public void testWrite_uncompressed() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + DescriptorDigest layerDigest = getCompressedBlobDescriptor(uncompressedLayerBlob).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")); Blob metadataBlob = Blobs.from("metadata"); CacheEntry cacheEntry = - verifyWrite( - CacheWrite.withSelectorAndMetadata(layerBlob, selector, metadataBlob), layerBlob); + new DefaultCacheStorageWriter(defaultCacheStorageFiles) + .write(new UncompressedCacheWrite(uncompressedLayerBlob, selector, metadataBlob)); + + verifyCacheEntry(cacheEntry, uncompressedLayerBlob); // Verifies cacheEntry is correct. Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); @@ -95,21 +109,17 @@ public void testWrite_withSelectorAndMetadata() throws IOException { Files.exists(defaultCacheStorageFiles.getMetadataFile(cacheEntry.getLayerDigest()))); } - private CacheEntry verifyWrite(CacheWrite cacheWriteEntry, Blob expectedLayerBlob) + private void verifyCacheEntry(CacheEntry cacheEntry, Blob uncompressedLayerBlob) throws IOException { - BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(expectedLayerBlob); - DescriptorDigest layerDiffId = getDigest(expectedLayerBlob); - - DefaultCacheStorageWriter defaultCacheStorageWriter = - new DefaultCacheStorageWriter(defaultCacheStorageFiles); - CacheEntry cacheEntry = defaultCacheStorageWriter.write(cacheWriteEntry); + BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(uncompressedLayerBlob); + DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob); // Verifies cacheEntry is correct. Assert.assertEquals(layerBlobDescriptor.getDigest(), cacheEntry.getLayerDigest()); Assert.assertEquals(layerDiffId, cacheEntry.getLayerDiffId()); Assert.assertEquals(layerBlobDescriptor.getSize(), cacheEntry.getLayerSize()); Assert.assertArrayEquals( - Blobs.writeToByteArray(expectedLayerBlob), + Blobs.writeToByteArray(uncompressedLayerBlob), Blobs.writeToByteArray(decompress(cacheEntry.getLayerBlob()))); // Verifies that the files are present. @@ -117,7 +127,5 @@ private CacheEntry verifyWrite(CacheWrite cacheWriteEntry, Blob expectedLayerBlo Files.exists( defaultCacheStorageFiles.getLayerFile( cacheEntry.getLayerDigest(), cacheEntry.getLayerDiffId()))); - - return cacheEntry; } } From 211efee9d244fb4f79488d382bc28fbeec0fdc15 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 27 Sep 2018 15:32:37 -0400 Subject: [PATCH 0245/2020] Rename EventEmitter to EventDispatcher (#1053) --- ...icationMethodRetrieverIntegrationTest.java | 8 +-- .../registry/BlobCheckerIntegrationTest.java | 8 +-- .../registry/BlobPullerIntegrationTest.java | 8 +-- .../registry/BlobPusherIntegrationTest.java | 6 +- .../ManifestPullerIntegrationTest.java | 10 ++-- .../ManifestPusherIntegrationTest.java | 10 ++-- .../RegistryAuthenticatorIntegrationTest.java | 6 +- .../cloud/tools/jib/api/Containerizer.java | 2 +- .../cloud/tools/jib/builder/BuildSteps.java | 12 ++-- ...Emitter.java => TimerEventDispatcher.java} | 55 ++++++++++--------- .../builder/steps/AuthenticatePushStep.java | 10 ++-- .../BuildAndCacheApplicationLayerStep.java | 16 +++--- .../jib/builder/steps/BuildImageStep.java | 6 +- .../jib/builder/steps/FinalizingStep.java | 2 +- .../jib/builder/steps/LoadDockerStep.java | 4 +- .../steps/PullAndCacheBaseImageLayerStep.java | 8 +-- .../PullAndCacheBaseImageLayersStep.java | 6 +- .../jib/builder/steps/PullBaseImageStep.java | 20 +++---- .../tools/jib/builder/steps/PushBlobStep.java | 11 ++-- .../steps/PushContainerConfigurationStep.java | 6 +- .../jib/builder/steps/PushImageStep.java | 10 ++-- .../jib/builder/steps/PushLayersStep.java | 6 +- .../RetrieveRegistryCredentialsStep.java | 20 +++---- .../jib/builder/steps/WriteTarFileStep.java | 4 +- .../jib/configuration/BuildConfiguration.java | 30 +++++----- ...itter.java => DefaultEventDispatcher.java} | 12 ++-- ...EventEmitter.java => EventDispatcher.java} | 6 +- .../cloud/tools/jib/event/EventHandlers.java | 2 +- .../google/cloud/tools/jib/event/Handler.java | 2 +- .../cloud/tools/jib/event/JibEvent.java | 4 +- .../tools/jib/event/events/TimerEvent.java | 2 +- .../frontend/CredentialRetrieverFactory.java | 33 +++++------ .../tools/jib/frontend/MainClassFinder.java | 12 ++-- .../jib/registry/RegistryAuthenticator.java | 18 +++--- .../tools/jib/registry/RegistryClient.java | 41 +++++++------- .../jib/registry/RegistryEndpointCaller.java | 18 +++--- ...est.java => TimerEventDispatcherTest.java} | 22 ++++---- ...BuildAndCacheApplicationLayerStepTest.java | 6 +- .../jib/builder/steps/BuildImageStepTest.java | 6 +- .../RetrieveRegistryCredentialsStepTest.java | 14 ++--- ...t.java => DefaultEventDispatcherTest.java} | 12 ++-- .../tools/jib/event/events/LogEventTest.java | 18 +++--- .../CredentialRetrieverFactoryTest.java | 28 +++++----- .../jib/frontend/MainClassFinderTest.java | 20 +++---- .../jib/registry/RegistryClientTest.java | 6 +- .../registry/RegistryEndpointCallerTest.java | 30 +++++----- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../jib/gradle/GradleProjectProperties.java | 22 ++++---- .../gradle/PluginConfigurationProcessor.java | 6 +- .../gradle/GradleProjectPropertiesTest.java | 6 +- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 4 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../jib/maven/MavenProjectProperties.java | 20 +++---- .../maven/PluginConfigurationProcessor.java | 6 +- .../jib/maven/MavenProjectPropertiesTest.java | 7 ++- .../jib/plugins/common/BuildStepsRunner.java | 19 ++++--- .../ConfigurationPropertyValidator.java | 20 +++---- .../jib/plugins/common/MainClassResolver.java | 14 ++--- .../jib/plugins/common/ProjectProperties.java | 4 +- .../plugins/common/BuildStepsRunnerTest.java | 6 +- .../ConfigurationPropertyValidatorTest.java | 39 +++++++------ .../plugins/common/MainClassResolverTest.java | 18 +++--- 65 files changed, 411 insertions(+), 388 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/builder/{TimerEventEmitter.java => TimerEventDispatcher.java} (51%) rename jib-core/src/main/java/com/google/cloud/tools/jib/event/{DefaultEventEmitter.java => DefaultEventDispatcher.java} (78%) rename jib-core/src/main/java/com/google/cloud/tools/jib/event/{EventEmitter.java => EventDispatcher.java} (85%) rename jib-core/src/test/java/com/google/cloud/tools/jib/builder/{TimerEventEmitterTest.java => TimerEventDispatcherTest.java} (91%) rename jib-core/src/test/java/com/google/cloud/tools/jib/event/{DefaultEventEmitterTest.java => DefaultEventDispatcherTest.java} (86%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 375a14b1e6..be34ccc9d2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -25,20 +25,20 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testGetRegistryAuthenticator() throws RegistryAuthenticationFailedException, IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(); RegistryClient authorizedRegistryClient = - RegistryClient.factory(EVENT_EMITTER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 5d38cc8a14..c53f8d4e26 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -29,13 +29,13 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testCheck_exists() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = @@ -50,7 +50,7 @@ public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); DescriptorDigest fakeBlobDigest = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 162ec06241..bd76ef03e6 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -36,7 +36,7 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -45,7 +45,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls the busybox image. localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -71,7 +71,7 @@ public void testPull_unknownBlob() try { RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 43e47e110a..f356e57ac0 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.security.DigestException; @@ -30,7 +30,7 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testPush() @@ -43,7 +43,7 @@ public void testPush() "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 7339a40cf9..c291a389d1 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -30,13 +30,13 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testPull_v21() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -50,7 +50,7 @@ public void testPull_v21() throws IOException, RegistryException, InterruptedExc public void testPull_v22() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_DISPATCHER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -64,7 +64,7 @@ public void testPull_unknownManifest() localRegistry.pullAndPushToLocal("busybox", "busybox"); try { RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 3d1d157133..b653200713 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -20,7 +20,7 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -34,18 +34,18 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_DISPATCHER, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); try { @@ -81,7 +81,7 @@ public void testPush() // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory(EVENT_EMITTER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null)); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 4f332d1e41..5581ee7d95 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -27,7 +27,7 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { - private static final EventEmitter EVENT_EMITTER = jibEvent -> {}; + private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test public void testAuthenticate() @@ -36,7 +36,7 @@ public void testAuthenticate() ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - EVENT_EMITTER, + EVENT_DISPATCHER, dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) .initialize(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index dc638aa834..fc938a7233 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -88,7 +88,7 @@ public Containerizer setCacheConfiguration(CacheConfiguration cacheConfiguration } /** - * Sets the {@link EventHandlers} to handle events emitted during Jib's execution. + * Sets the {@link EventHandlers} to handle events dispatched during Jib's execution. * * @param eventHandlers the {@link EventHandlers} * @return this diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index b4f373b571..d6c6e9f185 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -149,10 +149,10 @@ public BuildConfiguration getBuildConfiguration() { public void run() throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), description)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { try (Caches caches = cachesInitializer.init()) { Cache baseImageLayersCache = caches.getBaseCache(); Cache applicationLayersCache = caches.getApplicationCache(); @@ -169,10 +169,10 @@ public void run() } if (buildConfiguration.getContainerConfiguration() != null) { - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); buildConfiguration - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.lifecycle( "Container entrypoint set to " + buildConfiguration.getContainerConfiguration().getEntrypoint())); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java similarity index 51% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java index 0ab3973228..a3670557e4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventEmitter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.events.TimerEvent.State; import com.google.common.annotations.VisibleForTesting; @@ -25,75 +25,80 @@ import java.time.Duration; import javax.annotation.Nullable; -/** Handles {@link Timer}s to emit {@link TimerEvent}s. */ -public class TimerEventEmitter implements Closeable { +/** Handles {@link Timer}s to dispatch {@link TimerEvent}s. */ +public class TimerEventDispatcher implements Closeable { private static final Clock DEFAULT_CLOCK = Clock.systemUTC(); - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final String description; private final Clock clock; private final Timer timer; /** - * Creates a new {@link TimerEventEmitter}. + * Creates a new {@link TimerEventDispatcher}. * - * @param eventEmitter the {@link EventEmitter} used to emit the {@link TimerEvent}s + * @param eventDispatcher the {@link EventDispatcher} used to dispatch the {@link TimerEvent}s * @param description the default description for the {@link TimerEvent}s */ - public TimerEventEmitter(EventEmitter eventEmitter, String description) { - this(eventEmitter, description, DEFAULT_CLOCK, null); + public TimerEventDispatcher(EventDispatcher eventDispatcher, String description) { + this(eventDispatcher, description, DEFAULT_CLOCK, null); } @VisibleForTesting - TimerEventEmitter( - EventEmitter eventEmitter, String description, Clock clock, @Nullable Timer parentTimer) { - this.eventEmitter = eventEmitter; + TimerEventDispatcher( + EventDispatcher eventDispatcher, + String description, + Clock clock, + @Nullable Timer parentTimer) { + this.eventDispatcher = eventDispatcher; this.description = description; this.clock = clock; this.timer = new Timer(clock, parentTimer); - emitTimerEvent(State.START, Duration.ZERO, description); + dispatchTimerEvent(State.START, Duration.ZERO, description); } /** - * Creates a new {@link TimerEventEmitter} with its parent timer as this. + * Creates a new {@link TimerEventDispatcher} with its parent timer as this. * * @param description a new description - * @return the new {@link TimerEventEmitter} + * @return the new {@link TimerEventDispatcher} */ - public TimerEventEmitter subTimer(String description) { - return new TimerEventEmitter(eventEmitter, description, clock, timer); + public TimerEventDispatcher subTimer(String description) { + return new TimerEventDispatcher(eventDispatcher, description, clock, timer); } /** - * Captures the time since last lap or creation and emits an {@link State#LAP} {@link TimerEvent}. + * Captures the time since last lap or creation and dispatches an {@link State#LAP} {@link + * TimerEvent}. * * @see #lap(String) for using a different description */ public void lap() { - emitTimerEvent(State.LAP, timer.lap(), description); + dispatchTimerEvent(State.LAP, timer.lap(), description); } /** - * Captures the time since last lap or creation and emits an {@link State#LAP} {@link TimerEvent}. + * Captures the time since last lap or creation and dispatches an {@link State#LAP} {@link + * TimerEvent}. * - * @param newDescription the description to use instead of the {@link TimerEventEmitter}'s + * @param newDescription the description to use instead of the {@link TimerEventDispatcher}'s * description */ public void lap(String newDescription) { - emitTimerEvent(State.LAP, timer.lap(), newDescription); + dispatchTimerEvent(State.LAP, timer.lap(), newDescription); } - /** Laps and emits an {@link State#FINISHED} {@link TimerEvent} upon close. */ + /** Laps and dispatches a {@link State#FINISHED} {@link TimerEvent} upon close. */ @Override public void close() { - emitTimerEvent(State.FINISHED, timer.lap(), description); + dispatchTimerEvent(State.FINISHED, timer.lap(), description); } - private void emitTimerEvent(State state, Duration duration, String eventDescription) { - eventEmitter.emit( + private void dispatchTimerEvent(State state, Duration duration, String eventDescription) { + eventDispatcher.dispatch( new TimerEvent(state, timer, duration, timer.getElapsedTime(), eventDescription)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 238eb0e983..7c14849436 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; @@ -71,9 +71,9 @@ public ListenableFuture getFuture() { public Authorization call() throws ExecutionException, RegistryAuthenticationFailedException, IOException, RegistryException { - try (TimerEventEmitter ignored = - new TimerEventEmitter( - buildConfiguration.getEventEmitter(), + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventDispatcher(), String.format( DESCRIPTION, buildConfiguration.getTargetImageConfiguration().getImageRegistry()))) { @@ -86,7 +86,7 @@ public Authorization call() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getEventEmitter(), + buildConfiguration.getEventDispatcher(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 78177e7858..4ac67806ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.cache.CacheReader; @@ -48,8 +48,8 @@ static ImmutableList makeList( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, Cache cache) { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = ImmutableList.builderWithExpectedSize(buildConfiguration.getLayerConfigurations().size()); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { @@ -100,10 +100,10 @@ public ListenableFuture getFuture() { public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorruptedException { String description = "Building " + layerType + " layer"; - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle(description + "...")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), description)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { // Don't build the layer if it exists already. Optional optionalCachedLayer = new CacheReader(cache) @@ -117,8 +117,8 @@ public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorrupted .writeLayer(new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries())); buildConfiguration - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.debug( description + " built " + cachedLayer.getBlobDescriptor().getDigest())); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 810ac220ef..1c87ecaa66 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -93,8 +93,8 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayersSteps() throws ExecutionException, LayerPropertyNotFoundException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(); Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index b94de65c76..5656f46e54 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -84,7 +84,7 @@ public Void call() throws ExecutionException { Futures.whenAllSucceed(unwrappedDependencies) .call( () -> { - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Finalizing...")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); return null; }, listeningExecutorService); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index a8507f1d1f..adedfab16a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -91,7 +91,9 @@ private Void afterPushBaseImageLayerFuturesFuture() buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Loading to Docker daemon...")); + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); DockerClient dockerClient = new DockerClient(); dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 878b27238d..33dbc760d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheReader; import com.google.cloud.tools.jib.cache.CacheWriter; @@ -67,9 +67,9 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, RegistryException { - try (TimerEventEmitter ignored = - new TimerEventEmitter( - buildConfiguration.getEventEmitter(), String.format(DESCRIPTION, layerDigest))) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { RegistryClient registryClient = buildConfiguration .newBaseImageRegistryClientFactory() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 029dce2946..f3632d324c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -67,8 +67,8 @@ public ListenableFuture> getFuture @Override public ImmutableList call() throws ExecutionException, LayerPropertyNotFoundException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 33c8dac7e7..2c61c0feab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -105,23 +105,23 @@ public BaseImageWithAuthorization call() LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, RegistryAuthenticationFailedException { buildConfiguration - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.lifecycle( "Getting base image " + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. try { return new BaseImageWithAuthorization(pullBaseImage(null), null); } catch (RegistryUnauthorizedException ex) { buildConfiguration - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.lifecycle( "The base image requires auth. Trying again for " + buildConfiguration.getBaseImageConfiguration().getImage() @@ -150,15 +150,15 @@ public BaseImageWithAuthorization call() // See https://docs.docker.com/registry/spec/auth/token RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( - buildConfiguration.getEventEmitter(), + buildConfiguration.getEventDispatcher(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { buildConfiguration - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.error( "Failed to retrieve authentication challenge for registry that required token authentication")); throw registryUnauthorizedException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 4448fc28e0..58664395bb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -67,8 +67,9 @@ public ListenableFuture getFuture() { @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION + blobDescriptor)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() @@ -78,8 +79,8 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc // check if the BLOB is available if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { buildConfiguration - .getEventEmitter() - .emit(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); + .getEventDispatcher() + .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index c4f0495d10..8514f87905 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; @@ -77,8 +77,8 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); Blob containerConfigurationBlob = new ImageToJsonTranslator(image).getContainerConfigurationBlob(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index a13e36c9a9..ac08ce6d18 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -115,8 +115,8 @@ private ListenableFuture> afterPushSteps() throws Executi } private ListenableFuture afterAllPushed() throws ExecutionException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() @@ -141,8 +141,8 @@ private ListenableFuture afterAllPushed() throws ExecutionException { listeningExecutorService.submit( () -> { buildConfiguration - .getEventEmitter() - .emit(LogEvent.info("Tagging with " + tag + "...")); + .getEventDispatcher() + .dispatch(LogEvent.info("Tagging with " + tag + "...")); registryClient.pushManifest(manifestTemplate, tag); return null; })); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index a9a302b45f..76f1764bc1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.collect.ImmutableList; @@ -65,8 +65,8 @@ public ListenableFuture>> getFuture() { @Override public ImmutableList> call() throws ExecutionException { - try (TimerEventEmitter ignored = - new TimerEventEmitter(buildConfiguration.getEventEmitter(), DESCRIPTION)) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList> cachedLayerSteps = NonBlockingSteps.get(cachedLayerStepsStep); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index acbd40015b..2b3de9ea80 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -17,11 +17,11 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; @@ -44,7 +44,7 @@ static RetrieveRegistryCredentialsStep forBaseImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getEventEmitter(), + buildConfiguration.getEventDispatcher(), buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } @@ -54,12 +54,12 @@ static RetrieveRegistryCredentialsStep forTargetImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getEventEmitter(), + buildConfiguration.getEventDispatcher(), buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final String registry; private final ImmutableList credentialRetrievers; @@ -68,10 +68,10 @@ static RetrieveRegistryCredentialsStep forTargetImage( @VisibleForTesting RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, String registry, ImmutableList credentialRetrievers) { - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.registry = registry; this.credentialRetrievers = credentialRetrievers; @@ -87,9 +87,9 @@ public ListenableFuture getFuture() { @Nullable public Credential call() throws CredentialRetrievalException { String description = makeDescription(registry); - eventEmitter.emit(LogEvent.lifecycle(description + "...")); + eventDispatcher.dispatch(LogEvent.lifecycle(description + "...")); - try (TimerEventEmitter ignored = new TimerEventEmitter(eventEmitter, description)) { + try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventDispatcher, description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { @@ -99,7 +99,7 @@ public Credential call() throws CredentialRetrievalException { // If no credentials found, give an info (not warning because in most cases, the base image is // public and does not need extra credentials) and return null. - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info("No credentials could be retrieved for registry " + registry)); return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 9d43bc5fc9..6417ba05c7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -93,7 +93,9 @@ private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, I Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Build the image to a tarball - buildConfiguration.getEventEmitter().emit(LogEvent.lifecycle("Building image to tar file...")); + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.lifecycle("Building image to tar file...")); Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 2c0576ab8b..a1a3c96310 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -52,9 +52,9 @@ public static class Builder { private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; private String toolName = DEFAULT_TOOL_NAME; - private EventEmitter eventEmitter = + private EventDispatcher eventDispatcher = jibEvent -> { - /* No-op EventEmitter. */ + /* No-op EventDispatcher. */ }; private Builder() {} @@ -173,13 +173,13 @@ public Builder setToolName(String toolName) { } /** - * Sets the {@link EventEmitter} to emit events with. + * Sets the {@link EventDispatcher} to dispatch events with. * - * @param eventEmitter the {@link EventEmitter} + * @param eventDispatcher the {@link EventDispatcher} * @return this */ - public Builder setEventEmitter(EventEmitter eventEmitter) { - this.eventEmitter = eventEmitter; + public Builder setEventDispatcher(EventDispatcher eventDispatcher) { + this.eventDispatcher = eventDispatcher; return this; } @@ -204,7 +204,7 @@ public BuildConfiguration build() { throw new IllegalStateException("Required fields should not be null"); } if (baseImageConfiguration.getImage().usesDefaultTag()) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.warn( "Base image '" + baseImageConfiguration.getImage() @@ -222,7 +222,7 @@ public BuildConfiguration build() { allowInsecureRegistries, layerConfigurations, toolName, - eventEmitter); + eventDispatcher); case 1: throw new IllegalStateException(errorMessages.get(0)); @@ -256,7 +256,7 @@ public static Builder builder() { private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; private final String toolName; - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; /** Instantiate with {@link #builder}. */ private BuildConfiguration( @@ -270,7 +270,7 @@ private BuildConfiguration( boolean allowInsecureRegistries, ImmutableList layerConfigurations, String toolName, - EventEmitter eventEmitter) { + EventDispatcher eventDispatcher) { this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; this.additionalTargetImageTags = additionalTargetImageTags; @@ -281,7 +281,7 @@ private BuildConfiguration( this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; this.toolName = toolName; - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; } public ImageConfiguration getBaseImageConfiguration() { @@ -313,8 +313,8 @@ public String getToolName() { return toolName; } - public EventEmitter getEventEmitter() { - return eventEmitter; + public EventDispatcher getEventDispatcher() { + return eventDispatcher; } /** @@ -378,7 +378,7 @@ public RegistryClient.Factory newTargetImageRegistryClientFactory() { private RegistryClient.Factory newRegistryClientFactory(ImageConfiguration imageConfiguration) { return RegistryClient.factory( - getEventEmitter(), + getEventDispatcher(), imageConfiguration.getImageRegistry(), imageConfiguration.getImageRepository()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java similarity index 78% rename from jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java index 1faa1d8f3b..b49aa021d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventEmitter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java @@ -18,8 +18,8 @@ import com.google.common.collect.ImmutableMultimap; -/** Emits {@link JibEvent}s to event handlers. */ -public class DefaultEventEmitter implements EventEmitter { +/** Dispatches {@link JibEvent}s to event handlers. */ +public class DefaultEventDispatcher implements EventDispatcher { /** Maps from {@link JibEvent} class to handlers for that event type. */ private final ImmutableMultimap, Handler> handlers; @@ -29,17 +29,17 @@ public class DefaultEventEmitter implements EventEmitter { * * @param eventHandlers the {@link EventHandlers} to get the {@link Handler}s from */ - public DefaultEventEmitter(EventHandlers eventHandlers) { + public DefaultEventDispatcher(EventHandlers eventHandlers) { handlers = eventHandlers.getHandlers(); } /** - * Emits {@code jibEvent} to all the handlers that can handle it. + * Dispatches {@code jibEvent} to all the handlers that can handle it. * - * @param jibEvent the {@link JibEvent} to emit + * @param jibEvent the {@link JibEvent} to dispatch */ @Override - public void emit(JibEvent jibEvent) { + public void dispatch(JibEvent jibEvent) { handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java similarity index 85% rename from jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java index c3b068fa6c..3c617d63a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventEmitter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.event; -/** Emits {@link JibEvent}s. */ -public interface EventEmitter { +/** Dispatches {@link JibEvent}s. */ +public interface EventDispatcher { - void emit(JibEvent jibEvent); + void dispatch(JibEvent jibEvent); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index 06f9eafc27..469b7baec8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -31,7 +31,7 @@ public class EventHandlers { /** * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. * The order in which handlers are added is the order in which they are called when the event is - * emitted. + * dispatched. * *

Note: Implementations of {@code eventConsumer} must be thread-safe. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java index 080671f502..5661b371a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java @@ -19,7 +19,7 @@ import com.google.common.base.Preconditions; import java.util.function.Consumer; -/** Handles an emitted {@link JibEvent}. */ +/** Handles a dispatched {@link JibEvent}. */ class Handler { private final Class eventClass; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java index 8ad999a203..607c006fdc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.event; /** - * Type for events emitted by Jib Core. Implementation classes should not inherit from each - * other. + * Type for events dispatched by Jib Core. Implementation classes should not inherit from + * each other. */ public interface JibEvent {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java index 9f687e4c0f..a7aaba0e66 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java @@ -25,7 +25,7 @@ * *

Timer events follow a specific {@link Timer} through a {@link State#START}, {@link State#LAP}, * and {@link State#FINISHED} states. The duration indicates the duration since the last {@link - * TimerEvent} emitted for the {@link Timer}. + * TimerEvent} dispatched for the {@link Timer}. * *

Timers can also define a hierarchy. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 43bb5d4d8d..e711ecdc6b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; @@ -56,29 +56,30 @@ interface DockerCredentialHelperFactory { * Creates a new {@link CredentialRetrieverFactory} for an image. * * @param imageReference the image the credential are for - * @param eventEmitter an event emitter for emitting log events + * @param eventDispatcher an event dispatcher for dispatching log events * @return a new {@link CredentialRetrieverFactory} */ public static CredentialRetrieverFactory forImage( - ImageReference imageReference, EventEmitter eventEmitter) { - return new CredentialRetrieverFactory(imageReference, eventEmitter); + ImageReference imageReference, EventDispatcher eventDispatcher) { + return new CredentialRetrieverFactory(imageReference, eventDispatcher); } - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final ImageReference imageReference; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; - private CredentialRetrieverFactory(ImageReference imageReference, EventEmitter eventEmitter) { - this(imageReference, eventEmitter, DockerCredentialHelper::new); + private CredentialRetrieverFactory( + ImageReference imageReference, EventDispatcher eventDispatcher) { + this(imageReference, eventDispatcher, DockerCredentialHelper::new); } @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, DockerCredentialHelperFactory dockerCredentialHelperFactory) { this.imageReference = imageReference; - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } @@ -118,13 +119,13 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - eventEmitter.emit(LogEvent.info("Checking credentials from " + credentialHelper)); + eventDispatcher.dispatch(LogEvent.info("Checking credentials from " + credentialHelper)); try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); return Optional.empty(); @@ -168,9 +169,9 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - eventEmitter.emit(LogEvent.warn(ex.getMessage())); + eventDispatcher.dispatch(LogEvent.warn(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - eventEmitter.emit(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); + eventDispatcher.dispatch(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } } @@ -213,14 +214,14 @@ CredentialRetriever dockerConfig( try { Optional dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); if (dockerConfigCredentials.isPresent()) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info( "Using credentials from Docker config for " + imageReference.getRegistry())); return dockerConfigCredentials; } } catch (IOException ex) { - eventEmitter.emit(LogEvent.info("Unable to parse Docker config")); + eventDispatcher.dispatch(LogEvent.info("Unable to parse Docker config")); } return Optional.empty(); }; @@ -238,7 +239,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) } private void logGotCredentialsFrom(String credentialSource) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index f03113bf9e..dff27c699c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -107,17 +107,17 @@ public List getFoundMainClasses() { } private final ImmutableList files; - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; /** * Finds a class with {@code psvm} (see class javadoc) in {@code files}. * * @param files the files to check - * @param eventEmitter used for emitting log events. + * @param eventDispatcher used for dispatching log events. */ - public MainClassFinder(ImmutableList files, EventEmitter eventEmitter) { + public MainClassFinder(ImmutableList files, EventDispatcher eventDispatcher) { this.files = files; - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; } /** @@ -180,7 +180,7 @@ private Optional findMainClass(Path file) { } catch (IOException ex) { // Could not read class file. - eventEmitter.emit(LogEvent.warn("Could not read file: " + file)); + eventDispatcher.dispatch(LogEvent.warn("Could not read file: " + file)); } } catch (NotFoundException ex) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index a45e3bde9c..e5ea7f259c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; @@ -46,7 +46,7 @@ public class RegistryAuthenticator { /** Initializer for {@link RegistryAuthenticator}. */ public static class Initializer { - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final String serverUrl; private final String repository; private boolean allowInsecureRegistries = false; @@ -54,12 +54,12 @@ public static class Initializer { /** * Instantiates a new initializer for {@link RegistryAuthenticator}. * - * @param eventEmitter the event emitter used for emitting log events + * @param eventDispatcher the event dispatcher used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) */ - private Initializer(EventEmitter eventEmitter, String serverUrl, String repository) { - this.eventEmitter = eventEmitter; + private Initializer(EventDispatcher eventDispatcher, String serverUrl, String repository) { + this.eventDispatcher = eventDispatcher; this.serverUrl = serverUrl; this.repository = repository; } @@ -82,7 +82,7 @@ public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { public RegistryAuthenticator initialize() throws RegistryAuthenticationFailedException, IOException, RegistryException { try { - return RegistryClient.factory(eventEmitter, serverUrl, repository) + return RegistryClient.factory(eventDispatcher, serverUrl, repository) .setAllowInsecureRegistries(allowInsecureRegistries) .newRegistryClient() .getRegistryAuthenticator(); @@ -100,14 +100,14 @@ public RegistryAuthenticator initialize() /** * Gets a new initializer for {@link RegistryAuthenticator}. * - * @param eventEmitter the event emitter used for emitting log events + * @param eventDispatcher the event dispatcher used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param repository the image/repository name (also known as, namespace) * @return the new {@link Initializer} */ public static Initializer initializer( - EventEmitter eventEmitter, String serverUrl, String repository) { - return new Initializer(eventEmitter, serverUrl, repository); + EventDispatcher eventDispatcher, String serverUrl, String repository) { + return new Initializer(eventDispatcher, serverUrl, repository); } // TODO: Replace with a WWW-Authenticate header parser. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index d4ee4151cf..acb5939bf0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.TimerEventEmitter; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -40,7 +40,7 @@ public class RegistryClient { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; @@ -48,9 +48,9 @@ public static class Factory { @Nullable private Authorization authorization; private Factory( - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, RegistryEndpointRequestProperties registryEndpointRequestProperties) { - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.registryEndpointRequestProperties = registryEndpointRequestProperties; } @@ -95,7 +95,7 @@ public Factory setUserAgentSuffix(@Nullable String userAgentSuffix) { */ public RegistryClient newRegistryClient() { return new RegistryClient( - eventEmitter, + eventDispatcher, authorization, registryEndpointRequestProperties, allowInsecureRegistries, @@ -131,16 +131,18 @@ private String makeUserAgent() { /** * Creates a new {@link Factory} for building a {@link RegistryClient}. * - * @param eventEmitter the event emitter used for emitting log events + * @param eventDispatcher the event dispatcher used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) * @return the new {@link Factory} */ - public static Factory factory(EventEmitter eventEmitter, String serverUrl, String imageName) { - return new Factory(eventEmitter, new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory( + EventDispatcher eventDispatcher, String serverUrl, String imageName) { + return new Factory( + eventDispatcher, new RegistryEndpointRequestProperties(serverUrl, imageName)); } - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -149,18 +151,18 @@ public static Factory factory(EventEmitter eventEmitter, String serverUrl, Strin /** * Instantiate with {@link #factory}. * - * @param eventEmitter the event emitter used for emitting log events + * @param eventDispatcher the event dispatcher used for dispatching log events * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, String userAgent) { - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; @@ -270,9 +272,10 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); - try (TimerEventEmitter timerEventEmitter = new TimerEventEmitter(eventEmitter, "pushBlob")) { - try (TimerEventEmitter timerEventEmitter2 = - timerEventEmitter.subTimer("pushBlob POST " + blobDigest)) { + try (TimerEventDispatcher timerEventDispatcher = + new TimerEventDispatcher(eventDispatcher, "pushBlob")) { + try (TimerEventDispatcher timerEventDispatcher2 = + timerEventDispatcher.subTimer("pushBlob POST " + blobDigest)) { // POST /v2//blobs/uploads/ OR // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} @@ -282,13 +285,13 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String return true; } - timerEventEmitter2.lap("pushBlob PATCH " + blobDigest); + timerEventDispatcher2.lap("pushBlob PATCH " + blobDigest); // PATCH with BLOB URL putLocation = callRegistryEndpoint(blobPusher.writer(patchLocation)); Preconditions.checkNotNull(putLocation); - timerEventEmitter2.lap("pushBlob PUT " + blobDigest); + timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); // PUT ?digest={blob.digest} callRegistryEndpoint(blobPusher.committer(putLocation)); @@ -320,7 +323,7 @@ String getUserAgent() { private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { return new RegistryEndpointCaller<>( - eventEmitter, + eventDispatcher, userAgent, getApiRouteBase(), registryEndpointProvider, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 5877bc3625..e852cda092 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -19,7 +19,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; @@ -59,7 +59,7 @@ private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); } - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -76,7 +76,7 @@ private static boolean isHttpsProtocol(URL url) { /** * Constructs with parameters for making the request. * - * @param eventEmitter the event emitter used for emitting log events + * @param eventDispatcher the event dispatcher used for dispatching log events * @param userAgent {@code User-Agent} header to send with the request * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint @@ -86,7 +86,7 @@ private static boolean isHttpsProtocol(URL url) { * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -95,7 +95,7 @@ private static boolean isHttpsProtocol(URL url) { boolean allowInsecureRegistries) throws MalformedURLException { this( - eventEmitter, + eventDispatcher, userAgent, apiRouteBase, registryEndpointProvider, @@ -108,7 +108,7 @@ private static boolean isHttpsProtocol(URL url) { @VisibleForTesting RegistryEndpointCaller( - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -118,7 +118,7 @@ private static boolean isHttpsProtocol(URL url) { Function connectionFactory, @Nullable Function insecureConnectionFactory) throws MalformedURLException { - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.initialRequestUrl = registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; @@ -171,7 +171,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } try { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info( "Cannot verify server at " + url + ". Attempting again with no TLS verification.")); return call(url, getInsecureConnectionFactory()); @@ -185,7 +185,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr private T fallBackToHttp(URL url) throws IOException, RegistryException { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info( "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl)); return call(httpUrl.toURL(), connectionFactory); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java similarity index 91% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java index d27c3f3e29..69fe670cd9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventEmitterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.DefaultEventEmitter; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.TimerEvent; @@ -35,9 +35,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link TimerEventEmitter}. */ +/** Tests for {@link TimerEventDispatcher}. */ @RunWith(MockitoJUnitRunner.class) -public class TimerEventEmitterTest { +public class TimerEventDispatcherTest { private final Deque timerEventQueue = new ArrayDeque<>(); @@ -45,16 +45,18 @@ public class TimerEventEmitterTest { @Test public void testLogging() { - EventEmitter eventEmitter = - new DefaultEventEmitter(new EventHandlers().add(JibEventType.TIMING, timerEventQueue::add)); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.TIMING, timerEventQueue::add)); Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); - try (TimerEventEmitter parentTimerEventEmitter = - new TimerEventEmitter(eventEmitter, "description", mockClock, null)) { + try (TimerEventDispatcher parentTimerEventDispatcher = + new TimerEventDispatcher(eventDispatcher, "description", mockClock, null)) { Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1)); - parentTimerEventEmitter.lap(); + parentTimerEventDispatcher.lap(); Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1).plusNanos(1)); - try (TimerEventEmitter ignored = parentTimerEventEmitter.subTimer("child description")) { + try (TimerEventDispatcher ignored = + parentTimerEventDispatcher.subTimer("child description")) { Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(2)); // Laps on close. } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 91856f42a8..bf98ec892b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; @@ -79,7 +79,7 @@ private static LayerConfiguration makeLayerConfiguration( @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; private Path temporaryCacheDirectory; private LayerConfiguration fakeDependenciesLayerConfiguration; @@ -110,7 +110,7 @@ public void setUp() throws IOException, URISyntaxException { EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); - Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); + Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 7a1640f5b3..c0a400af8c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -47,7 +47,7 @@ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; @Mock private PullBaseImageStep mockPullBaseImageStep; @@ -70,7 +70,7 @@ public void setUp() throws DigestException { Paths.get(""), new BlobDescriptor(testDescriptorDigest), testDescriptorDigest), null); - Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); + Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index c467906f10..a8c652c62a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -40,7 +40,7 @@ @RunWith(MockitoJUnitRunner.class) public class RetrieveRegistryCredentialsStepTest { - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private ListeningExecutorService mockListeningExecutorService; @Test @@ -76,16 +76,16 @@ public void testCall_none() throws CredentialRetrievalException { mockListeningExecutorService, buildConfiguration) .call()); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.info("No credentials could be retrieved for registry baseregistry")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration) .call()); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.info("No credentials could be retrieved for registry baseregistry")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); } @Test @@ -115,7 +115,7 @@ private BuildConfiguration makeFakeBuildConfiguration( ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder() - .setEventEmitter(mockEventEmitter) + .setEventDispatcher(mockEventDispatcher) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(baseCredentialRetrievers) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java similarity index 86% rename from jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java index 578689fbc2..02b084e281 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventEmitterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java @@ -22,8 +22,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link DefaultEventEmitter}. */ -public class DefaultEventEmitterTest { +/** Tests for {@link DefaultEventDispatcher}. */ +public class DefaultEventDispatcherTest { /** Test {@link JibEvent}. */ private static class TestJibEvent1 implements JibEvent {} @@ -32,7 +32,7 @@ private static class TestJibEvent1 implements JibEvent {} private static class TestJibEvent2 implements JibEvent {} @Test - public void testEmit() { + public void testDispatch() { List emissions = new ArrayList<>(); EventHandlers eventHandlers = @@ -51,9 +51,9 @@ public void testEmit() { TestJibEvent1 testJibEvent1 = new TestJibEvent1(); TestJibEvent2 testJibEvent2 = new TestJibEvent2(); - EventEmitter eventEmitter = new DefaultEventEmitter(eventHandlers); - eventEmitter.emit(testJibEvent1); - eventEmitter.emit(testJibEvent2); + EventDispatcher eventDispatcher = new DefaultEventDispatcher(eventHandlers); + eventDispatcher.dispatch(testJibEvent1); + eventDispatcher.dispatch(testJibEvent2); Assert.assertEquals( Arrays.asList( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java index 72b951e672..47ec77efa6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.event.DefaultEventEmitter; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent.Level; @@ -33,17 +33,17 @@ public class LogEventTest { // Note that in actual code, the event handler should NOT perform thread unsafe operations like // here. - private final EventEmitter eventEmitter = - new DefaultEventEmitter( + private final EventDispatcher eventDispatcher = + new DefaultEventDispatcher( new EventHandlers().add(JibEventType.LOGGING, receivedLogEvents::offer)); @Test public void testFactories() { - eventEmitter.emit(LogEvent.error("error")); - eventEmitter.emit(LogEvent.lifecycle("lifecycle")); - eventEmitter.emit(LogEvent.warn("warn")); - eventEmitter.emit(LogEvent.info("info")); - eventEmitter.emit(LogEvent.debug("debug")); + eventDispatcher.dispatch(LogEvent.error("error")); + eventDispatcher.dispatch(LogEvent.lifecycle("lifecycle")); + eventDispatcher.dispatch(LogEvent.warn("warn")); + eventDispatcher.dispatch(LogEvent.info("info")); + eventDispatcher.dispatch(LogEvent.debug("debug")); verifyNextLogEvent(Level.ERROR, "error"); verifyNextLogEvent(Level.LIFECYCLE, "lifecycle"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index e01882ee44..0317c6cd74 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.image.ImageReference; @@ -64,7 +64,7 @@ private static DockerCredentialHelperFactory getTestFactory( }; } - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; @@ -87,7 +87,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("registry", null, null), - mockEventEmitter, + mockEventDispatcher, getTestFactory( "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); @@ -97,8 +97,8 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { .dockerCredentialHelper(Paths.get("docker-credential-helper")) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.info("Using docker-credential-helper for registry")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.info("Using docker-credential-helper for registry")); } @Test @@ -106,7 +106,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.gcr.io", null, null), - mockEventEmitter, + mockEventDispatcher, getTestFactory( "something.gcr.io", Paths.get("docker-credential-gcr"), @@ -118,8 +118,8 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { .inferCredentialHelper() .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); } @Test @@ -127,7 +127,7 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.amazonaws.com", null, null), - mockEventEmitter, + mockEventDispatcher, getTestFactory( "something.amazonaws.com", Paths.get("docker-credential-ecr-login"), @@ -137,15 +137,15 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); - Mockito.verify(mockEventEmitter).emit(LogEvent.warn("warning")); - Mockito.verify(mockEventEmitter).emit(LogEvent.info(" Caused by: the root cause")); + Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn("warning")); + Mockito.verify(mockEventDispatcher).dispatch(LogEvent.info(" Caused by: the root cause")); } @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( - ImageReference.of("registry", null, null), mockEventEmitter); + ImageReference.of("registry", null, null), mockEventDispatcher); Mockito.when(mockDockerConfigCredentialRetriever.retrieve()) .thenReturn(Optional.of(FAKE_CREDENTIALS)); @@ -156,7 +156,7 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException .dockerConfig(mockDockerConfigCredentialRetriever) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.info("Using credentials from Docker config for registry")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.info("Using credentials from Docker config for registry")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 0cdc20f878..9fe35aa93a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.MainClassFinder.Result.Type; import com.google.common.io.Resources; @@ -35,13 +35,13 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); @@ -52,7 +52,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -63,7 +63,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @@ -71,7 +71,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { public void testFindMainClass_multiple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertEquals( MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); @@ -84,7 +84,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException public void testFindMainClass_extension() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -95,7 +95,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -106,7 +106,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -117,7 +117,7 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventEmitter).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 7365002ccb..d95ce2ce39 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; @@ -31,7 +31,7 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { - @Mock private EventEmitter eventEmitter; + @Mock private EventDispatcher eventDispatcher; @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; @@ -39,7 +39,7 @@ public class RegistryClientTest { @Before public void setUp() { testRegistryClientFactory = - RegistryClient.factory(eventEmitter, "some.server.url", "some image name"); + RegistryClient.factory(eventDispatcher, "some.server.url", "some image name"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index c93f1e7486..9b83a8ddeb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -21,7 +21,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorizations; @@ -104,7 +104,7 @@ private static HttpResponse mockRedirectHttpResponse(String redirectLocation) th return mockHttpResponse(code307, new HttpHeaders().setLocation(redirectLocation)); } - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private Connection mockConnection; @Mock private Connection mockInsecureConnection; @Mock private Response mockResponse; @@ -164,8 +164,8 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); } @@ -192,12 +192,12 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @@ -220,8 +220,8 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @@ -343,12 +343,12 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); } @@ -566,7 +566,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) private RegistryEndpointCaller createRegistryEndpointCaller( boolean allowInsecure, int port) throws MalformedURLException { return new RegistryEndpointCaller<>( - mockEventEmitter, + mockEventDispatcher, "userAgent", (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 139feac5bc..c4e0dfbd7a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -84,7 +84,7 @@ public void buildDocker() throws InvalidImageReferenceException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), - gradleProjectProperties.getEventEmitter(), + gradleProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 6abe51205e..e18073839c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -91,10 +91,10 @@ public void buildImage() throws InvalidImageReferenceException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage( - targetImage, gradleProjectProperties.getEventEmitter())); + targetImage, gradleProjectProperties.getEventDispatcher())); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - gradleProjectProperties.getEventEmitter(), + gradleProjectProperties.getEventDispatcher(), "jib.to.auth.username", "jib.to.auth.password", jibExtension.getTo().getAuth()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 1f3ec51431..703a864f50 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -110,7 +110,7 @@ public void buildTar() throws InvalidImageReferenceException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTargetImage(), - gradleProjectProperties.getEventEmitter(), + gradleProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 2f1416fe51..c5a402e025 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.event.DefaultEventEmitter; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -63,7 +63,7 @@ static GradleProjectProperties getForProject( try { return new GradleProjectProperties( project, - makeEventEmitter(logger), + makeEventDispatcher(logger), GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot)); } catch (IOException ex) { @@ -71,9 +71,9 @@ static GradleProjectProperties getForProject( } } - private static EventEmitter makeEventEmitter(Logger logger) { + private static EventDispatcher makeEventDispatcher(Logger logger) { LogEventHandler logEventHandler = new LogEventHandler(logger); - return new DefaultEventEmitter( + return new DefaultEventDispatcher( new EventHandlers() .add(JibEventType.LOGGING, logEventHandler) .add( @@ -92,14 +92,16 @@ static War getWarTask(Project project) { } private final Project project; - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( - Project project, EventEmitter eventEmitter, JavaLayerConfigurations javaLayerConfigurations) { + Project project, + EventDispatcher eventDispatcher, + JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -109,8 +111,8 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { } @Override - public EventEmitter getEventEmitter() { - return eventEmitter; + public EventDispatcher getEventDispatcher() { + return eventDispatcher; } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index cdcee18f6b..4bce2ab586 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -107,10 +107,10 @@ static PluginConfigurationProcessor processCommonConfiguration( } DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventEmitter())); + CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventDispatcher())); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - projectProperties.getEventEmitter(), + projectProperties.getEventDispatcher(), "jib.from.auth.username", "jib.from.auth.password", jibExtension.getFrom().getAuth()); @@ -151,7 +151,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder() .setToolName(GradleProjectProperties.TOOL_NAME) - .setEventEmitter(projectProperties.getEventEmitter()) + .setEventDispatcher(projectProperties.getEventDispatcher()) .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 155bd607eb..70d2b08d3b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -49,7 +49,7 @@ public class GradleProjectPropertiesTest { @Mock private Convention mockConvention; @Mock private WarPluginConvention mockWarPluginConvection; @Mock private TaskContainer mockTaskContainer; - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private Manifest manifest; @@ -67,7 +67,7 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties(mockProject, mockEventEmitter, mockJavaLayerConfigurations); + new GradleProjectProperties(mockProject, mockEventDispatcher, mockJavaLayerConfigurations); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 0d4a850de1..af2240bf9a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -69,7 +69,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenProjectProperties.getEventEmitter(), + mavenProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 2090b3ffb4..a88b5c11a3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -95,10 +95,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage( - targetImage, mavenProjectProperties.getEventEmitter())); + targetImage, mavenProjectProperties.getEventDispatcher())); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - mavenProjectProperties.getEventEmitter(), + mavenProjectProperties.getEventDispatcher(), "jib.to.auth.username", "jib.to.auth.password", getTargetImageAuth()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 65ae04f9ad..5ea2adf1d3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -66,7 +66,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenProjectProperties.getEventEmitter(), + mavenProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index da805d3924..ad90b34d80 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.DefaultEventEmitter; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -66,7 +66,7 @@ static MavenProjectProperties getForProject( try { return new MavenProjectProperties( project, - makeEventEmitter(log), + makeEventDispatcher(log), MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); } catch (IOException ex) { @@ -78,24 +78,24 @@ static MavenProjectProperties getForProject( } } - private static EventEmitter makeEventEmitter(Log log) { - return new DefaultEventEmitter( + private static EventDispatcher makeEventDispatcher(Log log) { + return new DefaultEventDispatcher( new EventHandlers() .add(JibEventType.LOGGING, new LogEventHandler(log)) .add(JibEventType.TIMING, new TimerEventHandler(log::debug))); } private final MavenProject project; - private final EventEmitter eventEmitter; + private final EventDispatcher eventDispatcher; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventEmitter = eventEmitter; + this.eventDispatcher = eventDispatcher; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -105,8 +105,8 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { } @Override - public EventEmitter getEventEmitter() { - return eventEmitter; + public EventDispatcher getEventDispatcher() { + return eventDispatcher; } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index bd13957b7f..34a15ff3ad 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -106,10 +106,10 @@ static PluginConfigurationProcessor processCommonConfiguration( logger); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventEmitter())); + CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventDispatcher())); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - projectProperties.getEventEmitter(), + projectProperties.getEventDispatcher(), "jib.from.auth.username", "jib.from.auth.password", jibPluginConfiguration.getBaseImageAuth()); @@ -158,7 +158,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder() .setToolName(MavenProjectProperties.TOOL_NAME) - .setEventEmitter(projectProperties.getEventEmitter()) + .setEventDispatcher(projectProperties.getEventDispatcher()) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index ccb57b7552..5cfcb5f2a0 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; @@ -34,7 +34,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @@ -48,7 +48,8 @@ public class MavenProjectPropertiesTest { @Before public void setup() { mavenProjectProperties = - new MavenProjectProperties(mockMavenProject, mockEventEmitter, mockJavaLayerConfigurations); + new MavenProjectProperties( + mockMavenProject, mockEventDispatcher, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index a7da47512a..1df8208c16 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -26,7 +26,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; @@ -199,13 +199,14 @@ private static String capitalizeFirstLetter(String string) { */ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { try { - EventEmitter eventEmitter = buildSteps.getBuildConfiguration().getEventEmitter(); + EventDispatcher eventDispatcher = buildSteps.getBuildConfiguration().getEventDispatcher(); - eventEmitter.emit(LogEvent.lifecycle("")); - eventEmitter.emit(LogEvent.lifecycle(startupMessage)); + eventDispatcher.dispatch(LogEvent.lifecycle("")); + eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); // Logs the different source files used. - eventEmitter.emit(LogEvent.info("Containerizing application with the following files:")); + eventDispatcher.dispatch( + LogEvent.info("Containerizing application with the following files:")); for (LayerConfiguration layerConfiguration : buildSteps.getBuildConfiguration().getLayerConfigurations()) { @@ -213,18 +214,18 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut continue; } - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - eventEmitter.emit(LogEvent.info("\t\t" + layerEntry.getSourceFile())); + eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); } } buildSteps.run(); - eventEmitter.emit(LogEvent.lifecycle("")); - eventEmitter.emit(LogEvent.lifecycle(successMessage)); + eventDispatcher.dispatch(LogEvent.lifecycle("")); + eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { throw new BuildStepsExecutionException( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 8e9e47b626..b5333d18c5 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; @@ -33,7 +33,7 @@ public class ConfigurationPropertyValidator { * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. * - * @param eventEmitter the {@link EventEmitter} used to emit log events + * @param eventDispatcher the {@link EventDispatcher} used to dispatch log events * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials @@ -41,7 +41,7 @@ public class ConfigurationPropertyValidator { * {@link Optional#empty} if neither is configured. */ public static Optional getImageCredential( - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, String usernameProperty, String passwordProperty, AuthProperty auth) { @@ -55,7 +55,7 @@ public static Optional getImageCredential( // Warn if a system property is missing if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.warn( passwordProperty + " system property is set, but " @@ -63,7 +63,7 @@ public static Optional getImageCredential( + " is not; attempting other authentication methods.")); } if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.warn( usernameProperty + " system property is set, but " @@ -76,14 +76,14 @@ public static Optional getImageCredential( return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getUsername())) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.warn( auth.getUsernamePropertyDescriptor() + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.warn( auth.getPasswordPropertyDescriptor() + " is missing from build configuration; ignoring auth section.")); @@ -98,7 +98,7 @@ public static Optional getImageCredential( * {@code project-name:project-version} if target image is not configured * * @param targetImage the configured target image reference - * @param eventEmitter the {@link EventEmitter} used to emit log events + * @param eventDispatcher the {@link EventDispatcher} used to dispatch log events * @param projectName the project name, as determined by the plugin * @param projectVersion the project version, as determined by the plugin * @param helpfulSuggestions used for generating the message notifying the user of the generated @@ -110,13 +110,13 @@ public static Optional getImageCredential( */ public static ImageReference getGeneratedTargetDockerTag( @Nullable String targetImage, - EventEmitter eventEmitter, + EventDispatcher eventDispatcher, String projectName, String projectVersion, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { if (Strings.isNullOrEmpty(targetImage)) { - eventEmitter.emit( + eventDispatcher.dispatch( LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(projectName, projectVersion))); // Try to parse generated tag to verify that project name and version are valid (throws an diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 5f2ec2184b..08c29d35e3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -73,8 +73,8 @@ public static String resolveMainClass( Preconditions.checkNotNull(mainClass); if (!isValidJavaClass(mainClass)) { projectProperties - .getEventEmitter() - .emit(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); + .getEventDispatcher() + .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); } return mainClass; @@ -97,8 +97,8 @@ static boolean isValidJavaClass(String className) { @Nullable private static String getMainClassFromJar(ProjectProperties projectProperties) { projectProperties - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.info( "Searching for main class... Add a 'mainClass' configuration to '" + projectProperties.getPluginName() @@ -109,8 +109,8 @@ private static String getMainClassFromJar(ProjectProperties projectProperties) { private static String findMainClassInClassFiles(ProjectProperties projectProperties) throws MainClassInferenceException { projectProperties - .getEventEmitter() - .emit( + .getEventDispatcher() + .dispatch( LogEvent.debug( "Could not find a valid main class specified in " + projectProperties.getJarPluginName() @@ -125,7 +125,7 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert .collect(ImmutableList.toImmutableList()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(classesSourceFiles, projectProperties.getEventEmitter()).find(); + new MainClassFinder(classesSourceFiles, projectProperties.getEventDispatcher()).find(); switch (mainClassFinderResult.getType()) { case MAIN_CLASS_FOUND: diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 808349efd8..6220dc0b88 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.nio.file.Path; import javax.annotation.Nullable; @@ -27,7 +27,7 @@ public interface ProjectProperties { /** Directory name for the cache. The directory will be relative to the build output directory. */ String CACHE_DIRECTORY_NAME = "jib-cache"; - EventEmitter getEventEmitter(); + EventDispatcher getEventDispatcher(); String getPluginName(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index e12b573e9f..ea500e9f2c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; @@ -71,7 +71,7 @@ public class BuildStepsRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildSteps mockBuildSteps; - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @@ -85,7 +85,7 @@ public void setUpMocks() { testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps, "ignored", "ignored"); Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); - Mockito.when(mockBuildConfiguration.getEventEmitter()).thenReturn(mockEventEmitter); + Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); Mockito.when(mockBuildConfiguration.getLayerConfigurations()) .thenReturn( ImmutableList.of( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 18e341ec44..fb0284b85a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -33,7 +33,7 @@ @RunWith(MockitoJUnitRunner.class) public class ConfigurationPropertyValidatorTest { - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; @@ -50,7 +50,7 @@ public void testGetImageAuth() { Credential expected = Credential.basic("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( - mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); @@ -60,17 +60,17 @@ public void testGetImageAuth() { expected = Credential.basic("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); - Mockito.verify(mockEventEmitter, Mockito.never()).emit(LogEvent.warn(Mockito.any())); + Mockito.verify(mockEventDispatcher, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); // Auth completely missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); // Password missing @@ -78,20 +78,22 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); + Mockito.verify(mockEventDispatcher) + .dispatch( + LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); // Username missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventEmitter, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.warn("user is missing from build configuration; ignoring auth section.")); + Mockito.verify(mockEventDispatcher) + .dispatch( + LogEvent.warn("user is missing from build configuration; ignoring auth section.")); } @Test @@ -115,19 +117,20 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept // Target configured ImageReference result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - "a/b:c", mockEventEmitter, "project-name", "project-version", helpfulSuggestions); + "a/b:c", mockEventDispatcher, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockEventEmitter, Mockito.never()).emit(LogEvent.lifecycle(Mockito.any())); + Mockito.verify(mockEventDispatcher, Mockito.never()) + .dispatch(LogEvent.lifecycle(Mockito.any())); // Target not configured result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventEmitter, "project-name", "project-version", helpfulSuggestions); + null, mockEventDispatcher, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockEventEmitter) - .emit( + Mockito.verify(mockEventDispatcher) + .dispatch( LogEvent.lifecycle( "Tagging image with generated image reference project-name:project-version. If you'd " + "like to specify a different tag, you can set the to parameter in your " @@ -136,7 +139,7 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept // Generated tag invalid try { ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventEmitter, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); + null, mockEventDispatcher, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); Assert.fail(); } catch (InvalidImageReferenceException ignored) { } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index ac7af2777c..33d298cba5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.EventEmitter; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; @@ -41,7 +41,7 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassResolverTest { - @Mock private EventEmitter mockEventEmitter; + @Mock private EventDispatcher mockEventDispatcher; @Mock private ProjectProperties mockProjectProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @@ -49,7 +49,7 @@ public class MainClassResolverTest { @Before public void setup() { - Mockito.when(mockProjectProperties.getEventEmitter()).thenReturn(mockEventEmitter); + Mockito.when(mockProjectProperties.getEventDispatcher()).thenReturn(mockEventDispatcher); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) @@ -73,8 +73,8 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -91,8 +91,8 @@ public void testResolveMainClass_multipleInferredWithBackup() .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -128,8 +128,8 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventEmitter) - .emit(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test From fd6477865a9c3dd8632cd64a1cbf66125fcd4829 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 27 Sep 2018 16:11:32 -0400 Subject: [PATCH 0246/2020] New JavaLayerConfiguration API for more flexibility (#1049) * New JavaLayerConfigurations API * Add tests and Javadocs * Remove non-recursive add methods * Use alternative internal fileset representation * Restore CopyDirective extraction path * Add CHANGELOG entries --- .../frontend/JavaDockerContextGenerator.java | 46 ++--- .../jib/frontend/JavaLayerConfigurations.java | 179 ++++++++++-------- .../JavaDockerContextGeneratorTest.java | 79 +++----- .../frontend/JavaLayerConfigurationsTest.java | 136 ++++++++----- .../src/test/resources/random-contents/file1 | 0 .../src/test/resources/random-contents/file2 | 0 .../random-contents/sub-directory/file3 | 0 .../random-contents/sub-directory/file4 | 0 .../random-contents/sub-directory/leaf/file5 | 0 .../random-contents/sub-directory/leaf/file6 | 0 jib-core/src/test/resources/sampleDockerfile | 10 +- jib-gradle-plugin/CHANGELOG.md | 1 + .../jib/gradle/GradleLayerConfigurations.java | 41 ++-- jib-maven-plugin/CHANGELOG.md | 1 + .../jib/maven/MavenLayerConfigurations.java | 41 ++-- 15 files changed, 293 insertions(+), 241 deletions(-) create mode 100644 jib-core/src/test/resources/random-contents/file1 create mode 100644 jib-core/src/test/resources/random-contents/file2 create mode 100644 jib-core/src/test/resources/random-contents/sub-directory/file3 create mode 100644 jib-core/src/test/resources/random-contents/sub-directory/file4 create mode 100644 jib-core/src/test/resources/random-contents/sub-directory/leaf/file5 create mode 100644 jib-core/src/test/resources/random-contents/sub-directory/leaf/file6 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index ebdb4a7476..1d6ab203ac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -29,7 +29,6 @@ import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -98,13 +97,12 @@ private CopyDirective( private static void addIfNotEmpty( ImmutableList.Builder listBuilder, ImmutableList layerEntries, - String directoryInContext, - AbsoluteUnixPath extractionPath) { + String directoryInContext) { if (layerEntries.isEmpty()) { return; } - listBuilder.add(new CopyDirective(layerEntries, directoryInContext, extractionPath)); + listBuilder.add(new CopyDirective(layerEntries, directoryInContext, AbsoluteUnixPath.get("/"))); } /** @@ -153,36 +151,30 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getDependencyLayerEntries(), - DEPENDENCIES_LAYER_DIRECTORY, - javaLayerConfigurations.getDependencyExtractionPath()); + DEPENDENCIES_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getSnapshotDependencyLayerEntries(), - SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY, - javaLayerConfigurations.getSnapshotDependencyExtractionPath()); + SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getResourceLayerEntries(), - RESOURCES_LAYER_DIRECTORY, - javaLayerConfigurations.getResourceExtractionPath()); + RESOURCES_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getClassLayerEntries(), - CLASSES_LAYER_DIRECTORY, - javaLayerConfigurations.getClassExtractionPath()); + CLASSES_LAYER_DIRECTORY); // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, // snapshot dependencies, resources, and classes layers). Should copy files in the right // directories. (For example, "resources" will go into the webapp root.) addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getExplodedWarEntries(), - EXPLODED_WAR_LAYER_DIRECTORY, - javaLayerConfigurations.getExplodedWarExtractionPath()); + EXPLODED_WAR_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getExtraFilesLayerEntries(), - EXTRA_FILES_LAYER_DIRECTORY, - javaLayerConfigurations.getExtraFilesExtractionPath()); + EXTRA_FILES_LAYER_DIRECTORY); copyDirectives = copyDirectivesBuilder.build(); } @@ -280,19 +272,13 @@ public void generate(Path targetDirectory) throws IOException { // Copies the source files to the directoryInContext. for (LayerEntry layerEntry : copyDirective.layerEntries) { - // This resolves the path to copy the source file to in the {@code directory}. - // For example, for a 'baseDirectory' of 'target/jib-docker-context/classes', a - // 'baseExtractionPath' of '/app/classes', and an 'actualExtractionPath' of - // '/app/classes/com/test/HelloWorld.class', the resolved destination would be - // 'target/jib-docker-context/classes/com/test/HelloWorld.class'. - Path baseExtractionPath = Paths.get(copyDirective.extractionPath.toString()); - Path relativeEntryPath = - baseExtractionPath.relativize(Paths.get(layerEntry.getExtractionPath().toString())); - Path destination = directoryInContext.resolve(relativeEntryPath); + String noLeadingSlash = layerEntry.getAbsoluteExtractionPathString().substring(1); + Path destination = directoryInContext.resolve(noLeadingSlash); if (Files.isDirectory(layerEntry.getSourceFile())) { Files.createDirectories(destination); } else { + Files.createDirectories(destination.getParent()); Files.copy(layerEntry.getSourceFile(), destination); } } @@ -309,11 +295,11 @@ public void generate(Path targetDirectory) throws IOException { *

{@code
    * FROM [base image]
    *
-   * COPY libs [path/to/dependencies/]
-   * COPY snapshot-libs [path/to/dependencies/]
-   * COPY resources [path/to/resources/]
-   * COPY classes [path/to/classes/]
-   * COPY root [path/to/root/]
+   * COPY libs /
+   * COPY snapshot-libs /
+   * COPY resources /
+   * COPY classes /
+   * COPY root /
    *
    * EXPOSE [port]
    * [More EXPOSE instructions, if necessary]
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
index 7796fc3620..14d0e35e90 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java
@@ -25,9 +25,7 @@
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.EnumMap;
-import java.util.List;
 import java.util.Map;
 
 /** Builds {@link LayerConfiguration}s for a Java application. */
@@ -65,78 +63,136 @@ String getName() {
   /** Builds with each layer's files. */
   public static class Builder {
 
-    private final Map> layerFilesMap = new EnumMap<>(LayerType.class);
-    private final Map extractionPathMap =
+    private final Map layerBuilders =
         new EnumMap<>(LayerType.class);
 
     private Builder() {
       for (LayerType layerType : LayerType.values()) {
-        layerFilesMap.put(layerType, new ArrayList<>());
-        extractionPathMap.put(layerType, AbsoluteUnixPath.get("/"));
+        layerBuilders.put(layerType, LayerConfiguration.builder());
       }
     }
 
-    public Builder setDependencyFiles(List dependencyFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.DEPENDENCIES, dependencyFiles);
-      extractionPathMap.put(LayerType.DEPENDENCIES, extractionPath);
+    /**
+     * Adds a file to the dependency layer. If the source file is a directory, the directory and its
+     * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive}
+     * for concrete examples about how the file will be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
+    public Builder addDependencyFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.DEPENDENCIES))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
-    public Builder setSnapshotDependencyFiles(
-        List snapshotDependencyFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.SNAPSHOT_DEPENDENCIES, snapshotDependencyFiles);
-      extractionPathMap.put(LayerType.SNAPSHOT_DEPENDENCIES, extractionPath);
+    /**
+     * Adds a file to the snapshot dependency layer. If the source file is a directory, the
+     * directory and its contents will be added recursively. See {@link
+     * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will
+     * be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
+    public Builder addSnapshotDependencyFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.SNAPSHOT_DEPENDENCIES))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
-    public Builder setResourceFiles(List resourceFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.RESOURCES, resourceFiles);
-      extractionPathMap.put(LayerType.RESOURCES, extractionPath);
+    /**
+     * Adds a file to the resource layer. If the source file is a directory, the directory and its
+     * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive}
+     * for concrete examples about how the file will be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
+    public Builder addResourceFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.RESOURCES))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
-    public Builder setClassFiles(List classFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.CLASSES, classFiles);
-      extractionPathMap.put(LayerType.CLASSES, extractionPath);
+    /**
+     * Adds a file to the classes layer. If the source file is a directory, the directory and its
+     * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive}
+     * for concrete examples about how the file will be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
+    public Builder addClassFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.CLASSES))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
-    public Builder setExtraFiles(List extraFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.EXTRA_FILES, extraFiles);
-      extractionPathMap.put(LayerType.EXTRA_FILES, extractionPath);
+    /**
+     * Adds a file to the extra files layer. If the source file is a directory, the directory and
+     * its contents will be added recursively. See {@link
+     * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will
+     * be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
+    public Builder addExtraFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.EXTRA_FILES))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
+    /**
+     * Adds a file to the exploded WAR layer. If the source file is a directory, the directory and
+     * its contents will be added recursively. See {@link
+     * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will
+     * be placed in the image.
+     *
+     * @param sourceFile the source file to add to the layer
+     * @param pathInContainer the path in the container file system corresponding to the {@code
+     *     sourceFile}
+     * @return this
+     * @throws IOException if an exception occurred when recursively listing the directory
+     */
     // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot
     // dependencies, resources, and classes layers).
-    public Builder setExplodedWarFiles(
-        List explodedWarFiles, AbsoluteUnixPath extractionPath) {
-      layerFilesMap.put(LayerType.EXPLODED_WAR, explodedWarFiles);
-      extractionPathMap.put(LayerType.EXPLODED_WAR, extractionPath);
+    public Builder addExplodedWarFile(Path sourceFile, AbsoluteUnixPath pathInContainer)
+        throws IOException {
+      Preconditions.checkNotNull(layerBuilders.get(LayerType.EXPLODED_WAR))
+          .addEntryRecursive(sourceFile, pathInContainer);
       return this;
     }
 
-    public JavaLayerConfigurations build() throws IOException {
+    public JavaLayerConfigurations build() {
       ImmutableMap.Builder layerConfigurationsMap =
-          ImmutableMap.builderWithExpectedSize(LayerType.values().length);
-      for (LayerType layerType : LayerType.values()) {
-        AbsoluteUnixPath extractionPath =
-            Preconditions.checkNotNull(extractionPathMap.get(layerType));
-
-        LayerConfiguration.Builder layerConfigurationBuilder =
-            LayerConfiguration.builder().setName(layerType.getName());
-
-        // Adds all the layer files recursively.
-        List layerFiles = Preconditions.checkNotNull(layerFilesMap.get(layerType));
-        for (Path layerFile : layerFiles) {
-          layerConfigurationBuilder.addEntryRecursive(
-              layerFile, extractionPath.resolve(layerFile.getFileName()));
-        }
+          ImmutableMap.builderWithExpectedSize(layerBuilders.size());
 
-        layerConfigurationsMap.put(layerType, layerConfigurationBuilder.build());
-      }
-      return new JavaLayerConfigurations(
-          layerConfigurationsMap.build(), ImmutableMap.copyOf(extractionPathMap));
+      layerBuilders.forEach(
+          (type, builder) ->
+              layerConfigurationsMap.put(type, builder.setName(type.getName()).build()));
+      return new JavaLayerConfigurations(layerConfigurationsMap.build());
     }
   }
 
@@ -151,13 +207,10 @@ public static Builder builder() {
   public static final String DEFAULT_APP_ROOT = "/app";
 
   private final ImmutableMap layerConfigurationMap;
-  private final ImmutableMap extractionPathMap;
 
   private JavaLayerConfigurations(
-      ImmutableMap layerConfigurationMap,
-      ImmutableMap extractionPathMap) {
+      ImmutableMap layerConfigurationMap) {
     this.layerConfigurationMap = layerConfigurationMap;
-    this.extractionPathMap = extractionPathMap;
   }
 
   public ImmutableList getLayerConfigurations() {
@@ -190,35 +243,7 @@ public ImmutableList getExplodedWarEntries() {
     return getLayerEntries(LayerType.EXPLODED_WAR);
   }
 
-  public AbsoluteUnixPath getDependencyExtractionPath() {
-    return getExtractionPath(LayerType.DEPENDENCIES);
-  }
-
-  public AbsoluteUnixPath getSnapshotDependencyExtractionPath() {
-    return getExtractionPath(LayerType.SNAPSHOT_DEPENDENCIES);
-  }
-
-  public AbsoluteUnixPath getResourceExtractionPath() {
-    return getExtractionPath(LayerType.RESOURCES);
-  }
-
-  public AbsoluteUnixPath getClassExtractionPath() {
-    return getExtractionPath(LayerType.CLASSES);
-  }
-
-  public AbsoluteUnixPath getExtraFilesExtractionPath() {
-    return getExtractionPath(LayerType.EXTRA_FILES);
-  }
-
-  public AbsoluteUnixPath getExplodedWarExtractionPath() {
-    return getExtractionPath(LayerType.EXPLODED_WAR);
-  }
-
   private ImmutableList getLayerEntries(LayerType layerType) {
     return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries();
   }
-
-  private AbsoluteUnixPath getExtractionPath(LayerType layerType) {
-    return Preconditions.checkNotNull(extractionPathMap.get(layerType));
-  }
 }
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
index 86feb505c5..750664416f 100644
--- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
+++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java
@@ -44,15 +44,6 @@
 @RunWith(MockitoJUnitRunner.class)
 public class JavaDockerContextGeneratorTest {
 
-  private static final AbsoluteUnixPath EXPECTED_DEPENDENCIES_PATH =
-      AbsoluteUnixPath.get("/app/libs/");
-  private static final AbsoluteUnixPath EXPECTED_RESOURCES_PATH =
-      AbsoluteUnixPath.get("/app/resources/");
-  private static final AbsoluteUnixPath EXPECTED_CLASSES_PATH =
-      AbsoluteUnixPath.get("/app/classes/");
-  private static final AbsoluteUnixPath EXPECTED_EXPLODED_WAR_PATH =
-      AbsoluteUnixPath.get("/exploded/war/");
-
   private static void assertSameFiles(Path directory1, Path directory2) throws IOException {
     ImmutableList directory1Files =
         new DirectoryWalker(directory1)
@@ -73,15 +64,16 @@ private static void assertSameFiles(Path directory1, Path directory2) throws IOE
 
   @Mock private JavaLayerConfigurations mockJavaLayerConfigurations;
 
-  private ImmutableList filesToLayerEntries(
-      Path directory, AbsoluteUnixPath extractionPathRoot) throws IOException {
+  private ImmutableList filesToLayerEntries(Path directory, String extractionRootString)
+      throws IOException {
+    AbsoluteUnixPath extractionRoot = AbsoluteUnixPath.get(extractionRootString);
     return new DirectoryWalker(directory)
         .walk()
         .stream()
         .map(
             sourceFile ->
                 new LayerEntry(
-                    sourceFile, extractionPathRoot.resolve(directory.relativize(sourceFile))))
+                    sourceFile, extractionRoot.resolve(directory.relativize(sourceFile))))
         .collect(ImmutableList.toImmutableList());
   }
 
@@ -104,43 +96,28 @@ public void testGenerate() throws IOException, URISyntaxException {
     Files.delete(targetDirectory);
 
     Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries())
-        .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH));
+        .thenReturn(filesToLayerEntries(testDependencies, "/app/libs"));
     Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries())
-        .thenReturn(filesToLayerEntries(testSnapshotDependencies, EXPECTED_DEPENDENCIES_PATH));
+        .thenReturn(filesToLayerEntries(testSnapshotDependencies, "/snapshots"));
     Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries())
-        .thenReturn(filesToLayerEntries(testResources, EXPECTED_RESOURCES_PATH));
+        .thenReturn(filesToLayerEntries(testResources, "/more/resources"));
     Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries())
-        .thenReturn(filesToLayerEntries(testClasses, EXPECTED_CLASSES_PATH));
+        .thenReturn(filesToLayerEntries(testClasses, "/my/classes"));
     Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries())
-        .thenReturn(filesToLayerEntries(testExplodedWarFiles, EXPECTED_EXPLODED_WAR_PATH));
+        .thenReturn(filesToLayerEntries(testExplodedWarFiles, "/exploded/war"));
     Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries())
-        .thenReturn(filesToLayerEntries(testExtraFiles, AbsoluteUnixPath.get("/")));
-    Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries())
-        .thenReturn(filesToLayerEntries(testDependencies, EXPECTED_DEPENDENCIES_PATH));
-
-    Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath())
-        .thenReturn(EXPECTED_RESOURCES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath())
-        .thenReturn(EXPECTED_CLASSES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath())
-        .thenReturn(EXPECTED_EXPLODED_WAR_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath())
-        .thenReturn(AbsoluteUnixPath.get("/"));
+        .thenReturn(filesToLayerEntries(testExtraFiles, "/"));
 
     new JavaDockerContextGenerator(mockJavaLayerConfigurations)
         .setBaseImage("somebaseimage")
         .generate(targetDirectory);
 
     Assert.assertTrue(Files.exists(targetDirectory.resolve("Dockerfile")));
-    assertSameFiles(targetDirectory.resolve("libs"), testDependencies);
-    assertSameFiles(targetDirectory.resolve("snapshot-libs"), testSnapshotDependencies);
-    assertSameFiles(targetDirectory.resolve("resources"), testResources);
-    assertSameFiles(targetDirectory.resolve("classes"), testClasses);
-    assertSameFiles(targetDirectory.resolve("exploded-war"), testExplodedWarFiles);
+    assertSameFiles(targetDirectory.resolve("libs/app/libs"), testDependencies);
+    assertSameFiles(targetDirectory.resolve("snapshot-libs/snapshots"), testSnapshotDependencies);
+    assertSameFiles(targetDirectory.resolve("resources/more/resources"), testResources);
+    assertSameFiles(targetDirectory.resolve("classes/my/classes"), testClasses);
+    assertSameFiles(targetDirectory.resolve("exploded-war/exploded/war"), testExplodedWarFiles);
     assertSameFiles(targetDirectory.resolve("root"), testExtraFiles);
   }
 
@@ -163,31 +140,21 @@ public void testMakeDockerfile() throws IOException {
 
     Path ignored = Paths.get("ignored");
     Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries())
-        .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_DEPENDENCIES_PATH)));
+        .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/app/libs"))));
     Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries())
-        .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_DEPENDENCIES_PATH)));
+        .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/snapshots"))));
     Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries())
-        .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_RESOURCES_PATH)));
+        .thenReturn(
+            ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/resources"))));
     Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries())
-        .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_CLASSES_PATH)));
+        .thenReturn(
+            ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/classes/"))));
     Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries())
-        .thenReturn(ImmutableList.of(new LayerEntry(ignored, EXPECTED_EXPLODED_WAR_PATH)));
+        .thenReturn(
+            ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/exploded/war"))));
     Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries())
         .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/"))));
 
-    Mockito.when(mockJavaLayerConfigurations.getDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyExtractionPath())
-        .thenReturn(EXPECTED_DEPENDENCIES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getResourceExtractionPath())
-        .thenReturn(EXPECTED_RESOURCES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getClassExtractionPath())
-        .thenReturn(EXPECTED_CLASSES_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExplodedWarExtractionPath())
-        .thenReturn(EXPECTED_EXPLODED_WAR_PATH);
-    Mockito.when(mockJavaLayerConfigurations.getExtraFilesExtractionPath())
-        .thenReturn(AbsoluteUnixPath.get("/"));
-
     String dockerfile =
         new JavaDockerContextGenerator(mockJavaLayerConfigurations)
             .setBaseImage(expectedBaseImage)
diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
index d126b5a003..47315c2242 100644
--- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
+++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java
@@ -3,13 +3,16 @@
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
 import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.image.LayerEntry;
+import com.google.common.io.Resources;
 import java.io.IOException;
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.Test;
@@ -19,22 +22,13 @@ public class JavaLayerConfigurationsTest {
 
   private static JavaLayerConfigurations createFakeConfigurations() throws IOException {
     return JavaLayerConfigurations.builder()
-        .setDependencyFiles(
-            Collections.singletonList(Paths.get("dependency")),
-            AbsoluteUnixPath.get("/dependency/path"))
-        .setSnapshotDependencyFiles(
-            Collections.singletonList(Paths.get("snapshot dependency")),
-            AbsoluteUnixPath.get("/snapshots"))
-        .setResourceFiles(
-            Collections.singletonList(Paths.get("resource")),
-            AbsoluteUnixPath.get("/resources/here"))
-        .setClassFiles(
-            Collections.singletonList(Paths.get("class")), AbsoluteUnixPath.get("/classes/go/here"))
-        .setExplodedWarFiles(
-            Collections.singletonList(Paths.get("exploded war")), AbsoluteUnixPath.get("/for/war"))
-        .setExtraFiles(
-            Collections.singletonList(Paths.get("extra file")),
-            AbsoluteUnixPath.get("/some/extras"))
+        .addDependencyFile(Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path"))
+        .addSnapshotDependencyFile(
+            Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots"))
+        .addResourceFile(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here"))
+        .addClassFile(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here"))
+        .addExplodedWarFile(Paths.get("exploded war"), AbsoluteUnixPath.get("/for/war"))
+        .addExtraFile(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras"))
         .build();
   }
 
@@ -42,6 +36,41 @@ private static List layerEntriesToSourceFiles(List entries) {
     return entries.stream().map(LayerEntry::getSourceFile).collect(Collectors.toList());
   }
 
+  private static List layerEntriesToExtractionPaths(List entries) {
+    return entries.stream().map(LayerEntry::getExtractionPath).collect(Collectors.toList());
+  }
+
+  private static  List toSortedStrings(List paths) {
+    return paths.stream().map(T::toString).sorted().collect(Collectors.toList());
+  }
+
+  private static void verifyRecursiveAdd(
+      Supplier> layerEntriesSupplier, Path sourceRoot, String extractionRoot) {
+    AbsoluteUnixPath extractionRootPath = AbsoluteUnixPath.get(extractionRoot);
+    List expectedPaths =
+        Arrays.asList(
+            "",
+            "file1",
+            "file2",
+            "sub-directory",
+            "sub-directory/file3",
+            "sub-directory/file4",
+            "sub-directory/leaf",
+            "sub-directory/leaf/file5",
+            "sub-directory/leaf/file6");
+
+    List expectedSourcePaths =
+        expectedPaths.stream().map(sourceRoot::resolve).collect(Collectors.toList());
+    List expectedTargetPaths =
+        expectedPaths.stream().map(extractionRootPath::resolve).collect(Collectors.toList());
+
+    List sourcePaths = layerEntriesToSourceFiles(layerEntriesSupplier.get());
+    Assert.assertEquals(toSortedStrings(expectedSourcePaths), toSortedStrings(sourcePaths));
+
+    List targetPaths = layerEntriesToExtractionPaths(layerEntriesSupplier.get());
+    Assert.assertEquals(toSortedStrings(expectedTargetPaths), toSortedStrings(targetPaths));
+  }
+
   @Test
   public void testLabels() throws IOException {
     JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations();
@@ -58,7 +87,7 @@ public void testLabels() throws IOException {
   }
 
   @Test
-  public void testSetFiles_files() throws IOException {
+  public void testAddFile() throws IOException {
     JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations();
 
     List> expectedFiles =
@@ -80,39 +109,60 @@ public void testSetFiles_files() throws IOException {
   }
 
   @Test
-  public void testSetFiles_extractionPaths() throws IOException {
-    JavaLayerConfigurations configurations = createFakeConfigurations();
+  public void testAddFile_directories() throws IOException, URISyntaxException {
+    Path sourceDirectory = Paths.get(Resources.getResource("random-contents").toURI());
 
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/dependency/path"), configurations.getDependencyExtractionPath());
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/snapshots"), configurations.getSnapshotDependencyExtractionPath());
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/resources/here"), configurations.getResourceExtractionPath());
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/classes/go/here"), configurations.getClassExtractionPath());
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/for/war"), configurations.getExplodedWarExtractionPath());
-    Assert.assertEquals(
-        AbsoluteUnixPath.get("/some/extras"), configurations.getExtraFilesExtractionPath());
+    JavaLayerConfigurations configurations =
+        JavaLayerConfigurations.builder()
+            .addDependencyFile(sourceDirectory, AbsoluteUnixPath.get("/libs/dir"))
+            .addSnapshotDependencyFile(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target"))
+            .addResourceFile(sourceDirectory, AbsoluteUnixPath.get("/resources"))
+            .addClassFile(sourceDirectory, AbsoluteUnixPath.get("/classes/here"))
+            .addExplodedWarFile(sourceDirectory, AbsoluteUnixPath.get("/exploded-war"))
+            .addExtraFile(sourceDirectory, AbsoluteUnixPath.get("/extra/files"))
+            .build();
+
+    verifyRecursiveAdd(configurations::getDependencyLayerEntries, sourceDirectory, "/libs/dir");
+    verifyRecursiveAdd(
+        configurations::getSnapshotDependencyLayerEntries, sourceDirectory, "/snapshots/target");
+    verifyRecursiveAdd(configurations::getResourceLayerEntries, sourceDirectory, "/resources");
+    verifyRecursiveAdd(configurations::getClassLayerEntries, sourceDirectory, "/classes/here");
+    verifyRecursiveAdd(configurations::getExplodedWarEntries, sourceDirectory, "/exploded-war");
+    verifyRecursiveAdd(configurations::getExtraFilesLayerEntries, sourceDirectory, "/extra/files");
+  }
+
+  @Test
+  public void testAddFile_regularFiles() throws IOException, URISyntaxException {
+    Path sourceFile =
+        Paths.get(Resources.getResource("random-contents/sub-directory/leaf/file6").toURI());
+
+    JavaLayerConfigurations configurations =
+        JavaLayerConfigurations.builder()
+            .addDependencyFile(sourceFile, AbsoluteUnixPath.get("/libs/file"))
+            .addSnapshotDependencyFile(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file"))
+            .addResourceFile(sourceFile, AbsoluteUnixPath.get("/resources-file"))
+            .addClassFile(sourceFile, AbsoluteUnixPath.get("/classes/file"))
+            .addExplodedWarFile(sourceFile, AbsoluteUnixPath.get("/exploded-war/file"))
+            .addExtraFile(sourceFile, AbsoluteUnixPath.get("/some/file"))
+            .build();
 
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/dependency/path/dependency"),
-        configurations.getDependencyLayerEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/libs/file"))),
+        configurations.getDependencyLayerEntries());
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/snapshots/snapshot dependency"),
-        configurations.getSnapshotDependencyLayerEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file"))),
+        configurations.getSnapshotDependencyLayerEntries());
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/resources/here/resource"),
-        configurations.getResourceLayerEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/resources-file"))),
+        configurations.getResourceLayerEntries());
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/classes/go/here/class"),
-        configurations.getClassLayerEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/classes/file"))),
+        configurations.getClassLayerEntries());
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/for/war/exploded war"),
-        configurations.getExplodedWarEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/exploded-war/file"))),
+        configurations.getExplodedWarEntries());
     Assert.assertEquals(
-        AbsoluteUnixPath.get("/some/extras/extra file"),
-        configurations.getExtraFilesLayerEntries().get(0).getExtractionPath());
+        Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/some/file"))),
+        configurations.getExtraFilesLayerEntries());
   }
 }
diff --git a/jib-core/src/test/resources/random-contents/file1 b/jib-core/src/test/resources/random-contents/file1
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/random-contents/file2 b/jib-core/src/test/resources/random-contents/file2
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/random-contents/sub-directory/file3 b/jib-core/src/test/resources/random-contents/sub-directory/file3
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/random-contents/sub-directory/file4 b/jib-core/src/test/resources/random-contents/sub-directory/file4
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/random-contents/sub-directory/leaf/file5 b/jib-core/src/test/resources/random-contents/sub-directory/leaf/file5
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/random-contents/sub-directory/leaf/file6 b/jib-core/src/test/resources/random-contents/sub-directory/leaf/file6
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile
index 65c8f0a015..3f52564c13 100644
--- a/jib-core/src/test/resources/sampleDockerfile
+++ b/jib-core/src/test/resources/sampleDockerfile
@@ -1,10 +1,10 @@
 FROM somebaseimage
 
-COPY libs /app/libs/
-COPY snapshot-libs /app/libs/
-COPY resources /app/resources/
-COPY classes /app/classes/
-COPY exploded-war /exploded/war/
+COPY libs /
+COPY snapshot-libs /
+COPY resources /
+COPY classes /
+COPY exploded-war /
 COPY root /
 
 EXPOSE 1000/tcp
diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md
index 046a3f6e5f..c9445f9889 100644
--- a/jib-gradle-plugin/CHANGELOG.md
+++ b/jib-gradle-plugin/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
 ### Changed
 
 - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461))
+- `jibExportDockerContext` generates different directory layout and `Dockerfile`.
 
 ### Fixed
 
diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
index 6da4247f33..6c70fd2f7d 100644
--- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
+++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java
@@ -19,6 +19,7 @@
 import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
+import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -119,21 +120,31 @@ static JavaLayerConfigurations getForProject(
     Collections.sort(classesFiles);
     Collections.sort(extraFiles);
 
-    return JavaLayerConfigurations.builder()
-        .setDependencyFiles(
-            dependenciesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
-        .setSnapshotDependencyFiles(
-            snapshotDependenciesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
-        .setResourceFiles(
-            resourcesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE))
-        .setClassFiles(
-            classesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE))
-        .setExtraFiles(extraFiles, AbsoluteUnixPath.get("/"))
-        .build();
+    AbsoluteUnixPath dependenciesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE);
+    AbsoluteUnixPath resourcesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE);
+    AbsoluteUnixPath classesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE);
+
+    Builder layerBuilder = JavaLayerConfigurations.builder();
+    for (Path file : dependenciesFiles) {
+      layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : snapshotDependenciesFiles) {
+      layerBuilder.addSnapshotDependencyFile(
+          file, dependenciesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : resourcesFiles) {
+      layerBuilder.addResourceFile(file, resourcesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : classesFiles) {
+      layerBuilder.addClassFile(file, classesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : extraFiles) {
+      layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName()));
+    }
+    return layerBuilder.build();
   }
 
   private GradleLayerConfigurations() {}
diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md
index 6d5cd24dd0..127e7f2be9 100644
--- a/jib-maven-plugin/CHANGELOG.md
+++ b/jib-maven-plugin/CHANGELOG.md
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
 ### Changed
 
 - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461))
+- `jib:exportDockerContext` generates different directory layout and `Dockerfile`.
 
 ### Fixed
 
diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
index 56c90c05b9..66a2ee969c 100644
--- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
+++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java
@@ -19,6 +19,7 @@
 import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
 import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor;
 import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations;
+import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder;
 import java.io.IOException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
@@ -106,21 +107,31 @@ static JavaLayerConfigurations getForProject(
     Collections.sort(classesFiles);
     Collections.sort(extraFiles);
 
-    return JavaLayerConfigurations.builder()
-        .setDependencyFiles(
-            dependenciesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
-        .setSnapshotDependencyFiles(
-            snapshotDependenciesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE))
-        .setResourceFiles(
-            resourcesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE))
-        .setClassFiles(
-            classesFiles,
-            appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE))
-        .setExtraFiles(extraFiles, AbsoluteUnixPath.get("/"))
-        .build();
+    AbsoluteUnixPath dependenciesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE);
+    AbsoluteUnixPath resourcesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE);
+    AbsoluteUnixPath classesExtractionPath =
+        appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE);
+
+    Builder layerBuilder = JavaLayerConfigurations.builder();
+    for (Path file : dependenciesFiles) {
+      layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : snapshotDependenciesFiles) {
+      layerBuilder.addSnapshotDependencyFile(
+          file, dependenciesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : resourcesFiles) {
+      layerBuilder.addResourceFile(file, resourcesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : classesFiles) {
+      layerBuilder.addClassFile(file, classesExtractionPath.resolve(file.getFileName()));
+    }
+    for (Path file : extraFiles) {
+      layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName()));
+    }
+    return layerBuilder.build();
   }
 
   private MavenLayerConfigurations() {}

From c37ed3fd056b23b203d4ab86c6e3dbdf828ea165 Mon Sep 17 00:00:00 2001
From: Q Chen 
Date: Thu, 27 Sep 2018 13:41:01 -0700
Subject: [PATCH 0247/2020] Changes the builder steps to use the new
 ncache.Cache. (#1047)

- Changes the builder steps to use the new `ncache.Cache`.
- Removes the old `cache` package.
---
 .../builder/BuildStepsIntegrationTest.java    |  67 ++---
 .../registry/BlobPullerIntegrationTest.java   |  23 +-
 .../tools/jib/api/JibContainerBuilder.java    |   4 +-
 .../cloud/tools/jib/builder/BuildSteps.java   |  45 +---
 .../BuildAndCacheApplicationLayerStep.java    |  53 ++--
 .../jib/builder/steps/BuildImageStep.java     |  49 +++-
 .../jib/builder/steps/LoadDockerStep.java     |   4 +-
 .../steps/PullAndCacheBaseImageLayerStep.java |  42 ++-
 .../PullAndCacheBaseImageLayersStep.java      |   5 -
 .../jib/builder/steps/PullBaseImageStep.java  |  10 +-
 .../steps/PushContainerConfigurationStep.java |  12 +-
 .../jib/builder/steps/PushLayersStep.java     |  30 +--
 .../tools/jib/builder/steps/StepsRunner.java  |  49 +---
 .../jib/builder/steps/WriteTarFileStep.java   |   4 +-
 .../google/cloud/tools/jib/cache/Cache.java   | 143 ----------
 .../CacheDirectoryNotOwnedException.java      |  39 ---
 .../cloud/tools/jib/cache/CacheFiles.java     |  43 ---
 .../cloud/tools/jib/cache/CacheMetadata.java  | 168 ------------
 .../CacheMetadataCorruptedException.java      |  32 ---
 .../jib/cache/CacheMetadataTranslator.java    | 126 ---------
 .../cloud/tools/jib/cache/CacheReader.java    | 157 -----------
 .../cloud/tools/jib/cache/CacheWriter.java    | 151 -----------
 .../cloud/tools/jib/cache/CachedLayer.java    |  87 -------
 .../jib/cache/CachedLayerWithMetadata.java    |  37 ---
 .../google/cloud/tools/jib/cache/Caches.java  | 142 ----------
 .../cloud/tools/jib/cache/LayerMetadata.java  |  91 -------
 .../CacheMetadataLayerObjectTemplate.java     |  89 -------
 ...MetadataLayerPropertiesObjectTemplate.java |  83 ------
 .../jib/cache/json/CacheMetadataTemplate.java |  75 ------
 .../jib/configuration/BuildConfiguration.java |  62 +++--
 .../jib/configuration/CacheConfiguration.java |   2 +-
 .../CacheDirectoryCreationException.java      |  10 +-
 .../jib/docker/ImageToTarballTranslator.java  |  21 +-
 .../jib/image/ReproducibleLayerBuilder.java   |  17 +-
 .../cloud/tools/jib/image/UnwrittenLayer.java |  55 ----
 .../jib/image/json/ImageToJsonTranslator.java |  10 +-
 .../tools/jib/registry/RegistryClient.java    |  24 +-
 .../cloud/tools/jib/tar/TarStreamBuilder.java |  14 +
 .../jib/api/JibContainerBuilderTest.java      |   5 +-
 ...BuildAndCacheApplicationLayerStepTest.java | 171 ++++++------
 .../jib/builder/steps/BuildImageStepTest.java |  50 +++-
 .../RetrieveRegistryCredentialsStepTest.java  |  13 +-
 .../cloud/tools/jib/cache/CacheFilesTest.java |  65 -----
 .../tools/jib/cache/CacheMetadataTest.java    | 244 ------------------
 .../cache/CacheMetadataTranslatorTest.java    | 154 -----------
 .../tools/jib/cache/CacheReaderTest.java      | 214 ---------------
 .../cloud/tools/jib/cache/CacheTest.java      | 185 -------------
 .../tools/jib/cache/CacheWriterTest.java      | 223 ----------------
 .../tools/jib/cache/CachedLayerTest.java      |  69 -----
 .../cache/CachedLayerWithMetadataTest.java    |  33 ---
 .../cloud/tools/jib/cache/CachesTest.java     |  74 ------
 .../cache/json/CacheMetadataTemplateTest.java | 135 ----------
 .../configuration/BuildConfigurationTest.java |  25 +-
 .../configuration/CacheConfigurationTest.java |   1 -
 .../tools/jib/docker/DockerClientTest.java    |   4 +-
 .../docker/ImageToTarballTranslatorTest.java  |  31 ++-
 .../tools/jib/image/ImageLayersTest.java      |  41 ++-
 .../cloud/tools/jib/image/LayerTest.java      |  23 --
 .../image/ReproducibleLayerBuilderTest.java   |  10 +-
 .../image/json/ImageToJsonTranslatorTest.java |  27 +-
 .../tools/jib/tar/TarStreamBuilderTest.java   |  20 +-
 .../tools/jib/gradle/BuildDockerTask.java     |  15 +-
 .../tools/jib/gradle/BuildImageTask.java      |  14 +-
 .../cloud/tools/jib/gradle/BuildTarTask.java  |  18 +-
 .../tools/jib/maven/BuildDockerMojo.java      |  10 +-
 .../cloud/tools/jib/maven/BuildImageMojo.java |  50 ++--
 .../cloud/tools/jib/maven/BuildTarMojo.java   |  10 +-
 .../jib/plugins/common/BuildStepsRunner.java  |  66 +----
 .../plugins/common/BuildStepsRunnerTest.java  | 114 +-------
 69 files changed, 538 insertions(+), 3651 deletions(-)
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java
 rename jib-core/src/main/java/com/google/cloud/tools/jib/{cache => configuration}/CacheDirectoryCreationException.java (74%)
 delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java
 delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java

diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
index e5a6685e0e..66bc62e8ed 100644
--- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
+++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java
@@ -17,11 +17,9 @@
 package com.google.cloud.tools.jib.builder;
 
 import com.google.cloud.tools.jib.Command;
-import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException;
-import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException;
-import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException;
-import com.google.cloud.tools.jib.cache.Caches;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
+import com.google.cloud.tools.jib.configuration.CacheConfiguration;
+import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException;
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
 import com.google.cloud.tools.jib.configuration.ImageConfiguration;
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
@@ -123,10 +121,10 @@ public void setUp() throws IOException, URISyntaxException {
 
   @Test
   public void testSteps_forBuildToDockerRegistry()
-      throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+      throws IOException, InterruptedException, ExecutionException,
+          CacheDirectoryCreationException {
     BuildSteps buildImageSteps =
-        getBuildSteps(
+        BuildSteps.forBuildToDockerRegistry(
             getBuildConfigurationBuilder(
                     ImageReference.of("gcr.io", "distroless/java", "latest"),
                     ImageReference.of("localhost:5000", "testimage", "testtag"))
@@ -148,10 +146,10 @@ public void testSteps_forBuildToDockerRegistry()
 
   @Test
   public void testSteps_forBuildToDockerRegistry_multipleTags()
-      throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+      throws IOException, InterruptedException, ExecutionException,
+          CacheDirectoryCreationException {
     BuildSteps buildImageSteps =
-        getBuildSteps(
+        BuildSteps.forBuildToDockerRegistry(
             getBuildConfigurationBuilder(
                     ImageReference.of("gcr.io", "distroless/java", "latest"),
                     ImageReference.of("localhost:5000", "testimage", "testtag"))
@@ -188,10 +186,9 @@ public void testSteps_forBuildToDockerRegistry_multipleTags()
 
   @Test
   public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage()
-      throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException,
-          CacheDirectoryCreationException, CacheMetadataCorruptedException,
-          CacheDirectoryNotOwnedException {
-    getBuildSteps(
+      throws InvalidImageReferenceException, IOException, InterruptedException,
+          CacheDirectoryCreationException, ExecutionException {
+    BuildSteps.forBuildToDockerRegistry(
             getBuildConfigurationBuilder(
                     ImageReference.parse("openjdk:8-jre-alpine"),
                     ImageReference.of("localhost:5000", "testimage", "testtag"))
@@ -206,19 +203,15 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage()
 
   @Test
   public void testSteps_forBuildToDockerDaemon()
-      throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+      throws IOException, InterruptedException, ExecutionException,
+          CacheDirectoryCreationException {
     String imageReference = "testdocker";
     BuildConfiguration buildConfiguration =
         getBuildConfigurationBuilder(
                 ImageReference.of("gcr.io", "distroless/java", "latest"),
                 ImageReference.of(null, imageReference, null))
             .build();
-    Path cacheDirectory = temporaryFolder.newFolder().toPath();
-    BuildSteps.forBuildToDockerDaemon(
-            buildConfiguration,
-            new Caches.Initializer(cacheDirectory, false, cacheDirectory, false))
-        .run();
+    BuildSteps.forBuildToDockerDaemon(buildConfiguration).run();
 
     assertDockerInspect(imageReference);
     Assert.assertEquals(
@@ -227,8 +220,8 @@ public void testSteps_forBuildToDockerDaemon()
 
   @Test
   public void testSteps_forBuildToDockerDaemon_multipleTags()
-      throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+      throws IOException, InterruptedException, ExecutionException,
+          CacheDirectoryCreationException {
     String imageReference = "testdocker";
     BuildConfiguration buildConfiguration =
         getBuildConfigurationBuilder(
@@ -236,11 +229,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags()
                 ImageReference.of(null, imageReference, null))
             .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3"))
             .build();
-    Path cacheDirectory = temporaryFolder.newFolder().toPath();
-    BuildSteps.forBuildToDockerDaemon(
-            buildConfiguration,
-            new Caches.Initializer(cacheDirectory, false, cacheDirectory, false))
-        .run();
+    BuildSteps.forBuildToDockerDaemon(buildConfiguration).run();
 
     assertDockerInspect(imageReference);
     Assert.assertEquals(
@@ -257,34 +246,23 @@ public void testSteps_forBuildToDockerDaemon_multipleTags()
 
   @Test
   public void testSteps_forBuildToTarball()
-      throws IOException, InterruptedException, CacheMetadataCorruptedException, ExecutionException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+      throws IOException, InterruptedException, ExecutionException,
+          CacheDirectoryCreationException {
     BuildConfiguration buildConfiguration =
         getBuildConfigurationBuilder(
                 ImageReference.of("gcr.io", "distroless/java", "latest"),
                 ImageReference.of(null, "testtar", null))
             .build();
     Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar");
-    Path cacheDirectory = temporaryFolder.newFolder().toPath();
-    BuildSteps.forBuildToTar(
-            outputPath,
-            buildConfiguration,
-            new Caches.Initializer(cacheDirectory, false, cacheDirectory, false))
-        .run();
+    BuildSteps.forBuildToTar(outputPath, buildConfiguration).run();
 
     new Command("docker", "load", "--input", outputPath.toString()).run();
     Assert.assertEquals(
         "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run());
   }
 
-  private BuildSteps getBuildSteps(BuildConfiguration buildConfiguration) throws IOException {
-    Path cacheDirectory = temporaryFolder.newFolder().toPath();
-    return BuildSteps.forBuildToDockerRegistry(
-        buildConfiguration, new Caches.Initializer(cacheDirectory, false, cacheDirectory, false));
-  }
-
   private BuildConfiguration.Builder getBuildConfigurationBuilder(
-      ImageReference baseImage, ImageReference targetImage) {
+      ImageReference baseImage, ImageReference targetImage) throws IOException {
     ImageConfiguration baseImageConfiguration = ImageConfiguration.builder(baseImage).build();
     ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build();
     ContainerConfiguration containerConfiguration =
@@ -298,10 +276,13 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder(
                 ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp")))
             .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2"))
             .build();
+    Path cacheDirectory = temporaryFolder.newFolder().toPath();
     return BuildConfiguration.builder()
         .setBaseImageConfiguration(baseImageConfiguration)
         .setTargetImageConfiguration(targetImageConfiguration)
         .setContainerConfiguration(containerConfiguration)
+        .setBaseImageLayersCacheConfiguration(CacheConfiguration.forPath(cacheDirectory))
+        .setApplicationLayersCacheConfiguration(CacheConfiguration.forPath(cacheDirectory))
         .setAllowInsecureRegistries(true)
         .setLayerConfigurations(fakeLayerConfigurations)
         .setToolName("jib-integration-test");
diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java
index bd76ef03e6..3529608f1b 100644
--- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java
+++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java
@@ -22,7 +22,6 @@
 import com.google.cloud.tools.jib.image.json.V21ManifestTemplate;
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.security.DigestException;
 import org.hamcrest.CoreMatchers;
 import org.junit.Assert;
@@ -30,7 +29,6 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
-import org.mockito.Mockito;
 
 /** Integration tests for {@link BlobPuller}. */
 public class BlobPullerIntegrationTest {
@@ -56,28 +54,31 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti
     // Pulls a layer BLOB of the busybox image.
     CountingDigestOutputStream layerOutputStream =
         new CountingDigestOutputStream(ByteStreams.nullOutputStream());
-    registryClient.pullBlob(realDigest, layerOutputStream);
+    registryClient.pullBlob(realDigest).writeTo(layerOutputStream);
 
     Assert.assertEquals(realDigest, layerOutputStream.toBlobDescriptor().getDigest());
   }
 
   @Test
-  public void testPull_unknownBlob()
-      throws RegistryException, IOException, DigestException, InterruptedException {
+  public void testPull_unknownBlob() throws IOException, DigestException, InterruptedException {
     localRegistry.pullAndPushToLocal("busybox", "busybox");
     DescriptorDigest nonexistentDigest =
         DescriptorDigest.fromHash(
             "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
 
+    RegistryClient registryClient =
+        RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox")
+            .setAllowInsecureRegistries(true)
+            .newRegistryClient();
+
     try {
-      RegistryClient registryClient =
-          RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox")
-              .setAllowInsecureRegistries(true)
-              .newRegistryClient();
-      registryClient.pullBlob(nonexistentDigest, Mockito.mock(OutputStream.class));
+      registryClient.pullBlob(nonexistentDigest).writeTo(ByteStreams.nullOutputStream());
       Assert.fail("Trying to pull nonexistent blob should have errored");
 
-    } catch (RegistryErrorException ex) {
+    } catch (IOException ex) {
+      if (!(ex.getCause() instanceof RegistryErrorException)) {
+        throw ex;
+      }
       Assert.assertThat(
           ex.getMessage(),
           CoreMatchers.containsString(
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
index 796a7f0b8c..9386f12b86 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java
@@ -18,6 +18,7 @@
 // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up.
 
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
+import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException;
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
 import com.google.cloud.tools.jib.configuration.Port;
@@ -302,7 +303,8 @@ public JibContainerBuilder addLabel(String key, String value) {
   }
 
   @VisibleForTesting
-  BuildConfiguration toBuildConfiguration(TargetImage targetImage) {
+  BuildConfiguration toBuildConfiguration(TargetImage targetImage)
+      throws IOException, CacheDirectoryCreationException {
     BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder();
 
     buildConfigurationBuilder
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java
index d6c6e9f185..e6702b080a 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java
@@ -17,14 +17,8 @@
 package com.google.cloud.tools.jib.builder;
 
 import com.google.cloud.tools.jib.builder.steps.StepsRunner;
-import com.google.cloud.tools.jib.cache.Cache;
-import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException;
-import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException;
-import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException;
-import com.google.cloud.tools.jib.cache.Caches;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.event.events.LogEvent;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.concurrent.ExecutionException;
 
@@ -46,15 +40,12 @@ private interface StepsRunnerConsumer {
    * All the steps to build an image to a Docker registry.
    *
    * @param buildConfiguration the configuration parameters for the build
-   * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache
    * @return a new {@link BuildSteps} for building to a registry
    */
-  public static BuildSteps forBuildToDockerRegistry(
-      BuildConfiguration buildConfiguration, Caches.Initializer cachesInitializer) {
+  public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfiguration) {
     return new BuildSteps(
         DESCRIPTION_FOR_DOCKER_REGISTRY,
         buildConfiguration,
-        cachesInitializer,
         stepsRunner ->
             stepsRunner
                 .runRetrieveTargetRegistryCredentialsStep()
@@ -75,15 +66,12 @@ public static BuildSteps forBuildToDockerRegistry(
    * All the steps to build to Docker daemon
    *
    * @param buildConfiguration the configuration parameters for the build
-   * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache
    * @return a new {@link BuildSteps} for building to a Docker daemon
    */
-  public static BuildSteps forBuildToDockerDaemon(
-      BuildConfiguration buildConfiguration, Caches.Initializer cachesInitializer) {
+  public static BuildSteps forBuildToDockerDaemon(BuildConfiguration buildConfiguration) {
     return new BuildSteps(
         DESCRIPTION_FOR_DOCKER_DAEMON,
         buildConfiguration,
-        cachesInitializer,
         stepsRunner ->
             stepsRunner
                 .runPullBaseImageStep()
@@ -100,17 +88,12 @@ public static BuildSteps forBuildToDockerDaemon(
    *
    * @param outputPath the path to output the tarball to
    * @param buildConfiguration the configuration parameters for the build
-   * @param cachesInitializer the {@link Caches.Initializer} used to setup the cache
    * @return a new {@link BuildSteps} for building a tarball
    */
-  public static BuildSteps forBuildToTar(
-      Path outputPath,
-      BuildConfiguration buildConfiguration,
-      Caches.Initializer cachesInitializer) {
+  public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration buildConfiguration) {
     return new BuildSteps(
         DESCRIPTION_FOR_TARBALL,
         buildConfiguration,
-        cachesInitializer,
         stepsRunner ->
             stepsRunner
                 .runPullBaseImageStep()
@@ -124,21 +107,19 @@ public static BuildSteps forBuildToTar(
 
   private final String description;
   private final BuildConfiguration buildConfiguration;
-  private final Caches.Initializer cachesInitializer;
   private final StepsRunnerConsumer stepsRunnerConsumer;
 
   /**
    * @param description a description of what the steps do
+   * @param buildConfiguration the configuration parameters for the build
    * @param stepsRunnerConsumer accepts a {@link StepsRunner} by running the necessary steps
    */
   private BuildSteps(
       String description,
       BuildConfiguration buildConfiguration,
-      Caches.Initializer cachesInitializer,
       StepsRunnerConsumer stepsRunnerConsumer) {
     this.description = description;
     this.buildConfiguration = buildConfiguration;
-    this.cachesInitializer = cachesInitializer;
     this.stepsRunnerConsumer = stepsRunnerConsumer;
   }
 
@@ -146,26 +127,12 @@ public BuildConfiguration getBuildConfiguration() {
     return buildConfiguration;
   }
 
-  public void run()
-      throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException,
-          CacheDirectoryNotOwnedException, CacheDirectoryCreationException {
+  public void run() throws InterruptedException, ExecutionException {
     buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(""));
 
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) {
-      try (Caches caches = cachesInitializer.init()) {
-        Cache baseImageLayersCache = caches.getBaseCache();
-        Cache applicationLayersCache = caches.getApplicationCache();
-
-        StepsRunner stepsRunner =
-            new StepsRunner(buildConfiguration, baseImageLayersCache, applicationLayersCache);
-        stepsRunnerConsumer.accept(stepsRunner);
-
-        // Writes the cached layers to the cache metadata.
-        baseImageLayersCache.addCachedLayersToMetadata(stepsRunner.getCachedBaseImageLayers());
-        applicationLayersCache.addCachedLayersWithMetadataToMetadata(
-            stepsRunner.getCachedApplicationLayers());
-      }
+      stepsRunnerConsumer.accept(new StepsRunner(buildConfiguration));
     }
 
     if (buildConfiguration.getContainerConfiguration() != null) {
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java
index 4ac67806ff..4e26811127 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java
@@ -17,16 +17,15 @@
 package com.google.cloud.tools.jib.builder.steps;
 
 import com.google.cloud.tools.jib.async.AsyncStep;
+import com.google.cloud.tools.jib.blob.Blob;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
-import com.google.cloud.tools.jib.cache.Cache;
-import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException;
-import com.google.cloud.tools.jib.cache.CacheReader;
-import com.google.cloud.tools.jib.cache.CacheWriter;
-import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.LayerConfiguration;
 import com.google.cloud.tools.jib.event.events.LogEvent;
 import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder;
+import com.google.cloud.tools.jib.ncache.Cache;
+import com.google.cloud.tools.jib.ncache.CacheCorruptedException;
+import com.google.cloud.tools.jib.ncache.CacheEntry;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -35,8 +34,7 @@
 import java.util.concurrent.Callable;
 
 /** Builds and caches application layers. */
-class BuildAndCacheApplicationLayerStep
-    implements AsyncStep, Callable {
+class BuildAndCacheApplicationLayerStep implements AsyncStep, Callable {
 
   private static final String DESCRIPTION = "Building application layers";
 
@@ -45,9 +43,7 @@ class BuildAndCacheApplicationLayerStep
    * classes layers. Optionally adds an extra layer if configured to do so.
    */
   static ImmutableList makeList(
-      ListeningExecutorService listeningExecutorService,
-      BuildConfiguration buildConfiguration,
-      Cache cache) {
+      ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) {
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) {
       ImmutableList.Builder buildAndCacheApplicationLayerSteps =
@@ -63,8 +59,7 @@ static ImmutableList makeList(
                 layerConfiguration.getName(),
                 listeningExecutorService,
                 buildConfiguration,
-                layerConfiguration,
-                cache));
+                layerConfiguration));
       }
       return buildAndCacheApplicationLayerSteps.build();
     }
@@ -73,56 +68,52 @@ static ImmutableList makeList(
   private final String layerType;
   private final BuildConfiguration buildConfiguration;
   private final LayerConfiguration layerConfiguration;
-  private final Cache cache;
 
-  private final ListenableFuture listenableFuture;
+  private final ListenableFuture listenableFuture;
 
   private BuildAndCacheApplicationLayerStep(
       String layerType,
       ListeningExecutorService listeningExecutorService,
       BuildConfiguration buildConfiguration,
-      LayerConfiguration layerConfiguration,
-      Cache cache) {
+      LayerConfiguration layerConfiguration) {
     this.layerType = layerType;
     this.buildConfiguration = buildConfiguration;
     this.layerConfiguration = layerConfiguration;
-    this.cache = cache;
 
     listenableFuture = listeningExecutorService.submit(this);
   }
 
   @Override
-  public ListenableFuture getFuture() {
+  public ListenableFuture getFuture() {
     return listenableFuture;
   }
 
   @Override
-  public CachedLayerWithMetadata call() throws IOException, CacheMetadataCorruptedException {
+  public CacheEntry call() throws IOException, CacheCorruptedException {
     String description = "Building " + layerType + " layer";
 
     buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "..."));
 
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) {
+      Cache cache = buildConfiguration.getApplicationLayersCache();
+
       // Don't build the layer if it exists already.
-      Optional optionalCachedLayer =
-          new CacheReader(cache)
-              .getUpToDateLayerByLayerEntries(layerConfiguration.getLayerEntries());
-      if (optionalCachedLayer.isPresent()) {
-        return optionalCachedLayer.get();
+      Optional optionalCacheEntry =
+          cache.retrieve(layerConfiguration.getLayerEntries());
+      if (optionalCacheEntry.isPresent()) {
+        return optionalCacheEntry.get();
       }
 
-      CachedLayerWithMetadata cachedLayer =
-          new CacheWriter(cache)
-              .writeLayer(new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries()));
+      Blob layerBlob = new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries()).build();
+      CacheEntry cacheEntry =
+          cache.writeUncompressedLayer(layerBlob, layerConfiguration.getLayerEntries());
 
       buildConfiguration
           .getEventDispatcher()
-          .dispatch(
-              LogEvent.debug(
-                  description + " built " + cachedLayer.getBlobDescriptor().getDigest()));
+          .dispatch(LogEvent.debug(description + " built " + cacheEntry.getLayerDigest()));
 
-      return cachedLayer;
+      return cacheEntry;
     }
   }
 }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java
index 1c87ecaa66..3592280450 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java
@@ -18,14 +18,18 @@
 
 import com.google.cloud.tools.jib.async.AsyncStep;
 import com.google.cloud.tools.jib.async.NonBlockingSteps;
+import com.google.cloud.tools.jib.blob.Blob;
+import com.google.cloud.tools.jib.blob.BlobDescriptor;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.configuration.ContainerConfiguration;
+import com.google.cloud.tools.jib.image.DescriptorDigest;
 import com.google.cloud.tools.jib.image.Image;
 import com.google.cloud.tools.jib.image.Layer;
 import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException;
 import com.google.cloud.tools.jib.image.json.HistoryEntry;
+import com.google.cloud.tools.jib.ncache.CacheEntry;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -38,17 +42,38 @@
 
 /** Builds a model {@link Image}. */
 class BuildImageStep
-    implements AsyncStep>>, Callable>> {
+    implements AsyncStep>>, Callable>> {
 
   private static final String DESCRIPTION = "Building container configuration";
 
+  @VisibleForTesting
+  static Layer cacheEntryToLayer(CacheEntry cacheEntry) {
+    return new Layer() {
+
+      @Override
+      public Blob getBlob() throws LayerPropertyNotFoundException {
+        return cacheEntry.getLayerBlob();
+      }
+
+      @Override
+      public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException {
+        return new BlobDescriptor(cacheEntry.getLayerSize(), cacheEntry.getLayerDigest());
+      }
+
+      @Override
+      public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException {
+        return cacheEntry.getLayerDiffId();
+      }
+    };
+  }
+
   private final BuildConfiguration buildConfiguration;
   private final PullBaseImageStep pullBaseImageStep;
   private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep;
   private final ImmutableList buildAndCacheApplicationLayerSteps;
 
   private final ListeningExecutorService listeningExecutorService;
-  private final ListenableFuture>> listenableFuture;
+  private final ListenableFuture>> listenableFuture;
 
   BuildImageStep(
       ListeningExecutorService listeningExecutorService,
@@ -69,12 +94,12 @@ class BuildImageStep
   }
 
   @Override
-  public ListenableFuture>> getFuture() {
+  public ListenableFuture>> getFuture() {
     return listenableFuture;
   }
 
   @Override
-  public AsyncStep> call() throws ExecutionException {
+  public AsyncStep> call() throws ExecutionException {
     List> dependencies = new ArrayList<>();
 
     for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep :
@@ -85,18 +110,18 @@ public AsyncStep> call() throws ExecutionException {
         buildAndCacheApplicationLayerSteps) {
       dependencies.add(buildAndCacheApplicationLayerStep.getFuture());
     }
-    ListenableFuture> future =
+    ListenableFuture> future =
         Futures.whenAllSucceed(dependencies)
-            .call(this::afterCachedLayersSteps, listeningExecutorService);
+            .call(this::afterCacheEntrySteps, listeningExecutorService);
     return () -> future;
   }
 
-  private Image afterCachedLayersSteps()
+  private Image afterCacheEntrySteps()
       throws ExecutionException, LayerPropertyNotFoundException {
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) {
       // Constructs the image.
-      Image.Builder imageBuilder = Image.builder();
+      Image.Builder imageBuilder = Image.builder();
       Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage();
       ContainerConfiguration containerConfiguration =
           buildConfiguration.getContainerConfiguration();
@@ -105,7 +130,8 @@ private Image afterCachedLayersSteps()
       List baseImageLayers =
           NonBlockingSteps.get(pullAndCacheBaseImageLayersStep);
       for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : baseImageLayers) {
-        imageBuilder.addLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep));
+        imageBuilder.addLayer(
+            cacheEntryToLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)));
       }
 
       // Passthrough config and count non-empty history entries
@@ -136,7 +162,8 @@ private Image afterCachedLayersSteps()
       // Add built layers/configuration
       for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep :
           buildAndCacheApplicationLayerSteps) {
-        imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep));
+        imageBuilder.addLayer(
+            cacheEntryToLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)));
         imageBuilder.addHistory(
             HistoryEntry.builder()
                 .setCreationTimestamp(layerCreationTime)
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java
index adedfab16a..04b6aca189 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java
@@ -18,13 +18,13 @@
 
 import com.google.cloud.tools.jib.async.AsyncStep;
 import com.google.cloud.tools.jib.async.NonBlockingSteps;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.docker.DockerClient;
 import com.google.cloud.tools.jib.docker.ImageToTarballTranslator;
 import com.google.cloud.tools.jib.event.events.LogEvent;
 import com.google.cloud.tools.jib.image.Image;
 import com.google.cloud.tools.jib.image.ImageReference;
+import com.google.cloud.tools.jib.image.Layer;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -86,7 +86,7 @@ public Void call() throws ExecutionException, InterruptedException {
 
   private Void afterPushBaseImageLayerFuturesFuture()
       throws ExecutionException, InterruptedException, IOException {
-    Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
+    Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
     ImageReference targetImageReference =
         buildConfiguration.getTargetImageConfiguration().getImage();
 
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java
index 33dbc760d9..3ac1856109 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java
@@ -18,42 +18,37 @@
 
 import com.google.cloud.tools.jib.async.AsyncStep;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
-import com.google.cloud.tools.jib.cache.Cache;
-import com.google.cloud.tools.jib.cache.CacheReader;
-import com.google.cloud.tools.jib.cache.CacheWriter;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.http.Authorization;
 import com.google.cloud.tools.jib.image.DescriptorDigest;
+import com.google.cloud.tools.jib.ncache.Cache;
+import com.google.cloud.tools.jib.ncache.CacheCorruptedException;
+import com.google.cloud.tools.jib.ncache.CacheEntry;
 import com.google.cloud.tools.jib.registry.RegistryClient;
-import com.google.cloud.tools.jib.registry.RegistryException;
-import com.google.common.io.CountingOutputStream;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import java.io.IOException;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 import javax.annotation.Nullable;
 
 /** Pulls and caches a single base image layer. */
-class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable {
+class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable {
 
   private static final String DESCRIPTION = "Pulling base image layer %s";
 
   private final BuildConfiguration buildConfiguration;
-  private final Cache cache;
   private final DescriptorDigest layerDigest;
   private final @Nullable Authorization pullAuthorization;
 
-  private final ListenableFuture listenableFuture;
+  private final ListenableFuture listenableFuture;
 
   PullAndCacheBaseImageLayerStep(
       ListeningExecutorService listeningExecutorService,
       BuildConfiguration buildConfiguration,
-      Cache cache,
       DescriptorDigest layerDigest,
       @Nullable Authorization pullAuthorization) {
     this.buildConfiguration = buildConfiguration;
-    this.cache = cache;
     this.layerDigest = layerDigest;
     this.pullAuthorization = pullAuthorization;
 
@@ -61,32 +56,29 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable
   }
 
   @Override
-  public ListenableFuture getFuture() {
+  public ListenableFuture getFuture() {
     return listenableFuture;
   }
 
   @Override
-  public CachedLayer call() throws IOException, RegistryException {
+  public CacheEntry call() throws IOException, CacheCorruptedException {
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(
             buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) {
+      Cache cache = buildConfiguration.getBaseImageLayersCache();
+
+      // Checks if the layer already exists in the cache.
+      Optional optionalCacheEntry = cache.retrieve(layerDigest);
+      if (optionalCacheEntry.isPresent()) {
+        return optionalCacheEntry.get();
+      }
+
       RegistryClient registryClient =
           buildConfiguration
               .newBaseImageRegistryClientFactory()
               .setAuthorization(pullAuthorization)
               .newRegistryClient();
-
-      // Checks if the layer already exists in the cache.
-      CachedLayer cachedLayer = new CacheReader(cache).getLayer(layerDigest);
-      if (cachedLayer != null) {
-        return cachedLayer;
-      }
-
-      CacheWriter cacheWriter = new CacheWriter(cache);
-      CountingOutputStream layerOutputStream = cacheWriter.getLayerOutputStream(layerDigest);
-      registryClient.pullBlob(layerDigest, layerOutputStream);
-      layerOutputStream.close();
-      return cacheWriter.getCachedLayer(layerOutputStream.getCount(), layerDigest);
+      return cache.writeCompressedLayer(registryClient.pullBlob(layerDigest));
     }
   }
 }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java
index f3632d324c..6b07112f13 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java
@@ -20,7 +20,6 @@
 import com.google.cloud.tools.jib.async.NonBlockingSteps;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
 import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization;
-import com.google.cloud.tools.jib.cache.Cache;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.image.Layer;
 import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException;
@@ -39,7 +38,6 @@ class PullAndCacheBaseImageLayersStep
   private static final String DESCRIPTION = "Setting up base image caching";
 
   private final BuildConfiguration buildConfiguration;
-  private final Cache cache;
   private final PullBaseImageStep pullBaseImageStep;
 
   private final ListeningExecutorService listeningExecutorService;
@@ -48,11 +46,9 @@ class PullAndCacheBaseImageLayersStep
   PullAndCacheBaseImageLayersStep(
       ListeningExecutorService listeningExecutorService,
       BuildConfiguration buildConfiguration,
-      Cache cache,
       PullBaseImageStep pullBaseImageStep) {
     this.listeningExecutorService = listeningExecutorService;
     this.buildConfiguration = buildConfiguration;
-    this.cache = cache;
     this.pullBaseImageStep = pullBaseImageStep;
 
     listenableFuture =
@@ -79,7 +75,6 @@ public ImmutableList call()
             new PullAndCacheBaseImageLayerStep(
                 listeningExecutorService,
                 buildConfiguration,
-                cache,
                 layer.getBlobDescriptor().getDigest(),
                 pullBaseImageStepResult.getBaseImageAuthorization()));
       }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java
index 2c61c0feab..344a2f2e15 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java
@@ -47,9 +47,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import javax.annotation.Nullable;
@@ -213,12 +211,10 @@ private Image pullBaseImage(@Nullable Authorization registryAuthorization
                   + Blobs.writeToString(JsonTemplateMapper.toBlob(v22ManifestTemplate)));
         }
 
-        ByteArrayOutputStream containerConfigurationOutputStream = new ByteArrayOutputStream();
-        registryClient.pullBlob(
-            v22ManifestTemplate.getContainerConfiguration().getDigest(),
-            containerConfigurationOutputStream);
         String containerConfigurationString =
-            new String(containerConfigurationOutputStream.toByteArray(), StandardCharsets.UTF_8);
+            Blobs.writeToString(
+                registryClient.pullBlob(
+                    v22ManifestTemplate.getContainerConfiguration().getDigest()));
 
         ContainerConfigurationTemplate containerConfigurationTemplate =
             JsonTemplateMapper.readJson(
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java
index 8514f87905..04de287a0e 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java
@@ -21,10 +21,9 @@
 import com.google.cloud.tools.jib.blob.Blob;
 import com.google.cloud.tools.jib.blob.BlobDescriptor;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
-import com.google.cloud.tools.jib.hash.CountingDigestOutputStream;
 import com.google.cloud.tools.jib.image.Image;
+import com.google.cloud.tools.jib.image.Layer;
 import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator;
 import com.google.common.io.ByteStreams;
 import com.google.common.util.concurrent.Futures;
@@ -79,14 +78,11 @@ private PushBlobStep afterBuildConfigurationFutureFuture()
       throws ExecutionException, IOException {
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) {
-      Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
+      Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
       Blob containerConfigurationBlob =
           new ImageToJsonTranslator(image).getContainerConfigurationBlob();
-      CountingDigestOutputStream digestOutputStream =
-          new CountingDigestOutputStream(ByteStreams.nullOutputStream());
-      containerConfigurationBlob.writeTo(digestOutputStream);
-
-      BlobDescriptor blobDescriptor = digestOutputStream.toBlobDescriptor();
+      BlobDescriptor blobDescriptor =
+          containerConfigurationBlob.writeTo(ByteStreams.nullOutputStream());
 
       return new PushBlobStep(
           listeningExecutorService,
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java
index 76f1764bc1..9a4c896c7d 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java
@@ -18,9 +18,10 @@
 
 import com.google.cloud.tools.jib.async.AsyncStep;
 import com.google.cloud.tools.jib.async.NonBlockingSteps;
+import com.google.cloud.tools.jib.blob.BlobDescriptor;
 import com.google.cloud.tools.jib.builder.TimerEventDispatcher;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
+import com.google.cloud.tools.jib.ncache.CacheEntry;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -36,8 +37,8 @@ class PushLayersStep
 
   private final BuildConfiguration buildConfiguration;
   private final AuthenticatePushStep authenticatePushStep;
-  private final AsyncStep>>
-      cachedLayerStepsStep;
+  private final AsyncStep>>
+      cacheEntryStep;
 
   private final ListeningExecutorService listeningExecutorService;
   private final ListenableFuture>> listenableFuture;
@@ -46,16 +47,15 @@ class PushLayersStep
       ListeningExecutorService listeningExecutorService,
       BuildConfiguration buildConfiguration,
       AuthenticatePushStep authenticatePushStep,
-      AsyncStep>>
-          cachedLayerStepsStep) {
+      AsyncStep>>
+          cacheEntryStep) {
     this.listeningExecutorService = listeningExecutorService;
     this.buildConfiguration = buildConfiguration;
     this.authenticatePushStep = authenticatePushStep;
-    this.cachedLayerStepsStep = cachedLayerStepsStep;
+    this.cacheEntryStep = cacheEntryStep;
 
     listenableFuture =
-        Futures.whenAllSucceed(cachedLayerStepsStep.getFuture())
-            .call(this, listeningExecutorService);
+        Futures.whenAllSucceed(cacheEntryStep.getFuture()).call(this, listeningExecutorService);
   }
 
   @Override
@@ -67,12 +67,12 @@ public ListenableFuture>> getFuture() {
   public ImmutableList> call() throws ExecutionException {
     try (TimerEventDispatcher ignored =
         new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) {
-      ImmutableList> cachedLayerSteps =
-          NonBlockingSteps.get(cachedLayerStepsStep);
+      ImmutableList> cacheEntry =
+          NonBlockingSteps.get(cacheEntryStep);
 
       // Constructs a PushBlobStep for each layer.
       ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder();
-      for (AsyncStep cachedLayerStep : cachedLayerSteps) {
+      for (AsyncStep cachedLayerStep : cacheEntry) {
         ListenableFuture pushBlobStepFuture =
             Futures.whenAllSucceed(cachedLayerStep.getFuture())
                 .call(() -> makePushBlobStep(cachedLayerStep), listeningExecutorService);
@@ -83,14 +83,14 @@ public ImmutableList> call() throws ExecutionException {
     }
   }
 
-  private PushBlobStep makePushBlobStep(AsyncStep cachedLayerStep)
+  private PushBlobStep makePushBlobStep(AsyncStep cacheEntryStep)
       throws ExecutionException {
-    CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep);
+    CacheEntry cacheEntry = NonBlockingSteps.get(cacheEntryStep);
     return new PushBlobStep(
         listeningExecutorService,
         buildConfiguration,
         authenticatePushStep,
-        cachedLayer.getBlobDescriptor(),
-        cachedLayer.getBlob());
+        new BlobDescriptor(cacheEntry.getLayerSize(), cacheEntry.getLayerDigest()),
+        cacheEntry.getLayerBlob());
   }
 }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java
index 642b81964c..f2c8577441 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java
@@ -17,10 +17,6 @@
 package com.google.cloud.tools.jib.builder.steps;
 
 import com.google.cloud.tools.jib.async.AsyncSteps;
-import com.google.cloud.tools.jib.async.NonBlockingSteps;
-import com.google.cloud.tools.jib.cache.Cache;
-import com.google.cloud.tools.jib.cache.CachedLayer;
-import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.global.JibSystemProperties;
 import com.google.common.base.Preconditions;
@@ -28,10 +24,8 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -47,8 +41,6 @@ public class StepsRunner {
 
   private final ListeningExecutorService listeningExecutorService;
   private final BuildConfiguration buildConfiguration;
-  private final Cache baseLayersCache;
-  private final Cache applicationLayersCache;
 
   @Nullable private RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep;
   @Nullable private AuthenticatePushStep authenticatePushStep;
@@ -66,11 +58,8 @@ public class StepsRunner {
   @Nullable private LoadDockerStep loadDockerStep;
   @Nullable private WriteTarFileStep writeTarFileStep;
 
-  public StepsRunner(
-      BuildConfiguration buildConfiguration, Cache baseLayersCache, Cache applicationLayersCache) {
+  public StepsRunner(BuildConfiguration buildConfiguration) {
     this.buildConfiguration = buildConfiguration;
-    this.baseLayersCache = baseLayersCache;
-    this.applicationLayersCache = applicationLayersCache;
 
     ExecutorService executorService =
         JibSystemProperties.isSerializedExecutionEnabled()
@@ -105,7 +94,6 @@ public StepsRunner runPullAndCacheBaseImageLayersStep() {
         new PullAndCacheBaseImageLayersStep(
             listeningExecutorService,
             buildConfiguration,
-            baseLayersCache,
             Preconditions.checkNotNull(pullBaseImageStep));
     return this;
   }
@@ -122,8 +110,7 @@ public StepsRunner runPushBaseImageLayersStep() {
 
   public StepsRunner runBuildAndCacheApplicationLayerSteps() {
     buildAndCacheApplicationLayerSteps =
-        BuildAndCacheApplicationLayerStep.makeList(
-            listeningExecutorService, buildConfiguration, applicationLayersCache);
+        BuildAndCacheApplicationLayerStep.makeList(listeningExecutorService, buildConfiguration);
     return this;
   }
 
@@ -225,36 +212,4 @@ public void waitOnLoadDockerStep() throws ExecutionException, InterruptedExcepti
   public void waitOnWriteTarFileStep() throws ExecutionException, InterruptedException {
     Preconditions.checkNotNull(writeTarFileStep).getFuture().get();
   }
-
-  /**
-   * @return the layers cached by {@link #pullAndCacheBaseImageLayersStep}
-   * @throws ExecutionException if {@link #pullAndCacheBaseImageLayersStep} threw an exception
-   *     during execution
-   */
-  public List getCachedBaseImageLayers() throws ExecutionException {
-    ImmutableList pullAndCacheBaseImageLayerSteps =
-        NonBlockingSteps.get(Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep));
-
-    List cachedLayers = new ArrayList<>(pullAndCacheBaseImageLayerSteps.size());
-    for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep :
-        pullAndCacheBaseImageLayerSteps) {
-      cachedLayers.add(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep));
-    }
-    return cachedLayers;
-  }
-
-  /**
-   * @return the layers cached by {@link #buildAndCacheApplicationLayerSteps}
-   * @throws ExecutionException if {@link #buildAndCacheApplicationLayerSteps} threw an exception
-   *     during execution
-   */
-  public List getCachedApplicationLayers() throws ExecutionException {
-    List cachedLayersWithMetadata =
-        new ArrayList<>(Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps).size());
-    for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep :
-        buildAndCacheApplicationLayerSteps) {
-      cachedLayersWithMetadata.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep));
-    }
-    return cachedLayersWithMetadata;
-  }
 }
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java
index 6417ba05c7..beacb3623f 100644
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java
+++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java
@@ -18,12 +18,12 @@
 
 import com.google.cloud.tools.jib.async.AsyncStep;
 import com.google.cloud.tools.jib.async.NonBlockingSteps;
-import com.google.cloud.tools.jib.cache.CachedLayer;
 import com.google.cloud.tools.jib.configuration.BuildConfiguration;
 import com.google.cloud.tools.jib.docker.ImageToTarballTranslator;
 import com.google.cloud.tools.jib.event.events.LogEvent;
 import com.google.cloud.tools.jib.filesystem.FileOperations;
 import com.google.cloud.tools.jib.image.Image;
+import com.google.cloud.tools.jib.image.Layer;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -90,7 +90,7 @@ public Void call() throws ExecutionException, InterruptedException {
   }
 
   private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, IOException {
-    Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
+    Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep));
 
     // Build the image to a tarball
     buildConfiguration
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java
deleted file mode 100644
index e9bfd08954..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright 2017 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import com.google.cloud.tools.jib.blob.Blobs;
-import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate;
-import com.google.cloud.tools.jib.json.JsonTemplateMapper;
-import com.google.common.annotations.VisibleForTesting;
-import java.io.Closeable;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.NotDirectoryException;
-import java.nio.file.Path;
-import java.util.List;
-
-/** Manages a cache. Implementation is thread-safe. */
-public class Cache implements Closeable {
-
-  /**
-   * Initializes a cache with a directory. This also loads the cache metadata if it exists in the
-   * directory.
-   *
-   * @param cacheDirectory the directory to use for the cache.
-   * @return the initialized cache.
-   * @throws NotDirectoryException if {@code cacheDirectory} is not a directory.
-   * @throws CacheMetadataCorruptedException if loading the cache metadata fails.
-   */
-  public static Cache init(Path cacheDirectory)
-      throws NotDirectoryException, CacheMetadataCorruptedException {
-    if (!Files.isDirectory(cacheDirectory)) {
-      throw new NotDirectoryException("The cache can only write to a directory");
-    }
-    CacheMetadata cacheMetadata = loadCacheMetadata(cacheDirectory);
-
-    return new Cache(cacheDirectory, cacheMetadata);
-  }
-
-  private static CacheMetadata loadCacheMetadata(Path cacheDirectory)
-      throws CacheMetadataCorruptedException {
-    Path cacheMetadataJsonFile = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME);
-
-    if (!Files.exists(cacheMetadataJsonFile)) {
-      return CacheMetadata.builder().build();
-    }
-
-    try {
-      CacheMetadataTemplate cacheMetadataJson =
-          JsonTemplateMapper.readJsonFromFileWithLock(
-              cacheMetadataJsonFile, CacheMetadataTemplate.class);
-      return CacheMetadataTranslator.fromTemplate(cacheMetadataJson, cacheDirectory);
-    } catch (IOException ex) {
-      // The cache metadata is probably corrupted.
-      throw new CacheMetadataCorruptedException(ex);
-    }
-  }
-
-  /** The path to the root of the cache. */
-  private final Path cacheDirectory;
-
-  /** The metadata that corresponds to the cache at {@link #cacheDirectory}. */
-  private final CacheMetadata cacheMetadata;
-
-  /** Builds the updated cache metadata to save back to the cache. */
-  private final CacheMetadata.Builder cacheMetadataBuilder;
-
-  private Cache(Path cacheDirectory, CacheMetadata cacheMetadata) {
-    this.cacheDirectory = cacheDirectory;
-    this.cacheMetadata = cacheMetadata;
-    cacheMetadataBuilder = cacheMetadata.newAppendingBuilder();
-  }
-
-  /**
-   * Finishes the use of the cache by flushing any unsaved changes.
-   *
-   * @throws IOException if saving the cache metadata fails.
-   */
-  @Override
-  public void close() throws IOException {
-    saveCacheMetadata(cacheDirectory);
-  }
-
-  /**
-   * Adds the cached layer to the cache metadata. This is NOT thread-safe.
-   *
-   * @param cachedLayers the layers to add
-   */
-  public void addCachedLayersToMetadata(List cachedLayers) {
-    for (CachedLayer cachedLayer : cachedLayers) {
-      cacheMetadataBuilder.addLayer(new CachedLayerWithMetadata(cachedLayer, null));
-    }
-  }
-
-  /**
-   * Adds the cached layer to the cache metadata. This is NOT thread-safe.
-   *
-   * @param cachedLayersWithMetadata the layers to add
-   */
-  public void addCachedLayersWithMetadataToMetadata(
-      List cachedLayersWithMetadata) {
-    for (CachedLayerWithMetadata cachedLayerWithMetadata : cachedLayersWithMetadata) {
-      cacheMetadataBuilder.addLayer(cachedLayerWithMetadata);
-    }
-  }
-
-  @VisibleForTesting
-  Path getCacheDirectory() {
-    return cacheDirectory;
-  }
-
-  @VisibleForTesting
-  CacheMetadata getMetadata() {
-    return cacheMetadata;
-  }
-
-  @VisibleForTesting
-  CacheMetadata getUpdatedMetadata() {
-    return cacheMetadataBuilder.build();
-  }
-
-  /** Saves the updated cache metadata back to the cache. */
-  private void saveCacheMetadata(Path cacheDirectory) throws IOException {
-    Path cacheMetadataJsonFile = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME);
-
-    CacheMetadataTemplate cacheMetadataJson =
-        CacheMetadataTranslator.toTemplate(cacheMetadataBuilder.build());
-
-    Blobs.writeToFileWithLock(JsonTemplateMapper.toBlob(cacheMetadataJson), cacheMetadataJsonFile);
-  }
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java
deleted file mode 100644
index df9ff6b5ef..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryNotOwnedException.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2018 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import java.nio.file.Path;
-
-/**
- * Thrown when trying to use a directory as {@link Cache}, but the directory might be used by other
- * applications.
- */
-public class CacheDirectoryNotOwnedException extends Exception {
-
-  private final Path cacheDirectory;
-
-  /** Initializes with the cache directory that was unsafe to use. */
-  CacheDirectoryNotOwnedException(Path cacheDirectory) {
-    super();
-
-    this.cacheDirectory = cacheDirectory;
-  }
-
-  public Path getCacheDirectory() {
-    return cacheDirectory;
-  }
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java
deleted file mode 100644
index 49b3e86530..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheFiles.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2017 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import com.google.cloud.tools.jib.image.DescriptorDigest;
-import java.nio.file.Path;
-
-/** Methods for getting static cache filename properties. */
-class CacheFiles {
-
-  /** Increment the version prefix when the metadata format changes. */
-  static final String METADATA_FILENAME = "metadata-v3.json";
-
-  private static final String LAYER_FILE_EXTENSION = ".tar.gz";
-
-  static Path getMetadataFile(Path cacheDirectory) {
-    return cacheDirectory.resolve(METADATA_FILENAME);
-  }
-
-  /**
-   * Gets the path to the file for a layer digest. The file is {@code [cache directory]/[layer
-   * hash].tar.gz}.
-   */
-  static Path getLayerFile(Path cacheDirectory, DescriptorDigest layerDigest) {
-    return cacheDirectory.resolve(layerDigest.getHash() + LAYER_FILE_EXTENSION);
-  }
-
-  private CacheFiles() {}
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java
deleted file mode 100644
index f502e6369d..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadata.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 2017 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import com.google.cloud.tools.jib.image.ImageLayers;
-import com.google.cloud.tools.jib.image.LayerEntry;
-import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-import java.util.Iterator;
-import javax.annotation.Nullable;
-
-/**
- * The cache stores all the layer BLOBs as separate files and the cache metadata contains
- * information about each layer BLOB.
- */
-class CacheMetadata {
-
-  /** Builds a {@link CacheMetadata}. */
-  static class Builder {
-
-    private final ImageLayers.Builder layersBuilder =
-        ImageLayers.builder().removeDuplicates();
-
-    private Builder(ImageLayers initialLayers) {
-      layersBuilder.addAll(initialLayers);
-    }
-
-    private Builder() {}
-
-    /**
-     * Adds a layer. This method is NOT thread-safe.
-     *
-     * @param layer the layer to add
-     */
-    Builder addLayer(CachedLayerWithMetadata layer) {
-      layersBuilder.add(layer);
-      return this;
-    }
-
-    CacheMetadata build() {
-      return new CacheMetadata(layersBuilder.build());
-    }
-  }
-
-  /** Can be used to filter layers in the metadata. */
-  static class LayerFilter {
-
-    /**
-     * Checks if the layer entries matches the metadata layer entries.
-     *
-     * @param layerEntries the layer entries to check
-     * @param metadataEntries the metadata entries to match against
-     * @return {@code true} if the layer entries match the metadata entries; {@code false} otherwise
-     */
-    @VisibleForTesting
-    static boolean doLayerEntriesMatchMetadataEntries(
-        ImmutableList layerEntries,
-        ImmutableList metadataEntries) {
-      // Checks the layer entries are the same as the metadata layer entries.
-      if (layerEntries.size() != metadataEntries.size()) {
-        return false;
-      }
-
-      // Pairwise-compares all the layer entries with the metadata layer entries.
-      Iterator layerEntriesIterator = layerEntries.iterator();
-      Iterator metadataEntriesIterator =
-          metadataEntries.iterator();
-      while (layerEntriesIterator.hasNext() && metadataEntriesIterator.hasNext()) {
-        LayerEntry layerEntry = layerEntriesIterator.next();
-        LayerMetadata.LayerMetadataEntry metadataEntry = metadataEntriesIterator.next();
-
-        boolean areSourceFilesEqual =
-            layerEntry
-                .getAbsoluteSourceFileString()
-                .equals(metadataEntry.getAbsoluteSourceFileString());
-        boolean areExtractionPathsEqual =
-            layerEntry
-                .getAbsoluteExtractionPathString()
-                .equals(metadataEntry.getAbsoluteExtractionPathString());
-        if (!areSourceFilesEqual || !areExtractionPathsEqual) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    private final ImageLayers layers;
-
-    @Nullable private ImmutableList layerEntries;
-
-    private LayerFilter(ImageLayers layers) {
-      this.layers = layers;
-    }
-
-    /** Filters to a certain list of {@link LayerEntry}s. */
-    LayerFilter byLayerEntries(ImmutableList layerEntries) {
-      this.layerEntries = layerEntries;
-      return this;
-    }
-
-    /** Applies the filters to the metadata layers. */
-    ImageLayers filter() throws CacheMetadataCorruptedException {
-      try {
-        ImageLayers.Builder filteredLayersBuilder = ImageLayers.builder();
-
-        for (CachedLayerWithMetadata layer : layers) {
-          if (layerEntries != null) {
-            if (layer.getMetadata() == null) {
-              // There is no metadata, so it doesn't pass the filter.
-              continue;
-            }
-
-            if (!doLayerEntriesMatchMetadataEntries(
-                layerEntries, layer.getMetadata().getEntries())) {
-              // The layer entries do not match.
-              continue;
-            }
-          }
-
-          filteredLayersBuilder.add(layer);
-        }
-
-        return filteredLayersBuilder.build();
-
-      } catch (LayerPropertyNotFoundException ex) {
-        throw new CacheMetadataCorruptedException(ex);
-      }
-    }
-  }
-
-  static Builder builder() {
-    return new Builder();
-  }
-
-  private final ImageLayers layers;
-
-  private CacheMetadata(ImageLayers layers) {
-    this.layers = layers;
-  }
-
-  ImageLayers getLayers() {
-    return layers;
-  }
-
-  LayerFilter filterLayers() {
-    return new LayerFilter(layers);
-  }
-
-  /** @return a {@link Builder} starts with all the layers in this metadata */
-  Builder newAppendingBuilder() {
-    return new Builder(layers);
-  }
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java
deleted file mode 100644
index c92590a68f..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataCorruptedException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2017 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-/**
- * Exception thrown when the cache metadata failed to load correctly, indicating that it is probably
- * corrupted.
- */
-public class CacheMetadataCorruptedException extends Exception {
-
-  CacheMetadataCorruptedException(Throwable cause) {
-    super(cause);
-  }
-
-  public CacheMetadataCorruptedException(String message) {
-    super(message);
-  }
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
deleted file mode 100644
index dd0cf93f84..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslator.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2017 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import com.google.cloud.tools.jib.blob.BlobDescriptor;
-import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerObjectTemplate;
-import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate;
-import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate;
-import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate;
-import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath;
-import com.google.cloud.tools.jib.image.LayerEntry;
-import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Translates {@link CacheMetadata} to and from {@link CacheMetadataTemplate}. */
-public class CacheMetadataTranslator {
-
-  /** Translates {@link CacheMetadataTemplate} to {@link CacheMetadata}. */
-  static CacheMetadata fromTemplate(CacheMetadataTemplate template, Path cacheDirectory)
-      throws CacheMetadataCorruptedException {
-    CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder();
-
-    // Converts each layer object in the template to a cache metadata layer.
-    for (CacheMetadataLayerObjectTemplate layerObjectTemplate : template.getLayers()) {
-      if (layerObjectTemplate.getDigest() == null || layerObjectTemplate.getDiffId() == null) {
-        throw new CacheMetadataCorruptedException(
-            "Cannot translate cache metadata layer without a digest or diffId");
-      }
-
-      Path layerContentFile =
-          CacheFiles.getLayerFile(cacheDirectory, layerObjectTemplate.getDigest());
-
-      // Gets the properties for a layer. Properties only exist for application layers.
-      CacheMetadataLayerPropertiesObjectTemplate propertiesObjectTemplate =
-          layerObjectTemplate.getProperties();
-
-      // Constructs the cache metadata layer from a cached layer and layer metadata.
-      LayerMetadata layerMetadata = null;
-      if (propertiesObjectTemplate != null) {
-        // Converts the layer entry templates to layer entries.
-        ImmutableList.Builder layerEntries =
-            ImmutableList.builderWithExpectedSize(
-                propertiesObjectTemplate.getLayerEntries().size());
-        for (LayerEntryTemplate layerEntryTemplate : propertiesObjectTemplate.getLayerEntries()) {
-          if (layerEntryTemplate.getSourceFileString() == null
-              || layerEntryTemplate.getExtractionPathString() == null) {
-            throw new CacheMetadataCorruptedException(
-                "Cannot translate cache metadata layer entry without source files or extraction path");
-          }
-          layerEntries.add(
-              new LayerEntry(
-                  Paths.get(layerEntryTemplate.getSourceFileString()),
-                  AbsoluteUnixPath.get(layerEntryTemplate.getExtractionPathString())));
-        }
-
-        layerMetadata =
-            LayerMetadata.from(
-                layerEntries.build(), propertiesObjectTemplate.getLastModifiedTime());
-      }
-
-      CachedLayer cachedLayer =
-          new CachedLayer(
-              layerContentFile,
-              new BlobDescriptor(layerObjectTemplate.getSize(), layerObjectTemplate.getDigest()),
-              layerObjectTemplate.getDiffId());
-
-      CachedLayerWithMetadata cachedLayerWithMetadata =
-          new CachedLayerWithMetadata(cachedLayer, layerMetadata);
-      cacheMetadataBuilder.addLayer(cachedLayerWithMetadata);
-    }
-
-    return cacheMetadataBuilder.build();
-  }
-
-  /** Translates {@link CacheMetadata} to {@link CacheMetadataTemplate}. */
-  static CacheMetadataTemplate toTemplate(CacheMetadata cacheMetadata) {
-    CacheMetadataTemplate template = new CacheMetadataTemplate();
-
-    for (CachedLayerWithMetadata cachedLayerWithMetadata : cacheMetadata.getLayers()) {
-      CacheMetadataLayerObjectTemplate layerObjectTemplate =
-          new CacheMetadataLayerObjectTemplate()
-              .setSize(cachedLayerWithMetadata.getBlobDescriptor().getSize())
-              .setDigest(cachedLayerWithMetadata.getBlobDescriptor().getDigest())
-              .setDiffId(cachedLayerWithMetadata.getDiffId());
-
-      if (cachedLayerWithMetadata.getMetadata() != null) {
-        // Constructs the layer entry templates to add to the layer object template.
-        ImmutableList metadataEntries =
-            cachedLayerWithMetadata.getMetadata().getEntries();
-        List layerEntryTemplates = new ArrayList<>(metadataEntries.size());
-        for (LayerMetadata.LayerMetadataEntry metadataEntry : metadataEntries) {
-          layerEntryTemplates.add(
-              new LayerEntryTemplate(
-                  metadataEntry.getAbsoluteSourceFileString(),
-                  metadataEntry.getAbsoluteExtractionPathString()));
-        }
-
-        layerObjectTemplate.setProperties(
-            new CacheMetadataLayerPropertiesObjectTemplate()
-                .setLayerEntries(layerEntryTemplates)
-                .setLastModifiedTime(cachedLayerWithMetadata.getMetadata().getLastModifiedTime()));
-      }
-
-      template.addLayer(layerObjectTemplate);
-    }
-
-    return template;
-  }
-}
diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java
deleted file mode 100644
index 3eb00cb623..0000000000
--- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheReader.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2018 Google LLC.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.cloud.tools.jib.cache;
-
-import com.google.cloud.tools.jib.filesystem.DirectoryWalker;
-import com.google.cloud.tools.jib.image.DescriptorDigest;
-import com.google.cloud.tools.jib.image.ImageLayers;
-import com.google.cloud.tools.jib.image.LayerEntry;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.FileTime;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import javax.annotation.Nullable;
-
-/** Reads image content from the cache. */
-public class CacheReader {
-
-  /**
-   * Gets the last modified time for the file at {@code path}. If {@code path} is a directory, then
-   * gets the latest modified time of its subfiles.
-   *
-   * @param path the file to check
-   * @return the last modified time
-   * @throws IOException if checking the last modified time fails
-   */
-  private static FileTime getLastModifiedTime(Path path) throws IOException {
-    if (Files.isDirectory(path)) {
-      List subFiles = new DirectoryWalker(path).walk();
-      FileTime maxLastModifiedTime = FileTime.from(Instant.MIN);
-
-      // Finds the max last modified time for the subfiles.
-      for (Path subFilePath : subFiles) {
-        FileTime subFileLastModifiedTime = Files.getLastModifiedTime(subFilePath);
-        if (subFileLastModifiedTime.compareTo(maxLastModifiedTime) > 0) {
-          maxLastModifiedTime = subFileLastModifiedTime;
-        }
-      }
-
-      return maxLastModifiedTime;
-    }
-
-    return Files.getLastModifiedTime(path);
-  }
-
-  private final Cache cache;
-
-  public CacheReader(Cache cache) {
-    this.cache = cache;
-  }
-
-  /**
-   * @param layerDigest the layer digest of the layer to get.
-   * @return the cached layer with digest {@code layerDigest}, or {@code null} if not found.
-   */
-  @Nullable
-  public CachedLayer getLayer(DescriptorDigest layerDigest) {
-    return cache.getMetadata().getLayers().get(layerDigest);
-  }
-
-  /**
-   * Finds the file that stores the content BLOB for an application layer.
-   *
-   * @param layerEntries the entries for the layer content
-   * @return the newest cached layer file that matches the {@code layerType} and {@code
-   *     sourceFiles}, or {@code null} if there is no match.
-   * @throws CacheMetadataCorruptedException if getting the cache metadata fails.
-   */
-  @Nullable
-  public Path getLayerFile(ImmutableList layerEntries)
-      throws CacheMetadataCorruptedException {
-    CacheMetadata cacheMetadata = cache.getMetadata();
-    ImageLayers cachedLayers =
-        cacheMetadata.filterLayers().byLayerEntries(layerEntries).filter();
-
-    // Finds the newest cached layer for the layer type.
-    FileTime newestLastModifiedTime = FileTime.from(Instant.MIN);
-
-    Path newestLayerFile = null;
-    for (CachedLayerWithMetadata cachedLayer : cachedLayers) {
-      if (cachedLayer.getMetadata() == null) {
-        throw new IllegalStateException("Layers with sourceFiles should have metadata");
-      }
-
-      FileTime cachedLayerLastModifiedTime = cachedLayer.getMetadata().getLastModifiedTime();
-      if (cachedLayerLastModifiedTime.compareTo(newestLastModifiedTime) > 0) {
-        newestLastModifiedTime = cachedLayerLastModifiedTime;
-        newestLayerFile = cachedLayer.getContentFile();
-      }
-    }
-
-    return newestLayerFile;
-  }
-
-  /**
-   * Gets an up-to-date layer that is built from the {@code layerEntries}.
-   *
-   * 

The method returns the first up-to-date layer found. This is safe because the source files - * will not have been modified since creation of any up-to-date layer (ie. all up-to-date layers - * should have the same file contents). - * - * @param layerEntries the layer's content entries - * @return an up-to-date layer containing the source files. - * @throws IOException if reading the source files fails. - * @throws CacheMetadataCorruptedException if reading the cache metadata fails. - */ - public Optional getUpToDateLayerByLayerEntries( - ImmutableList layerEntries) throws IOException, CacheMetadataCorruptedException { - // Grabs all the layers that have matching source files. - ImageLayers cachedLayersWithSourceFiles = - cache.getMetadata().filterLayers().byLayerEntries(layerEntries).filter(); - if (cachedLayersWithSourceFiles.isEmpty()) { - return Optional.empty(); - } - - // Determines the latest modification time for the source files. - FileTime sourceFilesLastModifiedTime = FileTime.from(Instant.MIN); - for (LayerEntry layerEntry : layerEntries) { - FileTime lastModifiedTime = getLastModifiedTime(layerEntry.getSourceFile()); - if (lastModifiedTime.compareTo(sourceFilesLastModifiedTime) > 0) { - sourceFilesLastModifiedTime = lastModifiedTime; - } - } - - // Checks if at least one of the matched layers is up-to-date. - for (CachedLayerWithMetadata cachedLayer : cachedLayersWithSourceFiles) { - if (cachedLayer.getMetadata() == null) { - throw new IllegalStateException("Layers with sourceFiles should have metadata"); - } - - if (sourceFilesLastModifiedTime.compareTo(cachedLayer.getMetadata().getLastModifiedTime()) - <= 0) { - // This layer is an up-to-date layer. - return Optional.of(cachedLayer); - } - } - - return Optional.empty(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java deleted file mode 100644 index 9aa1538684..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheWriter.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; -import com.google.cloud.tools.jib.image.UnwrittenLayer; -import com.google.common.io.ByteStreams; -import com.google.common.io.CountingOutputStream; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** Writes {@link UnwrittenLayer}s to the cache. */ -public class CacheWriter { - - private final Cache cache; - - public CacheWriter(Cache cache) { - this.cache = cache; - } - - /** - * Builds an {@link UnwrittenLayer} from a {@link ReproducibleLayerBuilder} and compresses and - * writes the {@link UnwrittenLayer}'s uncompressed layer content BLOB to cache. - * - * @param reproducibleLayerBuilder the layer builder - * @return the cached layer with accompanying {@link LayerMetadata} - * @throws IOException if writing the layer to file fails - */ - public CachedLayerWithMetadata writeLayer(ReproducibleLayerBuilder reproducibleLayerBuilder) - throws IOException { - UnwrittenLayer unwrittenLayer = reproducibleLayerBuilder.build(); - - // Writes to a temporary file first because the UnwrittenLayer needs to be written first to - // obtain its digest. - Path tempLayerFile = Files.createTempFile(cache.getCacheDirectory(), null, null); - // TODO: Find a way to do this with java.nio.file - tempLayerFile.toFile().deleteOnExit(); - - // Writes the UnwrittenLayer layer BLOB to a file to convert into a CachedLayer. - try (CountingDigestOutputStream compressedDigestOutputStream = - new CountingDigestOutputStream( - new BufferedOutputStream(Files.newOutputStream(tempLayerFile)))) { - // Writes the layer with GZIP compression. The original bytes are captured as the layer's - // diff ID and the bytes outputted from the GZIP compression are captured as the layer's - // content descriptor. - GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream); - DescriptorDigest diffId = unwrittenLayer.getBlob().writeTo(compressorStream).getDigest(); - - // The GZIPOutputStream must be closed in order to write out the remaining compressed data. - compressorStream.close(); - BlobDescriptor compressedBlobDescriptor = compressedDigestOutputStream.toBlobDescriptor(); - - // Renames the temporary layer file to the correct filename. - Path layerFile = getLayerFile(compressedBlobDescriptor.getDigest()); - try { - Files.move(tempLayerFile, layerFile); - } catch (FileAlreadyExistsException ignored) { - // If the file already exists, we skip renaming and use the existing file. This happens if a - // new layer happens to have the same content as a previously-cached layer. - // - // Do not attempt to remove the try-catch block with the idea of checking file existence - // before moving; there can be concurrent file moves. - } - - CachedLayer cachedLayer = new CachedLayer(layerFile, compressedBlobDescriptor, diffId); - LayerMetadata layerMetadata = - LayerMetadata.from( - reproducibleLayerBuilder.getLayerEntries(), FileTime.from(Instant.now())); - return new CachedLayerWithMetadata(cachedLayer, layerMetadata); - } - } - - /** - * @param layerDigest the written layer's digest. - * @return the {@link CountingOutputStream} to write to to cache a layer with the specified - * compressed digest. - * @throws IOException if writing to the layer file's output stream fails. - */ - public CountingOutputStream getLayerOutputStream(DescriptorDigest layerDigest) - throws IOException { - Path layerFile = getLayerFile(layerDigest); - return new CountingOutputStream(new BufferedOutputStream(Files.newOutputStream(layerFile))); - } - - /** - * Gets the layer that was cached by writing the contents to a file in the cache. The returned - * {@link CachedLayer} should be added to the cache metadata. - * - * @param layerSize the size of the layer - * @param layerDigest the digest of the layer to retrieve - * @return a {@link CachedLayer} from a layer digest and the {@link CountingOutputStream} the - * layer BLOB was written to - * @throws IOException if closing the output stream or getting the layer diff ID fails - */ - public CachedLayer getCachedLayer(long layerSize, DescriptorDigest layerDigest) - throws IOException { - Path layerFile = getLayerFile(layerDigest); - - return new CachedLayer( - layerFile, new BlobDescriptor(layerSize, layerDigest), getDiffId(layerFile)); - } - - /** - * @param compressedDigest the compressed digest of the layer to find. - * @return the path to the file for the layer with the specified compressed digest. - */ - private Path getLayerFile(DescriptorDigest compressedDigest) { - return CacheFiles.getLayerFile(cache.getCacheDirectory(), compressedDigest); - } - - /** - * @param layerFile the layer content file. - * @return the layer diff ID by decompressing the layer content file. - * @throws IOException if reading the layer file fails. - */ - private DescriptorDigest getDiffId(Path layerFile) throws IOException { - CountingDigestOutputStream diffIdCaptureOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - try (InputStream fileInputStream = new BufferedInputStream(Files.newInputStream(layerFile)); - GZIPInputStream decompressorStream = new GZIPInputStream(fileInputStream)) { - ByteStreams.copy(decompressorStream, diffIdCaptureOutputStream); - } - return diffIdCaptureOutputStream.toBlobDescriptor().getDigest(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java deleted file mode 100644 index cf0d33a199..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.Layer; -import java.nio.file.Path; - -/** - * A {@link Layer} that has been written out to a cache and has its file-backed content BLOB, - * digest, size, and diff ID. - */ -public class CachedLayer implements Layer { - - private final Path contentFile; - private final BlobDescriptor blobDescriptor; - private final DescriptorDigest diffId; - - /** - * Initializes the layer with its file-backed content BLOB, content descriptor (digest and size), - * and diff ID. The {@code blobDescriptor} and {@code diffId} must match the BLOB stored in - * the file - no checks are made at runtime. - * - * @param contentFile the file with the layer's content BLOB - * @param blobDescriptor the content descriptor for the layer's content BLOB - * @param diffId the diff ID for the layer - * @see Layer - */ - public CachedLayer(Path contentFile, BlobDescriptor blobDescriptor, DescriptorDigest diffId) { - this.contentFile = contentFile; - this.blobDescriptor = blobDescriptor; - this.diffId = diffId; - } - - public Path getContentFile() { - return contentFile; - } - - @Override - public Blob getBlob() { - return Blobs.from(contentFile); - } - - @Override - public BlobDescriptor getBlobDescriptor() { - return blobDescriptor; - } - - @Override - public DescriptorDigest getDiffId() { - return diffId; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - if (!(other instanceof CachedLayer)) { - return false; - } - CachedLayer otherLayer = (CachedLayer) other; - return getBlobDescriptor().getDigest().equals(otherLayer.getBlobDescriptor().getDigest()); - } - - @Override - public int hashCode() { - return getBlobDescriptor().getDigest().hashCode(); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java deleted file mode 100644 index c087c78766..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadata.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import javax.annotation.Nullable; - -/** A {@link CachedLayer} with a last modified time. */ -public class CachedLayerWithMetadata extends CachedLayer { - - /** Extra layer properties for application layers. */ - @Nullable private final LayerMetadata metadata; - - public CachedLayerWithMetadata(CachedLayer cachedLayer, @Nullable LayerMetadata metadata) { - super(cachedLayer.getContentFile(), cachedLayer.getBlobDescriptor(), cachedLayer.getDiffId()); - - this.metadata = metadata; - } - - @Nullable - LayerMetadata getMetadata() { - return metadata; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java deleted file mode 100644 index 3c5a5294f3..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Caches.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.common.annotations.VisibleForTesting; -import java.io.Closeable; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Manages both the base image layers cache and the application image layers cache. - * - *

In general, the cache for base image layers should be shared between projects, while the cache - * for the application image layers should be specific to a single project. - */ -public class Caches implements Closeable { - - /** Initializes a {@link Caches} with directory paths. */ - public static class Initializer { - - /** A file to store in the default base image layers cache to check ownership by Jib. */ - private static final String OWNERSHIP_FILE_NAME = ".jib"; - - /** - * Ensures ownership of {@code cacheDirectory} by checking for the existence of {@link - * #OWNERSHIP_FILE_NAME}. - * - *

This is a safety check to make sure we are not writing to a directory not created by Jib. - */ - @VisibleForTesting - static void ensureOwnership(Path cacheDirectory) - throws CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Path ownershipFile = cacheDirectory.resolve(OWNERSHIP_FILE_NAME); - - if (Files.exists(cacheDirectory)) { - // Checks for the ownership file. - if (!Files.exists(ownershipFile)) { - throw new CacheDirectoryNotOwnedException(cacheDirectory); - } - - } else { - try { - // Creates the cache directory and ownership file. - Files.createDirectories(cacheDirectory); - Files.createFile(ownershipFile); - - } catch (IOException ex) { - throw new CacheDirectoryCreationException(cacheDirectory, ex); - } - } - } - - private final Path baseImageLayersCacheDirectory; - private final boolean shouldEnsureOwnershipOfBaseImageLayersCacheDirectory; - private final Path applicationLayersCacheDirectory; - private final boolean shouldEnsureOwnershipOfApplicationLayersCacheDirectory; - - /** - * @param baseImageLayersCacheDirectory cache for the application image layers - usually not - * local to the application project - * @param shouldEnsureOwnershipOfBaseImageLayersCacheDirectory if {@code true}, ensures the base - * image layers cache directory is safe to write to - * @param applicationLayersCacheDirectory cache for the application image layers - usually local - * to the application project - * @param shouldEnsureOwnershipOfApplicationLayersCacheDirectory if {@code true}, ensures the - * base image layers cache directory is safe to write to - */ - public Initializer( - Path baseImageLayersCacheDirectory, - boolean shouldEnsureOwnershipOfBaseImageLayersCacheDirectory, - Path applicationLayersCacheDirectory, - boolean shouldEnsureOwnershipOfApplicationLayersCacheDirectory) { - this.baseImageLayersCacheDirectory = baseImageLayersCacheDirectory; - this.shouldEnsureOwnershipOfBaseImageLayersCacheDirectory = - shouldEnsureOwnershipOfBaseImageLayersCacheDirectory; - this.applicationLayersCacheDirectory = applicationLayersCacheDirectory; - this.shouldEnsureOwnershipOfApplicationLayersCacheDirectory = - shouldEnsureOwnershipOfApplicationLayersCacheDirectory; - } - - public Caches init() - throws CacheMetadataCorruptedException, CacheDirectoryNotOwnedException, - CacheDirectoryCreationException, IOException { - if (shouldEnsureOwnershipOfBaseImageLayersCacheDirectory) { - ensureOwnership(baseImageLayersCacheDirectory); - } - if (shouldEnsureOwnershipOfApplicationLayersCacheDirectory) { - ensureOwnership(applicationLayersCacheDirectory); - } - - return new Caches(baseImageLayersCacheDirectory, applicationLayersCacheDirectory); - } - } - - private final Cache baseCache; - private final Cache applicationCache; - - /** Instantiate with {@link Initializer#init}. */ - private Caches(Path baseCacheDirectory, Path applicationCacheDirectory) - throws CacheMetadataCorruptedException, IOException { - applicationCache = Cache.init(applicationCacheDirectory); - - // Ensures that only one Cache is initialized if using the same directory. - if (Files.isSameFile(baseCacheDirectory, applicationCacheDirectory)) { - baseCache = applicationCache; - } else { - baseCache = Cache.init(baseCacheDirectory); - } - } - - public Cache getBaseCache() { - return baseCache; - } - - public Cache getApplicationCache() { - return applicationCache; - } - - @Override - public void close() throws IOException { - applicationCache.close(); - - if (baseCache != applicationCache) { - baseCache.close(); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java deleted file mode 100644 index d629fa7d70..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerMetadata.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import java.nio.file.attribute.FileTime; - -/** - * Metadata about an application layer stored in the cache. This is part of the {@link - * CacheMetadata}. - */ -class LayerMetadata { - - /** Entry into the layer metadata. */ - static class LayerMetadataEntry { - - /** The source file path string, in Unix form. The path should be an absolute path. */ - private final String absoluteSourceFileString; - - /** The extraction path string, in Unix form. The path should be an absolute path. */ - private final String absoluteExtractionPathString; - - String getAbsoluteSourceFileString() { - return absoluteSourceFileString; - } - - String getAbsoluteExtractionPathString() { - return absoluteExtractionPathString; - } - - @VisibleForTesting - LayerMetadataEntry(String absoluteSourceFileString, String absoluteExtractionPathString) { - this.absoluteSourceFileString = absoluteSourceFileString; - this.absoluteExtractionPathString = absoluteExtractionPathString; - } - } - - static LayerMetadata from(ImmutableList layerEntries, FileTime lastModifiedTime) { - ImmutableList.Builder entries = - ImmutableList.builderWithExpectedSize(layerEntries.size()); - - for (LayerEntry layerEntry : layerEntries) { - entries.add( - new LayerMetadataEntry( - layerEntry.getAbsoluteSourceFileString(), - layerEntry.getAbsoluteExtractionPathString())); - } - - return new LayerMetadata(entries.build(), lastModifiedTime); - } - - /** The entries that define the layer contents. */ - private ImmutableList entries; - - /** The last time the layer was constructed. */ - private final FileTime lastModifiedTime; - - LayerMetadata(ImmutableList entries, FileTime lastModifiedTime) { - this.entries = entries; - this.lastModifiedTime = lastModifiedTime; - } - - ImmutableList getEntries() { - return entries; - } - - FileTime getLastModifiedTime() { - return lastModifiedTime; - } - - @VisibleForTesting - void setEntries(ImmutableList layerMetadataEntries) { - entries = layerMetadataEntries; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java deleted file mode 100644 index 3c1b982a3a..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerObjectTemplate.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.json.JsonTemplate; -import javax.annotation.Nullable; - -/** - * Inner JSON template for storing metadata about a layer in the cache as part of {@link - * CacheMetadataTemplate}. - * - * @see CacheMetadataTemplate for example - */ -public class CacheMetadataLayerObjectTemplate implements JsonTemplate { - - /** The reference to the layer. */ - private final ReferenceObject reference = new ReferenceObject(); - - /** Additional properties for the layer. */ - @Nullable private CacheMetadataLayerPropertiesObjectTemplate properties; - - /** - * The reference for a layer consists of its size (in bytes), digest, and diff ID. - * - * @see Layer for details - */ - private static class ReferenceObject implements JsonTemplate { - - private long size; - @Nullable private DescriptorDigest digest; - @Nullable private DescriptorDigest diffId; - } - - public long getSize() { - return reference.size; - } - - @Nullable - public DescriptorDigest getDigest() { - return reference.digest; - } - - @Nullable - public DescriptorDigest getDiffId() { - return reference.diffId; - } - - @Nullable - public CacheMetadataLayerPropertiesObjectTemplate getProperties() { - return properties; - } - - public CacheMetadataLayerObjectTemplate setSize(long size) { - reference.size = size; - return this; - } - - public CacheMetadataLayerObjectTemplate setDigest(DescriptorDigest digest) { - reference.digest = digest; - return this; - } - - public CacheMetadataLayerObjectTemplate setDiffId(DescriptorDigest diffId) { - reference.diffId = diffId; - return this; - } - - public CacheMetadataLayerObjectTemplate setProperties( - CacheMetadataLayerPropertiesObjectTemplate properties) { - this.properties = properties; - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java deleted file mode 100644 index f6de5a10a1..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataLayerPropertiesObjectTemplate.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.json.JsonTemplate; -import java.nio.file.attribute.FileTime; -import java.util.Collections; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Inner JSON template for extra properties for an application layer, as part of {@link - * CacheMetadataLayerObjectTemplate}. - */ -public class CacheMetadataLayerPropertiesObjectTemplate implements JsonTemplate { - - /** Represents a pair of source files and extraction path. */ - public static class LayerEntryTemplate implements JsonTemplate { - - /** The path to the source file for this layer entry. */ - @Nullable private String sourceFile; - - /** The intended path to extract the source file to in the container. */ - @Nullable private String extractionPath; - - @Nullable - public String getSourceFileString() { - return sourceFile; - } - - @Nullable - public String getExtractionPathString() { - return extractionPath; - } - - public LayerEntryTemplate(String absoluteSourceFile, String absoluteExtractionPath) { - sourceFile = absoluteSourceFile; - extractionPath = absoluteExtractionPath; - } - - /** For Jackson JSON templating. */ - public LayerEntryTemplate() {} - } - - /** The content entries for the layer. */ - private List layerEntries = Collections.emptyList(); - - /** The last time the layer was constructed. */ - private long lastModifiedTime; - - public List getLayerEntries() { - return layerEntries; - } - - public FileTime getLastModifiedTime() { - return FileTime.fromMillis(lastModifiedTime); - } - - public CacheMetadataLayerPropertiesObjectTemplate setLayerEntries( - List layerEntries) { - this.layerEntries = layerEntries; - return this; - } - - public CacheMetadataLayerPropertiesObjectTemplate setLastModifiedTime(FileTime lastModifiedTime) { - this.lastModifiedTime = lastModifiedTime.toMillis(); - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java deleted file mode 100644 index 4cee1f87e9..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplate.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.json.JsonTemplate; -import java.util.ArrayList; -import java.util.List; - -/** - * JSON template for storing metadata about the cache. - * - *

Example: - * - *

{@code
- * {
- *   "layers": [
- *     {
- *       // This is a base image layer.
- *       "reference": {
- *         "size": 631,
- *         "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
- *         "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"
- *       }
- *     },
- *     ...
- *     {
- *       // This is an application layer (it has properties).
- *       "reference": {
- *         "size": 223,
- *         "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad",
- *         "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"
- *       }
- *       "properties": {
- *         "layerEntries": [
- *           {
- *             "sourceFile": "build/classes",
- *             "extractionPath": "/app/classes"
- *           },
- *           ...
- *         ],
- *         "lastModifiedTime": 255073580723571
- *       }
- *     },
- *     ...
- *   ]
- * }
- * }
- */ -public class CacheMetadataTemplate implements JsonTemplate { - - private final List layers = new ArrayList<>(); - - public List getLayers() { - return layers; - } - - public CacheMetadataTemplate addLayer(CacheMetadataLayerObjectTemplate layer) { - layers.add(layer); - return this; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index a1a3c96310..20cfb6aa3e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -20,9 +20,12 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.ncache.Cache; import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -187,8 +190,10 @@ public Builder setEventDispatcher(EventDispatcher eventDispatcher) { * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. * * @return the corresponding build configuration + * @throws IOException if an I/O exception occurs + * @throws CacheDirectoryCreationException if failed to create the configured cache directories */ - public BuildConfiguration build() { + public BuildConfiguration build() throws IOException, CacheDirectoryCreationException { // Validates the parameters. List errorMessages = new ArrayList<>(); if (baseImageConfiguration == null) { @@ -211,13 +216,21 @@ public BuildConfiguration build() { + "' does not use a specific image digest - build may not be reproducible")); } + if (baseImageLayersCacheConfiguration == null) { + baseImageLayersCacheConfiguration = + CacheConfiguration.forDefaultUserLevelCacheDirectory(); + } + if (applicationLayersCacheConfiguration == null) { + applicationLayersCacheConfiguration = CacheConfiguration.makeTemporary(); + } + return new BuildConfiguration( baseImageConfiguration, targetImageConfiguration, additionalTargetImageTags, containerConfiguration, - applicationLayersCacheConfiguration, - baseImageLayersCacheConfiguration, + Cache.withDirectory(baseImageLayersCacheConfiguration.getCacheDirectory()), + Cache.withDirectory(applicationLayersCacheConfiguration.getCacheDirectory()), targetFormat, allowInsecureRegistries, layerConfigurations, @@ -235,6 +248,18 @@ public BuildConfiguration build() { throw new IllegalStateException(); } } + + @Nullable + @VisibleForTesting + CacheConfiguration getBaseImageLayersCacheConfiguration() { + return baseImageLayersCacheConfiguration; + } + + @Nullable + @VisibleForTesting + CacheConfiguration getApplicationLayersCacheConfiguration() { + return applicationLayersCacheConfiguration; + } } /** @@ -250,8 +275,8 @@ public static Builder builder() { private final ImageConfiguration targetImageConfiguration; private final ImmutableSet additionalTargetImageTags; @Nullable private final ContainerConfiguration containerConfiguration; - @Nullable private final CacheConfiguration applicationLayersCacheConfiguration; - @Nullable private final CacheConfiguration baseImageLayersCacheConfiguration; + private final Cache baseImageLayersCache; + private final Cache applicationLayersCache; private Class targetFormat; private final boolean allowInsecureRegistries; private final ImmutableList layerConfigurations; @@ -264,8 +289,8 @@ private BuildConfiguration( ImageConfiguration targetImageConfiguration, ImmutableSet additionalTargetImageTags, @Nullable ContainerConfiguration containerConfiguration, - @Nullable CacheConfiguration applicationLayersCacheConfiguration, - @Nullable CacheConfiguration baseImageLayersCacheConfiguration, + Cache baseImageLayersCache, + Cache applicationLayersCache, Class targetFormat, boolean allowInsecureRegistries, ImmutableList layerConfigurations, @@ -275,8 +300,8 @@ private BuildConfiguration( this.targetImageConfiguration = targetImageConfiguration; this.additionalTargetImageTags = additionalTargetImageTags; this.containerConfiguration = containerConfiguration; - this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; - this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; + this.baseImageLayersCache = baseImageLayersCache; + this.applicationLayersCache = applicationLayersCache; this.targetFormat = targetFormat; this.allowInsecureRegistries = allowInsecureRegistries; this.layerConfigurations = layerConfigurations; @@ -318,23 +343,20 @@ public EventDispatcher getEventDispatcher() { } /** - * Gets the location of the cache for storing application layers. + * Gets the {@link Cache} for base image layers. * - * @return the application layers {@link CacheConfiguration}, or {@code null} if not set + * @return the {@link Cache} for base image layers */ - @Nullable - public CacheConfiguration getApplicationLayersCacheConfiguration() { - return applicationLayersCacheConfiguration; + public Cache getBaseImageLayersCache() { + return baseImageLayersCache; } - /** - * Gets the location of the cache for storing base image layers. + * Gets the {@link Cache} for application layers. * - * @return the base image layers {@link CacheConfiguration}, or {@code null} if not set + * @return the {@link Cache} for application layers */ - @Nullable - public CacheConfiguration getBaseImageLayersCacheConfiguration() { - return baseImageLayersCacheConfiguration; + public Cache getApplicationLayersCache() { + return applicationLayersCache; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java index f8250d918c..ca976a270c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.filesystem.UserCacheHome; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -77,6 +76,7 @@ public static CacheConfiguration forDefaultUserLevelCacheDirectory() { } private final Path cacheDirectory; + // TODO: Deprecate private final boolean shouldEnsureOwnership; private CacheConfiguration(Path cacheDirectory, boolean shouldEnsureOwnership) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java similarity index 74% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java index f15ae06189..cd103021b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheDirectoryCreationException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java @@ -14,19 +14,13 @@ * the License. */ -package com.google.cloud.tools.jib.cache; +package com.google.cloud.tools.jib.configuration; -import java.nio.file.Path; - -/** Thrown when a directory to be used as the {@link Cache} could not be created. */ +/** Thrown when a directory to be used as the cache could not be created. */ public class CacheDirectoryCreationException extends Exception { private static final String MESSAGE = "Could not create cache directory"; - CacheDirectoryCreationException(Path cacheDirectory, Throwable cause) { - super(MESSAGE + ": " + cacheDirectory, cause); - } - public CacheDirectoryCreationException(Throwable cause) { super(MESSAGE, cause); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java index c5e407dd82..edc6083fe7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java @@ -18,34 +18,36 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import java.io.IOException; -import java.nio.file.Path; import java.util.Collections; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** Translates an {@link Image} to a tarball that can be loaded into Docker. */ public class ImageToTarballTranslator { /** File name for the container configuration in the tarball. */ private static final String CONTAINER_CONFIGURATION_JSON_FILE_NAME = "config.json"; + /** File name for the manifest in the tarball. */ private static final String MANIFEST_JSON_FILE_NAME = "manifest.json"; - private final Image image; + /** File name extension for the layer content files. */ + private static final String LAYER_FILE_EXTENSION = ".tar.gz"; + + private final Image image; /** * Instantiate with an {@link Image}. * * @param image the image to convert into a tarball. */ - public ImageToTarballTranslator(Image image) { + public ImageToTarballTranslator(Image image) { this.image = image; } @@ -54,12 +56,11 @@ public Blob toTarballBlob(ImageReference imageReference) throws IOException { DockerLoadManifestEntryTemplate manifestTemplate = new DockerLoadManifestEntryTemplate(); // Adds all the layers to the tarball and manifest. - for (CachedLayer layer : image.getLayers()) { - Path layerContentFile = layer.getContentFile(); - String layerName = layerContentFile.getFileName().toString(); + for (Layer layer : image.getLayers()) { + String layerName = layer.getBlobDescriptor().getDigest().getHash() + LAYER_FILE_EXTENSION; - tarStreamBuilder.addTarArchiveEntry( - new TarArchiveEntry(layerContentFile.toFile(), layerName)); + tarStreamBuilder.addBlobEntry( + layer.getBlob(), layer.getBlobDescriptor().getSize(), layerName); manifestTemplate.addLayerFile(layerName); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 505228f739..f30db1aaff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; @@ -30,9 +31,9 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; /** - * Builds a reproducible {@link UnwrittenLayer} from files. The reproducibility is implemented by - * strips out all non-reproducible elements (modification time, group ID, user ID, user name, and - * group name) from name-sorted tar archive entries. + * Builds a reproducible layer {@link Blob} from files. The reproducibility is implemented by strips + * out all non-reproducible elements (modification time, group ID, user ID, user name, and group + * name) from name-sorted tar archive entries. */ public class ReproducibleLayerBuilder { @@ -88,11 +89,11 @@ public ReproducibleLayerBuilder(ImmutableList layerEntries) { } /** - * Builds and returns the layer. + * Builds and returns the layer {@link Blob}. * * @return the new layer */ - public UnwrittenLayer build() { + public Blob build() { UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. @@ -125,10 +126,6 @@ public UnwrittenLayer build() { tarStreamBuilder.addTarArchiveEntry(entry); } - return new UnwrittenLayer(tarStreamBuilder.toBlob()); - } - - public ImmutableList getLayerEntries() { - return layerEntries; + return tarStreamBuilder.toBlob(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java deleted file mode 100644 index 126d4c9bf5..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/UnwrittenLayer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; - -/** - * A layer that has not been written out and only has the unwritten content {@link Blob}. Once - * written, this layer becomes a {@link CachedLayer}. - */ -public class UnwrittenLayer implements Layer { - - private final Blob uncompressedBlob; - - /** - * Initializes with the uncompressed {@link Blob} of the layer content. - * - * @param uncompressedBlob the uncompressed {@link Blob} of the layer content - */ - public UnwrittenLayer(Blob uncompressedBlob) { - this.uncompressedBlob = uncompressedBlob; - } - - /** Gets the uncompressed layer content BLOB. */ - @Override - public Blob getBlob() { - return uncompressedBlob; - } - - @Override - public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException { - throw new LayerPropertyNotFoundException("Blob descriptor not available for unwritten layer"); - } - - @Override - public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { - throw new LayerPropertyNotFoundException("Diff ID not available for unwritten layer"); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 88ca26f703..bf8e4987d5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -18,10 +18,10 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -90,14 +90,14 @@ static ImmutableList environmentMapToList(@Nullable Map .collect(ImmutableList.toImmutableList()); } - private final Image image; + private final Image image; /** * Instantiate with an {@link Image}. * * @param image the image to translate. */ - public ImageToJsonTranslator(Image image) { + public ImageToJsonTranslator(Image image) { this.image = image; } @@ -111,7 +111,7 @@ public Blob getContainerConfigurationBlob() { ContainerConfigurationTemplate template = new ContainerConfigurationTemplate(); // Adds the layer diff IDs. - for (CachedLayer layer : image.getLayers()) { + for (Layer layer : image.getLayers()) { template.addLayerDiffId(layer.getDiffId()); } @@ -168,7 +168,7 @@ public T getManifestTemplate( template.setContainerConfiguration(containerConfigurationSize, containerConfigurationDigest); // Adds the layers. - for (CachedLayer layer : image.getLayers()) { + for (Layer layer : image.getLayers()) { template.addLayer( layer.getBlobDescriptor().getSize(), layer.getBlobDescriptor().getDigest()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index acb5939bf0..6ed8f62f6f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; @@ -30,7 +31,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; import javax.annotation.Nullable; @@ -238,20 +238,24 @@ public BlobDescriptor checkBlob(DescriptorDigest blobDigest) } /** - * Downloads the BLOB to a file. + * Gets the BLOB referenced by {@code blobDigest}. Note that the BLOB is only pulled when it is + * written out. * * @param blobDigest the digest of the BLOB to download - * @param destinationOutputStream the {@link OutputStream} to write the BLOB to * @return a {@link Blob} backed by the file at {@code destPath}. The file at {@code destPath} * must exist for {@link Blob} to be valid. - * @throws IOException if communicating with the endpoint fails - * @throws RegistryException if communicating with the endpoint fails */ - public Void pullBlob(DescriptorDigest blobDigest, OutputStream destinationOutputStream) - throws RegistryException, IOException { - BlobPuller blobPuller = - new BlobPuller(registryEndpointRequestProperties, blobDigest, destinationOutputStream); - return callRegistryEndpoint(blobPuller); + public Blob pullBlob(DescriptorDigest blobDigest) { + return Blobs.from( + outputStream -> { + try { + callRegistryEndpoint( + new BlobPuller(registryEndpointRequestProperties, blobDigest, outputStream)); + + } catch (RegistryException ex) { + throw new IOException(ex); + } + }); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 832bd04de5..8e38af8bb8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -77,6 +77,20 @@ public void addByteEntry(byte[] contents, String name) { archiveMap.put(entry, Blobs.from(outputStream -> outputStream.write(contents))); } + /** + * Adds a blob to the archive. Note that this should be used with non-file {@link Blob}s; for + * adding files to the archive, use {@link #addTarArchiveEntry}. + * + * @param blob the {@link Blob} to add to the tarball + * @param size the size (in bytes) of {@code blob} + * @param name the name of the entry (i.e. filename) + */ + public void addBlobEntry(Blob blob, long size, String name) { + TarArchiveEntry entry = new TarArchiveEntry(name); + entry.setSize(size); + archiveMap.put(entry, blob); + } + /** @return a new {@link Blob} that can stream the uncompressed tarball archive BLOB. */ public Blob toBlob() { return Blobs.from(this::writeEntriesAsTarArchive); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index f6f5554bf1..c087c71d07 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; @@ -26,6 +27,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableMap; +import java.io.IOException; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -91,7 +93,8 @@ public void testToContainerConfiguration_add() { @Test public void testToBuildConfiguration() - throws InvalidImageReferenceException, CredentialRetrievalException { + throws InvalidImageReferenceException, CredentialRetrievalException, IOException, + CacheDirectoryCreationException { RegistryImage baseImage = RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); RegistryImage targetImage = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index bf98ec892b..9adf7b0615 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -17,17 +17,19 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.CacheReader; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.ncache.Cache; +import com.google.cloud.tools.jib.ncache.CacheCorruptedException; +import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; @@ -76,11 +78,16 @@ private static LayerConfiguration makeLayerConfiguration( } } + private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOException { + Assert.assertArrayEquals(Blobs.writeToByteArray(expectedBlob), Blobs.writeToByteArray(blob)); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; + + private Cache cache; @Mock private EventDispatcher mockEventDispatcher; - private Path temporaryCacheDirectory; private LayerConfiguration fakeDependenciesLayerConfiguration; private LayerConfiguration fakeSnapshotDependenciesLayerConfiguration; @@ -110,35 +117,34 @@ public void setUp() throws IOException, URISyntaxException { EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); + + cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); + Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); - temporaryCacheDirectory = temporaryFolder.newFolder().toPath(); + Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); } - private ImageLayers buildFakeLayersToCache() - throws CacheMetadataCorruptedException, IOException, ExecutionException { - ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); - ImageLayers applicationLayers; + private ImageLayers buildFakeLayersToCache() throws ExecutionException { + ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); - try (Cache cache = Cache.init(temporaryCacheDirectory)) { - ImmutableList buildAndCacheApplicationLayerSteps = - BuildAndCacheApplicationLayerStep.makeList( - MoreExecutors.newDirectExecutorService(), mockBuildConfiguration, cache); + ImmutableList buildAndCacheApplicationLayerSteps = + BuildAndCacheApplicationLayerStep.makeList( + MoreExecutors.newDirectExecutorService(), mockBuildConfiguration); - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - applicationLayersBuilder.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); - } - - applicationLayers = applicationLayersBuilder.build(); - cache.addCachedLayersWithMetadataToMetadata(applicationLayers.getLayers()); + for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : + buildAndCacheApplicationLayerSteps) { + applicationLayersBuilder.add( + BuildImageStep.cacheEntryToLayer( + NonBlockingSteps.get(buildAndCacheApplicationLayerStep))); } - return applicationLayers; + + return applicationLayersBuilder.build(); } @Test public void testRun() - throws LayerPropertyNotFoundException, IOException, CacheMetadataCorruptedException, - ExecutionException { + throws LayerPropertyNotFoundException, IOException, ExecutionException, + CacheCorruptedException { ImmutableList fakeLayerConfigurations = ImmutableList.of( fakeDependenciesLayerConfiguration, @@ -150,12 +156,9 @@ public void testRun() .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(5, applicationLayers.size()); - // Re-initialize cache with the updated metadata. - Cache cache = Cache.init(temporaryCacheDirectory); - ImmutableList dependenciesLayerEntries = fakeLayerConfigurations.get(0).getLayerEntries(); ImmutableList snapshotDependenciesLayerEntries = @@ -167,58 +170,46 @@ public void testRun() ImmutableList extraFilesLayerEntries = fakeLayerConfigurations.get(4).getLayerEntries(); + CacheEntry dependenciesCacheEntry = + cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry snapshotDependenciesCacheEntry = + cache.retrieve(snapshotDependenciesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry resourcesCacheEntry = + cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry classesCacheEntry = + cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry extraFilesCacheEntry = + cache.retrieve(extraFilesLayerEntries).orElseThrow(AssertionError::new); + // Verifies that the cached layers are up-to-date. - CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( - applicationLayers.get(0).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(dependenciesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(0).getBlobDescriptor().getDigest(), + dependenciesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(1).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(snapshotDependenciesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(1).getBlobDescriptor().getDigest(), + snapshotDependenciesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(2).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(resourcesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(2).getBlobDescriptor().getDigest(), + resourcesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(3).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(classesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(3).getBlobDescriptor().getDigest(), + classesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(4).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(extraFilesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(4).getBlobDescriptor().getDigest(), + extraFilesCacheEntry.getLayerDigest()); // Verifies that the cache reader gets the same layers as the newest application layers. - Assert.assertEquals( - applicationLayers.get(0).getContentFile(), - cacheReader.getLayerFile(dependenciesLayerEntries)); - Assert.assertEquals( - applicationLayers.get(1).getContentFile(), - cacheReader.getLayerFile(snapshotDependenciesLayerEntries)); - Assert.assertEquals( - applicationLayers.get(2).getContentFile(), cacheReader.getLayerFile(resourcesLayerEntries)); - Assert.assertEquals( - applicationLayers.get(3).getContentFile(), cacheReader.getLayerFile(classesLayerEntries)); - Assert.assertEquals( - applicationLayers.get(4).getContentFile(), - cacheReader.getLayerFile(extraFilesLayerEntries)); + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCacheEntry.getLayerBlob()); + assertBlobsEqual( + applicationLayers.get(1).getBlob(), snapshotDependenciesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), resourcesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(3).getBlob(), classesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(4).getBlob(), extraFilesCacheEntry.getLayerBlob()); } @Test public void testRun_emptyLayersIgnored() - throws IOException, CacheMetadataCorruptedException, ExecutionException { + throws IOException, ExecutionException, CacheCorruptedException { ImmutableList fakeLayerConfigurations = ImmutableList.of( fakeDependenciesLayerConfiguration, @@ -230,7 +221,7 @@ public void testRun_emptyLayersIgnored() .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(3, applicationLayers.size()); ImmutableList dependenciesLayerEntries = @@ -240,39 +231,27 @@ public void testRun_emptyLayersIgnored() ImmutableList classesLayerEntries = fakeLayerConfigurations.get(3).getLayerEntries(); - // Re-initialize cache with the updated metadata. - Cache cache = Cache.init(temporaryCacheDirectory); + CacheEntry dependenciesCacheEntry = + cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry resourcesCacheEntry = + cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); + CacheEntry classesCacheEntry = + cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); // Verifies that the cached layers are up-to-date. - CacheReader cacheReader = new CacheReader(cache); Assert.assertEquals( - applicationLayers.get(0).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(dependenciesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(0).getBlobDescriptor().getDigest(), + dependenciesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(1).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(resourcesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(1).getBlobDescriptor().getDigest(), + resourcesCacheEntry.getLayerDigest()); Assert.assertEquals( - applicationLayers.get(2).getBlobDescriptor(), - cacheReader - .getUpToDateLayerByLayerEntries(classesLayerEntries) - .orElseThrow(AssertionError::new) - .getBlobDescriptor()); + applicationLayers.get(2).getBlobDescriptor().getDigest(), + classesCacheEntry.getLayerDigest()); // Verifies that the cache reader gets the same layers as the newest application layers. - Assert.assertEquals( - applicationLayers.get(0).getContentFile(), - cacheReader.getLayerFile(fakeLayerConfigurations.get(0).getLayerEntries())); - Assert.assertEquals( - applicationLayers.get(1).getContentFile(), - cacheReader.getLayerFile(fakeLayerConfigurations.get(2).getLayerEntries())); - Assert.assertEquals( - applicationLayers.get(2).getContentFile(), - cacheReader.getLayerFile(fakeLayerConfigurations.get(3).getLayerEntries())); + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(1).getBlob(), resourcesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), classesCacheEntry.getLayerBlob()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index c0a400af8c..e23a135206 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,9 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.cache.CachedLayerWithMetadata; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -26,13 +25,14 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; -import java.nio.file.Paths; import java.security.DigestException; import java.time.Instant; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.junit.Assert; @@ -64,11 +64,33 @@ public void setUp() throws DigestException { testDescriptorDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - CachedLayerWithMetadata testCachedLayer = - new CachedLayerWithMetadata( - new CachedLayer( - Paths.get(""), new BlobDescriptor(testDescriptorDigest), testDescriptorDigest), - null); + CacheEntry testCacheEntry = + new CacheEntry() { + @Override + public DescriptorDigest getLayerDigest() { + return testDescriptorDigest; + } + + @Override + public DescriptorDigest getLayerDiffId() { + return testDescriptorDigest; + } + + @Override + public long getLayerSize() { + return 0; + } + + @Override + public Blob getLayerBlob() { + return Blobs.from("ignored"); + } + + @Override + public Optional getMetadataBlob() { + return Optional.empty(); + } + }; Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) @@ -104,7 +126,7 @@ public void setUp() throws DigestException { .addHistory(emptyLayerHistory) .build(); Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + .thenReturn(Futures.immediateFuture(testCacheEntry)); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) .thenReturn( Futures.immediateFuture( @@ -117,7 +139,7 @@ public void setUp() throws DigestException { Futures.immediateFuture( new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null))); Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + .thenReturn(Futures.immediateFuture(testCacheEntry)); } @Test @@ -132,7 +154,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); } @@ -154,7 +176,7 @@ public void test_propagateBaseImageConfiguration() mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE"), image.getEnvironment()); @@ -180,7 +202,7 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep, mockBuildAndCacheApplicationLayerStep)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); // Make sure history is as expected HistoryEntry expectedAddedBaseLayerHistory = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index a8c652c62a..f076be98ca 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; @@ -25,6 +26,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -44,7 +46,8 @@ public class RetrieveRegistryCredentialsStepTest { @Mock private ListeningExecutorService mockListeningExecutorService; @Test - public void testCall_retrieved() throws CredentialRetrievalException { + public void testCall_retrieved() + throws CredentialRetrievalException, IOException, CacheDirectoryCreationException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( Arrays.asList( @@ -67,7 +70,8 @@ public void testCall_retrieved() throws CredentialRetrievalException { } @Test - public void testCall_none() throws CredentialRetrievalException { + public void testCall_none() + throws CredentialRetrievalException, IOException, CacheDirectoryCreationException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); @@ -89,7 +93,7 @@ public void testCall_none() throws CredentialRetrievalException { } @Test - public void testCall_exception() { + public void testCall_exception() throws IOException, CacheDirectoryCreationException { CredentialRetrievalException credentialRetrievalException = Mockito.mock(CredentialRetrievalException.class); BuildConfiguration buildConfiguration = @@ -111,7 +115,8 @@ public void testCall_exception() { private BuildConfiguration makeFakeBuildConfiguration( List baseCredentialRetrievers, - List targetCredentialRetrievers) { + List targetCredentialRetrievers) + throws IOException, CacheDirectoryCreationException { ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java deleted file mode 100644 index 3df1f2ce55..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheFilesTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.nio.file.Path; -import java.security.DigestException; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheFiles}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheFilesTest { - - @Mock private Path mockPath; - - @Test - public void testGetMetadataFile() { - ArgumentCaptor fileNameCaptor = ArgumentCaptor.forClass(String.class); - - Mockito.when(mockPath.resolve(fileNameCaptor.capture())).thenReturn(mockPath); - - Path metadataFile = CacheFiles.getMetadataFile(mockPath); - - Assert.assertEquals("metadata-v3.json", fileNameCaptor.getValue()); - Assert.assertEquals(mockPath, metadataFile); - } - - @Test - public void testGetLayerFile() throws DigestException { - DescriptorDigest layerDigest = - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - - ArgumentCaptor fileNameCaptor = ArgumentCaptor.forClass(String.class); - - Mockito.when(mockPath.resolve(fileNameCaptor.capture())).thenReturn(mockPath); - - Path layerFile = CacheFiles.getLayerFile(mockPath, layerDigest); - - Assert.assertEquals( - "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad.tar.gz", - fileNameCaptor.getValue()); - Assert.assertEquals(mockPath, layerFile); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java deleted file mode 100644 index f93292bb8c..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheMetadata}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheMetadataTest { - - private static CachedLayer mockCachedLayer() { - CachedLayer mockCachedLayer = Mockito.mock(CachedLayer.class); - BlobDescriptor mockBlobDescriptor = Mockito.mock(BlobDescriptor.class); - DescriptorDigest mockDescriptorDigest = Mockito.mock(DescriptorDigest.class); - Mockito.when(mockCachedLayer.getBlobDescriptor()).thenReturn(mockBlobDescriptor); - Mockito.when(mockBlobDescriptor.getDigest()).thenReturn(mockDescriptorDigest); - return mockCachedLayer; - } - - @Mock private LayerEntry mockLayerEntry; - - @Test - public void testAddLayer() { - CachedLayerWithMetadata testCachedLayerWithMetadata = - new CachedLayerWithMetadata(mockCachedLayer(), Mockito.mock(LayerMetadata.class)); - - CacheMetadata cacheMetadata = - CacheMetadata.builder().addLayer(testCachedLayerWithMetadata).build(); - - Assert.assertEquals( - Collections.singletonList(testCachedLayerWithMetadata), - cacheMetadata.getLayers().getLayers()); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_mismatchSize() { - Assert.assertFalse( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of(mockLayerEntry), ImmutableList.of())); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_extractionPath() { - ImmutableList metadataEntries = - LayerMetadata.from( - ImmutableList.of( - new LayerEntry( - Paths.get("anotherSourceFile"), - AbsoluteUnixPath.get("/another/extraction/path"))), - FileTime.fromMillis(0)) - .getEntries(); - - Assert.assertFalse( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of( - new LayerEntry(Paths.get("sourceFile"), AbsoluteUnixPath.get("/extraction/path"))), - metadataEntries)); - Assert.assertTrue( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - ImmutableList.of( - new LayerEntry( - Paths.get("anotherSourceFile"), - AbsoluteUnixPath.get("/another/extraction/path"))), - metadataEntries)); - } - - @Test - public void testFilter_doLayerEntriesMatchMetadataEntries_pass() { - LayerEntry layerEntry1 = - new LayerEntry(Paths.get("sourceFile1"), AbsoluteUnixPath.get("/extraction/path")); - LayerEntry layerEntry2 = - new LayerEntry(Paths.get("sourceFile2"), AbsoluteUnixPath.get("/extraction/path")); - LayerEntry layerEntry3 = - new LayerEntry(Paths.get("sourceFile3"), AbsoluteUnixPath.get("/another/extraction/path")); - LayerEntry layerEntry4 = - new LayerEntry(Paths.get("sourceFile4"), AbsoluteUnixPath.get("/another/extraction/path")); - - ImmutableList layerEntries = - ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4); - ImmutableList metadataEntries = - LayerMetadata.from( - ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4), - FileTime.fromMillis(0)) - .getEntries(); - - Assert.assertTrue( - CacheMetadata.LayerFilter.doLayerEntriesMatchMetadataEntries( - layerEntries, metadataEntries)); - } - - @Test - public void testFilter_bySourceFiles() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(6).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry1 = - new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); - LayerEntry fakeLayerEntry2 = - new LayerEntry( - Paths.get("some/source/directory"), AbsoluteUnixPath.get("/extraction/path")); - - LayerMetadata fakeExpectedSourceFilesClassesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2), FileTime.fromMillis(0)); - LayerMetadata fakeExpectedSourceFilesResourcesLayerMetadata = - LayerMetadata.from( - ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2), FileTime.fromMillis(0)); - LayerMetadata fakeOtherSourceFilesLayerMetadata = - LayerMetadata.from( - ImmutableList.of( - new LayerEntry( - Paths.get("not/the/same/source/file"), - AbsoluteUnixPath.get("/extraction/path"))), - FileTime.fromMillis(0)); - LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(), FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeOtherSourceFilesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(1), fakeExpectedSourceFilesResourcesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(2), fakeOtherSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(3), fakeEmptySourceFilesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(4), fakeExpectedSourceFilesClassesLayerMetadata), - new CachedLayerWithMetadata( - mockLayers.get(5), fakeExpectedSourceFilesResourcesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata - .filterLayers() - .byLayerEntries(ImmutableList.of(fakeLayerEntry1, fakeLayerEntry2)) - .filter(); - - Assert.assertEquals(3, filteredLayers.size()); - Assert.assertEquals( - fakeExpectedSourceFilesResourcesLayerMetadata, filteredLayers.get(0).getMetadata()); - Assert.assertEquals( - fakeExpectedSourceFilesClassesLayerMetadata, filteredLayers.get(1).getMetadata()); - Assert.assertEquals( - fakeExpectedSourceFilesResourcesLayerMetadata, filteredLayers.get(2).getMetadata()); - } - - @Test - public void testFilter_byNoEntries() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry = - new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); - - LayerMetadata fakeSourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeNoEntriesLayerMetadata = - new LayerMetadata(ImmutableList.of(), FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(1), fakeNoEntriesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of()).filter(); - - Assert.assertEquals(1, filteredLayers.size()); - Assert.assertEquals(fakeNoEntriesLayerMetadata, filteredLayers.get(0).getMetadata()); - } - - @Test - public void testFilter_byEmptySourceFiles() throws CacheMetadataCorruptedException { - List mockLayers = - Stream.generate(CacheMetadataTest::mockCachedLayer).limit(2).collect(Collectors.toList()); - - LayerEntry fakeLayerEntry = - new LayerEntry(Paths.get("some/source/file"), AbsoluteUnixPath.get("/extraction/path")); - - LayerMetadata fakeSourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(fakeLayerEntry), FileTime.fromMillis(0)); - LayerMetadata fakeEmptySourceFilesLayerMetadata = - LayerMetadata.from(ImmutableList.of(), FileTime.fromMillis(0)); - - List cachedLayers = - Arrays.asList( - new CachedLayerWithMetadata(mockLayers.get(0), fakeSourceFilesLayerMetadata), - new CachedLayerWithMetadata(mockLayers.get(1), fakeEmptySourceFilesLayerMetadata)); - - CacheMetadata.Builder cacheMetadataBuilder = CacheMetadata.builder(); - for (CachedLayerWithMetadata cachedLayer : cachedLayers) { - cacheMetadataBuilder.addLayer(cachedLayer); - } - CacheMetadata cacheMetadata = cacheMetadataBuilder.build(); - - ImageLayers filteredLayers = - cacheMetadata.filterLayers().byLayerEntries(ImmutableList.of()).filter(); - - Assert.assertEquals(1, filteredLayers.size()); - Assert.assertEquals(fakeEmptySourceFilesLayerMetadata, filteredLayers.get(0).getMetadata()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java deleted file mode 100644 index 50548f1bde..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheMetadataTranslatorTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.json.CacheMetadataTemplate; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.List; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CacheMetadataTranslator}. */ -@RunWith(MockitoJUnitRunner.class) -public class CacheMetadataTranslatorTest { - - private static final String CLASSES_LAYER_SOURCE_FILE = "/some/source/path"; - private static final AbsoluteUnixPath CLASSES_LAYER_EXTRACTION_PATH = - AbsoluteUnixPath.get("/some/extraction/path"); - private static final FileTime CLASSES_LAYER_LAST_MODIFIED_TIME = - FileTime.fromMillis(255073580723571L); - - @Mock private Path mockPath; - - private BlobDescriptor baseLayerBlobDescriptor; - private DescriptorDigest baseLayerDiffId; - private BlobDescriptor classesLayerBlobDescriptor; - private DescriptorDigest classesLayerDiffId; - - @Before - public void setUp() throws DigestException { - baseLayerBlobDescriptor = - new BlobDescriptor( - 631, - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef")); - baseLayerDiffId = - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"); - classesLayerBlobDescriptor = - new BlobDescriptor( - 223, - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); - classesLayerDiffId = - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"); - } - - @Test - public void testFromTemplate() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - Path fakePath = Paths.get("fake/path"); - - // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - - // Deserializes into a metadata JSON object. - CacheMetadataTemplate metadataTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, CacheMetadataTemplate.class); - - CacheMetadata cacheMetadata = CacheMetadataTranslator.fromTemplate(metadataTemplate, fakePath); - - List layers = cacheMetadata.getLayers().getLayers(); - - // Checks that the base layer was translated correctly. - CachedLayerWithMetadata baseLayer = layers.get(0); - Assert.assertEquals( - CacheFiles.getLayerFile(fakePath, baseLayerBlobDescriptor.getDigest()), - baseLayer.getContentFile()); - Assert.assertEquals(baseLayerBlobDescriptor, baseLayer.getBlobDescriptor()); - Assert.assertEquals(baseLayerDiffId, baseLayer.getDiffId()); - - // Checks that the classses layer was translated correctly. - CachedLayerWithMetadata classesLayer = layers.get(1); - Assert.assertEquals( - CacheFiles.getLayerFile(fakePath, classesLayerBlobDescriptor.getDigest()), - classesLayer.getContentFile()); - Assert.assertEquals(classesLayerBlobDescriptor, classesLayer.getBlobDescriptor()); - Assert.assertEquals(classesLayerDiffId, classesLayer.getDiffId()); - Assert.assertNotNull(classesLayer.getMetadata()); - Assert.assertEquals( - CLASSES_LAYER_SOURCE_FILE, - classesLayer.getMetadata().getEntries().get(0).getAbsoluteSourceFileString()); - Assert.assertEquals( - CLASSES_LAYER_EXTRACTION_PATH.toString(), - classesLayer.getMetadata().getEntries().get(0).getAbsoluteExtractionPathString()); - Assert.assertEquals( - CLASSES_LAYER_LAST_MODIFIED_TIME, classesLayer.getMetadata().getLastModifiedTime()); - } - - @Test - public void testToTemplate() throws URISyntaxException, IOException { - Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - - CachedLayer baseCachedLayer = - new CachedLayer(mockPath, baseLayerBlobDescriptor, baseLayerDiffId); - CachedLayerWithMetadata baseLayer = new CachedLayerWithMetadata(baseCachedLayer, null); - - CachedLayer classesCachedLayer = - new CachedLayer(mockPath, classesLayerBlobDescriptor, classesLayerDiffId); - LayerMetadata classesLayerMetadata = - LayerMetadata.from( - ImmutableList.of( - new LayerEntry( - Paths.get(CLASSES_LAYER_SOURCE_FILE), CLASSES_LAYER_EXTRACTION_PATH)), - CLASSES_LAYER_LAST_MODIFIED_TIME); - CachedLayerWithMetadata classesLayer = - new CachedLayerWithMetadata(classesCachedLayer, classesLayerMetadata); - - CacheMetadata cacheMetadata = - CacheMetadata.builder().addLayer(baseLayer).addLayer(classesLayer).build(); - - CacheMetadataTemplate cacheMetadataTemplate = CacheMetadataTranslator.toTemplate(cacheMetadata); - - // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(cacheMetadataTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java deleted file mode 100644 index 9157dcc343..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheReaderTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.Comparator; -import java.util.Optional; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link CacheReader}. */ -public class CacheReaderTest { - - // TODO: Replace with filesystem.DirectoryWalker. - private static void copyDirectory(Path source, Path destination) throws IOException { - new DirectoryWalker(source) - .filter(path -> !path.equals(source)) - .walk( - path -> { - Path newPath = destination.resolve(source.relativize(path)); - Files.copy(path, newPath); - }); - } - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private Path testCacheFolder; - - @Before - public void setUp() throws IOException, URISyntaxException { - testCacheFolder = temporaryFolder.newFolder().toPath(); - - // Copies the test resource cache to the temporary test cache folder. - Path resourceCache = Paths.get(Resources.getResource("cache").toURI()); - copyDirectory(resourceCache, testCacheFolder); - } - - @Test - public void testAreBaseImageLayersCached() - throws DigestException, CacheMetadataCorruptedException, IOException { - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - Assert.assertNotNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"))); - Assert.assertNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"))); - Assert.assertNotNull( - cacheReader.getLayer( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"))); - } - } - - @Test - public void testGetLayerFile() throws CacheMetadataCorruptedException, IOException { - Path expectedFile = - testCacheFolder.resolve( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz"); - - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - - Assert.assertEquals( - expectedFile, - cacheReader.getLayerFile( - ImmutableList.of( - new LayerEntry( - Paths.get("/some/source/path"), - AbsoluteUnixPath.get("/some/extraction/path"))))); - Assert.assertNull(cacheReader.getLayerFile(ImmutableList.of())); - } - } - - @Test - public void testGetUpToDateLayerBySourceFiles() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - // The two last modified times to use. Must be in thousands as most file time granularity is in - // seconds. - FileTime olderLastModifiedTime = FileTime.fromMillis(1000); - FileTime newerLastModifiedTime = FileTime.fromMillis(2000); - - // Copies test files to a modifiable temporary folder. - Path resourceSourceFilesPath = Paths.get(Resources.getResource("layer").toURI()); - Path testSourceFilesPath = temporaryFolder.newFolder().toPath(); - copyDirectory(resourceSourceFilesPath, testSourceFilesPath); - - // Gets the contents of the test source files. - ImmutableList testSourceFiles = new DirectoryWalker(testSourceFilesPath).walk(); - - // Walk the files in reverse order so that the subfiles are changed before the parent - // directories are. - ImmutableList paths = - ImmutableList.sortedCopyOf(Comparator.reverseOrder(), testSourceFiles); - for (Path path : paths) { - Files.setLastModifiedTime(path, olderLastModifiedTime); - } - - // Sets the metadata source file to the new temporary folder. - CachedLayerWithMetadata classesCachedLayer; - try (Cache cache = Cache.init(testCacheFolder)) { - ImageLayers cachedLayers = - cache.getMetadata().filterLayers().filter(); - - Assert.assertEquals(3, cachedLayers.size()); - classesCachedLayer = cachedLayers.get(2); - - Assert.assertNotNull(classesCachedLayer.getMetadata()); - classesCachedLayer - .getMetadata() - .setEntries( - testSourceFiles - .stream() - .map( - testSourceFile -> - new LayerMetadata.LayerMetadataEntry( - testSourceFile.toString(), - AbsoluteUnixPath.get("/some/extraction/path") - .resolve(testSourceFilesPath.relativize(testSourceFile)) - .toString())) - .collect(ImmutableList.toImmutableList())); - } - - try (Cache cache = Cache.init(testCacheFolder)) { - CacheReader cacheReader = new CacheReader(cache); - - ImmutableList upToDateLayerEntries = - testSourceFiles - .stream() - .map( - testSourceFile -> - new LayerEntry( - testSourceFile, - AbsoluteUnixPath.get("/some/extraction/path") - .resolve(testSourceFilesPath.relativize(testSourceFile)))) - .collect(ImmutableList.toImmutableList()); - - Optional optionalUpToDateLayer = - cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries); - Assert.assertEquals( - classesCachedLayer.getBlobDescriptor(), - optionalUpToDateLayer.orElseThrow(AssertionError::new).getBlobDescriptor()); - - // Changes a file and checks that the change is detected. - Files.setLastModifiedTime( - testSourceFilesPath.resolve("a").resolve("b").resolve("bar"), newerLastModifiedTime); - Assert.assertFalse( - cacheReader.getUpToDateLayerByLayerEntries(upToDateLayerEntries).isPresent()); - Assert.assertFalse( - cacheReader - .getUpToDateLayerByLayerEntries( - testSourceFiles - .stream() - .map( - testSourceFile -> - new LayerEntry( - testSourceFile, - AbsoluteUnixPath.get("/another/extraction/path") - .resolve(testSourceFilesPath.relativize(testSourceFile)))) - .collect(ImmutableList.toImmutableList())) - .isPresent()); - - // Any non-cached directory should be deemed modified. - Assert.assertFalse( - cacheReader - .getUpToDateLayerByLayerEntries( - new DirectoryWalker(resourceSourceFilesPath) - .walk() - .stream() - .map( - resourceSourceFile -> - new LayerEntry( - resourceSourceFile, - AbsoluteUnixPath.get("/some/extraction/path") - .resolve( - resourceSourceFilesPath.relativize(resourceSourceFile)))) - .collect(ImmutableList.toImmutableList())) - .isPresent()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java deleted file mode 100644 index 8d6888a7d0..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.LayerMetadata.LayerMetadataEntry; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.NotDirectoryException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link Cache}. */ -public class CacheTest { - - @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); - - @Test - public void testInit_empty() throws IOException, CacheMetadataCorruptedException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - - Cache cache = Cache.init(cacheDirectory); - Assert.assertEquals(0, cache.getMetadata().getLayers().getLayers().size()); - } - - @Test - public void testInit_notDirectory() throws CacheMetadataCorruptedException, IOException { - Path tempFile = temporaryCacheDirectory.newFile().toPath(); - - try { - Cache.init(tempFile); - Assert.fail("Cache should not be able to initialize on non-directory"); - - } catch (NotDirectoryException ex) { - Assert.assertEquals("The cache can only write to a directory", ex.getMessage()); - } - } - - @Test - public void testInit_withMetadata() - throws URISyntaxException, IOException, CacheMetadataCorruptedException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - - Path resourceMetadataJsonPath = - Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); - Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); - - try (Cache cache = Cache.init(cacheDirectory)) { - Assert.assertEquals(2, cache.getMetadata().getLayers().getLayers().size()); - } - - Assert.assertArrayEquals( - Files.readAllBytes(resourceMetadataJsonPath), Files.readAllBytes(testMetadataJsonPath)); - } - - @Test - public void test_saveMetadata_noDuplicates() - throws IOException, CacheMetadataCorruptedException, DigestException, URISyntaxException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - - Path resourceMetadataJsonPath = - Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - Path testMetadataJsonPath = cacheDirectory.resolve(CacheFiles.METADATA_FILENAME); - Files.copy(resourceMetadataJsonPath, testMetadataJsonPath); - - DescriptorDigest descriptorDigest1 = - DescriptorDigest.fromHash( - "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - DescriptorDigest descriptorDigest2 = - DescriptorDigest.fromHash( - "6f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); - - LayerEntry layerEntry1 = - new LayerEntry(Paths.get("some", "file"), AbsoluteUnixPath.get("/extraction/path/1")); - LayerEntry layerEntry2 = - new LayerEntry( - Paths.get("some", "other", "file"), AbsoluteUnixPath.get("/extraction/path/1")); - LayerEntry layerEntry3 = - new LayerEntry(Paths.get("another", "file"), AbsoluteUnixPath.get("/extraction/path/2")); - LayerEntry layerEntry4 = - new LayerEntry( - Paths.get("yet", "another", "file"), AbsoluteUnixPath.get("/extraction/path/2")); - - LayerMetadata layerMetadata1 = - LayerMetadata.from( - ImmutableList.of(layerEntry1, layerEntry2, layerEntry3, layerEntry4), - FileTime.from(Instant.now())); - LayerMetadata layerMetadata2 = - LayerMetadata.from( - ImmutableList.of(layerEntry3, layerEntry4), FileTime.from(Instant.EPOCH)); - - DescriptorDigest mockDiffId = - DescriptorDigest.fromHash( - "91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"); - - // Layers ABA. - List cachedLayersWithMetadata = - Arrays.asList( - new CachedLayerWithMetadata( - new CachedLayer( - Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest1), mockDiffId), - layerMetadata1), - new CachedLayerWithMetadata( - new CachedLayer( - Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest2), mockDiffId), - layerMetadata2), - new CachedLayerWithMetadata( - new CachedLayer( - Paths.get("nonexistent"), new BlobDescriptor(descriptorDigest1), mockDiffId), - layerMetadata2)); - - // Saves the new layers to the cache metadata. - try (Cache cache = Cache.init(cacheDirectory)) { - cache.addCachedLayersWithMetadataToMetadata(cachedLayersWithMetadata); - } - - // Reload the cache and check that all digests are unique. - try (Cache cache = Cache.init(cacheDirectory)) { - Set encounteredDigests = new HashSet<>(); - for (CachedLayerWithMetadata layer : cache.getMetadata().getLayers()) { - DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); - Assert.assertFalse(encounteredDigests.contains(layerDigest)); - encounteredDigests.add(layerDigest); - } - - // The layer metadata for layer with digest descriptorDigest1 should be layerMetadata2. - CachedLayerWithMetadata descriptorDigest1Layer = - cache.getMetadata().getLayers().get(descriptorDigest1); - Assert.assertNotNull(descriptorDigest1Layer); - LayerMetadata layerMetadata = descriptorDigest1Layer.getMetadata(); - Assert.assertNotNull(layerMetadata); - Assert.assertEquals(2, layerMetadata.getEntries().size()); - Assert.assertEquals(FileTime.from(Instant.EPOCH), layerMetadata.getLastModifiedTime()); - Assert.assertEquals( - ImmutableList.of( - layerEntry3.getAbsoluteSourceFileString(), layerEntry4.getAbsoluteSourceFileString()), - layerMetadata - .getEntries() - .stream() - .map(LayerMetadataEntry::getAbsoluteSourceFileString) - .collect(ImmutableList.toImmutableList())); - Assert.assertEquals( - ImmutableList.of( - layerEntry3.getAbsoluteExtractionPathString(), - layerEntry4.getAbsoluteExtractionPathString()), - layerMetadata - .getEntries() - .stream() - .map(LayerMetadataEntry::getAbsoluteExtractionPathString) - .collect(ImmutableList.toImmutableList())); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java deleted file mode 100644 index ced3a601ce..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheWriterTest.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; -import com.google.cloud.tools.jib.image.UnwrittenLayer; -import com.google.common.collect.ImmutableList; -import com.google.common.io.ByteStreams; -import com.google.common.io.CountingOutputStream; -import com.google.common.io.Resources; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.stream.Stream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -/** Tests for {@link CacheWriter}. */ -public class CacheWriterTest { - - private static boolean isTarGz(Path path) { - return path.toString().endsWith(".tar.gz"); - } - - @Rule public final TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); - - private Cache testCache; - private CacheWriter cacheWriter; - - private Path resourceBlob; - - private static class ExpectedLayer { - - private final BlobDescriptor blobDescriptor; - private final DescriptorDigest diffId; - private final Blob blob; - - private ExpectedLayer(BlobDescriptor blobDescriptor, DescriptorDigest diffId, Blob blob) { - this.blobDescriptor = blobDescriptor; - this.diffId = diffId; - this.blob = blob; - } - } - - @Before - public void setUp() throws CacheMetadataCorruptedException, IOException, URISyntaxException { - Path cacheDirectory = temporaryCacheDirectory.newFolder().toPath(); - - testCache = Cache.init(cacheDirectory); - // Writes resourceBlob as a layer to the cache. - cacheWriter = new CacheWriter(testCache); - - resourceBlob = Paths.get(Resources.getResource("blobA").toURI()); - } - - @Test - public void testWriteLayer_unwritten() throws IOException { - UnwrittenLayer unwrittenLayer = new UnwrittenLayer(Blobs.from(resourceBlob)); - - ReproducibleLayerBuilder mockReproducibleLayerBuilder = - Mockito.mock(ReproducibleLayerBuilder.class); - Mockito.when(mockReproducibleLayerBuilder.build()).thenReturn(unwrittenLayer); - Mockito.when(mockReproducibleLayerBuilder.getLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry( - Paths.get("/some/source/file"), - AbsoluteUnixPath.get("/some/extraction/path")))); - - CachedLayerWithMetadata cachedLayerWithMetadata = - cacheWriter.writeLayer(mockReproducibleLayerBuilder); - testCache.addCachedLayersWithMetadataToMetadata( - Collections.singletonList(cachedLayerWithMetadata)); - - LayerMetadata layerMetadata = testCache.getUpdatedMetadata().getLayers().get(0).getMetadata(); - Assert.assertNotNull(layerMetadata); - Assert.assertEquals(1, layerMetadata.getEntries().size()); - Assert.assertEquals( - "/some/source/file", layerMetadata.getEntries().get(0).getAbsoluteSourceFileString()); - Assert.assertEquals( - "/some/extraction/path", - layerMetadata.getEntries().get(0).getAbsoluteExtractionPathString()); - - verifyCachedLayerIsExpected(getExpectedLayer(), cachedLayerWithMetadata); - } - - // Windows file overwrite issue: https://github.com/GoogleContainerTools/jib/issues/719 - @Test - public void testWriteLayer_doesNotOverwriteExistingTarGz() - throws IOException, InterruptedException { - // Writes resourceBlob as a layer to the cache. - UnwrittenLayer unwrittenLayer = new UnwrittenLayer(Blobs.from(resourceBlob)); - - ReproducibleLayerBuilder layerBuilder = Mockito.mock(ReproducibleLayerBuilder.class); - Mockito.when(layerBuilder.build()).thenReturn(unwrittenLayer); - LayerEntry layerEntry = - new LayerEntry( - Paths.get("some/source/file"), AbsoluteUnixPath.get("/some/extraction/path")); - Mockito.when(layerBuilder.getLayerEntries()).thenReturn(ImmutableList.of(layerEntry)); - - cacheWriter.writeLayer(layerBuilder); - Assert.assertEquals(1, getTarGzCountInCache()); - long tarGzModifiedTime = getTarGzModifiedTimeInCache(); - - Thread.sleep(1000); // to have different modified time - cacheWriter.writeLayer(layerBuilder); - Assert.assertEquals(1, getTarGzCountInCache()); - Assert.assertEquals(tarGzModifiedTime, getTarGzModifiedTimeInCache()); - } - - private long getTarGzCountInCache() throws IOException { - try (Stream stream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { - return stream.filter(CacheWriterTest::isTarGz).count(); - } - } - - private long getTarGzModifiedTimeInCache() throws IOException { - try (Stream fileStream = Files.walk(temporaryCacheDirectory.getRoot().toPath())) { - return fileStream - .filter(CacheWriterTest::isTarGz) - .findFirst() - .orElseThrow(AssertionError::new) - .toFile() - .lastModified(); - } - } - - @Test - public void testGetLayerOutputStream() throws IOException { - ExpectedLayer expectedLayer = getExpectedLayer(); - - CountingOutputStream layerOutputStream = - cacheWriter.getLayerOutputStream(expectedLayer.blobDescriptor.getDigest()); - expectedLayer.blob.writeTo(layerOutputStream); - layerOutputStream.close(); - CachedLayer cachedLayer = - cacheWriter.getCachedLayer( - layerOutputStream.getCount(), expectedLayer.blobDescriptor.getDigest()); - testCache.addCachedLayersToMetadata(Collections.singletonList(cachedLayer)); - - CachedLayerWithMetadata layerInMetadata = testCache.getUpdatedMetadata().getLayers().get(0); - Assert.assertNull(layerInMetadata.getMetadata()); - - verifyCachedLayerIsExpected(expectedLayer, cachedLayer); - } - - /** - * @return the expected layer to test against, represented by the {@code resourceBlob} resource - * file - */ - private ExpectedLayer getExpectedLayer() throws IOException { - // Gets the expected content descriptor, diff ID, and compressed BLOB. - ByteArrayOutputStream compressedBlobOutputStream = new ByteArrayOutputStream(); - CountingDigestOutputStream compressedDigestOutputStream = - new CountingDigestOutputStream(compressedBlobOutputStream); - CountingDigestOutputStream uncompressedDigestOutputStream; - try (GZIPOutputStream compressorStream = new GZIPOutputStream(compressedDigestOutputStream)) { - uncompressedDigestOutputStream = new CountingDigestOutputStream(compressorStream); - byte[] expectedBlobABytes = Files.readAllBytes(resourceBlob); - uncompressedDigestOutputStream.write(expectedBlobABytes); - } - - BlobDescriptor expectedBlobADescriptor = compressedDigestOutputStream.toBlobDescriptor(); - DescriptorDigest expectedBlobADiffId = - uncompressedDigestOutputStream.toBlobDescriptor().getDigest(); - - ByteArrayInputStream compressedBlobInputStream = - new ByteArrayInputStream(compressedBlobOutputStream.toByteArray()); - Blob blob = Blobs.from(compressedBlobInputStream); - - return new ExpectedLayer(expectedBlobADescriptor, expectedBlobADiffId, blob); - } - - private void verifyCachedLayerIsExpected(ExpectedLayer expectedLayer, CachedLayer cachedLayer) - throws IOException { - // Reads the cached layer back. - Path compressedBlobFile = cachedLayer.getContentFile(); - - try (GZIPInputStream in = new GZIPInputStream(Files.newInputStream(compressedBlobFile))) { - byte[] decompressedBytes = ByteStreams.toByteArray(in); - byte[] expectedBlobABytes = Files.readAllBytes(resourceBlob); - Assert.assertArrayEquals(expectedBlobABytes, decompressedBytes); - Assert.assertEquals( - expectedLayer.blobDescriptor.getSize(), cachedLayer.getBlobDescriptor().getSize()); - Assert.assertEquals( - expectedLayer.blobDescriptor.getDigest(), cachedLayer.getBlobDescriptor().getDigest()); - Assert.assertEquals(expectedLayer.blobDescriptor, cachedLayer.getBlobDescriptor()); - Assert.assertEquals(expectedLayer.diffId, cachedLayer.getDiffId()); - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java deleted file mode 100644 index 38effaba14..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link CachedLayer}. */ -@RunWith(MockitoJUnitRunner.class) -public class CachedLayerTest { - - @Mock private Path mockPath; - @Mock private BlobDescriptor mockBlobDescriptor; - @Mock private DescriptorDigest mockDiffId; - - @Test - public void testNew() { - CachedLayer layer = new CachedLayer(mockPath, mockBlobDescriptor, mockDiffId); - - Assert.assertEquals(mockPath, layer.getContentFile()); - Assert.assertEquals(mockBlobDescriptor, layer.getBlobDescriptor()); - Assert.assertEquals(mockDiffId, layer.getDiffId()); - } - - @Test - public void testGetBlob() throws URISyntaxException, IOException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); - String expectedFileAString = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); - - CachedLayer cachedLayer = new CachedLayer(fileA, mockBlobDescriptor, mockDiffId); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Blob fileBlob = cachedLayer.getBlob(); - fileBlob.writeTo(outputStream); - - Assert.assertEquals( - expectedFileAString, new String(outputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals(mockBlobDescriptor, cachedLayer.getBlobDescriptor()); - Assert.assertEquals(mockDiffId, cachedLayer.getDiffId()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java deleted file mode 100644 index eed8db7262..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerWithMetadataTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -/** Tests for {@link CachedLayerWithMetadata}. */ -public class CachedLayerWithMetadataTest { - - @Test - public void testNew() { - LayerMetadata mockLayerMetadata = Mockito.mock(LayerMetadata.class); - CachedLayerWithMetadata cachedLayerWithMetadata = - new CachedLayerWithMetadata(Mockito.mock(CachedLayer.class), mockLayerMetadata); - Assert.assertEquals(mockLayerMetadata, cachedLayerWithMetadata.getMetadata()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java deleted file mode 100644 index ca6290ad23..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachesTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link Caches}. */ -public class CachesTest { - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void testInitializer() - throws CacheMetadataCorruptedException, IOException, CacheDirectoryNotOwnedException, - CacheDirectoryCreationException { - Path tempBaseCacheDirectory = temporaryFolder.newFolder().toPath(); - Path tempApplicationCacheDirectory = temporaryFolder.newFolder().toPath(); - - try (Caches caches = - new Caches.Initializer(tempBaseCacheDirectory, false, tempApplicationCacheDirectory, false) - .init()) { - Assert.assertEquals(tempBaseCacheDirectory, caches.getBaseCache().getCacheDirectory()); - Assert.assertEquals( - tempApplicationCacheDirectory, caches.getApplicationCache().getCacheDirectory()); - } - - // Checks that the caches were closed (metadata.json saved). - Assert.assertTrue(Files.exists(tempBaseCacheDirectory.resolve(CacheFiles.METADATA_FILENAME))); - Assert.assertTrue( - Files.exists(tempApplicationCacheDirectory.resolve(CacheFiles.METADATA_FILENAME))); - } - - @Test - public void testEnsureOwnership_notOwned() throws IOException, CacheDirectoryCreationException { - Path cacheDirectory = temporaryFolder.newFolder().toPath(); - - try { - Caches.Initializer.ensureOwnership(cacheDirectory); - Assert.fail("Expected CacheDirectoryNotOwnedException to be thrown"); - - } catch (CacheDirectoryNotOwnedException ex) { - Assert.assertEquals(cacheDirectory, ex.getCacheDirectory()); - } - } - - @Test - public void testEnsureOwnership_create() - throws IOException, CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - Path cacheDirectory = temporaryFolder.newFolder().toPath(); - Path nonexistentDirectory = cacheDirectory.resolve("somefolder"); - - Caches.Initializer.ensureOwnership(nonexistentDirectory); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java deleted file mode 100644 index 6458b1962d..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/json/CacheMetadataTemplateTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache.json; - -import com.google.cloud.tools.jib.cache.json.CacheMetadataLayerPropertiesObjectTemplate.LayerEntryTemplate; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.security.DigestException; -import java.util.Collections; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link CacheMetadataTemplate}. */ -public class CacheMetadataTemplateTest { - - @Test - public void testToJson() throws URISyntaxException, IOException, DigestException { - // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - - CacheMetadataTemplate cacheMetadataTemplate = new CacheMetadataTemplate(); - - // Adds a base layer. - CacheMetadataLayerObjectTemplate baseLayerTemplate = - new CacheMetadataLayerObjectTemplate() - .setSize(631) - .setDigest( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef")) - .setDiffId( - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647")); - - // Adds an application layer. - CacheMetadataLayerPropertiesObjectTemplate propertiesTemplate = - new CacheMetadataLayerPropertiesObjectTemplate() - .setLayerEntries( - Collections.singletonList( - new LayerEntryTemplate("/some/source/path", "/some/extraction/path"))) - .setLastModifiedTime(FileTime.fromMillis(255073580723571L)); - CacheMetadataLayerObjectTemplate classesLayerTemplate = - new CacheMetadataLayerObjectTemplate() - .setSize(223) - .setDigest( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")) - .setDiffId( - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372")) - .setProperties(propertiesTemplate); - - cacheMetadataTemplate.addLayer(baseLayerTemplate).addLayer(classesLayerTemplate); - - // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(cacheMetadataTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); - } - - @Test - public void testFromJson() throws URISyntaxException, IOException, DigestException { - // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/metadata-v3.json").toURI()); - - // Deserializes into a metadata JSON object. - CacheMetadataTemplate metadataTemplate = - JsonTemplateMapper.readJsonFromFile(jsonFile, CacheMetadataTemplate.class); - - List layers = metadataTemplate.getLayers(); - - Assert.assertEquals(2, layers.size()); - - // Checks the first layer is correct. - CacheMetadataLayerObjectTemplate baseLayerTemplate = layers.get(0); - Assert.assertEquals(631, baseLayerTemplate.getSize()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"), - baseLayerTemplate.getDigest()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"), - baseLayerTemplate.getDiffId()); - Assert.assertNull(baseLayerTemplate.getProperties()); - - // Checks the second layer is correct. - CacheMetadataLayerObjectTemplate classesLayerTemplate = layers.get(1); - Assert.assertEquals(223, classesLayerTemplate.getSize()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), - classesLayerTemplate.getDigest()); - Assert.assertEquals( - DescriptorDigest.fromDigest( - "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"), - classesLayerTemplate.getDiffId()); - Assert.assertNotNull(classesLayerTemplate.getProperties()); - Assert.assertEquals(1, classesLayerTemplate.getProperties().getLayerEntries().size()); - Assert.assertEquals( - "/some/source/path", - classesLayerTemplate.getProperties().getLayerEntries().get(0).getSourceFileString()); - Assert.assertEquals( - "/some/extraction/path", - classesLayerTemplate.getProperties().getLayerEntries().get(0).getExtractionPathString()); - Assert.assertEquals( - FileTime.fromMillis(255073580723571L), - classesLayerTemplate.getProperties().getLastModifiedTime()); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 2966837231..d0e4738dbd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.io.IOException; import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; @@ -143,10 +144,10 @@ public void testBuilder() throws Exception { Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); Assert.assertEquals( expectedApplicationLayersCacheConfiguration, - buildConfiguration.getApplicationLayersCacheConfiguration()); + buildConfigurationBuilder.getApplicationLayersCacheConfiguration()); Assert.assertEquals( expectedBaseImageLayersCacheConfiguration, - buildConfiguration.getBaseImageLayersCacheConfiguration()); + buildConfigurationBuilder.getBaseImageLayersCacheConfiguration()); Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); Assert.assertEquals( @@ -155,7 +156,7 @@ public void testBuilder() throws Exception { } @Test - public void testBuilder_default() { + public void testBuilder_default() throws IOException, CacheDirectoryCreationException { // These are required and don't have defaults. String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; @@ -174,16 +175,22 @@ public void testBuilder_default() { ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .build(); - BuildConfiguration buildConfiguration = + BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder() .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .build(); + .setTargetImageConfiguration(targetImageConfiguration); + BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); - Assert.assertNull(buildConfiguration.getApplicationLayersCacheConfiguration()); - Assert.assertNull(buildConfiguration.getBaseImageLayersCacheConfiguration()); + Assert.assertNotNull(buildConfigurationBuilder.getApplicationLayersCacheConfiguration()); + Assert.assertNotEquals( + CacheConfiguration.forDefaultUserLevelCacheDirectory().getCacheDirectory(), + buildConfigurationBuilder.getApplicationLayersCacheConfiguration().getCacheDirectory()); + Assert.assertNotNull(buildConfigurationBuilder.getBaseImageLayersCacheConfiguration()); + Assert.assertEquals( + CacheConfiguration.forDefaultUserLevelCacheDirectory().getCacheDirectory(), + buildConfigurationBuilder.getBaseImageLayersCacheConfiguration().getCacheDirectory()); Assert.assertNull(buildConfiguration.getContainerConfiguration()); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); @@ -191,7 +198,7 @@ public void testBuilder_default() { } @Test - public void testBuilder_missingValues() { + public void testBuilder_missingValues() throws IOException, CacheDirectoryCreationException { // Target image is missing try { BuildConfiguration.builder() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java index 61383c2ac1..b994c249b0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import java.nio.file.Files; import java.nio.file.Paths; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 52a709a3a6..138d13d07d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -155,14 +155,14 @@ public void testLoad_stdoutFail() throws InterruptedException { Mockito.when(mockProcess.getOutputStream()).thenReturn(ByteStreams.nullOutputStream()); Mockito.when(mockProcess.getInputStream()) - .thenReturn(new ByteArrayInputStream("failed".getBytes(StandardCharsets.UTF_8))); + .thenReturn(new ByteArrayInputStream("ignored".getBytes(StandardCharsets.UTF_8))); try { testDockerClient.load(Blobs.from("jib")); Assert.fail("Process should have failed"); } catch (IOException ex) { - Assert.assertEquals("'docker load' command failed with output: failed", ex.getMessage()); + Assert.assertEquals("'docker load' command failed with output: ignored", ex.getMessage()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java index 73e10168af..a15a7f33ff 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java @@ -19,12 +19,12 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -35,6 +35,7 @@ import java.io.InputStreamReader; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; @@ -42,16 +43,26 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link ImageToTarballTranslator}. */ +@RunWith(MockitoJUnitRunner.class) public class ImageToTarballTranslatorTest { + @Mock private Layer mockLayer1; + @Mock private Layer mockLayer2; + @Test public void testToTarballBlob() throws InvalidImageReferenceException, IOException, URISyntaxException, LayerPropertyNotFoundException, DigestException { Path fileA = Paths.get(Resources.getResource("fileA").toURI()); Path fileB = Paths.get(Resources.getResource("fileB").toURI()); + long fileASize = Files.size(fileA); + long fileBSize = Files.size(fileB); DescriptorDigest fakeDigestA = DescriptorDigest.fromHash( @@ -60,11 +71,15 @@ public void testToTarballBlob() DescriptorDigest.fromHash( "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc6"); - Image testImage = - Image.builder() - .addLayer(new CachedLayer(fileA, new BlobDescriptor(fakeDigestA), fakeDigestA)) - .addLayer(new CachedLayer(fileB, new BlobDescriptor(fakeDigestB), fakeDigestB)) - .build(); + Mockito.when(mockLayer1.getBlob()).thenReturn(Blobs.from(fileA)); + Mockito.when(mockLayer1.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(fileASize, fakeDigestA)); + Mockito.when(mockLayer1.getDiffId()).thenReturn(fakeDigestA); + Mockito.when(mockLayer2.getBlob()).thenReturn(Blobs.from(fileB)); + Mockito.when(mockLayer2.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(fileBSize, fakeDigestB)); + Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); + Image testImage = Image.builder().addLayer(mockLayer1).addLayer(mockLayer2).build(); Blob tarballBlob = new ImageToTarballTranslator(testImage).toTarballBlob(ImageReference.parse("my/image:tag")); @@ -75,7 +90,7 @@ public void testToTarballBlob() new TarArchiveInputStream(tarballBytesStream)) { // Verifies layer with fileA was added. TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("fileA", headerFileALayer.getName()); + Assert.assertEquals(fakeDigestA.getHash() + ".tar.gz", headerFileALayer.getName()); String fileAString = CharStreams.toString( new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); @@ -83,7 +98,7 @@ public void testToTarballBlob() // Verifies layer with fileB was added. TarArchiveEntry headerFileBLayer = tarArchiveInputStream.getNextTarEntry(); - Assert.assertEquals("fileB", headerFileBLayer.getName()); + Assert.assertEquals(fakeDigestB.getHash() + ".tar.gz", headerFileBLayer.getName()); String fileBString = CharStreams.toString( new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index 3346f786ba..2c9c6ae23c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; import java.util.Arrays; import java.util.List; import org.hamcrest.CoreMatchers; @@ -33,10 +32,10 @@ @RunWith(MockitoJUnitRunner.class) public class ImageLayersTest { - @Mock private CachedLayer mockCachedLayer; + @Mock private Layer mockLayer; @Mock private ReferenceLayer mockReferenceLayer; @Mock private DigestOnlyLayer mockDigestOnlyLayer; - @Mock private UnwrittenLayer mockUnwrittenLayer; + @Mock private Layer mockLayer2; @Before public void setUpFakes() throws LayerPropertyNotFoundException { @@ -44,28 +43,27 @@ public void setUpFakes() throws LayerPropertyNotFoundException { DescriptorDigest mockDescriptorDigest2 = Mockito.mock(DescriptorDigest.class); DescriptorDigest mockDescriptorDigest3 = Mockito.mock(DescriptorDigest.class); - BlobDescriptor cachedLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); + BlobDescriptor layerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); BlobDescriptor referenceLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest2); BlobDescriptor referenceNoDiffIdLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest3); - // Intentionally the same digest as the mockCachedLayer. - BlobDescriptor unwrittenLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); + // Intentionally the same digest as the mockLayer. + BlobDescriptor anotherBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); - Mockito.when(mockCachedLayer.getBlobDescriptor()).thenReturn(cachedLayerBlobDescriptor); + Mockito.when(mockLayer.getBlobDescriptor()).thenReturn(layerBlobDescriptor); Mockito.when(mockReferenceLayer.getBlobDescriptor()).thenReturn(referenceLayerBlobDescriptor); Mockito.when(mockDigestOnlyLayer.getBlobDescriptor()) .thenReturn(referenceNoDiffIdLayerBlobDescriptor); - Mockito.when(mockUnwrittenLayer.getBlobDescriptor()).thenReturn(unwrittenLayerBlobDescriptor); + Mockito.when(mockLayer2.getBlobDescriptor()).thenReturn(anotherBlobDescriptor); } @Test public void testAddLayer_success() throws LayerPropertyNotFoundException { - List expectedLayers = - Arrays.asList(mockCachedLayer, mockReferenceLayer, mockDigestOnlyLayer); + List expectedLayers = Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer); ImageLayers imageLayers = ImageLayers.builder() - .add(mockCachedLayer) + .add(mockLayer) .add(mockReferenceLayer) .add(mockDigestOnlyLayer) .build(); @@ -77,20 +75,15 @@ public void testAddLayer_success() throws LayerPropertyNotFoundException { public void testAddLayer_maintainDuplicates() throws LayerPropertyNotFoundException { // must maintain duplicate List expectedLayers = - Arrays.asList( - mockCachedLayer, - mockReferenceLayer, - mockDigestOnlyLayer, - mockUnwrittenLayer, - mockCachedLayer); + Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); ImageLayers imageLayers = ImageLayers.builder() - .add(mockCachedLayer) + .add(mockLayer) .add(mockReferenceLayer) .add(mockDigestOnlyLayer) - .add(mockUnwrittenLayer) - .add(mockCachedLayer) + .add(mockLayer2) + .add(mockLayer) .build(); Assert.assertEquals(expectedLayers, imageLayers.getLayers()); @@ -100,16 +93,16 @@ public void testAddLayer_maintainDuplicates() throws LayerPropertyNotFoundExcept public void testAddLayer_removeDuplicates() throws LayerPropertyNotFoundException { // remove duplicates: last layer should be kept List expectedLayers = - Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockUnwrittenLayer, mockCachedLayer); + Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); ImageLayers imageLayers = ImageLayers.builder() .removeDuplicates() - .add(mockCachedLayer) + .add(mockLayer) .add(mockReferenceLayer) .add(mockDigestOnlyLayer) - .add(mockUnwrittenLayer) - .add(mockCachedLayer) + .add(mockLayer2) + .add(mockLayer) .build(); Assert.assertEquals(expectedLayers, imageLayers.getLayers()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java index 3e70fae524..0faa3bb480 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import org.junit.Assert; import org.junit.Test; @@ -28,32 +27,10 @@ @RunWith(MockitoJUnitRunner.class) public class LayerTest { - @Mock private Blob mockUncompressedBlob; @Mock private DescriptorDigest mockDescriptorDigest; @Mock private BlobDescriptor mockBlobDescriptor; @Mock private DescriptorDigest mockDiffId; - @Test - public void testNew_unwritten() throws LayerPropertyNotFoundException { - Layer layer = new UnwrittenLayer(mockUncompressedBlob); - - Assert.assertEquals(mockUncompressedBlob, layer.getBlob()); - - try { - layer.getBlobDescriptor(); - Assert.fail("Blob descriptor should not be available for unwritten layer"); - } catch (LayerPropertyNotFoundException ex) { - Assert.assertEquals("Blob descriptor not available for unwritten layer", ex.getMessage()); - } - - try { - layer.getDiffId(); - Assert.fail("Diff ID should not be available for unwritten layer"); - } catch (LayerPropertyNotFoundException ex) { - Assert.assertEquals("Diff ID not available for unwritten layer", ex.getMessage()); - } - } - @Test public void testNew_reference() throws LayerPropertyNotFoundException { Layer layer = new ReferenceLayer(mockBlobDescriptor, mockDiffId); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index cccc80491a..829739ccaa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -106,11 +106,11 @@ public void testBuild() throws URISyntaxException, IOException { .getLayerEntries()); // Writes the layer tar to a temporary file. - UnwrittenLayer unwrittenLayer = layerBuilder.build(); + Blob unwrittenBlob = layerBuilder.build(); Path temporaryFile = temporaryFolder.newFile().toPath(); try (OutputStream temporaryFileOutputStream = new BufferedOutputStream(Files.newOutputStream(temporaryFile))) { - unwrittenLayer.getBlob().writeTo(temporaryFileOutputStream); + unwrittenBlob.writeTo(temporaryFileOutputStream); } // Reads the file back. @@ -166,15 +166,13 @@ public void testToBlob_reproducibility() throws IOException { ImmutableList.of( new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) - .build() - .getBlob(); + .build(); Blob reproduced = new ReproducibleLayerBuilder( ImmutableList.of( new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) - .build() - .getBlob(); + .build(); byte[] layerContent = Blobs.writeToByteArray(layer); byte[] reproducedLayerContent = Blobs.writeToByteArray(reproduced); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index d3ec7777d1..edc8e40bb8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -18,10 +18,11 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; @@ -43,7 +44,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; /** Tests for {@link ImageToJsonTranslator}. */ public class ImageToJsonTranslatorTest { @@ -52,7 +52,7 @@ public class ImageToJsonTranslatorTest { @Before public void setUp() throws DigestException, LayerPropertyNotFoundException { - Image.Builder testImageBuilder = Image.builder(); + Image.Builder testImageBuilder = Image.builder(); testImageBuilder.setCreated(Instant.ofEpochSecond(20)); testImageBuilder.addEnvironmentVariable("VAR1", "VAL1"); @@ -67,9 +67,24 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); - CachedLayer fakeLayer = - new CachedLayer(Mockito.mock(Path.class), new BlobDescriptor(1000, fakeDigest), fakeDigest); - testImageBuilder.addLayer(fakeLayer); + testImageBuilder.addLayer( + new Layer() { + + @Override + public Blob getBlob() throws LayerPropertyNotFoundException { + return Blobs.from("ignored"); + } + + @Override + public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException { + return new BlobDescriptor(1000, fakeDigest); + } + + @Override + public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { + return fakeDigest; + } + }); testImageBuilder.addHistory( HistoryEntry.builder() .setCreationTimestamp(Instant.EPOCH) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index 2ea09946a2..4486e9d514 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -17,14 +17,13 @@ package com.google.cloud.tools.jib.tar; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -100,6 +99,8 @@ public void testToBlob_stringsAndTarArchiveEntriesWithCompression() throws IOExc public void testToBlob_multiByte() throws IOException { testTarStreamBuilder.addByteEntry("日本語".getBytes(StandardCharsets.UTF_8), "test"); testTarStreamBuilder.addByteEntry("asdf".getBytes(StandardCharsets.UTF_8), "crepecake"); + testTarStreamBuilder.addBlobEntry( + Blobs.from("jib"), "jib".getBytes(StandardCharsets.UTF_8).length, "jib"); Blob blob = testTarStreamBuilder.toBlob(); // Writes the BLOB and captures the output. @@ -116,15 +117,18 @@ public void testToBlob_multiByte() throws IOException { // Verify multi-byte characters are written/read correctly TarArchiveEntry headerFile = tarArchiveInputStream.getNextTarEntry(); Assert.assertEquals("test", headerFile.getName()); - String fileString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals("日本語", fileString); + Assert.assertEquals( + "日本語", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); headerFile = tarArchiveInputStream.getNextTarEntry(); Assert.assertEquals("crepecake", headerFile.getName()); - fileString = - CharStreams.toString(new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - Assert.assertEquals("asdf", fileString); + Assert.assertEquals( + "asdf", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); + + headerFile = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("jib", headerFile.getName()); + Assert.assertEquals( + "jib", new String(ByteStreams.toByteArray(tarArchiveInputStream), StandardCharsets.UTF_8)); Assert.assertNull(tarArchiveInputStream.getNextTarEntry()); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index c4e0dfbd7a..ef48ff9395 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; +import java.io.IOException; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -65,7 +66,9 @@ public void setTargetImage(String targetImage) { } @TaskAction - public void buildDocker() throws InvalidImageReferenceException { + public void buildDocker() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + BuildStepsExecutionException { if (!new DockerClient().isDockerInstalled()) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); @@ -112,13 +115,7 @@ public void buildDocker() throws InvalidImageReferenceException { .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); - // Uses a directory in the Gradle build cache as the Jib cache. - try { - BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); - - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { - throw new GradleException(ex.getMessage(), ex.getCause()); - } + BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index e18073839c..ec85cb6a57 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import java.io.IOException; import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -69,7 +70,9 @@ public void setTargetImage(String targetImage) { } @TaskAction - public void buildImage() throws InvalidImageReferenceException { + public void buildImage() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + BuildStepsExecutionException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); @@ -133,12 +136,7 @@ public void buildImage() throws InvalidImageReferenceException { .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); - try { - BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); - - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { - throw new GradleException(ex.getMessage(), ex.getCause()); - } + BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 703a864f50..e62af1974a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; @@ -27,10 +27,10 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; +import java.io.IOException; import java.nio.file.Paths; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; -import org.gradle.api.GradleException; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Nested; @@ -97,7 +97,9 @@ private String getTargetPath() { } @TaskAction - public void buildTar() throws InvalidImageReferenceException { + public void buildTar() + throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, + CacheDirectoryCreationException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); @@ -137,14 +139,8 @@ public void buildTar() throws InvalidImageReferenceException { .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); - // Uses a directory in the Gradle build cache as the Jib cache. - try { - BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) - .build(helpfulSuggestions); - - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { - throw new GradleException(ex.getMessage(), ex.getCause()); - } + BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) + .build(helpfulSuggestions); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index af2240bf9a..8174195e0c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -100,9 +101,10 @@ public void execute() throws MojoExecutionException { BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException - | BuildStepsExecutionException - | InvalidImageReferenceException ex) { + } catch (CacheDirectoryCreationException | InvalidImageReferenceException | IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + + } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index a88b5c11a3..fde58f5827 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import java.io.IOException; import java.util.Arrays; import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; @@ -122,32 +123,35 @@ public void execute() throws MojoExecutionException, MojoFailureException { .setCredentialRetrievers(defaultCredentialRetrievers.asList()) .build(); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(targetImageConfiguration) - .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) - .build(); - - HelpfulSuggestions helpfulSuggestions = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) - .build(); - try { + BuildConfiguration buildConfiguration = + pluginConfigurationProcessor + .getBuildConfigurationBuilder() + .setBaseImageConfiguration( + pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) + .setTargetImageConfiguration(targetImageConfiguration) + .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) + .setContainerConfiguration( + pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) + .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) + .build(); + + HelpfulSuggestions helpfulSuggestions = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) + .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) + .build(); + BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException | BuildStepsExecutionException ex) { + } catch (CacheDirectoryCreationException | IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + + } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 5ea2adf1d3..214cefa755 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -99,9 +100,10 @@ public void execute() throws MojoExecutionException { .build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException - | BuildStepsExecutionException - | InvalidImageReferenceException ex) { + } catch (CacheDirectoryCreationException | InvalidImageReferenceException | IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + + } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 1df8208c16..93b92fb40f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -19,12 +19,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; -import com.google.cloud.tools.jib.cache.Caches.Initializer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -37,7 +32,6 @@ import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; -import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.Path; import java.util.StringJoiner; @@ -84,13 +78,10 @@ private static String buildMessageWithTargetImageReferences( * * @param buildConfiguration the configuration parameters for the build * @return a {@link BuildStepsRunner} for building to a registry - * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfiguration) - throws CacheDirectoryCreationException { + public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfiguration) { return new BuildStepsRunner( - BuildSteps.forBuildToDockerRegistry( - buildConfiguration, getCacheInitializer(buildConfiguration)), + BuildSteps.forBuildToDockerRegistry(buildConfiguration), buildMessageWithTargetImageReferences( buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "..."), buildMessageWithTargetImageReferences( @@ -102,13 +93,10 @@ buildConfiguration, getCacheInitializer(buildConfiguration)), * * @param buildConfiguration the configuration parameters for the build * @return a {@link BuildStepsRunner} for building to a Docker daemon - * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildToDockerDaemon(BuildConfiguration buildConfiguration) - throws CacheDirectoryCreationException { + public static BuildStepsRunner forBuildToDockerDaemon(BuildConfiguration buildConfiguration) { return new BuildStepsRunner( - BuildSteps.forBuildToDockerDaemon( - buildConfiguration, getCacheInitializer(buildConfiguration)), + BuildSteps.forBuildToDockerDaemon(buildConfiguration), buildMessageWithTargetImageReferences( buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), buildMessageWithTargetImageReferences( @@ -121,37 +109,15 @@ buildConfiguration, getCacheInitializer(buildConfiguration)), * @param outputPath the path to output the tarball to * @param buildConfiguration the configuration parameters for the build * @return a {@link BuildStepsRunner} for building a tarball - * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created */ - public static BuildStepsRunner forBuildTar(Path outputPath, BuildConfiguration buildConfiguration) - throws CacheDirectoryCreationException { + public static BuildStepsRunner forBuildTar( + Path outputPath, BuildConfiguration buildConfiguration) { return new BuildStepsRunner( - BuildSteps.forBuildToTar( - outputPath, buildConfiguration, getCacheInitializer(buildConfiguration)), + BuildSteps.forBuildToTar(outputPath, buildConfiguration), String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); } - // TODO: Move this up to somewhere where defaults for cache location are provided and ownership is - // checked rather than in Caches.Initializer. - private static Initializer getCacheInitializer(BuildConfiguration buildConfiguration) - throws CacheDirectoryCreationException { - CacheConfiguration applicationLayersCacheConfiguration = - buildConfiguration.getApplicationLayersCacheConfiguration() == null - ? CacheConfiguration.makeTemporary() - : buildConfiguration.getApplicationLayersCacheConfiguration(); - CacheConfiguration baseImageLayersCacheConfiguration = - buildConfiguration.getBaseImageLayersCacheConfiguration() == null - ? CacheConfiguration.forDefaultUserLevelCacheDirectory() - : buildConfiguration.getBaseImageLayersCacheConfiguration(); - - return new Initializer( - baseImageLayersCacheConfiguration.getCacheDirectory(), - applicationLayersCacheConfiguration.shouldEnsureOwnership(), - applicationLayersCacheConfiguration.getCacheDirectory(), - applicationLayersCacheConfiguration.shouldEnsureOwnership()); - } - private static void handleRegistryUnauthorizedException( RegistryUnauthorizedException registryUnauthorizedException, HelpfulSuggestions helpfulSuggestions) @@ -227,10 +193,6 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); - } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forCacheNeedsClean(), cacheMetadataCorruptedException); - } catch (ExecutionException executionException) { Throwable exceptionDuringBuildSteps = executionException.getCause(); @@ -279,21 +241,9 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut helpfulSuggestions.none(), executionException.getCause()); } - } catch (InterruptedException | IOException | CacheDirectoryCreationException ex) { + } catch (InterruptedException ex) { // TODO: Add more suggestions for various build failures. throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); - - } catch (CacheDirectoryNotOwnedException ex) { - String helpfulSuggestion = - helpfulSuggestions.forCacheDirectoryNotOwned(ex.getCacheDirectory()); - CacheConfiguration applicationLayersCacheConfiguration = - buildSteps.getBuildConfiguration().getApplicationLayersCacheConfiguration(); - if (applicationLayersCacheConfiguration != null - && ex.getCacheDirectory() - .equals(applicationLayersCacheConfiguration.getCacheDirectory())) { - helpfulSuggestion = helpfulSuggestions.forCacheNeedsClean(); - } - throw new BuildStepsExecutionException(helpfulSuggestion, ex); } } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index ea500e9f2c..f2b79a5a0e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -19,11 +19,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.cache.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.cache.CacheDirectoryNotOwnedException; -import com.google.cloud.tools.jib.cache.CacheMetadataCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -32,9 +28,7 @@ import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.collect.ImmutableList; -import java.io.IOException; import java.net.UnknownHostException; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; @@ -105,28 +99,9 @@ public void testBuildImage_pass() throws BuildStepsExecutionException { testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); } - @Test - public void testBuildImage_cacheMetadataCorruptedException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - CacheMetadataCorruptedException mockCacheMetadataCorruptedException = - Mockito.mock(CacheMetadataCorruptedException.class); - Mockito.doThrow(mockCacheMetadataCorruptedException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean(), ex.getMessage()); - Assert.assertEquals(mockCacheMetadataCorruptedException, ex.getCause()); - } - } - @Test public void testBuildImage_executionException_httpHostConnectException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { HttpHostConnectException mockHttpHostConnectException = Mockito.mock(HttpHostConnectException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); @@ -144,8 +119,7 @@ public void testBuildImage_executionException_httpHostConnectException() @Test public void testBuildImage_executionException_unknownHostException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); @@ -162,8 +136,7 @@ public void testBuildImage_executionException_unknownHostException() @Test public void testBuildImage_executionException_insecureRegistryException() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { InsecureRegistryException mockInsecureRegistryException = Mockito.mock(InsecureRegistryException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); @@ -181,8 +154,7 @@ public void testBuildImage_executionException_insecureRegistryException() @Test public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getImageReference()) @@ -207,8 +179,7 @@ public void testBuildImage_executionException_registryUnauthorizedException_stat @Test public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); @@ -232,8 +203,7 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr @Test public void testBuildImage_executionException_registryCredentialsNotSentException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { Mockito.when(mockExecutionException.getCause()) .thenReturn(mockRegistryCredentialsNotSentException); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); @@ -250,8 +220,7 @@ public void testBuildImage_executionException_registryCredentialsNotSentExceptio @Test public void testBuildImage_executionException_other() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { + throws InterruptedException, ExecutionException { Throwable throwable = new Throwable(); Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); @@ -265,73 +234,4 @@ public void testBuildImage_executionException_other() Assert.assertEquals(throwable, ex.getCause()); } } - - @Test - public void testBuildImage_otherException() - throws InterruptedException, ExecutionException, CacheMetadataCorruptedException, IOException, - CacheDirectoryNotOwnedException, CacheDirectoryCreationException { - IOException ioException = new IOException(); - Mockito.doThrow(ioException).when(mockBuildSteps).run(); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); - Assert.assertEquals(ioException, ex.getCause()); - } - } - - @Test - public void testBuildImage_cacheDirectoryNotOwnedException_needsClean() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { - Path expectedCacheDirectory = Paths.get("some/path"); - - CacheDirectoryNotOwnedException mockCacheDirectoryNotOwnedException = - Mockito.mock(CacheDirectoryNotOwnedException.class); - Mockito.when(mockCacheDirectoryNotOwnedException.getCacheDirectory()) - .thenReturn(expectedCacheDirectory); - Mockito.doThrow(mockCacheDirectoryNotOwnedException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getApplicationLayersCacheConfiguration()) - .thenReturn(CacheConfiguration.forPath(expectedCacheDirectory)); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean(), ex.getMessage()); - Assert.assertEquals(mockCacheDirectoryNotOwnedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_cacheDirectoryNotOwnedException() - throws InterruptedException, ExecutionException, CacheDirectoryNotOwnedException, - CacheMetadataCorruptedException, IOException, CacheDirectoryCreationException { - Path expectedCacheDirectory = Paths.get("some/path"); - - CacheDirectoryNotOwnedException mockCacheDirectoryNotOwnedException = - Mockito.mock(CacheDirectoryNotOwnedException.class); - Mockito.when(mockCacheDirectoryNotOwnedException.getCacheDirectory()) - .thenReturn(expectedCacheDirectory); - Mockito.doThrow(mockCacheDirectoryNotOwnedException).when(mockBuildSteps).run(); - - Mockito.when(mockBuildConfiguration.getApplicationLayersCacheConfiguration()) - .thenReturn(CacheConfiguration.forPath(Paths.get("another/path"))); - - try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forCacheDirectoryNotOwned(expectedCacheDirectory), - ex.getMessage()); - Assert.assertEquals(mockCacheDirectoryNotOwnedException, ex.getCause()); - } - } } From 420e597b68edc0ae27b4a85396eb84bfafe2bcb3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 27 Sep 2018 18:39:34 -0400 Subject: [PATCH 0248/2020] Remove sorting / add resources directory directly (#1058) --- .../jib/gradle/GradleLayerConfigurations.java | 19 +-- .../gradle/GradleLayerConfigurationsTest.java | 108 +++++++++--------- .../jib/maven/MavenLayerConfigurations.java | 8 -- .../maven/MavenLayerConfigurationsTest.java | 92 +++++++-------- 4 files changed, 97 insertions(+), 130 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 6c70fd2f7d..944279593d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -25,7 +25,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -61,7 +60,6 @@ static JavaLayerConfigurations getForProject( List dependenciesFiles = new ArrayList<>(); List snapshotDependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); List classesFiles = new ArrayList<>(); List extraFiles = new ArrayList<>(); @@ -82,13 +80,7 @@ static JavaLayerConfigurations getForProject( logger.warn("No classes files were found - did you compile your project?"); } - // Adds each file in the resources output directory to the resources files list. Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); - if (Files.exists(resourcesOutputDirectory)) { - try (Stream resourceFileStream = Files.list(resourcesOutputDirectory)) { - resourceFileStream.forEach(resourcesFiles::add); - } - } // Adds all other files to the dependencies files list. FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); @@ -113,13 +105,6 @@ static JavaLayerConfigurations getForProject( } } - // Sorts all files by path for consistent ordering. - Collections.sort(dependenciesFiles); - Collections.sort(snapshotDependenciesFiles); - Collections.sort(resourcesFiles); - Collections.sort(classesFiles); - Collections.sort(extraFiles); - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); AbsoluteUnixPath resourcesExtractionPath = @@ -135,8 +120,8 @@ static JavaLayerConfigurations getForProject( layerBuilder.addSnapshotDependencyFile( file, dependenciesExtractionPath.resolve(file.getFileName())); } - for (Path file : resourcesFiles) { - layerBuilder.addResourceFile(file, resourcesExtractionPath.resolve(file.getFileName())); + if (Files.exists(resourcesOutputDirectory)) { + layerBuilder.addResourceFile(resourcesOutputDirectory, resourcesExtractionPath); } for (Path file : classesFiles) { layerBuilder.addClassFile(file, classesExtractionPath.resolve(file.getFileName())); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index bd97b0fc77..15b2b74078 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -31,8 +31,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; @@ -59,8 +59,8 @@ private static class TestFileCollection extends AbstractFileCollection { private final Set files; - private TestFileCollection(Set files) { - this.files = files; + private TestFileCollection(Set files) { + this.files = files.stream().map(Path::toFile).collect(Collectors.toSet()); } @Override @@ -74,17 +74,22 @@ public Set getFiles() { } } - private static ImmutableList getSourceFilesFromLayerEntries( - ImmutableList layerEntries) { - return layerEntries - .stream() - .map(LayerEntry::getSourceFile) - .collect(ImmutableList.toImmutableList()); + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); } - private static List getExtractionPathFromLayerEntries(List layerEntries) { - Stream stream = layerEntries.stream(); - return stream.map(LayerEntry::getAbsoluteExtractionPathString).collect(Collectors.toList()); + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } @Mock private Project mockProject; @@ -97,26 +102,21 @@ private static List getExtractionPathFromLayerEntries(List l @Before public void setUp() throws URISyntaxException { - Set classesFiles = - ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI()).toFile()); + Set classesFiles = + ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI())); FileCollection classesFileCollection = new TestFileCollection(classesFiles); - File resourcesOutputDir = - Paths.get(Resources.getResource("application/resources").toURI()).toFile(); + Path resourcesOutputDir = Paths.get(Resources.getResource("application/resources").toURI()); - Set allFiles = new HashSet<>(classesFiles); + Set allFiles = new HashSet<>(classesFiles); allFiles.add(resourcesOutputDir); + allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); + allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI())); allFiles.add( - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI()).toFile()); - allFiles.add( - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()).toFile()); - allFiles.add( - Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI()) - .toFile()); + Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI())); allFiles.add( Paths.get( - Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") - .toURI()) - .toFile()); + Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") + .toURI())); FileCollection runtimeFileCollection = new TestFileCollection(allFiles); Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); @@ -126,7 +126,7 @@ public void setUp() throws URISyntaxException { Mockito.when(mockSourceSetContainer.getByName("main")).thenReturn(mockMainSourceSet); Mockito.when(mockMainSourceSet.getOutput()).thenReturn(mockMainSourceSetOutput); Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); - Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir); + Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); } @@ -143,6 +143,7 @@ public void test_correctFiles() throws URISyntaxException, IOException { applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); ImmutableList expectedResourcesFiles = ImmutableList.of( + applicationDirectory.resolve("resources"), applicationDirectory.resolve("resources/resourceA"), applicationDirectory.resolve("resources/resourceB"), applicationDirectory.resolve("resources/world")); @@ -156,27 +157,22 @@ public void test_correctFiles() throws URISyntaxException, IOException { JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( mockProject, mockLogger, Paths.get("nonexistent/path"), appRoot); - Assert.assertEquals( - expectedDependenciesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); - Assert.assertEquals( + assertSourcePathsUnordered( + expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); + assertSourcePathsUnordered( expectedSnapshotDependenciesFiles, - getSourceFilesFromLayerEntries( - javaLayerConfigurations.getSnapshotDependencyLayerEntries())); - Assert.assertEquals( - expectedResourcesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getResourceLayerEntries())); - Assert.assertEquals( - expectedClassesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getClassLayerEntries())); - Assert.assertEquals( - expectedExtraFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); + javaLayerConfigurations.getSnapshotDependencyLayerEntries()); + assertSourcePathsUnordered( + expectedResourcesFiles, javaLayerConfigurations.getResourceLayerEntries()); + assertSourcePathsUnordered( + expectedClassesFiles, javaLayerConfigurations.getClassLayerEntries()); + assertSourcePathsUnordered( + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); } @Test public void test_noClassesFiles() throws IOException { - File nonexistentFile = new File("/nonexistent/file"); + Path nonexistentFile = Paths.get("/nonexistent/file"); Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); @@ -207,9 +203,8 @@ public void test_extraFiles() throws URISyntaxException, IOException { extraFilesDirectory.resolve("c/cat"), extraFilesDirectory.resolve("foo")); - Assert.assertEquals( - expectedExtraFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); + assertSourcePathsUnordered( + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); } @Test @@ -220,26 +215,27 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxE GradleLayerConfigurations.getForProject( mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); - Assert.assertEquals( + assertExtractionPathsUnordered( Arrays.asList( "/my/app/libs/dependency-1.0.0.jar", "/my/app/libs/libraryA.jar", "/my/app/libs/libraryB.jar"), - getExtractionPathFromLayerEntries(configuration.getDependencyLayerEntries())); - Assert.assertEquals( + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - getExtractionPathFromLayerEntries(configuration.getSnapshotDependencyLayerEntries())); - Assert.assertEquals( + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList( + "/my/app/resources", "/my/app/resources/resourceA", "/my/app/resources/resourceB", "/my/app/resources/world"), - getExtractionPathFromLayerEntries(configuration.getResourceLayerEntries())); - Assert.assertEquals( + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList("/my/app/classes/HelloWorld.class", "/my/app/classes/some.class"), - getExtractionPathFromLayerEntries(configuration.getClassLayerEntries())); - Assert.assertEquals( + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - getExtractionPathFromLayerEntries(configuration.getExtraFilesLayerEntries())); + configuration.getExtraFilesLayerEntries()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 66a2ee969c..366a60fa0e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -26,7 +26,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -100,13 +99,6 @@ static JavaLayerConfigurations getForProject( } } - // Sort all files by path for consistent ordering. - Collections.sort(dependenciesFiles); - Collections.sort(snapshotDependenciesFiles); - Collections.sort(resourcesFiles); - Collections.sort(classesFiles); - Collections.sort(extraFiles); - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); AbsoluteUnixPath resourcesExtractionPath = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index f591096176..5c4e0fe8b4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -27,11 +27,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; @@ -48,17 +47,22 @@ @RunWith(MockitoJUnitRunner.class) public class MavenLayerConfigurationsTest { - private static ImmutableList getSourceFilesFromLayerEntries( - ImmutableList layerEntries) { - return layerEntries - .stream() - .map(LayerEntry::getSourceFile) - .collect(ImmutableList.toImmutableList()); + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); } - private static List getExtractionPathFromLayerEntries(List layerEntries) { - Stream stream = layerEntries.stream(); - return stream.map(LayerEntry::getAbsoluteExtractionPathString).collect(Collectors.toList()); + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } @Rule public TestRepository testRepository = new TestRepository(); @@ -89,12 +93,10 @@ public void setUp() throws URISyntaxException { @Test public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedDependenciesFiles = - // on windows, these files may be in a different order, so sort - ImmutableList.sortedCopyOf( - ImmutableList.of( - testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), - Paths.get("application", "dependencies", "libraryA.jar"), - Paths.get("application", "dependencies", "libraryB.jar"))); + ImmutableList.of( + testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), + Paths.get("application", "dependencies", "libraryA.jar"), + Paths.get("application", "dependencies", "libraryB.jar")); ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); @@ -116,19 +118,15 @@ public void test_correctFiles() throws URISyntaxException, IOException { JavaLayerConfigurations javaLayerConfigurations = MavenLayerConfigurations.getForProject( mockMavenProject, Paths.get("nonexistent/path"), AbsoluteUnixPath.get("/app")); - Assert.assertEquals( - expectedDependenciesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getDependencyLayerEntries())); - Assert.assertEquals( + assertSourcePathsUnordered( + expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); + assertSourcePathsUnordered( expectedSnapshotDependenciesFiles, - getSourceFilesFromLayerEntries( - javaLayerConfigurations.getSnapshotDependencyLayerEntries())); - Assert.assertEquals( - expectedResourcesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getResourceLayerEntries())); - Assert.assertEquals( - expectedClassesFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getClassLayerEntries())); + javaLayerConfigurations.getSnapshotDependencyLayerEntries()); + assertSourcePathsUnordered( + expectedResourcesFiles, javaLayerConfigurations.getResourceLayerEntries()); + assertSourcePathsUnordered( + expectedClassesFiles, javaLayerConfigurations.getClassLayerEntries()); } @Test @@ -148,9 +146,8 @@ public void test_extraFiles() throws URISyntaxException, IOException { extraFilesDirectory.resolve("c/cat"), extraFilesDirectory.resolve("foo")); - Assert.assertEquals( - expectedExtraFiles, - getSourceFilesFromLayerEntries(javaLayerConfigurations.getExtraFilesLayerEntries())); + assertSourcePathsUnordered( + expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); } @Test @@ -161,36 +158,33 @@ public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOE JavaLayerConfigurations configuration = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); - Assert.assertEquals( - // on windows, these files may be in a different order, so use Set - new HashSet<>( - Arrays.asList( - "/my/app/libs/dependency-1.0.0.jar", - "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar")), - new HashSet<>( - getExtractionPathFromLayerEntries(configuration.getDependencyLayerEntries()))); - Assert.assertEquals( + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - getExtractionPathFromLayerEntries(configuration.getSnapshotDependencyLayerEntries())); - Assert.assertEquals( + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList( "/my/app/resources/directory", "/my/app/resources/directory/somefile", "/my/app/resources/resourceA", "/my/app/resources/resourceB", "/my/app/resources/world"), - getExtractionPathFromLayerEntries(configuration.getResourceLayerEntries())); - Assert.assertEquals( + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList( "/my/app/classes/HelloWorld.class", "/my/app/classes/package", "/my/app/classes/package/some.class", "/my/app/classes/some.class"), - getExtractionPathFromLayerEntries(configuration.getClassLayerEntries())); - Assert.assertEquals( + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - getExtractionPathFromLayerEntries(configuration.getExtraFilesLayerEntries())); + configuration.getExtraFilesLayerEntries()); } private Artifact makeArtifact(Path path) { From 71a695cd30f66ff1e377e792eb148d17873df117 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 27 Sep 2018 15:57:23 -0700 Subject: [PATCH 0249/2020] Archives Proposal: Improved cache mechanism (#1057) --- proposals/{ => archives}/cache_v2.md | 2 ++ 1 file changed, 2 insertions(+) rename proposals/{ => archives}/cache_v2.md (99%) diff --git a/proposals/cache_v2.md b/proposals/archives/cache_v2.md similarity index 99% rename from proposals/cache_v2.md rename to proposals/archives/cache_v2.md index 4bc8f3f439..0ec17b4457 100644 --- a/proposals/cache_v2.md +++ b/proposals/archives/cache_v2.md @@ -1,5 +1,7 @@ # Proposal: Improved cache mechanism +Implemented in: **v0.10.0** + The tracking issue is at [#637](https://github.com/GoogleContainerTools/jib/issues/637). ## TLDR; From a2c80a87949f5c213b75082436299549be29e926 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 28 Sep 2018 07:42:33 -0700 Subject: [PATCH 0250/2020] Renames ncache to cache. (#1056) --- .../builder/steps/BuildAndCacheApplicationLayerStep.java | 6 +++--- .../cloud/tools/jib/builder/steps/BuildImageStep.java | 2 +- .../jib/builder/steps/PullAndCacheBaseImageLayerStep.java | 6 +++--- .../cloud/tools/jib/builder/steps/PushLayersStep.java | 2 +- .../com/google/cloud/tools/jib/{ncache => cache}/Cache.java | 2 +- .../jib/{ncache => cache}/CacheCorruptedException.java | 2 +- .../cloud/tools/jib/{ncache => cache}/CacheEntry.java | 2 +- .../cloud/tools/jib/{ncache => cache}/CacheStorage.java | 2 +- .../tools/jib/{ncache => cache}/DefaultCacheEntry.java | 2 +- .../tools/jib/{ncache => cache}/DefaultCacheStorage.java | 2 +- .../jib/{ncache => cache}/DefaultCacheStorageFiles.java | 2 +- .../jib/{ncache => cache}/DefaultCacheStorageReader.java | 2 +- .../jib/{ncache => cache}/DefaultCacheStorageWriter.java | 2 +- .../jib/{ncache => cache}/LastModifiedTimeMetadata.java | 2 +- .../tools/jib/{ncache => cache}/LayerEntriesSelector.java | 2 +- .../tools/jib/{ncache => cache}/UncompressedCacheWrite.java | 2 +- .../cloud/tools/jib/configuration/BuildConfiguration.java | 2 +- .../steps/BuildAndCacheApplicationLayerStepTest.java | 6 +++--- .../cloud/tools/jib/builder/steps/BuildImageStepTest.java | 2 +- .../google/cloud/tools/jib/{ncache => cache}/CacheTest.java | 2 +- .../tools/jib/{ncache => cache}/DefaultCacheEntryTest.java | 2 +- .../jib/{ncache => cache}/DefaultCacheStorageFilesTest.java | 2 +- .../{ncache => cache}/DefaultCacheStorageReaderTest.java | 2 +- .../{ncache => cache}/DefaultCacheStorageWriterTest.java | 2 +- .../jib/{ncache => cache}/LastModifiedTimeMetadataTest.java | 2 +- .../jib/{ncache => cache}/LayerEntriesSelectorTest.java | 4 ++-- 26 files changed, 33 insertions(+), 33 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/Cache.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/CacheCorruptedException.java (95%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/CacheEntry.java (97%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/CacheStorage.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheEntry.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorage.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageFiles.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageReader.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageWriter.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/LastModifiedTimeMetadata.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/LayerEntriesSelector.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{ncache => cache}/UncompressedCacheWrite.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/CacheTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheEntryTest.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageFilesTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageReaderTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/DefaultCacheStorageWriterTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/LastModifiedTimeMetadataTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{ncache => cache}/LayerEntriesSelectorTest.java (96%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 4e26811127..91d996bb5a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -19,13 +19,13 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; -import com.google.cloud.tools.jib.ncache.Cache; -import com.google.cloud.tools.jib.ncache.CacheCorruptedException; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 3592280450..7fa6a42c67 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -28,7 +29,6 @@ import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 3ac1856109..75c34b476a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -18,12 +18,12 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.ncache.Cache; -import com.google.cloud.tools.jib.ncache.CacheCorruptedException; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 9a4c896c7d..9c25dd4949 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -20,8 +20,8 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 9274f730ba..1778244ed7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java index 4a9b4b2330..e3b250e0d5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheCorruptedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; /** Thrown if the the cache was found to be corrupted. */ public class CacheCorruptedException extends Exception { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java similarity index 97% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java index c30469c780..f139702dda 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java index 7bb0286870..00fa173868 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java index 6853e465cd..dc2f00214e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java index 9a0ecdc3e1..1c76eaf014 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java index af12046bc2..e7dde41c2d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java index 77b9fa3d17..e4f7ceaf72 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java index 3c9c4899f3..ed8bcf3a14 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java index 6204bc11fb..fdc57d965c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadata.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index 1950687c15..55f3c1ae08 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java index 9a777c715e..d8dfc2c2c9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ncache/UncompressedCacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 20cfb6aa3e..0d68fbf032 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.ncache.Cache; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 9adf7b0615..3aa7df0c59 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -19,6 +19,9 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -27,9 +30,6 @@ import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; -import com.google.cloud.tools.jib.ncache.Cache; -import com.google.cloud.tools.jib.ncache.CacheCorruptedException; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index e23a135206..0e6f903c13 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.HistoryEntry; -import com.google.cloud.tools.jib.ncache.CacheEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Futures; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 2b2a1c63f3..1a4b5e0f1c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java index dbd4ea3673..1999801faf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java index 250c50baaa..d3e52c82bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Paths; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java index b3d269dbeb..5ffadae803 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java index e5f367fce9..086e17e717 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java index 7d73dce04f..349efc7d6b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LastModifiedTimeMetadataTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java similarity index 96% rename from jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 68adbc0f0b..989f3bd339 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/ncache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -14,13 +14,13 @@ * the License. */ -package com.google.cloud.tools.jib.ncache; +package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.ncache.LayerEntriesSelector.LayerEntryTemplate; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import java.io.IOException; From 6793bfbba4f16dc5fb961c0f6137861cc049ecf6 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 28 Sep 2018 08:21:01 -0700 Subject: [PATCH 0251/2020] Proposal RFC: Jib Core as a library for Java (#987) --- proposals/jib_core_library.md | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 proposals/jib_core_library.md diff --git a/proposals/jib_core_library.md b/proposals/jib_core_library.md new file mode 100644 index 0000000000..082d64f8e7 --- /dev/null +++ b/proposals/jib_core_library.md @@ -0,0 +1,76 @@ +# Proposal: Jib Core as a library for Java + +The tracking issue is at [#337](https://github.com/GoogleContainerTools/jib/issues/337). + +# Goal + +Design for Jib Core as a Java library for building container images. + +# Proposed API + +`Jib` - the main entrypoint for using Jib Core +- `JibContainerBuilder from(String baseImageReference)` +- `JibContainerBuilder from(ImageReference baseImageReference)` +- `JibContainerBuilder from(RegistryImage baseImage)` + +`JibContainerBuilder` - configures the container to build +- `JibContainerBuilder addLayer(List files, Path pathInContainer)` +- `JibContainerBuilder addLayer(LayerConfiguration)` +- `JibContainerBuilder setLayers(List/LayerConfiguration...)` + +- `JibContainerBuilder setEntrypoint(List/String...)` +- `JibContainerBuilder setProgramArguments(List/String...)` +- `JibContainerBuilder setEnvironment(Map environmentMap)` +- `JibContainerBuilder addEnvironmentVariable(String name, String value)` +- `JibContainerBuilder setExposedPorts(List/Port...)` +- `JibContainerBuilder addExposedPort(Port port)` +- `JibContainerBuilder setLabels(Map labelMap)` +- `JibContainerBuilder addLabel(String key, String value)` +- `JibContainer containerize(Containerizer)` + +Three `TargetImage` types (`RegistryImage`, `DockerDaemonImage`, and `TarImage`) define the 3 different targets Jib can build to. + +`RegistryImage` - builds to a container registry +- `static RegistryImage named(ImageReference/String)` +- `RegistryImage addCredential(String username, String password)` +- `RegistryImage addCredentialRetriever(CredentialRetriever)` + +`DockerDaemonImage` - builds to a Docker daemon +- `static DockerDaemonImage named(ImageReference/String)` +- `DockerDaemonImage setDockerExecutable(Path)` + +`TarImage` - builds to a tarball archive +- `Builder` + - `TarImage saveTo(Path outputFile)` +- `static Builder named(ImageReference/String)` + +`Containerizer` - configures how and where to containerize to +- `static Containerizer to(RegistryImage)` +- `static Containerizer to(DockerDaemonImage)` +- `static Containerizer to(TarImage)` +- `Containerizer setExecutorService(ExecutorService)` +- `Containerizer setCacheConfiguration(CacheConfiguration)` +- `Containerizer setEventHandlers(EventHandlers)` + +## For Java containers + +`JavaContainerBuilder` - builds a `JibContainerBuilder` for Java apps +- `static JavaContainerBuilder builder()` +- `JavaContainerBuilder addDependencies(List dependencyFiles)` +- `JavaContainerBuilder addResources(List resourceFiles)` +- `JavaContainerBuilder addClasses(List classFiles)` +- `JavaContainerBuilder addToClasspath(List otherFiles)` +- `JavaContainerBuilder setJvmFlags(List/String... jvmFlags)` +- `JavaContainerBuilder setMainClass(String mainClass)` +- `JavaContainerBuilder toContainerBuilder()` + +# Simple example + +```java +Jib.from("busybox") + .addLayer(Arrays.asList(Paths.get("helloworld.sh")), "/helloworld.sh") + .setEntrypoint("/helloworld.sh") + .containerize( + Jib.to(RegistryImage.named("coollog/jibtestimage") + .setCredential("coollog", "notmyrealpassword")); +``` From 04e712f50e6cd6f1b8b21655d833e6e8066518f7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 28 Sep 2018 14:21:02 -0400 Subject: [PATCH 0252/2020] Support stringifying ImageReference with digest, add validity checks to ImageReference#of (#1063) --- .../cloud/tools/jib/image/ImageReference.java | 23 ++++++++++++-- .../CredentialRetrieverFactoryTest.java | 8 ++--- .../tools/jib/image/ImageReferenceTest.java | 31 ++++++++++++++++++- .../plugins/common/BuildStepsRunnerTest.java | 2 +- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index a3e71f8fc4..da5824b13a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -154,6 +155,10 @@ public static ImageReference parse(String reference) throws InvalidImageReferenc */ public static ImageReference of( @Nullable String registry, String repository, @Nullable String tag) { + Preconditions.checkArgument(Strings.isNullOrEmpty(registry) || isValidRegistry(registry)); + Preconditions.checkArgument(isValidRepository(repository)); + Preconditions.checkArgument(Strings.isNullOrEmpty(tag) || isValidTag(tag)); + if (Strings.isNullOrEmpty(registry)) { registry = DOCKER_HUB_REGISTRY; } @@ -193,7 +198,7 @@ public static boolean isValidRepository(String repository) { * @return {@code true} if is a valid tag; {@code false} otherwise */ public static boolean isValidTag(String tag) { - return tag.matches(TAG_REGEX); + return tag.matches(TAG_REGEX) || tag.matches(DescriptorDigest.DIGEST_REGEX); } /** @@ -256,6 +261,16 @@ public boolean usesDefaultTag() { return isDefaultTag(tag); } + /** + * Returns {@code true} if the {@link ImageReference} uses a SHA-256 digest as its tag; {@code + * false} if not. + * + * @return {@code true} if tag is a SHA-256 digest; {@code false} if not + */ + public boolean isTagDigest() { + return tag.matches(DescriptorDigest.DIGEST_REGEX); + } + /** * Gets an {@link ImageReference} with the same registry and repository, but a different tag. * @@ -267,7 +282,8 @@ public ImageReference withTag(String newTag) { } /** - * Stringifies the {@link ImageReference}. + * Stringifies the {@link ImageReference}. When the tag is a digest, it is prepended with the at + * {@code @} symbol instead of a colon {@code :}. * * @return the image reference in Docker-readable format (inverse of {@link #parse}) */ @@ -290,7 +306,8 @@ public String toString() { // Use tag if not the default tag. if (!DEFAULT_TAG.equals(tag)) { - referenceString.append(':').append(tag); + // Append with "@tag" instead of ":tag" if tag is a digest + referenceString.append(isTagDigest() ? '@' : ':').append(tag); } return referenceString.toString(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 0317c6cd74..d9abf4da35 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -86,7 +86,7 @@ public void setUp() public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( - ImageReference.of("registry", null, null), + ImageReference.of("registry", "repository", null), mockEventDispatcher, getTestFactory( "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); @@ -105,7 +105,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( - ImageReference.of("something.gcr.io", null, null), + ImageReference.of("something.gcr.io", "repository", null), mockEventDispatcher, getTestFactory( "something.gcr.io", @@ -126,7 +126,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { public void testInferCredentialHelper_warn() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( - ImageReference.of("something.amazonaws.com", null, null), + ImageReference.of("something.amazonaws.com", "repository", null), mockEventDispatcher, getTestFactory( "something.amazonaws.com", @@ -145,7 +145,7 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( - ImageReference.of("registry", null, null), mockEventDispatcher); + ImageReference.of("registry", "repository", null), mockEventDispatcher); Mockito.when(mockDockerConfigCredentialRetriever.retrieve()) .thenReturn(Optional.of(FAKE_CREDENTIALS)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java index 7c760c5f74..826a98497b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java @@ -121,7 +121,7 @@ public void testOf_smoke() { } @Test - public void testToString() { + public void testToString() throws InvalidImageReferenceException { Assert.assertEquals("someimage", ImageReference.of(null, "someimage", null).toString()); Assert.assertEquals("someimage", ImageReference.of("", "someimage", "").toString()); Assert.assertEquals( @@ -132,6 +132,19 @@ public void testToString() { Assert.assertEquals( "anotherregistry/anotherimage:sometag", ImageReference.of("anotherregistry", "anotherimage", "sometag").toString()); + + Assert.assertEquals( + "someimage@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ImageReference.of( + null, + "someimage", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .toString()); + Assert.assertEquals( + "gcr.io/distroless/java@sha256:b430543bea1d8326e767058bdab3a2482ea45f59d7af5c5c61334cd29ede88a1", + ImageReference.parse( + "gcr.io/distroless/java@sha256:b430543bea1d8326e767058bdab3a2482ea45f59d7af5c5c61334cd29ede88a1") + .toString()); } @Test @@ -151,6 +164,22 @@ public void testToStringWithTag() { ImageReference.of("anotherregistry", "anotherimage", "sometag").toStringWithTag()); } + @Test + public void testIsTagDigest() throws InvalidImageReferenceException { + Assert.assertFalse(ImageReference.of(null, "someimage", null).isTagDigest()); + Assert.assertFalse(ImageReference.of(null, "someimage", "latest").isTagDigest()); + Assert.assertTrue( + ImageReference.of( + null, + "someimage", + "sha256:b430543bea1d8326e767058bdab3a2482ea45f59d7af5c5c61334cd29ede88a1") + .isTagDigest()); + Assert.assertTrue( + ImageReference.parse( + "someimage@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .isTagDigest()); + } + private void verifyParse(String registry, String repository, String tagSeparator, String tag) throws InvalidImageReferenceException { // Gets the expected parsed components. diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index f2b79a5a0e..5b1f82ed48 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -54,7 +54,7 @@ public class BuildStepsRunnerTest { false, "baseImageCredHelperConfiguration", registry -> "baseImageAuthConfiguration " + registry, - ImageReference.of("toRegistry", "toRepository", null), + ImageReference.of("toRegistry", "torepository", null), false, "targetImageCredHelperConfiguration", registry -> "targetImageAuthConfiguration " + registry, From 5ecf99905e8da87300d4384abf2daec44e10f704 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 28 Sep 2018 15:01:47 -0400 Subject: [PATCH 0253/2020] Fix docker error output not being captured (#1064) --- .../com/google/cloud/tools/jib/docker/DockerClient.java | 6 +++++- .../com/google/cloud/tools/jib/docker/DockerClientTest.java | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 77c6f06019..7673de6471 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -108,7 +108,11 @@ public String load(Blob imageTarballBlob) throws InterruptedException, IOExcepti String output = CharStreams.toString(stdout); if (dockerProcess.waitFor() != 0) { - throw new IOException("'docker load' command failed with output: " + output); + try (InputStreamReader stderr = + new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { + throw new IOException( + "'docker load' command failed with output: " + CharStreams.toString(stderr)); + } } return output; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 138d13d07d..36c249c126 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -156,13 +156,15 @@ public void testLoad_stdoutFail() throws InterruptedException { Mockito.when(mockProcess.getOutputStream()).thenReturn(ByteStreams.nullOutputStream()); Mockito.when(mockProcess.getInputStream()) .thenReturn(new ByteArrayInputStream("ignored".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { testDockerClient.load(Blobs.from("jib")); Assert.fail("Process should have failed"); } catch (IOException ex) { - Assert.assertEquals("'docker load' command failed with output: ignored", ex.getMessage()); + Assert.assertEquals("'docker load' command failed with output: error", ex.getMessage()); } } From d62e6bf617c17fba16d2c87acadc4f2a88a9bca5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 28 Sep 2018 16:39:03 -0400 Subject: [PATCH 0254/2020] Add jib version to layer history (#1066) --- .../google/cloud/tools/jib/ProjectInfo.java | 3 +++ .../jib/builder/steps/BuildImageStep.java | 3 ++- .../tools/jib/registry/RegistryClient.java | 6 ++---- .../jib/builder/steps/BuildImageStepTest.java | 18 +++++++++--------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java index 72d80c55be..b032e218a5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java @@ -25,5 +25,8 @@ public class ProjectInfo { /** Link to file an issue against the GitHub repository. */ public static final String GITHUB_NEW_ISSUE_URL = GITHUB_URL + "/issues/new"; + /** The project version. */ + public static final String VERSION = ProjectInfo.class.getPackage().getImplementationVersion(); + private ProjectInfo() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 7fa6a42c67..43147e2338 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; @@ -168,7 +169,7 @@ private Image afterCacheEntrySteps() HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") - .setCreatedBy(buildConfiguration.getToolName()) + .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 6ed8f62f6f..b04644e11c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; @@ -115,12 +116,9 @@ private String makeUserAgent() { return ""; } - String version = RegistryClient.class.getPackage().getImplementationVersion(); StringBuilder userAgentBuilder = new StringBuilder(); userAgentBuilder.append("jib"); - if (version != null) { - userAgentBuilder.append(" ").append(version); - } + userAgentBuilder.append(" ").append(ProjectInfo.VERSION); if (userAgentSuffix != null) { userAgentBuilder.append(" ").append(userAgentSuffix); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 0e6f903c13..d437d0e4ed 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -214,20 +214,20 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted HistoryEntry.builder() .setCreationTimestamp(Instant.EPOCH) .setAuthor("Jib") - .setCreatedBy("jib") + .setCreatedBy("jib:null") .build(); // Base layers (1 non-empty propagated, 2 empty propagated, 2 non-empty generated) - Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); - Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); - Assert.assertEquals(image.getHistory().get(2), emptyLayerHistory); - Assert.assertEquals(image.getHistory().get(3), expectedAddedBaseLayerHistory); - Assert.assertEquals(image.getHistory().get(4), expectedAddedBaseLayerHistory); + Assert.assertEquals(nonEmptyLayerHistory, image.getHistory().get(0)); + Assert.assertEquals(emptyLayerHistory, image.getHistory().get(1)); + Assert.assertEquals(emptyLayerHistory, image.getHistory().get(2)); + Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(3)); + Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(4)); // Application layers (3 generated) - Assert.assertEquals(image.getHistory().get(5), expectedApplicationLayerHistory); - Assert.assertEquals(image.getHistory().get(6), expectedApplicationLayerHistory); - Assert.assertEquals(image.getHistory().get(7), expectedApplicationLayerHistory); + Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(5)); + Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(6)); + Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(7)); // Should be exactly 8 total Assert.assertEquals(8, image.getHistory().size()); From 8110c1da0d8acb8951bf4c67d665a4b843ca1207 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 28 Sep 2018 16:59:15 -0400 Subject: [PATCH 0255/2020] Construct correct classes and resources layers for Maven builds (#1062) * Fix possible resources slipping into classes * Handle directories correctly when adding to layers * Use Files.list() instead of File.list() * Add tests --- .../jib/maven/MavenLayerConfigurations.java | 149 ++++++++++-------- .../jib/maven/DockerContextMojoTest.java | 1 - .../maven/MavenLayerConfigurationsTest.java | 147 +++++++++++++++-- 3 files changed, 219 insertions(+), 78 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 366a60fa0e..548cb6579e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -17,17 +17,17 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; @@ -46,84 +46,103 @@ class MavenLayerConfigurations { */ static JavaLayerConfigurations getForProject( MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { - Path classesSourceDirectory = Paths.get(project.getBuild().getSourceDirectory()); - Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); - List dependenciesFiles = new ArrayList<>(); - List snapshotDependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); - List classesFiles = new ArrayList<>(); - List extraFiles = new ArrayList<>(); + AbsoluteUnixPath dependenciesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); + AbsoluteUnixPath resourcesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); + AbsoluteUnixPath classesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + + Builder layerBuilder = JavaLayerConfigurations.builder(); // Gets all the dependencies. for (Artifact artifact : project.getArtifacts()) { + Path artifactPath = artifact.getFile().toPath(); if (artifact.isSnapshot()) { - snapshotDependenciesFiles.add(artifact.getFile().toPath()); + layerBuilder.addSnapshotDependencyFile( + artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); } else { - dependenciesFiles.add(artifact.getFile().toPath()); + layerBuilder.addDependencyFile( + artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); } } - // Gets the classes files in the 'classes' output directory. It finds the files that are classes - // files by matching them against the .java source files. All other files are deemed resources. - try (Stream classFileStream = Files.list(classesOutputDirectory)) { - classFileStream.forEach( - classFile -> { - /* - * Adds classFile to classesFiles if it is a .class file or is a directory that also - * exists in the classes source directory; otherwise, adds file to resourcesFiles. - */ - if (Files.isDirectory(classFile) - && Files.exists( - classesSourceDirectory.resolve(classesOutputDirectory.relativize(classFile)))) { - classesFiles.add(classFile); - return; - } - - if (FileSystems.getDefault().getPathMatcher("glob:**.class").matches(classFile)) { - classesFiles.add(classFile); - return; - } - - resourcesFiles.add(classFile); - }); - } + Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); + + // Gets the classes files in the 'classes' output directory. + Predicate isClassFile = path -> path.toString().endsWith(".class"); + addFilesToLayer( + classesOutputDirectory, isClassFile, classesExtractionPath, layerBuilder::addClassFile); + + // Gets the resources files in the 'classes' output directory. + addFilesToLayer( + classesOutputDirectory, + isClassFile.negate(), + resourcesExtractionPath, + layerBuilder::addResourceFile); // Adds all the extra files. if (Files.exists(extraDirectory)) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { - extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); + AbsoluteUnixPath extractionBase = AbsoluteUnixPath.get("/"); + addFilesToLayer(extraDirectory, path -> true, extractionBase, layerBuilder::addExtraFile); + } + + return layerBuilder.build(); + } + + @FunctionalInterface + @VisibleForTesting + static interface FileToLayerAdder { + + void add(Path sourcePath, AbsoluteUnixPath pathInContainer) throws IOException; + } - } catch (IOException ex) { - throw new IOException("Failed to list directory for extra files: " + extraDirectory, ex); + @VisibleForTesting + static boolean isEmptyDirectory(Path path) throws IOException { + if (Files.isDirectory(path)) { + try (Stream stream = Files.list(path)) { + return !stream.findAny().isPresent(); } } + return false; + } - AbsoluteUnixPath dependenciesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - AbsoluteUnixPath resourcesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - AbsoluteUnixPath classesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + /** + * Adds files to a layer selectively and recursively. {@code sourceRoot} must be a directory. + * Non-empty directories will be ignored, while empty directories will always be added. Note that + * empty directories will always be added regardless of {@code pathFilter}. + * + *

The contents of {@code sourceRoot} will be placed into {@code basePathInContainer}. For + * example, if {@code sourceRoot} is {@code /usr/home}, {@code /usr/home/passwd} exists locally, + * and {@code basePathInContainer} is {@code /etc}, then the image will have {@code /etc/passwd}. + * + * @param sourceRoot root directory whose contents will be added + * @param pathFilter only the files satisfying the filter will be added, unless the files are + * empty directories + * @param basePathInContainer directory in the layer into which the source contents are added + * @param addFileToLayer function that should add the file to the layer; the function gets the + * path of the source file (may be a directory) and the final destination path in the layer + * @throws IOException error while listing directories + * @throws NotDirectoryException if {@code sourceRoot} is not a directory + */ + @VisibleForTesting + static void addFilesToLayer( + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer, + FileToLayerAdder addFileToLayer) + throws IOException { - Builder layerBuilder = JavaLayerConfigurations.builder(); - for (Path file : dependenciesFiles) { - layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName())); - } - for (Path file : snapshotDependenciesFiles) { - layerBuilder.addSnapshotDependencyFile( - file, dependenciesExtractionPath.resolve(file.getFileName())); - } - for (Path file : resourcesFiles) { - layerBuilder.addResourceFile(file, resourcesExtractionPath.resolve(file.getFileName())); - } - for (Path file : classesFiles) { - layerBuilder.addClassFile(file, classesExtractionPath.resolve(file.getFileName())); - } - for (Path file : extraFiles) { - layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName())); - } - return layerBuilder.build(); + new DirectoryWalker(sourceRoot) + .walk( + path -> { + // Always add empty directories. However, ignore non-empty directories because + // otherwise JavaLayerConfigurations will add files recursively. + if (isEmptyDirectory(path) || (!Files.isDirectory(path) && pathFilter.test(path))) { + addFileToLayer.add(path, basePathInContainer.resolve(sourceRoot.relativize(path))); + } + }); } private MavenLayerConfigurations() {} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 3437c1eb3a..3543d287ad 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -49,7 +49,6 @@ public void setUp() throws IOException { MavenProject project = Mockito.mock(MavenProject.class); Build build = Mockito.mock(Build.class); Mockito.when(project.getBuild()).thenReturn(build); - Mockito.when(build.getSourceDirectory()).thenReturn(projectRoot.newFolder("src").toString()); Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); mojo = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 5c4e0fe8b4..7baf056a0f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -24,20 +24,24 @@ import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; 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.Mockito; @@ -65,18 +69,19 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } - @Rule public TestRepository testRepository = new TestRepository(); + @Rule public final TestRepository testRepository = new TestRepository(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private MavenProject mockMavenProject; @Mock private Build mockBuild; + @Mock private MavenLayerConfigurations.FileToLayerAdder fileToLayerAdder; + @Before public void setUp() throws URISyntaxException { - Path sourcePath = Paths.get(Resources.getResource("application/source").toURI()); Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); - Mockito.when(mockBuild.getSourceDirectory()).thenReturn(sourcePath.toString()); Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); Set artifacts = @@ -103,7 +108,6 @@ public void test_correctFiles() throws URISyntaxException, IOException { Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); ImmutableList expectedResourcesFiles = ImmutableList.of( - applicationDirectory.resolve("output/directory"), applicationDirectory.resolve("output/directory/somefile"), applicationDirectory.resolve("output/resourceA"), applicationDirectory.resolve("output/resourceB"), @@ -111,7 +115,6 @@ public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedClassesFiles = ImmutableList.of( applicationDirectory.resolve("output/HelloWorld.class"), - applicationDirectory.resolve("output/package"), applicationDirectory.resolve("output/package/some.class"), applicationDirectory.resolve("output/some.class")); @@ -139,10 +142,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { ImmutableList expectedExtraFiles = ImmutableList.of( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), extraFilesDirectory.resolve("c/cat"), extraFilesDirectory.resolve("foo")); @@ -169,7 +169,6 @@ public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOE configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( - "/my/app/resources/directory", "/my/app/resources/directory/somefile", "/my/app/resources/resourceA", "/my/app/resources/resourceB", @@ -178,13 +177,137 @@ public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOE assertExtractionPathsUnordered( Arrays.asList( "/my/app/classes/HelloWorld.class", - "/my/app/classes/package", "/my/app/classes/package/some.class", "/my/app/classes/some.class"), configuration.getClassLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); + Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + } + + @Test + public void testIsEmptyDirectory() throws IOException { + Assert.assertTrue( + MavenLayerConfigurations.isEmptyDirectory(temporaryFolder.getRoot().toPath())); + } + + @Test + public void testIsEmptyDirectory_file() throws IOException { + Assert.assertFalse( + MavenLayerConfigurations.isEmptyDirectory(temporaryFolder.newFile().toPath())); + } + + @Test + public void testIsEmptyDirectory_nonExistent() throws IOException { + Assert.assertFalse(MavenLayerConfigurations.isEmptyDirectory(Paths.get("non/existent"))); + } + + @Test + public void testAddFilesToLayer_file() throws IOException { + temporaryFolder.newFile("file"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder).add(sourceRoot.resolve("file"), basePath.resolve("file")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); + } + + @Test + public void testAddFilesToLayer_emptyDirectory() throws IOException { + temporaryFolder.newFolder("leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + + MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder).add(sourceRoot.resolve("leaf"), basePath.resolve("leaf")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); + } + + @Test + public void testAddFilesToLayer_nonEmptyDirectoryIgnored() throws IOException { + temporaryFolder.newFolder("non-empty", "leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); + } + + @Test + public void testAddFilesToLayer_filter() throws IOException { + temporaryFolder.newFile("non-target"); + temporaryFolder.newFolder("sub"); + temporaryFolder.newFile("sub/target"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + + Predicate nameIsTarget = path -> "target".equals(path.getFileName().toString()); + MavenLayerConfigurations.addFilesToLayer(sourceRoot, nameIsTarget, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); + } + + @Test + public void testAddFilesToLayer_emptyDirectoryForced() throws IOException { + temporaryFolder.newFolder("sub", "leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> false, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); + } + + @Test + public void testAddFilesToLayer_fileAsSource() throws IOException { + Path sourceFile = temporaryFolder.newFile("foo").toPath(); + + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + try { + MavenLayerConfigurations.addFilesToLayer( + sourceFile, path -> true, basePath, fileToLayerAdder); + } catch (NotDirectoryException ex) { + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("foo is not a directory")); + } + } + + @Test + public void testAddFilesToLayer_complex() throws IOException { + temporaryFolder.newFile("A.class"); + temporaryFolder.newFile("B.java"); + temporaryFolder.newFolder("example", "dir"); + temporaryFolder.newFile("example/dir/C.class"); + temporaryFolder.newFile("example/C.class"); + temporaryFolder.newFolder("test", "resources", "leaf"); + temporaryFolder.newFile("test/resources/D.java"); + temporaryFolder.newFile("test/D.class"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/base"); + + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + + MavenLayerConfigurations.addFilesToLayer(sourceRoot, isClassFile, basePath, fileToLayerAdder); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("A.class"), basePath.resolve("A.class")); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("example/dir/C.class"), basePath.resolve("example/dir/C.class")); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("example/C.class"), basePath.resolve("example/C.class")); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("test/resources/leaf"), basePath.resolve("test/resources/leaf")); + Mockito.verify(fileToLayerAdder) + .add(sourceRoot.resolve("test/D.class"), basePath.resolve("test/D.class")); + Mockito.verifyNoMoreInteractions(fileToLayerAdder); } private Artifact makeArtifact(Path path) { From 304d9c3ab2802ef821627f1c28402bede59c27f8 Mon Sep 17 00:00:00 2001 From: Cyrille HEIT Date: Mon, 1 Oct 2018 16:38:11 +0200 Subject: [PATCH 0256/2020] Proposal War support with Gradle (#923) * Add preliminary support of "War" project (gradle plugin only for now) * Add unit test on empty folder (in WEB-INF/classes) --- .../frontend/JavaDockerContextGenerator.java | 10 -- .../jib/frontend/JavaLayerConfigurations.java | 30 ---- .../JavaDockerContextGeneratorTest.java | 42 ++++- .../frontend/JavaLayerConfigurationsTest.java | 63 ++++++- jib-core/src/test/resources/sampleDockerfile | 1 - .../src/test/resources/webappSampleDockerfile | 10 ++ .../tools/jib/gradle/ContainerParameters.java | 3 +- .../tools/jib/gradle/DockerContextTask.java | 14 +- .../tools/jib/gradle/ExplodedWarTask.java | 48 ++++++ .../jib/gradle/GradleLayerConfigurations.java | 135 ++++++++++++++- .../jib/gradle/GradleProjectProperties.java | 7 +- .../cloud/tools/jib/gradle/JibExtension.java | 3 - .../cloud/tools/jib/gradle/JibPlugin.java | 59 ++++++- .../gradle/PluginConfigurationProcessor.java | 50 ++++-- .../jib/gradle/DockerContextTaskTest.java | 30 +++- .../gradle/GradleLayerConfigurationsTest.java | 163 +++++++++++++++++- .../gradle/GradleProjectPropertiesTest.java | 2 +- .../tools/jib/gradle/JibExtensionTest.java | 4 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 92 ++++++++++ .../PluginConfigurationProcessorTest.java | 16 ++ .../jib-exploded-war/META-INF/context.xml | 0 .../webapp/jib-exploded-war/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 1 + .../WEB-INF/classes/package/Other.class | 1 + .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../webapp/jib-exploded-war/WEB-INF/web.xml | 0 .../jib/plugins/common/ProjectProperties.java | 6 + 29 files changed, 683 insertions(+), 107 deletions(-) create mode 100644 jib-core/src/test/resources/webappSampleDockerfile create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/META-INF/context.xml create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/Test.jsp create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/web.xml diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 1d6ab203ac..ffe91b8684 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -57,9 +57,6 @@ public class JavaDockerContextGenerator { private static final String SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY = "snapshot-libs"; private static final String RESOURCES_LAYER_DIRECTORY = "resources"; private static final String CLASSES_LAYER_DIRECTORY = "classes"; - // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, - // snapshot dependencies, resources, and classes layers). Should copy files in the right - private static final String EXPLODED_WAR_LAYER_DIRECTORY = "exploded-war"; private static final String EXTRA_FILES_LAYER_DIRECTORY = "root"; private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -164,13 +161,6 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration copyDirectivesBuilder, javaLayerConfigurations.getClassLayerEntries(), CLASSES_LAYER_DIRECTORY); - // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, - // snapshot dependencies, resources, and classes layers). Should copy files in the right - // directories. (For example, "resources" will go into the webapp root.) - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getExplodedWarEntries(), - EXPLODED_WAR_LAYER_DIRECTORY); addIfNotEmpty( copyDirectivesBuilder, javaLayerConfigurations.getExtraFilesLayerEntries(), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 14d0e35e90..a2288bcd5c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -38,9 +38,6 @@ enum LayerType { SNAPSHOT_DEPENDENCIES("snapshot dependencies"), RESOURCES("resources"), CLASSES("classes"), - // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, - // snapshot dependencies, resources, and classes layers). Should copy files in the right - EXPLODED_WAR("exploded war"), EXTRA_FILES("extra files"); private final String name; @@ -164,27 +161,6 @@ public Builder addExtraFile(Path sourceFile, AbsoluteUnixPath pathInContainer) return this; } - /** - * Adds a file to the exploded WAR layer. If the source file is a directory, the directory and - * its contents will be added recursively. See {@link - * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will - * be placed in the image. - * - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @return this - * @throws IOException if an exception occurred when recursively listing the directory - */ - // TODO: remove this and put files in WAR into the relevant layers (i.e., dependencies, snapshot - // dependencies, resources, and classes layers). - public Builder addExplodedWarFile(Path sourceFile, AbsoluteUnixPath pathInContainer) - throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.EXPLODED_WAR)) - .addEntryRecursive(sourceFile, pathInContainer); - return this; - } - public JavaLayerConfigurations build() { ImmutableMap.Builder layerConfigurationsMap = ImmutableMap.builderWithExpectedSize(layerBuilders.size()); @@ -237,12 +213,6 @@ public ImmutableList getExtraFilesLayerEntries() { return getLayerEntries(LayerType.EXTRA_FILES); } - // TODO: remove this once we put files in WAR into the relevant layers (i.e., dependencies, - // snapshot dependencies, resources, and classes layers). Should copy files in the right - public ImmutableList getExplodedWarEntries() { - return getLayerEntries(LayerType.EXPLODED_WAR); - } - private ImmutableList getLayerEntries(LayerType layerType) { return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 750664416f..b1eac8bd55 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -84,7 +84,6 @@ public void testGenerate() throws IOException, URISyntaxException { Paths.get(Resources.getResource("application/snapshot-dependencies").toURI()); Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); - Path testExplodedWarFiles = Paths.get(Resources.getResource("exploded-war").toURI()); Path testExtraFiles = Paths.get(Resources.getResource("layer").toURI()); Path targetDirectory = temporaryFolder.newFolder().toPath(); @@ -103,8 +102,6 @@ public void testGenerate() throws IOException, URISyntaxException { .thenReturn(filesToLayerEntries(testResources, "/more/resources")); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) .thenReturn(filesToLayerEntries(testClasses, "/my/classes")); - Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) - .thenReturn(filesToLayerEntries(testExplodedWarFiles, "/exploded/war")); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) .thenReturn(filesToLayerEntries(testExtraFiles, "/")); @@ -117,7 +114,6 @@ public void testGenerate() throws IOException, URISyntaxException { assertSameFiles(targetDirectory.resolve("snapshot-libs/snapshots"), testSnapshotDependencies); assertSameFiles(targetDirectory.resolve("resources/more/resources"), testResources); assertSameFiles(targetDirectory.resolve("classes/my/classes"), testClasses); - assertSameFiles(targetDirectory.resolve("exploded-war/exploded/war"), testExplodedWarFiles); assertSameFiles(targetDirectory.resolve("root"), testExtraFiles); } @@ -149,9 +145,6 @@ public void testMakeDockerfile() throws IOException { Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) .thenReturn( ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/classes/")))); - Mockito.when(mockJavaLayerConfigurations.getExplodedWarEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/exploded/war")))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/")))); @@ -172,4 +165,39 @@ public void testMakeDockerfile() throws IOException { Resources.readLines(Resources.getResource("sampleDockerfile"), StandardCharsets.UTF_8); Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); } + + @Test + public void testMakeDockerfileWithWebapp() throws IOException { + String expectedBaseImage = "tomcat:8.5-jre8-alpine"; + AbsoluteUnixPath exepectedAppRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); + + Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) + .thenReturn( + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib")))); + Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) + .thenReturn( + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib")))); + Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), exepectedAppRoot))); + Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) + .thenReturn( + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/classes")))); + Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) + .thenReturn( + ImmutableList.of(new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/")))); + String dockerfile = + new JavaDockerContextGenerator(mockJavaLayerConfigurations) + .setBaseImage(expectedBaseImage) + .setEntrypoint(ImmutableList.of("catalina.sh", "run")) + .makeDockerfile(); + + // Need to split/rejoin the string here to avoid cross-platform troubles + List sampleDockerfile = + Resources.readLines( + Resources.getResource("webappSampleDockerfile"), StandardCharsets.UTF_8); + Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 47315c2242..6da695105b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -3,6 +3,7 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -27,7 +28,6 @@ private static JavaLayerConfigurations createFakeConfigurations() throws IOExcep Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots")) .addResourceFile(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here")) .addClassFile(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here")) - .addExplodedWarFile(Paths.get("exploded war"), AbsoluteUnixPath.get("/for/war")) .addExtraFile(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras")) .build(); } @@ -96,7 +96,6 @@ public void testAddFile() throws IOException { Collections.singletonList(Paths.get("snapshot dependency")), Collections.singletonList(Paths.get("resource")), Collections.singletonList(Paths.get("class")), - Collections.singletonList(Paths.get("exploded war")), Collections.singletonList(Paths.get("extra file"))); List> actualFiles = javaLayerConfigurations @@ -118,7 +117,6 @@ public void testAddFile_directories() throws IOException, URISyntaxException { .addSnapshotDependencyFile(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target")) .addResourceFile(sourceDirectory, AbsoluteUnixPath.get("/resources")) .addClassFile(sourceDirectory, AbsoluteUnixPath.get("/classes/here")) - .addExplodedWarFile(sourceDirectory, AbsoluteUnixPath.get("/exploded-war")) .addExtraFile(sourceDirectory, AbsoluteUnixPath.get("/extra/files")) .build(); @@ -127,7 +125,6 @@ public void testAddFile_directories() throws IOException, URISyntaxException { configurations::getSnapshotDependencyLayerEntries, sourceDirectory, "/snapshots/target"); verifyRecursiveAdd(configurations::getResourceLayerEntries, sourceDirectory, "/resources"); verifyRecursiveAdd(configurations::getClassLayerEntries, sourceDirectory, "/classes/here"); - verifyRecursiveAdd(configurations::getExplodedWarEntries, sourceDirectory, "/exploded-war"); verifyRecursiveAdd(configurations::getExtraFilesLayerEntries, sourceDirectory, "/extra/files"); } @@ -142,7 +139,6 @@ public void testAddFile_regularFiles() throws IOException, URISyntaxException { .addSnapshotDependencyFile(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file")) .addResourceFile(sourceFile, AbsoluteUnixPath.get("/resources-file")) .addClassFile(sourceFile, AbsoluteUnixPath.get("/classes/file")) - .addExplodedWarFile(sourceFile, AbsoluteUnixPath.get("/exploded-war/file")) .addExtraFile(sourceFile, AbsoluteUnixPath.get("/some/file")) .build(); @@ -158,11 +154,62 @@ public void testAddFile_regularFiles() throws IOException, URISyntaxException { Assert.assertEquals( Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/classes/file"))), configurations.getClassLayerEntries()); - Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/exploded-war/file"))), - configurations.getExplodedWarEntries()); Assert.assertEquals( Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/some/file"))), configurations.getExtraFilesLayerEntries()); } + + @Test + public void testWebApp() throws IOException { + AbsoluteUnixPath expectedAppRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); + JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); + + JavaLayerConfigurations configurations = + layerBuilder + .addResourceFile(Paths.get("test.jsp"), expectedAppRoot.resolve("test.jsp")) + .addResourceFile(Paths.get("META-INF/"), expectedAppRoot.resolve("META-INF/")) + .addResourceFile( + Paths.get("context.xml"), expectedAppRoot.resolve("WEB-INF/context.xml")) + .addResourceFile(Paths.get("sub_dir/"), expectedAppRoot.resolve("WEB-INF/sub_dir/")) + .addDependencyFile( + Paths.get("myLib.jar"), expectedAppRoot.resolve("WEB-INF/lib/myLib.jar")) + .addSnapshotDependencyFile( + Paths.get("my-SNAPSHOT.jar"), + expectedAppRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) + .addClassFile( + Paths.get("test.class"), expectedAppRoot.resolve("WEB-INF/classes/test.class")) + .addExtraFile(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file")) + .build(); + + ImmutableList expectedDependenciesLayer = + ImmutableList.of( + new LayerEntry( + Paths.get("myLib.jar"), expectedAppRoot.resolve("WEB-INF/lib/myLib.jar"))); + ImmutableList expectedSnapshotDependenciesLayer = + ImmutableList.of( + new LayerEntry( + Paths.get("my-SNAPSHOT.jar"), + expectedAppRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"))); + ImmutableList expectedResourcesLayer = + ImmutableList.of( + new LayerEntry(Paths.get("test.jsp"), expectedAppRoot.resolve("test.jsp")), + new LayerEntry(Paths.get("META-INF"), expectedAppRoot.resolve("META-INF")), + new LayerEntry( + Paths.get("context.xml"), expectedAppRoot.resolve("WEB-INF/context.xml")), + new LayerEntry(Paths.get("sub_dir"), expectedAppRoot.resolve("WEB-INF/sub_dir"))); + ImmutableList expectedClassesLayer = + ImmutableList.of( + new LayerEntry( + Paths.get("test.class"), expectedAppRoot.resolve("WEB-INF/classes/test.class"))); + ImmutableList expectedExtraLayer = + ImmutableList.of( + new LayerEntry(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file"))); + + Assert.assertEquals(expectedDependenciesLayer, configurations.getDependencyLayerEntries()); + Assert.assertEquals( + expectedSnapshotDependenciesLayer, configurations.getSnapshotDependencyLayerEntries()); + Assert.assertEquals(expectedResourcesLayer, configurations.getResourceLayerEntries()); + Assert.assertEquals(expectedClassesLayer, configurations.getClassLayerEntries()); + Assert.assertEquals(expectedExtraLayer, configurations.getExtraFilesLayerEntries()); + } } diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile index 3f52564c13..c2eae1c103 100644 --- a/jib-core/src/test/resources/sampleDockerfile +++ b/jib-core/src/test/resources/sampleDockerfile @@ -4,7 +4,6 @@ COPY libs / COPY snapshot-libs / COPY resources / COPY classes / -COPY exploded-war / COPY root / EXPOSE 1000/tcp diff --git a/jib-core/src/test/resources/webappSampleDockerfile b/jib-core/src/test/resources/webappSampleDockerfile new file mode 100644 index 0000000000..f8771a9a5b --- /dev/null +++ b/jib-core/src/test/resources/webappSampleDockerfile @@ -0,0 +1,10 @@ +FROM tomcat:8.5-jre8-alpine + +COPY libs / +COPY snapshot-libs / +COPY resources / +COPY classes / +COPY root / + +ENTRYPOINT ["catalina.sh","run"] +CMD [] \ No newline at end of file diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 8bdf32339f..57b7e602c4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.common.base.Preconditions; @@ -42,7 +41,7 @@ public class ContainerParameters { private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); private Map labels = Collections.emptyMap(); - private String appRoot = JavaLayerConfigurations.DEFAULT_APP_ROOT; + private String appRoot = ""; @Input @Optional diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 33f5e5df09..ce8b860bb3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; @@ -113,16 +112,9 @@ public void generateDockerContext() { getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); String targetDir = getTargetDir(); - List entrypoint = jibExtension.getContainer().getEntrypoint(); - if (entrypoint.isEmpty()) { - String mainClass = gradleProjectProperties.getMainClass(jibExtension); - entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - appRoot, jibExtension.getContainer().getJvmFlags(), mainClass); - } else if (jibExtension.getContainer().getMainClass() != null - || !jibExtension.getContainer().getJvmFlags().isEmpty()) { - getLogger().warn("mainClass and jvmFlags are ignored when entrypoint is specified"); - } + List entrypoint = + PluginConfigurationProcessor.computeEntrypoint( + getLogger(), jibExtension, gradleProjectProperties); try { // Validate port input, but don't save the output because we don't want the ranges expanded diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java new file mode 100644 index 0000000000..ce96f2375a --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import java.io.File; +import java.nio.file.Path; +import javax.annotation.Nullable; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.Sync; + +/** This Gradle Task explodes a War file into a directory */ +public class ExplodedWarTask extends Sync { + @Nullable private File explodedWarDirectory; + + public void setWarFile(File warFile) { + from(getProject().zipTree(warFile)); + } + + /** + * Sets the output directory of Sync Task + * + * @param explodedWarDirectory the directory where to extract the war file + */ + public void setExplodedWarDirectory(Path explodedWarDirectory) { + this.explodedWarDirectory = explodedWarDirectory.toFile(); + into(explodedWarDirectory); + } + + @OutputDirectory + @Nullable + public File getExplodedWarDirectory() { + return explodedWarDirectory; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 944279593d..b711860f94 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -17,11 +17,13 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -40,8 +42,17 @@ class GradleLayerConfigurations { /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; + /** The filename suffix for a maven/gradle snapshot dependency */ + private static final String SNAPSHOT = "SNAPSHOT"; + /** The standard directory name containing libs, classes, web.xml, etc... in a War Project */ + private static final String WEB_INF = "WEB-INF"; + /** The standard directory name containing libs and snapshot-libs in a War Project */ + private static final String WEB_INF_LIB = WEB_INF + "/lib/"; + /** The standard directory name containing classes and some resources in a War Project */ + private static final String WEB_INF_CLASSES = WEB_INF + "/classes/"; + /** - * Resolves the source files configuration for a Gradle {@link Project}. + * Resolves the {@link JavaLayerConfigurations} for a Gradle {@link Project}. * * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution @@ -53,6 +64,27 @@ class GradleLayerConfigurations { static JavaLayerConfigurations getForProject( Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + if (GradleProjectProperties.getWarTask(project) != null) { + logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); + return getForWarProject(project, logger, extraDirectory, appRoot); + } else { + return getForNonWarProject(project, logger, extraDirectory, appRoot); + } + } + + /** + * Resolves the {@link JavaLayerConfigurations} for a non-war Gradle {@link Project}. + * + * @param project the Gradle {@link Project} + * @param logger the logger for providing feedback about the resolution + * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed + * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} + * @throws IOException if an I/O exception occurred during resolution + */ + private static JavaLayerConfigurations getForNonWarProject( + Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + throws IOException { JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); @@ -91,7 +123,7 @@ static JavaLayerConfigurations getForProject( if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { continue; } - if (dependencyFile.getName().contains("SNAPSHOT")) { + if (dependencyFile.getName().contains(SNAPSHOT)) { snapshotDependenciesFiles.add(dependencyFile.toPath()); } else { dependenciesFiles.add(dependencyFile.toPath()); @@ -132,5 +164,104 @@ static JavaLayerConfigurations getForProject( return layerBuilder.build(); } + /** + * Resolves the {@link JavaLayerConfigurations} for a War Gradle {@link Project}. + * + * @param project the Gradle {@link Project} + * @param logger the build logger for providing feedback about the resolution + * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed + * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} + * @throws IOException if an I/O exception occurred during resolution + */ + private static JavaLayerConfigurations getForWarProject( + Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + throws IOException { + List dependenciesFiles = new ArrayList<>(); + List snapshotDependenciesFiles = new ArrayList<>(); + List resourcesFiles = new ArrayList<>(); + List resourcesWebInfFiles = new ArrayList<>(); + List extraFiles = new ArrayList<>(); + + Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); + + Path libOutputDirectory = explodedWarPath.resolve(WEB_INF_LIB); + try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { + dependencyFileStream.forEach( + path -> { + if (path.toString().contains(SNAPSHOT)) { + snapshotDependenciesFiles.add(path); + } else { + dependenciesFiles.add(path); + } + }); + } + // All files except classes and libs in resources + try (Stream fileStream = Files.list(explodedWarPath)) { + fileStream.filter(path -> !path.endsWith(WEB_INF)).forEach(resourcesFiles::add); + } + // Some files in WEB-INF need to be in resources directory (e.g. web.xml, ...) + Path webinfOutputDirectory = explodedWarPath.resolve(WEB_INF); + try (Stream fileStream = Files.list(webinfOutputDirectory)) { + fileStream + .filter(path -> !path.endsWith("classes") && !path.endsWith("lib")) + .forEach(resourcesWebInfFiles::add); + } + + // Adds all the extra files. + if (Files.exists(extraDirectory)) { + try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { + extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); + } + } + + Builder layerBuilder = JavaLayerConfigurations.builder(); + AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(WEB_INF_LIB); + AbsoluteUnixPath classesExtractionPath = appRoot.resolve(WEB_INF_CLASSES); + AbsoluteUnixPath webInfExtractionPath = appRoot.resolve(WEB_INF); + + // For "WEB-INF/classes", *.class in class layer, other files and empty directories in resource + // layer + Path srcWebInfClasses = explodedWarPath.resolve(WEB_INF_CLASSES); + new DirectoryWalker(srcWebInfClasses) + .walk( + path -> { + if (FileSystems.getDefault().getPathMatcher("glob:**.class").matches(path)) { + layerBuilder.addClassFile( + path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + + } else if (Files.isDirectory(path)) { + try (Stream dirStream = Files.list(path)) { + if (!dirStream.findAny().isPresent()) { + // The directory is empty + layerBuilder.addResourceFile( + path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + } + } + } else { + layerBuilder.addResourceFile( + path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + } + }); + + for (Path file : dependenciesFiles) { + layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName())); + } + for (Path file : snapshotDependenciesFiles) { + layerBuilder.addSnapshotDependencyFile( + file, dependenciesExtractionPath.resolve(file.getFileName())); + } + for (Path file : resourcesFiles) { + layerBuilder.addResourceFile(file, appRoot.resolve(file.getFileName())); + } + for (Path file : resourcesWebInfFiles) { + layerBuilder.addResourceFile(file, webInfExtractionPath.resolve(file.getFileName())); + } + for (Path file : extraFiles) { + layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName())); + } + return layerBuilder.build(); + } + private GradleLayerConfigurations() {} } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index c5a402e025..13c3d9e06b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -91,6 +91,10 @@ static War getWarTask(Project project) { return (War) warPluginConvention.getProject().getTasks().findByName("war"); } + static Path getExplodedWarDirectory(Project project) { + return project.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME); + } + private final Project project; private final EventDispatcher eventDispatcher; private final JavaLayerConfigurations javaLayerConfigurations; @@ -142,8 +146,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - // TODO: replace with "getWarTask(project) != null" once ready - return false; + return getWarTask(project) != null; } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 1d4ba4adb0..5a80ae2fc1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -63,7 +63,6 @@ public class JibExtension { // Defines default configuration values. - private static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; private static final boolean DEFAULT_USE_ONLY_PROJECT_CACHE = false; private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; @@ -98,8 +97,6 @@ public JibExtension(Project project) { allowInsecureRegistries = objectFactory.property(Boolean.class); extraDirectory = objectFactory.property(Path.class); - // Sets defaults. - from.setImage(DEFAULT_FROM_IMAGE); jvmFlags.set(Collections.emptyList()); args.set(Collections.emptyList()); useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 65f7acbb56..757feea222 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -28,6 +28,7 @@ import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.bundling.War; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { @@ -40,6 +41,21 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; + @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; + + @VisibleForTesting static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; + @VisibleForTesting static final String DEFAULT_WEBAPP_FROM_IMAGE = "gcr.io/distroless/java/jetty"; + /** + * The default app root in the image. For example, if this is set to {@code "/app"}, dependency + * JARs will be in {@code "/app/libs"}. + */ + @VisibleForTesting static final String DEFAULT_APP_ROOT = "/app"; + + /** + * The default webapp root in the image. For example, if this is set to {@code + * "/jetty/webapps/ROOT"}, dependency JARs will be in {@code "/jetty/webapps/ROOT/WEB-INF/lib"}. + */ + @VisibleForTesting static final String DEFAULT_WEBAPP_ROOT = "/jetty/webapps/ROOT"; /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of @@ -108,14 +124,43 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { + // TODO move this to a seperate place try { - // Has all tasks depend on the 'classes' task. - Task classesTask = projectAfterEvaluation.getTasks().getByPath("classes"); - buildImageTask.dependsOn(classesTask); - dockerContextTask.dependsOn(classesTask); - buildDockerTask.dependsOn(classesTask); - buildTarTask.dependsOn(classesTask); - filesTask.dependsOn(classesTask); + War warTask = GradleProjectProperties.getWarTask(project); + final Task dependsOnTask; + if (warTask != null) { + if (jibExtension.getFrom().getImage() == null) { + jibExtension.getFrom().setImage(DEFAULT_WEBAPP_FROM_IMAGE); + } + if (jibExtension.getContainer().getAppRoot().isEmpty()) { + jibExtension.getContainer().setAppRoot(DEFAULT_WEBAPP_ROOT); + } + // Has all tasks depend on the 'exploded war' task. + ExplodedWarTask explodedWarTask = + (ExplodedWarTask) + project + .getTasks() + .create(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class) + .dependsOn(warTask); + explodedWarTask.setWarFile(warTask.getArchivePath()); + explodedWarTask.setExplodedWarDirectory( + GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); + dependsOnTask = explodedWarTask; + } else { + if (jibExtension.getFrom().getImage() == null) { + jibExtension.getFrom().setImage(DEFAULT_FROM_IMAGE); + } + if (jibExtension.getContainer().getAppRoot().isEmpty()) { + jibExtension.getContainer().setAppRoot(DEFAULT_APP_ROOT); + } + // Has all tasks depend on the 'classes' task. + dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); + } + buildImageTask.dependsOn(dependsOnTask); + dockerContextTask.dependsOn(dependsOnTask); + buildDockerTask.dependsOn(dependsOnTask); + buildTarTask.dependsOn(dependsOnTask); + filesTask.dependsOn(dependsOnTask); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 4bce2ab586..7f3fcfb516 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -123,18 +123,7 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - List entrypoint = jibExtension.getContainer().getEntrypoint(); - if (entrypoint.isEmpty()) { - String mainClass = projectProperties.getMainClass(jibExtension); - entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - getAppRootChecked(jibExtension), - jibExtension.getContainer().getJvmFlags(), - mainClass); - } else if (jibExtension.getContainer().getMainClass() != null - || !jibExtension.getContainer().getJvmFlags().isEmpty()) { - logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); - } + List entrypoint = computeEntrypoint(logger, jibExtension, projectProperties); ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() .setEntrypoint(entrypoint) @@ -171,6 +160,43 @@ static PluginConfigurationProcessor processCommonConfiguration( optionalFromCredential.isPresent()); } + /** + * Compute the container entrypoint, in this order : + * + *

    + *
  1. the user specified one, if set + *
  2. for a war project, the jetty default one + *
  3. for a jar project, by resolving the main class + *
+ * + * @param logger the logger used to display messages. + * @param jibExtension the {@link JibExtension} providing the configuration data + * @param projectProperties used for providing additional information + * @return the entrypoint + */ + static List computeEntrypoint( + Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) { + List entrypoint = jibExtension.getContainer().getEntrypoint(); + if (!entrypoint.isEmpty()) { + if (jibExtension.getContainer().getMainClass() != null + || !jibExtension.getContainer().getJvmFlags().isEmpty()) { + logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); + } + } else { + if (projectProperties.isWarProject()) { + entrypoint = JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); + } else { + String mainClass = projectProperties.getMainClass(jibExtension); + entrypoint = + JavaEntrypointConstructor.makeDefaultEntrypoint( + AbsoluteUnixPath.get(jibExtension.getContainer().getAppRoot()), + jibExtension.getContainer().getJvmFlags(), + mainClass); + } + } + return entrypoint; + } + private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 0577e9021c..27ca6ea136 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.gradle; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -39,22 +40,25 @@ public class DockerContextTaskTest { @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); - @Mock private ContainerParameters ContainerParameters; + @Mock private ContainerParameters containerParameters; private DockerContextTask task; + private Project project; @Before public void setUp() throws IOException { + projectRoot.newFolder("build", "jib-exploded-war", "WEB-INF", "lib"); + projectRoot.newFolder("build", "jib-exploded-war", "WEB-INF", "classes"); projectRoot.newFolder("build", "jib-docker-context"); JibExtension jibExtension = Mockito.mock(JibExtension.class); - Mockito.when(jibExtension.getContainer()).thenReturn(ContainerParameters); + Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); Mockito.when(jibExtension.getExtraDirectoryPath()).thenReturn(projectRoot.getRoot().toPath()); Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); Mockito.when(jibExtension.getBaseImage()).thenReturn("base image"); - Mockito.when(ContainerParameters.getAppRoot()).thenReturn("/app"); + Mockito.when(containerParameters.getAppRoot()).thenReturn("/app"); - Project project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); + project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); project.getPluginManager().apply("java"); task = project.getTasks().create("jibExportDockerContext", DockerContextTask.class); @@ -72,7 +76,7 @@ public void testEntrypoint() throws IOException { @Test public void testEntrypoint_nonDefaultAppRoot() throws IOException { - Mockito.when(ContainerParameters.getAppRoot()).thenReturn("/"); + Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); task.generateDockerContext(); Assert.assertEquals( @@ -80,9 +84,19 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException { getEntrypoint()); } + @Test + public void testEntrypoint_defaultWebappRoot() throws IOException, URISyntaxException { + Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); + project.getPluginManager().apply("war"); + + task.generateDockerContext(); + + Assert.assertEquals("ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]", getEntrypoint()); + } + @Test public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { - Mockito.when(ContainerParameters.getAppRoot()).thenReturn("relative/path"); + Mockito.when(containerParameters.getAppRoot()).thenReturn("relative/path"); try { task.generateDockerContext(); @@ -95,7 +109,7 @@ public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { @Test public void testGenerateDockerContext_errorOnWindowsAppRoot() { - Mockito.when(ContainerParameters.getAppRoot()).thenReturn("\\windows\\path"); + Mockito.when(containerParameters.getAppRoot()).thenReturn("\\windows\\path"); try { task.generateDockerContext(); @@ -108,7 +122,7 @@ public void testGenerateDockerContext_errorOnWindowsAppRoot() { @Test public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { - Mockito.when(ContainerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); + Mockito.when(containerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); try { task.generateDockerContext(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 15b2b74078..5564f3a91e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -39,9 +40,12 @@ import org.gradle.api.logging.Logger; import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.SourceSetOutput; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.bundling.War; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -93,15 +97,19 @@ private static void assertExtractionPathsUnordered( } @Mock private Project mockProject; + @Mock private Project mockWebappProject; @Mock private Convention mockConvention; @Mock private JavaPluginConvention mockJavaPluginConvention; + @Mock private WarPluginConvention mockWarPluginConvention; @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; @Mock private Logger mockLogger; + @Mock private TaskContainer taskContainer; + @Mock private War war; @Before - public void setUp() throws URISyntaxException { + public void setUp() throws URISyntaxException, IOException { Set classesFiles = ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI())); FileCollection classesFileCollection = new TestFileCollection(classesFiles); @@ -128,6 +136,11 @@ public void setUp() throws URISyntaxException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); + // We can't commit an empty directory in Git, so it's created if it does not exist + Path emptyDirectory = + Paths.get(Resources.getResource("webapp").toURI()) + .resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); + Files.createDirectories(emptyDirectory); } @Test @@ -238,4 +251,152 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxE Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); } + + @Test + public void testGetForProject_defaultAppRoot() throws IOException, URISyntaxException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + GradleLayerConfigurations.getForProject( + mockProject, + mockLogger, + extraFilesDirectory, + AbsoluteUnixPath.get(JibPlugin.DEFAULT_APP_ROOT)); + + assertExtractionPathsUnordered( + Arrays.asList( + "/app/libs/dependency-1.0.0.jar", "/app/libs/libraryA.jar", "/app/libs/libraryB.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/app/resources", + "/app/resources/resourceA", + "/app/resources/resourceB", + "/app/resources/world"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/app/classes/HelloWorld.class", "/app/classes/some.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); + } + + @Test + public void testWebApp() throws URISyntaxException, IOException { + Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); + Mockito.when(mockWebappProject.getBuildDir()).thenReturn(webappDirectory.toFile()); + Mockito.when(mockWebappProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) + .thenReturn(mockWarPluginConvention); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebappProject); + Mockito.when(mockWebappProject.getTasks()).thenReturn(taskContainer); + Mockito.when(taskContainer.findByName("war")).thenReturn(war); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + GradleLayerConfigurations.getForProject( + mockWebappProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); + ImmutableList expectedDependenciesFiles = + ImmutableList.of( + webappDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); + ImmutableList expectedSnapshotDependenciesFiles = + ImmutableList.of( + webappDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); + ImmutableList expectedResourcesFiles = + ImmutableList.of( + webappDirectory.resolve("jib-exploded-war/META-INF"), + webappDirectory.resolve("jib-exploded-war/META-INF/context.xml"), + webappDirectory.resolve("jib-exploded-war/Test.jsp"), + webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), + webappDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")); + ImmutableList expectedClassesFiles = + ImmutableList.of( + webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), + webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); + + assertSourcePathsUnordered( + expectedDependenciesFiles, configuration.getDependencyLayerEntries()); + assertSourcePathsUnordered( + expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); + assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); + assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); + + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/META-INF", + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/web.xml"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/package/Other.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); + } + + @Test + public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOException { + Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); + Mockito.when(mockWebappProject.getBuildDir()).thenReturn(webappDirectory.toFile()); + Mockito.when(mockWebappProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) + .thenReturn(mockWarPluginConvention); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebappProject); + Mockito.when(mockWebappProject.getTasks()).thenReturn(taskContainer); + Mockito.when(taskContainer.findByName("war")).thenReturn(war); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + GradleLayerConfigurations.getForProject( + mockWebappProject, + mockLogger, + extraFilesDirectory, + AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEBAPP_ROOT)); + + assertExtractionPathsUnordered( + Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/jetty/webapps/ROOT/META-INF", + "/jetty/webapps/ROOT/META-INF/context.xml", + "/jetty/webapps/ROOT/Test.jsp", + "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", + "/jetty/webapps/ROOT/WEB-INF/classes/package/test.properties", + "/jetty/webapps/ROOT/WEB-INF/web.xml"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/jetty/webapps/ROOT/WEB-INF/classes/HelloWorld.class", + "/jetty/webapps/ROOT/WEB-INF/classes/package/Other.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 70d2b08d3b..9f81d8df2a 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -93,7 +93,7 @@ public void testGetMainClassFromJar_multiple() { @Test public void testIsWarProject() { - Assert.assertFalse(gradleProjectProperties.isWarProject()); + Assert.assertTrue(gradleProjectProperties.isWarProject()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index bfccc9ff51..2cffdb78c0 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -44,7 +44,7 @@ public void setUp() { @Test public void testFrom() { - Assert.assertEquals("gcr.io/distroless/java", testJibExtension.getFrom().getImage()); + Assert.assertEquals(null, testJibExtension.getFrom().getImage()); Assert.assertNull(testJibExtension.getFrom().getCredHelper()); testJibExtension.from( @@ -87,7 +87,7 @@ public void testContainer() { Assert.assertEquals(V22ManifestTemplate.class, testJibExtension.getContainer().getFormat()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); - Assert.assertEquals("/app", testJibExtension.getContainer().getAppRoot()); + Assert.assertEquals("", testJibExtension.getContainer().getAppRoot()); testJibExtension.container( container -> { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index d3d85b7ae5..d04022a6b7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -27,6 +28,7 @@ import java.util.stream.Collectors; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.UnknownTaskException; import org.gradle.api.internal.project.ProjectInternal; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; @@ -148,4 +150,94 @@ public void testProjectDependencyAssembleTasksAreRun() { .collect(Collectors.toSet())); }); } + + @Test + public void testWebappProject() { + Project rootProject = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + rootProject.getPluginManager().apply("java"); + rootProject.getPluginManager().apply("war"); + rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + ((ProjectInternal) rootProject).evaluate(); + Assert.assertNotNull(rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME)); + ExplodedWarTask explodedWarTask = + (ExplodedWarTask) rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + Assert.assertEquals( + rootProject + .getBuildDir() + .toPath() + .resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME) + .toFile(), + explodedWarTask.getExplodedWarDirectory()); + + Assert.assertEquals( + explodedWarTask, + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME) + .getDependsOn() + .iterator() + .next()); + Assert.assertEquals( + explodedWarTask, + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME) + .getDependsOn() + .iterator() + .next()); + Assert.assertEquals( + explodedWarTask, + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_TAR_TASK_NAME) + .getDependsOn() + .iterator() + .next()); + Assert.assertEquals( + explodedWarTask, + rootProject + .getTasks() + .getByPath(JibPlugin.DOCKER_CONTEXT_TASK_NAME) + .getDependsOn() + .iterator() + .next()); + Assert.assertEquals( + JibPlugin.DEFAULT_WEBAPP_FROM_IMAGE, + ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) + .getJib() + .getBaseImage()); + Assert.assertEquals( + JibPlugin.DEFAULT_WEBAPP_ROOT, + ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) + .getJib() + .getContainer() + .getAppRoot()); + } + + @Test + public void testNonWebappProject() { + Project rootProject = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + rootProject.getPluginManager().apply("java"); + rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + ((ProjectInternal) rootProject).evaluate(); + Assert.assertEquals( + JibPlugin.DEFAULT_FROM_IMAGE, + ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) + .getJib() + .getBaseImage()); + Assert.assertEquals( + JibPlugin.DEFAULT_APP_ROOT, + ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) + .getJib() + .getContainer() + .getAppRoot()); + try { + rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + Assert.fail(); + } catch (UnknownTaskException ex) { + Assert.assertNotNull(ex.getMessage()); + } + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index a38d7fc99f..bfd3e30a58 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collections; import org.gradle.api.GradleException; @@ -128,10 +129,25 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageRefer mockLogger, mockJibExtension, mockProjectProperties); ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + Assert.assertEquals("java", configuration.getEntrypoint().get(0)); + Assert.assertEquals("-cp", configuration.getEntrypoint().get(1)); Assert.assertEquals( "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2)); } + @Test + public void testWebappEntrypoint_default() throws InvalidImageReferenceException { + Mockito.doReturn(true).when(mockProjectProperties).isWarProject(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals( + ImmutableList.of("java", "-jar", "/jetty/start.jar"), configuration.getEntrypoint()); + } + @Test public void testGetAppRootChecked() { Mockito.doReturn("/some/root").when(mockContainerParameters).getAppRoot(); diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/META-INF/context.xml b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/META-INF/context.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/Test.jsp b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/Test.jsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class @@ -0,0 +1 @@ + diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class @@ -0,0 +1 @@ + diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/web.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 6220dc0b88..6b792060a9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -27,6 +27,12 @@ public interface ProjectProperties { /** Directory name for the cache. The directory will be relative to the build output directory. */ String CACHE_DIRECTORY_NAME = "jib-cache"; + /** + * Directory name for the exploded war. The directory will be relative to the build output + * directory. + */ + String EXPLODED_WAR_DIRECTORY_NAME = "jib-exploded-war"; + EventDispatcher getEventDispatcher(); String getPluginName(); From f1c1bb4b081969d164d5522933a06042a53d7211 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 1 Oct 2018 13:17:52 -0400 Subject: [PATCH 0257/2020] Minor cleanups after merging WAR Gradle support contribution (#1072) --- .../JavaDockerContextGeneratorTest.java | 4 +- .../frontend/JavaLayerConfigurationsTest.java | 41 +++++------- ...ampleDockerfile => webAppSampleDockerfile} | 0 .../tools/jib/gradle/ExplodedWarTask.java | 9 +-- .../jib/gradle/GradleLayerConfigurations.java | 67 +++++++++---------- .../cloud/tools/jib/gradle/JibExtension.java | 1 + .../cloud/tools/jib/gradle/JibPlugin.java | 30 ++++----- .../gradle/PluginConfigurationProcessor.java | 40 +++++------ .../jib/gradle/DockerContextTaskTest.java | 3 +- .../gradle/GradleLayerConfigurationsTest.java | 40 ++++++----- .../tools/jib/gradle/JibExtensionTest.java | 2 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 46 ++++++------- .../PluginConfigurationProcessorTest.java | 4 +- .../jib/plugins/common/ProjectProperties.java | 2 +- 14 files changed, 137 insertions(+), 152 deletions(-) rename jib-core/src/test/resources/{webappSampleDockerfile => webAppSampleDockerfile} (100%) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index b1eac8bd55..47d0d0b296 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -167,7 +167,7 @@ public void testMakeDockerfile() throws IOException { } @Test - public void testMakeDockerfileWithWebapp() throws IOException { + public void testMakeDockerfileWithWebApp() throws IOException { String expectedBaseImage = "tomcat:8.5-jre8-alpine"; AbsoluteUnixPath exepectedAppRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); @@ -197,7 +197,7 @@ public void testMakeDockerfileWithWebapp() throws IOException { // Need to split/rejoin the string here to avoid cross-platform troubles List sampleDockerfile = Resources.readLines( - Resources.getResource("webappSampleDockerfile"), StandardCharsets.UTF_8); + Resources.getResource("webAppSampleDockerfile"), StandardCharsets.UTF_8); Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 6da695105b..b5b5b4d27e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -161,46 +161,37 @@ public void testAddFile_regularFiles() throws IOException, URISyntaxException { @Test public void testWebApp() throws IOException { - AbsoluteUnixPath expectedAppRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); - JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); JavaLayerConfigurations configurations = - layerBuilder - .addResourceFile(Paths.get("test.jsp"), expectedAppRoot.resolve("test.jsp")) - .addResourceFile(Paths.get("META-INF/"), expectedAppRoot.resolve("META-INF/")) - .addResourceFile( - Paths.get("context.xml"), expectedAppRoot.resolve("WEB-INF/context.xml")) - .addResourceFile(Paths.get("sub_dir/"), expectedAppRoot.resolve("WEB-INF/sub_dir/")) - .addDependencyFile( - Paths.get("myLib.jar"), expectedAppRoot.resolve("WEB-INF/lib/myLib.jar")) + JavaLayerConfigurations.builder() + .addResourceFile(Paths.get("test.jsp"), appRoot.resolve("test.jsp")) + .addResourceFile(Paths.get("META-INF/"), appRoot.resolve("META-INF/")) + .addResourceFile(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml")) + .addResourceFile(Paths.get("sub_dir/"), appRoot.resolve("WEB-INF/sub_dir/")) + .addDependencyFile(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar")) .addSnapshotDependencyFile( - Paths.get("my-SNAPSHOT.jar"), - expectedAppRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) - .addClassFile( - Paths.get("test.class"), expectedAppRoot.resolve("WEB-INF/classes/test.class")) + Paths.get("my-SNAPSHOT.jar"), appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) + .addClassFile(Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class")) .addExtraFile(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file")) .build(); ImmutableList expectedDependenciesLayer = ImmutableList.of( - new LayerEntry( - Paths.get("myLib.jar"), expectedAppRoot.resolve("WEB-INF/lib/myLib.jar"))); + new LayerEntry(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar"))); ImmutableList expectedSnapshotDependenciesLayer = ImmutableList.of( new LayerEntry( - Paths.get("my-SNAPSHOT.jar"), - expectedAppRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"))); + Paths.get("my-SNAPSHOT.jar"), appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"))); ImmutableList expectedResourcesLayer = ImmutableList.of( - new LayerEntry(Paths.get("test.jsp"), expectedAppRoot.resolve("test.jsp")), - new LayerEntry(Paths.get("META-INF"), expectedAppRoot.resolve("META-INF")), - new LayerEntry( - Paths.get("context.xml"), expectedAppRoot.resolve("WEB-INF/context.xml")), - new LayerEntry(Paths.get("sub_dir"), expectedAppRoot.resolve("WEB-INF/sub_dir"))); + new LayerEntry(Paths.get("test.jsp"), appRoot.resolve("test.jsp")), + new LayerEntry(Paths.get("META-INF"), appRoot.resolve("META-INF")), + new LayerEntry(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml")), + new LayerEntry(Paths.get("sub_dir"), appRoot.resolve("WEB-INF/sub_dir"))); ImmutableList expectedClassesLayer = ImmutableList.of( - new LayerEntry( - Paths.get("test.class"), expectedAppRoot.resolve("WEB-INF/classes/test.class"))); + new LayerEntry(Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class"))); ImmutableList expectedExtraLayer = ImmutableList.of( new LayerEntry(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file"))); diff --git a/jib-core/src/test/resources/webappSampleDockerfile b/jib-core/src/test/resources/webAppSampleDockerfile similarity index 100% rename from jib-core/src/test/resources/webappSampleDockerfile rename to jib-core/src/test/resources/webAppSampleDockerfile diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java index ce96f2375a..38fd416fd6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java @@ -22,18 +22,19 @@ import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.Sync; -/** This Gradle Task explodes a War file into a directory */ +/** Gradle task that explodes a WAR file into a directory. */ public class ExplodedWarTask extends Sync { + @Nullable private File explodedWarDirectory; - public void setWarFile(File warFile) { + public void setWarFile(Path warFile) { from(getProject().zipTree(warFile)); } /** - * Sets the output directory of Sync Task + * Sets the exploded WAR output directory of this {@link Sync} task. * - * @param explodedWarDirectory the directory where to extract the war file + * @param explodedWarDirectory the directory where to extract the WAR file */ public void setExplodedWarDirectory(Path explodedWarDirectory) { this.explodedWarDirectory = explodedWarDirectory.toFile(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index b711860f94..faf8356c26 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import java.io.File; import java.io.IOException; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -42,23 +41,17 @@ class GradleLayerConfigurations { /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; - /** The filename suffix for a maven/gradle snapshot dependency */ + /** The filename suffix for snapshot dependency JARs. */ private static final String SNAPSHOT = "SNAPSHOT"; - /** The standard directory name containing libs, classes, web.xml, etc... in a War Project */ - private static final String WEB_INF = "WEB-INF"; - /** The standard directory name containing libs and snapshot-libs in a War Project */ - private static final String WEB_INF_LIB = WEB_INF + "/lib/"; - /** The standard directory name containing classes and some resources in a War Project */ - private static final String WEB_INF_CLASSES = WEB_INF + "/classes/"; /** * Resolves the {@link JavaLayerConfigurations} for a Gradle {@link Project}. * * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution - * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectory path to the source directory for the extra files layer * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} + * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ static JavaLayerConfigurations getForProject( @@ -77,9 +70,9 @@ static JavaLayerConfigurations getForProject( * * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution - * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectory path to the source directory for the extra files layer * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} + * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ private static JavaLayerConfigurations getForNonWarProject( @@ -169,9 +162,9 @@ private static JavaLayerConfigurations getForNonWarProject( * * @param project the Gradle {@link Project} * @param logger the build logger for providing feedback about the resolution - * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectory path to the source directory for the extra files layer * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} + * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ private static JavaLayerConfigurations getForWarProject( @@ -185,7 +178,7 @@ private static JavaLayerConfigurations getForWarProject( Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); - Path libOutputDirectory = explodedWarPath.resolve(WEB_INF_LIB); + Path libOutputDirectory = explodedWarPath.resolve("WEB-INF/lib"); try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { dependencyFileStream.forEach( path -> { @@ -196,13 +189,15 @@ private static JavaLayerConfigurations getForWarProject( } }); } - // All files except classes and libs in resources + + // First, all files except WEB-INF go into the resources layer. try (Stream fileStream = Files.list(explodedWarPath)) { - fileStream.filter(path -> !path.endsWith(WEB_INF)).forEach(resourcesFiles::add); + fileStream.filter(path -> !path.endsWith("WEB-INF")).forEach(resourcesFiles::add); } - // Some files in WEB-INF need to be in resources directory (e.g. web.xml, ...) - Path webinfOutputDirectory = explodedWarPath.resolve(WEB_INF); - try (Stream fileStream = Files.list(webinfOutputDirectory)) { + // Some files in WEB-INF/ (e.g. web.xml, ...) need to go into the resources layer. However, + // don't add or look into WEB-INF/classes and WEB-INF/lib. + Path webInfOutputDirectory = explodedWarPath.resolve("WEB-INF"); + try (Stream fileStream = Files.list(webInfOutputDirectory)) { fileStream .filter(path -> !path.endsWith("classes") && !path.endsWith("lib")) .forEach(resourcesWebInfFiles::add); @@ -216,31 +211,31 @@ private static JavaLayerConfigurations getForWarProject( } Builder layerBuilder = JavaLayerConfigurations.builder(); - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(WEB_INF_LIB); - AbsoluteUnixPath classesExtractionPath = appRoot.resolve(WEB_INF_CLASSES); - AbsoluteUnixPath webInfExtractionPath = appRoot.resolve(WEB_INF); + AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib"); + AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes"); + AbsoluteUnixPath webInfExtractionPath = appRoot.resolve("WEB-INF"); - // For "WEB-INF/classes", *.class in class layer, other files and empty directories in resource - // layer - Path srcWebInfClasses = explodedWarPath.resolve(WEB_INF_CLASSES); - new DirectoryWalker(srcWebInfClasses) + // For "WEB-INF/classes", *.class go into the class layer. All other files and empty directories + // go into the resource layer. + Path webInfClasses = explodedWarPath.resolve("WEB-INF/classes"); + new DirectoryWalker(webInfClasses) .walk( path -> { - if (FileSystems.getDefault().getPathMatcher("glob:**.class").matches(path)) { - layerBuilder.addClassFile( - path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + AbsoluteUnixPath pathInContainer = + classesExtractionPath.resolve(webInfClasses.relativize(path)); + + if (path.getFileName().toString().endsWith(".class")) { + layerBuilder.addClassFile(path, pathInContainer); } else if (Files.isDirectory(path)) { - try (Stream dirStream = Files.list(path)) { - if (!dirStream.findAny().isPresent()) { + try (Stream fileStream = Files.list(path)) { + if (!fileStream.findAny().isPresent()) { // The directory is empty - layerBuilder.addResourceFile( - path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + layerBuilder.addResourceFile(path, pathInContainer); } } } else { - layerBuilder.addResourceFile( - path, classesExtractionPath.resolve(srcWebInfClasses.relativize(path))); + layerBuilder.addResourceFile(path, pathInContainer); } }); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 5a80ae2fc1..759caaf923 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -97,6 +97,7 @@ public JibExtension(Project project) { allowInsecureRegistries = objectFactory.property(Boolean.class); extraDirectory = objectFactory.property(Path.class); + // Sets defaults. jvmFlags.set(Collections.emptyList()); args.set(Collections.emptyList()); useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 757feea222..11a56db9a1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.stream.Collectors; @@ -44,18 +45,13 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; @VisibleForTesting static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; - @VisibleForTesting static final String DEFAULT_WEBAPP_FROM_IMAGE = "gcr.io/distroless/java/jetty"; - /** - * The default app root in the image. For example, if this is set to {@code "/app"}, dependency - * JARs will be in {@code "/app/libs"}. - */ - @VisibleForTesting static final String DEFAULT_APP_ROOT = "/app"; + @VisibleForTesting static final String DEFAULT_WAR_FROM_IMAGE = "gcr.io/distroless/java/jetty"; /** - * The default webapp root in the image. For example, if this is set to {@code - * "/jetty/webapps/ROOT"}, dependency JARs will be in {@code "/jetty/webapps/ROOT/WEB-INF/lib"}. + * The default app root in the image for WAR. For example, if this is set to {@code + * /jetty/webapps/ROOT}, dependency JARs will be in {@code /jetty/webapps/ROOT/WEB-INF/lib}. */ - @VisibleForTesting static final String DEFAULT_WEBAPP_ROOT = "/jetty/webapps/ROOT"; + @VisibleForTesting static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of @@ -124,36 +120,36 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { - // TODO move this to a seperate place + // TODO move this to a separate place try { War warTask = GradleProjectProperties.getWarTask(project); - final Task dependsOnTask; + Task dependsOnTask; if (warTask != null) { if (jibExtension.getFrom().getImage() == null) { - jibExtension.getFrom().setImage(DEFAULT_WEBAPP_FROM_IMAGE); + jibExtension.getFrom().setImage(DEFAULT_WAR_FROM_IMAGE); } if (jibExtension.getContainer().getAppRoot().isEmpty()) { - jibExtension.getContainer().setAppRoot(DEFAULT_WEBAPP_ROOT); + jibExtension.getContainer().setAppRoot(DEFAULT_WEB_APP_ROOT); } - // Has all tasks depend on the 'exploded war' task. ExplodedWarTask explodedWarTask = (ExplodedWarTask) project .getTasks() .create(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class) .dependsOn(warTask); - explodedWarTask.setWarFile(warTask.getArchivePath()); + explodedWarTask.setWarFile(warTask.getArchivePath().toPath()); explodedWarTask.setExplodedWarDirectory( GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); + // Have all tasks depend on the 'jibExplodedWar' task. dependsOnTask = explodedWarTask; } else { if (jibExtension.getFrom().getImage() == null) { jibExtension.getFrom().setImage(DEFAULT_FROM_IMAGE); } if (jibExtension.getContainer().getAppRoot().isEmpty()) { - jibExtension.getContainer().setAppRoot(DEFAULT_APP_ROOT); + jibExtension.getContainer().setAppRoot(JavaLayerConfigurations.DEFAULT_APP_ROOT); } - // Has all tasks depend on the 'classes' task. + // Have all tasks depend on the 'classes' task. dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); } buildImageTask.dependsOn(dependsOnTask); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 7f3fcfb516..badd48e53d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -49,7 +49,7 @@ class PluginConfigurationProcessor { * Gets the value of the {@code container.appRoot} parameter. Throws {@link GradleException} if it * is not an absolute path in Unix-style. * - * @param jibExtension the Jib plugin extension + * @param jibExtension the {@link JibExtension} providing the configuration data * @return the app root value * @throws GradleException if the app root is not an absolute path in Unix-style */ @@ -85,7 +85,7 @@ static void disableHttpLogging() { * setting up the base image reference/authorization, container configuration, cache * configuration, and layer configuration. * - * @param logger the logger used to display messages. + * @param logger the logger used to display messages * @param jibExtension the {@link JibExtension} providing the configuration data * @param projectProperties used for providing additional information * @return a new {@link PluginConfigurationProcessor} containing pre-configured builders @@ -161,40 +161,36 @@ static PluginConfigurationProcessor processCommonConfiguration( } /** - * Compute the container entrypoint, in this order : + * Compute the container entrypoint, in this order: * *
    *
  1. the user specified one, if set - *
  2. for a war project, the jetty default one - *
  3. for a jar project, by resolving the main class + *
  4. for a WAR project, the Jetty default one + *
  5. for a non-WAR project, by resolving the main class *
* - * @param logger the logger used to display messages. + * @param logger the logger used to display messages * @param jibExtension the {@link JibExtension} providing the configuration data * @param projectProperties used for providing additional information * @return the entrypoint */ static List computeEntrypoint( Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) { - List entrypoint = jibExtension.getContainer().getEntrypoint(); - if (!entrypoint.isEmpty()) { - if (jibExtension.getContainer().getMainClass() != null - || !jibExtension.getContainer().getJvmFlags().isEmpty()) { + ContainerParameters parameters = jibExtension.getContainer(); + if (!parameters.getEntrypoint().isEmpty()) { + if (parameters.getMainClass() != null || !parameters.getJvmFlags().isEmpty()) { logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } - } else { - if (projectProperties.isWarProject()) { - entrypoint = JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); - } else { - String mainClass = projectProperties.getMainClass(jibExtension); - entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get(jibExtension.getContainer().getAppRoot()), - jibExtension.getContainer().getJvmFlags(), - mainClass); - } + return parameters.getEntrypoint(); } - return entrypoint; + + if (projectProperties.isWarProject()) { + return JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); + } + + String mainClass = projectProperties.getMainClass(jibExtension); + return JavaEntrypointConstructor.makeDefaultEntrypoint( + AbsoluteUnixPath.get(parameters.getAppRoot()), parameters.getJvmFlags(), mainClass); } private final BuildConfiguration.Builder buildConfigurationBuilder; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 27ca6ea136..729914305f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import java.io.IOException; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -85,7 +84,7 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException { } @Test - public void testEntrypoint_defaultWebappRoot() throws IOException, URISyntaxException { + public void testEntrypoint_defaultWebAppRoot() throws IOException { Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); project.getPluginManager().apply("war"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 5564f3a91e..2739543f0e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -97,7 +97,7 @@ private static void assertExtractionPathsUnordered( } @Mock private Project mockProject; - @Mock private Project mockWebappProject; + @Mock private Project mockWebAppProject; @Mock private Convention mockConvention; @Mock private JavaPluginConvention mockJavaPluginConvention; @Mock private WarPluginConvention mockWarPluginConvention; @@ -136,7 +136,7 @@ public void setUp() throws URISyntaxException, IOException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - // We can't commit an empty directory in Git, so it's created if it does not exist + // We can't commit an empty directory in Git, so create (if not exist). Path emptyDirectory = Paths.get(Resources.getResource("webapp").toURI()) .resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); @@ -261,7 +261,7 @@ public void testGetForProject_defaultAppRoot() throws IOException, URISyntaxExce mockProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_APP_ROOT)); + AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_APP_ROOT)); assertExtractionPathsUnordered( Arrays.asList( @@ -288,19 +288,19 @@ public void testGetForProject_defaultAppRoot() throws IOException, URISyntaxExce @Test public void testWebApp() throws URISyntaxException, IOException { Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); - Mockito.when(mockWebappProject.getBuildDir()).thenReturn(webappDirectory.toFile()); - Mockito.when(mockWebappProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webappDirectory.toFile()); + Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebappProject); - Mockito.when(mockWebappProject.getTasks()).thenReturn(taskContainer); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); + Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); Mockito.when(taskContainer.findByName("war")).thenReturn(war); Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( - mockWebappProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); + mockWebAppProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); ImmutableList expectedDependenciesFiles = ImmutableList.of( webappDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); @@ -319,6 +319,14 @@ public void testWebApp() throws URISyntaxException, IOException { ImmutableList.of( webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); + ImmutableList expectedExtraFiles = + ImmutableList.of( + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")); assertSourcePathsUnordered( expectedDependenciesFiles, configuration.getDependencyLayerEntries()); @@ -326,9 +334,7 @@ public void testWebApp() throws URISyntaxException, IOException { expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); + assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); assertExtractionPathsUnordered( Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), @@ -358,22 +364,22 @@ public void testWebApp() throws URISyntaxException, IOException { @Test public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOException { Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); - Mockito.when(mockWebappProject.getBuildDir()).thenReturn(webappDirectory.toFile()); - Mockito.when(mockWebappProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webappDirectory.toFile()); + Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebappProject); - Mockito.when(mockWebappProject.getTasks()).thenReturn(taskContainer); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); + Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); Mockito.when(taskContainer.findByName("war")).thenReturn(war); Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( - mockWebappProject, + mockWebAppProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEBAPP_ROOT)); + AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); assertExtractionPathsUnordered( Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 2cffdb78c0..4f7431c73d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -44,7 +44,7 @@ public void setUp() { @Test public void testFrom() { - Assert.assertEquals(null, testJibExtension.getFrom().getImage()); + Assert.assertNull(testJibExtension.getFrom().getImage()); Assert.assertNull(testJibExtension.getFrom().getCredHelper()); testJibExtension.from( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index d04022a6b7..07cbd04a1d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -30,6 +31,7 @@ import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.api.tasks.TaskContainer; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; @@ -61,18 +63,19 @@ public void testCheckGradleVersion_pass() { @Test public void testCheckGradleVersion_fail() throws IOException { + // Copy build file to temp dir + Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); + InputStream buildFileContent = + getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); + Files.copy(buildFileContent, buildFile); + + GradleRunner gradleRunner = + GradleRunner.create() + .withProjectDir(testProjectRoot.getRoot()) + .withPluginClasspath() + .withGradleVersion("4.3"); try { - // Copy build file to temp dir - Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); - InputStream buildFileContent = - getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); - Files.copy(buildFileContent, buildFile); - - GradleRunner.create() - .withProjectDir(testProjectRoot.getRoot()) - .withPluginClasspath() - .withGradleVersion("4.3") - .build(); + gradleRunner.build(); Assert.fail(); } catch (UnexpectedBuildFailure ex) { Assert.assertTrue( @@ -152,7 +155,7 @@ public void testProjectDependencyAssembleTasksAreRun() { } @Test - public void testWebappProject() { + public void testWebAppProject() { Project rootProject = ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); rootProject.getPluginManager().apply("java"); @@ -163,12 +166,8 @@ public void testWebappProject() { ExplodedWarTask explodedWarTask = (ExplodedWarTask) rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); Assert.assertEquals( - rootProject - .getBuildDir() - .toPath() - .resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME) - .toFile(), - explodedWarTask.getExplodedWarDirectory()); + rootProject.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME), + explodedWarTask.getExplodedWarDirectory().toPath()); Assert.assertEquals( explodedWarTask, @@ -203,12 +202,12 @@ public void testWebappProject() { .iterator() .next()); Assert.assertEquals( - JibPlugin.DEFAULT_WEBAPP_FROM_IMAGE, + JibPlugin.DEFAULT_WAR_FROM_IMAGE, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() .getBaseImage()); Assert.assertEquals( - JibPlugin.DEFAULT_WEBAPP_ROOT, + JibPlugin.DEFAULT_WEB_APP_ROOT, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() .getContainer() @@ -216,7 +215,7 @@ public void testWebappProject() { } @Test - public void testNonWebappProject() { + public void testNonWebAppProject() { Project rootProject = ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); rootProject.getPluginManager().apply("java"); @@ -228,13 +227,14 @@ public void testNonWebappProject() { .getJib() .getBaseImage()); Assert.assertEquals( - JibPlugin.DEFAULT_APP_ROOT, + JavaLayerConfigurations.DEFAULT_APP_ROOT, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() .getContainer() .getAppRoot()); + TaskContainer tasks = rootProject.getTasks(); try { - rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + tasks.getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); Assert.fail(); } catch (UnknownTaskException ex) { Assert.assertNotNull(ex.getMessage()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index bfd3e30a58..cfafedd806 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -44,7 +44,7 @@ public class PluginConfigurationProcessorTest { @Mock private GradleProjectProperties mockProjectProperties; @Before - public void setUp() throws Exception { + public void setUp() { Mockito.doReturn("gcr.io/distroless/java").when(mockJibExtension).getBaseImage(); Mockito.doReturn(mockBaseImageParameters).when(mockJibExtension).getFrom(); Mockito.doReturn(new AuthParameters("mock")).when(mockBaseImageParameters).getAuth(); @@ -136,7 +136,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageRefer } @Test - public void testWebappEntrypoint_default() throws InvalidImageReferenceException { + public void testWebAppEntrypoint_default() throws InvalidImageReferenceException { Mockito.doReturn(true).when(mockProjectProperties).isWarProject(); PluginConfigurationProcessor processor = diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 6b792060a9..1e2f47173b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -28,7 +28,7 @@ public interface ProjectProperties { String CACHE_DIRECTORY_NAME = "jib-cache"; /** - * Directory name for the exploded war. The directory will be relative to the build output + * Directory name for the exploded WAR. The directory will be relative to the build output * directory. */ String EXPLODED_WAR_DIRECTORY_NAME = "jib-exploded-war"; From c3bb5520061b22685a0ccb6ded14fb638e1bea6d Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 1 Oct 2018 12:17:36 -0700 Subject: [PATCH 0258/2020] Removes CacheConfiguration in favor of setting directories directly on BuildConfiguration. (#1075) --- .../builder/BuildStepsIntegrationTest.java | 24 ++-- .../cloud/tools/jib/api/Containerizer.java | 62 +++++++++-- .../tools/jib/api/JibContainerBuilder.java | 6 +- .../jib/configuration/BuildConfiguration.java | 78 ++++++------- .../jib/configuration/CacheConfiguration.java | 104 ------------------ .../jib/api/JibContainerBuilderTest.java | 13 ++- .../RetrieveRegistryCredentialsStepTest.java | 14 +-- .../configuration/BuildConfigurationTest.java | 60 ++++++---- .../configuration/CacheConfigurationTest.java | 48 -------- .../tools/jib/gradle/BuildDockerTask.java | 4 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 +- .../gradle/PluginConfigurationProcessor.java | 14 +-- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 3 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../maven/PluginConfigurationProcessor.java | 14 +-- 17 files changed, 178 insertions(+), 280 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 66bc62e8ed..20e2d04506 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -18,8 +18,6 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; @@ -121,8 +119,7 @@ public void setUp() throws IOException, URISyntaxException { @Test public void testSteps_forBuildToDockerRegistry() - throws IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException { + throws IOException, InterruptedException, ExecutionException { BuildSteps buildImageSteps = BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( @@ -146,8 +143,7 @@ public void testSteps_forBuildToDockerRegistry() @Test public void testSteps_forBuildToDockerRegistry_multipleTags() - throws IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException { + throws IOException, InterruptedException, ExecutionException { BuildSteps buildImageSteps = BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( @@ -186,8 +182,7 @@ public void testSteps_forBuildToDockerRegistry_multipleTags() @Test public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() - throws InvalidImageReferenceException, IOException, InterruptedException, - CacheDirectoryCreationException, ExecutionException { + throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException { BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( ImageReference.parse("openjdk:8-jre-alpine"), @@ -203,8 +198,7 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() @Test public void testSteps_forBuildToDockerDaemon() - throws IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException { + throws IOException, InterruptedException, ExecutionException { String imageReference = "testdocker"; BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( @@ -220,8 +214,7 @@ public void testSteps_forBuildToDockerDaemon() @Test public void testSteps_forBuildToDockerDaemon_multipleTags() - throws IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException { + throws IOException, InterruptedException, ExecutionException { String imageReference = "testdocker"; BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( @@ -246,8 +239,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() @Test public void testSteps_forBuildToTarball() - throws IOException, InterruptedException, ExecutionException, - CacheDirectoryCreationException { + throws IOException, InterruptedException, ExecutionException { BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( ImageReference.of("gcr.io", "distroless/java", "latest"), @@ -281,8 +273,8 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setContainerConfiguration(containerConfiguration) - .setBaseImageLayersCacheConfiguration(CacheConfiguration.forPath(cacheDirectory)) - .setApplicationLayersCacheConfiguration(CacheConfiguration.forPath(cacheDirectory)) + .setBaseImageLayersCacheDirectory(cacheDirectory) + .setApplicationLayersCacheDirectory(cacheDirectory) .setAllowInsecureRegistries(true) .setLayerConfigurations(fakeLayerConfigurations) .setToolName("jib-integration-test"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index fc938a7233..20f6e5254a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -17,8 +17,12 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.configuration.CacheConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.UserCacheHome; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -28,6 +32,14 @@ // TODO: Add tests once JibContainerBuilder#containerize() is added. public class Containerizer { + /** + * The default directory for caching the base image layers, in {@code [user cache + * home]/google-cloud-tools-java/jib}. + */ + // TODO: Reduce scope once plugins are migrated to use the new Jib Core API. + public static final Path DEFAULT_BASE_CACHE_DIRECTORY = + UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); + /** * Gets a new {@link Containerizer} that containerizes to a container registry. * @@ -61,7 +73,8 @@ public static Containerizer to(TarImage tarImage) { private final TargetImage targetImage; @Nullable private ExecutorService executorService; - @Nullable private CacheConfiguration cacheConfiguration; + private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; + @Nullable private Path applicationLayersCacheDirectory; @Nullable private EventHandlers eventHandlers; /** Instantiate with {@link #to}. */ @@ -81,9 +94,29 @@ public Containerizer setExecutorService(@Nullable ExecutorService executorServic return this; } - // TODO: Rethink this method. - public Containerizer setCacheConfiguration(CacheConfiguration cacheConfiguration) { - this.cacheConfiguration = cacheConfiguration; + /** + * Sets the directory to use for caching base image layers. This cache can (and should) be shared + * between multiple images. The default base image layers cache directory is {@code [user cache + * home]/google-cloud-tools-java/jib} ({@link #DEFAULT_BASE_CACHE_DIRECTORY}. This directory can + * be the same directory used for {@link #setApplicationLayersCache}. + * + * @param cacheDirectory the cache directory + * @return this + */ + public Containerizer setBaseImageLayersCache(Path cacheDirectory) { + baseImageLayersCacheDirectory = cacheDirectory; + return this; + } + /** + * Sets the directory to use for caching application layers. This cache can be shared between + * multiple images. If not set, a temporary directory will be used as the application layers + * cache. This directory can be the same directory used for {@link #setBaseImageLayersCache}. + * + * @param cacheDirectory the cache directory + * @return this + */ + public Containerizer setApplicationLayersCache(Path cacheDirectory) { + applicationLayersCacheDirectory = cacheDirectory; return this; } @@ -106,8 +139,23 @@ Optional getExecutorService() { return Optional.ofNullable(executorService); } - Optional getCacheConfiguration() { - return Optional.ofNullable(cacheConfiguration); + Path getBaseImageLayersCacheDirectory() { + return baseImageLayersCacheDirectory; + } + + Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException { + if (applicationLayersCacheDirectory == null) { + // Uses a temporary directory if application layers cache directory is not set. + try { + Path temporaryDirectory = Files.createTempDirectory(null); + temporaryDirectory.toFile().deleteOnExit(); + this.applicationLayersCacheDirectory = temporaryDirectory; + + } catch (IOException ex) { + throw new CacheDirectoryCreationException(ex); + } + } + return applicationLayersCacheDirectory; } Optional getEventHandlers() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 9386f12b86..381cb85cf6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -18,7 +18,6 @@ // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; @@ -303,8 +302,7 @@ public JibContainerBuilder addLabel(String key, String value) { } @VisibleForTesting - BuildConfiguration toBuildConfiguration(TargetImage targetImage) - throws IOException, CacheDirectoryCreationException { + BuildConfiguration.Builder toBuildConfiguration(TargetImage targetImage) { BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(); buildConfigurationBuilder @@ -316,7 +314,7 @@ BuildConfiguration toBuildConfiguration(TargetImage targetImage) // TODO: Allow users to configure this. buildConfigurationBuilder.setToolName("jib-core"); - return buildConfigurationBuilder.build(); + return buildConfigurationBuilder; } @VisibleForTesting diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 0d68fbf032..484f705af8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -23,12 +23,15 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.StringJoiner; import javax.annotation.Nullable; /** Immutable configuration options for the builder process. */ @@ -49,8 +52,8 @@ public static class Builder { @Nullable private ImageConfiguration targetImageConfiguration; private ImmutableSet additionalTargetImageTags = ImmutableSet.of(); @Nullable private ContainerConfiguration containerConfiguration; - @Nullable private CacheConfiguration applicationLayersCacheConfiguration; - @Nullable private CacheConfiguration baseImageLayersCacheConfiguration; + @Nullable private Path applicationLayersCacheDirectory; + @Nullable private Path baseImageLayersCacheDirectory; private boolean allowInsecureRegistries = false; private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; @@ -110,24 +113,22 @@ public Builder setContainerConfiguration(ContainerConfiguration containerConfigu /** * Sets the location of the cache for storing application layers. * - * @param applicationLayersCacheConfiguration the application layers {@link CacheConfiguration} + * @param applicationLayersCacheDirectory the application layers cache directory * @return this */ - public Builder setApplicationLayersCacheConfiguration( - @Nullable CacheConfiguration applicationLayersCacheConfiguration) { - this.applicationLayersCacheConfiguration = applicationLayersCacheConfiguration; + public Builder setApplicationLayersCacheDirectory(Path applicationLayersCacheDirectory) { + this.applicationLayersCacheDirectory = applicationLayersCacheDirectory; return this; } /** * Sets the location of the cache for storing base image layers. * - * @param baseImageLayersCacheConfiguration the base image layers {@link CacheConfiguration} + * @param baseImageLayersCacheDirectory the base image layers cache directory * @return this */ - public Builder setBaseImageLayersCacheConfiguration( - @Nullable CacheConfiguration baseImageLayersCacheConfiguration) { - this.baseImageLayersCacheConfiguration = baseImageLayersCacheConfiguration; + public Builder setBaseImageLayersCacheDirectory(Path baseImageLayersCacheDirectory) { + this.baseImageLayersCacheDirectory = baseImageLayersCacheDirectory; return this; } @@ -191,24 +192,26 @@ public Builder setEventDispatcher(EventDispatcher eventDispatcher) { * * @return the corresponding build configuration * @throws IOException if an I/O exception occurs - * @throws CacheDirectoryCreationException if failed to create the configured cache directories */ - public BuildConfiguration build() throws IOException, CacheDirectoryCreationException { + public BuildConfiguration build() throws IOException { // Validates the parameters. - List errorMessages = new ArrayList<>(); + List missingFields = new ArrayList<>(); if (baseImageConfiguration == null) { - errorMessages.add("base image configuration is required but not set"); + missingFields.add("base image configuration"); } if (targetImageConfiguration == null) { - errorMessages.add("target image configuration is required but not set"); + missingFields.add("target image configuration"); + } + if (baseImageLayersCacheDirectory == null) { + missingFields.add("base image layers cache directory"); + } + if (applicationLayersCacheDirectory == null) { + missingFields.add("application layers cache directory"); } - switch (errorMessages.size()) { + switch (missingFields.size()) { case 0: // No errors - if (baseImageConfiguration == null || targetImageConfiguration == null) { - throw new IllegalStateException("Required fields should not be null"); - } - if (baseImageConfiguration.getImage().usesDefaultTag()) { + if (Preconditions.checkNotNull(baseImageConfiguration).getImage().usesDefaultTag()) { eventDispatcher.dispatch( LogEvent.warn( "Base image '" @@ -216,21 +219,13 @@ public BuildConfiguration build() throws IOException, CacheDirectoryCreationExce + "' does not use a specific image digest - build may not be reproducible")); } - if (baseImageLayersCacheConfiguration == null) { - baseImageLayersCacheConfiguration = - CacheConfiguration.forDefaultUserLevelCacheDirectory(); - } - if (applicationLayersCacheConfiguration == null) { - applicationLayersCacheConfiguration = CacheConfiguration.makeTemporary(); - } - return new BuildConfiguration( baseImageConfiguration, - targetImageConfiguration, + Preconditions.checkNotNull(targetImageConfiguration), additionalTargetImageTags, containerConfiguration, - Cache.withDirectory(baseImageLayersCacheConfiguration.getCacheDirectory()), - Cache.withDirectory(applicationLayersCacheConfiguration.getCacheDirectory()), + Cache.withDirectory(Preconditions.checkNotNull(baseImageLayersCacheDirectory)), + Cache.withDirectory(Preconditions.checkNotNull(applicationLayersCacheDirectory)), targetFormat, allowInsecureRegistries, layerConfigurations, @@ -238,27 +233,32 @@ public BuildConfiguration build() throws IOException, CacheDirectoryCreationExce eventDispatcher); case 1: - throw new IllegalStateException(errorMessages.get(0)); + throw new IllegalStateException(missingFields.get(0) + " is required but not set"); case 2: - throw new IllegalStateException(errorMessages.get(0) + " and " + errorMessages.get(1)); + throw new IllegalStateException( + missingFields.get(0) + " and " + missingFields.get(1) + " are required but not set"); default: - // Should never reach here. - throw new IllegalStateException(); + missingFields.add("and " + missingFields.remove(missingFields.size() - 1)); + StringJoiner errorMessage = new StringJoiner(", ", "", " are required but not set"); + for (String missingField : missingFields) { + errorMessage.add(missingField); + } + throw new IllegalStateException(errorMessage.toString()); } } @Nullable @VisibleForTesting - CacheConfiguration getBaseImageLayersCacheConfiguration() { - return baseImageLayersCacheConfiguration; + Path getBaseImageLayersCacheDirectory() { + return baseImageLayersCacheDirectory; } @Nullable @VisibleForTesting - CacheConfiguration getApplicationLayersCacheConfiguration() { - return applicationLayersCacheConfiguration; + Path getApplicationLayersCacheDirectory() { + return applicationLayersCacheDirectory; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java deleted file mode 100644 index ca976a270c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheConfiguration.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import com.google.cloud.tools.jib.filesystem.UserCacheHome; -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Represents the location of the cache. Provides static methods for resolving a location for the - * cache. - */ -public class CacheConfiguration { - - /** - * The default directory for caching the base image layers, in {@code [user cache - * home]/google-cloud-tools-java/jib}. - */ - @VisibleForTesting - static final Path DEFAULT_BASE_CACHE_DIRECTORY = - UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); - - /** - * The cache is at an arbitrary path. - * - * @param cacheDirectory the path to the cache directory. This must be a non-existent directory or - * a previously-used cache directory. - * @return the corresponding {@link CacheConfiguration} - */ - public static CacheConfiguration forPath(Path cacheDirectory) { - return new CacheConfiguration(cacheDirectory, true); - } - - /** - * The cache is a temporary directory that is deleted afterwards. - * - * @return the corresponding {@link CacheConfiguration} - * @throws CacheDirectoryCreationException if a temporary directory cannot be created - */ - public static CacheConfiguration makeTemporary() throws CacheDirectoryCreationException { - try { - Path temporaryDirectory = Files.createTempDirectory(null); - temporaryDirectory.toFile().deleteOnExit(); - return new CacheConfiguration(temporaryDirectory, false); - - } catch (IOException ex) { - throw new CacheDirectoryCreationException(ex); - } - } - - /** - * The cache is at the default user-level cache directory. This is usually to store base image - * layers, which can be shared between projects. The default user-level cache directory is {@code - * [user cache home]/google-cloud-tools-java/jib}. - * - * @return the corresponding {@link CacheConfiguration} - */ - public static CacheConfiguration forDefaultUserLevelCacheDirectory() { - return new CacheConfiguration(DEFAULT_BASE_CACHE_DIRECTORY, true); - } - - private final Path cacheDirectory; - // TODO: Deprecate - private final boolean shouldEnsureOwnership; - - private CacheConfiguration(Path cacheDirectory, boolean shouldEnsureOwnership) { - this.cacheDirectory = cacheDirectory; - this.shouldEnsureOwnership = shouldEnsureOwnership; - } - - /** - * Gets the path to the cache directory. - * - * @return the cache directory path - */ - public Path getCacheDirectory() { - return cacheDirectory; - } - - /** - * Gets whether or not the cache directory should be checked for write safety. - * - * @return {@code true} if ownership by Jib should be checked - */ - public boolean shouldEnsureOwnership() { - return shouldEnsureOwnership; - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index c087c71d07..2703333cc6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; @@ -28,6 +27,7 @@ import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.nio.file.Paths; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -93,8 +93,7 @@ public void testToContainerConfiguration_add() { @Test public void testToBuildConfiguration() - throws InvalidImageReferenceException, CredentialRetrievalException, IOException, - CacheDirectoryCreationException { + throws InvalidImageReferenceException, CredentialRetrievalException, IOException { RegistryImage baseImage = RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); RegistryImage targetImage = @@ -104,7 +103,13 @@ public void testToBuildConfiguration() JibContainerBuilder jibContainerBuilder = Jib.from(baseImage) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); - BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(targetImage); + BuildConfiguration buildConfiguration = + jibContainerBuilder + .toBuildConfiguration(targetImage) + // Fills in defaults. + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .build(); Assert.assertEquals( jibContainerBuilder.toContainerConfiguration(), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index f076be98ca..0e73892f4d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; @@ -27,6 +26,7 @@ import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -46,8 +46,7 @@ public class RetrieveRegistryCredentialsStepTest { @Mock private ListeningExecutorService mockListeningExecutorService; @Test - public void testCall_retrieved() - throws CredentialRetrievalException, IOException, CacheDirectoryCreationException { + public void testCall_retrieved() throws CredentialRetrievalException, IOException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( Arrays.asList( @@ -70,8 +69,7 @@ public void testCall_retrieved() } @Test - public void testCall_none() - throws CredentialRetrievalException, IOException, CacheDirectoryCreationException { + public void testCall_none() throws CredentialRetrievalException, IOException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); @@ -93,7 +91,7 @@ public void testCall_none() } @Test - public void testCall_exception() throws IOException, CacheDirectoryCreationException { + public void testCall_exception() throws IOException { CredentialRetrievalException credentialRetrievalException = Mockito.mock(CredentialRetrievalException.class); BuildConfiguration buildConfiguration = @@ -116,7 +114,7 @@ public void testCall_exception() throws IOException, CacheDirectoryCreationExcep private BuildConfiguration makeFakeBuildConfiguration( List baseCredentialRetrievers, List targetCredentialRetrievers) - throws IOException, CacheDirectoryCreationException { + throws IOException { ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder() @@ -129,6 +127,8 @@ private BuildConfiguration makeFakeBuildConfiguration( ImageConfiguration.builder(targetImage) .setCredentialRetrievers(targetCredentialRetrievers) .build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) .build(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index d0e4738dbd..e8f23d2d15 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; @@ -61,10 +62,8 @@ public void testBuilder() throws Exception { ImmutableList expectedExposedPorts = ImmutableList.of(Port.tcp(1000), Port.tcp(2000)); Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); Class expectedTargetFormat = OCIManifestTemplate.class; - CacheConfiguration expectedApplicationLayersCacheConfiguration = - CacheConfiguration.forPath(Paths.get("application/layers")); - CacheConfiguration expectedBaseImageLayersCacheConfiguration = - CacheConfiguration.forPath(Paths.get("base/image/layers")); + Path expectedApplicationLayersCacheDirectory = Paths.get("application/layers"); + Path expectedBaseImageLayersCacheDirectory = Paths.get("base/image/layers"); List expectedLayerConfigurations = Collections.singletonList( LayerConfiguration.builder() @@ -98,8 +97,8 @@ public void testBuilder() throws Exception { .setTargetImageConfiguration(targetImageConfiguration) .setAdditionalTargetImageTags(additionalTargetImageTags) .setContainerConfiguration(containerConfiguration) - .setApplicationLayersCacheConfiguration(expectedApplicationLayersCacheConfiguration) - .setBaseImageLayersCacheConfiguration(expectedBaseImageLayersCacheConfiguration) + .setApplicationLayersCacheDirectory(expectedApplicationLayersCacheDirectory) + .setBaseImageLayersCacheDirectory(expectedBaseImageLayersCacheDirectory) .setTargetFormat(OCIManifestTemplate.class) .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) @@ -143,11 +142,11 @@ public void testBuilder() throws Exception { Assert.assertEquals(expectedLabels, buildConfiguration.getContainerConfiguration().getLabels()); Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); Assert.assertEquals( - expectedApplicationLayersCacheConfiguration, - buildConfigurationBuilder.getApplicationLayersCacheConfiguration()); + expectedApplicationLayersCacheDirectory, + buildConfigurationBuilder.getApplicationLayersCacheDirectory()); Assert.assertEquals( - expectedBaseImageLayersCacheConfiguration, - buildConfigurationBuilder.getBaseImageLayersCacheConfiguration()); + expectedBaseImageLayersCacheDirectory, + buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); Assert.assertEquals( @@ -156,7 +155,7 @@ public void testBuilder() throws Exception { } @Test - public void testBuilder_default() throws IOException, CacheDirectoryCreationException { + public void testBuilder_default() throws IOException { // These are required and don't have defaults. String expectedBaseImageServerUrl = "someserver"; String expectedBaseImageName = "baseimage"; @@ -178,19 +177,19 @@ public void testBuilder_default() throws IOException, CacheDirectoryCreationExce BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder() .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration); + .setTargetImageConfiguration(targetImageConfiguration) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); - Assert.assertNotNull(buildConfigurationBuilder.getApplicationLayersCacheConfiguration()); - Assert.assertNotEquals( - CacheConfiguration.forDefaultUserLevelCacheDirectory().getCacheDirectory(), - buildConfigurationBuilder.getApplicationLayersCacheConfiguration().getCacheDirectory()); - Assert.assertNotNull(buildConfigurationBuilder.getBaseImageLayersCacheConfiguration()); - Assert.assertEquals( - CacheConfiguration.forDefaultUserLevelCacheDirectory().getCacheDirectory(), - buildConfigurationBuilder.getBaseImageLayersCacheConfiguration().getCacheDirectory()); + Assert.assertNotNull(buildConfigurationBuilder.getApplicationLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("ignored"), buildConfigurationBuilder.getApplicationLayersCacheDirectory()); + Assert.assertNotNull(buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("ignored"), buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); Assert.assertNull(buildConfiguration.getContainerConfiguration()); Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); @@ -198,12 +197,14 @@ public void testBuilder_default() throws IOException, CacheDirectoryCreationExce } @Test - public void testBuilder_missingValues() throws IOException, CacheDirectoryCreationException { + public void testBuilder_missingValues() throws IOException { // Target image is missing try { BuildConfiguration.builder() .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) .build(); Assert.fail("Build configuration should not be built with missing values"); @@ -211,6 +212,20 @@ public void testBuilder_missingValues() throws IOException, CacheDirectoryCreati Assert.assertEquals("target image configuration is required but not set", ex.getMessage()); } + // Two required fields missing + try { + BuildConfiguration.builder() + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .build(); + Assert.fail("Build configuration should not be built with missing values"); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "base image configuration and target image configuration are required but not set", + ex.getMessage()); + } + // All required fields missing try { BuildConfiguration.builder().build(); @@ -218,7 +233,8 @@ public void testBuilder_missingValues() throws IOException, CacheDirectoryCreati } catch (IllegalStateException ex) { Assert.assertEquals( - "base image configuration is required but not set and target image configuration is required but not set", + "base image configuration, target image configuration, base image layers cache directory, " + + "and application layers cache directory are required but not set", ex.getMessage()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java deleted file mode 100644 index b994c249b0..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/CacheConfigurationTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.configuration; - -import java.nio.file.Files; -import java.nio.file.Paths; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link CacheConfiguration}. */ -public class CacheConfigurationTest { - - @Test - public void testAtPath() { - CacheConfiguration cacheConfiguration = CacheConfiguration.forPath(Paths.get("/path/to/cache")); - Assert.assertEquals(Paths.get("/path/to/cache"), cacheConfiguration.getCacheDirectory()); - Assert.assertTrue(cacheConfiguration.shouldEnsureOwnership()); - } - - @Test - public void testMakeTemporary() throws CacheDirectoryCreationException { - CacheConfiguration cacheConfiguration = CacheConfiguration.makeTemporary(); - Assert.assertTrue(Files.exists(cacheConfiguration.getCacheDirectory())); - Assert.assertFalse(cacheConfiguration.shouldEnsureOwnership()); - } - - @Test - public void testAtDefaultUserLevelCacheDirectory() { - CacheConfiguration cacheConfiguration = CacheConfiguration.forDefaultUserLevelCacheDirectory(); - Assert.assertEquals( - CacheConfiguration.DEFAULT_BASE_CACHE_DIRECTORY, cacheConfiguration.getCacheDirectory()); - Assert.assertTrue(cacheConfiguration.shouldEnsureOwnership()); - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index ef48ff9395..0c251a5e28 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -67,8 +66,7 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildDocker() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - BuildStepsExecutionException { + throws InvalidImageReferenceException, IOException, BuildStepsExecutionException { if (!new DockerClient().isDockerInstalled()) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index ec85cb6a57..472ccd7ce7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -71,8 +70,7 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildImage() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - BuildStepsExecutionException { + throws InvalidImageReferenceException, IOException, BuildStepsExecutionException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index e62af1974a..bd7d0f58bd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; @@ -98,8 +97,7 @@ private String getTargetPath() { @TaskAction public void buildTar() - throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, - CacheDirectoryCreationException { + throws InvalidImageReferenceException, BuildStepsExecutionException, IOException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index badd48e53d..1f4408c46a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -144,13 +144,13 @@ static PluginConfigurationProcessor processCommonConfiguration( .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(projectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); + buildConfigurationBuilder.setApplicationLayersCacheDirectory( + projectProperties.getCacheDirectory()); + buildConfigurationBuilder.setBaseImageLayersCacheDirectory( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); + buildConfigurationBuilder.setBaseImageLayersCacheDirectory( + projectProperties.getCacheDirectory()); } return new PluginConfigurationProcessor( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 8174195e0c..e99a1f0cce 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -101,7 +100,7 @@ public void execute() throws MojoExecutionException { BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException | InvalidImageReferenceException | IOException ex) { + } catch (InvalidImageReferenceException | IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index fde58f5827..de3baed23e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -148,7 +147,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException | IOException ex) { + } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 214cefa755..78070f4472 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; @@ -100,7 +99,7 @@ public void execute() throws MojoExecutionException { .build(helpfulSuggestions); getLog().info(""); - } catch (CacheDirectoryCreationException | InvalidImageReferenceException | IOException ex) { + } catch (InvalidImageReferenceException | IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 34a15ff3ad..9c416f6471 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -162,13 +162,13 @@ static PluginConfigurationProcessor processCommonConfiguration( .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); - CacheConfiguration applicationLayersCacheConfiguration = - CacheConfiguration.forPath(projectProperties.getCacheDirectory()); - buildConfigurationBuilder.setApplicationLayersCacheConfiguration( - applicationLayersCacheConfiguration); + buildConfigurationBuilder.setApplicationLayersCacheDirectory( + projectProperties.getCacheDirectory()); + buildConfigurationBuilder.setBaseImageLayersCacheDirectory( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); if (jibPluginConfiguration.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheConfiguration( - applicationLayersCacheConfiguration); + buildConfigurationBuilder.setBaseImageLayersCacheDirectory( + projectProperties.getCacheDirectory()); } return new PluginConfigurationProcessor( From ffddc9f8899a17c5530e191ad939398856a7e7de Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 1 Oct 2018 16:27:26 -0400 Subject: [PATCH 0259/2020] Check directory existence (#1077) --- .../jib/gradle/GradleLayerConfigurations.java | 64 +++++----- .../gradle/GradleLayerConfigurationsTest.java | 109 +++++++++++------- 2 files changed, 105 insertions(+), 68 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index faf8356c26..4e0de4c285 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -179,15 +179,17 @@ private static JavaLayerConfigurations getForWarProject( Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); Path libOutputDirectory = explodedWarPath.resolve("WEB-INF/lib"); - try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { - dependencyFileStream.forEach( - path -> { - if (path.toString().contains(SNAPSHOT)) { - snapshotDependenciesFiles.add(path); - } else { - dependenciesFiles.add(path); - } - }); + if (Files.exists(libOutputDirectory)) { + try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { + dependencyFileStream.forEach( + path -> { + if (path.toString().contains(SNAPSHOT)) { + snapshotDependenciesFiles.add(path); + } else { + dependenciesFiles.add(path); + } + }); + } } // First, all files except WEB-INF go into the resources layer. @@ -197,10 +199,12 @@ private static JavaLayerConfigurations getForWarProject( // Some files in WEB-INF/ (e.g. web.xml, ...) need to go into the resources layer. However, // don't add or look into WEB-INF/classes and WEB-INF/lib. Path webInfOutputDirectory = explodedWarPath.resolve("WEB-INF"); - try (Stream fileStream = Files.list(webInfOutputDirectory)) { - fileStream - .filter(path -> !path.endsWith("classes") && !path.endsWith("lib")) - .forEach(resourcesWebInfFiles::add); + if (Files.exists(webInfOutputDirectory)) { + try (Stream fileStream = Files.list(webInfOutputDirectory)) { + fileStream + .filter(path -> !path.endsWith("classes") && !path.endsWith("lib")) + .forEach(resourcesWebInfFiles::add); + } } // Adds all the extra files. @@ -218,26 +222,28 @@ private static JavaLayerConfigurations getForWarProject( // For "WEB-INF/classes", *.class go into the class layer. All other files and empty directories // go into the resource layer. Path webInfClasses = explodedWarPath.resolve("WEB-INF/classes"); - new DirectoryWalker(webInfClasses) - .walk( - path -> { - AbsoluteUnixPath pathInContainer = - classesExtractionPath.resolve(webInfClasses.relativize(path)); + if (Files.exists(webInfClasses)) { + new DirectoryWalker(webInfClasses) + .walk( + path -> { + AbsoluteUnixPath pathInContainer = + classesExtractionPath.resolve(webInfClasses.relativize(path)); - if (path.getFileName().toString().endsWith(".class")) { - layerBuilder.addClassFile(path, pathInContainer); + if (path.getFileName().toString().endsWith(".class")) { + layerBuilder.addClassFile(path, pathInContainer); - } else if (Files.isDirectory(path)) { - try (Stream fileStream = Files.list(path)) { - if (!fileStream.findAny().isPresent()) { - // The directory is empty - layerBuilder.addResourceFile(path, pathInContainer); + } else if (Files.isDirectory(path)) { + try (Stream fileStream = Files.list(path)) { + if (!fileStream.findAny().isPresent()) { + // The directory is empty + layerBuilder.addResourceFile(path, pathInContainer); + } } + } else { + layerBuilder.addResourceFile(path, pathInContainer); } - } else { - layerBuilder.addResourceFile(path, pathInContainer); - } - }); + }); + } for (Path file : dependenciesFiles) { layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName())); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 2739543f0e..61f80f0fb0 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -48,7 +48,9 @@ import org.gradle.api.tasks.bundling.War; import org.junit.Assert; import org.junit.Before; +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.Mockito; @@ -96,6 +98,8 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private Project mockProject; @Mock private Project mockWebAppProject; @Mock private Convention mockConvention; @@ -108,6 +112,8 @@ private static void assertExtractionPathsUnordered( @Mock private TaskContainer taskContainer; @Mock private War war; + private Path extraFilesDirectory; + @Before public void setUp() throws URISyntaxException, IOException { Set classesFiles = @@ -141,6 +147,8 @@ public void setUp() throws URISyntaxException, IOException { Paths.get(Resources.getResource("webapp").toURI()) .resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); + + extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); } @Test @@ -200,9 +208,7 @@ public void test_noClassesFiles() throws IOException { } @Test - public void test_extraFiles() throws URISyntaxException, IOException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - + public void test_extraFiles() throws IOException { JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/app")); @@ -221,9 +227,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { } @Test - public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - + public void testGetForProject_nonDefaultAppRoot() throws IOException { JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); @@ -253,9 +257,7 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException, URISyntaxE } @Test - public void testGetForProject_defaultAppRoot() throws IOException, URISyntaxException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - + public void testGetForProject_defaultAppRoot() throws IOException { JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( mockProject, @@ -287,38 +289,30 @@ public void testGetForProject_defaultAppRoot() throws IOException, URISyntaxExce @Test public void testWebApp() throws URISyntaxException, IOException { - Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); - Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webappDirectory.toFile()); - Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); - Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); - Mockito.when(taskContainer.findByName("war")).thenReturn(war); - - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + Path webAppDirectory = Paths.get(Resources.getResource("webapp").toURI()); + setUpWarProject(webAppDirectory); JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( mockWebAppProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); ImmutableList expectedDependenciesFiles = ImmutableList.of( - webappDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( - webappDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); ImmutableList expectedResourcesFiles = ImmutableList.of( - webappDirectory.resolve("jib-exploded-war/META-INF"), - webappDirectory.resolve("jib-exploded-war/META-INF/context.xml"), - webappDirectory.resolve("jib-exploded-war/Test.jsp"), - webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), - webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), - webappDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")); + webAppDirectory.resolve("jib-exploded-war/META-INF"), + webAppDirectory.resolve("jib-exploded-war/META-INF/context.xml"), + webAppDirectory.resolve("jib-exploded-war/Test.jsp"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")); ImmutableList expectedClassesFiles = ImmutableList.of( - webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), - webappDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); ImmutableList expectedExtraFiles = ImmutableList.of( extraFilesDirectory.resolve("a"), @@ -363,16 +357,7 @@ public void testWebApp() throws URISyntaxException, IOException { @Test public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOException { - Path webappDirectory = Paths.get(Resources.getResource("webapp").toURI()); - Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webappDirectory.toFile()); - Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); - Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); - Mockito.when(taskContainer.findByName("war")).thenReturn(war); - - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + setUpWarProject(Paths.get(Resources.getResource("webapp").toURI())); JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( @@ -405,4 +390,50 @@ public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOExceptio Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); } + + @Test + public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOException { + temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + + GradleLayerConfigurations.getForProject( + mockWebAppProject, + mockLogger, + extraFilesDirectory, + AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + } + + @Test + public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOException { + temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + + GradleLayerConfigurations.getForProject( + mockWebAppProject, + mockLogger, + extraFilesDirectory, + AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + } + + @Test + public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOException { + temporaryFolder.newFolder("jib-exploded-war"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + + GradleLayerConfigurations.getForProject( + mockWebAppProject, + mockLogger, + extraFilesDirectory, + AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + } + + private void setUpWarProject(Path webAppDirectory) { + Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); + Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) + .thenReturn(mockWarPluginConvention); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); + Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); + Mockito.when(taskContainer.findByName("war")).thenReturn(war); + } } From 24e32fe9ac41f8e28966c54401f425f35e530d8f Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 2 Oct 2018 08:36:15 -0700 Subject: [PATCH 0260/2020] Makes BuildSteps#run return the built image digest. (#1078) --- .../builder/BuildStepsIntegrationTest.java | 14 +++- .../ManifestPusherIntegrationTest.java | 15 ++++- .../cloud/tools/jib/builder/BuildSteps.java | 26 ++++++- .../jib/builder/steps/LoadDockerStep.java | 30 +++++++-- .../jib/builder/steps/PushImageStep.java | 28 +++++--- .../tools/jib/builder/steps/StepsRunner.java | 13 ++-- .../jib/builder/steps/WriteTarFileStep.java | 33 +++++++-- .../tools/jib/registry/ManifestPusher.java | 67 +++++++++++++++++-- .../tools/jib/registry/RegistryClient.java | 10 ++- .../jib/registry/ManifestPusherTest.java | 67 ++++++++++++++++++- 10 files changed, 259 insertions(+), 44 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 20e2d04506..a37e956b58 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; @@ -128,17 +129,26 @@ public void testSteps_forBuildToDockerRegistry() .build()); long lastTime = System.nanoTime(); - buildImageSteps.run(); + DescriptorDigest imageDigest1 = buildImageSteps.run(); logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); lastTime = System.nanoTime(); - buildImageSteps.run(); + DescriptorDigest imageDigest2 = buildImageSteps.run(); logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + Assert.assertEquals(imageDigest1, imageDigest2); + String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); assertDockerInspect(imageReference); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); + + String imageReferenceByDigest = "localhost:5000/testimage@" + imageDigest1; + localRegistry.pull(imageReferenceByDigest); + assertDockerInspect(imageReferenceByDigest); + Assert.assertEquals( + "Hello, world. An argument.\n", + new Command("docker", "run", "--rm", imageReferenceByDigest).run()); } @Test diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index b653200713..432b5ea7d9 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -24,6 +24,8 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -90,7 +92,7 @@ public void testPush() testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null)); // Pushes the manifest. - registryClient.pushManifest(expectedManifestTemplate, "latest"); + DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); // Pulls the manifest. V22ManifestTemplate manifestTemplate = @@ -100,5 +102,16 @@ public void testPush() Assert.assertEquals( testContainerConfigurationBlobDigest, manifestTemplate.getContainerConfiguration().getDigest()); + + // Pulls the manifest by digest. + V22ManifestTemplate manifestTemplateByDigest = + registryClient.pullManifest(imageDigest.toString(), V22ManifestTemplate.class); + Assert.assertEquals( + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(), + JsonTemplateMapper.toBlob(manifestTemplateByDigest) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index e6702b080a..517e5636de 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; import java.util.concurrent.ExecutionException; @@ -33,7 +34,16 @@ public class BuildSteps { @FunctionalInterface private interface StepsRunnerConsumer { - void accept(StepsRunner stepsRunner) throws ExecutionException, InterruptedException; + /** + * Runs a {@link StepsRunner}. + * + * @param stepsRunner the {@link StepsRunner} to run + * @return the digest of the built image + * @throws ExecutionException if an exception occurs during execution + * @throws InterruptedException if the execution is interrupted + */ + DescriptorDigest accept(StepsRunner stepsRunner) + throws ExecutionException, InterruptedException; } /** @@ -127,12 +137,20 @@ public BuildConfiguration getBuildConfiguration() { return buildConfiguration; } - public void run() throws InterruptedException, ExecutionException { + /** + * Executes the build. + * + * @return the built image digest + * @throws InterruptedException if the execution is interrupted + * @throws ExecutionException if an exception occurs during execution + */ + public DescriptorDigest run() throws InterruptedException, ExecutionException { buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); + DescriptorDigest imageDigest; try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { - stepsRunnerConsumer.accept(new StepsRunner(buildConfiguration)); + imageDigest = stepsRunnerConsumer.accept(new StepsRunner(buildConfiguration)); } if (buildConfiguration.getContainerConfiguration() != null) { @@ -144,5 +162,7 @@ public void run() throws InterruptedException, ExecutionException { "Container entrypoint set to " + buildConfiguration.getContainerConfiguration().getEntrypoint())); } + + return imageDigest; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 04b6aca189..2030cc6be0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -18,14 +18,20 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -34,7 +40,7 @@ import java.util.concurrent.ExecutionException; /** Adds image layers to a tarball and loads into Docker daemon. */ -class LoadDockerStep implements AsyncStep, Callable { +class LoadDockerStep implements AsyncStep, Callable { private final BuildConfiguration buildConfiguration; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; @@ -42,7 +48,7 @@ class LoadDockerStep implements AsyncStep, Callable { private final BuildImageStep buildImageStep; private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; LoadDockerStep( ListeningExecutorService listeningExecutorService, @@ -63,12 +69,12 @@ class LoadDockerStep implements AsyncStep, Callable { } @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public Void call() throws ExecutionException, InterruptedException { + public DescriptorDigest call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { @@ -84,7 +90,7 @@ public Void call() throws ExecutionException, InterruptedException { .get(); } - private Void afterPushBaseImageLayerFuturesFuture() + private DescriptorDigest afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = @@ -106,6 +112,18 @@ private Void afterPushBaseImageLayerFuturesFuture() dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } - return null; + // TODO: Consolide image digest generation with PushImageStep and WriteTarFileStep. + // Gets the image manifest to generate the image digest. + ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); + BlobDescriptor containerConfigurationBlobDescriptor = + imageToJsonTranslator + .getContainerConfigurationBlob() + .writeTo(ByteStreams.nullOutputStream()); + BuildableManifestTemplate manifestTemplate = + imageToJsonTranslator.getManifestTemplate( + buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); + return JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index ac08ce6d18..6beb0cc376 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -21,20 +21,24 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -/** Pushes the final image. */ -class PushImageStep implements AsyncStep, Callable { +/** Pushes the final image. Outputs the pushed image digest. */ +class PushImageStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Pushing new image"; @@ -47,7 +51,7 @@ class PushImageStep implements AsyncStep, Callable { private final BuildImageStep buildImageStep; private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; PushImageStep( ListeningExecutorService listeningExecutorService, @@ -75,12 +79,12 @@ class PushImageStep implements AsyncStep, Callable { } @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public Void call() throws ExecutionException, InterruptedException { + public DescriptorDigest call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); dependenciesBuilder.add(authenticatePushStep.getFuture()); for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { @@ -99,7 +103,8 @@ public Void call() throws ExecutionException, InterruptedException { .get(); } - private ListenableFuture> afterPushSteps() throws ExecutionException { + private ListenableFuture> afterPushSteps() + throws ExecutionException { List> dependencies = new ArrayList<>(); for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); @@ -114,7 +119,8 @@ private ListenableFuture> afterPushSteps() throws Executi .call(this::afterAllPushed, listeningExecutorService); } - private ListenableFuture afterAllPushed() throws ExecutionException { + private ListenableFuture afterAllPushed() + throws ExecutionException, IOException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { RegistryClient registryClient = @@ -147,7 +153,13 @@ private ListenableFuture afterAllPushed() throws ExecutionException { return null; })); } - return Futures.whenAllSucceed(pushAllTagsFutures).call(() -> null, listeningExecutorService); + + DescriptorDigest imageDigest = + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + return Futures.whenAllSucceed(pushAllTagsFutures) + .call(() -> imageDigest, listeningExecutorService); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index f2c8577441..dc7401e2e5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; @@ -201,15 +202,15 @@ public StepsRunner runWriteTarFileStep(Path outputPath) { return this; } - public void waitOnPushImageStep() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(pushImageStep).getFuture().get(); + public DescriptorDigest waitOnPushImageStep() throws ExecutionException, InterruptedException { + return Preconditions.checkNotNull(pushImageStep).getFuture().get(); } - public void waitOnLoadDockerStep() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(loadDockerStep).getFuture().get(); + public DescriptorDigest waitOnLoadDockerStep() throws ExecutionException, InterruptedException { + return Preconditions.checkNotNull(loadDockerStep).getFuture().get(); } - public void waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(writeTarFileStep).getFuture().get(); + public DescriptorDigest waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { + return Preconditions.checkNotNull(writeTarFileStep).getFuture().get(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index beacb3623f..3f1f8ac889 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -18,13 +18,19 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -36,7 +42,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -public class WriteTarFileStep implements AsyncStep, Callable { +public class WriteTarFileStep implements AsyncStep, Callable { private final Path outputPath; private final BuildConfiguration buildConfiguration; @@ -45,7 +51,7 @@ public class WriteTarFileStep implements AsyncStep, Callable { private final BuildImageStep buildImageStep; private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; WriteTarFileStep( ListeningExecutorService listeningExecutorService, @@ -68,12 +74,12 @@ public class WriteTarFileStep implements AsyncStep, Callable { } @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public Void call() throws ExecutionException, InterruptedException { + public DescriptorDigest call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { @@ -89,10 +95,11 @@ public Void call() throws ExecutionException, InterruptedException { .get(); } - private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, IOException { + private DescriptorDigest afterPushBaseImageLayerFuturesFuture() + throws ExecutionException, IOException { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - // Build the image to a tarball + // Builds the image to a tarball. buildConfiguration .getEventDispatcher() .dispatch(LogEvent.lifecycle("Building image to tar file...")); @@ -104,6 +111,18 @@ private Void afterPushBaseImageLayerFuturesFuture() throws ExecutionException, I .writeTo(outputStream); } - return null; + // TODO: Consolide image digest generation with PushImageStep and LoadDockerStep. + // Gets the image manifest to generate the image digest. + ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); + BlobDescriptor containerConfigurationBlobDescriptor = + imageToJsonTranslator + .getContainerConfigurationBlob() + .writeTo(ByteStreams.nullOutputStream()); + BuildableManifestTemplate manifestTemplate = + imageToJsonTranslator.getManifestTemplate( + buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); + return JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 7dff52623d..540b5e99d5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -18,30 +18,65 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.security.DigestException; import java.util.Collections; import java.util.List; +import java.util.StringJoiner; import org.apache.http.HttpStatus; /** Pushes an image's manifest. */ -class ManifestPusher implements RegistryEndpointProvider { +class ManifestPusher implements RegistryEndpointProvider { + + /** Response header containing digest of pushed image. */ + private static final String RESPONSE_DIGEST_HEADER = "Docker-Content-Digest"; + + /** + * Makes the warning for when the registry responds with an image digest that is not the expected + * digest of the image. + * + * @param expectedDigest the expected image digest + * @param receivedDigests the received image digests + * @return the warning message + */ + private static String makeUnexpectedImageDigestWarning( + DescriptorDigest expectedDigest, List receivedDigests) { + if (receivedDigests.isEmpty()) { + return "Expected image digest " + expectedDigest + ", but received none"; + } + + StringJoiner message = + new StringJoiner(", ", "Expected image digest " + expectedDigest + ", but received: ", ""); + for (String receivedDigest : receivedDigests) { + message.add(receivedDigest); + } + return message.toString(); + } private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final BuildableManifestTemplate manifestTemplate; private final String imageTag; + private final EventDispatcher eventDispatcher; ManifestPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, BuildableManifestTemplate manifestTemplate, - String imageTag) { + String imageTag, + EventDispatcher eventDispatcher) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.manifestTemplate = manifestTemplate; this.imageTag = imageTag; + this.eventDispatcher = eventDispatcher; } @Override @@ -56,7 +91,7 @@ public List getAccept() { } @Override - public Void handleHttpResponseException(HttpResponseException httpResponseException) + public DescriptorDigest handleHttpResponseException(HttpResponseException httpResponseException) throws HttpResponseException, RegistryErrorException { // docker registry 2.0 and 2.1 returns: // 400 Bad Request @@ -85,8 +120,30 @@ public Void handleHttpResponseException(HttpResponseException httpResponseExcept } @Override - public Void handleResponse(Response response) { - return null; + public DescriptorDigest handleResponse(Response response) throws IOException { + // Checks if the image digest is as expected. + DescriptorDigest expectedDigest = + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + + List receivedDigests = response.getHeader(RESPONSE_DIGEST_HEADER); + if (receivedDigests.size() == 1) { + try { + DescriptorDigest receivedDigest = DescriptorDigest.fromDigest(receivedDigests.get(0)); + if (expectedDigest.equals(receivedDigest)) { + return expectedDigest; + } + + } catch (DigestException ex) { + // Invalid digest. + } + } + + // The received digest is not as expected. Warns about this. + eventDispatcher.dispatch( + LogEvent.warn(makeUnexpectedImageDigestWarning(expectedDigest, receivedDigests))); + return expectedDigest; } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b04644e11c..9cdd705447 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Verify; import java.io.IOException; import java.net.URL; import javax.annotation.Nullable; @@ -212,13 +213,16 @@ public ManifestTemplate pullManifest(String imageTag) throws IOException, Regist * * @param manifestTemplate the image manifest * @param imageTag the tag to push on + * @return the digest of the pushed image * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public void pushManifest(BuildableManifestTemplate manifestTemplate, String imageTag) + public DescriptorDigest pushManifest(BuildableManifestTemplate manifestTemplate, String imageTag) throws IOException, RegistryException { - callRegistryEndpoint( - new ManifestPusher(registryEndpointRequestProperties, manifestTemplate, imageTag)); + return Verify.verifyNotNull( + callRegistryEndpoint( + new ManifestPusher( + registryEndpointRequestProperties, manifestTemplate, imageTag, eventDispatcher))); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 2f9f971325..c2a3697423 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -18,10 +18,14 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -32,12 +36,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; import org.apache.http.HttpStatus; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -45,6 +52,9 @@ @RunWith(MockitoJUnitRunner.class) public class ManifestPusherTest { + @Mock private Response mockResponse; + @Mock private EventDispatcher mockEventDispatcher; + private Path v22manifestJsonFile; private V22ManifestTemplate fakeManifestTemplate; private ManifestPusher testManifestPusher; @@ -59,7 +69,8 @@ public void setUp() throws URISyntaxException, IOException { new ManifestPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeManifestTemplate, - "test-image-tag"); + "test-image-tag", + mockEventDispatcher); } @Test @@ -78,8 +89,58 @@ public void testGetContent() throws IOException { } @Test - public void testHandleResponse() { - Assert.assertNull(testManifestPusher.handleResponse(Mockito.mock(Response.class))); + public void testHandleResponse_valid() throws IOException { + DescriptorDigest expectedDigest = + JsonTemplateMapper.toBlob(fakeManifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.singletonList(expectedDigest.toString())); + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + } + + @Test + public void testHandleResponse_noDigest() throws IOException { + DescriptorDigest expectedDigest = + JsonTemplateMapper.toBlob(fakeManifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.emptyList()); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.warn("Expected image digest " + expectedDigest + ", but received none")); + } + + @Test + public void testHandleResponse_multipleDigests() throws IOException { + DescriptorDigest expectedDigest = + JsonTemplateMapper.toBlob(fakeManifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Arrays.asList("too", "many")); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventDispatcher) + .dispatch( + LogEvent.warn("Expected image digest " + expectedDigest + ", but received: too, many")); + } + + @Test + public void testHandleResponse_invalidDigest() throws IOException { + DescriptorDigest expectedDigest = + JsonTemplateMapper.toBlob(fakeManifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) + .thenReturn(Collections.singletonList("not valid")); + + Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); + Mockito.verify(mockEventDispatcher) + .dispatch( + LogEvent.warn("Expected image digest " + expectedDigest + ", but received: not valid")); } @Test From d24a6f98d5834a21961c6fd2881677a8f51b071b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 2 Oct 2018 11:45:01 -0400 Subject: [PATCH 0261/2020] Add system properties for remaining maven config (#1080) --- jib-maven-plugin/CHANGELOG.md | 2 + .../cloud/tools/jib/maven/BuildImageMojo.java | 4 +- .../jib/maven/JibPluginConfiguration.java | 180 +++++++++++++++++- .../maven/PluginConfigurationProcessor.java | 4 +- .../jib/maven/JibPluginConfigurationTest.java | 76 ++++++++ .../ConfigurationPropertyValidator.java | 56 ++++++ .../ConfigurationPropertyValidatorTest.java | 28 +++ 7 files changed, 340 insertions(+), 10 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 127e7f2be9..73f4d5cdb9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Properties for each configuration parameter, allowing any parameter to be set via commandline ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) + ### Changed - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index de3baed23e..aa03d3c0b7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -99,8 +99,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( mavenProjectProperties.getEventDispatcher(), - "jib.to.auth.username", - "jib.to.auth.password", + JibPluginConfiguration.PropertyNames.toAuthUsername, + JibPluginConfiguration.PropertyNames.toAuthPassword, getTargetImageAuth()); if (optionalToCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index b82391884e..428cd284d4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -18,8 +18,10 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; import java.io.File; import java.nio.file.Path; import java.util.Collections; @@ -39,6 +41,34 @@ /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ abstract class JibPluginConfiguration extends AbstractMojo { + /** Names of system properties used to set configuration via commandline. */ + static class PropertyNames { + private static final String fromImage = "jib.from.image"; + private static final String fromCredHelper = "jib.from.credHelper"; + static final String fromAuthUsername = "jib.from.auth.username"; + static final String fromAuthPassword = "jib.from.auth.password"; + private static final String toImage = "image"; + private static final String toImageAlternate = "jib.to.image"; + private static final String toTags = "jib.to.tags"; + private static final String toCredHelper = "jib.to.credHelper"; + static final String toAuthUsername = "jib.to.auth.username"; + static final String toAuthPassword = "jib.to.auth.password"; + private static final String containerAppRoot = "jib.container.appRoot"; + private static final String containerArgs = "jib.container.args"; + private static final String containerEntrypoint = "jib.container.entrypoint"; + private static final String containerEnvironment = "jib.container.environment"; + private static final String containerFormat = "jib.container.format"; + private static final String containerJvmFlags = "jib.container.jvmFlags"; + private static final String containerLabels = "jib.container.labels"; + private static final String containerMainClass = "jib.container.mainClass"; + private static final String containerPorts = "jib.container.ports"; + private static final String containerUseCurrentTimestamp = "jib.container.useCurrentTimestamp"; + private static final String useOnlyProjectCache = "jib.useOnlyProjectCache"; + private static final String allowInsecureRegistries = "jib.allowInsecureRegistries"; + private static final String extraDirectory = "jib.extraDirectory"; + private static final String skip = "jib.skip"; + } + /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ public static class AuthConfiguration implements AuthProperty { @@ -152,23 +182,28 @@ public static class ContainerParameters { @Parameter private FromConfiguration from = new FromConfiguration(); - @Parameter(property = "image") - private ToConfiguration to = new ToConfiguration(); + @Parameter private ToConfiguration to = new ToConfiguration(); @Parameter private ContainerParameters container = new ContainerParameters(); - @Parameter(defaultValue = "false", required = true) + @Parameter(defaultValue = "false", required = true, property = PropertyNames.useOnlyProjectCache) private boolean useOnlyProjectCache; - @Parameter(defaultValue = "false", required = true) + @Parameter( + defaultValue = "false", + required = true, + property = PropertyNames.allowInsecureRegistries) private boolean allowInsecureRegistries; // this parameter is cloned in FilesMojo @Nullable - @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) + @Parameter( + defaultValue = "${project.basedir}/src/main/jib", + required = true, + property = PropertyNames.extraDirectory) private File extraDirectory; - @Parameter(defaultValue = "false", property = "jib.skip") + @Parameter(defaultValue = "false", property = PropertyNames.skip) private boolean skip; @Nullable @Component protected SettingsDecrypter settingsDecrypter; @@ -187,76 +222,209 @@ MavenProject getProject() { return Preconditions.checkNotNull(project); } + /** + * Gets the base image reference. + * + * @return the configured base image reference + */ String getBaseImage() { + if (System.getProperty(PropertyNames.fromImage) != null) { + return System.getProperty(PropertyNames.fromImage); + } return Preconditions.checkNotNull(Preconditions.checkNotNull(from).image); } + /** + * Gets the base image credential helper. + * + * @return the configured base image credential helper name + */ @Nullable String getBaseImageCredentialHelperName() { + if (System.getProperty(PropertyNames.fromCredHelper) != null) { + return System.getProperty(PropertyNames.fromCredHelper); + } return Preconditions.checkNotNull(from).credHelper; } AuthConfiguration getBaseImageAuth() { + // System properties are handled in ConfigurationPropertyValidator return from.auth; } + /** + * Gets the target image reference. + * + * @return the configured target image reference + */ @Nullable String getTargetImage() { + if (System.getProperty(PropertyNames.toImage) != null) { + return System.getProperty(PropertyNames.toImage); + } + if (System.getProperty(PropertyNames.toImageAlternate) != null) { + return System.getProperty(PropertyNames.toImageAlternate); + } return to.image; } + /** + * Gets the additional target image tags. + * + * @return the configured extra tags. + */ Set getTargetImageAdditionalTags() { + if (System.getProperty(PropertyNames.toTags) != null) { + return ImmutableSet.copyOf( + ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.toTags))); + } return new HashSet<>(to.tags); } + /** + * Gets the target image credential helper. + * + * @return the configured target image credential helper name + */ @Nullable String getTargetImageCredentialHelperName() { + if (System.getProperty(PropertyNames.toCredHelper) != null) { + return System.getProperty(PropertyNames.toCredHelper); + } return Preconditions.checkNotNull(to).credHelper; } AuthConfiguration getTargetImageAuth() { + // System properties are handled in ConfigurationPropertyValidator return to.auth; } + /** + * Gets whether or not to use the current timestamp for the container build. + * + * @return {@code true} if the build should use the current timestamp, {@code false} if not + */ boolean getUseCurrentTimestamp() { + if (System.getProperty(PropertyNames.containerUseCurrentTimestamp) != null) { + return Boolean.getBoolean(PropertyNames.containerUseCurrentTimestamp); + } return container.useCurrentTimestamp; } + /** + * Gets the configured entrypoint. + * + * @return the configured entrypoint + */ List getEntrypoint() { + if (System.getProperty(PropertyNames.containerEntrypoint) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.containerEntrypoint)); + } return container.entrypoint; } + /** + * Gets the configured jvm flags. + * + * @return the configured jvm flags + */ List getJvmFlags() { + if (System.getProperty(PropertyNames.containerJvmFlags) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.containerJvmFlags)); + } return container.jvmFlags; } + /** + * Gets the configured environment variables. + * + * @return the configured environment variables + */ @Nullable Map getEnvironment() { + if (System.getProperty(PropertyNames.containerEnvironment) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.containerEnvironment)); + } return container.environment; } + /** + * Gets the name of the main class. + * + * @return the configured main class name + */ @Nullable String getMainClass() { + if (System.getProperty(PropertyNames.containerMainClass) != null) { + return System.getProperty(PropertyNames.containerMainClass); + } return container.mainClass; } + /** + * Gets the configured main arguments. + * + * @return the configured main arguments + */ List getArgs() { + if (System.getProperty(PropertyNames.containerArgs) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.containerArgs)); + } return container.args; } + /** + * Gets the configured exposed ports. + * + * @return the configured exposed ports + */ List getExposedPorts() { + if (System.getProperty(PropertyNames.containerPorts) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.containerPorts)); + } return container.ports; } + /** + * Gets the configured labels. + * + * @return the configured labels + */ Map getLabels() { + if (System.getProperty(PropertyNames.containerLabels) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.containerLabels)); + } return container.labels; } + /** + * Gets the configured app root directory. + * + * @return the configured app root directory + */ String getAppRoot() { + if (System.getProperty(PropertyNames.containerAppRoot) != null) { + return System.getProperty(PropertyNames.containerAppRoot); + } return container.appRoot; } + /** + * Gets the configured container image format. + * + * @return the configured container image format + */ String getFormat() { + if (System.getProperty(PropertyNames.containerFormat) != null) { + return System.getProperty(PropertyNames.containerFormat); + } return Preconditions.checkNotNull(container.format); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 9c416f6471..cf04a5f45f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -110,8 +110,8 @@ static PluginConfigurationProcessor processCommonConfiguration( Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( projectProperties.getEventDispatcher(), - "jib.from.auth.username", - "jib.from.auth.password", + JibPluginConfiguration.PropertyNames.fromAuthUsername, + JibPluginConfiguration.PropertyNames.fromAuthPassword, jibPluginConfiguration.getBaseImageAuth()); if (optionalFromCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index b7d00fc71e..b739a4fadd 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -16,6 +16,10 @@ package com.google.cloud.tools.jib.maven; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,8 +29,28 @@ public class JibPluginConfigurationTest { private JibPluginConfiguration testPluginConfiguration; + private static void clearProperties() { + System.clearProperty("jib.from.image"); + System.clearProperty("jib.from.credHelper"); + System.clearProperty("image"); + System.clearProperty("jib.to.image"); + System.clearProperty("jib.to.tags"); + System.clearProperty("jib.to.credHelper"); + System.clearProperty("jib.container.appRoot"); + System.clearProperty("jib.container.args"); + System.clearProperty("jib.container.entrypoint"); + System.clearProperty("jib.container.environment"); + System.clearProperty("jib.container.format"); + System.clearProperty("jib.container.jvmFlags"); + System.clearProperty("jib.container.labels"); + System.clearProperty("jib.container.mainClass"); + System.clearProperty("jib.container.ports"); + System.clearProperty("jib.container.useCurrentTimestamp"); + } + @Before public void setup() { + clearProperties(); testPluginConfiguration = new JibPluginConfiguration() { @Override @@ -34,6 +58,11 @@ public void execute() {} }; } + @After + public void teardown() { + clearProperties(); + } + @Test public void testAuthDefaults() { Assert.assertEquals( @@ -50,4 +79,51 @@ public void testAuthDefaults() { testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); Assert.assertEquals("/app", testPluginConfiguration.getAppRoot()); } + + @Test + public void testSystemProperties() { + System.setProperty("jib.from.image", "fromImage"); + Assert.assertEquals("fromImage", testPluginConfiguration.getBaseImage()); + System.setProperty("jib.from.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testPluginConfiguration.getBaseImageCredentialHelperName()); + + System.setProperty("image", "toImage"); + Assert.assertEquals("toImage", testPluginConfiguration.getTargetImage()); + System.clearProperty("image"); + System.setProperty("jib.to.image", "toImage2"); + Assert.assertEquals("toImage2", testPluginConfiguration.getTargetImage()); + System.setProperty("jib.to.tags", "tag1,tag2,tag3"); + Assert.assertEquals( + ImmutableSet.of("tag1", "tag2", "tag3"), + testPluginConfiguration.getTargetImageAdditionalTags()); + System.setProperty("jib.to.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testPluginConfiguration.getTargetImageCredentialHelperName()); + + System.setProperty("jib.container.appRoot", "appRoot"); + Assert.assertEquals("appRoot", testPluginConfiguration.getAppRoot()); + System.setProperty("jib.container.args", "arg1,arg2,arg3"); + Assert.assertEquals( + ImmutableList.of("arg1", "arg2", "arg3"), testPluginConfiguration.getArgs()); + System.setProperty("jib.container.entrypoint", "entry1,entry2,entry3"); + Assert.assertEquals( + ImmutableList.of("entry1", "entry2", "entry3"), testPluginConfiguration.getEntrypoint()); + System.setProperty("jib.container.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), testPluginConfiguration.getEnvironment()); + System.setProperty("jib.container.format", "format"); + Assert.assertEquals("format", testPluginConfiguration.getFormat()); + System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); + Assert.assertEquals( + ImmutableList.of("flag1", "flag2", "flag3"), testPluginConfiguration.getJvmFlags()); + System.setProperty("jib.container.labels", "label1=val1,label2=val2"); + Assert.assertEquals( + ImmutableMap.of("label1", "val1", "label2", "val2"), testPluginConfiguration.getLabels()); + System.setProperty("jib.container.mainClass", "main"); + Assert.assertEquals("main", testPluginConfiguration.getMainClass()); + System.setProperty("jib.container.ports", "port1,port2,port3"); + Assert.assertEquals( + ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); + System.setProperty("jib.container.useCurrentTimestamp", "true"); + Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index b5333d18c5..e54453a024 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -23,12 +23,23 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nullable; /** Validator for plugin configuration parameters and system properties. */ public class ConfigurationPropertyValidator { + /** Matches key-value pairs in the form of "key=value" */ + private static final Pattern ENVIRONMENT_PATTERN = Pattern.compile("(?[^=]+)=(?.*)"); + /** * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. @@ -128,5 +139,50 @@ public static ImageReference getGeneratedTargetDockerTag( } } + /** + * Parses a string in the form of "key1=value1,key2=value2,..." into a map. + * + * @param property the map string to parse, with entries separated by "," and key-value pairs + * separated by "=" + * @return the map of parsed values + */ + public static Map parseMapProperty(String property) { + Map result = new HashMap<>(); + + // Split on non-escaped commas + List entries = parseListProperty(property); + for (String entry : entries) { + Matcher matcher = ENVIRONMENT_PATTERN.matcher(entry); + if (!matcher.matches()) { + throw new IllegalArgumentException("'" + entry + "' is not a valid key-value pair"); + } + result.put(matcher.group("name"), matcher.group("value")); + } + return ImmutableMap.copyOf(result); + } + + /** + * Parses a comma-separated string into a list. Ignores commas escaped with "\". + * + * @param property the comma-separated string + * @return the list of parsed values + */ + public static List parseListProperty(String property) { + List items = new ArrayList<>(); + int startIndex = 0; + for (int endIndex = 0; endIndex < property.length(); endIndex++) { + if (property.charAt(endIndex) == ',') { + // Split on non-escaped comma + items.add(property.substring(startIndex, endIndex)); + startIndex = endIndex + 1; + } else if (property.charAt(endIndex) == '\\') { + // Found a backslash, ignore next character + endIndex++; + } + } + items.add(property.substring(startIndex)); + return ImmutableList.copyOf(items); + } + private ConfigurationPropertyValidator() {} } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index fb0284b85a..e539f21e73 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -144,4 +146,30 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept } catch (InvalidImageReferenceException ignored) { } } + + @Test + public void testParseListProperty() { + Assert.assertEquals( + ImmutableList.of("abc"), ConfigurationPropertyValidator.parseListProperty("abc")); + Assert.assertEquals( + ImmutableList.of("abcd", "efg\\,hi\\\\", "", "\\jkl\\,", "\\\\\\,mnop", ""), + ConfigurationPropertyValidator.parseListProperty( + "abcd,efg\\,hi\\\\,,\\jkl\\,,\\\\\\,mnop,")); + Assert.assertEquals(ImmutableList.of(""), ConfigurationPropertyValidator.parseListProperty("")); + } + + @Test + public void testParseMapProperty() { + Assert.assertEquals( + ImmutableMap.of("abc", "def"), ConfigurationPropertyValidator.parseMapProperty("abc=def")); + Assert.assertEquals( + ImmutableMap.of("abc", "def", "gh\\,i", "j\\\\\\,kl", "mno", "", "pqr", "stu"), + ConfigurationPropertyValidator.parseMapProperty("abc=def,gh\\,i=j\\\\\\,kl,mno=,pqr=stu")); + try { + ConfigurationPropertyValidator.parseMapProperty("not valid"); + Assert.fail(); + } catch (IllegalArgumentException ignored) { + // pass + } + } } From bc818c30fef4b3e290e8d2978f228932ff97746f Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 2 Oct 2018 08:48:12 -0700 Subject: [PATCH 0262/2020] Makes Docker executable configurable. (#1084) --- .../builder/BuildStepsIntegrationTest.java | 5 ++- .../cloud/tools/jib/builder/BuildSteps.java | 7 ++- .../jib/builder/steps/LoadDockerStep.java | 4 +- .../tools/jib/builder/steps/StepsRunner.java | 4 +- .../cloud/tools/jib/docker/DockerClient.java | 44 ++++++++++++++----- .../tools/jib/gradle/BuildDockerTask.java | 7 ++- .../tools/jib/maven/BuildDockerMojo.java | 7 ++- .../jib/plugins/common/BuildStepsRunner.java | 7 ++- 8 files changed, 63 insertions(+), 22 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index a37e956b58..ae126d1730 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; @@ -215,7 +216,7 @@ public void testSteps_forBuildToDockerDaemon() ImageReference.of("gcr.io", "distroless/java", "latest"), ImageReference.of(null, imageReference, null)) .build(); - BuildSteps.forBuildToDockerDaemon(buildConfiguration).run(); + BuildSteps.forBuildToDockerDaemon(DockerClient.newClient(), buildConfiguration).run(); assertDockerInspect(imageReference); Assert.assertEquals( @@ -232,7 +233,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() ImageReference.of(null, imageReference, null)) .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) .build(); - BuildSteps.forBuildToDockerDaemon(buildConfiguration).run(); + BuildSteps.forBuildToDockerDaemon(DockerClient.newClient(), buildConfiguration).run(); assertDockerInspect(imageReference); Assert.assertEquals( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 517e5636de..da90882128 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; @@ -75,10 +76,12 @@ public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfig /** * All the steps to build to Docker daemon * + * @param dockerClient the {@link DockerClient} for running {@code docker} commands * @param buildConfiguration the configuration parameters for the build * @return a new {@link BuildSteps} for building to a Docker daemon */ - public static BuildSteps forBuildToDockerDaemon(BuildConfiguration buildConfiguration) { + public static BuildSteps forBuildToDockerDaemon( + DockerClient dockerClient, BuildConfiguration buildConfiguration) { return new BuildSteps( DESCRIPTION_FOR_DOCKER_DAEMON, buildConfiguration, @@ -89,7 +92,7 @@ public static BuildSteps forBuildToDockerDaemon(BuildConfiguration buildConfigur .runBuildAndCacheApplicationLayerSteps() .runBuildImageStep() .runFinalizingBuildStep() - .runLoadDockerStep() + .runLoadDockerStep(dockerClient) .waitOnLoadDockerStep()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 2030cc6be0..5ff9f12db0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -42,6 +42,7 @@ /** Adds image layers to a tarball and loads into Docker daemon. */ class LoadDockerStep implements AsyncStep, Callable { + private final DockerClient dockerClient; private final BuildConfiguration buildConfiguration; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; @@ -52,11 +53,13 @@ class LoadDockerStep implements AsyncStep, Callable buildAndCacheApplicationLayerSteps, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; + this.dockerClient = dockerClient; this.buildConfiguration = buildConfiguration; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -100,7 +103,6 @@ private DescriptorDigest afterPushBaseImageLayerFuturesFuture() buildConfiguration .getEventDispatcher() .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); - DockerClient dockerClient = new DockerClient(); dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index dc7401e2e5..63e73baaa6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.base.Preconditions; @@ -179,10 +180,11 @@ public StepsRunner runPushImageStep() { return this; } - public StepsRunner runLoadDockerStep() { + public StepsRunner runLoadDockerStep(DockerClient dockerClient) { loadDockerStep = new LoadDockerStep( listeningExecutorService, + dockerClient, buildConfiguration, Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 7673de6471..9aea451cb2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -24,6 +24,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -32,24 +33,47 @@ /** Calls out to the {@code docker} CLI. */ public class DockerClient { + private static final String DEFAULT_DOCKER_CLIENT = "docker"; + + /** + * Instantiates with the default {@code docker} executable. + * + * @return a new {@link DockerClient} + */ + public static DockerClient newClient() { + return new DockerClient(defaultProcessBuilderFactory(DEFAULT_DOCKER_CLIENT)); + } + + /** + * Instantiates with a custom {@code docker} executable. + * + * @param dockerExecutable path to {@code docker} + * @return a new {@link DockerClient} + */ + public static DockerClient newClient(Path dockerExecutable) { + return new DockerClient(defaultProcessBuilderFactory(dockerExecutable.toString())); + } + /** - * @param dockerSubCommand the subcommand to run after {@code docker} + * Gets a function that takes a {@code docker} subcommand and gives back a {@link ProcessBuilder} + * for that {@code docker} command. + * + * @param dockerExecutable path to {@code docker} * @return the default {@link ProcessBuilder} factory for running a {@code docker} subcommand */ - private static ProcessBuilder defaultProcessBuilder(List dockerSubCommand) { - List dockerCommand = new ArrayList<>(1 + dockerSubCommand.size()); - dockerCommand.add("docker"); - dockerCommand.addAll(dockerSubCommand); - return new ProcessBuilder(dockerCommand); + private static Function, ProcessBuilder> defaultProcessBuilderFactory( + String dockerExecutable) { + return dockerSubCommand -> { + List dockerCommand = new ArrayList<>(1 + dockerSubCommand.size()); + dockerCommand.add(dockerExecutable); + dockerCommand.addAll(dockerSubCommand); + return new ProcessBuilder(dockerCommand); + }; } /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ private final Function, ProcessBuilder> processBuilderFactory; - public DockerClient() { - this(DockerClient::defaultProcessBuilder); - } - @VisibleForTesting DockerClient(Function, ProcessBuilder> processBuilderFactory) { this.processBuilderFactory = processBuilderFactory; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 0c251a5e28..a6749e2313 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -40,6 +40,8 @@ public class BuildDockerTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; + private static final DockerClient DOCKER_CLIENT = DockerClient.newClient(); + @Nullable private JibExtension jibExtension; /** @@ -67,7 +69,7 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildDocker() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException { - if (!new DockerClient().isDockerInstalled()) { + if (!DOCKER_CLIENT.isDockerInstalled()) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } @@ -113,7 +115,8 @@ public void buildDocker() .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); - BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); + BuildStepsRunner.forBuildToDockerDaemon(DOCKER_CLIENT, buildConfiguration) + .build(helpfulSuggestions); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index e99a1f0cce..005f56d6bf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -42,6 +42,8 @@ public class BuildDockerMojo extends JibPluginConfiguration { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; + private static final DockerClient DOCKER_CLIENT = DockerClient.newClient(); + @Override public void execute() throws MojoExecutionException { if (isSkipped()) { @@ -53,7 +55,7 @@ public void execute() throws MojoExecutionException { return; } - if (!new DockerClient().isDockerInstalled()) { + if (!DOCKER_CLIENT.isDockerInstalled()) { throw new MojoExecutionException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } @@ -97,7 +99,8 @@ public void execute() throws MojoExecutionException { .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) .build(); - BuildStepsRunner.forBuildToDockerDaemon(buildConfiguration).build(helpfulSuggestions); + BuildStepsRunner.forBuildToDockerDaemon(DOCKER_CLIENT, buildConfiguration) + .build(helpfulSuggestions); getLog().info(""); } catch (InvalidImageReferenceException | IOException ex) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 93b92fb40f..9cad3d09a0 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; @@ -91,12 +92,14 @@ public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfigurati /** * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. * + * @param dockerClient the {@link DockerClient} for running {@code docker} commands * @param buildConfiguration the configuration parameters for the build * @return a {@link BuildStepsRunner} for building to a Docker daemon */ - public static BuildStepsRunner forBuildToDockerDaemon(BuildConfiguration buildConfiguration) { + public static BuildStepsRunner forBuildToDockerDaemon( + DockerClient dockerClient, BuildConfiguration buildConfiguration) { return new BuildStepsRunner( - BuildSteps.forBuildToDockerDaemon(buildConfiguration), + BuildSteps.forBuildToDockerDaemon(dockerClient, buildConfiguration), buildMessageWithTargetImageReferences( buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), buildMessageWithTargetImageReferences( From 1208bc27566b52d0641d1f58ecb962bfbfe6233e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 2 Oct 2018 16:08:39 -0400 Subject: [PATCH 0263/2020] Cleanup JibExtension (#1086) --- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../tools/jib/gradle/DockerContextTask.java | 9 ++- .../cloud/tools/jib/gradle/JibExtension.java | 73 +------------------ .../gradle/PluginConfigurationProcessor.java | 12 +-- .../jib/gradle/DockerContextTaskTest.java | 4 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 6 +- .../PluginConfigurationProcessorTest.java | 2 +- 9 files changed, 26 insertions(+), 88 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index a6749e2313..1042708b67 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -86,7 +86,7 @@ public void buildDocker() ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTargetImage(), + jibExtension.getTo().getImage(), gradleProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion().toString(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 472ccd7ce7..2a93c57753 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -78,7 +78,7 @@ public void buildImage() GradleProjectProperties.getForProject( getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); - if (Strings.isNullOrEmpty(jibExtension.getTargetImage())) { + if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( HelpfulSuggestions.forToNotConfigured( "Missing target image parameter", @@ -87,7 +87,7 @@ public void buildImage() "gradle jib --image ")); } - ImageReference targetImage = ImageReference.parse(jibExtension.getTargetImage()); + ImageReference targetImage = ImageReference.parse(jibExtension.getTo().getImage()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index bd7d0f58bd..0e65d37b4c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -109,7 +109,7 @@ public void buildTar() new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTargetImage(), + jibExtension.getTo().getImage(), gradleProjectProperties.getEventDispatcher(), getProject().getName(), getProject().getVersion().toString(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index ce8b860bb3..8020938e30 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -101,6 +101,7 @@ public void setTargetDir(String targetDir) { @TaskAction public void generateDockerContext() { Preconditions.checkNotNull(jibExtension); + Preconditions.checkNotNull(jibExtension.getFrom().getImage()); JibSystemProperties.checkHttpTimeoutProperty(); // TODO: Instead of disabling logging, have authentication credentials be provided @@ -119,14 +120,14 @@ public void generateDockerContext() { try { // Validate port input, but don't save the output because we don't want the ranges expanded // here. - ExposedPortsParser.parse(jibExtension.getExposedPorts()); + ExposedPortsParser.parse(jibExtension.getContainer().getPorts()); new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) - .setBaseImage(jibExtension.getBaseImage()) + .setBaseImage(jibExtension.getFrom().getImage()) .setEntrypoint(entrypoint) .setJavaArguments(jibExtension.getContainer().getArgs()) - .setExposedPorts(jibExtension.getExposedPorts()) - .setLabels(jibExtension.getLabels()) + .setExposedPorts(jibExtension.getContainer().getPorts()) + .setLabels(jibExtension.getContainer().getLabels()) .generate(Paths.get(targetDir)); getLogger().lifecycle("Created Docker context at " + targetDir); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 759caaf923..71d5acb658 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -16,18 +16,11 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.common.base.Preconditions; import java.io.File; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; @@ -76,10 +69,6 @@ private static Path resolveDefaultExtraDirectory(Path projectDirectory) { private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; private final Property extraDirectory; - private final ListProperty jvmFlags; - private final Property mainClass; - private final ListProperty args; - private final Property format; public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); @@ -88,18 +77,11 @@ public JibExtension(Project project) { to = objectFactory.newInstance(TargetImageParameters.class, "jib.to"); container = objectFactory.newInstance(ContainerParameters.class); - jvmFlags = objectFactory.listProperty(String.class); - mainClass = objectFactory.property(String.class); - args = objectFactory.listProperty(String.class); - format = objectFactory.property(ImageFormat.class); - useOnlyProjectCache = objectFactory.property(Boolean.class); allowInsecureRegistries = objectFactory.property(Boolean.class); extraDirectory = objectFactory.property(Path.class); // Sets defaults. - jvmFlags.set(Collections.emptyList()); - args.set(Collections.emptyList()); useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath())); @@ -117,26 +99,6 @@ public void container(Action action) { action.execute(container); } - public void setJvmFlags(List jvmFlags) { - this.jvmFlags.set(jvmFlags); - } - - public void setMainClass(String mainClass) { - this.mainClass.set(mainClass); - } - - public void setArgs(List args) { - this.args.set(args); - } - - public void setFormat(ImageFormat format) { - this.format.set(format); - } - - public void setUseOnlyProjectCache(boolean useOnlyProjectCache) { - this.useOnlyProjectCache.set(useOnlyProjectCache); - } - public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } @@ -145,15 +107,8 @@ public void setExtraDirectory(File extraDirectory) { this.extraDirectory.set(extraDirectory.toPath()); } - @Internal - String getBaseImage() { - return Preconditions.checkNotNull(from.getImage()); - } - - @Internal - @Nullable - String getTargetImage() { - return to.getImage(); + void setUseOnlyProjectCache(boolean useOnlyProjectCache) { + this.useOnlyProjectCache.set(useOnlyProjectCache); } @Nested @@ -174,30 +129,6 @@ public ContainerParameters getContainer() { return container; } - @Internal - @Optional - Map getEnvironment() { - return container.getEnvironment(); - } - - @Internal - @Optional - List getExposedPorts() { - return container.getPorts(); - } - - @Internal - @Optional - Map getLabels() { - return container.getLabels(); - } - - @Internal - @Optional - boolean getUseCurrentTimestamp() { - return container.getUseCurrentTimestamp(); - } - @Input @Optional boolean getUseOnlyProjectCache() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 1f4408c46a..8ebbbaf0d8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.common.base.Preconditions; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -98,7 +99,8 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided disableHttpLogging(); - ImageReference baseImage = ImageReference.parse(jibExtension.getBaseImage()); + ImageReference baseImage = + ImageReference.parse(Preconditions.checkNotNull(jibExtension.getFrom().getImage())); if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { logger.warn( @@ -127,11 +129,11 @@ static PluginConfigurationProcessor processCommonConfiguration( ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() .setEntrypoint(entrypoint) - .setEnvironment(jibExtension.getEnvironment()) + .setEnvironment(jibExtension.getContainer().getEnvironment()) .setProgramArguments(jibExtension.getContainer().getArgs()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getExposedPorts())) - .setLabels(jibExtension.getLabels()); - if (jibExtension.getUseCurrentTimestamp()) { + .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) + .setLabels(jibExtension.getContainer().getLabels()); + if (jibExtension.getContainer().getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); containerConfigurationBuilder.setCreationTime(Instant.now()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 729914305f..fb9fbc15b1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -40,6 +40,7 @@ public class DockerContextTaskTest { @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); @Mock private ContainerParameters containerParameters; + @Mock private BaseImageParameters baseImageParameters; private DockerContextTask task; private Project project; @@ -54,7 +55,8 @@ public void setUp() throws IOException { Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); Mockito.when(jibExtension.getExtraDirectoryPath()).thenReturn(projectRoot.getRoot().toPath()); Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); - Mockito.when(jibExtension.getBaseImage()).thenReturn("base image"); + Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); + Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); Mockito.when(containerParameters.getAppRoot()).thenReturn("/app"); project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 07cbd04a1d..a142c2f749 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -205,7 +205,8 @@ public void testWebAppProject() { JibPlugin.DEFAULT_WAR_FROM_IMAGE, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() - .getBaseImage()); + .getFrom() + .getImage()); Assert.assertEquals( JibPlugin.DEFAULT_WEB_APP_ROOT, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) @@ -225,7 +226,8 @@ public void testNonWebAppProject() { JibPlugin.DEFAULT_FROM_IMAGE, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() - .getBaseImage()); + .getFrom() + .getImage()); Assert.assertEquals( JavaLayerConfigurations.DEFAULT_APP_ROOT, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index cfafedd806..911b1aa8c2 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -45,7 +45,7 @@ public class PluginConfigurationProcessorTest { @Before public void setUp() { - Mockito.doReturn("gcr.io/distroless/java").when(mockJibExtension).getBaseImage(); + Mockito.doReturn("gcr.io/distroless/java").when(mockBaseImageParameters).getImage(); Mockito.doReturn(mockBaseImageParameters).when(mockJibExtension).getFrom(); Mockito.doReturn(new AuthParameters("mock")).when(mockBaseImageParameters).getAuth(); Mockito.doReturn(mockContainerParameters).when(mockJibExtension).getContainer(); From 5d65371319bf7b4f606b1e918f00a890c91d9462 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 2 Oct 2018 18:41:13 -0400 Subject: [PATCH 0264/2020] Fix to/from closure type in JibExtension (#1061) * Fix to/from closure type in JibExtension * Remove interface --- .../tools/jib/gradle/BaseImageParameters.java | 19 ++++--- .../tools/jib/gradle/ImageParameters.java | 53 ------------------- .../cloud/tools/jib/gradle/JibExtension.java | 4 +- .../jib/gradle/TargetImageParameters.java | 17 +++--- 4 files changed, 22 insertions(+), 71 deletions(-) delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java index 6956633a1f..34c2ce747b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -20,9 +20,12 @@ import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; -/** {@link ImageParameters} that configure the base image. */ -public class BaseImageParameters implements ImageParameters { +/** Object in {@link JibExtension} that configures the base image. */ +public class BaseImageParameters { private final AuthParameters auth; @@ -34,34 +37,34 @@ public BaseImageParameters(ObjectFactory objectFactory, String imageDescriptor) auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); } + @Input @Nullable - @Override + @Optional public String getImage() { return image; } - @Override public void setImage(String image) { this.image = image; } + @Input @Nullable - @Override + @Optional public String getCredHelper() { return credHelper; } - @Override public void setCredHelper(String credHelper) { this.credHelper = credHelper; } - @Override + @Nested + @Optional public AuthParameters getAuth() { return auth; } - @Override public void auth(Action action) { action.execute(auth); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java deleted file mode 100644 index 1f97ef5d7a..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ImageParameters.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import javax.annotation.Nullable; -import org.gradle.api.Action; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Nested; -import org.gradle.api.tasks.Optional; - -/** - * A bean that configures an image to be used in the build steps. This is configurable with Groovy - * closures and can be validated when used as a task input. - * - *

{@code image} (required) is the image reference and {@code credHelper} (optional) is the name - * (after {@code docker-credential} of the credential helper for accessing the {@code image}. - */ -interface ImageParameters { - - @Input - @Nullable - @Optional - String getImage(); - - void setImage(String image); - - @Input - @Nullable - @Optional - String getCredHelper(); - - void setCredHelper(String credHelper); - - @Nested - @Optional - AuthParameters getAuth(); - - void auth(Action action); -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 71d5acb658..6183899b6d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -87,11 +87,11 @@ public JibExtension(Project project) { extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath())); } - public void from(Action action) { + public void from(Action action) { action.execute(from); } - public void to(Action action) { + public void to(Action action) { action.execute(to); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java index cf3f3e91b6..f03acc2148 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -23,10 +23,11 @@ import org.gradle.api.Action; import org.gradle.api.model.ObjectFactory; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; -/** {@link ImageParameters} that configure the target image. */ -public class TargetImageParameters implements ImageParameters { +/** Object in {@link JibExtension} that configures the target image. */ +public class TargetImageParameters { private final AuthParameters auth; @@ -39,13 +40,13 @@ public TargetImageParameters(ObjectFactory objectFactory, String imageDescriptor auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); } + @Input @Nullable - @Override + @Optional public String getImage() { return image; } - @Override public void setImage(String image) { this.image = image; } @@ -60,23 +61,23 @@ public void setTags(Set tags) { this.tags = tags; } + @Input @Nullable - @Override + @Optional public String getCredHelper() { return credHelper; } - @Override public void setCredHelper(String credHelper) { this.credHelper = credHelper; } - @Override + @Nested + @Optional public AuthParameters getAuth() { return auth; } - @Override public void auth(Action action) { action.execute(auth); } From 605b264cd40113009741545fccdb9273317453ca Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 3 Oct 2018 10:02:41 -0400 Subject: [PATCH 0265/2020] Add system properties for gradle config (#1087) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../tools/jib/gradle/BaseImageParameters.java | 8 ++ .../tools/jib/gradle/BuildImageTask.java | 5 +- .../tools/jib/gradle/ContainerParameters.java | 39 +++++++ .../cloud/tools/jib/gradle/JibExtension.java | 14 +++ .../gradle/PluginConfigurationProcessor.java | 5 +- .../jib/gradle/TargetImageParameters.java | 15 +++ .../tools/jib/gradle/JibExtensionTest.java | 74 +++++++++++++ .../cloud/tools/jib/maven/BuildImageMojo.java | 5 +- .../jib/maven/JibPluginConfiguration.java | 104 +++++++----------- .../maven/PluginConfigurationProcessor.java | 5 +- .../jib/plugins/common/PropertyNames.java | 45 ++++++++ 12 files changed, 249 insertions(+), 72 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c9445f9889..173f46813d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Properties for each configuration parameter, allowing any parameter to be set via commandline ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) + ### Changed - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java index 34c2ce747b..8b8caa62ad 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import javax.annotation.Nullable; import javax.inject.Inject; import org.gradle.api.Action; @@ -41,6 +42,9 @@ public BaseImageParameters(ObjectFactory objectFactory, String imageDescriptor) @Nullable @Optional public String getImage() { + if (System.getProperty(PropertyNames.FROM_IMAGE) != null) { + return System.getProperty(PropertyNames.FROM_IMAGE); + } return image; } @@ -52,6 +56,9 @@ public void setImage(String image) { @Nullable @Optional public String getCredHelper() { + if (System.getProperty(PropertyNames.FROM_CRED_HELPER) != null) { + return System.getProperty(PropertyNames.FROM_CRED_HELPER); + } return credHelper; } @@ -62,6 +69,7 @@ public void setCredHelper(String credHelper) { @Nested @Optional public AuthParameters getAuth() { + // System properties are handled in ConfigurationPropertyValidator return auth; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 2a93c57753..f3ad912051 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; @@ -96,8 +97,8 @@ public void buildImage() Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( gradleProjectProperties.getEventDispatcher(), - "jib.to.auth.username", - "jib.to.auth.password", + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, jibExtension.getTo().getAuth()); optionalToCredential.ifPresent( toCredential -> diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 57b7e602c4..7cc1a1a685 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.util.Collections; import java.util.List; @@ -46,6 +48,9 @@ public class ContainerParameters { @Input @Optional public boolean getUseCurrentTimestamp() { + if (System.getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP) != null) { + return Boolean.getBoolean(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); + } return useCurrentTimestamp; } @@ -56,6 +61,10 @@ public void setUseCurrentTimestamp(boolean useCurrentTimestamp) { @Input @Optional public List getEntrypoint() { + if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT)); + } return entrypoint; } @@ -66,6 +75,10 @@ public void setEntrypoint(List entrypoint) { @Input @Optional public List getJvmFlags() { + if (System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS)); + } return jvmFlags; } @@ -76,6 +89,10 @@ public void setJvmFlags(List jvmFlags) { @Input @Optional public Map getEnvironment() { + if (System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT)); + } return environment; } @@ -87,6 +104,9 @@ public void setEnvironment(Map environment) { @Nullable @Optional public String getMainClass() { + if (System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS) != null) { + return System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS); + } return mainClass; } @@ -97,6 +117,10 @@ public void setMainClass(String mainClass) { @Input @Optional public List getArgs() { + if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_ARGS)); + } return args; } @@ -107,6 +131,10 @@ public void setArgs(List args) { @Input @Optional public Class getFormat() { + if (System.getProperty(PropertyNames.CONTAINER_FORMAT) != null) { + return ImageFormat.valueOf(System.getProperty(PropertyNames.CONTAINER_FORMAT)) + .getManifestTemplateClass(); + } return Preconditions.checkNotNull(format).getManifestTemplateClass(); } @@ -117,6 +145,10 @@ public void setFormat(ImageFormat format) { @Input @Optional public List getPorts() { + if (System.getProperty(PropertyNames.CONTAINER_PORTS) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_PORTS)); + } return ports; } @@ -127,6 +159,10 @@ public void setPorts(List ports) { @Input @Optional public Map getLabels() { + if (System.getProperty(PropertyNames.CONTAINER_LABELS) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.CONTAINER_LABELS)); + } return labels; } @@ -137,6 +173,9 @@ public void setLabels(Map labels) { @Input @Optional public String getAppRoot() { + if (System.getProperty(PropertyNames.CONTAINER_APP_ROOT) != null) { + return System.getProperty(PropertyNames.CONTAINER_APP_ROOT); + } return appRoot; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 6183899b6d..8d37d461c9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; @@ -132,12 +134,18 @@ public ContainerParameters getContainer() { @Input @Optional boolean getUseOnlyProjectCache() { + if (System.getProperty(PropertyNames.USE_ONLY_PROJECT_CACHE) != null) { + return Boolean.getBoolean(PropertyNames.USE_ONLY_PROJECT_CACHE); + } return useOnlyProjectCache.get(); } @Input @Optional boolean getAllowInsecureRegistries() { + if (System.getProperty(PropertyNames.ALLOW_INSECURE_REGISTRIES) != null) { + return Boolean.getBoolean(PropertyNames.ALLOW_INSECURE_REGISTRIES); + } return allowInsecureRegistries.get(); } @@ -145,12 +153,18 @@ boolean getAllowInsecureRegistries() { String getExtraDirectory() { // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a // String to make them go away. + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY) != null) { + return System.getProperty(PropertyNames.EXTRA_DIRECTORY); + } return extraDirectory.get().toString(); } @Internal Path getExtraDirectoryPath() { // TODO: Should inform user about nonexistent directory if using custom directory. + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY) != null) { + return Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY)); + } return extraDirectory.get(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 8ebbbaf0d8..4612c11655 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.List; @@ -113,8 +114,8 @@ static PluginConfigurationProcessor processCommonConfiguration( Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( projectProperties.getEventDispatcher(), - "jib.from.auth.username", - "jib.from.auth.password", + PropertyNames.FROM_AUTH_USERNAME, + PropertyNames.FROM_AUTH_PASSWORD, jibExtension.getFrom().getAuth()); optionalFromCredential.ifPresent( fromCredential -> diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java index f03acc2148..f4bea234e6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -16,6 +16,9 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.common.collect.ImmutableSet; import java.util.Collections; import java.util.Set; import javax.annotation.Nullable; @@ -44,6 +47,9 @@ public TargetImageParameters(ObjectFactory objectFactory, String imageDescriptor @Nullable @Optional public String getImage() { + if (System.getProperty(PropertyNames.TO_IMAGE) != null) { + return System.getProperty(PropertyNames.TO_IMAGE); + } return image; } @@ -54,6 +60,11 @@ public void setImage(String image) { @Input @Optional public Set getTags() { + if (System.getProperty(PropertyNames.TO_TAGS) != null) { + return ImmutableSet.copyOf( + ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.TO_TAGS))); + } return tags; } @@ -65,6 +76,9 @@ public void setTags(Set tags) { @Nullable @Optional public String getCredHelper() { + if (System.getProperty(PropertyNames.TO_CRED_HELPER) != null) { + return System.getProperty(PropertyNames.TO_CRED_HELPER); + } return credHelper; } @@ -75,6 +89,7 @@ public void setCredHelper(String credHelper) { @Nested @Optional public AuthParameters getAuth() { + // System properties are handled in ConfigurationPropertyValidator return auth; } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 4f7431c73d..595ca8d214 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -19,11 +19,14 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.Collections; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -33,8 +36,27 @@ public class JibExtensionTest { private JibExtension testJibExtension; + private static void clearProperties() { + System.clearProperty("jib.from.image"); + System.clearProperty("jib.from.credHelper"); + System.clearProperty("jib.to.image"); + System.clearProperty("jib.to.tags"); + System.clearProperty("jib.to.credHelper"); + System.clearProperty("jib.container.appRoot"); + System.clearProperty("jib.container.args"); + System.clearProperty("jib.container.entrypoint"); + System.clearProperty("jib.container.environment"); + System.clearProperty("jib.container.format"); + System.clearProperty("jib.container.jvmFlags"); + System.clearProperty("jib.container.labels"); + System.clearProperty("jib.container.mainClass"); + System.clearProperty("jib.container.ports"); + System.clearProperty("jib.container.useCurrentTimestamp"); + } + @Before public void setUp() { + clearProperties(); Project fakeProject = ProjectBuilder.builder().build(); testJibExtension = fakeProject @@ -42,6 +64,11 @@ public void setUp() { .create(JibPlugin.JIB_EXTENSION_NAME, JibExtension.class, fakeProject); } + @After + public void teardown() { + clearProperties(); + } + @Test public void testFrom() { Assert.assertNull(testJibExtension.getFrom().getImage()); @@ -115,6 +142,53 @@ public void testContainer() { Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); } + @Test + public void testProperties() { + System.setProperty("jib.from.image", "fromImage"); + Assert.assertEquals("fromImage", testJibExtension.getFrom().getImage()); + System.setProperty("jib.from.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testJibExtension.getFrom().getCredHelper()); + + System.setProperty("jib.to.image", "toImage"); + Assert.assertEquals("toImage", testJibExtension.getTo().getImage()); + System.setProperty("jib.to.tags", "tag1,tag2,tag3"); + Assert.assertEquals( + ImmutableSet.of("tag1", "tag2", "tag3"), testJibExtension.getTo().getTags()); + System.setProperty("jib.to.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testJibExtension.getTo().getCredHelper()); + + System.setProperty("jib.container.appRoot", "appRoot"); + Assert.assertEquals("appRoot", testJibExtension.getContainer().getAppRoot()); + System.setProperty("jib.container.args", "arg1,arg2,arg3"); + Assert.assertEquals( + ImmutableList.of("arg1", "arg2", "arg3"), testJibExtension.getContainer().getArgs()); + System.setProperty("jib.container.entrypoint", "entry1,entry2,entry3"); + Assert.assertEquals( + ImmutableList.of("entry1", "entry2", "entry3"), + testJibExtension.getContainer().getEntrypoint()); + System.setProperty("jib.container.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), + testJibExtension.getContainer().getEnvironment()); + System.setProperty("jib.container.format", "OCI"); + Assert.assertEquals( + ImageFormat.OCI.getManifestTemplateClass(), testJibExtension.getContainer().getFormat()); + System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); + Assert.assertEquals( + ImmutableList.of("flag1", "flag2", "flag3"), testJibExtension.getContainer().getJvmFlags()); + System.setProperty("jib.container.labels", "label1=val1,label2=val2"); + Assert.assertEquals( + ImmutableMap.of("label1", "val1", "label2", "val2"), + testJibExtension.getContainer().getLabels()); + System.setProperty("jib.container.mainClass", "main"); + Assert.assertEquals("main", testJibExtension.getContainer().getMainClass()); + System.setProperty("jib.container.ports", "port1,port2,port3"); + Assert.assertEquals( + ImmutableList.of("port1", "port2", "port3"), testJibExtension.getContainer().getPorts()); + System.setProperty("jib.container.useCurrentTimestamp", "true"); + Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); + } + @Test public void testUseOnlyProjectCache() { Assert.assertFalse(testJibExtension.getUseOnlyProjectCache()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index aa03d3c0b7..cbdf0dac7f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.io.IOException; @@ -99,8 +100,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( mavenProjectProperties.getEventDispatcher(), - JibPluginConfiguration.PropertyNames.toAuthUsername, - JibPluginConfiguration.PropertyNames.toAuthPassword, + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, getTargetImageAuth()); if (optionalToCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 428cd284d4..66060e914c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; @@ -41,34 +42,6 @@ /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ abstract class JibPluginConfiguration extends AbstractMojo { - /** Names of system properties used to set configuration via commandline. */ - static class PropertyNames { - private static final String fromImage = "jib.from.image"; - private static final String fromCredHelper = "jib.from.credHelper"; - static final String fromAuthUsername = "jib.from.auth.username"; - static final String fromAuthPassword = "jib.from.auth.password"; - private static final String toImage = "image"; - private static final String toImageAlternate = "jib.to.image"; - private static final String toTags = "jib.to.tags"; - private static final String toCredHelper = "jib.to.credHelper"; - static final String toAuthUsername = "jib.to.auth.username"; - static final String toAuthPassword = "jib.to.auth.password"; - private static final String containerAppRoot = "jib.container.appRoot"; - private static final String containerArgs = "jib.container.args"; - private static final String containerEntrypoint = "jib.container.entrypoint"; - private static final String containerEnvironment = "jib.container.environment"; - private static final String containerFormat = "jib.container.format"; - private static final String containerJvmFlags = "jib.container.jvmFlags"; - private static final String containerLabels = "jib.container.labels"; - private static final String containerMainClass = "jib.container.mainClass"; - private static final String containerPorts = "jib.container.ports"; - private static final String containerUseCurrentTimestamp = "jib.container.useCurrentTimestamp"; - private static final String useOnlyProjectCache = "jib.useOnlyProjectCache"; - private static final String allowInsecureRegistries = "jib.allowInsecureRegistries"; - private static final String extraDirectory = "jib.extraDirectory"; - private static final String skip = "jib.skip"; - } - /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ public static class AuthConfiguration implements AuthProperty { @@ -186,13 +159,16 @@ public static class ContainerParameters { @Parameter private ContainerParameters container = new ContainerParameters(); - @Parameter(defaultValue = "false", required = true, property = PropertyNames.useOnlyProjectCache) + @Parameter( + defaultValue = "false", + required = true, + property = PropertyNames.USE_ONLY_PROJECT_CACHE) private boolean useOnlyProjectCache; @Parameter( defaultValue = "false", required = true, - property = PropertyNames.allowInsecureRegistries) + property = PropertyNames.ALLOW_INSECURE_REGISTRIES) private boolean allowInsecureRegistries; // this parameter is cloned in FilesMojo @@ -200,10 +176,10 @@ public static class ContainerParameters { @Parameter( defaultValue = "${project.basedir}/src/main/jib", required = true, - property = PropertyNames.extraDirectory) + property = PropertyNames.EXTRA_DIRECTORY) private File extraDirectory; - @Parameter(defaultValue = "false", property = PropertyNames.skip) + @Parameter(defaultValue = "false", property = PropertyNames.SKIP) private boolean skip; @Nullable @Component protected SettingsDecrypter settingsDecrypter; @@ -228,8 +204,8 @@ MavenProject getProject() { * @return the configured base image reference */ String getBaseImage() { - if (System.getProperty(PropertyNames.fromImage) != null) { - return System.getProperty(PropertyNames.fromImage); + if (System.getProperty(PropertyNames.FROM_IMAGE) != null) { + return System.getProperty(PropertyNames.FROM_IMAGE); } return Preconditions.checkNotNull(Preconditions.checkNotNull(from).image); } @@ -241,8 +217,8 @@ String getBaseImage() { */ @Nullable String getBaseImageCredentialHelperName() { - if (System.getProperty(PropertyNames.fromCredHelper) != null) { - return System.getProperty(PropertyNames.fromCredHelper); + if (System.getProperty(PropertyNames.FROM_CRED_HELPER) != null) { + return System.getProperty(PropertyNames.FROM_CRED_HELPER); } return Preconditions.checkNotNull(from).credHelper; } @@ -259,11 +235,11 @@ AuthConfiguration getBaseImageAuth() { */ @Nullable String getTargetImage() { - if (System.getProperty(PropertyNames.toImage) != null) { - return System.getProperty(PropertyNames.toImage); + if (System.getProperty(PropertyNames.TO_IMAGE_ALTERNATE) != null) { + return System.getProperty(PropertyNames.TO_IMAGE_ALTERNATE); } - if (System.getProperty(PropertyNames.toImageAlternate) != null) { - return System.getProperty(PropertyNames.toImageAlternate); + if (System.getProperty(PropertyNames.TO_IMAGE) != null) { + return System.getProperty(PropertyNames.TO_IMAGE); } return to.image; } @@ -274,10 +250,10 @@ String getTargetImage() { * @return the configured extra tags. */ Set getTargetImageAdditionalTags() { - if (System.getProperty(PropertyNames.toTags) != null) { + if (System.getProperty(PropertyNames.TO_TAGS) != null) { return ImmutableSet.copyOf( ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.toTags))); + System.getProperty(PropertyNames.TO_TAGS))); } return new HashSet<>(to.tags); } @@ -289,8 +265,8 @@ Set getTargetImageAdditionalTags() { */ @Nullable String getTargetImageCredentialHelperName() { - if (System.getProperty(PropertyNames.toCredHelper) != null) { - return System.getProperty(PropertyNames.toCredHelper); + if (System.getProperty(PropertyNames.TO_CRED_HELPER) != null) { + return System.getProperty(PropertyNames.TO_CRED_HELPER); } return Preconditions.checkNotNull(to).credHelper; } @@ -306,8 +282,8 @@ AuthConfiguration getTargetImageAuth() { * @return {@code true} if the build should use the current timestamp, {@code false} if not */ boolean getUseCurrentTimestamp() { - if (System.getProperty(PropertyNames.containerUseCurrentTimestamp) != null) { - return Boolean.getBoolean(PropertyNames.containerUseCurrentTimestamp); + if (System.getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP) != null) { + return Boolean.getBoolean(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); } return container.useCurrentTimestamp; } @@ -318,9 +294,9 @@ boolean getUseCurrentTimestamp() { * @return the configured entrypoint */ List getEntrypoint() { - if (System.getProperty(PropertyNames.containerEntrypoint) != null) { + if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.containerEntrypoint)); + System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT)); } return container.entrypoint; } @@ -331,9 +307,9 @@ List getEntrypoint() { * @return the configured jvm flags */ List getJvmFlags() { - if (System.getProperty(PropertyNames.containerJvmFlags) != null) { + if (System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS) != null) { return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.containerJvmFlags)); + System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS)); } return container.jvmFlags; } @@ -345,9 +321,9 @@ List getJvmFlags() { */ @Nullable Map getEnvironment() { - if (System.getProperty(PropertyNames.containerEnvironment) != null) { + if (System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT) != null) { return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.containerEnvironment)); + System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT)); } return container.environment; } @@ -359,8 +335,8 @@ Map getEnvironment() { */ @Nullable String getMainClass() { - if (System.getProperty(PropertyNames.containerMainClass) != null) { - return System.getProperty(PropertyNames.containerMainClass); + if (System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS) != null) { + return System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS); } return container.mainClass; } @@ -371,9 +347,9 @@ String getMainClass() { * @return the configured main arguments */ List getArgs() { - if (System.getProperty(PropertyNames.containerArgs) != null) { + if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.containerArgs)); + System.getProperty(PropertyNames.CONTAINER_ARGS)); } return container.args; } @@ -384,9 +360,9 @@ List getArgs() { * @return the configured exposed ports */ List getExposedPorts() { - if (System.getProperty(PropertyNames.containerPorts) != null) { + if (System.getProperty(PropertyNames.CONTAINER_PORTS) != null) { return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.containerPorts)); + System.getProperty(PropertyNames.CONTAINER_PORTS)); } return container.ports; } @@ -397,9 +373,9 @@ List getExposedPorts() { * @return the configured labels */ Map getLabels() { - if (System.getProperty(PropertyNames.containerLabels) != null) { + if (System.getProperty(PropertyNames.CONTAINER_LABELS) != null) { return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.containerLabels)); + System.getProperty(PropertyNames.CONTAINER_LABELS)); } return container.labels; } @@ -410,8 +386,8 @@ Map getLabels() { * @return the configured app root directory */ String getAppRoot() { - if (System.getProperty(PropertyNames.containerAppRoot) != null) { - return System.getProperty(PropertyNames.containerAppRoot); + if (System.getProperty(PropertyNames.CONTAINER_APP_ROOT) != null) { + return System.getProperty(PropertyNames.CONTAINER_APP_ROOT); } return container.appRoot; } @@ -422,8 +398,8 @@ String getAppRoot() { * @return the configured container image format */ String getFormat() { - if (System.getProperty(PropertyNames.containerFormat) != null) { - return System.getProperty(PropertyNames.containerFormat); + if (System.getProperty(PropertyNames.CONTAINER_FORMAT) != null) { + return System.getProperty(PropertyNames.CONTAINER_FORMAT); } return Preconditions.checkNotNull(container.format); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index cf04a5f45f..5362b8279c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.time.Instant; import java.util.List; @@ -110,8 +111,8 @@ static PluginConfigurationProcessor processCommonConfiguration( Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( projectProperties.getEventDispatcher(), - JibPluginConfiguration.PropertyNames.fromAuthUsername, - JibPluginConfiguration.PropertyNames.fromAuthPassword, + PropertyNames.FROM_AUTH_USERNAME, + PropertyNames.FROM_AUTH_PASSWORD, jibPluginConfiguration.getBaseImageAuth()); if (optionalFromCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java new file mode 100644 index 0000000000..baf10329b6 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** Names of system properties used to set configuration via commandline. */ +public class PropertyNames { + public static final String FROM_IMAGE = "jib.from.image"; + public static final String FROM_CRED_HELPER = "jib.from.credHelper"; + public static final String FROM_AUTH_USERNAME = "jib.from.auth.username"; + public static final String FROM_AUTH_PASSWORD = "jib.from.auth.password"; + public static final String TO_IMAGE = "jib.to.image"; + public static final String TO_IMAGE_ALTERNATE = "image"; + public static final String TO_TAGS = "jib.to.tags"; + public static final String TO_CRED_HELPER = "jib.to.credHelper"; + public static final String TO_AUTH_USERNAME = "jib.to.auth.username"; + public static final String TO_AUTH_PASSWORD = "jib.to.auth.password"; + public static final String CONTAINER_APP_ROOT = "jib.container.appRoot"; + public static final String CONTAINER_ARGS = "jib.container.args"; + public static final String CONTAINER_ENTRYPOINT = "jib.container.entrypoint"; + public static final String CONTAINER_ENVIRONMENT = "jib.container.environment"; + public static final String CONTAINER_FORMAT = "jib.container.format"; + public static final String CONTAINER_JVM_FLAGS = "jib.container.jvmFlags"; + public static final String CONTAINER_LABELS = "jib.container.labels"; + public static final String CONTAINER_MAIN_CLASS = "jib.container.mainClass"; + public static final String CONTAINER_PORTS = "jib.container.ports"; + public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; + public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; + public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; + public static final String EXTRA_DIRECTORY = "jib.extraDirectory"; + public static final String SKIP = "jib.skip"; +} From 4147c736b66338088482d640148830655d052809 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 3 Oct 2018 15:01:13 -0400 Subject: [PATCH 0266/2020] Add FAQ for how to set up file system structure with copy-resources (#1092) --- docs/faq.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index b1f78869a7..be5c8020d8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -13,6 +13,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ +[I need to add files generated during the build process to a custom directory on the image.](#i-need-to-add-files-generated-during-the-build-process-to-a-custom-directory-on-the-image)\ [Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ @@ -148,15 +149,9 @@ See [Extended Usage](../jib-maven-plugin#extended-usage) for the ` +

File copying examples +

+ +#### Maven + +In Maven, you can use the `maven-resources-plugin` to copy files to your extra directory. For example, if you generate files in `target/generated/files` and want to add them to `/my/files` on the container, you can add the following to your `pom.xml`: + +```xml + + ... + + jib-maven-plugin + ... + + ${project.basedir}/target/extra-directory/ + + + ... + + maven-resources-plugin + 3.1.0 + + ${project.basedir}/target/extra-directory/my/files + + + ${project.basedir}/target/generated/files + + + + + ... + +``` + +The `copy-resources` goal will run automatically before compile, so if you are copying files from your build output to the extra directory, you will need to either set the life-cycle phase to `post-compile` or later, or run the goal manually: + +```sh +mvn compile resources:copy-resources jib:build +``` + +#### Gradle + +The same can be accomplished in Gradle by using a `Copy` task. In your `build.gradle`: + +```groovy +jib.extraDirectory = file('build/extra-directory') + +task setupExtraDir(type: Copy) { + from file('build/generated/files') + into file('build/extra-directory/my/files') +} +tasks.jib.dependsOn setupExtraDir +``` + +The files will be copied to your extra directory when you run the `jib` task. + +

+ + ### Can I build to a local Docker daemon? There are several ways of doing this: From 1171796e7239af419a7e9aebe2063712234b1cef Mon Sep 17 00:00:00 2001 From: Cyrille HEIT Date: Thu, 4 Oct 2018 16:50:28 +0200 Subject: [PATCH 0267/2020] Maven War support (#1068) --- .../jib/frontend/JavaLayerConfigurations.java | 9 + .../jib/gradle/GradleLayerConfigurations.java | 7 +- .../cloud/tools/jib/gradle/JibPlugin.java | 10 +- .../gradle/GradleLayerConfigurationsTest.java | 8 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 2 +- .../tools/jib/maven/DockerContextMojo.java | 13 +- .../jib/maven/JibPluginConfiguration.java | 13 +- .../jib/maven/MavenLayerConfigurations.java | 89 ++++++++- .../jib/maven/MavenProjectProperties.java | 2 +- .../maven/PluginConfigurationProcessor.java | 90 +++++++-- .../jib/maven/DockerContextMojoTest.java | 128 +++++++++++- .../jib/maven/JibPluginConfigurationTest.java | 2 +- .../maven/MavenLayerConfigurationsTest.java | 183 +++++++++++++++--- .../PluginConfigurationProcessorTest.java | 123 +++++++++++- .../webapp/final-name/META-INF/context.xml | 0 .../test/resources/webapp/final-name/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 1 + .../WEB-INF/classes/package/Other.class | 1 + .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../webapp/final-name/WEB-INF/web.xml | 0 22 files changed, 596 insertions(+), 85 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/META-INF/context.xml create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/Test.jsp create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/test.properties create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/web.xml diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index a2288bcd5c..00abd34a20 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -182,6 +182,15 @@ public static Builder builder() { */ public static final String DEFAULT_APP_ROOT = "/app"; + /** + * The default webapp root in the image. For example, if this is set to {@code + * "/jetty/webapps/ROOT"}, dependency JARs will be in {@code "/jetty/webapps/ROOT/WEB-INF/lib"}. + */ + public static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; + + /** The filename suffix for a maven/gradle snapshot dependency */ + public static final String SNAPSHOT_FILENAME_SUFFIX = "SNAPSHOT"; + private final ImmutableMap layerConfigurationMap; private JavaLayerConfigurations( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 4e0de4c285..1633553efb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -41,9 +41,6 @@ class GradleLayerConfigurations { /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; - /** The filename suffix for snapshot dependency JARs. */ - private static final String SNAPSHOT = "SNAPSHOT"; - /** * Resolves the {@link JavaLayerConfigurations} for a Gradle {@link Project}. * @@ -116,7 +113,7 @@ private static JavaLayerConfigurations getForNonWarProject( if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { continue; } - if (dependencyFile.getName().contains(SNAPSHOT)) { + if (dependencyFile.getName().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX)) { snapshotDependenciesFiles.add(dependencyFile.toPath()); } else { dependenciesFiles.add(dependencyFile.toPath()); @@ -183,7 +180,7 @@ private static JavaLayerConfigurations getForWarProject( try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { dependencyFileStream.forEach( path -> { - if (path.toString().contains(SNAPSHOT)) { + if (path.toString().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX)) { snapshotDependenciesFiles.add(path); } else { dependenciesFiles.add(path); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 11a56db9a1..d71e23d0ef 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -47,12 +47,6 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; @VisibleForTesting static final String DEFAULT_WAR_FROM_IMAGE = "gcr.io/distroless/java/jetty"; - /** - * The default app root in the image for WAR. For example, if this is set to {@code - * /jetty/webapps/ROOT}, dependency JARs will be in {@code /jetty/webapps/ROOT/WEB-INF/lib}. - */ - @VisibleForTesting static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; - /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of * configuration [compile, runtime, etc]. It potentially will collect common test libraries in @@ -129,7 +123,9 @@ public void apply(Project project) { jibExtension.getFrom().setImage(DEFAULT_WAR_FROM_IMAGE); } if (jibExtension.getContainer().getAppRoot().isEmpty()) { - jibExtension.getContainer().setAppRoot(DEFAULT_WEB_APP_ROOT); + jibExtension + .getContainer() + .setAppRoot(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); } ExplodedWarTask explodedWarTask = (ExplodedWarTask) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 61f80f0fb0..3493de32b1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -364,7 +364,7 @@ public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOExceptio mockWebAppProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); + AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); assertExtractionPathsUnordered( Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), @@ -400,7 +400,7 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOE mockWebAppProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } @Test @@ -412,7 +412,7 @@ public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOExcep mockWebAppProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } @Test @@ -424,7 +424,7 @@ public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOExceptio mockWebAppProject, mockLogger, extraFilesDirectory, - AbsoluteUnixPath.get(JibPlugin.DEFAULT_WEB_APP_ROOT)); // should pass + AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } private void setUpWarProject(Path webAppDirectory) { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index a142c2f749..8a6ca1fc01 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -208,7 +208,7 @@ public void testWebAppProject() { .getFrom() .getImage()); Assert.assertEquals( - JibPlugin.DEFAULT_WEB_APP_ROOT, + JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT, ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) .getJib() .getContainer() diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index a5374d2690..bbf051dab5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; @@ -76,14 +75,8 @@ public void execute() throws MojoExecutionException { MavenProjectProperties mavenProjectProperties = MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); - List entrypoint = getEntrypoint(); - if (entrypoint.isEmpty()) { - String mainClass = mavenProjectProperties.getMainClass(this); - entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint(appRoot, getJvmFlags(), mainClass); - } else if (getMainClass() != null || !getJvmFlags().isEmpty()) { - getLog().warn(" and are ignored when is specified"); - } + List entrypoint = + PluginConfigurationProcessor.computeEntrypoint(getLog(), this, mavenProjectProperties); try { // Validate port input, but don't save the output because we don't want the ranges expanded @@ -91,7 +84,7 @@ public void execute() throws MojoExecutionException { ExposedPortsParser.parse(getExposedPorts()); new JavaDockerContextGenerator(mavenProjectProperties.getJavaLayerConfigurations()) - .setBaseImage(getBaseImage()) + .setBaseImage(PluginConfigurationProcessor.getBaseImage(this)) .setEntrypoint(entrypoint) .setJavaArguments(getArgs()) .setExposedPorts(getExposedPorts()) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 66060e914c..f1e22b4cb3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -88,15 +87,12 @@ private void setPropertyDescriptors(String descriptorPrefix) { } } - /** - * Configuration for {@code from} parameter, where image by default is {@code - * gcr.io/distroless/java}. - */ + /** Configuration for {@code from} parameter, */ public static class FromConfiguration { @Nullable @Parameter(required = true) - private String image = "gcr.io/distroless/java"; + private String image; @Nullable @Parameter private String credHelper; @@ -142,7 +138,7 @@ public static class ContainerParameters { @Parameter private Map labels = Collections.emptyMap(); - @Parameter private String appRoot = JavaLayerConfigurations.DEFAULT_APP_ROOT; + @Parameter private String appRoot = ""; } @Nullable @@ -203,11 +199,12 @@ MavenProject getProject() { * * @return the configured base image reference */ + @Nullable String getBaseImage() { if (System.getProperty(PropertyNames.FROM_IMAGE) != null) { return System.getProperty(PropertyNames.FROM_IMAGE); } - return Preconditions.checkNotNull(Preconditions.checkNotNull(from).image); + return Preconditions.checkNotNull(from).image; } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 548cb6579e..17a6edb1e0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -34,9 +34,8 @@ /** Builds {@link JavaLayerConfigurations} based on inputs from a {@link MavenProject}. */ class MavenLayerConfigurations { - /** - * Resolves the source files configuration for a {@link MavenProject}. + * Resolves the {@link JavaLayerConfigurations} for a {@link MavenProject}. * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer @@ -46,6 +45,24 @@ class MavenLayerConfigurations { */ static JavaLayerConfigurations getForProject( MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + if ("war".equals(project.getPackaging())) { + return getForWarProject(project, extraDirectory, appRoot); + } else { + return getForNonWarProject(project, extraDirectory, appRoot); + } + } + + /** + * Resolves the source files configuration for a non-war {@link MavenProject}. + * + * @param project the {@link MavenProject} + * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed + * @return a {@link JavaLayerConfigurations} for the project + * @throws IOException if collecting the project files fails + */ + private static JavaLayerConfigurations getForNonWarProject( + MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); @@ -91,6 +108,74 @@ static JavaLayerConfigurations getForProject( return layerBuilder.build(); } + /** + * Resolves the source files configuration for a War {@link MavenProject}. + * + * @param project the {@link MavenProject} + * @param extraDirectory path to the directory for the extra files layer + * @param appRoot root directory in the image where the app will be placed + * @return a {@link JavaLayerConfigurations} for the project + * @throws IOException if collecting the project files fails + */ + private static JavaLayerConfigurations getForWarProject( + MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + + // TODO explode the WAR file rather than using this directory. The contents of the final WAR may + // be different from this directory (it's possible to include or exclude files when packaging a + // WAR). Also the exploded WAR directory is configurable with and may not be + // at build.getFinalName(). + Path explodedWarPath = + Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); + AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib/"); + AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes/"); + + Builder layerBuilder = JavaLayerConfigurations.builder(); + + // Gets all the dependencies. + Predicate isSnapshotDependency = + path -> path.toString().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX); + if (Files.exists(explodedWarPath.resolve("WEB-INF/lib"))) { + addFilesToLayer( + explodedWarPath.resolve("WEB-INF/lib/"), + isSnapshotDependency, + dependenciesExtractionPath, + layerBuilder::addSnapshotDependencyFile); + addFilesToLayer( + explodedWarPath.resolve("WEB-INF/lib/"), + isSnapshotDependency.negate(), + dependenciesExtractionPath, + layerBuilder::addDependencyFile); + } + + // Gets the classes files in the 'WEB-INF/classes' output directory. + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + if (Files.exists(explodedWarPath.resolve("WEB-INF/classes"))) { + addFilesToLayer( + explodedWarPath.resolve("WEB-INF/classes/"), + isClassFile, + classesExtractionPath, + layerBuilder::addClassFile); + } + + // Gets the resources + Predicate isResources = + path -> { + boolean inWebInfClasses = path.startsWith(explodedWarPath.resolve("WEB-INF/classes/")); + boolean inWebInfLib = path.startsWith(explodedWarPath.resolve("WEB-INF/lib/")); + + return (!inWebInfClasses && !inWebInfLib) || (inWebInfClasses && !isClassFile.test(path)); + }; + addFilesToLayer(explodedWarPath, isResources, appRoot, layerBuilder::addResourceFile); + + // Adds all the extra files. + if (Files.exists(extraDirectory)) { + AbsoluteUnixPath extractionBase = AbsoluteUnixPath.get("/"); + addFilesToLayer(extraDirectory, path -> true, extractionBase, layerBuilder::addExtraFile); + } + + return layerBuilder.build(); + } + @FunctionalInterface @VisibleForTesting static interface FileToLayerAdder { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index ad90b34d80..3541fcf3ca 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -152,7 +152,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - return false; // TODO: to be implemented. For now, assume false. + return "war".equals(project.getPackaging()); } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 5362b8279c..f8130164b7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -40,10 +41,20 @@ /** Configures and provides builders for the image building goals. */ class PluginConfigurationProcessor { + /** + * Returns true if the maven packaging type is "war" + * + * @param jibPluginConfiguration the Jib plugin configuration + * @return true if the maven packaging type is "war" + */ + private static boolean isWarPackaging(JibPluginConfiguration jibPluginConfiguration) { + return "war".equals(jibPluginConfiguration.getProject().getPackaging()); + } /** - * Gets the value of the {@code } parameter. Throws {@link - * MojoExecutionException} if it is not an absolute path in Unix-style. + * Gets the value of the {@code } parameter. If the parameter is empty, + * returns {@link JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for project with WAR packaging or + * {@link JavaLayerConfigurations#DEFAULT_APP_ROOT} for other packaging. * * @param jibPluginConfiguration the Jib plugin configuration * @return the app root value @@ -52,6 +63,12 @@ class PluginConfigurationProcessor { static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) throws MojoExecutionException { String appRoot = jibPluginConfiguration.getAppRoot(); + if (appRoot.isEmpty()) { + appRoot = + isWarPackaging(jibPluginConfiguration) + ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT + : JavaLayerConfigurations.DEFAULT_APP_ROOT; + } try { return AbsoluteUnixPath.get(appRoot); } catch (IllegalArgumentException ex) { @@ -60,6 +77,24 @@ static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfig } } + /** + * Gets the value of the {@code } parameter. If the parameter is null, returns + * "gcr.io/distroless/java/jetty" for projects with WAR packaging or "gcr.io/distroless/java" for + * other packaging. + * + * @param jibPluginConfiguration the Jib plugin configuration + * @return the base image value + */ + static String getBaseImage(JibPluginConfiguration jibPluginConfiguration) { + String baseImage = jibPluginConfiguration.getBaseImage(); + if (baseImage == null) { + return isWarPackaging(jibPluginConfiguration) + ? "gcr.io/distroless/java/jetty" + : "gcr.io/distroless/java"; + } + return baseImage; + } + /** Disables annoying Apache HTTP client logging. */ static void disableHttpLogging() { System.setProperty( @@ -92,7 +127,7 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided disableHttpLogging(); - ImageReference baseImage = parseImageReference(jibPluginConfiguration.getBaseImage(), "from"); + ImageReference baseImage = parseImageReference(getBaseImage(jibPluginConfiguration), "from"); // Checks Maven settings for registry credentials. if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { @@ -131,18 +166,7 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - List entrypoint = jibPluginConfiguration.getEntrypoint(); - if (entrypoint.isEmpty()) { - String mainClass = projectProperties.getMainClass(jibPluginConfiguration); - entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - getAppRootChecked(jibPluginConfiguration), - jibPluginConfiguration.getJvmFlags(), - mainClass); - } else if (jibPluginConfiguration.getMainClass() != null - || !jibPluginConfiguration.getJvmFlags().isEmpty()) { - logger.warn(" and are ignored when is specified"); - } + List entrypoint = computeEntrypoint(logger, jibPluginConfiguration, projectProperties); ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder() .setEntrypoint(entrypoint) @@ -193,6 +217,42 @@ static ImageReference parseImageReference(String image, String type) { } } + /** + * Compute the container entrypoint, in this order : + * + *
    + *
  1. the user specified one, if set + *
  2. for a war project, the jetty default one + *
  3. for a jar project, by resolving the main class + *
+ * + * @param logger the logger used to display messages. + * @param jibPluginConfiguration the {@link JibPluginConfiguration} providing the configuration + * data + * @param projectProperties used for providing additional information + * @return the entrypoint + * @throws MojoExecutionException if the http timeout system property is misconfigured + */ + static List computeEntrypoint( + Log logger, + JibPluginConfiguration jibPluginConfiguration, + MavenProjectProperties projectProperties) + throws MojoExecutionException { + if (!jibPluginConfiguration.getEntrypoint().isEmpty()) { + if (jibPluginConfiguration.getMainClass() != null + || !jibPluginConfiguration.getJvmFlags().isEmpty()) { + logger.warn(" and are ignored when is specified"); + } + return jibPluginConfiguration.getEntrypoint(); + } + if (isWarPackaging(jibPluginConfiguration)) { + return JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); + } + String mainClass = projectProperties.getMainClass(jibPluginConfiguration); + return JavaEntrypointConstructor.makeDefaultEntrypoint( + getAppRootChecked(jibPluginConfiguration), jibPluginConfiguration.getJvmFlags(), mainClass); + } + private final BuildConfiguration.Builder buildConfigurationBuilder; private final ImageConfiguration.Builder baseImageConfigurationBuilder; private final ContainerConfiguration.Builder containerConfigurationBuilder; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 3543d287ad..5e99affcd1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -30,6 +31,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -41,13 +43,13 @@ public class DockerContextMojoTest { private DockerContextMojo mojo; private String appRoot = "/app"; + private File outputFolder; + private @Mock MavenProject project; + private @Mock Build build; @Before public void setUp() throws IOException { - File outputFolder = projectRoot.newFolder("target"); - - MavenProject project = Mockito.mock(MavenProject.class); - Build build = Mockito.mock(Build.class); + outputFolder = projectRoot.newFolder("target"); Mockito.when(project.getBuild()).thenReturn(build); Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); @@ -139,4 +141,122 @@ private String getEntrypoint() throws IOException { List lines = Files.readAllLines(dockerfile); return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); } + + private String getBaseImage() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("FROM")).findFirst().get(); + } + + @Test + public void testBaseImage_nonWarPackaging() throws MojoExecutionException, IOException { + mojo.execute(); + + Assert.assertEquals("FROM gcr.io/distroless/java", getBaseImage()); + } + + @Test + public void testBaseImage_warPackaging() throws MojoExecutionException, IOException { + Mockito.doReturn("war").when(project).getPackaging(); + Mockito.doReturn("final-name").when(build).getFinalName(); + projectRoot.newFolder("final-name", "WEB-INF", "lib"); + projectRoot.newFolder("final-name", "WEB-INF", "classes"); + Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); + mojo.execute(); + + Assert.assertEquals("FROM gcr.io/distroless/java/jetty", getBaseImage()); + } + + @Test + public void testBaseImage_nonDefault() throws MojoExecutionException, IOException { + Mockito.doReturn("war").when(project).getPackaging(); + Mockito.doReturn("final-name").when(build).getFinalName(); + mojo = + new DockerContextMojo() { + @Override + MavenProject getProject() { + return project; + } + + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } + + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + String getBaseImage() { + return "tomcat:8.5-jre8-alpine"; + } + + @Override + String getAppRoot() { + return appRoot; + } + }; + mojo.targetDir = outputFolder.toString(); + + projectRoot.newFolder("final-name", "WEB-INF", "lib"); + projectRoot.newFolder("final-name", "WEB-INF", "classes"); + Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); + mojo.execute(); + + Assert.assertEquals("FROM tomcat:8.5-jre8-alpine", getBaseImage()); + } + + @Test + public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException, IOException { + Mockito.doReturn("war").when(project).getPackaging(); + Mockito.doReturn("final-name").when(build).getFinalName(); + projectRoot.newFolder("final-name", "WEB-INF", "lib"); + projectRoot.newFolder("final-name", "WEB-INF", "classes"); + Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); + mojo.execute(); + + Assert.assertEquals("ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]", getEntrypoint()); + } + + @Test + public void testEntrypoint_warPackaging() throws MojoExecutionException, IOException { + Mockito.doReturn("war").when(project).getPackaging(); + Mockito.doReturn("final-name").when(build).getFinalName(); + projectRoot.newFolder("final-name", "WEB-INF", "lib"); + projectRoot.newFolder("final-name", "WEB-INF", "classes"); + Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); + mojo = + new DockerContextMojo() { + @Override + MavenProject getProject() { + return project; + } + + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } + + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + List getEntrypoint() { + return ImmutableList.of("catalina.sh", "run"); + } + + @Override + String getAppRoot() { + return appRoot; + } + }; + mojo.targetDir = outputFolder.toString(); + mojo.execute(); + + Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getEntrypoint()); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index b739a4fadd..137cfde198 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -77,7 +77,7 @@ public void testAuthDefaults() { Assert.assertEquals( "", testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); - Assert.assertEquals("/app", testPluginConfiguration.getAppRoot()); + Assert.assertEquals("", testPluginConfiguration.getAppRoot()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 7baf056a0f..a38320563a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -24,6 +24,7 @@ import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; @@ -69,6 +70,33 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } + private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuration) { + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/resources/directory/somefile", + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/classes/HelloWorld.class", + "/my/app/classes/package/some.class", + "/my/app/classes/some.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + } + @Rule public final TestRepository testRepository = new TestRepository(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -78,7 +106,7 @@ private static void assertExtractionPathsUnordered( @Mock private MavenLayerConfigurations.FileToLayerAdder fileToLayerAdder; @Before - public void setUp() throws URISyntaxException { + public void setUp() throws URISyntaxException, IOException { Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); @@ -93,6 +121,11 @@ public void setUp() throws URISyntaxException { testRepository.findArtifact("com.test", "dependency", "1.0.0"), testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); + + Path emptyDirectory = + Paths.get(Resources.getResource("webapp").toURI()) + .resolve("final-name/WEB-INF/classes/empty_dir"); + Files.createDirectories(emptyDirectory); } @Test @@ -158,30 +191,7 @@ public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOE JavaLayerConfigurations configuration = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/libs/dependency-1.0.0.jar", - "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/resources/directory/somefile", - "/my/app/resources/resourceA", - "/my/app/resources/resourceB", - "/my/app/resources/world"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/classes/HelloWorld.class", - "/my/app/classes/package/some.class", - "/my/app/classes/some.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + assertNonDefaultAppRoot(configuration); } @Test @@ -315,4 +325,127 @@ private Artifact makeArtifact(Path path) { Mockito.when(artifact.getFile()).thenReturn(path.toFile()); return artifact; } + + @Test + public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, IOException { + Path outputPath = Paths.get(Resources.getResource("webapp").toURI()); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); + JavaLayerConfigurations configuration = + MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + + ImmutableList expectedDependenciesFiles = + ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")); + ImmutableList expectedSnapshotDependenciesFiles = + ImmutableList.of( + outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); + ImmutableList expectedResourcesFiles = + ImmutableList.of( + outputPath.resolve("final-name/META-INF/context.xml"), + outputPath.resolve("final-name/Test.jsp"), + outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), + outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), + outputPath.resolve("final-name/WEB-INF/web.xml")); + ImmutableList expectedClassesFiles = + ImmutableList.of( + outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), + outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), // Not sure about that + outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")); + + assertSourcePathsUnordered( + expectedDependenciesFiles, configuration.getDependencyLayerEntries()); + assertSourcePathsUnordered( + expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); + assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); + assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); + + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/web.xml"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", // Not sure about that + "/my/app/WEB-INF/classes/package/Other.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + } + + @Test + public void testGetForJarProject_nonDefaultAppRoot() throws URISyntaxException, IOException { + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + // Test when the default packaging is set + Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); + + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); + JavaLayerConfigurations configuration = + MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + + assertNonDefaultAppRoot(configuration); + } + + @Test + public void testGetForWarProject_noErrorIfWebInfDoesNotExist() + throws IOException, URISyntaxException { + temporaryFolder.newFolder("final-name"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, appRoot); // should pass + } + + @Test + public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() + throws IOException, URISyntaxException { + temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, appRoot); // should pass + } + + @Test + public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() + throws IOException, URISyntaxException { + temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); + + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, appRoot); // should pass + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index ee3b33e266..0f24393fd6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -19,12 +19,16 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collections; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Settings; import org.junit.Assert; import org.junit.Before; @@ -43,19 +47,20 @@ public class PluginConfigurationProcessorTest { @Mock private MavenProjectProperties mockProjectProperties; @Mock private MavenSession mockMavenSession; @Mock private Settings mockMavenSettings; + @Mock private MavenProject mavenProject; @Before public void setUp() throws Exception { Mockito.doReturn(mockMavenSession).when(mockJibPluginConfiguration).getSession(); Mockito.doReturn(mockMavenSettings).when(mockMavenSession).getSettings(); - Mockito.doReturn("gcr.io/distroless/java").when(mockJibPluginConfiguration).getBaseImage(); Mockito.doReturn(new AuthConfiguration()).when(mockJibPluginConfiguration).getBaseImageAuth(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getEntrypoint(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getJvmFlags(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getArgs(); Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getExposedPorts(); Mockito.doReturn("/app").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); Mockito.doReturn(JavaLayerConfigurations.builder().build()) .when(mockProjectProperties) @@ -67,7 +72,8 @@ public void setUp() throws Exception { /** Test with our default mocks, which try to mimic the bare Maven configuration. */ @Test - public void testPluginConfigurationProcessor_defaults() throws MojoExecutionException { + public void testPluginConfigurationProcessor_defaults() + throws MojoExecutionException, InvalidImageReferenceException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); @@ -75,6 +81,25 @@ public void testPluginConfigurationProcessor_defaults() throws MojoExecutionExce Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), configuration.getEntrypoint()); + + Assert.assertEquals( + ImageReference.parse("gcr.io/distroless/java").toString(), + processor.getBaseImageConfigurationBuilder().build().getImage().toString()); + Mockito.verifyZeroInteractions(mockLog); + } + + @Test + public void testPluginConfigurationProcessor_warPackaging() + throws MojoExecutionException, InvalidImageReferenceException { + Mockito.doReturn("war").when(mavenProject).getPackaging(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLog, mockJibPluginConfiguration, mockProjectProperties); + + Assert.assertEquals( + ImageReference.parse("gcr.io/distroless/java/jetty").toString(), + processor.getBaseImageConfigurationBuilder().build().getImage().toString()); Mockito.verifyZeroInteractions(mockLog); } @@ -93,6 +118,39 @@ public void testEntrypoint() throws MojoExecutionException { Mockito.verifyZeroInteractions(mockLog); } + @Test + public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException { + Mockito.doReturn(ImmutableList.of()).when(mockJibPluginConfiguration).getEntrypoint(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn("war").when(mavenProject).getPackaging(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLog, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals( + Arrays.asList("java", "-jar", "/jetty/start.jar"), configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockLog); + } + + @Test + public void testEntrypoint_defaulNonWarPackaging() throws MojoExecutionException { + Mockito.doReturn(ImmutableList.of()).when(mockJibPluginConfiguration).getEntrypoint(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn(null).when(mavenProject).getPackaging(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLog, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals( + Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), + configuration.getEntrypoint()); + Mockito.verifyZeroInteractions(mockLog); + } + @Test public void testEntrypoint_warningOnJvmFlags() throws MojoExecutionException { Mockito.doReturn(Arrays.asList("custom", "entrypoint")) @@ -191,5 +249,66 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { } } + @Test + public void testGetAppRootChecked_defaultNonWarPackaging() throws MojoExecutionException { + Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn(null).when(mavenProject).getPackaging(); + + Assert.assertEquals( + AbsoluteUnixPath.get("/app"), + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); + } + + @Test + public void testGetAppRootChecked_defaultJarPackaging() throws MojoExecutionException { + Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn("jar").when(mavenProject).getPackaging(); + + Assert.assertEquals( + AbsoluteUnixPath.get("/app"), + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); + } + + @Test + public void testGetAppRootChecked_defaultWarPackaging() throws MojoExecutionException { + Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn("war").when(mavenProject).getPackaging(); + + Assert.assertEquals( + AbsoluteUnixPath.get("/jetty/webapps/ROOT"), + PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); + } + + @Test + public void testGetBaseImage_defaultWarPackaging() { + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn("war").when(mavenProject).getPackaging(); + + Assert.assertEquals( + "gcr.io/distroless/java/jetty", + PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); + } + + @Test + public void testGetBaseImage_defaultNonWarPackaging() { + Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); + Mockito.doReturn(null).when(mavenProject).getPackaging(); + + Assert.assertEquals( + "gcr.io/distroless/java", + PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); + } + + @Test + public void testGetBaseImage_nonDefault() { + Mockito.doReturn("tomcat").when(mockJibPluginConfiguration).getBaseImage(); + + Assert.assertEquals( + "tomcat", PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); + } + // TODO should test other behaviours } diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/META-INF/context.xml b/jib-maven-plugin/src/test/resources/webapp/final-name/META-INF/context.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/Test.jsp b/jib-maven-plugin/src/test/resources/webapp/final-name/Test.jsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class @@ -0,0 +1 @@ + diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class @@ -0,0 +1 @@ + diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/test.properties b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/test.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/web.xml b/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/web.xml new file mode 100644 index 0000000000..e69de29bb2 From 1e2797892668b0b47ff9da50645030ffb544831e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 4 Oct 2018 11:37:15 -0400 Subject: [PATCH 0268/2020] Set up integration test for Gradle WAR image building (#1094) --- .../jib/gradle/JibPluginIntegrationTest.java | 159 ++++-------------- .../cloud/tools/jib/gradle/JibRunHelper.java | 127 ++++++++++++++ .../tools/jib/gradle/WarIntegrationTest.java | 73 ++++++++ .../projects/default-target/build.gradle | 22 +-- .../resources/projects/empty/build.gradle | 26 +-- .../resources/projects/simple/build.gradle | 34 ++-- .../projects/simple/complex-build.gradle | 58 +++---- .../projects/war_jetty_servlet25/build.gradle | 37 ++++ .../war_jetty_servlet25/src/extra_js/bogus.js | 1 + .../src/extra_static/bogus.html | 1 + .../src/main/java/example/HelloWorld.java | 48 ++++++ .../src/main/resources/world | 1 + .../src/main/webapp/META-INF/MANIFEST.MF | 2 + .../src/main/webapp/WEB-INF/web.xml | 15 ++ .../src/main/webapp/index.html | 20 +++ .../resources/projects/simple/build.gradle | 16 +- 16 files changed, 435 insertions(+), 205 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java index 67e0477b67..f5b9b9efd3 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; @@ -55,38 +54,10 @@ public class JibPluginIntegrationTest { @ClassRule public static final TestProject defaultTargetTestProject = new TestProject("default-target"); - private static String buildAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { - BuildResult buildResult = - testProject.build( - "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); - assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - return pullAndRunBuiltImage(imageReference); - } - - private static void buildAndRunAdditionalTag( - TestProject testProject, String imageReference, String additionalTag, String expectedOutput) - throws InvalidImageReferenceException, IOException, InterruptedException { - BuildResult buildResult = - testProject.build( - "clean", - JibPlugin.BUILD_IMAGE_TASK_NAME, - "-D_TARGET_IMAGE=" + imageReference, - "-D_ADDITIONAL_TAG=" + additionalTag); - assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - String additionalImageReference = - ImageReference.parse(imageReference).withTag(additionalTag).toString(); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString(additionalImageReference)); - - Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(imageReference)); - Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(additionalImageReference)); - assertCreationTimeEpoch(imageReference); - assertCreationTimeEpoch(additionalImageReference); + @Before + public void setup() throws IOException, InterruptedException { + // Pull distroless and push to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); } private static String buildAndRunComplex( @@ -95,14 +66,14 @@ private static String buildAndRunComplex( BuildResult buildResult = simpleTestProject.build( "clean", - JibPlugin.BUILD_IMAGE_TASK_NAME, + "jib", "-D_TARGET_IMAGE=" + imageReference, "-D_TARGET_USERNAME=" + username, "-D_TARGET_PASSWORD=" + password, "-DsendCredentialsOverHttp=true", "-b=complex-build.gradle"); - assertBuildSuccess(buildResult, JibPlugin.BUILD_IMAGE_TASK_NAME, "Built and pushed image as "); + JibRunHelper.assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); targetRegistry.pull(imageReference); @@ -112,59 +83,6 @@ private static String buildAndRunComplex( return new Command("docker", "run", "--rm", imageReference).run(); } - private static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { - BuildResult buildResult = - testProject.build( - "clean", JibPlugin.BUILD_DOCKER_TASK_NAME, "-D_TARGET_IMAGE=" + imageReference); - assertBuildSuccess( - buildResult, JibPlugin.BUILD_DOCKER_TASK_NAME, "Built image to Docker daemon as "); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - assertDockerInspect(imageReference); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", "--rm", imageReference).run(); - } - - /** - * Pulls a built image and attempts to run it. Also verifies the container configuration and - * history of the built image. - * - * @param imageReference the image reference of the built image - * @return the container output - * @throws IOException if an I/O exception occurs - * @throws InterruptedException if the process was interrupted - */ - private static String pullAndRunBuiltImage(String imageReference) - throws IOException, InterruptedException { - new Command("docker", "pull", imageReference).run(); - assertDockerInspect(imageReference); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - - return new Command("docker", "run", "--rm", imageReference).run(); - } - - /** - * Asserts that the test project build output indicates a success. - * - * @param buildResult the builds results of the project under test - * @param taskName the name of the Jib task that was run - * @param successMessage a Jib-specific success message to check for - */ - private static void assertBuildSuccess( - BuildResult buildResult, String taskName, String successMessage) { - BuildTask classesTask = buildResult.task(":classes"); - BuildTask jibTask = buildResult.task(":" + taskName); - - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(successMessage)); - } - /** * Asserts that the creation time of the simple test project is set. If the time parsed from the * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the @@ -211,19 +129,6 @@ private static void assertDockerInspect(String imageReference) + " }")); } - private static void assertCreationTimeEpoch(String imageReference) - throws IOException, InterruptedException { - Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); - } - - @Before - public void setup() throws IOException, InterruptedException { - // Pull distroless and push to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - } - @Test public void testBuild_empty() throws IOException, InterruptedException { String targetImage = @@ -231,8 +136,9 @@ public void testBuild_empty() throws IOException, InterruptedException { + IntegrationTestingConfiguration.getGCPProject() + "/emptyimage:gradle" + System.nanoTime(); - Assert.assertEquals("", buildAndRun(emptyTestProject, targetImage)); - assertCreationTimeEpoch(targetImage); + Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); + assertDockerInspect(targetImage); + JibRunHelper.assertCreationTimeEpoch(targetImage); } @Test @@ -243,7 +149,9 @@ public void testBuild_multipleTags() + IntegrationTestingConfiguration.getGCPProject() + "/multitag-image:gradle" + System.nanoTime(); - buildAndRunAdditionalTag(emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); + JibRunHelper.buildAndRunAdditionalTag( + emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); + assertDockerInspect(targetImage); } @Test @@ -256,8 +164,7 @@ public void testBuild_simple() throws IOException, InterruptedException { // Test empty output error try { - simpleTestProject.build( - "clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes", "-D_TARGET_IMAGE=" + targetImage); + simpleTestProject.build("clean", "jib", "-x=classes", "-D_TARGET_IMAGE=" + targetImage); Assert.fail(); } catch (UnexpectedBuildFailure ex) { @@ -269,7 +176,9 @@ public void testBuild_simple() throws IOException, InterruptedException { Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", buildAndRun(simpleTestProject, targetImage)); + "Hello, world. An argument.\nfoo\ncat\n", + JibRunHelper.buildAndRun(simpleTestProject, targetImage)); + assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -277,7 +186,7 @@ public void testBuild_simple() throws IOException, InterruptedException { public void testBuild_defaultTarget() { // Test error when 'to' is missing try { - defaultTargetTestProject.build("clean", JibPlugin.BUILD_IMAGE_TASK_NAME, "-x=classes"); + defaultTargetTestProject.build("clean", "jib", "-x=classes"); Assert.fail(); } catch (UnexpectedBuildFailure ex) { Assert.assertThat( @@ -312,10 +221,11 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr @Test public void testDockerDaemon_empty() throws IOException, InterruptedException { String targetImage = "emptyimage:gradle" + System.nanoTime(); - Assert.assertEquals("", buildToDockerDaemonAndRun(emptyTestProject, targetImage)); + Assert.assertEquals("", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage)); Assert.assertEquals( "1970-01-01T00:00:00Z", new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + assertDockerInspect(targetImage); } @Test @@ -324,16 +234,18 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException { Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nfoo\ncat\n", - buildToDockerDaemonAndRun(simpleTestProject, targetImage)); + JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + assertDockerInspect(targetImage); } @Test public void testDockerDaemon_defaultTarget() throws IOException, InterruptedException { Assert.assertEquals( "Hello, world. An argument.\n", - buildToDockerDaemonAndRun( + JibRunHelper.buildToDockerDaemonAndRun( defaultTargetTestProject, "default-target-name:default-target-version")); + assertDockerInspect("default-target-name:default-target-version"); } @Test @@ -344,10 +256,9 @@ public void testBuildTar_simple() throws IOException, InterruptedException { simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); Instant beforeBuild = Instant.now(); BuildResult buildResult = - simpleTestProject.build( - "clean", JibPlugin.BUILD_TAR_TASK_NAME, "-D_TARGET_IMAGE=" + targetImage); + simpleTestProject.build("clean", "jibBuildTar", "-D_TARGET_IMAGE=" + targetImage); - assertBuildSuccess(buildResult, JibPlugin.BUILD_TAR_TASK_NAME, "Built image tarball at "); + JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); new Command("docker", "load", "--input", outputPath).run(); @@ -360,11 +271,10 @@ public void testBuildTar_simple() throws IOException, InterruptedException { @Test public void testDockerContext() throws IOException, InterruptedException { - BuildResult buildResult = - simpleTestProject.build("clean", JibPlugin.DOCKER_CONTEXT_TASK_NAME, "--info"); + BuildResult buildResult = simpleTestProject.build("clean", "jibExportDockerContext", "--info"); - assertBuildSuccess( - buildResult, JibPlugin.DOCKER_CONTEXT_TASK_NAME, "Created Docker context at "); + JibRunHelper.assertBuildSuccess( + buildResult, "jibExportDockerContext", "Created Docker context at "); String imageName = "jib-gradle-plugin/integration-test" + System.nanoTime(); new Command( @@ -386,9 +296,7 @@ public void testDockerContext() throws IOException, InterruptedException { // Checks that generating the Docker context again is skipped. BuildTask upToDateJibDockerContextTask = - simpleTestProject - .build(JibPlugin.DOCKER_CONTEXT_TASK_NAME) - .task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); + simpleTestProject.build("jibExportDockerContext").task(":jibExportDockerContext"); Assert.assertNotNull(upToDateJibDockerContextTask); Assert.assertEquals(TaskOutcome.UP_TO_DATE, upToDateJibDockerContextTask.getOutcome()); @@ -402,9 +310,7 @@ public void testDockerContext() throws IOException, InterruptedException { .resolve("newfile")); try { BuildTask reexecutedJibDockerContextTask = - simpleTestProject - .build(JibPlugin.DOCKER_CONTEXT_TASK_NAME) - .task(":" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); + simpleTestProject.build("jibExportDockerContext").task(":jibExportDockerContext"); Assert.assertNotNull(reexecutedJibDockerContextTask); Assert.assertEquals(TaskOutcome.SUCCESS, reexecutedJibDockerContextTask.getOutcome()); @@ -421,11 +327,10 @@ public void testDockerContext() throws IOException, InterruptedException { @Test public void testMultiProject() { BuildResult buildResult = - multiprojectTestProject.build( - "clean", ":a_packaged:" + JibPlugin.DOCKER_CONTEXT_TASK_NAME, "--info"); + multiprojectTestProject.build("clean", ":a_packaged:jibExportDockerContext", "--info"); BuildTask classesTask = buildResult.task(":a_packaged:classes"); - BuildTask jibTask = buildResult.task(":a_packaged:" + JibPlugin.DOCKER_CONTEXT_TASK_NAME); + BuildTask jibTask = buildResult.task(":a_packaged:jibExportDockerContext"); Assert.assertNotNull(classesTask); Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); Assert.assertNotNull(jibTask); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java new file mode 100644 index 0000000000..64c7d53fab --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; + +/** Helper class to run integration tests. */ +public class JibRunHelper { + + static String buildAndRun( + TestProject testProject, String imageReference, String... extraRunArguments) + throws IOException, InterruptedException { + BuildResult buildResult = + testProject.build("clean", "jib", "-D_TARGET_IMAGE=" + imageReference); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + return pullAndRunBuiltImage(imageReference, extraRunArguments); + } + + static void buildAndRunAdditionalTag( + TestProject testProject, String imageReference, String additionalTag, String expectedOutput) + throws InvalidImageReferenceException, IOException, InterruptedException { + BuildResult buildResult = + testProject.build( + "clean", + "jib", + "-D_TARGET_IMAGE=" + imageReference, + "-D_ADDITIONAL_TAG=" + additionalTag); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + String additionalImageReference = + ImageReference.parse(imageReference).withTag(additionalTag).toString(); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString(additionalImageReference)); + + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(imageReference)); + Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(additionalImageReference)); + assertCreationTimeEpoch(imageReference); + assertCreationTimeEpoch(additionalImageReference); + } + + static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) + throws IOException, InterruptedException { + BuildResult buildResult = + testProject.build("clean", "jibDockerBuild", "-D_TARGET_IMAGE=" + imageReference); + assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + /** + * Pulls a built image and attempts to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @param extraRunArguments extra arguments passed to {@code docker run} + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + List command = new ArrayList<>(Arrays.asList("docker", "run", "--rm")); + command.addAll(Arrays.asList(extraRunArguments)); + command.add(imageReference); + return new Command(command).run(); + } + + /** + * Asserts that the test project build output indicates a success. + * + * @param buildResult the builds results of the project under test + * @param taskName the name of the Jib task that was run + * @param successMessage a Jib-specific success message to check for + */ + static void assertBuildSuccess(BuildResult buildResult, String taskName, String successMessage) { + BuildTask classesTask = buildResult.task(":classes"); + BuildTask jibTask = buildResult.task(":" + taskName); + + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(successMessage)); + } + + static void assertCreationTimeEpoch(String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java new file mode 100644 index 0000000000..64411271c4 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building WAR images. */ +public class WarIntegrationTest { + + @ClassRule + public static final TestProject jettyServlet25Project = new TestProject("war_jetty_servlet25"); + + @Nullable + private static String getContent(URL url) throws InterruptedException { + for (int i = 0; i < 40; i++) { + Thread.sleep(500); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + int code = connection.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteStreams.copy(connection.getInputStream(), out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } catch (IOException ex) { + } + } + return null; + } + + @Test + public void testBuild_jettyServlet25Project() throws IOException, InterruptedException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/war_jetty_servlet25:gradle" + + System.nanoTime(); + + String containerName = + JibRunHelper.buildAndRun(jettyServlet25Project, targetImage, "--detach", "-p8080:8080"); + try { + String content = getContent(new URL("http://localhost:8080/hello")); + Assert.assertEquals("Hello world", content); + } finally { + new Command("docker", "stop", containerName.trim()).run(); + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle index 316276249b..d8fb81ef62 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle @@ -1,26 +1,26 @@ plugins { - id 'java' - id 'com.google.cloud.tools.jib' + id 'java' + id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + mavenCentral() } dependencies { - compile files('libs/dependency-1.0.0.jar') + compile files('libs/dependency-1.0.0.jar') } jib { - container { - args = ['An argument.'] - ports = ['1000/tcp', '2000-2003/udp'] - labels = [key1:'value1', key2:'value2'] - } + container { + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index c1bce5d2a2..7226c1588f 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.google.cloud.tools.jib' + id 'com.google.cloud.tools.jib' } // Applies the java plugin after Jib to make sure it works in this order. @@ -9,23 +9,23 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + mavenCentral() } jib { - to { - image = System.getProperty("_TARGET_IMAGE") - credHelper = 'gcr' - } - container { - ports = ['1000/tcp', '2000-2003/udp'] - labels = [key1:'value1', key2:'value2'] - } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true } def additionalTag = System.getProperty("_ADDITIONAL_TAG") if (additionalTag != null && !additionalTag.isEmpty()) { - jib.to.tags = [System.getProperty("_ADDITIONAL_TAG")] + jib.to.tags = [System.getProperty("_ADDITIONAL_TAG")] } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 3d91c631bd..2cf938a3c2 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -1,32 +1,32 @@ plugins { - id 'java' - id 'com.google.cloud.tools.jib' + id 'java' + id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + mavenCentral() } dependencies { - compile files('libs/dependency-1.0.0.jar') + compile files('libs/dependency-1.0.0.jar') } jib { - to { - image = System.getProperty("_TARGET_IMAGE") - credHelper = 'gcr' - } - container { - useCurrentTimestamp = true - args = ['An argument.'] - ports = ['1000/tcp', '2000-2003/udp'] - labels = [key1:'value1', key2:'value2'] - } - extraDirectory = file('src/main/custom-extra-dir') + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + useCurrentTimestamp = true + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } + extraDirectory = file('src/main/custom-extra-dir') - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index 988b50d5e9..0fd21b3dd4 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -1,46 +1,46 @@ plugins { - id 'java' - id 'com.google.cloud.tools.jib' + id 'java' + id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + mavenCentral() } dependencies { - compile files('libs/dependency-1.0.0.jar') + compile files('libs/dependency-1.0.0.jar') } jib { - from { - image = 'localhost:5000/distroless/java' - auth { - username = 'testuser' - password = 'testpassword' - } + from { + image = 'localhost:5000/distroless/java' + auth { + username = 'testuser' + password = 'testpassword' } - to { - image = System.getProperty("_TARGET_IMAGE") - auth { - username = System.getProperty("_TARGET_USERNAME") - password = System.getProperty("_TARGET_PASSWORD") - } + } + to { + image = System.getProperty("_TARGET_IMAGE") + auth { + username = System.getProperty("_TARGET_USERNAME") + password = System.getProperty("_TARGET_PASSWORD") } - container { - useCurrentTimestamp = true - args = ['An argument.'] - mainClass = 'com.test.HelloWorld' - jvmFlags = ['-Xms512m', '-Xdebug'] - environment = [env1:'envvalue1', env2:'envvalue2'] - ports = ['1000/tcp', '2000-2003/udp'] - labels = [key1:'value1', key2:'value2'] - } - allowInsecureRegistries = true - extraDirectory = file('src/main/custom-extra-dir') + } + container { + useCurrentTimestamp = true + args = ['An argument.'] + mainClass = 'com.test.HelloWorld' + jvmFlags = ['-Xms512m', '-Xdebug'] + environment = [env1:'envvalue1', env2:'envvalue2'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + } + allowInsecureRegistries = true + extraDirectory = file('src/main/custom-extra-dir') - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle new file mode 100644 index 0000000000..dfded0f525 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'javax.servlet:servlet-api:2.5' + moreLibs 'javax.annotation:javax.annotation-api:1.2' // random extra JAR +} + +war { + from ('src/extra_static') + from ('src/extra_js', { into 'js' }) + classpath configurations.moreLibs +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..05618c6777 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..65b9fe16a6 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

Hello World!

+ + + + + + + + +
Available Servlets:
The HelloWorld servlet
+ + diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle index adb59af775..4832a01f69 100644 --- a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle @@ -1,22 +1,22 @@ plugins { - id 'java' - id 'com.google.cloud.tools.jib' + id 'java' + id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + mavenCentral() } dependencies { - compile files('libs/dependency-1.0.0.jar') + compile files('libs/dependency-1.0.0.jar') } jib { - to { - image = System.getProperty("_TARGET_IMAGE") - } - extraDirectory = file('src/main/custom-extra-dir') + to { + image = System.getProperty("_TARGET_IMAGE") + } + extraDirectory = file('src/main/custom-extra-dir') } From 035b2e831e1b1af80c7955f40dcfd7fe5af4ced5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 4 Oct 2018 16:32:24 -0400 Subject: [PATCH 0269/2020] Break up JibPluginIntegrationTest (#1098) --- .../DefaultTargetProjectIntegrationTest.java | 85 +++++++++ .../gradle/EmptyProjectIntegrationTest.java | 95 ++++++++++ .../gradle/MultiProjectIntegrationTest.java | 47 +++++ ...java => SingleProjectIntegrationTest.java} | 170 +++++------------- ...st.java => WarProjectIntegrationTest.java} | 4 +- 5 files changed, 271 insertions(+), 130 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java rename jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/{JibPluginIntegrationTest.java => SingleProjectIntegrationTest.java} (75%) rename jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/{WarIntegrationTest.java => WarProjectIntegrationTest.java} (96%) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java new file mode 100644 index 0000000000..6f8951f5b1 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import java.io.IOException; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building "default-target" project images. */ +public class DefaultTargetProjectIntegrationTest { + + @ClassRule + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); + + /** + * Asserts that the test project has the required exposed ports and labels. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspect = new Command("docker", "inspect", imageReference).run(); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); + } + + @Test + public void testBuild_defaultTarget() { + // Test error when 'to' is missing + try { + defaultTargetTestProject.build("clean", "jib", "-x=classes"); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Missing target image parameter, perhaps you should add a 'jib.to.image' " + + "configuration parameter to your build.gradle or set the parameter via the " + + "commandline (e.g. 'gradle jib --image ').")); + } + } + + @Test + public void testDockerDaemon_defaultTarget() throws IOException, InterruptedException { + Assert.assertEquals( + "Hello, world. An argument.\n", + JibRunHelper.buildToDockerDaemonAndRun( + defaultTargetTestProject, "default-target-name:default-target-version")); + assertDockerInspect("default-target-name:default-target-version"); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java new file mode 100644 index 0000000000..189b866696 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.io.IOException; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building empty project images. */ +public class EmptyProjectIntegrationTest { + + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); + + /** + * Asserts that the test project has the required exposed ports and labels. + * + * @param imageReference the image to test + * @throws IOException if the {@code docker inspect} command fails to run + * @throws InterruptedException if the {@code docker inspect} command is interrupted + */ + private static void assertDockerInspect(String imageReference) + throws IOException, InterruptedException { + String dockerInspect = new Command("docker", "inspect", imageReference).run(); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"ExposedPorts\": {\n" + + " \"1000/tcp\": {},\n" + + " \"2000/udp\": {},\n" + + " \"2001/udp\": {},\n" + + " \"2002/udp\": {},\n" + + " \"2003/udp\": {}")); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Labels\": {\n" + + " \"key1\": \"value1\",\n" + + " \"key2\": \"value2\"\n" + + " }")); + } + + @Test + public void testBuild_empty() throws IOException, InterruptedException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/emptyimage:gradle" + + System.nanoTime(); + Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); + assertDockerInspect(targetImage); + JibRunHelper.assertCreationTimeEpoch(targetImage); + } + + @Test + public void testBuild_multipleTags() + throws IOException, InterruptedException, InvalidImageReferenceException { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/multitag-image:gradle" + + System.nanoTime(); + JibRunHelper.buildAndRunAdditionalTag( + emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); + assertDockerInspect(targetImage); + } + + @Test + public void testDockerDaemon_empty() throws IOException, InterruptedException { + String targetImage = "emptyimage:gradle" + System.nanoTime(); + Assert.assertEquals("", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage)); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + assertDockerInspect(targetImage); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java new file mode 100644 index 0000000000..297613b9e6 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building multi-project images. */ +public class MultiProjectIntegrationTest { + + @ClassRule + public static final TestProject multiprojectTestProject = new TestProject("multiproject"); + + @Test + public void testMultiProject() { + BuildResult buildResult = + multiprojectTestProject.build("clean", ":a_packaged:jibExportDockerContext", "--info"); + + BuildTask classesTask = buildResult.task(":a_packaged:classes"); + BuildTask jibTask = buildResult.task(":a_packaged:jibExportDockerContext"); + Assert.assertNotNull(classesTask); + Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java similarity index 75% rename from jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java rename to jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index f5b9b9efd3..ffa4e27dbe 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibPluginIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.file.Files; @@ -33,8 +32,8 @@ import org.junit.ClassRule; import org.junit.Test; -/** Integration tests for {@link JibPlugin}. */ -public class JibPluginIntegrationTest { +/** Integration tests for building single project images. */ +public class SingleProjectIntegrationTest { @ClassRule public static final LocalRegistry localRegistry1 = @@ -44,45 +43,8 @@ public class JibPluginIntegrationTest { public static final LocalRegistry localRegistry2 = new LocalRegistry(6000, "testuser2", "testpassword2"); - @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); - @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject multiprojectTestProject = new TestProject("multiproject"); - - @ClassRule - public static final TestProject defaultTargetTestProject = new TestProject("default-target"); - - @Before - public void setup() throws IOException, InterruptedException { - // Pull distroless and push to local registry so we can test 'from' credentials - localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); - } - - private static String buildAndRunComplex( - String imageReference, String username, String password, LocalRegistry targetRegistry) - throws IOException, InterruptedException { - BuildResult buildResult = - simpleTestProject.build( - "clean", - "jib", - "-D_TARGET_IMAGE=" + imageReference, - "-D_TARGET_USERNAME=" + username, - "-D_TARGET_PASSWORD=" + password, - "-DsendCredentialsOverHttp=true", - "-b=complex-build.gradle"); - - JibRunHelper.assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); - - targetRegistry.pull(imageReference); - assertDockerInspect(imageReference); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", "--rm", imageReference).run(); - } - /** * Asserts that the creation time of the simple test project is set. If the time parsed from the * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the @@ -129,29 +91,33 @@ private static void assertDockerInspect(String imageReference) + " }")); } - @Test - public void testBuild_empty() throws IOException, InterruptedException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/emptyimage:gradle" - + System.nanoTime(); - Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); - assertDockerInspect(targetImage); - JibRunHelper.assertCreationTimeEpoch(targetImage); + private static String buildAndRunComplex( + String imageReference, String username, String password, LocalRegistry targetRegistry) + throws IOException, InterruptedException { + BuildResult buildResult = + simpleTestProject.build( + "clean", + "jib", + "-D_TARGET_IMAGE=" + imageReference, + "-D_TARGET_USERNAME=" + username, + "-D_TARGET_PASSWORD=" + password, + "-DsendCredentialsOverHttp=true", + "-b=complex-build.gradle"); + + JibRunHelper.assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); + + targetRegistry.pull(imageReference); + assertDockerInspect(imageReference); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + return new Command("docker", "run", "--rm", imageReference).run(); } - @Test - public void testBuild_multipleTags() - throws IOException, InterruptedException, InvalidImageReferenceException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/multitag-image:gradle" - + System.nanoTime(); - JibRunHelper.buildAndRunAdditionalTag( - emptyTestProject, targetImage, "gradle-2" + System.nanoTime(), ""); - assertDockerInspect(targetImage); + @Before + public void setup() throws IOException, InterruptedException { + // Pull distroless and push to local registry so we can test 'from' credentials + localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); } @Test @@ -182,22 +148,6 @@ public void testBuild_simple() throws IOException, InterruptedException { assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } - @Test - public void testBuild_defaultTarget() { - // Test error when 'to' is missing - try { - defaultTargetTestProject.build("clean", "jib", "-x=classes"); - Assert.fail(); - } catch (UnexpectedBuildFailure ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "Missing target image parameter, perhaps you should add a 'jib.to.image' " - + "configuration parameter to your build.gradle or set the parameter via the " - + "commandline (e.g. 'gradle jib --image ').")); - } - } - @Test public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); @@ -218,16 +168,6 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } - @Test - public void testDockerDaemon_empty() throws IOException, InterruptedException { - String targetImage = "emptyimage:gradle" + System.nanoTime(); - Assert.assertEquals("", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage)); - Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - assertDockerInspect(targetImage); - } - @Test public void testDockerDaemon_simple() throws IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); @@ -239,36 +179,6 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException { assertDockerInspect(targetImage); } - @Test - public void testDockerDaemon_defaultTarget() throws IOException, InterruptedException { - Assert.assertEquals( - "Hello, world. An argument.\n", - JibRunHelper.buildToDockerDaemonAndRun( - defaultTargetTestProject, "default-target-name:default-target-version")); - assertDockerInspect("default-target-name:default-target-version"); - } - - @Test - public void testBuildTar_simple() throws IOException, InterruptedException { - String targetImage = "simpleimage:gradle" + System.nanoTime(); - - String outputPath = - simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); - Instant beforeBuild = Instant.now(); - BuildResult buildResult = - simpleTestProject.build("clean", "jibBuildTar", "-D_TARGET_IMAGE=" + targetImage); - - JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); - Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); - - new Command("docker", "load", "--input", outputPath).run(); - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - new Command("docker", "run", "--rm", targetImage).run()); - assertDockerInspect(targetImage); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); - } - @Test public void testDockerContext() throws IOException, InterruptedException { BuildResult buildResult = simpleTestProject.build("clean", "jibExportDockerContext", "--info"); @@ -325,17 +235,23 @@ public void testDockerContext() throws IOException, InterruptedException { } @Test - public void testMultiProject() { + public void testBuildTar_simple() throws IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + + String outputPath = + simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); + Instant beforeBuild = Instant.now(); BuildResult buildResult = - multiprojectTestProject.build("clean", ":a_packaged:jibExportDockerContext", "--info"); + simpleTestProject.build("clean", "jibBuildTar", "-D_TARGET_IMAGE=" + targetImage); - BuildTask classesTask = buildResult.task(":a_packaged:classes"); - BuildTask jibTask = buildResult.task(":a_packaged:jibExportDockerContext"); - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); + JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); + + new Command("docker", "load", "--input", outputPath).run(); + Assert.assertEquals( + "Hello, world. An argument.\nfoo\ncat\n", + new Command("docker", "run", "--rm", targetImage).run()); + assertDockerInspect(targetImage); + assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java similarity index 96% rename from jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java rename to jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 64411271c4..b622020e8d 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -24,18 +24,16 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -import javax.annotation.Nullable; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; /** Integration tests for building WAR images. */ -public class WarIntegrationTest { +public class WarProjectIntegrationTest { @ClassRule public static final TestProject jettyServlet25Project = new TestProject("war_jetty_servlet25"); - @Nullable private static String getContent(URL url) throws InterruptedException { for (int i = 0; i < 40; i++) { Thread.sleep(500); From 0bcf0f981f2f9413344f199877e02bc155bcfea4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 4 Oct 2018 17:56:34 -0400 Subject: [PATCH 0270/2020] Add Tomcat WAR integration test (#1097) --- .../cloud/tools/jib/gradle/JibRunHelper.java | 57 +++++++++++-------- .../jib/gradle/WarProjectIntegrationTest.java | 47 ++++++++++----- .../war_servlet25/build-tomcat.gradle | 42 ++++++++++++++ .../build.gradle | 0 .../src/extra_js/bogus.js | 0 .../src/extra_static/bogus.html | 0 .../src/main/java/example/HelloWorld.java | 0 .../src/main/resources/world | 0 .../src/main/webapp/META-INF/MANIFEST.MF | 0 .../src/main/webapp/WEB-INF/web.xml | 0 .../src/main/webapp/index.html | 0 11 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/extra_js/bogus.js (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/extra_static/bogus.html (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/main/java/example/HelloWorld.java (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/main/resources/world (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/main/webapp/META-INF/MANIFEST.MF (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/main/webapp/WEB-INF/web.xml (100%) rename jib-gradle-plugin/src/integration-test/resources/projects/{war_jetty_servlet25 => war_servlet25}/src/main/webapp/index.html (100%) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 64c7d53fab..4460cea302 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -32,11 +32,20 @@ /** Helper class to run integration tests. */ public class JibRunHelper { + static String buildAndRun(TestProject testProject, String imageReference) + throws IOException, InterruptedException { + return buildAndRun(testProject, imageReference, "build.gradle"); + } + static String buildAndRun( - TestProject testProject, String imageReference, String... extraRunArguments) + TestProject testProject, + String imageReference, + String gradleBuildFile, + String... extraRunArguments) throws IOException, InterruptedException { BuildResult buildResult = - testProject.build("clean", "jib", "-D_TARGET_IMAGE=" + imageReference); + testProject.build( + "clean", "jib", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); @@ -78,28 +87,6 @@ static String buildToDockerDaemonAndRun(TestProject testProject, String imageRef return new Command("docker", "run", "--rm", imageReference).run(); } - /** - * Pulls a built image and attempts to run it. Also verifies the container configuration and - * history of the built image. - * - * @param imageReference the image reference of the built image - * @param extraRunArguments extra arguments passed to {@code docker run} - * @return the container output - * @throws IOException if an I/O exception occurs - * @throws InterruptedException if the process was interrupted - */ - private static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) - throws IOException, InterruptedException { - new Command("docker", "pull", imageReference).run(); - String history = new Command("docker", "history", imageReference).run(); - Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - - List command = new ArrayList<>(Arrays.asList("docker", "run", "--rm")); - command.addAll(Arrays.asList(extraRunArguments)); - command.add(imageReference); - return new Command(command).run(); - } - /** * Asserts that the test project build output indicates a success. * @@ -124,4 +111,26 @@ static void assertCreationTimeEpoch(String imageReference) "1970-01-01T00:00:00Z", new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } + + /** + * Pulls a built image and attempts to run it. Also verifies the container configuration and + * history of the built image. + * + * @param imageReference the image reference of the built image + * @param extraRunArguments extra arguments passed to {@code docker run} + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) + throws IOException, InterruptedException { + new Command("docker", "pull", imageReference).run(); + String history = new Command("docker", "history", imageReference).run(); + Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + List command = new ArrayList<>(Arrays.asList("docker", "run", "--rm")); + command.addAll(Arrays.asList(extraRunArguments)); + command.add(imageReference); + return new Command(command).run(); + } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index b622020e8d..d899d266c2 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -24,6 +24,8 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import org.junit.After; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -31,9 +33,9 @@ /** Integration tests for building WAR images. */ public class WarProjectIntegrationTest { - @ClassRule - public static final TestProject jettyServlet25Project = new TestProject("war_jetty_servlet25"); + @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); + @Nullable private static String getContent(URL url) throws InterruptedException { for (int i = 0; i < 40; i++) { Thread.sleep(500); @@ -51,21 +53,36 @@ private static String getContent(URL url) throws InterruptedException { return null; } - @Test - public void testBuild_jettyServlet25Project() throws IOException, InterruptedException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/war_jetty_servlet25:gradle" - + System.nanoTime(); + private String containerName; - String containerName = - JibRunHelper.buildAndRun(jettyServlet25Project, targetImage, "--detach", "-p8080:8080"); - try { - String content = getContent(new URL("http://localhost:8080/hello")); - Assert.assertEquals("Hello world", content); - } finally { + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { new Command("docker", "stop", containerName.trim()).run(); } } + + @Test + public void testBuild_jettyServlet25Project() throws IOException, InterruptedException { + buildAndRunDetached(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); + + String content = getContent(new URL("http://localhost:8080/hello")); + Assert.assertEquals("Hello world", content); + } + + @Test + public void testBuild_tomcatServlet25Project() throws IOException, InterruptedException { + buildAndRunDetached(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); + + String content = getContent(new URL("http://localhost:8080/hello")); + Assert.assertEquals("Hello world", content); + } + + private void buildAndRunDetached(TestProject project, String imageName, String gradleBuildFile) + throws IOException, InterruptedException { + String repository = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; + String targetImage = repository + imageName + System.nanoTime(); + containerName = + JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); + } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle new file mode 100644 index 0000000000..a71aa65fc3 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +configurations { + moreLibs +} + +dependencies { + providedCompile 'javax.servlet:servlet-api:2.5' + moreLibs 'javax.annotation:javax.annotation-api:1.2' // random extra JAR +} + +war { + from ('src/extra_static') + from ('src/extra_js', { into 'js' }) + classpath configurations.moreLibs +} + +jib { + from.image = 'tomcat:8.5-jre8-alpine' + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + appRoot = '/usr/local/tomcat/webapps/ROOT' + entrypoint = ['catalina.sh', 'run'] + } + + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_js/bogus.js similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_js/bogus.js rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_js/bogus.js diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_static/bogus.html similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/extra_static/bogus.html rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_static/bogus.html diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/java/example/HelloWorld.java rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/resources/world rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/META-INF/MANIFEST.MF rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/WEB-INF/web.xml rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/index.html similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_jetty_servlet25/src/main/webapp/index.html rename to jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/index.html From dbaa762ec3bd065e926ef5205b36f58f216081d5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 5 Oct 2018 11:20:55 -0400 Subject: [PATCH 0271/2020] Minor cleanups and fixes (#1102) --- .../jib/gradle/GradleLayerConfigurations.java | 2 +- .../jib/maven/MavenLayerConfigurations.java | 35 ++++++++-------- .../maven/PluginConfigurationProcessor.java | 16 ++++--- .../jib/maven/DockerContextMojoTest.java | 24 +++++------ .../maven/MavenLayerConfigurationsTest.java | 42 ++++++++----------- 5 files changed, 59 insertions(+), 60 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 1633553efb..4ecf340167 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -63,7 +63,7 @@ static JavaLayerConfigurations getForProject( } /** - * Resolves the {@link JavaLayerConfigurations} for a non-war Gradle {@link Project}. + * Resolves the {@link JavaLayerConfigurations} for a non-WAR Gradle {@link Project}. * * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 17a6edb1e0..292d0b5639 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -34,6 +34,7 @@ /** Builds {@link JavaLayerConfigurations} based on inputs from a {@link MavenProject}. */ class MavenLayerConfigurations { + /** * Resolves the {@link JavaLayerConfigurations} for a {@link MavenProject}. * @@ -53,7 +54,7 @@ static JavaLayerConfigurations getForProject( } /** - * Resolves the source files configuration for a non-war {@link MavenProject}. + * Resolves the {@link JavaLayerConfigurations} for a non-WAR {@link MavenProject}. * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer @@ -109,7 +110,7 @@ private static JavaLayerConfigurations getForNonWarProject( } /** - * Resolves the source files configuration for a War {@link MavenProject}. + * Resolves the {@link JavaLayerConfigurations} for a WAR {@link MavenProject}. * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer @@ -126,22 +127,25 @@ private static JavaLayerConfigurations getForWarProject( // at build.getFinalName(). Path explodedWarPath = Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib/"); - AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes/"); + Path webInfClasses = explodedWarPath.resolve("WEB-INF/classes"); + Path webInfLib = explodedWarPath.resolve("WEB-INF/lib"); + + AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib"); + AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes"); Builder layerBuilder = JavaLayerConfigurations.builder(); // Gets all the dependencies. Predicate isSnapshotDependency = path -> path.toString().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX); - if (Files.exists(explodedWarPath.resolve("WEB-INF/lib"))) { + if (Files.exists(webInfLib)) { addFilesToLayer( - explodedWarPath.resolve("WEB-INF/lib/"), + webInfLib, isSnapshotDependency, dependenciesExtractionPath, layerBuilder::addSnapshotDependencyFile); addFilesToLayer( - explodedWarPath.resolve("WEB-INF/lib/"), + webInfLib, isSnapshotDependency.negate(), dependenciesExtractionPath, layerBuilder::addDependencyFile); @@ -149,23 +153,20 @@ private static JavaLayerConfigurations getForWarProject( // Gets the classes files in the 'WEB-INF/classes' output directory. Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - if (Files.exists(explodedWarPath.resolve("WEB-INF/classes"))) { + if (Files.exists(webInfClasses)) { addFilesToLayer( - explodedWarPath.resolve("WEB-INF/classes/"), - isClassFile, - classesExtractionPath, - layerBuilder::addClassFile); + webInfClasses, isClassFile, classesExtractionPath, layerBuilder::addClassFile); } - // Gets the resources - Predicate isResources = + // Gets the resources. + Predicate isResource = path -> { - boolean inWebInfClasses = path.startsWith(explodedWarPath.resolve("WEB-INF/classes/")); - boolean inWebInfLib = path.startsWith(explodedWarPath.resolve("WEB-INF/lib/")); + boolean inWebInfClasses = path.startsWith(webInfClasses); + boolean inWebInfLib = path.startsWith(webInfLib); return (!inWebInfClasses && !inWebInfLib) || (inWebInfClasses && !isClassFile.test(path)); }; - addFilesToLayer(explodedWarPath, isResources, appRoot, layerBuilder::addResourceFile); + addFilesToLayer(explodedWarPath, isResource, appRoot, layerBuilder::addResourceFile); // Adds all the extra files. if (Files.exists(extraDirectory)) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index f8130164b7..fd0d864876 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -41,11 +41,12 @@ /** Configures and provides builders for the image building goals. */ class PluginConfigurationProcessor { + /** - * Returns true if the maven packaging type is "war" + * Returns true if the Maven packaging type is "war". * * @param jibPluginConfiguration the Jib plugin configuration - * @return true if the maven packaging type is "war" + * @return true if the Maven packaging type is "war" */ private static boolean isWarPackaging(JibPluginConfiguration jibPluginConfiguration) { return "war".equals(jibPluginConfiguration.getProject().getPackaging()); @@ -218,12 +219,12 @@ static ImageReference parseImageReference(String image, String type) { } /** - * Compute the container entrypoint, in this order : + * Compute the container entrypoint, in this order: * *
    *
  1. the user specified one, if set - *
  2. for a war project, the jetty default one - *
  3. for a jar project, by resolving the main class + *
  4. for a WAR project, the Jetty default one + *
  5. for a non-WAR project, by resolving the main class *
* * @param logger the logger used to display messages. @@ -231,7 +232,8 @@ static ImageReference parseImageReference(String image, String type) { * data * @param projectProperties used for providing additional information * @return the entrypoint - * @throws MojoExecutionException if the http timeout system property is misconfigured + * @throws MojoExecutionException if resolving the main class fails or the app root parameter is + * not an absolute path in Unix-style */ static List computeEntrypoint( Log logger, @@ -245,9 +247,11 @@ static List computeEntrypoint( } return jibPluginConfiguration.getEntrypoint(); } + if (isWarPackaging(jibPluginConfiguration)) { return JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); } + String mainClass = projectProperties.getMainClass(jibPluginConfiguration); return JavaEntrypointConstructor.makeDefaultEntrypoint( getAppRootChecked(jibPluginConfiguration), jibPluginConfiguration.getJvmFlags(), mainClass); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 5e99affcd1..783f408a29 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -136,18 +136,6 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } } - private String getEntrypoint() throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); - } - - private String getBaseImage() throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("FROM")).findFirst().get(); - } - @Test public void testBaseImage_nonWarPackaging() throws MojoExecutionException, IOException { mojo.execute(); @@ -259,4 +247,16 @@ String getAppRoot() { Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getEntrypoint()); } + + private String getEntrypoint() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); + } + + private String getBaseImage() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("FROM")).findFirst().get(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index a38320563a..6cdc49bae0 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -105,6 +105,8 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio @Mock private MavenLayerConfigurations.FileToLayerAdder fileToLayerAdder; + private Path extraFilesDirectory; + @Before public void setUp() throws URISyntaxException, IOException { Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); @@ -116,8 +118,8 @@ public void setUp() throws URISyntaxException, IOException { ImmutableSet.of( makeArtifact(Paths.get("application", "dependencies", "libraryB.jar")), makeArtifact(Paths.get("application", "dependencies", "libraryA.jar")), - // maven reads and populates "Artifacts" with it's own processing, so read some from - // a repository + // Maven reads and populates "Artifacts" with its own processing, so read some from a + // repository testRepository.findArtifact("com.test", "dependency", "1.0.0"), testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); @@ -126,6 +128,8 @@ public void setUp() throws URISyntaxException, IOException { Paths.get(Resources.getResource("webapp").toURI()) .resolve("final-name/WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); + + extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); } @Test @@ -166,9 +170,7 @@ public void test_correctFiles() throws URISyntaxException, IOException { } @Test - public void test_extraFiles() throws URISyntaxException, IOException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - + public void test_extraFiles() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); JavaLayerConfigurations javaLayerConfigurations = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); @@ -184,9 +186,7 @@ public void test_extraFiles() throws URISyntaxException, IOException { } @Test - public void testGetForProject_nonDefaultAppRoot() throws URISyntaxException, IOException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - + public void testGetForProject_nonDefaultAppRoot() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); JavaLayerConfigurations configuration = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); @@ -333,8 +333,6 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); JavaLayerConfigurations configuration = MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); @@ -356,6 +354,11 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), // Not sure about that outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")); + ImmutableList expectedExtraFiles = + ImmutableList.of( + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")); assertSourcePathsUnordered( expectedDependenciesFiles, configuration.getDependencyLayerEntries()); @@ -363,6 +366,7 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); + assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); assertExtractionPathsUnordered( Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), @@ -389,8 +393,7 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, } @Test - public void testGetForJarProject_nonDefaultAppRoot() throws URISyntaxException, IOException { - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + public void testGetForJarProject_nonDefaultAppRoot() throws IOException { // Test when the default packaging is set Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); @@ -402,8 +405,7 @@ public void testGetForJarProject_nonDefaultAppRoot() throws URISyntaxException, } @Test - public void testGetForWarProject_noErrorIfWebInfDoesNotExist() - throws IOException, URISyntaxException { + public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOException { temporaryFolder.newFolder("final-name"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); Mockito.when(mockBuild.getDirectory()) @@ -411,15 +413,12 @@ public void testGetForWarProject_noErrorIfWebInfDoesNotExist() Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - MavenLayerConfigurations.getForProject( mockMavenProject, extraFilesDirectory, appRoot); // should pass } @Test - public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() - throws IOException, URISyntaxException { + public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOException { temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); Mockito.when(mockBuild.getDirectory()) @@ -427,15 +426,12 @@ public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - MavenLayerConfigurations.getForProject( mockMavenProject, extraFilesDirectory, appRoot); // should pass } @Test - public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() - throws IOException, URISyntaxException { + public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOException { temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); Mockito.when(mockBuild.getDirectory()) @@ -443,8 +439,6 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); - MavenLayerConfigurations.getForProject( mockMavenProject, extraFilesDirectory, appRoot); // should pass } From bd1d8f09a4dbcd79da12640893a513dbcb77f72f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 5 Oct 2018 11:42:13 -0400 Subject: [PATCH 0272/2020] Add Jetty/Tomcat WAR integration tests for Maven (#1100) --- .../jib/gradle/WarProjectIntegrationTest.java | 33 +++-- .../src/main/webapp/META-INF/MANIFEST.MF | 2 +- .../maven/BuildImageMojoIntegrationTest.java | 118 ++++++++++++------ .../cloud/tools/jib/maven/TestProject.java | 39 +++--- .../projects/war_servlet25/pom-tomcat.xml | 65 ++++++++++ .../resources/projects/war_servlet25/pom.xml | 55 ++++++++ .../war_servlet25/src/extra_js/bogus.js | 1 + .../war_servlet25/src/extra_static/bogus.html | 1 + .../src/main/java/example/HelloWorld.java | 48 +++++++ .../war_servlet25/src/main/resources/world | 1 + .../src/main/webapp/META-INF/MANIFEST.MF | 2 + .../src/main/webapp/WEB-INF/web.xml | 15 +++ .../war_servlet25/src/main/webapp/index.html | 20 +++ 13 files changed, 327 insertions(+), 73 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml create mode 100644 jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index d899d266c2..3e3087aa66 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -18,12 +18,11 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.common.io.ByteStreams; -import java.io.ByteArrayOutputStream; +import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import org.junit.After; import org.junit.Assert; @@ -41,11 +40,10 @@ private static String getContent(URL url) throws InterruptedException { Thread.sleep(500); try { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - int code = connection.getResponseCode(); - if (code == HttpURLConnection.HTTP_OK) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ByteStreams.copy(connection.getInputStream(), out); - return new String(out.toByteArray(), StandardCharsets.UTF_8); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream in = connection.getInputStream()) { + return Blobs.writeToString(Blobs.from(in)); + } } } catch (IOException ex) { } @@ -53,12 +51,12 @@ private static String getContent(URL url) throws InterruptedException { return null; } - private String containerName; + @Nullable private String containerName; @After public void tearDown() throws IOException, InterruptedException { if (containerName != null) { - new Command("docker", "stop", containerName.trim()).run(); + new Command("docker", "stop", containerName).run(); } } @@ -66,23 +64,22 @@ public void tearDown() throws IOException, InterruptedException { public void testBuild_jettyServlet25Project() throws IOException, InterruptedException { buildAndRunDetached(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); - String content = getContent(new URL("http://localhost:8080/hello")); - Assert.assertEquals("Hello world", content); + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); } @Test public void testBuild_tomcatServlet25Project() throws IOException, InterruptedException { buildAndRunDetached(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); - String content = getContent(new URL("http://localhost:8080/hello")); - Assert.assertEquals("Hello world", content); + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); } - private void buildAndRunDetached(TestProject project, String imageName, String gradleBuildFile) + private void buildAndRunDetached(TestProject project, String label, String gradleBuildFile) throws IOException, InterruptedException { - String repository = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; - String targetImage = repository + imageName + System.nanoTime(); - containerName = + String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; + String targetImage = nameBase + label + System.nanoTime(); + String output = JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); + containerName = output.trim(); } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF index 65b9fe16a6..e3c07ab38a 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -1,2 +1,2 @@ Manifest-Version: 1.0 -Class-Path: +Class-Path: diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 840165e362..834354c66a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,10 +18,14 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -30,9 +34,11 @@ import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nullable; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; @@ -54,10 +60,6 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - @ClassRule - public static final TestProject complexTestProject = - new TestProject(testPlugin, "simple", "pom-complex.xml"); - @ClassRule public static final TestProject emptyTestProject = new TestProject(testPlugin, "empty"); @@ -68,6 +70,31 @@ public class BuildImageMojoIntegrationTest { public static final TestProject defaultTargetTestProject = new TestProject(testPlugin, "default-target"); + @ClassRule + public static final TestProject servlet25Project = new TestProject(testPlugin, "war_servlet25"); + + private static String getGcrImageReference(String label) { + String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; + return nameBase + label + System.nanoTime(); + } + + @Nullable + private static String getContent(URL url) throws InterruptedException { + for (int i = 0; i < 40; i++) { + Thread.sleep(500); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream in = connection.getInputStream()) { + return Blobs.writeToString(Blobs.from(in)); + } + } + } catch (IOException ex) { + } + } + return null; + } + /** * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. @@ -81,21 +108,16 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole verifier.executeGoals(Arrays.asList("clean", "compile")); // Builds twice, and checks if the second build took less time. - verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); + verifier.executeGoal("jib:build"); float timeOne = getBuildTimeFromVerifierLog(verifier); if (runTwice) { verifier.resetStreams(); - verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); + verifier.executeGoal("jib:build"); float timeTwo = getBuildTimeFromVerifierLog(verifier); - Assert.assertTrue( - "First build time (" - + timeOne - + ") is not greater than second build time (" - + timeTwo - + ")", - timeOne > timeTwo); + String failMessage = "First build time (%s) is not greater than second build time (%s)"; + Assert.assertTrue(String.format(failMessage, timeOne, timeTwo), timeOne > timeTwo); } verifier.verifyErrorFreeLog(); @@ -112,9 +134,7 @@ private static String buildAndRunAdditionalTag( verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); verifier.setAutoclean(false); verifier.addCliOption("-X"); - verifier.executeGoals(Arrays.asList("clean", "compile")); - - verifier.executeGoal("jib:" + BuildImageMojo.GOAL_NAME); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); verifier.verifyErrorFreeLog(); String additionalImageReference = @@ -134,7 +154,7 @@ private static String buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) throws VerificationException, IOException, InterruptedException { Instant before = Instant.now(); - Verifier verifier = new Verifier(complexTestProject.getProjectRoot().toString()); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setSystemProperty("_TARGET_USERNAME", username); verifier.setSystemProperty("_TARGET_PASSWORD", password); @@ -217,26 +237,31 @@ private static void assertCreationTimeEpoch(String imageReference) new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } + @Nullable private String detachedContainerName; + @Before - public void setup() throws IOException, InterruptedException { + public void setUp() throws IOException, InterruptedException { // Pull distroless to local registry so we can test 'from' credentials localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); } + @After + public void tearDown() throws IOException, InterruptedException { + if (detachedContainerName != null) { + new Command("docker", "stop", detachedContainerName).run(); + } + } + @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/simpleimage:maven" - + System.nanoTime(); + String targetImage = getGcrImageReference("simpleimage:maven"); // Test empty output error try { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); - verifier.executeGoals(Arrays.asList("clean", "jib:" + BuildImageMojo.GOAL_NAME)); + verifier.executeGoals(Arrays.asList("clean", "jib:build")); Assert.fail(); } catch (VerificationException ex) { @@ -275,12 +300,7 @@ public void testExecute_simple() throws VerificationException, IOException, Inte @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/emptyimage:maven" - + System.nanoTime(); - + String targetImage = getGcrImageReference("emptyimage:maven"); Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); assertCreationTimeEpoch(targetImage); } @@ -289,11 +309,7 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi public void testExecute_multipleTags() throws IOException, InterruptedException, InvalidImageReferenceException, VerificationException { - String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() - + "/multitag-image:maven" - + System.nanoTime(); + String targetImage = getGcrImageReference("multitag-image:maven"); Assert.assertEquals( "", buildAndRunAdditionalTag( @@ -306,7 +322,7 @@ public void testExecute_defaultTarget() throws IOException { try { Verifier verifier = new Verifier(defaultTargetTestProject.getProjectRoot().toString()); verifier.setAutoclean(false); - verifier.executeGoals(Arrays.asList("clean", "jib:" + BuildImageMojo.GOAL_NAME)); + verifier.executeGoals(Arrays.asList("clean", "jib:build")); Assert.fail(); } catch (VerificationException ex) { @@ -341,4 +357,34 @@ public void testExecute_complex_sameFromAndToRegistry() public void testExecute_skipJibGoal() throws VerificationException, IOException { SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildImageMojo.GOAL_NAME); } + + @Test + public void testExecute_jettyServlet25() + throws VerificationException, IOException, InterruptedException { + buildAndRunWar("jetty-servlet25:maven", "pom.xml"); + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + } + + @Test + public void testExecute_tomcatServlet25() + throws VerificationException, IOException, InterruptedException { + buildAndRunWar("tomcat-servlet25:maven", "pom-tomcat.xml"); + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + } + + private void buildAndRunWar(String label, String pomXml) + throws VerificationException, IOException, InterruptedException { + String targetImage = getGcrImageReference(label); + + Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=" + pomXml); + verifier.executeGoals(Arrays.asList("clean", "package", "jib:build")); + verifier.verifyErrorFreeLog(); + + detachedContainerName = + new Command("docker", "run", "--rm", "--detach", "-p8080:8080", targetImage).run().trim(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index c75e429dc8..71f70a676c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -21,6 +21,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.maven.it.util.ResourceExtractor; import org.junit.rules.TemporaryFolder; @@ -29,22 +31,20 @@ public class TestProject extends TemporaryFolder implements Closeable { private static final String PROJECTS_PATH_IN_RESOURCES = "/projects/"; + private static boolean isPomXml(Path path) { + String filename = path.getFileName().toString(); + return filename.startsWith("pom") && filename.endsWith(".xml"); + } + private final TestPlugin testPlugin; private final String projectDir; - private final String pomFilename; private Path projectRoot; /** Initialize to a specific project directory. */ public TestProject(TestPlugin testPlugin, String projectDir) { - this(testPlugin, projectDir, "pom.xml"); - } - - /** Initialize to a specific project directory with a non-default pom.xml. */ - TestProject(TestPlugin testPlugin, String projectDir, String pomFilename) { this.testPlugin = testPlugin; this.projectDir = projectDir; - this.pomFilename = pomFilename; } /** Get the project root resolved as a real path */ @@ -52,6 +52,11 @@ public Path getProjectRoot() throws IOException { return projectRoot.toRealPath(); } + @Override + public void close() { + after(); + } + @Override protected void before() throws Throwable { super.before(); @@ -66,16 +71,14 @@ private void copyProject() throws IOException { .toPath(); // Puts the correct plugin version into the test project pom.xml. - Path pomXml = projectRoot.resolve(pomFilename); - Files.write( - pomXml, - new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) - .replace("@@PluginVersion@@", testPlugin.getVersion()) - .getBytes(StandardCharsets.UTF_8)); - } - - @Override - public void close() { - after(); + try (Stream files = Files.list(projectRoot)) { + for (Path pomXml : files.filter(TestProject::isPomXml).collect(Collectors.toList())) { + Files.write( + pomXml, + new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) + .replace("@@PluginVersion@@", testPlugin.getVersion()) + .getBytes(StandardCharsets.UTF_8)); + } + } } } diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml new file mode 100644 index 0000000000..7c2bbbbb3b --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml @@ -0,0 +1,65 @@ + + 4.0.0 + + com.test + servlet25 + 1 + war + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + javax.servlet + servlet-api + 2.5 + provided + + + javax.annotation + javax.annotation-api + 1.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + tomcat:8.5-jre8-alpine + + + ${_TARGET_IMAGE} + + + /usr/local/tomcat/webapps/ROOT + + catalina.sh + run + + + + true + + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml new file mode 100644 index 0000000000..06e0ed6e08 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + com.test + servlet25 + 1 + war + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + javax.servlet + servlet-api + 2.5 + provided + + + javax.annotation + javax.annotation-api + 1.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + true + + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..05618c6777 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e3c07ab38a --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

Hello World!

+ + + + + + + + +
Available Servlets:
The HelloWorld servlet
+ + From 2e343b97e62c52277d121363468677d2c231a8e4 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Fri, 5 Oct 2018 19:45:00 +0200 Subject: [PATCH 0273/2020] Upgrade / add Gradle wrappers (#1095) --- .gitattributes | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/helloworld/gradlew | 172 ++++++++++ examples/helloworld/gradlew.bat | 84 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/micronaut/gradlew.bat | 168 ++++----- .../gradle/wrapper/gradle-wrapper.jar | Bin 54712 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- examples/multi-module/gradlew.bat | 168 ++++----- examples/multi-module/mvnw.cmd | 322 +++++++++--------- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/spring-boot-draft/gradlew | 172 ++++++++++ examples/spring-boot-draft/gradlew.bat | 84 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/spring-boot-kubernetes/gradlew.bat | 168 ++++----- examples/spring-boot-kubernetes/mvnw.cmd | 322 +++++++++--------- jib-core/build.gradle | 4 - jib-core/gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- jib-core/gradlew.bat | 168 ++++----- jib-gradle-plugin/build.gradle | 4 - .../gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- jib-gradle-plugin/gradlew.bat | 168 ++++----- jib-maven-plugin/mvnw.cmd | 290 ++++++++-------- jib-plugins-common/build.gradle | 4 - .../gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- jib-plugins-common/gradlew.bat | 168 ++++----- 33 files changed, 1502 insertions(+), 991 deletions(-) create mode 100644 .gitattributes create mode 100644 examples/helloworld/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/helloworld/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/helloworld/gradlew create mode 100644 examples/helloworld/gradlew.bat create mode 100644 examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/spring-boot-draft/gradlew create mode 100644 examples/spring-boot-draft/gradlew.bat diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..24a7442795 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.bat eol=crlf +*.cmd eol=crlf diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.jar b/examples/helloworld/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..29953ea141f55e3b8fc691d31b5ca8816d89fa87 GIT binary patch literal 56177 zcmagFV{~WVwk?_pE4FRhwr$(CRk3Z`c2coz+fFL^#m=jD_df5v|GoR1_hGCxKaAPt z?5)i;2YO!$(jcHHKtMl#0s#RD{xu*V;Q#dm0)qVemK9YIq?MEtqXz*}_=jUJ`nb5z zUkCNS_ILXK>nJNICn+YXtU@O%b}u_MDI-lwHxDaKOEoh!+oZ&>#JqQWH$^)pIW0R) zElKkO>LS!6^{7~jvK^hY^r+ZqY@j9c3=``N6W|1J`tiT5`FENBXLF!`$M#O<|Hr=m zzdq3a_Az%dG_f)LA6=3E>FVxe=-^=L^nXkt;*h0g0|Nr0hXMkk{m)Z`?Co8gUH;CO zHMF!-b}@8vF?FIdwlQ>ej#1NgUlc?5LYq`G68Sj-$su4QLEuKmR+5|=T>6WUWDgWe zxE!*C;%NhMOo?hz$E$blz1#Poh2GazA4f~>{M`DT`i=e#G$*Bc4?Fwhs9KG=iTU1_ znfp#3-rpN&56JH)Q82UMm6+B@cJwQOmm^!avj=B5n8}b6-%orx(1!3RBhL~LO~Q_) z08-2}(`c{;%({toq#^5eD&g&LhE&rdu6Xo6?HW)dn#nW17y(4VDNRo}2Tz*KZeOJ=Gqg{aO>;;JnlqFiMVA+byk#lYskJf)bJ=Q) z8Z9b3bI9$rE-t9r5=Uhh={6sj%B;jj)M&G`lVH9Y*O*|2Qx{g3u&tETV~m)LwKEm7 zT}U%CvR7RA&X0<;L?i24Vi<+zU^$IbDbi|324Qk)pPH={pEwumUun5Zs*asDRPM8b z5ubzmua81PTymsv=oD9C!wsc%ZNy20pg(ci)Tela^>YG-p}A()CDp}KyJLp7^&ZEd z**kfem_(nl!mG9(IbD|-i?9@BbLa{R>y-AA+MIlrS7eH44qYo%1exzFTa1p>+K&yc z<5=g{WTI8(vJWa!Sw-MdwH~r;vJRyX}8pFLp7fEWHIe2J+N;mJkW0t*{qs_wO51nKyo;a zyP|YZy5it}{-S^*v_4Sp4{INs`_%Apd&OFg^iaJ;-~2_VAN?f}sM9mX+cSn-j1HMPHM$PPC&s>99#34a9HUk3;Bwf6BZG%oLAS*cq*)yqNs=7}gqn^ZKvuW^kN+x2qym zM_7hv4BiTDMj#<>Ax_0g^rmq=`4NbKlG1@CWh%_u&rx`9Xrlr0lDw zf}|C`$ey5IS3?w^Y#iZ!*#khIx8Vm+0msFN>$B~cD~;%#iqV|mP#EHY@t_VV77_@I zK@x`ixdjvu=j^jTc%;iiW`jIptKpX09b9LV{(vPu1o0LcG)50H{Wg{1_)cPq9rH+d zP?lSPp;sh%n^>~=&T533yPxuXFcTNvT&eGl9NSt8qTD5{5Z`zt1|RV%1_>;odK2QV zT=PT^2>(9iMtVP==YMXX#=dxN{~Z>=I$ob}1m(es=ae^3`m5f}C~_YbB#3c1Bw&3lLRp(V)^ZestV)Xe{Yk3^ijWw@xM16StLG)O zvCxht23Raf)|5^E3Mjt+b+*U7O%RM$fX*bu|H5E{V^?l_z6bJ8jH^y2J@9{nu)yCK z$MXM!QNhXH!&A`J#lqCi#nRZ&#s1&1CPi7-9!U^|7bJPu)Y4J4enraGTDP)ssm_9d z4Aj_2NG8b&d9jRA#$ehl3??X9-{c^vXH5**{}=y+2ShoNl-71whx;GS=a~*?bN{cm zCy+j0p4J4h{?MSnkQ5ZV4UJ(fs7p#3tmo7i*sWH?FmuDj0o>4|CIYAj=g@ZbEmMgl z6J-XPr67r}Ke$)WkD)hVD2|tn{e!x-z)koN$iH!2AUD0#&3&3g8mHKMr%iUusrnOd>R?l~q-#lr2Ki zb)XkR$bT5#or!s~fN5(K@`VL)5=CrQDiLQE;KrxvC78a+BXkAL$!KCJ3m1g%n4o4Z z@+*qk1bK{*U#?bZ$>8-Syw@3dG~GF=)-`%bU56v^)3b7`EW+tkkrSA?osI4}*~X?i zWO^kL8*xM{x-Ix}u=$wq8=Nl5bzHhAT)N&dg{HA$_n!ys67s~R1r7)(4i^ZB@P9sF z|N4Y-G$9R8Rz1J`EL)hhVuCdsX)!cl)`ZIXF>D+$NazAcg3$y)N1g~`ibIxbdAOtE zb2!M7*~GEENaTc+x#hOFY_n0y3`1mnNGu&QTmNh~%X$^tdi_4%ZjQk{_O^$=mcm|! z%xAxO*?qsc`IPrL?xgPmHAvEdG5A>rJ{Lo;-uQf3`5I~EC(PPgq2@n1Wc}lV&2O~t z1{|U92JH6zB?#yX!M`}Ojw+L1Z8{Is0pe?^ZxzOe_ZQcPCXnEVCy;+Yugc`E!nA(I z%O%hk_^!(IZso}h@Qe3{Fwl3nztZ$&ipk?FSr2Mo@18#FM^=PCyaDZ35%7gPt-%35 z$P4|4J8DnNH{_l_z@JQPY07;`(!M-{9j2=y__fxmbp59aaV4d)Y=@N(iUgGm0K!28 zMp;Ig3KkNy9z>t5BvQWtMY82$c}}d6;1`IJ^~At0(2|*C(NG#SWoa2rs|hBM8+HW(P5TMki>=KRlE+dThLZkdG387dOSY2X zWHr}5+)x`9lO#fSD1v&fL&wqU@b&THBot8Z?V;E4ZA$y42=95pP3iW)%$=UW_xC3; zB6t^^vl~v5csW5=aiZLZt9JLP*ph4~Q*l96@9!R8?{~a#m)tdNxFzQaeCgYIBA1+o+4UMmZoUO9z?Owi@Z=9VeCI6_ z7DV)=*v<&VRY|hWLdn^Ps=+L2+#Yg9#5mHcf*s8xp4nbrtT-=ju6wO976JQ(L+r=)?sfT?!(-}k!y?)>5c}?GB-zU zS*r8)PVsD;^aVhf^57tq(S%&9a;}F}^{ir}y0W|0G_=U9#W6y2FV}8NTpXJX*ivt{ zwQLhX0sSB8J?bmh(eUKq#AVmTO{VudFZpsIn-|i-8WlsexQ<;@WNn)OF=UpDJ7BI= z%-95NYqOY#)S?LIW-+rfw84@6Me}ya4*ltE*R^fy&W7?rEggZBxN@BR6=0!WH%4x0 zXg7=Ws|9Em`0pAt8k0cyQlr+>htn8GYs)+o>)IIf)p+yR`>lvz>5xFt(ep7>no4?4 zA%SUJ=L2D=;wq*f8WFl|&57Apa1;cT?b?bfJc8h&vkBvm%#ypP{=`6RL#Tf-dCq`;$!eR%>29EqpIkV*9 zEZl_>P3&}hY7)~q6UYw?*cBCsuPi$TU zRe}A|5nl7L_#e`8W0Hcpd~NWjAaV#3ngl$CoE3dz!= z?$3`dPgn5I+Q8 z@Bk>MqB7;kQqnDK=buPc+DsEDP-S;8#I(_z!*u&%_%nqI3+srxxsf9-Qg6%$l$Rtl zK2Wn-OtsBE5<1d}1Hl!l-r8eqD+{%b5$jfxQZw`2%)f+_^HMfbWyW4@j!^9M({>e; zeqCfR5b?^xh7MhHfmDvoXm8Wq;Jl2RU;jY*+a&o*H02$`#5HsG9#HOR4{g9 z#2mgNt%ep|IWrmctj=e%3xV&o^@8%OrR6io()6^sr!nQ3WIyQ3)0Mn}w}p^&t*V0G z03mUjJXbSCUG!o#-x*;_v>N8n-`yh1%Dp(1P)vz$^`oevMVh?u3}mgh}Qr(jhy;-09o$EB6jjWR!2F&xz^66M!F z-g}JBWLcw=j&Vb>xW#PQ3vICRT_UZ@wllScxk@ZQe&h-y)4B5kUJptVO%U-Ff3Hka zEyLldFsaM5E5`k>m}||+u`11;)tG@FL6TGzoF`A{R}?RZ@Ba!AS(tqAf{a_wtnlv>p|+&EEs(x%d4eq*RQ;Pq;) za9*J(n&C2dmFcNXb`WJi&XPu>t+m)Qp}c;$^35-Fj6soilnd4=b;ZePF27IdjE6PZ zvx{|&5tApKU2=ItX*ilhDx-a2SqQVjcV40Yn})Kaz$=$+3ZK~XXtrzTlKbR7C9)?2 zJ<^|JKX!eG231Oo=94kd1jC49mqE6G0x!-Qd}UkEm)API zKEemM1b4u_4LRq9IGE3e8XJq0@;%BCr|;BYW_`3R2H86QfSzzDg8eA>L)|?UEAc$< zaHY&MN|V#{!8}cryR+ygu!HI#$^;fxT|rmDE0zx|;V!ER3yW@09`p#zt}4S?Eoqx8 zk3FxI12)>eTd+c0%38kZdNwB`{bXeqO;vNI>F-l3O%-{`<3pNVdCdwqYsvso!Fw($ z`@$1&U=XH|%FFs>nq#e0tnS_jHVZLaEmnK#Ci==~Q!%Vr?{K0b$dSu(S!2VjZ}316b_I5Uk*L!8cJd>6W67+#0>-1P0i{eI%`C(_FkwRC zm}5eHEb0v^w3Wkqv#biSHXBG4yPC=^E!@hV8J5*JYf73=BqO!Ps#sP0fx~&C9PMN= z+V%$50uI|KE4^LCUXI74-qw$aRG&3kN-aOzVpRS1AX(Ua;Ewy>SlDn@lV(<^W?t-x z%K2iVK+;lG_~XF&Glk7w4<=Z!@-qDLc7)$q!>H^AU{s6e7krRmr!AZLf?8~$rRuP) zc$@c*PhIA^Lsu;uR{^x2)9nvsm}-67I`+iFZkhfNASUD>*LqxD=sAtpn{zY0xMxFp z4@USzYjMULeKc1lBe*8vxJDGNiSTtq_b#zd+Vzdc%$~+xf0;s|LR{F$YKe7YJVR$U}jKOo6=D+|6vnryopFbmNXEo-~I z*nm(LHmEGwkB%h%tXF4r|5h2p%VnRLx5rRsFpPR|e)*)C`WG-Iz94xsO&>1k8g6W? zG6#40`>I=B^scgmt_6!uU}=b3HgE@Jhj-X3jP!w-y>81ZD*~9C6ZRN4vlAFJQwK&l zP9&CP4%l-eN@0>Ihb_UWtp2kcPnh+L(fFJfQLc0`qqFbCkzr`8y2%{@RNrQbx*;tj zKtW!BWJFR$9(9^!Y%I%@3p?0zX#;(G?}sRkL{U>2rH4Wc{3{0@MV+vEaFcD18KIy% z7OyQTp?-N_)i%g+O#h(eLt_3ZDo)2l4PwjVS#=FzUNVvW{kFijz-@Y9-66fQL=xoc zXfLAC8<-!nnpM87K#eT;D^sW^HL5kS))Qj`kxT`%OewTXS(FT^X~VlkkZJJ?3*R8J zR>c>6)9K+9lg_a7!#<`KC$oEk-!~2N)@V}eq4O2xP)~N-lc}vH8qSe7tmQ3p@$pPde;Xk30uHYJ+VXeA@=yordN?7_ zpGsTlLlI{(qgtjOIlbx8DI{Nczj!*I>_-3ahzG;Kt&~8G_4G8qqF6IDn&g+zo>^L< z@zeVTB`{B9S*@M2_7@_(iHTQMCdC3zDi3_pE2!Lsg`K)$SiZj2X>=b2U#h^?x0j$Y zYuRf9vtRT~dxvF2Onn>?FfYPan1uc&eKyfBOK(|g7}E)t7}?{4GI%_KoO#8;_{N6! zDAqx7%0J`PG@O{(_)9yAFF!7l zWy1|Utdlc)^&J3OKhPI+S|Fc3R7vMVdN?PgoiQzo200oGpcy;TjSQ^e$a}Kh&C~xm zsG!Pqpqt5T`1`X$yas7{1hk?-r(Um>%&@?P2#NMETeQYhvk~nZW#BApGOLS2hdH)d zn!sf)7DotO?tRXBE#UpfKk-s}6%TfS0|7#>Rgk z%Np7ln*SH#6tzufY<0|UT+M}zJ1)1ap_cE@;QZp)+e-;k24 z3lZG_EA?tM$Eg|x3CK3!k`T7!*0}{fh8#=t^2EJ>TTo`6!CUm(HFUl7fFIB9Zlt4a z!4=|s-ZSn!@6Yc&+r1w*?*2fxKX>Hz2(vBwgE*>E=`A?Y1W-;{d2$4B%$NFAI?v5e zmYT{blxWeHn2J(0Vbz%FDz9~baqE#)R2TMG24xMZjCLcPfc1mR?5H4L%GnMR7ua{B zCu=nN(vV)5dJ_B80WBCy`tJ#YH6GyltGBSQvsN#q0;6XU1&60$&PC$0r}FUdr@1I+ zINcU{Ow6t4Qzmyk=A6u*z_!A*$^hBXJeKQ96bnF2qD$46hN!?1C|io|<_u@g16@Wd z(Fg?1=p8)dkWz<^ml6Tj5gO$hpB1N5msV!#PB5pfwCOBu`cv__=7kQq*r#Tc7E@6z zdr}5qs*slXK39`Yn%?=rslQgOTH0x?@z|h%fI5Y7kQ{X00BcL#8Jae4Dc9M zR%ySU5qODGnM;n#&up^M+PIddhxizA9@V%@0QQMY#1n z%{E8NS=?1?d((9Bk_ZC|{^(juH!;Mih{pTo&tu<^$Twk1aF;#W$;gxw!3g-zy(iiM z^+8nFS<9DJfk4+}(_Nza@Ukw}!*svpqJ)Nkh^sd%oHva}7+y)|5_aZ=JOZ6jnoYHQ zE2$FAnQ2mILoK*+6&(O9=%_tfQCYO%#(4t_5xP~W%Yw7Y4wcK|Ynd#YB3`rxli+9(uIQcRuQW_2EFA@J_ae$<%!EbI9c5htL`8>3Myy)@^=J)4p@nB2*&sWCOmwH zwYi;-9HOboaw0ov-WBk89LqGY!{)>8KxU1g%%wMq9h@Aie^42!f9`?o32T4;!dly? z(N?67=yo%jNp;oIVu7;esQ$wG=Vr+`rqPB&RLzr@@v`H-KK6wTa=8b<;$yE1lQGy?A1;JX|2hSzg9`a{;-5oh|=bFSzv&b zst=xa%|xW;id+~(8Fj7hS5BPVD(@(`3t@HUu))Q{0ZrqE2Jg zm6Gv~A*$A7Q#MU25zXD)iEUbLML1b++l4fJvP^PYOSK~^;n$EzdTE(zW3F1OpKztF zharBT_Ym7Y%lt#=p2&$3gs=g4xkM8A%Cbm*xR)9BnI}5=Oxp4GEF*bjFF^87xkP4L z;StW)zkX!yzz5^Q4HfEicKi{8elkFQx|0TH5Mtzsln>TN2*5Nypl(7sj_UxoN|KSyOP0g{L+vTbHlOyIEJ@ zjfku4x;`_FLga2P{FJLrgpIt;A-ukDuPsuW4#ApWE7|&i85Frv()~gOM`v`YVsF0c zx|J0}YRtNo7DIl>N&+%c(o1^C?%>Zf5<-<(yVcj~p88d;@=(jtox_$Af#v4%=g4oD ziv4MKh%Uf}NHP$SqF6mZj>}_HfC-@2>S~<3qOIu*R^%7;`VGN{ay@0(xmKM^5g9H4 zaq4>^38z|jszHqa)d>j#7Ccxz$*DGEG9PtB(d31?a;2$u>bY`CigPsg$zpDTW?zKg z+Ye-wtTjYHi#Hs`5$aDA=5Gl4J>p1Xs3PJZWWgax9~(h;G{hDip2I=+bW1ng3BrMC za72TsJR+;*0fSYuVnHsA;BnH5x8yc5Z=Bno0CUc14%hAC=b4*&iEzgAB!L= z`hhC!k&WLZPFYJY4X1pELFsAnJ!}Y@cW6I~)S53UOve!$ECM^q8ZE{e{o}hoflqqy z1*ubPGaeqs1&92?_Z|pDIR*gw{Tf^KJV)G*JLdzktzF;w@W<(X2;}XY0Mlzs8J?$L z$HVp2*+(o8?*n6cqx3_k6 z_&05@yeYRSfWQk)=oa0v#3BHNBBd>{fP`)#O^*^0_#?tW5jf!vCBp<2W+WCTEYeSv z9x0#bu>tB9M0W%_p^S7&BHa{2hfNL5eUUq4dFsGvgW}38M#j+AdeC5Q0pg^g zVzX3vrRi^YI(~*BW_Jv^o?2;5SRY4UiQy4mO}td`T?9Cn>K+dHL)+V&T+H2e9cz36 z3w!e<82_a0Abraxx8?L{a%&###&w=O83@y6xz0Yz{8$Wp? zpRHDDFRKHe+@^Y7*&@z$+aA;ksdi7xdV}c(i1><3F00dIA(v8LW(^O*HX)5kc#IRw zqF;w9l3uQK5us~@YEWk+?*7*(7!*}^OBGk+&H=rcQ31wWiI7@}vU8P`@-3x85BGy25yPLiFcZ9Ix z&g>o*aIM5;Y#3A-9~8-WmTezK5V~98kP{j^ZZ|WDa{ZX{nzq*qy3?Lw?|D4hN>kzB|OT6-b>reho-)KPiAg^M6 z^V7T^-LL<$VK9OM_AsP21hWykSObS?gk4L=NQ@Wevk9nXUWk~lu4S>zqFX4H{cWCE z8{eF=%>j8Xll5o2)cdA;Gx}>chr}9ZPv2kT=8x~q=B4i_@+{8-#jh5lsK}aj>0zxd zIl8*E$!(}Vii%YIB_2V6>|Ove`W+f~dqsd+*K|~yHvkUoMukz^XnLgcXunf+E9#k| zU0yT>#IG*W)+6ue)vv=xfDT{9k$;BDL!duM&qpGVui6NbuaKa`h?7i(W~4YUu2O@t zV=FEUMaC0QAIZg2c%Yb_WFI$vZ0z*fj-GdWkVMt>lDy@w)qhCE7c^Vx0i34{@bnQJ zMhB3B>8stMqGsKyqUsN>cE5xczm}r!D&5+?zTtYl6!U!4nmiPv?E)Pe$l(A@E1T7dD)Px*$)#pB(Mccz%i%RKcuskizkH& zM^+m#S#sK2?f8;gH5BaXCfyI z=Mo5s;fHbBh@$hNB(!H7;BeU>q)!Z^jaCks!;!d2W7 zv{8hf2+z&R2zAS%9Tu1(dKX~*{rOT|yjLsg6Bx_1@bTy#0{R-?J}i!IObk@Tql*9w zzz?AV8Z)xiNz}%2zKEIZ6UoVuri+AT8vVZBot|VA=8|~z-!4-N@}@Bfq$~F4`^LO) z?K#tKQ7_DzB_Z%wfZ*v)GUASW0eOy}aw!V^?FkG?fcp7dg4lvM$f-%IEnIAQEx7dJ zjeQdmuCCRe*a?o*QD#kfEAsvNYaVL>s2?e^Vg|OK!_F0B;_5TuXF?H0Pn&9-qO85; zmDYsjdxHi?{3_Il0sibc3V2IAP74l2a#&X0f6EdwEb_ zCHuQC@Q$(2$$0W&FuxtPzZJ`{zM{%lcw)>^c&ZZe3{GU#x8ZmhC${E>XcP+}<0zKn z`!He406MT}e^f*=$WZoCHO>xt?AE)A6xB*54a+>4&{!W0*`Q93ibK&4*}N2!PdjOa z8?@WRHjyEXqa(1=JSuglKreLS>x>SiHMYiH7)EW4L&&HyJUh+>opC2p&vz)-)hLZx z$xgyMGH)3R3o|Ptu(n3@oM8uX^(hq+q=`-aC1BlQp2I$eKj1tJuqDUh( zDkDsZ^23iaH3;bn7U>k)AD&%$u4G55$I=scldY;vFs+SJmR6mE&8&=C%8}PL3Pz1e zQ8C!gVj0PV2ym8>BOJZh9EPGH7B0X&x$=hK?E>1-@+vYaj!Grfw5!*_$pLHotuVn@tVzDd6inT? zVRbufqa&mdvhz=1^!A^mshoYUOn2TjV3fhuz*2mdNqBX{nUrI%6StBzCpt&mPbl5F zvw_Cj$en(bhzY^UOim8~W)nxy)zWKuy$oSS;qRzt zGB#g+Xbic&C4Zo0-$ZvuXA7-ka&rf8*Kn)MO$ggardqZ=0LyU3(T};RwH9seBsgBc z$6-BI}BN*-yID>S62)&!|-r4rDIfw zn19#SN$JA4xngbeGE4txEV5qszS(EnvzvVfh08c;IO5>d^UpU#m~24P{^7AVO7JAS zXZ6RdAp5-_yL;j@AlsMp8N&HVwHV>9DfH4c81xmzCzVZ3fXAQ+=RnI0B<;YfHZuqa zH|&*09Aj{ZsDVS+5jB{XEkd)PR5JO&0q`JK;9>!6T7%b14rbcBtNiw}OPI9h?u#%^ z{#w3(2+S5shq7N4smmX#Ns_ayWl5jP^7M^2hVn&gl1y>C@BvQ$Ah*^_cgzF=iG z39Lr1x6KpDuS0W9tH%r}N=vnOgCk^E`0I|6X8%H)E5a1{r;Ooi{4RF@DssCC6!o~J zDpXb3^$sNds;bMqm6n#cJ8M2#j7A_?^(fYr0QA$GrTQV$n;9;Qkh~$WT|e1Yq}o;h zEk_Ww1Kf4%%?R!{!c91CSJ*2fr<8xHF)(7!_%EKZ*$KsDg&ALtP>P19z99^whu6ms z^F(P(PMjgfp#lXpZt(?04@z5J{`JHow@|N~KFN{8WLok3u$zxk=`cv$?EaF;?XU6*mT&GJ_`>Ma3MgI?U07^UN9N3Fe37d_Q@ z-K2Z>R)Wso&W%+APtaorr8H4bEP6FH4p7!F)=w=jfs{I20h3Vck4N=Y(~XC1-kIAd zy5x^LnlUYu)zXH(P}oXq?U#Bgp{4bf<(9x%vx;I>b+jS0&jtaYZ?(5Pfi=RUF`r58 zPQbIAX=tIC=*W@cR#+`*i)vPR-|p^(ORBp*UB+Ei6;0-CF@No`$y^MQ8{I(2`CNzye&0=Q^qYjw%}y zZk$+l#(MVftcugPvORxL+@7k(4XzR~ti3!@toSymCaI5}vo}ri9vdMZa)_TzEsCB^ zLAkET9Z0E*!fv>)%Z#tIxUhYw%QRE2;98~{O{W%9rXI<-_{I=y%%qwb%iNi=+!>Qf zK(HtaA|ze7afz`txb*_lkb0u$(ijK97^%;axfg0J0#7NIs61X5HEQ=zq4Zv>VMu>$ z2~v10H$A`~ZB}6dK%@F2UgC9sMoSgd@q}!<7mY~z+C3H5tBW}xeKN&KIXP_?N=ed~ zFv^}TDs}$Eb(JDOQ;H7ZUNrivfKib({Ix|*X$AZawRj(j{g<^=Frb3--rEyv z6xZd8uQqr-K=@KuDrN*E`gfQ`mxKf_5w*!nJcKf(S=suW%7rFjx+s2> zi#9ouh%>Rl2Ch+}ie_3lybm-tkHbTSJILVkcjl~h@Q}u~N~u`668%(zQ9>9i7C#5$ zx{s(#H|$tR^Isy#9Q9XsY<1MHT-F7OyLQJdGEvzDtP8S6C2h^jU=C=>>*UM{Ijd1dNe~wr z+2V*%W+RpfrPRjc)E0!+gT^{TN*3CN1C}}95a1F4XwxwLS9A^ttvzq%M4HJ+$y?4I z`yKD+?Z?h%Uf%Z`@?6k*M1Nf&Cz(V^NgBygk_J*oqqX3`NcK^Lkg7rqVHhw@z>zv- z%X}I!;8!nQ^_RTCBos2Bl+SVD9Fa##0@yip*+{E)wPQxv$$hRA!c&QWLoLFG2$U zYDR(@dUI1w4`Zyv?%zhHwZ){BfpG(vq}!Y;6q(jI@xnbko7P(N3{;tEgWTp9X{GP3 z8Eh9fNgec!7)M?OE!e8wyw>Gtn}5IO|5~^)!F(*STx1KCRz?o>7RZbDJd>Dg##z!; zo}rG4d{6=c-pIFA4k|&90#~oqAIhkOeb6poAgkn^-%j66XICvZs}RA0IXj6u*rG#zR07|(JUt8bvX^$La@O#!;a) ziCtKmEDwgAp}1=mhU`6(nvaz%KG1c@?X8FbZK*QU*6mn${cWs15OGLA-803ZO-?=7 zah4u9yUPx8iI^Q~Bc7;DSaf@k0S@+p?!2(*$4}3v|?Nx~swkjwTmia)C!dVfht zzo1E-1vmsM(nC);|(Kp4yaPusRKec@I0b0J(n9k*tg>E zC-M)?LH%OLASR6}G-`?oyQ%KJ3(+KfS;-Rndh?ku8frhoZdKm<$0bj0e4I_lCX`7S#zIYBZ*s)i1dsNx5wX6~IDx z(Oz=(Bo4-fnzObxxiw~v`H}FuI<4v9nlM*7QryonD7aNenD4Iivwde7(TYd34Y|)E zZ;|i*$m}OZEsYWN9Xn+cJ?tl$HcJt&tK#m5)0pE@XV}gwcJV80^2W;>rR>%lUXzzrnFRHk2?0nQST``j1g;Rr}E@4Bo##q3%WJ3kW9`oLwIq zA0vY(vUKK{!(xz~Aai`k?GLCg(L^>jk7c19wzM!kci)KXbo`HMF5|jVUqOh5zPHx~ z7u)Wv`L*($bdq$~K@z$=!D+{HF@qBwO~Iv@@Nxw?Fyp2O5_#Ys8J$}5^H>J%`@CS{ zt-hYIu7NOhv0I=tr-?4EH2w4i=#_UUmFjs z%A-veHM(n~V=b%q0^_6lN0yt~Pi!0-4-LyFFewUhvZI$BFGs7)rVm2-{L|9h^f~Z)eyKyr z7?*u`rR)t7ZJ=8!I1#4|5kHXDmljgsWr(i6WPJ0eCg9K=mNGR7`F@<9Y)ptr=d(G2 zyFZ6ui;z7lu4{L3aCARB69KtaMekNz59bzEC8)@)F`W`q&hnF!@hlaZlivmQh~9 z8R-`kyDt3>Is4#t4`YaCAl(Y_9rDyTs1KYE_5gKHl-~>Ih(L@+s?${L`>}yrDEr-q zaZJ6`3Uhb_efWr)4dESDe#xM2C-gvCth%+_s@(-6U(RvIlv?Ex6v_UD{5h)9b*>N7 zzip!Gp<%x}c#!@x5`?mLYygtk7JG(HNpnAPnU%2^Gmjs75I>IS^yb*`pyeYn!J7D^ z_Z#@1;rrh7(T48tPjx2LKtKflO``Iz@cr-po+gBW$}#TuxAUQHEQAn2AEUg92@)F; z3M`=n3n&Q;h^mjIUSbe7;14c|RaJ{dweE`QJlDm5psETI1Mo@!_NG-@iUZ5tf+VTP5naWV2+Jq7qEv=`|Y`Kg-zESx3Ez zQ)3pq8v?(5LV8cnz-rlKv&6J}4*g7EdUU6RwAv#hOEPPngAzg>(I@$3kIb+#Z%^>q zC6ClJv0EE@{7Gk%QkBdOEd0}w2A}A(xKmF(szcN4$yDCezH)ILk`wx*R!dqa012KxWj{K;{m4IE$*u6C-i^Xn@6TimgZXs~mpQrA%YziFDYm9%33^x>MsMr{K`bk4 zmTYOFO0uD{fWnFuXf{4lKEGfjCSAEiBcUh~-RK~vwagYh%d^zqS*rgiNnc4TX!3<4FL7tr3;DA>RcYrMt3 z7h~TlyR(x;>v|5s1e#?b~H|Pqc=q};~YvHmKp(4Zk9bYF9IcEMmW{Q;%denJT?l4 z70{bSJ{{dIb)jJC54M+j%am#jwFugdb8V~47)xgJ;{uA!=Zs?&88BQVhSI&P+}(>q_==| z7JnM15Q4kwb~Px<@LEs%cxdZlH`{A~E3?IKpfJGR2rv7%N}=c)V?JJ@W7AH|AkZUh zvi2w)>RY)$6mkHQRo9L;PYl3PPg~?S(CX$-5+P!2B}GqIGEw- z3&}?!>|j7^Vh!EMc2U!gsDhS&8#Pq)SlamRXJ#FxX`caWHH_RW3%~WsoF&WECP$2g z3vaHqsO>V7k2xZwX3!-T2cj>VPidn8C|_4c?CyU;gpnaO(?YGO=a)9=Sc(n>Zb)C_ z>8fRKP6=d9Wg?&2G&5nNVU7Xk_8F-TmDrM6uNLZNK!U|gEn(vb`sw~_Q7LRLhitWE zJ{DBl&v1l}uTVoMM*y8$1{W*UIP`Ju*BeYbo`gJO3-K_tZ&4g%BSpS&lGf9 zD<3|fTK@&&<9U(QZ?zOW4zHKQXw`?v;uSZJ3ZIAji)F;jrOD;GeX1VSR+>@*5?@>z zVUfy2G!UmbDU$F&S&~3{;e=EUs{9uU^x(oT)!;)yX4Es>NE-7X%5^brZcL7_$KhIv zr5CGYP6|tw9`3$Cz3Myl8 znbJvOI4#W@<>Cyg>1I0>WiZtflPr-GM&DAaVv>AI;InpOh-5usQbSpOmTKY9e3EKR z;Hno1gPK2lJj!r+UKn9Zp#3yQStL5eP+`n?y*fm?v zA84*u&xPM4%6OaA%lsEMxp<}G&L4b#3zXfT`Q&U=2$xO!&?4X~_EUw`E}jd$70B`D z%VO!*-NSxZ=hz=*vGi#2+0DPI?Nr{|cA-Xm?8(IBQT5razQXk&(-b@ZJgwDKQH#!m zNC}wPd|`LEdw{jkq}>P?kLv_l`1H;`3Ypo z<=~^h)h>9lcSp#~`+8{d*nkO{Q57=hcqST+<>@KCkjsY4-m!~JrSs!7e3YBf5+gie z@3YxN5s{0Nw97uJlOQ$kM!sMpu6~+PJ9*Ym^Ru?p*)mlo*nLP}tQcyY@^-0%KE==U z9_PrE;U|ZK{=rZX`6#d#514_!C+5->pSvmgNS}EpK($i?)6CZ!Huf)`&x;5Z1A(&Q z@DlP6YDZ(sbd(>nxM#=4mhsQA4E;<+v`Q%cvx`xmNiP4h>WvTUPJ22uWaL49LZe&$ zu1$oP!=mMt@SLsRR9nk&V1bN$rN33*%D|rhd|xC)oT5}P_9ccwLRy4*EnFy#-VG|7&>jsJ2#RpDz#r@68GuOAE*sQSmL#Re$ z8y$k2M}GP&w8RPob)Z+eZez0hGJ6;ig$hoS`OMO5oKKR#YtoGWNpHT|{A-<2v@r9k zdHaj`SnX5h4E^0M=!*2hM>m9i#hdJD+AEofPeP$bAN9B`?Qin)0|4sWhwTizniPlA$1E6xG?)-y`KbWVB#R7|wk*IeoeRw}# zv0XV|5pzw9*e0TCxIsLcdLNFOYX4Y^gpD&=N$!;WMK)%4;Wh80b>{oPy}ot6_RYmF zZFlk2_X|kWVuVY)O#Vf9iHpmhr1G2no4g{P?=gJ_UpU}HpD|jo+qJb=ynu~|cc+v- z;x`}SwQprny~&aqm;cD>#RsRo_#Tf(pEw{Z8_{2^g#CKVen}EUK}tsX@2GvX6kFB{ zz@BgZBarBKocTk%rxxP`3yE^XTF~#~>G?6S_kr*M-OA&x38`~(+>=FcD7CF1Zzp~R z`rhZwkz2j21wH7{BU2yzTYRZMGS+cNw5Qs<(MJzN+PcO{SFY&&dRNlj2{vylsOs_+ zxNOcD(t>RX?HVbjT||`Df>@!92R)`K$w3^9!FYA7Zh8->KU!x)e?ztv$;IVrH@|W@fd8 z7BiE@%*;%u*_qv$`FHN(BD$hGqB^>w>&yBw^JV6HC=#GpjX!WQ(zeKjLwM3%)TCMT z#xyLTD8e|^YTKwg=Vv1|?|13o6!&U$_A}W2wWMcD^#DSn@g(5GbsHO6W$I9JNSxoCmsH}pFn8j_Wxk~5^ zVhEXZ+s@i0YjOeagPLSQYoxR{i2biszj7RW*S<_0j2Dw-Ef7qqLN%~y`ZAHIINOP} zvmaSn7x|DlC&W$UxkMbbJ&xpGD97rRFi#}3H61(AYVcPN9YUF0n72Zo#a#jfh`6TX z7!Pw#0~N0S?BC*wDZ0l04tmB!J145jwS;Pci*%m~ID_r&x0H;>J>$x}okimL!WLb^ z%m!KzacfeEw#alud8ZbsYF& z1@a|GCQHDAcQ3iM5LfSbz{fwQEh%&k<8f6$Q`yJ~Y7aO&6=u1}-*Gqw6$crh2cZ*X zMJE4cPZcdI%GQ>e=U|%r7EWn5pWBsM{|l8thH#qb@2{EkxwMBgjvOdH_IVX`Hh3}l zHcZa5HIB;>NekQX)ukMQJ`DTqS}jZ#j|$iH=Y_~kA^2?d%gm$PmPGuA)POynhUyaK zegRG1n2fzKfWg9@a>C@^5M)xpFSicmIRz7$?!Cq3uh(hTvD(>sag!Yf5*aMvtv=^^ zleZUVg$1$=zDs9p6Q1CAH&);!jkC-ZJ{fW`hE2o0x^4F_jcyr4#!ggqbcMo}icm`y zQ_77P#ZDAzmQz~g1=4DW!t7IZa}Z7thh#dEqn7+`5Lf8=4OAj_>AZ3IGQlz5loU2V zh|Ok)*^>O^ITIz*6(a6LT46*2Z8qn|UEzXV(Cl(`t!NL2^RU)JQ5CwNXU<%q`gjnv zF8YRI{0Qs{HiYEeK^2%=T5HFvrq^)R3Z~s+&dp-ZNpWu25qg9QUYwJZRjYFp(D>*A=`$9U_~N!BjcnQhdaf0Wf4k~Wb-yz6v=9i4rRTbdv0 zO)%vr@`J~@XKn3Cmo;jazVHe{VYoA-^m4ZO7VwZ~TARsMO7PY(!ck&QGkAgY9Q9RJ zLr}6J8cX!W%WFefwo9}P-hOjJJd>||gfOKNQ$xEbxDL$!N<$66h}w{A$tdnEEUq5; zQB17>Yh#_2o^GIeLQ`D^c**S1E;}*EAjaUHZAmh>Q~WW`RrCigz!CK>NF|IY`w>Yt zHl!vK+Cf`LljiFI=u=(p3$f!)&jk0aE{~>@e!_NZAc2Omti-mkw)JiJbz_^F-VP%u zQ&y+sQ5}T;hcIKT?jPxfEv!MA!t{oa;sV+#hIQ7_qx8Lz5Sulr_iep}MwMTaYYHyE z;th6PF7kKkE$1mPSGQC0?W9DiI&FS zPw(Wqb7k(snDvn6ol!D7!#GhJjH2M&gJc}C(-vuZ?+cGXPm&H#hftWUx3POg66a6n zfN##yl=25{SXg!9w>RJsk>cLGe2X4*AU?QPz|qi6XRQfR&>EZ1ay72<=1iIAao!gl z=iXCdaqY-04x%}=Y(<*>tlU_^(VrHIH)W}5({50@Pf_Emkvmy1_vz}FN4%!arFz{@ zGv%Z<%-w_KloV$v=!Z~|Z<%S|Y2a7~>BkxgdN}R+5+GE`KL1&xvnC1ZF`O&)@+-)Gcq!xuuB9S0X>R-t2pteqfiBX18=s!G>_Y z1xdnN_B)8}I9o<`n6y`b6?TV^e{iJi5!y5A8#Yc0miLEe zI33k{;HS8^<|IEkcVzjj#3rzLtPbmdq8r6_xeOf+1flw@2u{ z7ph8+9FzeiT#-P8tS?i#BdQ^$h{Ww*F=6X>5d^;jC>JrKa`a2vZCP4F`(r%|qT)+p z8I(A**}QO~>w_{AcjCG6S2(!)!0Q0koYHOqp0J7jIN>?pqxj+UPbG(ZzH%R7XM90` zj$jS22XlLiS_ef1-*ioM!Q*00STA}&18-3EN|(Q&<%b4;8@@tEm^uU}c!LZu9o`^A zX?d0=!n9~@Op+U(i2*`#N{3pe!XtMPb%k4>*#6S)3<-sC5x+);@IFHe;)vLac7gVb+ zVy%FX+y_#;fY94b0?IYZkO^Ow#D_#PU~5k6IsF|@9#PExC0GDbVu*%(SN5nu45KYs zKy!crklZl|C;1xq4#gk_`Nhg`S}5lC++i0e&GcafLxzk_hVLkBG5d2y{94=Z+|x=1 z%axSnz&LR0GB_NUJ02Lc;Ywvu?Q4ScA)Ezcg)!G2B1)N>;~wK=y{3lDg{gpiV|7Qn z#pOEzcxTd{r1`A7Q=fO{Wkuq(Nu{edMD>fb`0?+_%wU!>D5zX;AqW)-;3!Ex0vhNX zU(=77+{)#g(yr-uoy1;VzA7=eqw-JnGPqHOS9eh-G-@b?^PL|t*sa0#ONj?=tb;`? zl3AWgQ;F`_s;d-UQw4ap81^{HPK`38^=*#j0=$C|aKZrRIa{?amtPS#3sAyjQNNE= zMb?g$oC)nJIPC#jz%sw{QK8};07-+BdV^4n4PcL?xNe2Unx(ja7Qv=z_StA;h(t@` z(NNC7C@e%oWn=;U?G`?^0-gqzf+ur;K~}LsU5XJOUlJ1+>uC@)ch>nl zTSAKzE;N|>ob6G}%w)1smx;CC>fI+tlBydTE74*M`xWyfEVkhU0|-YvvQ@BS*=1*E z51c1H+!>B81O@#;EpxFY;eQ!72d*%yDa90owz9bww$P3P!PL8B1NB1>hZm6;z}(0;}OlhLJezvWPX0@NORT*jtJ!^cR@vI;g*o2t`ZiJwUsBg)gff zZE|OPnxbToa;liDWvy7?*;dfZj1DP^FbC{!haAw0nvpCY1``va4NgJN+5Q4oFCb0h zt^a99;!%c9Qzhh3JiTHZ?tWHR5Wz2sk&=FEtvf)LAVL}ekqCQE?nH=)#wWLp>@1CT zsg*%F!$+?0Z2>!V;;{xXE<^&RS}z%8PcOkF{p!LGufDBPhMPC^ zG$q{wZ z#Ja4}W6245crq5zje}Y@*c9{lc@AzpQqmGuXJ~LY$*{`hg&Gf3P11|WiFee_O|b}! zVRY5AG_P@)S3`T7$B`vU`zoGU;5|1#4QY$XU%4+;XJ0S*Gf z^`C83$;j1G*u}-n&e+z>nM}^X#K>0cbBxQ`${65k4P9l~vmH4wj!dK9Ds-qvw$pf(6VOiY2 zE?B}k{2zUxzM&EhG6jZ^@X=))R&lRCJ#H4rUE-D}<&<(5y_%LK&nIcv={%BK0e!`un#9Tp#Xwr-Fflcti3K={AE}6#+kt{Qie|AZ6 z6*&nr;n(wh^uhJE3@XxoOU#BJE&q;S)ux&^y%En`f>||6x$_bSMn;dC71xBhpU~E{ z5f2v|P{1Cv^jl+$^NJs3E!XibZM8w%4kl>uy8yA#xpwUfn$HvbVs|_LMy>AUN(Ar4 z6ZtLFzwcQpxj;zF&-MnRPYxT3{|`I(dzBso9p=4TUAQ4of#Wd3q@H-0Gz8C6U2uxl#VXmC}x+B`>D)ffK;%ZXO>H zPVvNavG%b4+j~NPJ?rVff87JMOM5lOQOltlI~`eXFb2A)9UhlOiw3q{Ke>OF<`kMl zD=jNgN&(C4hl51!cB-wzNNv$JDl%R#CFx^wJ8zI;*wqhcfv8FGOLzgs8B8@F<^2`p z%)SN|zLITOn%{T>nk3;{6-GYt$(;vrEOutbF+({n^elu<|244j+ z86+n$mOkc15>j*V=xfd1B$*G_jnCJcV9-J8EZ4((lhmZiNJw`_M7fwG&8pHy-Ke_I zrkS&<(%!(i9Q}xb&7WPk`{_kfquVmahoIG>3~7f7S+RSV+E92f8X9;%>e3J=Cr>x0 z&~#wS|C19#Hq^JQmKY}+yCL3daSWFY*=wp%?jSI5|8X-huuF_swuyAM*laABQv<nM&9OUnkdus9i3(4|D}`eMP1@}Y5Bb1U(z#8*%%$T>s4~qFx5>;H zHo2s5PKg@JpAq1ZZ4ryNp{ihW>z)*VLmyu=cWSVjU!#O$Av&KhM`<{OsHeT4W^L$D z{FjnPLb}b$BGoEeF$aDxO-llzmVFo67b$7hXg_8Tqtl11I(W(^t~3EMSd=YsUc-tL zeLEb+dK9(xLL!m2ow1)kliqtx)H+c?rCAXtFh}k)h<{do_@=OvP_jjD3nLJIHX;cA zVfvn9=>eu_t@R0_vlV-GJm~znRBf*`LeMt24Wb(uH5ag1#POrx5gcU1N=^GbQA zX9vONEw_HE$REtCE;n>zdhek^PUnZ};@#Hm_lec6sYLgf#WB9v_nsZ5KeZMY7auW5 z_kJ*q9eK)**B@+THL8Vch#NR9ncS;4qP#j6})Vi(T4b#5_y$z z7?C9%S=An`M&>9nt=_&CMr#bKi5!PK%Oi^X!xk~)OE$*!pzhBbDl|3c_cJ?Jt|od% zuYTxQifMN~M*;jbwvtdar!}ipi6*ul!tJ)0=`QptvVjiLWO?Ld6ii1euZ#(56TeW0VKXYA zO;JSEAuLdOhiOC(zo^YHO>63rTdS-vZ#(9539=q3ZSysm;qjs%@UoRNo1fD+cYOcer$pT%eNH6nAI) zF#HH}KZtL)Sp+0rH3lrc-tc*6T!UfgJ4jfcO4jby`$s!NkCaEoshYG5Jo6~Z904c_ zN@%e>N*~A}l2(TI*J0P&&ek!u&;b12$=W|DWJ0HN04;s(4eX5ydQQ`7)_VOrV%JU| zAsp{6!;B$uFYtT>M{r;b#P62;8PhsNPB~ zDoO@&p=doKv4mZP-D#zF_D~qc8PYJQJ|xuo%cr(3q7)B2GZMPwDGIJ&zZi;fUEyQ^ zlcs~)j^o>q<<~(~Ioj!$ZboT%dYqkYXq&vL*WDjLt_ESAA*A_+)v9X4Z~1?D*Gu@I zNYE?q&aC%8EUc1@Gw-PszuMQ!Erq`S#kHQj5KwM@PRZ4NlK(ROXVva0&c~E!#qtJ0ujV8(>y;aKR3G#1Mf43 zs*c3YkGCB~5XCJWkhOHBOJ@*-bm(s=s<7LjkA==WAdsxiSCN_HG*VRQs+ZOv^y!x- z2C;A|nMuaXAm|6=uTAFdv78xK6bw>VseGo>i1Y#EWJOx3B56}m<5I*`T}qD9x%_qM z>9{{znOJ%GMVUDWcqR9C$0bwpMbQjd+S2r_HA|s-X~_nZcDoQ?DCv38rI(hSCE_ZV zbvPUoTrAj=%zqNQ7P^-Fp>bqVgI}m6*^!WlyGKv+92^oWZlrs7 zLP%PeYC`}14V}Z>{6=9~EdATJEHiIgFI)OD3;bRds~f#P3rA87s!!-^uI1br2CapZ z`1v@|yHda{pTH)AkuX@Swr8a=g6N?>VNRM z7dRL!$B(sDymlKemGkMDPE2d*y(`$P4}_OZoiG2^U!|m)OKnsrH$J?=XL-5>htARqAgN!n1k0v0x4yHek#IorCFRo7^?-1;kV#W$fYQ!QZ- zomxY^(n$ZyZEU3bRd(Qmx=%pGu6}>mQ28S?VS|^mSzr&Wfbtc!fa(?ZZ>1~p-zrz^ zzm3k-e4;KOo(bR9U`{KmT>prvOF+)a;9Ml_ou|vL{IM=Wwe`oeC6zehu8qmGfVHua z1Y$@hbgk2??zN>r8?u<}nJOl7GDqOU+A)^>wkuZ=$Y+0?aq+`izt9p#hof!8mlE^O zf~Gi`+8)>#I!~O!_k0@}6j5)Cw87lr9N9gq4%B4BC9m4se#V(Ln8hzIpyRB}YGS^g zuNz)bukTc4-C-cH9TGtxvp~CV=`XTDd&4S2E=a~QX zH34ta32)bdsH=6WJ#2@#8V6}tbI48DGdKfUvU_^LA8y+nb4GUQkR}LPxm+CNd1|r_ z1{{kl@@K!{B?`H_fqa2bMp=P_xGQl3^UVQO)zE&*>6|fd0-ij2&(}+rzuIf z5BCVJgPeH`_W2=)_-9p+r-e~Ku;noOyq)`Rpluve)JTNOUH0EkxO#^Pz8g7A>2|Gu zo_MJ?scrYD45&6ToEltGJj8>3)|>Uy;dJZ@3c-Eg_+sB9D&U1|zG;L97$k}{!5VLm zZTG>$Pkz}N1Z_+lLxbHRQ6so1{TgU- zNgLZjHZh}%$P)p3^Gekk&O5Tieo9&&cDwA6`Vp6H4v$08e1lb0n7X`!_x6ZQd5Ncr z-1or8K7tmVoT%EEwQD=~7Pr?K#Q{0Fu|sSC$>>4Wb1Msgv(Z1Z(3m7U zMO0y=!H*S-W8oYSQ1PnB#xO?}$Q)^p(#SI7QlV{J=a2?GYE5VN`98&>h?oe*R}ep{ zozpe2vsQT@R#sltkEM-?rp}MoSIFEzNh`e`A6Ph1sa~lqf`_P8wdR(|ad7+8L@kAF z;vhFm@833@Jipi6uq3Pp_bF!`={6RZ)_q3e&#G#EWcSA-dg~O=vK_0rWH@i|&I%f1 zoygC}jg8DWcewP#zZ&O+CV8OUQ)Dm2p4Bjk$?oZgE_%JhAOFZW({kXYL>TpT;Lzz_ zI|FZMvT5ZIj4~Y)tmhAPt~%q0DYhX1((N?ZWM}JC*I_>20dJ=5-SmxUPm+W65rj^`Sjpw$s`^3 zE*(gDcZAiVe8og}D*eTK{{60Jzb!|N-s5|xL@(8VWewvmO-}3iw=6G!_s9I7pXH&* zrdXkqzmYytJaFoVEQefFHzj&&L-8Ck-zIBhH1+A6Dx7TbAE^RAhyx%HXL5skx89S4{#ET7{&c zmPoAZzn~8EGBAIa)Vb6MJ!#GZi5MYbm5C>b(F_nXi)XRA1togzy^M087T#tVYDd`x z;*c=}(IpnMfRND&nI{v8vJ54n?8f4lN`3K^%b)}oat1TifJuxO&ZZTXv5pUhub0Va z0wwYURnZ6}Gm9@r5z`F%e3zeTCje1FB69h@e{T5iwyiaFBF^|31@L?}B2xY5NZ=o~ zE$(4v0{AEMu;!Eh>^}AfO&zIZILKE}6cHN{5EEVqDy8a~1SAO{o{UWYu(Q(T`PAts5V>@5aLwuP6?A4V6(t8AZ*csoO|B$?XQ9mzToari6>M0&(#_q-@sf0G2g@us?RlnK?i5>!_})FfdEnul&4?fFyZ!m znCK()B;nqc9yH<3(+;1HNFSx>BO2|cmH9_>Fz+Q=1y^syP5ZMgbdJd#BU7(9as%Ha z^HX%VEDCVvM$S*Chwpb+?xd6lMjE*fvLWo&C>YLzd&w85R^HGrZ7(kpVPCu?l0Gs1 z>hIk~pj+7mBThy96}uG6s>OMG6mD=@i)9C}#fhwl)Jyp^xn=OVCWhssK}rg8=eT@_ z#MM-!#b3{H*Xr$FEUim5yRH+?cP*`J{c|f&rbWvFlCDFuH4#)*;lNUt$}#2XSF&9v zrQcdn7C`A`pBI)gGu9`(w@al@TAb`ex0c_we6RkY{rql>Q9pi>PGM8b2KT7qFnaxV5b zmoEvhO^tU`ABvOe!>+KynhALJ%$E>t)0)=h(O|==6SCC1QdZFZD5R7X(TTm*Q7_hO z7=l`B@tJOngSoFD`AxA6D{dmf-hq?o<*Jej1-3o?L1`s6?+mT&LguymtaBrJyuUnZ z?rVkLYMuzew?h6~WR}&&rjgWu%Ol0zRpK~!e`c9{nSB|I6c>-U%w~d<3Pru2oslnD z!7N9~Pvko?^+^eupC}q1Sey*kNzo2lD|DB`-Rbj%!6@17B|U@DbT%ss`OK13)V3c zBwneSClO9vQ^N*Z%RXYO`Wr~pe)sPVHe|_LFY!-A<-IfJFyW4DQ`-%WQ$+9`xjvG( zpQ|w~wLPi9e&l?tir%<7e!wa+NTIeV($?_M8K9Ok9K|eg(1Gw$>)_r!@~1mMWch?I zlu47XEEFQ?B*b6E2Mn(`k^R%I5MNchehcs$@A>Qon=44fmd(0d!g;b+#n@O=a#iwYWb+LEvPA@*#Kw4&DzJnYfh;LQnC6!87g zdeW^0s%^91PAO0q`>$Mb==p<41NxthJ-IB>>x%WSPot3rFI* zMf_9_Wl1cS$EV%`sC?Jhn@_2EIcHtJ_h7LBu5E^=&na;`bMz8S&E_6(zjFs3RZeiQ zuRTJN2!tO#0FHtOBj@_b2Se=SHmzr0Tt=WHWsm zPs9+a0tP&xdv8i{VnZqpkkTa`J-)KLAX(5g`{CFP0HkK9R?;p};94=j88#urqEf@h zNp86`#tPiH=peJZ1GkQ~j!|~G>DtG7jQ3c|>9GN9;LJVY1=w~3+AxFB$^Eo!vtkY< z^lHsv3=oH=6dYkZUJB8!gnGuu>Mpma_%KKAHQD%Qw+A~YE zE7L`H=rT?lQtq`I0KgG}wsC>BEIza!{njtF{Q`O>%)n&}o3jSMpQUFP%j1UC+HN<| z%(W?wu*JQbLVt+3ZDuiiDA#YyF+Ybg*l!h`SyN{^k0hQeu)8@TkKFQCrJXjud)K0> zE{25F{XD-Q59a5JYP&@17qn_&5_&P?3hqsnwKyDL`c}1=5ZJU0UskWz3a|b_9B++G zN)j91j2Rf7HbdQc&*p52&{LV;l9GveK^#X>?Yyoup(pf4w|r>&$=OG@Y_VMwA6hl! zIwQFIwy79_k(kp+&XQW7iS%nnfT|GF1~u@KPe&}8SiTJ;%RF2cz}~XJ6NDb<=rK#j zVHko2=aA8x+I!P%vZ!O9)e9UMJ0?eeR#JpbX0d512u#wxBlv;hf62v?LqwumZ%wcg zHVp25KY-e>DBPKKKy-JtDgj!RZ(S-1&dd=Xfl&QQQBJ6^qysCBFAbkG_9f#dv+)s1 z-L3APDR&JQ*PJ&s9> zB@&43RN*^1zQA-|GKN~I4qBYTZiMEPc`j3U596%W1rSO;yzSV-svR6&RH9>mD7B=u z8}eph-j#vh0v4B6McTDb$}TryMb+$sTV5 zi}_AlY6U+=R!x+it_{Fws^cQRi&m1^#pnUclQP{S=|M!jX6e!UuBpP(5qVg`=VuE5 zSpDtgx;0OGi1AVvVZScV;hZR4>PKLNj0j~Daguy8P6p8aJ#Wk2&=#n`iu={^&Cuoy z-OsacXUkkO&0G=_vb3pgg0D+_3b#{KW7s4b3?1@R)oPF<|d zG_ke%UusA5tAf>hpXrV2XKnZ|oQZ$?y0G!zbdF41MIG$yJ~1FUD|@rgG{@}|75Z;9 zC`IibDim;0C(9(jCO=WZUxP;=Hp0PKO>Q?1=4@jTW27?wUSwYJ5=htt-^akbm08Acywa z?nLL@sHAx-9N~vRRHk5`7W$g&)+fS=7KXruHCEE+=h`IRE~j?$(+$Nuv|ud;8rc|h zjdgESU_~0ZjvT}PN$$DBE25Xd!H!-qq-$f;-@rXwG-;l9#g7}!%cbSj%7`g-jyxA_ z0$^z@B zu8A=6hEd*PVO0if!FvNKOXTxHr=b0u@#o{$PVZQee5{z+S>bCizS`MmieM)ykX4gZhRpUGL6F zOkE$%^Gm`Lbd9qfXKCCp+^1dWmdg-NcoY+kwC`Rb+&@P{ix_T1_FL9HZn=tICT|&< z$H{Fd^@RXGa-_mGD1nN-V{GI0VrHfZ-iIa5NBVY7d=2t7+GO%A8@~x-5WU&2kH3_D zqk`_7tUqx{tWQlZ-v4d6|80u@L?!?4Mp>n?rirVL^s#1|6k-NPhJuub9zPdcC}t;X zlSfrFHxP;_4{1f~)}Y-ZvKZ5b3;!(mc+UO%q3O5S6&}Cuz2Hp2pO&BT6t;!bgS)$a zV_9(B5LMlN&4d5ZT`tN%!FUkZm!{_`EP1t|i5H*9W6l-hV^L zx!qJXeRAxC%aOh`>VU)L$Lc!pX&4TJA|Y^ok|g zGfQh;Rq}&N2EcF_JpyGSyGxM67#h+Ah=vdzPjUHZ_san!2g91j89&82?co8PbaI{{V*nJH-6oY-Z7TN1S54VidmMQ1IuCPAZY34*eyYOy*dkm= zWBmKt^*?yxjMko^(;OB+>mxwSTDg_&Nl3kTd_i5(x1YIH)T#2#9z=oU?&C~X&VJh* zC&dao)x@Os%2go&Td7bn6)YQM?7DCgOVd$hW<_kcf^{WhDRMGkvZ{&qjlF;(tv{(W z7$>A%gQ_qOYF&LitAX_s zomK?d5dU)Ok%o9z@e`X9dtYzo3)In;lfq*F;iGLslrQFTj^L#bFN^{P8Tk8zAsf z#keSh$;y9iM*Sqr_l1wz=EFXba$=NjYTWp-_yIAkN(S$eb$CC-PN#PoowN+o!DMey z#1(8Z4#=6dGYIRbLJMW+NVx09_`a_oo2N5P6Z`Tkkoz#_$XUhstzb@kZOA5N-Y!&% zw`TU0oGR(@E?u*=*M7z>?Wu^u7Z1R*c26GLw>%x<^sLJa@s8Z>F+cnGE%Ai`xC$d^wpgSo<>ze4WIAUE6Lvdxh;telK?xt9P)*x!)dTu6T=j*xL zkiLe*hoAV9l5hLoLxsK<7T_|lg=&wrp z*p>*BX3Uskrs5!gzfdod;X7^vSzcbzyR-0=!S>ltmUOBo(|z6E{s8j`iup7Rq~vE7 zRnWHm0f!Stlaf!zjvNbv9ylRrAYS{z{=tAs9k;ZNLce>*n4SX8jOywN_%rLNaG}t~ z3h7z*K+BU_xjdJ`t2JLTP$_d_le(Q74H##t9LWR}SnS@N19=Bkcl~6^qYRq5j{F_{(HdqNhjv^v)WoRlgkB#D!dh)d)H`V7AzDMv^$;{C4^ z(Dq~@#uN*gj+&HwR7MHYDiPnX`kXeGWIfJ9eqj8bvQ2arlrH)hxXo0QSh5|MBTKeE zn5cG-Uw&+L!y!~bvoll=Czr{~1HZ_c!tHx2zp8bUQBFMx795^CHcZ}?I3aiRZ8Jt@ z_{Hn+8>RJw9-4C{0#Rp|wR+54)ebE0`@9tpTE5X1Xwi_`zv5^+*X5_|WJ80m%iU#! zT$4bGhj}sl7l<6Z0^tq*6CTg}-@Q72iy{Bz{wn^9sb^_OyU%K%z3+0RnnaOdp-_&A zQpL(UuCU2T_aYTHVh0pT!zd})&LdL+6U;(qJd1Bq<=yFVF^WpMKADb6Dj1$ITTdnr zkEq|WD~GPtoLj?PH)h*5-p)HVd?zkG0du&3gDZJxTqlEp5F{V2jX(sCDo9KxX{~aP zv9JUY9(aVBC`pL{5iA~t(Polf=)9)gCaTKHT4&*1Q6EEeIM(pMN8<=dWxi^di<509 z(Sc7PN2z!hPuWQ`IF#i9hKhwb)9IO*-DGnF8Ot9ttlIN585zN6DTZM(vZCYWiK?k( z7OX+Nw@PZPs(N$ve{RS5vNXIEVz8|9x=3v*9zwT!STp~?Qmg(NmI|Nik%c~5QgbqB zYEC2?PcR%9L%(TgZ6eC+%rKl7BV#Sj;Ak`*nMxvU=@)1JNif^6T!`Pdk1J#2sVZBR znwpA)HPg__PDhM$6HM5|rkcgs*u9Po^PZrmgIYu~Cg$X1z*^GJDa@6o5`#TI*T1|3 zznkgm;}!R_d3@?ilQRYNV-;l9{Kma&PfC-Er}SYZ{KO0|#PQyAu1iHR9Xr5GZ+xX1 z$YVe3p(Ocvf+RYOR}K zqi8EWh=!!)B@I*IE%9u;V<-m1N_NcrdL8g z?a`g{d?N z(w+7w)4f1)n_7Zi9{9NXYDO>am#{o);@PlG(P+lnkeTc2M^U1R`+n3=5-SaTeBM0) z%kNRG@}o6-%AToQ(590ntVT?F6@U)=&6Isy2)}N*L1f4m5LPgamROcTYv*(iPyZ7c z#oWFCg`-d6eUw=UClhNO#vmqk7d}WW7zq;B057V=1_yWz^`sQ|iCPKK-*76K4e|ht!@`_yeX!1BAATkU7xFeYV z1PZo?&s`Us8+@fNYnk8(bz&7v_8NI9_DcEqlA8O-SC!D9g9; ze)c@z0tWx5DPDXxE&%#5N?4|>b4aw8>yRvSSEiX0?vLOiRHB=2|NhsXiZGo^5&B@< zeI31A+X0#Tx|c~iFv?`0v!=blr=KbwgLb78Gt8U_OIAAE2z9eNK&!s5F3F0>=8W!r zKT;oYg44jC_`bW%@*i!jZbKwGRx%8gdl9{Hbb1jDI`x3IjAJZW5Ei6(S>l@9E&B&0 zB3*=O@#A7@kk#)a|5-MdEKD-rCeGj6t~5#M&W2oS;K0izF)(Eg#omlB(Rx#OB)aoT z#GwXoK_5A|4xhFvu3CMq($#~xb8~18q6z}|Mk(d{j*7ZYQanRcz1UwW+(Xbs<`luO zHb8f`LI0u?3T)Otb_0X6$!xt|`V&k)`37wFO)&S%>7x!C60RXywvpkR*hEEuATHLB zx@Mc;`Zkyu+td&XI? zbu%d4p@UVsAW5iTL@C%3XR+Bptl=TbDEL_lvW3tV3l)rQ*yEL9_5{2}*ri^pn2SG} zR+-zw0QeD)q(v=8w55$|>$m^`e=SRmAT^m5fBNae&*Lv;slWJ>PpPj@Hs}8)xC)6D z{+kM@_=jba4xHOwYq(92K^_%!WFTeunUd}dMB?$5o(Bjbd2zGrme0Pwz*zf#={HE= zk-#G(=Qp%0W&TPr?xACqCk52iu;mm2Y}17p~)Pp;4!j)g8pxkGAfftTfDxEj~L%JS-YlQ79DmS zN^OP@{~`ohPv?81{MqY#@>z!a4@vL8_|AX)S7Gx{=taWH*~L{AVEm8Me{X*6*Emr? zRYrPOpr*5hLko^{?~9y*>xc*tZ&YiM%KMfA@nN^p#E|?c8W35t>GBAcZmA?4{UPUr zmeY-OaEd_%oDz|Gb=lAS!M&m9W`6(rdUJ;x06jy(gJfSoPLhvmgsi*@_=ffX5ej3s65C6K;Qq$m8<98QKQ&(2=PnxU-p zy1o$8j9+3oDY6_(6~00AZvJDQX{iOaWATzEh(B-7G*n?ii^k5}^sObC8mWZ$GqLO` zFQk3dGhc3LgXh1}46U4`@|u=PV=ro6Gk-U&3KzERYKq8iQ&`M{ z66z)|kDF*;2!t0`h2%3jtiMmCM!^ZbbEazf%%%b%rN^OWL#s=lwAd}0e;=qX?usTA z9(Zn-UmlKH6$@~yBkPop@gA+{^6&}OC$4EF1IHAN{w%|uvsCbY>|1Y3+n*y}m=gfM_MD2y2ybg5Ee#G4-0q!EQiw8pk8 zajMzrRw<+V4n|~tR*qNe&{ACV!QlqG+Tu_laOhYoqD#AJ;#RB7epfO@XP3?5L=4w| zHUPUmS;`H7X9qE!R2UvMsm6A;@=1O#5XSU1sWSQI@4a zZGFgOeXx}tmJs?=@*}5@_Cw*EWqjMYiP;ArX6+xYip?F}`38=k++5@zfoItr7BvNp zF4AQz;o;d5e2Pd(OFTD+j|Q|942$uF+L(@u_{M20MhtWi8oj``eZXbdJ;tUMbs@T5 z2y5LW6wZ&jO#>UCoMKMSy6g6DP)D&BF@YE9UtKg?xrubeFm**3WxIPdoUuJm6|>fa+?m%l%uRVj9gvr3LL<9h zzwJCHAAzE&-HEze3O~GobD}0Q8+EwwOWusWqu$p8zx0Xc)rsjG`nO_2#mkonxKUW8 zdT^tvODb;w?|v&f4=o3rG4P^EMVhblocIjZ`>hvC`9QX&{`gG;d5Q(*;i-d2Xpw&Q z(C@{o(K1N_^R@FKtK=F!$oRG`ANJ|~1L!u@kE-(fHSnoz^B9DTIMV%qFHDsLJLx;a z{kiDL9o$beEYbKDFhRicb1(FhJbGP|=3Wa8j344(w4YiN#2MMp;ozg{ZV|3@nlHrC zW^uW#Wd@qdwly%Kn#Y-3@(E1S1%~fg$8y?v55Ejv(DaH8Mi2lDLbwD&5!bxl1li;o z(LdPNVw+uqJe!`sO+I-1;BEVZO!%Dz_O@S66!?*QN}cGHJ0w6VOK24*rD{2LcnT6} z?;~uSqXzkQdoCHMAs~sk5Ds?W8B0!Ldi>wV}UtY5jdD4LGbGekgSgCxr;tWYlL{X}jf-~Z+7*=_Z1Km-EIkFnc0w}d*@k;T?0~RO(X-cMt?gUsdi*&sn>-7~!6{jts1NIoIy~YrX86%dgI}?$~|o75S{0+o3V$9hED;=AC2cw%Uuz zn%c_kE}cfHoSWej)Zc!aoh-n&ZK3_#(~$eJS8R2BuOn~A=IX3_35k7z6YhpHcdy?T zKih&CDm+TZQ+|d2B7GxKmyr)L^LpH%>r{7P+NA>@T2c_uw_wh}K= z{~#_+Nj<<2q>=ewjhBlt2DB&B#;NNHLLb&fj9u06uW|Ud5K!YyMi_OJ%*>q>C92EM z;>IlY(CJs-@UI?NF>1~-TU(XGwu|5~DS1{Lf9-8?OV3s@sIuccBOP*vKf>i@a+@$VGIzJD@${J?%^ zbWR$Kh@|3gAi3o+$wOkin1d7AoX>tYxR^ft5(7R*bJfR)v>mbg6-;nitLx>KfB0b0 z^R~_tVhPem2#B0P>L0Ca+st1MG&OmIKG0GA=mB{yop&crMUe&u{f>E@M9R(+e8Ni% z*kG=uijDODHo=eQsQfCP4ijs#+ve{s^Ck58tsW-rT2IDABK( zeZdFd?BB}%F6P((0YEmP3v&Vnlj%yt>UUG<0=6c-yY4qn()-Z5_dBePVW5rSoXDv6 zv8I!H;5&?F&m}_q9}C63GW9WD8U(lJ|8ioI7FNCX;8Vp}8QfcR?|g8Q>Enk2oF z%&lWU`bbvMjQq9e!|U7LrSj=juRk{#iT|GsM%2i~OxoVX%-+Sy^;6eO^>gme-r_S3 zb~O5Iyma_Si+Yi&yu<7#aChR<4D%Ji3O83tM<(wnUtt6^PYoRjhFS$ys_g$z_7+fi zC0Q3J1h?Ss?(QDk-3jjQuEE{i-Q6L$JA~kF!GaT9-`9W7yzXXt`pv7g?&7i*wd+#% zRNYfm=j`pVNwQiy*i_M^bg6a^-)2XN1Tm228%TlQ(5#}Y2#Ex7J~7qh&TQN9^zalC z1H^Vo0E6t>kUAp;eRo}NlV8|xjI4spihPIp{qy&vUN)h8%} zz?D7T5Tc;y#e*q4HO2E?Jtj9&@8CVOJCW6!pyTmRco8Kv0Xe@6$Aa0@irX*O@&*?;0Xf=JVLq>VInqATRQrg0KFw6m) zYg7;|g=VSrv)PxGi8one{g1!M%v@sL?hdjIV?Y@vbPGfEogW)9_IE1kkDEfOO9HE> zYwdcQW>QETgH6=aL}R#kOEDiOF+E%)Fg#=%8_Y}-im<;Z@9{>u{=gWSNna4S1xp!i zAp$Z{_|iqq(#N5J$R*J%UzJ5r*LjUrR#bPJU>Hs&SnMxaTLXxHH(F*_2V~o8hA|nc zp3>%Gs8VfFxr5*6ZDUmI(nJcX0m( zYBNX@GlF#qx-^JPA^N33M@fAMI*Z(nd!S}V)@;#^^kg&FUafSD$R=LIXP^A9zF-U( zH$4Wx4}3%f0^fE3yj8TPNFT;nA0(Zw3*4 zrB&9mN&Yb5^O_1&=JFLH13`qCvwlv+Q_`9U>}z+ZaViQ51E_P&%67bG!@m8FJg-oA z(H`d$B-%*g$70WK@hf+v7$rs^YtUhvm zHNWOcwjm+ukW6e!ptxSP#z>z}0xX0Yz%+@Algwn)EqKbBhT=UeQ#cuNu`WYx%-Bnl zt29^>_UO?mZfPJheZdvvf?K5wkq2;ys>AL{1du4}apz}9PKeB>gLKFs8-Lt6Bk{L$ z6_P1=jn$8sIE!1$aC+3U=C6J{O}hRGCFHD#Mp>QK-1+@Uwp=uSp5GOs!tv3$z4&y3 z{EkQOEa__=H|_`ig#*(ZW0Wi69Q?y&zvXY_2!~9&feRWFNHTC%-zzibWhC+w#U@hI zPn2l0y1fm)%pjF&8K(9JAIvA3Rgav1vQg+`Gs4PJC1TCRjP9AgS>CotwJrypkL;^-V)FCwm@eg^K46Nze^kOIrx>Xm8;V1!@~5 zjePDRBu#2!$$GR&S@dX{ss-0edeZ{El>0Y0=SODhhkB;oX$+_ui6vV77$DHsXMPfE zpR*zx19U6vU42UUQy!XKeNK4v%ToprR+MHPX5+y|OJ~`bF`8_&k6Do)wI~fqtGDKL z{2q{jPaA2Ru{ZfTn&gIx)Cmg^tC&`5m5aL?rH34}hzcMS{Dx+q5~oU3J{zXzfQ~<( z?vtESZ-7w3vlkP#kfY<$ZR{|F~eYQaL!%@WRn^)=9Suhl8TN zY)-M#liNT`Tnt;$%w(1( zg}2^JS8f-j6fSZtO&|A5Gw6M zYKO*RxVR%@k##Du;j)qW1$B2tW+d5e%ZiNjk+~9>xOq3Pbf*7D8PDDd&M9 z{!%^(kHTc$I_nSki$=X~yO&{Vq0%Nb4HI))Tv@YL8z`rpSTGZ5f&_?C*bE^|NvfX3 zwMCad0|fcQ`mPfyF!t6C%~Ym3r?Se{+nAksT#IeQYvRYvw7-mxkF^GUjR#v(Fh8Jr zTnQ4)2a?$yLPQB1#DMN6M^NVv&PPNE$q*$7$`C_<;SDb$IjIQ4L_m1M7!}bdpV_h~lgB{l{?ze1J5!l0w-9X3U zGyVmIb>DbJScwTXf=NEc-JS0U+GF7EKz<#3I)kF(Jx)UwuESdYv3k?^F;{QYK(j_* z;Le43=8!W~vmPBsWDrleZqHsB`lL4#S-mw|pYQ2VnS7rKVF!7K3tGhMCss1ANZ0nU zwoV>GTsCu8lS_IU<>BWi2ILHb;)FaX5dqz}t>FN2dc{E6-B)bGb_nMLt(z~EV^Bs= zzW8EIrp^ij$lM_t>IEE&+E%bQl0vl{xQV1~0Zg(GqH?nwQ-%$wjU2jL*jfnIR(K+l z+rFvcKjtjLmwaD+YVNR18KQj~A*&|TsN58f?N z`sBJk#VpbL3`tzVbfI_ekY8p*s6phlB-CGkhdUCw=pot+$OIls^wlm-E)yp{;YHQ{ zvOn$l)r#42pH>%Ie~Pjoe#jk!1actbgIwzI}$(lrU6Co)9xQL(kItc^-ug$3N+ zN)toZeqHnQ(ill$2%O4%yV~Y1LUIV#M`5&emYxdJwM}HOB1(RpS}(zpFc=NJ*nq0z z)Jzl-ea6fF%bWXhv}Ne7YPtg2fMEJL#9LbfE;mTtdt!+AFU!-vZNJkH0I@(B28pvLecY{H*DArFRNkf%@R`Pa}@rm?Qm zZlL8~M%iA^0(N482GD(g_!BSJnkRszhLXunIa>~%rwmsBVQVko3=ycfP$*6$3exc` zRdX3!im3{wq@+o^sZqOV0sB^-$;3OUh8P~(qW?EyPRz80IZ54jFgA+9}W-3;&y@QUu8Qnb3`fPU#*+ymcX zqURlh7>E(hjLDVwT-mLb4{!7;te)HK;$drFN%uKLHbuLbg&+i%WY4j#~h|Vxt1INLW8So(L_McXXgO7AHCm2>eK`_a_wgl+^ zMCpgZ%Bo%K$Nm1|XS-Sqtu%Gh!SHo6Jgb}iE*?>$2Eadh8obE?;t(Mgun@J&I3 zf$2cf`-~vn#gk`p^&#{;hvUtgRhBktk9~HNoIsR(L^wB@LWC_5V)}=fBL}Ro}t*KOD{~mH*p@^f^;qsG_zZ znn3sJWi+zt(UXit*ZmSoD9e(j;lFv-%tifK%7%L;XNUeG0-ptuHU76ChapF)-ndDW zFkO!`&V#mTM~~^Y(`nsJUmywt)?khymcv#;wOuS;0Qp$#Z0vAhI3*kvG?fXe3Ckmf86&t4znPfK40DOkk2q9Y>{k6doM4N=0G z@nYkzu9$cx0o%P-$f)4PlhsOfP?$?rE#<*(LlrXNu!$#FwyLcRMduKx8gxQGN24uQ z7RKn%yEK>g==N^l#+e2*6S$)VT7!D1m^;%BwG(Jxn=N9=*Fa$V<(sd=yZ3|0TCjrZ zsiiCGSS~XOCq#tM){+X7mllexaghdMP}^4`=vsGnjc;f3n_p7T-N=7L`KdOq=9^Sz zTn#8{gU%`{i+zy5HD#$Tl!;Mf^tgGDpSUTzGH(1$W2UlkUJxtqD;ghak ztEOJQZkWo2dC(iD0DmK^=CEd(%5VG`lk9EJO{J3Ii$0Ir3Uk8-iV^(6nKu$i<`Di9r@K zFQ!;FXBGi`FBD|75XU1tFz*`bYRQEMc1qG@Y5 zVvZ@gH(q(_QzV1JO`P#2f_umu-yH4HD69&ecgz5v!RM|D@9Pa!3yXL^8N#t*Zl?&b zuOhm4TvaN8LwIH4$VPM2Tmdjfj>@8$ulxr|2)I^wizpB1V}|JnjP(s9Ok!xGhqiwm z3e4s^PrZPlPz4wY?ElN!>-VAXev2UK--BRbMu82ZX3R^#ehfO2=@UXY`W^~>E;c`Y4<6|DZq~W?QzYtE)dOD zkUxtF%5{VozKQV!Wh_HYZYUUL1XD5!$sk{tF(&ngSK*=ZNLEZPq3N&Y8L!|%JT+%b z;-scI%&^MR8Mf@$o@?HQCmMyAelx#@(; ztyb4)HG&W91!+`qTB_%@4L5f*Cz)9L*kC<%1Kq7#@mw8KI4RiM7FHB;)gGuJKgjW7 zxKT?n4Jd?ciIyc1750xn;*Tz0nVGNst; zRbA|!Qy@zaJb;pCFgVf_mU_|3OMd(o5$o6n;h7UNgVJi7b8=(Pg~3WRmp*$vT9r8aMf`?_kijY9*qyhS?hiFHQmAhqx4k zWTMe7LXER#MdLvO*OUhM5~2F3*}Q_IUHXAPl!1CEYy`E0EEEo({YH=)>83LYe87)r zxkYx6J*Eh4r(H@H3Ykd;yIL6NvOaNkg)YQ!Ao>n7Jo!=HHlR9F>U}JLK0>o;VbU1F zjSoBkSsMg>ke%s0iz6{^rf7fCccC^S)F~`6otj~ndP6RZuHi7?f=ov2))KFmw4|wo zKi0{q1G0-V{{Vj(dO}3+H!WmcHQOq1OfpXs^}*d(f=<4Y#2k7ql*Zcu+AZ?r-KfZh zx!NxU#JCmzCvVo@pHBUk&4?sL?caE_cpEetj>v{c=Eb|M=1>YkD|R9ZA=%_LAvMJ> z^K280mSmSE#!d?F(VscJsjhng@%%{VRv!e222OY~xm~AuQ#{Ys_@BE$>>}m(n3gWK z4f=&9`^kiE8W9b3_L%3NJB9m;|k zUY9SQ0b_4C<$S0gLHJfUt#9bsb*-epuUg281#OJc#j*nO8Ulf+rvHsmv%I#g)_@UZ zA6u@t+-Se15m7})tPc_%;M**jPb~6TtjKV%hrr&X)Rrlb;~iz+Q=KZ7GiQQu>jO)T zc$6~Z(04%xf1fKFKl^lTHu55(Ww4aa4=rSkH(E7=?4sXIgTsy7_H%}ofFz=>@eY1U z7aHe>V*JeuS`7tVB-BM6Y-=N1qEh9Sb9jZiRGq~y(s3_lM1E2yvYiw6%b%$XXmSND zZYjx~au4{Wyc8*UzYyIQhoSYu?6MGw)`@S=2L)%H^LZG=HL5;&!u7@O3TB(wp+0q+qbWt(23#?l3&o1 zdu)^dCgS(B6leE^YS)++mSC*+R?77Tl(TwZdpiYkMz<*piGX(~65AxVH>ir2dH4 zw!4eGy*tK=6W}CKV6qad6P!YA&$_h0&g zCdw1q=PKJc`EAprZSd~;!o5J>Qzd_uE_ZPLB(0ds0}nCsyIg7>zItBRcMgg1Fv{7q z_%0m}M{gtR_@vy1VGhB*RIX3oQ~7{aQ_5bLXeG`QUI~kH6G&tAC17KHS!DYOs(}@e zjZ^1@34@$gL>r_jto3H@gN^8%L!;?2UV)u|L7MBk#OKV|L!MFxN7H|u(mGM_5p?*8 zpe~)nbB)n5x(n`2l^E7SW%GS-1PVAo7BQ9SW8Qg|6FTuxNvtBHqN)?$g0xP-R|!8W zX&HQhW&VulO{VowAzAQzgAPsvRCi8b!b?(yFr9%LzR{&q_LdS=}sc%(-pEdt>W z`Q(=fEI0z`M?D~qeEY%h z%M|A(CwGf(SLYj~9%2R8W87@sxR8*JkU~hf*j4JH-k4=P43;Do8fN@)vtyNSeN?d7f@_Ht)J~b(8)&nLa!yS6wtuvge+wlA38{lW$mYA|j@a zO+xlW(qgSL%%aKdybn}^ZVJuuMw?)*9mztFA9?sma6BLS32e*p!iOrzcUospllr(l zLsW@rTs^N;;G|$fFLy+P zQ@)8@UQ9V)`f<6HE-w);J%yLot%V^850q`D3`0W2E1`#Q`w+krMzhG!{}j8+CFunu z#e<5d86DvQDRGKsBSz9<7s4X@Bbgz%J&`%We2rL!6b>beg>6|4gNEt=`D#6a_F9udtCDAgC| zxg}dx+7r~enD`(xecQC#)^=YIuAe!c0jYMi&p)76BQn}mY1YB-7|<@aq;nBqU(~ zohC}+GxO*aO3n#t4h>#jd?BywPK$lU9vPFDVt=@~qbQuKhD}{y!W+zA%_n zRyKgcE&l(-tW<0)|KVt>Q$X`bTscPqxp5f~6#Q9Zu8N*PgS#zBahO zJ)Lp`xv!}r^tbwdly>??MLto;ptM6!qld+;pcS=)6`*z7S|Y|cjNm)4UVl~{1{Cnv z)9mcJyt7xYW0IxkA8 zwU&O6-Yg(?*+-bHe^1dctyH;7E^gG@C}SHZAct>iCHqb1GR-;oqF$+R=c~w=MNwl} zd(1;|Q3N_Cm`#=ABFYm1#%*>w$@d=Qr?%6MMtmFhV#7C5Qy9`r(BcDE%&)FFDJfb7 zir=kc=44FSC{C6Vw>|woBNy*OGwWMuv?G_`z!^Fo z;o+>ZdH2{gRB|Pe4CsX0j_c#(R*GYqlH|qX)A`Hw-4N8%a&_ zRT2d`|4<_nrg|zKT|@ES`7}E;wAPldMw1uL4Rgwn;nV(y!pc+Pt9{6OPh9nCKl)fE zl?xpABa#bv{LFH)IUSPS{5K-9A?{p_LL7S$!Bx^G7sM5@#7wV|Qb@F0Wc%BS>O$e9 zB(Cof#Zkt?@I5Zk$~V2k)5?w(DuZ^U-#CM30K|shyQU11F1d;ICrrol z6P_7Fc2a||(B4uTIAm0Gh++aUGBmW{seRw&UXPFpwH6@(0Vz=Z2Wjo!F2a8Iyt6di z^%Ccs-m)gHWV*bp{D2B*5RpbDfd~cFL4?61fCBW?2M8a;!GqH{m=SlPrL-;b7K*?u zEzMcyEsjNj3YMs~MN$+-cFd?Ic-CR2+u}j1O5s$#@P~MM#DRKH6jMuni=T>o7{E?l8wu zw*{w?1xx83{0~A~n!#sP1YEsY&rzNcgl~nRQ%RgU;E)DUJ~RK)*?ACjm9MQn_DhKDok6 zvF6(5V$|ZsGm6kshJ~^>Wt1VhFitFY!Xh3?XyM_9gYlvV@@L}!EbZ+Cvc0URVypPc zVyif6?|K#UzF)0liC?UKNi=9$F%F=8(yM|DIX$eGCqQd3^slQ}-R%``WyFIE{+uG> z(gcz3=SE^N;?n!W*e|t{2&bXHPLIbeYCT7s;rq7ifhB5WH%|vM&N8kG+9GH^Blijh z{D8I4O6zWssRj(RsBzi`Aw?;){=M((#5~y4v^>F@<{o5fHx-g~l|>Y|rl5<8BZYcWt+fh+75CVbu5enxhdg;B zS8uzR^?19KPi)^m@aEX-Xkls><`b9u(!vjYSQTW;I@Cshh1iV%t&abG^Wm;uJfiCQ zKo$_<-rT`ELLBtNtYxI0o+g;5}Z<-WB!e^q9=7I@Z$hA?}Ge1+_0ZljRpD2ub4x14Mz zs7Ucar1@!l0-|Inr6`w7SahQ)8VqQJOGT!OSVFam+PtvKaYH{a>oG$`3y zMAJ%f@crm8;m;>#Ov{-XMY^7I8`aY!oXkuz-73AQipx#2XCxh3$dJxF9p~rK3ahQi?VPCCNpUK2z1|1{~C=jNsdCcTxe&jfy znt}=LFkqw81hQfG1W>h*HB$a0cs!;;7-FeND(S0Zg{h~A^|Pd|JNignb+El_m__!fl2 z+Qw*S$5TPf&5|o`e&)}J&&5L|e%}Qz7H62tuNO0047f6u>LP-m;Vi|uj6G@jQE^pE zs+;gc`@mH?One2m(?J@N*!T*;K~PHjQ0x_vq=|N~EO4bd1Y8rb!UnI-;27$xy7?sR zey1?cV&Oet0hoR>`7Z=2HnkmW~*tApcum_s%BG zL$t$I!c`*aW)eB?1o9`Y8=s}7ufvcbp1 zubAR>eS(8}qlihCh7CeFgkq>KjA$_CO-KS&tOy1&D|HdB#^pLDa6eLYII1|W^%^3fZmmW+cU%|O@fZhQHglOrY=~QiDD-A{L(!joMUy?i{di-Wt%SbW;usj$Zw~C=kWj*P8Pxo1jB;w z?hT2c^q$5xJ#WiHHom=Wt45b`{O9oFWS4o7dKpbGzyj9KlYedl;Jw^q#TsRn!yZUo$%Vf7B9h4YgHnTY9M-UJZk?{K6;Cm;FVxW{htB)QqiR?#>r-XUN-w1j26pdz zXWR&lUJRIwjXnm9MiTP0K6$$`_-~_m#(225n}3IP&ZMr-FtNCpF{e;ZKQ-e!-f$0F zrEn?pi1q;C5(>lCFwQCZSb(9+6YqhNVx;2jR)K5EJ6qCqG$%;-c{`EaDCG05HJ9|! zmk#k(LL^zdEpeGNmIB$M0}GXJ4nECG<7i8C8xyeE3uc7{-a_)H2|3v}KZ*Ur8_Wa9 zor#E^{6w!7W-WDWRI#DGq3aoVrLkf?{9?w$bq^APuNED+7jWRnx{I4CO5WCJ$lzz7 zHnLnwM1O31N8AAK!N!EMe_b!>7Bs`cZ_z#X%D8Yi6b||2oOh0!<b_~5R!$;2kxcsIITT^RU^G~Pi_}lxBBYK07*XZ|rS1TJ z(vpT}U!Vhh2s)6hUe5BLdlX{4$%OYEc$@wFT^ToS-9N>m)nd3`@kFusikCNrb)~j< zLdT88w&;%iN{%2qLgIc!?sw#z+9?7#ZVhQgj@WMlzt-d6@r2ShY>v0w0V`6w!z>@v zPSaBJLldlq?gIUU>qZmf|kw*@C@A4IGmWgF}&U99xR~zeB_**D8O)qcgXP2 zV@u+V$ut~6#_@9o?f>b?&{0QiXUjx~)=?z-|3h@J%bqw7Lzrd0w$w!WT z2q(7WIs4h)CX)9{952RVq53ep(`bL@t?OxNJ?=Xt@zHJ&N(byV@RpI)i$7&mzNfHaRwbVn9q9~{9 zE<`zqXl+D6&&!owK6tN}@_g~?rZ=Zk>0P(*@CYd3Y9UZ-tNe+u|DEbp(FJuOHH~O8 zP@I|6!K2^0?fblEK1@VeL}5jS`nlkxo(Cn768>^za5XbCRXbzDjyWzNRd%)r*lH8T zv~X&;=$rwr>W)M6F=7w+$pGr1FtSabXmLN;(7FjvIISC=+7850IQ}lxb9f@Y9`)4(v? z!S}$knJ+s0`b!vwKe=w7nD5Hw1s2Sz_b&9rDb1adpk*0p`S|~GknJ1S*X-i1bxzzh zbRz_ob>t{u=%;YR53Z<$mz0LXe=-|-W#M5$GJ!O02#*COIx7f$Y6xA5!0R{+jg?%n zv9oCq%qC7%(cO@D?^ro4zeRC_UJFT`1IyN6-3T{w(TNp8HaXDix5hK+c|sj#5c?*7 z)Pp#rLiVjxQ(swxo$lo4OKBy2dC5h`r|$d11PS3D%##ZDa7#>5Y`34-m|&8dlRTFa zkt7FNGW&f}!t&_bUqOc@4u&XDeg(qM^feW_rG5SiHH~~z*4`LM@@QkiM{#|_=&I9O zaV>pSnU#i|sbI>BdZrV8gXK2aa}2(rNA0vaOuzYa=-3!78~1Uffqfbw`}Kb7vgTVAvYk_m!c|woPx# z;oQ(i_jORr9?CTjnmTc5F|NcIKQOL49@)mXdXpzuN;}*KoLFpKq9SoplDj4xt7@Hu zRnp89#SH~T6<5T&Da5`|9Sgj^u|!>!njWVgYqFZ1zlF%R>WNfq;fEqjl>d-TWr4si zs`y(iStaPun&V&W9HQ<_BN=N@VIK|8c_SC8vn2+9Hbs6yAa@8u@yQpav^PLAG=-ZX z>S| z)1UD@yv2xpBl*QmOs7BQhfD|cIRasV_#;8`u60mEYuZw^0e6Zge{{D#4))p$Uq=8w zQ#8LIqL1)bturpfbBk!!xuS@Tt95VQfeRWzl$T_CRnUzJ(n@5P9QH_`!hl&F%Uw2$$5xrg|YA zAosxu7#3bR#C%EMK#k#&!LD5T*(U<44bA!HHPYV27@tg5jX)6p z>Ciag6<4-9GJlimunzNDg>_>XX=7Ka%pR9-uC6Y0MY(qB8S+h5?uk=&&7~6Y738hV z-j?(=g1k!JhSDc$(<~yHf$z3x(NvW4ZM@QGrJ&{^ddk^m=f{PkTtLePkwez+_qS-5+mGxLRRa|BEPyr-P zFB_TBc1Tu^Di@A;CFSM@}5c4wSMEw4G-a+7F*HY$+#?UTn zn)I$BNL75_P*bFGgjn(6b4!N4sVNAuo);3_Bcz!e2{yvyfVOypHm z7h7+0Q%0}IwAdq=vu|+;Sr5CF+~Wu?#kPDByvr6h&~{U1Cx=6_8;oakt=iN27Cwg* zF1!%!=a>7+oQ|oq^DAQ4&$Xm|qY3Fh=*<=x`26KNg^tz7UoE;Q3r-AA4jN(_&h>oZ z22V}8Lo%~YYMe7#qhD?^@rPf*Z`td+!;brxHz$1PpFXc~wkEw;7j|d89Ei7QcHDoq zJ$rkXwcbE;2J-^gA~pnUc9H$(Hu3+RH5mOXIsG@zz<(Vvs~zj&sA2k;&`;D$L(0?n zksXok)ze6QBUu5WO!_tu2n0}XBAGu7%%Vx4<2G_d6S9=~T%~#LDpR#s?iQ9l2P%1a zE92{P_qqEfN8a}VEXUErWyv@MynCYKVB(4Iz&q#8!R5{U{Ina0Ba~lc#vcqdCz9w( zkOhgo%Af&?zUgJA8&A!Sl7ccfH~rk!Y^!Pj`enRZN97JP6(6<;E?WLln3}}}r9crpBED>xpqWg3=UtWLP&^z{^p_ahC7Rw7tz3 z#oRE2>Atgt5NCPdD7rDSGNsz}d?C?aJl4O*%?BZwo5^TOi$Mury3lHIaJ{Ydl|jtQ zW-e(fG7UiI*JW-Ab5dSlvd|cU(l{W6BD*Xq+nve?-abtU8Kq7ssYMbo-zONfJcx*IkSvFubJA6=28~V^^CZY%cW9YEg#0diCV% zB%99)q36QH)1m5?l3G)EBl{y`VQyPy@ZbXxs+iYx%*G~fTrzG#Gv6;7OL@V%RF!Ap zLAk7CMTWzaN^60LKvAoTCHSaIn{FI)HRxn(SW~5fWXh{8U2LCZ6?b$E=fDnenci&r zC1_1**l5%V=`n;fwaI5F=9H3T2OW|PdY+sQ`%7EG3U*GbXk9vL(?1^!W>^QQS-&1B ztyi9*?Q4|aN+3@LH$;exFStpl#Hgo5G7@W`FK{!fdQ7M@FzFz(KT%VQ-}@}(`+B}i zU&FsVljVocSa(nUoDKH&n!PZmSdc%uKdM|>Bl?2tK}Cu32L@nwz3~6lnf@r! zM}L2~(GB$)W5;TGg*JU$iXqN-c+JXXj_SZX1f?YHw-0>}(q|4QcEODFRp7e>FaLP- z;w4G>YHuC4>P84<|CjasMtO#liCo^ zY0hJ5iYOr{NgbclRCT*cfpb#4DVupU+s_a1gH9%D-amPx3;7@vEJaD2_(gTPVZv{t z4%{>Q;zxhqApxmZh!A58q|*9?j@KV@FJ=@U+Rq`{p|BIPWgq+snVqN$;{O3>80wQG zK3TZGQX*?tR+fTf31tg$qila}I3wyV71L1e8L?5sD^Y@xe^#_h=M1fyN^ zN8)cDSm_n7k;zAT{;;LgORSu@NCr_T{eqE@m$Z!=i46W9hZ}{04>{&{xo{8yrYB8f z&#BI`w1u!6F1FmvMn>m8iC@q-+Nq1%eC+eo5n@@c^~Cfnj)(Kyt6p)a=y z;Q~%c9@P;65}#?~e@buO&}@*wDoe7Y1FtK_;bdt3vc3gJ&pr7=Em0G@Z9}elWz+~= z14WFybXGKEz%T#YQ0LOs^USHgr>K4ho!dOc9!XxqEgs( z_T?66y$W0I6}Nri8{_&n%=n^B;&M+gZC{!2K4{5BY@-Rv+iHOar1k71n_-+DBy`*% z3r;9uF^ED-L<-lLL9!ny<8BMa^>R!wfg--vXT{PI>_OUYDnQ^5mEC{i-WXlSDj-;=LKdg zesdllPgSy-wnyTZbJf{Wag0hCkI44)osR$e#Q^-p!%qR#tP-7 z_rOGa?0RZn0!uwbd8#s&=!f@ zROV>B9%OFObFdYv=r{!myU8WFC3b95T(L&Olx@D3QZ@|i%Ab-uRbuH@;Y#{)phjJ` zaE=m?B!u8SP@S@Bwe4`4X(=rag=GO6D=4s8PTFiTHVg?gm-pYFpzrD^h=C^6tk3po zSI2E@X|qiiTsFFK66$Aa!$Yu47%Fo4rOEdnH2bfG*MA5UOO?fZnw@T@n!mvKg@s0v zH}i&lPMMf=BcnqIzbY3Kd=^RV^5Hz$yl8t&frec-C^xY(`g@NiII2%VS4E$8`Fy9f zR-P|~6h8)>^jGn7IxdlKQ5>hE4x04xMjsVcfR}gp5_brRET2MsL{1uVyyH|Kbp5Fe zlxM}bX-9@hub=KgT5$|c1J!2-Z9~uVPZ7eJGQY%SNP)xqiOgU3 z+ifY+PuCOD=v*DDn?sUkfuHg{@=A9{wNC`RjKW++>4ZPR%6{a{N|+3izHZdT2IAw` z_=kls__3-{xFmH!7-TC7Lobqy3;?eXxy@RPVK50-PM4e<1iLw~`&;tCeeERN`4y{5 zXIG%zOE%aEWKAfy)t5Yo%_H)F)X z*237(>3^X^&We|k>-&TfGz|tS?8PtNpMTN=nvUVTORNw{olk;sC&Zo1XdMCz0`(@T zMn?CW4DK#UIpdP>F3s6dCg1s&0BjCvG(kmvO6v57Q2( zVh%|crSI2B6Ok9dqmeG7gQ9V$LUhAQ_d5A+7DBlwh(dV$Rss!tCFi4Vq0n)wtCqr@ zu1t<~sHE;%=W(Gon~LGoRW>fLR6B7a3)ajT@ECnZEaCckeLqIoaRg+!LTJ`)aws#H zp7CR0%3tdjPi3T8Cq_=4@&;s22tk7>H6T0U!W5&G02f3cdqIseYQ=0{YyPwcr}Y+^ z)jgE_ke)3v9(HK)Aw5lm8mjccmAvfcofJ3pGzaf*@AMfk_i_H`JAJRa_opS)J8IIb z_;JbpPbk6DOBL2l%?lRuB5SOI$npb0=&@+%iuCeFKIwR~aU{rOvw|CvYW^_zJt0Ws z<_Kj10~(pkzoy?NGut|RJGy{-fUQyp;G>AFQ1UbaCqG!B=86#bj`5I9Lm90+#(ruZ z9~RGDF~!@EUPlb~%X5~5OPksYYato_oXkOQ;Y2!_jTrumT>LZ4u!6M0RH z5EESc?CTu1ScFR(yAn}2@&{IIV*_Yg@6lGV+?j=^7$;Gg5RYcgSbz8C`eq+>PYOy$ zJ83<3W4c;UDODP{du4UE(fsh6?nDz|Fy&kzkq?Dpxi|yz!)hpgyTFpx)n-2RRYUkJ zoC2p7ZdFY)wQyClj{Ro06L6+;Y56t?9M8k7Wvkk`bfSJJbMf7dwGf;)TMFYJ!lv?f z>ao(Okdqvr=s#tvm_kWX?Hks8G)AR%3>c$k?1G*LJtMIz?z(RL!q%OaM(;!mHc6Au zU1kRONtdq)UCw8DqWSiYT^9bWUk#w21O!+L|DU@0zxezC0U!U&<-hly!5@fLjA+b1NfS2V+BHb33O$s{%;TQcX=v|Dv9hk)*9>ondDA#{2;gkpcl}`P7z# z2B`VlW64Vae?a-|?oa3dEBoDMjsUu1pKiY;Q9^rk3tE! z{eP>;2*^r^iYO`5$%wv3_^rmj8wLa|{;6aE?thah_@^2G{-HmW-hb8jm$1P;Ww3A6od` zUwaSd?kAm}2Y?v^T)&ZI|526!=Kc?Gfaf)JFm`m52B^Io+x%OA;ypa2M`3>lpew^* zf6s;Z1AY|qZ{YzH+*Zzx04^C(b1P#3Lqk9dGWs_9rvI&htlLpg4?u?p13LUSMZiDG z0>R%lAm*SCP)}6>Fjb1%S{qB-+FCl>{e9PvZ4aY80Bo)U&=G(bvOkp!fUW#Z*ZdBx z1~5E;QtNNF_xHGuI~e=r0JK%WMf4|BAfPq6zr~gKx7GbU9``Cak1xQw*b(024blHS zo{giEzLnK~v*BOHH&%3jX~l>d2#DY>&ldzp@%x+q8^8ec8{XeP-9eLe z{$J28rT!L8+Sc^HzU@GBexQ25pjQQWVH|$}%aZ+DFnNG>i-4n}v9$p}F_%Qz)==L{ z7+|mt<_6Ax@Vvh_+V^tze>7Ai|Nq^}-*>}%o!>t&fzO6ZBt23g4r?*WLL8)z|!gQsH?I_!|Jg%KoqXrnK`% z*#H3k$!LFz{d`~fz3$E*mEkP@qw>F{PyV|*_#XbfmdYRSsaF3L{(o6Yyl?2e;=vyc zeYXFPhW_;Y|3&}cJ^Xv>{y*R^9sUXaowxiR_B~_$AFv8e{{;KzZHV`n?^%ogz|8ab zC(PdyGydDm_?{p5|Ec8cRTBuJD7=ktkw-{nV;#0k5o;S?!9D>&LLkM0AP6Feg`f{0 zDQpB`k<`JrvB<<-J;OKd%+1!z`DQP}{M_XnsTQvW)#kKd4xjO+0(FK~P*t8f?34gT zNeb{dG5{jMk|Z%xPNd?)Kr$uFk;z0bG4oFYGnNlV6q8Vd`WhQhkz5p#m^vZSc48n^ z)8XlE1_e=c^$WG1no(|j8Tc`PgwP}{$Z2MV1V$=SXvP)gXKtqW)?5PUcJu&?e*#h! zqs>gH(jDQk$9cz8;-w$cc*dE1}qLepfsBCXA@(bAJ66ft0aCq$Wrcq)WXX{0nm+#w=uBj1o9rLyA i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/helloworld/gradlew.bat b/examples/helloworld/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/examples/helloworld/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.jar b/examples/micronaut/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49492 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 RFWVll$ONUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.jar b/examples/multi-module/gradle/wrapper/gradle-wrapper.jar index e6aa9d513585948a7652f8426ec6b5b2a85de665..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 50063 zcmY(JQ*__$6YbNmv28nPY}>Z&#>Tg?ZQFLz*tTsaO@6W3ocFAAbN+X8xu3OW%{+TP zyQ3Nc)CQ5j1>1)`Atee12BwuLtPYEJjtg=DxqJglz)NiNfFt8ppyz}F17n6u7S_Q{ zR{BN&=y@(lqWr^}(lizSJ_^e%Y${FS!Nggbk`_|-iKoGwv^_16LnmbZc5*$ zZ~mjl|FD(W-@%ALSMlP)?t}iF`5iJhwPqhb=k&NnldYNU!Jm!q-Uu1+1=|M;_)ZqW zCUPyQJDL;oM&jel8R6>169HvD>>~J`mXK8_ty0! zBC4S^mPKqHk;g2JCcSQS2qCxbxy4rHnP;itjghak`ov{Z{UIj=^(AEB<~TIeEmxL+ zNo6%I!bHx%UW!5#{x3nh)-=DEQEuP}0M9$OA)_Ug))PCtWB4=<&($Ts$|~`k9Z|jg z#N)ogQjDjfkjqq9RIj?9-0tGHvMOK9aO7U4F2Z(i(`tIG*>Pfg6VI`*E+3;lpL1B5 z#vzWBru)dtB|Tt?%V{mfs;Imiqtoc(x57((Q7pL9p-V|2$OfB!{; zL~3`aH4xr0H2+uI&*KemCCW?V4pp#B)>pp7t$7;i3r=vLAl)UF>^J@IHipFy^#Q9_ z{Q>OPB(N^;SYPj0YDp$^`m~sO0K+fVu|kPR%CG{bP4n<-#GE^mmEK3tHp4@`Jg(7u z+3-LGTW|RRSHRz%vYAoNRj3hYcF!S*9zxfetVM1NAi36&s{SO zgQkG3?grt%3xv^YMnxMFqnER*3`>k@Hci(yRq<=Y2W0JR?Ngl09EMtRfY<}Qv_I(x{3s+s}-(%5dbh4;N<9;QB0@UAm&8)8W6yIQDoP z4k$bol?tJT(~H{O7Q5pifRiC6TX*8s6VKibLoplWN*TNek?$tREAkhXd~{6KuQt+J z#QCRjhg{~xa9=L}fsuS<`DuRL@N-5PB}t{PalCcrmW^LMEzkr99@oVh&WJx^a0Dv# zSVT45GHyuuGR;F18#(MV`jCw@-w9P$xeP=5qJ&S0j_G(;=XpqX0I?rr8jUPup(%8n z3a{kx>WW<6=SRrI8;0@}=WKu{eIPms{QsJ>Uwde}NzQj)qa7dBD9SS0P?s`s=nf5w~P z?HGDL9xQ}e1T1J{SD)%7G%W^UQ`1O}j6A~pUm}SwS<<8JYbaSx-OA@qHnxdnS@h`CC;n(>--wp*y2v#9BtEkZH z4|{^26%H5s5dSN5&BeSoSrJAZFsN!;uDAX{1!-m~DxYkNzEn;tZ#?4>ju4z;WPX_3 zyDo_i{1TetZe_AGNEmk9Oak}l8rGq;Z6UpEuJ(grIas>;58*ZZQ`l`dfZ=@AG%#dP z2i{B!gZ`V)4R%ZO-bwl9qAkL0eqWak`b+}@Rs-IS6|4WV6b5w%_n>DLzRQNL0A&O&am-c9X0ISZbN?`s&iDNc#r|JG-)!RfYIQGss6ik5>Ay^`iqt7I~x!hDKoKpBc6pEs1e2Uw= z4mlX*CZP&`6gJLTZGU!J6nsh?e12c-w8Rpg_&Orr$l|ZS7I59Z?tR?e{#Eij>0<7N zG)~q>F#cW-Js%4&Yc;)GRlP;o28eO=RcZoL!*<_3IRnP}Z=q58Z=vo!UW38@p#UXW z^>kT6nlt&3PB8zL`_~Zs1b*>;>VS!SdV>Lhed<$t$}>ipuUBCIp!d{g_Nq>A!C!qp z#lXHI0M(hRRP(1fs$R_gzI`La&+JG5*_iQPG5DntqcLzO1o6uJ>517J`vtoXfB-7V zA>1Q}mWA=@E4-#%a&o^T`$ zyhqMsHxU~XCCM0&zB|Kj6Fany%aOhdB3p7DL0P-KLmwZ{l4Qtk1n0Lr6bXtV?owoV zkJlZZ({ZIaE*4TE8opn>{=OeX5pH?wdPvKzlK`ku3QeucWL}HqZ{MIWm!z-i77BITrE91rp63XJaka? zs3f;>)r(Yr-HT76GfY&wa-YPO^W*I_j&lUL#Fu?{sNBXsVbrfS~;Nusk~-h=b5;l?HE*;&RurWHrJ zwn$o@V-1ZSELnBPbQ;abkGBtkFEc~)fg+HBvQg!pUrV?#NQiJy{vlsAhe z6GR(!2Lc_|2k2g#N&ZdwUa4}~9=S`i$|f!>^zfu5PBM+$1zC!gDI?v2<_EZbi-XZ# zkzvY*Ty|4zHw+g^KjnOmfdX1SLs0m7Df-(~jA7eW6SSRs2`~)a%Jzf5D9bJ8lSp}N zB8y{wVuoc>4tY%^p{~SCqRJIjc}j9vZQN~Vgr0$d3Pj)NY$h_4ffTth&zC2+=WFB_ zYFy0&2&wqzFb?^UXlJqRI|Pi79&zsQ36r02w`>29kU?-R#pP;I zWt7|auHVsY^;jn{;L7h2$QfgJMwkvHiD!!z7dKG%@}frFWZ&5iX|E_4{NUzvS?gN- zr1j;J216<}eD%gTfz`!2=g19P8QIx|9=2a%Dfy{eY4YQ5_Qx1Mjnem2lE38?N5;tO zXofbJjh9d&Pm!~`el7UVAT+d3@kO1x=v~PgCz8}y&b8+dN4Mt0jfU$!ujkz;#PLCz z@(LSCz>j(VN=L2D^(^C)SJT(H z6*CAjjw6g&TE>VsQ9@*YdyXn|h&88QrPf=c9(uY;-g@jqgfft08t;;3MW#a4lCANW zWvI_Vp65JG0$8K@Aa7N*gb>xRTkv{)Ct~Q$z0Ki|Jt>zcLFNS95Ru}#TiApi# zyIZOlV3%n6&x+Av)v|^C?h#*6`@LLOSSIn$K)1lw1T&*Dfj25z?Q#}7V{RqYPU7!` z2S*JfT%^59wo599>(L=E8Jir^%7XdXG@`n8{4-t7gq*6DY1StPj=tEPzLTXp!dq(d`7KX4g~?L@^mbU9WR6O#@_TiLZUu=* zomES|LnZ#3^?X}yu5DS)oc1j8D4L9h$O3iZfQnRM!oa^k@x`e}AK3Vfyjp#WCQphJ znh;Hy*11-xQ8xvye`%%`f8@`Q+Z8VM%N1_4U4XRerje$}veS0;R;ybRjX|F`JLQW? z6^Tr@q3bMfJ*vZKV`}zv20h)S-buBMqt!1V(Gwh+{Np^RF4=1y9xJ5-T2~^xsop)j z8=TmDxt!9gTUwK`WJ-CxOPeBuRl$o&qu&g)rG-9uoYTbQ-p6y@yH|Xt52;bTnb=VP zH*gTQvCDudt6dcWr$bjWfbyHvX@ATmXF%uK3~chpsB^b*j4Q3;h zPiSerzz2J3o=YhxzfVn0c3HItjAI`E>7lHK7zZ4x4WU6pu`m?JF}}z82RaV&1%Z(& z`4^04mzl!_hGXs~v-)FWu>Pj*vM+|#PI+aLvw1E;L54?1a zMW@i<5Uin9R}Co)C%S2PJ$;d!){=y}9FFwO%u^phWvRT1z3|u*WM(TaJptqS8b8Wl z(Z8|{SxV(?QKMHC3T7tRU(i7iT*_R+qtRsz9n{B4W-{6Jg)GO}gQ{76I@zfAP-3Ly zu_vTPc=VfeSgS`5mf4k(?e?(hKY`k+ZC%2u@0m6=1na6g&lIC|W*T_aLO~<>65JhB zv3iciH^NRk7^{8VB|gq4l)iRPNpyc}byZuxx5sJ(?8CBi`XQYRyt8u>tR>H`(#^g) zl2r)CMnj~hsSCwJ3uTE}z8y}ozS97ND4Sy=Q#N1U-~5d?x;9pGi_ z@ofcK_D{~-=6k}{Y?{_UjDKf39?Nkx`*-vm$wxf002l~!YKR*{ULWk-Cj71W**?Pa zVZ%pLVzTvkGwyzdYx7Y;^9Vvy8uukyfsSeA-=bCJ&X9A99DSmf%t`MY2>aFCf%Qie zy@6`4RDa4bAYM7ocKc-x)Bq7Wr#|h5){i|U8QVL!-RPHv!t6VhPwwDVk}7fHw`=t;+a{{Rm_-*#hpn*Krc_>ig9J{Nl zAl-ucp2!C~<^E0zpiSQ7YQ=B%{V9wM3a0^DNGW-?;T1|%Fj&b_$B!4j zmJc7V+{shvq54h$8|W@i>|V6)0+)lip6*-S4rvwk1KhJ>`n^uslV; zvT$Nidijt@J+U&418LG?$> zYSPKDYh!(=rteSzY?`(O4#Kqx&~L0^-t?Gu|&tAuwNxZ_znHZ8M;uqn` zF9{(wH#2#_unUf7LlzvMJFuzU3XBof+4b$%XQ0URE!2Mga?U zECj?berymGRLhftS^kg^X#eYZfjbLE0flzWT+b&3@j1>42_lrt^R#_Uh9lSJ@Go@{QQSe8uja5a`e&8J>8~@N9_T4(RVMgG$7f%n$vG$Nk zDM8;^&)$!hkKUcW&s$Fb?C$%n;y!7P%|I}Bbs95Z&7CfcamK-)D@t?0xhVVGZShNw@G7tu}o^cMFn8~0c^hu;mFb_R3$pA!}ecL!@O zxEJdnp>alXWK;An2+^B2;W0CH3bPEp#)tzgEtu7tIvf4B@6R)<^)K(_%g@sWU&Vm7 zk93DzT~{f0-JLCqr7JQVIOO9SU4x`hmtqFr+dohJ!{T40sUh8tSf``YE+nJS-}j#x z2epPU5$9}P+k}JMQ!Mt#8Y0TOIgT;{R*DlF10%2BZ3*7BpAZCOTuZsw7V}B;#~8LW z3v6Xeb!KNMT}^b6HhxM7s z`yDx<&?r1_Kv5bt2wcQjZ>+Mac+-RD791vQyUKu_V2ukdkKNxUMIlwHcpU)Cavpd> zfD$?a|I6yuje+-G8K*W5zwJpq4lCa6O(--pRnr-#<)knMsc855Uj>2>T#a-J!TAFv zHgb=*wNro|s-wexsxW2M9r$z-$2z#n>Br?nWrw21YdD4tU zQP-s1UTlQwaXXepciq}*4PdN0YrS&(xm`AkZL;T<(DW8yx$1lrQ68Ceftmu^AHJ0z2W*pFg617W`!-Sh3YQQC zXT0KbZOAAW8~?s#?E=Ul#7M(M`dyuWB;lWp(Vy?-pnr(6@JA_y7@-8>qF$#Iet*dy zE+3a*OWY$5;}qs^i#T8I#WRhLP%;P~b#O&w%!Os*XfX^I+@0F8XUDBAyul;VoKvVT z&=A3Env;q}*y|#o|L56MaW_hc6I{r|_0nu+?FoGC2(!qP5?C_yi4 zvHVj6;!q?cJ~73-a)_IyXzQ5ekksyRXuYk!MobX%j~vI)+6pAZn7HFY#1Y3hVk9|Y zzI_cO3d#+=qIF4bB_JcX5l~45q+X>`usGl@8^q%>s4||iE&`x!l<|sWw73$nN0|Yv%Q0g_27QZkQUOoA$H+9tL>P@D2#*TTU`8t-dul}fTUK>hgH993 zZ!E{wTzup@TjL3Q{RN@-BVb`X9!AEL!%_X9C9NX~35hAu7K$!_nWL1ajrguCYi=+K zpIA;gEuCS3qtB^{1$Zv;g|18=x@^*1TgJ+pIM@vt&o9E9HdbzxsjW8uw|P!p5XiZl zLvN;6+Y&b>uvcUG#nzIp)cu!0u4M*C!vMUa{pscWUH6IhKX5T${zS9yjGdJ41=J2a ze$5^&V?BhfbEkvhdh9nj7doap)?uIi)sqR*U4}Z$zLpcGz&rI^je19UJ@nKL-OK7W zd7j74kZx#=XL|NKLJvsXUwmpsiy<4mjQ%QnWC*btZCu)Gf@BW+CQOC$YPFjTTRTjf zJ~L%sZ6W?!QTzu&LYa!W5?hXCS@oSg{2n)8vK&Gj32MhZl&l7~oZ}0cX zLjqCO+UDAFwg!mw2-7I36B@PL@symV(e#R7Sq!m^%fO77(1>q^t3;W(os89oP*PM< zpW-17Z~ZG!8q9#slMQ!-X8k` z`>!u?|3AVRVCTJg6!?qZ?P*cZllg+ePrtsS;t1h7>b%z`FLO%P?PMPm*&sNO8y<7@A zf_)=E`}U9j?ben582TLlBh9Z?)99$$4!IHCZbhd0^r;=VKPoD81EpTUL0mo{D&}1V zPe(unq}e?a-R=uQg1S{_(KDSH?K(c_(%6oEq3KYPh=mBKk8<$JKYk17^b=+xR(BSf zrSlRV2sZSS4ic;}g%vC!DK+JxQfC+~35u`MrtRF0566)A1~o+S@)sSdUFPh1y4|5<1LSc1Km9MgL{f{gL0yy=pzA898h#v{+bz-?+b#L z`IN)#8>~OM$H(j&xmEup0+jEhFd<%(g>RmFZgLCj#6I4;o{2UW)|T8W9AAVz0i*g~ z%x>yJg)qfF_7v-mBjk;VoPm=_Y-tUT85|W-$zj3;5#7yp1wDMrE14&1xsn^r^<8!( z@CG?BjAV3MXps^%wcwObh{51_NUj-+mf>V9#auid>k-K3;l*LA_7E-AMEpH zv65)u)nb-@6x?WQ;l+}7=K93M0N3Hb&9g^e^VI@53*HItk(zP9-vQHPhYKISkk2ZE zIH+azvs@qjg2{o1i~)zlih-pnP{)KAy(tBSxNu~4*$OV{&y5ZuB^*DU#A+*UJXntm zGczB1ZM?3nQPSf323$T7$s}VjQjxEV5Q~_b*{DgA`9xB3s2YJVMxDM1#kD-cl<7_grY_pkI|?-kiO4nBoQNfP0=)6SPl zIo`kMa(4b$D-8bX#C+pZ1+5(<0hY<_`C$f?BG`DEm4tp=#Bz{6BGw;u@k}a=15EAn z!``18-Nh=JrBfzAxi>2o$CC*s+}UwS81@mTk4nl}7o%9CUc2NLI;>etBwo5#l|CKh zrG7%a>5-iF_W0|MF$c3yD!NSX&I=J&!pXFYUD^#zTGKYF(}>QjJC>rNtg1-7?NLKl z#34c>TIEDJveU@7q`EW#3o7hDGA!#sW-L0>L@KOk$|!B1D$;_iAmSuTo0O?=iAlNR z@x`t8T4A(iJR?yc#kR&A*UmI`dCfJ%($=)K0)2h6aLlv}6NmFWj)h8;gkS|d(%Fa8 zX*dRE@e%c_71oBdo@>2JHvE}woh1}kz$dFw+GB(RP>F`set0DVH z(cPK?P)Qj>cwT(C9;<|g3c6}tGDfyI&eqDq@ z)_Tkq#sb~dktOdz_l&q|yM@aS!tEH|Hvv|qiNFP*!+SB4t4c@pbi%*Uj73w~=u}l# zXYWUHbkDSWwom8PtMfN3|4jTNaKc*VnQl{crIs?id1Fs zc!6-6xr48_SB`4`TH-_y@#ZoPif9(I3E`NYb( zjp>QW5#7M$;|t`gm*URBX5Sb$h_M|8wpG5-Ibv@t9ixFa!!rP^KEO^uBrRkC8l!?c4sbEX?^R%;}l@ z8<^uy`2yqmtWkn;-yC-39s>D>;g`m#D9mqMOmL`c36Xy2Q=xom31!8Gmb*HFQcB)=6MW@P?#ZtU!rVk z)^&R=(V}%V`Zl>kN5uZ!(=r@f>hVTepnsP)c}v*i3&ce4Fo&a`u+f04F2C7@C0^5T zd$85jmGa~$z>3ulqN*9gyB3BAp66|}j#%F<2@ynO8sJ#=v_{ZlaRZ^5^{kg+v8lU@zaZDfIo<6OT+sZDC5|KEl>m)sq)z-Tg2(LyZK3P^}nc?BC=L7w|crbj>n< zaIM@{i+A3x8L-Jz?{$SJmK@k3=o4~{JMYT zq!sWW`jP^vG#6TTMdiwM`>SAPGsre$D2)*yM#@PNM38&Qd}lQPa#r7*^x(n!X=dn% z%%L(vsL=Zd!{fPT!uUD~$Iiza!&Y#MZi@c+C1=Yb|EUS)F_-+s!n*jy0+RQd+KH0B zIQ*9HrrhjWmlxlUzFa5u8mV=<$zpmbb^3{lS8yD~-cq)OrQDM>+mj`^6atU7FjB^i zkB}T+o6j&P9jzCLnNMdKf?}aRPvKjvvkp3=U8eJTThgxE$AmL{t3jarwk<;AwbJ~E ze7seS3xbRuh1l1jKj4-g!U#MxY&^hm>KhvkKZO3*!@VEkoF=m{ ziU0#wb#w@jA`8Adlj}4$6-#gSM1Ekzve)XCBos#XXLAqk0GM{cZQ&Iu$!la?$}&Hx zd|7sm5szAtdAI>a0PT`}t_$T7q`B{K-lmdJSj4-eYEsqH$qM0ym$^yFnpCtM zW08SyV^AoG9Ah(OpHxk2N=a))X*+DHH=$)`Y&>M22M+}=6^6G6H+sY7hSA;kWlLh1 zijukG>2=)oec5^HdJz!#xTVg6Y(NDJL|x>QWMmj*$|xEsolSxDkf128F_h5iA{;RU zC?3$U7H!m#3qPrrqO6N-lJ9@K8^ODqBdq=4MrE5`YnN1O7k8xVIMH*=iFlV>>yP6I zgjc<}SAI@!A0}}>j2?)TpJpPw_%%XbkZ?!})QBU{9C5=ymI7Vi z{08B6f^=waTmcr|J8w)QWsn4>5t_A7ot{Bj@)Y)X>9xh4T}< z&!wkY2$l`hX6d7&9#z4>Hwzrpf=jWZp|$FahBNCBAN2}}2d^aA{On2&JUT z{AB5ZtOHuoc4B;af_8FbnRt5sS&wQRxI%)+qn008W^SK z?)OcK$}5ODpz#)py%k9v2)G!?VtQNFXcd#SbZ>d%s2fZJY2H!P67MMmVAdNHxh59!Ybr6S-5rCMI-{6lv78Ap6K2ed`>|TGYdXX23 z;BJZXnVEEj*Oq>1bzZtir8y#*o0QEG6k(GZ@+8Y?YFeM$m_wGxYVPGtU40l6R)2(# z=W)P+$7vQFR-4p~x5fTw(iKLyUTMPBE2HyN#qI}RkKWOS8rYsDPjANpR;Rh3s z27vXaV4?ZXB-If{4+AYz|9zufuT)|O=t@|6x# zYw(vIz`PcQ5g*uK3a#Fu^A{id=jg)nk8~`BhnVjOmI@BiRcGseh~yyPq#ekR4wFM% zb(GWmS@Sad56J}t@3DI084lmF|L6Be005mIP5Q6m#H7b&+?r0)Eem`uq-yz@6G{*G z0;(7@uLm*Im&)&F>Q9{^qY~I)R_$smdYZ|f(>?u{63IpiGNq0URHq$vr6Rg6|%AMO`&zf@BnO#COUYZ5y(0S1e$ zn%6w>3S(Lr@JboVGMYAY&ES}}9_&ixm_l{i;gSqfjk*;O`JNqZ z&93*+T}WE;phkWZh1Mf}{Vg0h8QNH8TzzKuv2W%lclTXcR95SvLE`z(fY2)M9&Y;_ zb9cHnA$7zK3A}k%O6d9b)^wdc5s77^UiY~`o=**j4F-qK2Uhz^n1K@vz(HdGtJ3)6 z>o44c#6ltIaJ8m2n~)er%9XPF zu{3FY)e%(^Q+oE+bZA=c;QZb23Q<$4W->faz zZHt#2eW|0ev{XKv%34R5zGwxCg0qv#K@hOemnvh`gwC!mdF^lkzEz7w&FFLu+(f;OA1NEkifD*3CDvXuzxXv037C$` zSer%tKhI7#CneMvRtc{Xu~bYPV@ZkXHc<~x z@gxQ2F_GuUBpR=Pv7>`#lp>>;2$p{#YJT$T+81BK3MkoI5i{(l-XYf0Xp!EgAoj#p zlt!;OX}>USzdW2be5KxyZ;g1qBf8sENgW1{M@TF|e^YX>j&RVBO||)>{12UDBFb6l z@`k>Fz>(h9`6h|W%4x5VOpOXl+?Rico`3RrhX089LAf0Y_z!z)A2tLngm^)ECMoSl za7UuXpw;WB^ncqVEK>OD$2p;2ZbLXahCh13%z^O@uekc5^;7hbvP5HhrA@CJL8*fK zo;9u7D>#blANt=h_3jHJgn=D%4KHoSy`jU5?@oAuY!3LdOsYBuQ7arQCAj%Q z@I%D$WM{W0G$x0wpHYc+p#1z!=MM5K`Dyw`$iItZFRqUJiJy1ZLJ+|*ol|DYjGKIak z6qkHv-JU(bCN6Ex*>LggWB?&knW%l+Cs!>R>OiF*?Hp&8L#&$_euRAuip|aCI zEG6Zacvf2R$_g*{^@g5X_DV~ayt8K}o^mxWQ_t$eBR(r_^ovQ4Kl3)EJ3>Ex5Vx5u z$=r*d@(vuuBr>c3$uyZIrc*hWPUl}AsFl>CxnNJ}wx)-H(2$vd)PKwNhNB#YhO_kD zIJL`Osp*yw*sF@kWtv$l;3l!cuW{i@>6vWohSi`V-bDNomY(f#1w1Z0 zf`kKJW{rCvX2!#UQBr0`tn+B@>w^ft7HKx4qvme)A(dD&LtWCIlTr=8nDC=T#{|pX zbS{_b@GXA7HWXh#ku!g44>PUz?BJn9$cW~xcNE)eA_QmQvw+y~5We)Sa5TH8B~DAu zHPQYvaTMF`(~Mcd+%W?ng9)J1|2I7)bS8KFfQI{56vHKQG@TVS*$@{gd*QwSH9C`p zYA+}%)PlY$t=>Rcr`b>EK3%=uK)1~gRZVe)*DM%NL1Q0OpybeGNBNmGwcN*# z7N;Y*u4@B%R2I%`<^n^mpkSF|W3EB5slJH8ffr;sF8Itdl&KCo!2J!3(_qK=I*e#A*uTc*>SEpWpvB5FMUF_dq@oenphJkflDTjKjU@q* z=tJJI7y*6E$8yn<1u?%}5tAXoH9%7FvMQx8#EK1RinWvzLr~M?OVh$Kj}ecjNrIM_ z?MN?xLW`OiL3g>dmU)kikHKvV_HJFk#I#3WBvJ-^<9uzfNv+Y=jAZP*(6h~owPCYb z85WZd4jOz9YLSR#9&^*UwJF05Pq}{;EdQm0J`E4PfT6O(e+Bn;DID!5mezpYED<-t z-V1I!VCv8R9aM(O&s0yH@+rT2WX-9U$^C0j*1Ab0a1L&OQgUm{%+jA9uVGUbYY59E z0v;Stu$)dxjxBoJ#)zAAlG_H?U;1v6N($|1(A}saFJL$0=cQ+W-{PnZt;U(KiSYyR zR&2%3T^7T@4EN~@8=rA<7KIsQi=$Wd$jH_WridGys-|wDWpWUae#W|>kfxU!$F%!-CGz5QVYDa4$N8O`nBn>a6PJ5bfJ5(%S+i=WDxJ_0Q5 zqIDcI5p74%TNIGD-`KJhc-s}p@d%NvbeHtpW(#mfMKz7PCF3EC6bfkVwLujdTBtzJ zz%wd>Jh`PVW^IFzoO$PROc;1$#yWp@9al?g5Jws-_4JmdIVS^KEHt0rXib>(w(LfY zXwMHcMBKDePVV6Ep1+n9U)=C#5AP?U_!t-InjPk)BKNB#U!{r!c-Y&LNXe-cD zO$uU0^+>)R84dX?8KOeK%KQGR5WxbWF+T`Y$0DWK55lSp*NduxP?7}h`G*N3YT-f> zmw$L1pZv#<617`N8=CN^3up(pvP3-AD@G3KHhw|)d0%D^*e-`mAR8vL*!H8c`@~OR zBIgtSVjs11Yj(Tom~OyykfW7`C6P2TLas7T)r=zYJlLeC+IJ@=A<#hjuXwwTv(n7ovJnISC;oTeB za_>#Q6)Tz053=^_o8{{;I78a4d|(O?&Qi=J#z3lD^ycb`lwa`o(&4v<61Ai&V$1bT zG9Ax1RDGpZh)}ra5}yV#ga{fxD=ZnwQJ(P+IPIg!sHG@66rQyx&|3DQUhz@a{#w$z zcsI-lakSPuuvR7h{FJ*~HPJm2=+=UKPMGaG$(1iBPKkj0gQG0|S?DMgIE(y1&TAkw ze_Q;SG5bM+`BM{0r@(N;3@BO1j|`K9fV1WH5%m)7z~anuRvHJaAGq-G<$+wAu)k@j zwuvQw_HY`qsFl7~zYF5s>&=9Pjy@_)YWFDJw~Lc=TerTEUT_Xr=;=BSE>cmi4zwBB z8UrNI2jZbpG-V=judy`Ja^h_35u)kbe;i{ZMFWN$C5~7U(+#W?utq;GI4X%twJHHo zdr@v&tiP({G@)ek*)ls9ahQMdS9*8H zq?rMaW0S(;@{^#a^5ORNh4)KDa!$UgypuLGX5W|{BzAXnuUKYL(|D&G~( z(%-1e{Su43`spVe1g?JMevxjC`nl4k#gGb!iUxs#?xp$H3-qy{X!Xf-3f}d8)2jn! z6uOt`PN1sM`ioCqp%DIVc56Hq7P7wWjFE!^l@7H672I`6a{I{Ykw&Q)`U50-x`Pv4 z3QnFFRY38K>s29kZ}Bbvvu*b4(6ga!q3+!+AboG?&Ho>mg!L$|RJqct*fK9KKvDI@ z^+jEoAo!FqODq=nX~Ytv(&OG%B2~!AFR)w29Qj)i^~S%dxWRl`lNT6WyZq#Fl-lR_ z>X!m#=b!?TrCc}Tj>|F-%Chom|5Y7pW%vq!4~VP4>RS-m?|7@n1aDoINp=dg$o1gg zCo>DTd;RWceB zW_J?qAgv60ySET7ap_jJEQIKrDn4MTx$0SqZ#tZ>Y?`uC72uf?Gxg1-PE%iY@_n9Y zdAYSR+k9)JlaLrus=v2tcbNai*U(i4wDJY}S07XDCGTC?P|4zp8~Zm7E9NDLch$07 zvaLwl#(YF%iju24{mI)2+iRGxU`H9%^b4(Q9mE8_-MW!Tl=d+&KymC~9}eJ_k;eT8 znJ2qaH}I6?qM~N|GCs&X9knzfy`W7+Mq+o;w>m$Yj0>a&&r8 zJ+zUw6gb4zxcqz8LEe-pSTBthf<l7P$neRvksG^QmC?oaa!XKVDMjz^fk=36kqB+FNT{vh1BvYX*9S)vDC*T`u}is zj=_~h-PR5}?%1|%+qP}<#K}p=)`>bDyJL6Qv2EM7lg`a|Z+$=Bw`y1IAN$|hyVlxs zj4_|NQa<@yxj!b4Z8rszU6>iSyel(b37bg@C70pQRl8zGDdcxllR|8VlKug8_`T{+ zu5-Y~x9Ova5{m34qA7Kl1`h227BV$AM~M#36C1l?e=@hEgvosOsFU{R(Sj@0c!N?P z>ts@U)2S=HmSFYpF^u_@RU7O@!ZkpMgl8s!`8e-9Sm}20@E%`6*(339$SXF+B}rAD z4NNOSwAXad1$^ItjdUXZ1y9!f917nzj@VLG4)DULd-F<+WtJ1_dv!8Sv`n42&~gS& zv^Ii_58Mv|cCiVi+vM;-BU66DCY%Xi_i6B(E!ejZxojb8{)3dZB7B3>L4ROwJ1Be} z>v>&BTh-JR=Ae0QI>KgIu=98vCILFg07@^$mO}B{fjUNZjDrI$-_Z=^J%Y-90j~$G z;WrwiOwn}lzu&VM^XQN1T)5Euo$@Q{>{;CAYFt*MKuf!b)n#15{!E9!4xLsO z>u04~#>*nqrZ7T|`c>93UKVS|oe_ZClIKXJ>K{aXWU>F)y_{XzT zbzaF_uIFHR_4{qQUA%6NLBdq$kyIvIWSjq`!PNcLMNy%AL%_-f$D|MB!<{W;Y2zGg zg@2|(oN?1gpMLw}Tcc+tiJ2>qrjDUV)J7R*#d=z`5v#W<-Ra6)Ajs0aKi}8N#0LicnhpO>Szg44I*04FK2JDQDW0M8`cP z*pvm?x!-2B({?SYYBYehQNsM{^g>O>(+hu2D7^$DpRsROOW%cU* zJ~PTB#p0c71z9;N|D50{UPK6TL4OEQiy+B)Mv;#XtI2|O))fJ2(|tbzf2qrRBX!y9 zo#egO=hJpIOxkS=Cel)sm)aH8wY!hJZEV*t*kcA~$?lRqse=N3iZaLRFUmIT;0(zX zWi*796w+5QZL*J@WE_l!*>A}F+cj8fRQofvq2&H2(P-cOs$j3)W`PQOeK)nhsVYtt z>`zc%MShb#4RRT<6zqZ^RyjzPx=@nH;E3wrM!YQXab_>iizn+4&a#(0|ALy;4bGL; zXbcVS5iIkVlfGEn?@)9cc8YvtCXC8*vz^m7nzUdh7jk=!u1qjw%i6Y!u%m&1gdG3w z%_Mfnne^yU2~SHugS{q1NoRZ^+rzBNk2`gN&+Slrg7*NZ!sgMLR}w;ZZqtS`^X0eL zA~2p8Nj7C`=+bXddri|4YEZgbv-ay7mPfs(&8PW3&amXBHOxOkALwCOPjN`Vg~Z}yJfU{WOEC*L<_9Rl0I_&fjLZYWadQ3`UctJ zc8UN)qvcO9UeP~r@mg{GKE;3o>zH=;1#C9~v`xUP!YR~_&WYUiB*zooHNWwn=)XUg0xrSvO4s31V1MxnEF^Q$|Oe$Qu9KLw5^&wfwdx^Y+ zYh1kjkaYk%ZduPDANXV-US#f%7(oQXbLnCK@4P3tnct9Lqu7I-AUr=}Ho?&$w>^^G z33%9(HjAGyARE+uGZe4r1z{mSMs6^XrjLP;L?6&W79(2}XsxfZB-qw%jUa`Q*|=tC zZrN~~G(^;HlI}>mGlxIAU18yH?vl5tP<|H}!Q28{vZgI+Ma@HcFijrQx-|O!NLM74Dq?Bu@T-oj7*;!q~nWiul5`1t8Z7i70%cBnpXq+$Evi zvJIOHWJ`TYwE%YJbzU?6At)kCIK71mK}!wOQSQs1G)Q2zrwC~eG67OLrx2vyga5%W zNkD(IHJ%ZP`#XQ!4Nf<6T)~tnrHjl37Mko|D( z|BrQ5nRH`H<>muf^C~@Y;ztTAgpC{;IuAk^QbiIZ8pvNr3zO@6#wm+uxq>S&{g!dx zRpRaKy9$#Ry5MvllP7g#bvJENqn1jfz~u>(;|BbIjAZcECPdUSh0VDvjrg8}rdrW0 zO^{!!aj&cV^cYim|Mqwuz%K*|F9L9xyHopi{T+F`5G_I;{l{okbnSTQg~+~=>p^oX zMAi=h8gzoAX7W%_UkJLH5&Q{IhUZ>nyhVnm**erB-6;oy0-0n*G8V?IIR*zASrt{t zaJN-}qmZwGt?)O$^hf~p%DD>34X2bA-rtalNFgNR4HbJTF-Ipyc( zj@%|cy_im}goT0NHhH+xh=0slg4b9zRN@N5+c><6TIna6AvAm9gAK!vN0w2TAo)#n zysdP?-?B*6c*}r~$8+^>!K53k3^#THJQ$Ng!W<_St>*UY=dMGEexEG6M7ZSPW<#DH znqJ;p7^g51$PvA&{LR$}r6VX@j@hKzCj@s-2Ar+M6^2W!44t$;Mnz(!44z=!za##x z9&7Sqewn6bl5wq=s%5WO;@efy&Wa5yj|jEo_mu#4&@ zi*c=!Nj)n-sbsQ*n>P7egPag11y^m5f_7k#WS((z6-dx(CW8H#QQ!uWL(~rtCwH}F z2}UkUJckAjaZKc||HKOMUGYD}@M6CDXP^+cdg`uCGq;5T$(gxnxuW!{UeKsyn&gn_ z=?6Ol*>5l0Z8vZQ8<1}ZD{p~~O67-%TA3_IgzJ3d1Jc8~)>2CyjKbXap9nB!H+&19 zrta?|GZ2=PfRC^ajZsl4czBI<@ukZu0coO*`>6sI4)wb^bEog%e!`Kml1gkX3gw!o zMAgZX>a)j=rzGVQ$XUlW4(Z(CQY{eXJA3xZ$V%B>U?AM3z^j%QX+6<2$u6fPKhU~0 z#Un9CFjUXcy(eVd3?FQtMwhA_w$izJ(G;PoY*0D98*ij>ul)l4SCi29P+MWJdT=l> zEl4mhp8r$F6%~;{D)JP-mW7rFtqV#$;$5u`vH*1`RVdPBu^40_45|?nYD2F+i^As` z3`8;Fckq8SCqG$J2H|@mvU&epu|;$Op95e_(20jwg2TuJ7&4EmcsF03w+j`n@ykKE zWn88Ot7u1LTDtyuXsrv*7xaqPx9SVJFA6%oQTM*vO|XsLQ3beLZ!$J7F=9Y-9%5pO$PS<7<=-#{|yuEv?F0Jh*9uBv~FL`pV3HQ zCB>>9quOXzZ_Gg6S8Th3fymQB0t%IQ|l* zAY`&sB)B>5c@SwTt70frnJ4}q&M5unWjT}<2U<@ww!AtV{n(V5;qXh_v!b_TCfn6U z#U~GHN3z!ZxUMH|0Kb=2Vo)U23(M8U)8SvgNyE+Tmm}b&H+r}3dYp@na}RC?s=-hk zY;gx7KAR35e7#}Hz#QE{9sRwH#23-v&AECy)3)p|rf zvYK=`DcPQ=7m(2nTP z75m3&4Mj&|;lYn%?hOeFiVX8gO-BxbzqqmF!vlWTlH%pyHYEBkkK=Qo`c5}090dm- zK5r)ncT14UH<8bkSdIfzxV4b{M2#o#gPHf!J=Rj1x^P_<73(i^nU-5qU{c#G&jt!i zOPS6lUvWBD^GX!+3U5B9gFPv%dEWNw(3~yKSe;A$ER7qiEJ}KLTsx}5Y@)eDt*jc+ z!{cSz!j_SAl&8ny8baVs&Q?-4uZBTv)OM!QF}K_n(_5+bIJ^U^R{XF7xrRIRn(xx@ zXZa`xr$LwYL9^E`YvHPNC%opfT7KCl1AIe#Yv;Eo}!f04_s9?Y!xs zYC)}1uBF%l;#`%HFp}Pm!kUfYW63`%-boquYs)n293@V(ok5zkY%K^_gpQp@Yk^BTr(F zK4y~ss{9N{7O&MiDov4ejqXDj z2FI*~?3eKZsrZD4m~m24!i`7mFuG>1?UMpS(lN6Fo&Z2*E+f?&eI)jb1H&l=p}bu$ zyubuu7Ad@ck;1mm?9G@SZT(y}Oq@=KvuWo0p$ioSEYV%CgV=y&i|5)NHb3A7#Y(cyHLHqXDj4KQ2cLmXB6@Rv>h zeFP4j=~w|{@JBM6qr2>%BPdKToa@Ob?~cqXt21*Z`cCbeJHmeK&O6 z9~~#&!U8|}I|c*@hy`>ChrTy1e*e%QJH zF}dD7o$M92JT4IbuXm4SvDW(P>N5`IF%Yh?GhY?xa{I|aQKMe8fxLy+n#H?*C(f^@ z;~Ts*4{;-EYoG7NTEWkO9cxU8EtOxHfE8WZnO*JOW@?2N*X9>sZMwmqjU zIAR?)v0`sl5AOB4rAAkC`8d0G<6&-a@*aqZqn!TWe-b2E;~))Fb6&Gvm2B?dE!yFL zok0h*K0_gs=NSMNH@q|3OK}r*^U^$%Xe&m#f8-0EK@mTSj+$Q9F}nFjRy?_D@xY*K z($ut-WAjMyKnB2W8~KMOAJXm=;_NYe8w`L%R#*2RHzP^%CsYK`Ulc+AGl77@Ilt`I&#Fe; zg(dfekWD7)r3yD(V)7}nuihBM=Mkc zjw%*}`z5-za*l8hO9R7>_V;D(`w7=Uq$W?}C^NpEE^fRcQk|Oj07MO!D^Dw!x`Sg{ ztMbok`xgxLe?+SlLr$@KJbEYYFd*QD^8Q>uRv>ZSC+Ynzn~>DI)}ZzGI|0AUTL>&e zXDPdL>Dv8zV5}SLXnAykx_Rs1E1$C534p4wlpe>s?1>Tzv^H|M3BTEXv5B>V0TA>{ z(fvgw7e12jDD;1T>5fH*50N8~wOO7l@>T~Rui2Se{2jU(zI>P0WRcYww;!IX|g z^C_hA5Dr;b$@nq;Av@{Y=ZSbt>oRQUSTEt z-+!H>7jHz=+fZO&wIE{+exSNG#y>7u9#R4ck$D}BXsuqxI_wHc8GTGU8YnrM$j)VP z9xCZR-jk6i5$DIi8s+TC<%qw4bA?j&N6PP(B|@Gh!Xtc5TS8H!a5H>AE5G)yEhE1R z=c>-+uFsD>WiaI(J+O@om1{RJqzqUSG&?i1NM3sw18|^M;B^<|a+RVg{Kph?1gMCrvDUI%YzKw5nUhMK({c zE+Yf~R~rjEzo0lP7ZJbo!9L|hG}efX6%Bq`G zLglI+SH)UVzwhLXwv|H+!qBw4H7CgG7pX^)N9iJ+3yTKgQZ3n@lvKhaX1dSNutZ5C z!_x~AXlc3$^M?s0mzC9>(u_ogiAlDlpM$Rh<8N!iJ~rDs1K{G)iA;m~3+9GLId7&> zTM;#30bXn$L_6JN0&iOtZyI8tbTdJNKxXu5Ep(=QBtVMsYmuc$^j*(3Fb^5I3T9OX zo_rsLj)uw!;~kW5JtxW({Cj7T(_8x<3b*Tn5#`es%1s8URMhimO^XyJM%Dyu&UfH^ z_Cp}y=$)w_fM3=EGEGeFdigF(4vNhh-%(|n{2fDhc`wAtx@H2?4?*NFOy!LuCYKu> z!AX0ek|FH*8oWQ3Gv8pek?Fd+Uxm64c|+wIM8u%OT7A3rT-!=^U8Ch{+n?mMfTX%C zjiez=S}n~=Q^Rlf@zGv4Il)hw{y`P{h6549<jA$fZtrYvZ!4 z;qC3Eo3Uh*x;Gis586{VES)&Ny9hrasbu3pCD&Tycbq~bZywZ`xM4)dZ zl#u{t17WGorgsOohA)$+34K0Bhr~1+qwTTxxJUOds)MyT@F}Yfc3<&kjOGZ-3mLd8 zvzu6N7wSgVl$?58W*s6Pye%^E*xP<4gIO^SF6E9fAIQD&i`TV4?}Pt;R#y*Bfj)8D58H27+<5oy*EdN59nVjJYsKup#J^w>kE7`!_x$3nUwQ!(~}R8`HZw4 z#CM(w%6oc5@)+XIXL2eCJ0`rSf<7;bX@*#haLV6%Gy!ktnx;h|UhQ`JamLd(LiEGb z891%qz4`Uzt=hb{!@Dx5+SGV@Xhr#$0QS{jE>Mr#r8^`H|B36HQ?Iy?FRInHDFI-J zB;Finq9eY;pMWssZD$C)F?ooK4Tl0KXi=4(3qFf>=$+MII*UfhihyJG2XT)Mw^OP{ zcw6>8*!r42zLjEfRv(!!3jXe$rY({mIJ&}lPF^2zzv`X#nUG_Jh)30Ohf`IQwz@~~ z@r+A-O`wk2|0KXUiV>TVtlpTt?=b(7aL^UQ1wFO3M`{Gk>{7lh30+Epk@XkylNQ># z?Xp0&XIU7hOI&%+Y1g0TR>Y2U?$s=7NDsRz){P zxTrzr%6gYY|28WV*~NDYYapQ2cJo^3S1yiCx;3Rv?qXCM*TPp@Q-<5a^v(`}1?PS_0ROmQJ$e00{W)4ezG-7Gg8 zVhk-#Ga^`VXwhHCr7Wk4*T_?`EJI^(*V?f;#6Y$f*-&<1xy>gU1QH#PoaN!a#;-K zpz~inUTOKqi7B9Yr*pDpv~cXAeH+WdPx|o4;NOd{BL_O|TRD>P>x`&H`;Lw2TK0$k z^2bB(VSBCyqxX+4sXm%Pn;Pan+T3;vAKAVDzt4rmU*+Mb7C7Nke^EYDz47XYZHC>| zx25`O!wHT zD|XRB1ZoBzu4DoPiPKPm5nSI51mf()I>a_mH>;)qNx(J{S(p_vTc>a6gMCb_Kcw}p zwZhL@^1-4;N!aBQ$>OwwBrFrzZ12HI-utd&oM}aO*@ipN_M7o2m_itmOW43D=tZnj zNW|HOlhO9$@hoJ%`6y&kLyEXTSZKi|CyM6Ki^|z0h9cI>Wj2A6_<*Zv1yfiimZM2% zhkV@?X=Xc!z|>Gk+C=MWa$Zn}j0OOkk8Hg5FjfT}1&gCdKj?vUTROdF3u#^S<7d8;I47^)BCUFWJEqDovrW z^FX#qLK-=T2Gx-SRNkc6ognRy?ANF4Kypr3iD5^>+HQ>WlF4otLOdcWLU5F5WhMgil*fD@5-h0^-@E7Z+ z7JtT|zqYmdj1A?##;D-wtGMfUb%4z^e_&?mlSFM0m|}ea#zxAcHSgUi1nTt){O!)0 zlJUk$r}RfPhTj>3z>b!jClb92&f zw@mh|)zwj~JDg}E!zPy{MM29^*p$YnZ7Fa$ho3R0AU`8Br%?W3uT9h~zTDQm%WAp| ze-5HqYOKdOd>Npoh<$?h1Dfvv(YCovdhB<=z;D(CAdbeM&XP+)j#hmHhWK8m`>-d! z+em3q1Y|}fi-+l`#_W)9YKMD`RYKa_cqhc7w#~^^8&U)mtVdN3=&N~X&TjKYq&sVV z_@?TQS%KjX4WE=9jEau@Lrn3F8cdpz!DvJt3mXV5Y3}7-xAGMsj_HXW+G9y_L-;jL zcuOH4VDCaH$u7M`T|ONyf+jIry$5v#{m&w5c`?2CTRqJZgN9+4jaL7e#ss*>3bMac zyYQ0cVw|0L$-W+HX)qAGGoRG95({pyy6zOd`>%R;;-`nJV^#sSgl$gAfpUMfP1`a0 zG?3^Wzy~r&?9T!$645e|X!BmIAfOot?|QmKfvL~sV&L^F-%r&o?jYQ9(GlrV0W)W* zYulwY=NQ)>xxF;}{+AncYEp#S(JS_G4o8fnanE95h!6kB76QWUEF?s(dBmez}!q$YW5ZZ@c89Dz6u-gKVI?Ivw4c@#Qy^2TA@a6?{Zz z18ujqO=Sk%pQ#>hk|!VsvVJT6cKQt`!^0;jx1HpwMpYl^Xc|CKILB4`3-y6Qb4rTt z7n;^54mZ;+DAsvrjp3SqKHjM=$f5(5In5P}ImH!agKbcEBi&SE(CbDp8FZszAEBo5 zl>-q@bVWlReI}Ks+moR?EQ%#&9ZLAQ4BVzHp~Ce2r8Yo|D_qA)lzgLZ1tOllrG*c* zlYhol2O3G${s8ub8nfkZmI9N}`5(-<%z2~RHTFXjl#v2PZ%?Ei=W8s=9L{QRpSho_ zVz+KipKyweQfcjrb+x?p25A+H$FNJC^`Lw;I(hd`fDA{-Y+rhUDtXF{wNf+#P)hP$ za?tpO8x@#-gZ!@E{Z~lP?7)9N)JjPz{`Z{cHG&UkRh+#YRl$M3Vp|$j3O0Rbba9D$ ziQDX6u5a$6!QohsthlggmjmTyAad{SXd7nj;L`!UJ;A(CMWMi=&WnC+v&Jsx{j?Ho z32^P2OOJo(gI^`Yc#d2*20qmpIFW4g;x}aQfXR$zO@iq$udNZ#lhBG5?Ec_u^yVQ6 zU89dKLARbrF;j)g=o|xr-;7hN>miKpjN0fxZ%=COP?2Z(eP1-1&?6S8b}Ub3y*e@d z5kwQP!P#<7%g;UM@uL@q18{3UGIL3d-<4~SWMMh~g#Uv#_@r`@i*w@{2u+DU*YB*C z%d54zej3pW;CcqgbYZ98(`BRqs{U?aV}_e?et7AKd?wk=M0U3P@?_34#N?U@%(d>y zdL+#GkX!Zgef%>M*k!#X_Qo_7$k@ByY)g1J={PYzW@{u8-<70`Glfm9VV=mXn# zSB4iKR&j)qA?@P*ozoo!XwA?Pi03#6keQ`fOTR`#Xv2bJ+J=&dNsF8yyXVmwFZASL zfX?X&-Z|AdMce0P=os%cJ76nk#079bvi3$6;yM!P!VSfy4A67TNjR4>YP3W!K^wJF-f3D>KCR7&6Zr zQXh320L`O9?Gr-n*#a0p2khQz2ZRjFS({g_?JpVtiz~10Hm42iwLPBVSsT+_5z7a6 zML?*8T~-M~7!MQHxL?DWocGFZ>}j)ICsMB!xIJ9aC!PiCvaVY{{A$tFm%}3JeqCNs zh6?h;v|nQk2-~&-Sw!9}c|!Zr|GKk|x@rl9R9J!XaXKem;l zu#$5dDcRBrwNd`;3DFc`+oK0x3+PG2k}9@?Wm`q+12|iOOMD@i!7ko&3|z)x7zoap zeQAAnE#}L5PT76UqvNNPn6E0z@F{HeO@+ZA-3OwdXuxY8wp?4wgn{_Ghssg(*aY$CK#3-cdcdBw?r4>DRSwdMv(D=8^U#w;)Xm$7c2Nt|F zJUsJ$w9r&QmJ=j($cU&fnEDyBGB8Zd?kX-(_;0gcc}MLLIq8esg-zSYFRU?8cH?WH?<;J!(dmGtmwa_k+I7 z7IB=Snj|Ab7l74N$3yuLt0K3L)eDg?V{t&%i;RBD)@$%XTaB$V`nXge&;bCj{ePFo zj_3mYG1gmMl+&gnms_%ISs;-qDl&|H)LF{e1O-6%?}7toLwm3iJN|c<4`cfa%s`+1 zy&Ws zKV+)A-IzS)+OX6^dW=}_g;7~&^3>=axX<(P1>r%)xxi~CPwyi)Nk}|mzOCA z?&x>;jfUZ$Q|D0$w?l@T^d#9G}r*uLX2347gdtMY^rC*M|t~vn&SDvaAZYjTYAq1-CnWfxORNRSK;SN-oH;xew9pxO5@p%3hmxBH#<+bRYRQj6}eY5xKocll?`h z8$ijLL)!RheLb{$hS*#1a|!2_tu4PQ<;gu0U_62ug*KT{u4bUkkbQL~(X%q+7;cX{ zM^^-&mdtEN+f-JH<<@LYTi#&xlXAk%W5S5z=?Rm3YJn+p5FC}$J`G5!F&+Q&yS9A1 z6CNUDowt=XuZo)F08LSSd@(+>8R28?<|2V4vz<@)43~AY-3cIWqukcvEUaEKv9Jzg z@++^3Hchd`nMuth1O#M=9Unh^^LQ$Pw^LA=xIYnTb}wL**q^$!q@SW-umR zl#a80oOx2#BcuxZsMf>Zr@Fa*(#O=7_O9sKU{Q5mFxllH@;P}lRE0rPqbgj#-+a_Z ziy0?+@9@1{JKinAWY^kb$7v|YU}Ryvx`eO6l{t#^)3|A=dKY*&8JbR>R&TYU*2K>@ zWl+%YnseA@(GhPN{tx79*%8HxyN81=s^CmcEtZ-TL1iAC>rhBy7Lj{Q=4pAJPh(~r z4i0i-Dh#c$(AnnxEJ1{^{5H(fy~IYxv){+eY8>j0WGoNp%L$FwrlEs*^06iu9ZrVdvWB2VP`vG3j&5EB zdaQSS5I}%p%H78~e^%6I%)g4C8kqmiOGH44Rb;hUm1HehMO8YIOM?g%rm!6GDATI1 zInpT%Za^q=PDf6tOFB7WDgS`^A7&I2-ghKSP9MCw$g>@(+nyE2)na^N$bF;53ShX$KLCo zg==SotJy7u!)hk<8f0yJpY0Uf0t5Fu^7pXB$TJ{0fH3FEH9kCKJ@TOIZQZtz$ z@sNJ+Qsu^kdGw+OqZ^k`BVZCrk0SudE)7R{O3WNcu<6FlsAemUf&_~UqM3f+H&Eo~ z7yok;oi^IN_{Ek%^I~zR-!~9s6-Bj>u7#5F%BBw9emLlS#%x~tb;6>?!(j$b3L8n* zzHxpsq0=U;?4mL5KQL#cLb9TJcmNjXe3Y(~+S#Ff>YnKN(kfTyQ~7lkN(Ut><<9KW zcJpEZ6D*aZS%5<8{fEnD{vq}Kz}`oXxxZnA9{Hf6xR&jp#F0u8@6^| zL0(R>Kmo2|5=m898Vc{NlCyfsjbTsq*ENA>$amFUt}rwOctah}!->C7;XiY?YMjBI zc)X?oE^;o(3XCmWp@Ga<_0V_fC83{YEmnIeFqG#2frBWx4mycr0Vec+wiYFa-zQ<6 zD8kIBma<&5sRd|ElV;SSOmO;Lw^!u7FT;Lf0sl@rB)bXA7Jm-jXDLgqI1J8Hq}Uxz zR(mC`KDT+|5vyT18f!*U%H`)nUS&_?_kWb%)OyOPrk#ZfkIbJMr|e6eQMv@Qo1rrO z;dlOv*7nH=%e@wbthdizJ#MA$2$BC|!L}JzvC*N~x*=^~&BbB9J5bfto{mtTx7!r8 zhB%3w0Q+j>zXVmt3ZxIjFEKUiI|x`n0!o}f1ZHc=dd+EJ{M+YAGmoLbL8BfQr-@f| zS>sTU?L#SP#0@Hvg;P;+SU{y|PO=Q2^(nbaQ}klQ$0{!E{vyYeRxrI{x1CKLpU{5f zR1V*2xV1%kY|XWAeOz{K1x#LjDXs`XM>mSF_^w8SVJQAQTfs=)RnXY3vO4A7;}J<> zKt4pIniy%RK`|UYrm+^XKG^)G{U(~q%33$keQ|$dOa%7G;AgWaX;p!0xT~{WSMkBG zQExJd?mqHUmI!F8gG?1m^A_Ie!t&HLMm9_8;QQ+54!=|4vIw zL&2GCXyarc#k&h7ZsJhS=5FlS#D)8pWT1y$Og1jz>T1MZI>0T;ldcwryD&en3S2?l z&+_p1P59L^G5xK{jf(cU3OajX>(Qe(waMK__P7#pa+V+J^k62>y`qGH6JTjDu~yy8 z=QQH2E@n1BQXrBaYo*9UZwILSBc;w?{)Bf8$6OZH7oS0a*l(9I-<}+NQE`KAr=Mi6 z>>xRWfvjAzKKh>Ur8;@?Zpksi02QiKI5Kp-CYH!0H)QM&NvN|NRquQle-SOgLv!}U zQ<)=8O)U(f62hgR@SMbqkyPy)h`K2=o{mOin6^bAk$Y|Ehm`0rN4RId*=9wZ`iS2V zsCuB$C=;a@=B#KP`xXeSLN+Dq&bUaMX`Z4VETg{>>onJwx=FGZ6D|#y00E0xtBfj` z=e_##0ldAkO|{)L@LZ-1v41c-LN4H&qoiw=TZ^|1JBCEnuZ1RQp1(LOHZ@M+u?ZOd zvC7MBZ1)*Pd4UB;`*|iXIjSu2%9s===BiVvS8WLJMuQo_*~Q2QeyPMxd!fNo*R9td zVm;~4ay?6A_i3e=<-yPUz;{bV$-t!5F>{}WU8+V)_J*vyfIzBJb^L!P4W;q2u&0+j zlkkGx)oMT6a;BYF%7yK_=pt$)WY+WM3emXwrT&evir{78Y{D7E#qT4AXoR9b;~NpgwA$k}-@*iv13zP6Nyr+ZA0z0A zp8~{;F54o28o`bY8~)Ovx9eA~|0vHtJTEWAh?m-1Um+E*?>^MtQH!3q7HFZw%#al3ZZUY(pvDqpv02sh{7{@}61zow6J!z#Qz+9}{1UR^hjb zvjZ9*=PrY;;0S+N5G3~C17M$HMCE>W4U6~j98+ZN%e8PLEDA5fBzp!79De_$4A-?1 zWeCCB-`U5r86f(v*-&w{~9?kjc({HF068uMNKW z!zwT&8RaTMM-U%di=zGzb#c{7x(H;oOImQSTWOx!lLNidz(tTpW@Nz{j=P;$s`H`r zO!DBHj7JOL0m7a*dyJg7DX-gpuhKVwd+>B_384uA1jMKm1dNG$Oi{f;RlPY~D?LIn zMCTU6u{Xf>w8Q>k5D9KEJoSV?u+M6{U0Doeocd_)2uQLD%5R#Ba>X#HVC`VwupgJTx8tyE2Ra(Ne zhO3zeT$XtnmF+RWYg~~fRWPF!?ntuPZz0$4kz^MBDg)a=YLABr#Wn8r&9r=JzD3%n z-KdyPq&t~Wn?Jw;1d@)X$1F#+r;q9Cxit$e9BnQ;haO4&N9MYRQ zW`jA=!^=IY}9C#$u~)b`gB}tVoLc!nn%}cyFWu@*r>?Hek%B5d-k_S$wpn(4v&`f0a(X1>uV-;O%UM|}%OFk{$k;}#xf)Da?fJ-)geIPHr{_|U>V z6&3N5or;z2Pqn#=p*prRw6 z-SX?>G20R$UQJZJ@^Su?HGMt-k^1Bfos{NrWz)qaL`WR2$zrTiX#%dZfk&?XbQWVW z#2l$h)=1l`BI^U(Fd%kPyPS2X^>~z%flF@~6L^EtxT#UMIyF|J#c@qKNqAISe1{6R zhNFI!DrUe?RMwXbxL${m>TU>$Fx1_ifDVPnz+Mqs)wa)FHW3>YTNwG_6CNSIdI?G^(`gc0a z&Zk`VCr8=&3urLm$Ik#%{jK`0lRV#!I3vfvqpbEZ@_orQp?(mJjK* z+Y$xt7INh@Gbtq+vEAd?i%a1|zwGP{weISw;JmVfVJ*uW|24gW&?+xXyzVEwdhZWf za3Kim!%64EpeqO*R(AppRyf*ztq#49JBh~<9GJm8H z6mUTDX4$EaY=*M^8T&tfxgQUVEsH=bzPdnEf7lTTbPH6Y41;FA@{OvK=w0dS_GdA) zR0;I|Z5eRVmfnzpALKX@JX&`L9%cDWIsV^VPJuLzvW)q}K@DcvwrE=<*>O7-*3yD9 zvm8=&*E0Z9Ii>F6%T6=)7lrXh?((Z z2LRt#Dv9!Qx&(WRW2>^2k1(+rfYQZRiR}*C#O)MFcPk z;}H%=-thep=1^$F{N&t-Z}AB6X?iu8dh-6s-@b@S{DTeten3II`7`gFHrY3CScqJn z*fv2%-Ddpw94y+&U(_sD1N!6sQaV^mNE&%9bg(&*e9dK!&?gH5Le>mdu#^=;4BIBf zNN(3WCN;Ptl$i{%ct8a>>_%l0yX`;htw2x1=eCu!&T&5_GmPjb)C5isUOVmK2<}d* zB8M%bCi6FPjMSI6QWvDLN3ZrustICufU&ibR$I`ssFIx=>y;MY47g3YQEzhvYu-xR zk)r6+hynGcnyeLZgx`=9R`iU*@zIKux~rL*1a(Z#dNa`igTB!+&FJKzzHod%B8`i@ z{TD5-)US#P=1cc0LI4930J;320I38J0x#6oq%Z{O5hN^_(gLmaZ&dv4AMw9pe+`X$h8O9H zH~(nZo;cTv9WFY?z*l~OLBtzwDc91+vj5O&z;U8dS@ru}{K1dYP<_I#?RYs{cLJbS z@{#Y=PlX7+Q0;cgw>z_Wl59oly!}^Xm*GaXBATTebnV-5x!|5ZG1!NJca@DVd0E7zK7{{n1yR13}A1Jg(*o5iH`&14sbnNMk1`FLkCvP@wwDw#PjFA*@!+UFjo?LUyS9>e4S9Z>(rjgggnTo?q@Of z$(DZYBB=rGoGSqU_rOlh4+)nBC;+NCLxgK{D22XMykQR&P#>@}<=6eVeU{OW(R0{o zdkP0@hTuv;dx$dw>P74{ox79@13njeYW1$Wz)I};+qXmCV*XAw__r(s{19LnYl+&+ z88b42!#)`D-Of>2jH`=8{%uIf6>+lSAI`vPM=kNM(nu=UXbRU36%P!k zWef@AU^WL)ypzpuVnO>$SJ93Bw^6_!(9x~-6+XKKIs8xtDmu@Jpah)xx4?Zzvc=C+ z(UZ%6DNq^(&)2|2fumJ}m-+3xMX^>p&{WZ+exv=2-0DJ$2Gjo&7)nFj(5$JU28#xB zH9q;*huyj3?d2^Z0gUU01OeTvA!9}!{^`Ensi3IO5?vFW6a5KyR)03NhfBObW7Axw zx#NdP#)XVFP=J{5F?97nFoG;Be(7kENL%e{&xFmJG9df_>q&C$A?$2Sgeb--uZZ8l zCqmlbrdu6RYYh-)$GiRnO(}5UZD)gwNzBuG$v;bVPU;2Ib}C=(pTAh5!i75?*iy)~ zD4z`(IPMtHF3vB;MNNLdb>J=i<9DNn)SXSu>DRJ}NMvq#^UQo7_vSqA;y>t##FNqdv5=(2u7}cassHKfE1=?dvacby z1^2<-Jvbz|y9al7w+RGy7~I|63GVLh7A!b{;N+WM_W$i}w$JHvdb(~`SM|J-?t1Uu z0M|1E(e?C&4tcfJYHk(3YdLGT!Ek3nzEKgJ6vma(&z4`-CVnfJ8NL5WZRsBS`~E1~ zBo>-&!k{qB$nB{S?{KdISby3ST3r%UJzeYA+ccX(=@MeDl_n9(Cs>OAG>Px+E{*4{ z(AZ#U0V*Q=qIMt{2(=V8m(H3$DbLB5x44i#D+UR`J%}P~Sq;;LQIAuk3 zzd$At)FQ=!616p8=U&rQZY^*yuTD6-iN-loE}jPc4!sxbo#8HlZ8he!@xm+})lrVp zE1}bCsB1E5MunO%nR>w`^vSh>*!O&AuNi3jLCV+>f@|oNK}=eSFAe&tT0@9gG>-fO zHG2w{%Z^lM+qq7frDNpw3F7{TG=EQ{Vqy4M@W4@1Pv{`DbQ;jfGyu4%b>e}kj`h3q z!+GJ!m!cWt$iC7lyZNL*q=-cwWqK>63h{wpGW_`f*WxKT9W?HZd+d01W#s|vg=RHJ z!HZ#Fzl*>d0&$G@?;R0Mc@J6pa9lCGQgwSyVd} zr3id(4jmtAhSy4;XaX=4@{pS{^%Js8D@_oRVK6vcAPG~f97`0>yts;N!H5vJ#jx9j zhO-wyN{pO`fErv2h5B=ff=nP{IQ9CPG}~|tROE@M`12|%$inNHcgw#Ei)BX|fu;Uf z7DGSzD&mXnd%ka0w|vR=l$0GZiA^Bd+f_Tf^u;9b+du=14h66{$x>d+TA-|HD8bt% zg_kv#;5y9SlDjmyN}CIGz=BKrixEB(2&=3Z(kg3$IRGH94u7Ehgh5wNv^Yz7Ms777 ztWOQ!`2F4uL%5x_~AIV8uc0jo1KXvggPK}*^b81g8# zbCns3^3wTKu#qEiM;jx~*cN!0& zMtX(GC>8?x?M0Z<&i|sw-Wb=`Zr_Lw^<02)--s6#_xhX_iVLe*pTlgh(Z)?7c8oC_ z=Y3YxdtEx9303vZqJF?5Cggb|qZ{G2P)C#EP>~i{0eThtTCG}fSEJmJvo3i7oN~a> z{Bz<|iTY=g^c?8%m-9o)bA~OL>V?HMH<^tE+d)>MLFE?92y5CCKFhq%F8S}5?AIy> zM;cyvdM-Md_fY^?TB}yHYdyIQOcce)70v9HX`q=N1`p1-@tB)JBQP-+QG|_l{zH&+I}yq3l9^ zpUScEN%k@EC3*3P1j`BwR0qu)*T-tjtvHr|$T=$KqYqJR4*&|)+WRrdA=-YSkbR6H zm_%RTAmu(%CR*!vVg34d=Ny$6bRs{UXVNX)_SxL(}`Sw0BzNL&oWL%Zz$k5+2UW zQFdle{_iPENaz_vkGZ6Bi++Be0`weR>S=HOcJ)ivmuU89Sua$&eMDWjc?vnBv38@@ zIC(ZxnF#cz4rM5TSMwmPDJS_>A*hmhYLLdOak#6#?-LDLcr$w$Wu&5$4s&X7by)`XEz z!l)*_BoQr~VD}?fW{Qc#gh!Y;s9{jh#=L;YLz2IpY zrdS=p=J)lx#`n2~qsBTlp9b#TDQ1s(!*uF#0pf$iYIO%rYKUjOnV4Fw*+xRo$7(c( z>}Y$KdwhuEzCQ?(1;x8|PxbWk(BiW>x|-b6)98*rKeJ+H7#D-`5X3(j#P1A$rD0JC zV%mQrG--!(1L@vc(1h}Y zKkmtn4Li`5Z|^Z*6NL=kuh9ADdOXLju=o#2Un6zlcJI+#XGChk<+T7o^;fVFE@bY8 z==KBhL8sDvwab&M6p7Eo(X~IF&U=2g;*86xsm9&Mdxp~v@V z6CbRIR@!7R1lf><;NbEW8`F23TS~y=&iXoi`R%nDV-vQok*9G55mnQH+Cg7k~C;D)R7NjA3tIahT_qrPZ$Q)FjuB= zZ8B})MgtC zAC|}1CK4np+ch+reJ)ZjS^b``-xP%ZAfNq(Q8f`;R%=TzBv@DY%L|{V?P8g#(AP0X zyOK7mF-_n3V$g1R!{E6aGgDD$DBER~_(_aFi|#T;G=!JIhrV*0c8l3rkjyCO4+OIU-yHWKaD=FKxx=V51vOW@PWl)%eec zCbCRp#|Rnti>OdvM@rW_Ac-uH84V|T$S?3(gT`T#S%JiGb%)RE6d@=2IuoqxK2Z47 z-&(#J{lMx+AjqoS=qH+c4RBa5MpT`;>tOx19g$T`ClF%GZDD!>l^mo~WWdNK+_&oJL;@C5AYd6Lt|#K;{zXl_=1{*0UvQfnjlC%gH*-o3JB`IDX6*TZi@y zOQcD}i3C{Zz?2Q!w9sw`s2ZGggv9lGZznzCC2>s$Rfr^vwmJOK$(v;D-lSDgh?x+t z+3^IegfgoTepHi{IPfwAff_s2If^9I-+r8JHIVu2`b7ZhgLtRp?E z@edk}Uibx;%D{?3#Tm@B0pY)Aft^)#*R|lcjxkv9ERYGL8;kcZJNCcOSgK;MC!fIe z?PW8y?J<;z!!`I4My9}-rX9F_(agv2S=OF^HfN9JDw2c~oh#v=mF^)I+1~g`tZ)PG z3*Mu)X&3M5w)1I&Z6E}#F;@uB5a)3#ndUx$AecBPZa!|fFoF(+9+BRhb3jSWNp+AZ z#BX$Gn=aYzeRLKoG1m&nR%v$M(Yp$SRABeqVHAYVMuO@3Y8?Xpifv1Ii04p+d<1gh8G(aFD{Iju9JV&I=#SADo3*7e zX522F)dS{(JCt$u0UZ#3%%|4i9(s<>v|~LG(_1spI*w)o3b|jY`8J~aFqpc9Ah@1E zuA+|fI+JWdToh*`sW5qEso0JPV;~m0eJzr?b#LG7*L;5SYwD#Z&G+a4jpp@y^U92y z0Tn&yc@;IuchW=_Y&8WW7OopZwGl8xkyg*ARnOZ9R(~W91 zwaPbvCaHY*N4@dNsCNpQ&Knk~!e*cGsed1Ms#mgpF%s}x<7n98l$1ZJU8eZ7uh_YN z_wk>OTP;k(ORwhq}hJv2XNj?LO}A8XruV`+IPv}l+ua`&!wWmUk> zi!D~jnMZ>?2!k3e1b9aT<;?(4lq{y>HOa+*skZDX+fd_mHSh=2_h@h5FJaD$1Y|`` z3Z!b0Luixp0q0~s=wQ|l3bD)-)-w*41Qz9Z-8ZB}R7qd-k-9PWE3p_)^42JZM2pr4 z^c)LZKjQM#=O|X?yLhv0G_TMFp#`RsQDk*uzt{mGMh{H~D_ zlHbG)?)j?JgNBI9q%chv82G*-nYyS+WWEU>C7}`75-(#t;*Oc5qp{UDP_j$nj2=7u zZPOf9d(XWfq6D%3Z^}!v${+A*#(Y0HjUh&{e?*IbpR!D5G-Mau8)mmRmtIb2rg%dJ z!W+E2lfdY=_#Lg>O)#xTm}#pb*SzI2?KR~x1^#~g>$GOt0D^HRIFQpE>o!u7#N2Fx zE{?S(Hw*?pl06xVa!@GT44>iMo7hl!6c;|9==3+BRmK~vs6fpHyA+j04hOQDT!sTT zq0fe&$(eQ`emM_U{~DLB>X~TK_ihfskcp{02R`>vD*!?DEHr!^AJJlQP3H?2KKESG9oN8uM85-f) z#X4vws(!>Rxh|aTKV`TU<9L)c{-ESANGWaUwjYKdy33q z+P!qCkb`ottA&+0>mzSh*itMHI&hfvIe_k01GXET;rjx_6CC>%cI_degrFm+jX~%~ zh1uSVcuKQo+V*E7cLq~3{lXH9sabAW&EJ@XF*Q8KeDKNW-MUisGB%8rsHgflbr6p6 zR_{=+!_LTA0;Wans^(iiol-6`M!)kilV(<`q_jd@2xIhfM)Zt2`yboRM!GpH1u-Nk z08glgC>N!=Z5OZ#!YcM63*kQ~>A>qqUFSJ~Oji_ECdSSRA8=oI{pfFd!OqDTm$vUd zkq?{+Wb%7V3#Ry7YzV(}Lpeq%3bC(T?Hc!sPWDra{%TO0%VaYpXxBuxXShHLNdm`O z7;~N>zJfd`#?;oFVkI?oWqooe^?rV)1|lDf+fbO?eLWRBj@b?I$L?aFa%I?)CJ%F- zi*dsXO24N-za__S+}n+-=sM@9u9yzv#E+=USIR(gGMREaHi{(sT{vxI;?Z@hsw}Kg z$VGr?hS$Zp2K5#ShpuTf!=_VS+g5ALhIL4&SX^#;Sks8Et}odKj|!e8tXNGf1BI;D zA8##3Ud45dSJH(M zMeT@3t7b9Xjk*~D{FrS&W|>GSBkLOPQKt+L0pC>;?p|?fXqlrBuIi9m8$TqfR_?e( zRA|Z8S^uOO_vJtrrgEj;JM+5!!JW#Aqgc@5nUdV8_()PJNSl)P6Wwch7||%)F$=9| zgW~T#NiQT6ss=pHc7PaDgn`Ovf*$MjX4*1L2Kltm5sgS@bombf5eun!=yFQPZ|%0+ zW=`$vXUq~LgEUtxl<&f+g}z)CI$dlA8fHKUyI+94eK{=5FW#PhzS4Z*W%S4=R3!+v zLcvcA;su428R3{=yAY@)2DLH`_!4-Co#|oUt8G|FUF8nNeO}!qX#<`(*O*sJ2J9~q zv%5Y-k6a6?;1ErR6LZMn=-@Lt+|5No<2=h!gTytD?Z>eAFNq!)5NCI|4G{T_2tCZ znpCx+z(Qfg@$pDqzXZtV7i`=v(Y{P!@8>y4m=F^IhkP;;TcfT7fv2xudiRqq`M@(# zu|Xs;IMrmtfHF2O@^dm#$m7imW059e3P5UotH}+>U3+#Ev(o<&sV_=u)(HmeQ-6zC zS6Tq4ZLCi}Tk0zCt~09lcV3DIQHwPkU$&w2FnkEUmh`Y2as|+HOz9hzTgaL4OH&om zdc;&~Ou^6;2}S2a;K~;q34yRa4~53bnQim7!TZ}QuL2_<_55|_+`+}Ov|>JQV)Upv z#ne0!t-SC#9_jILy7{z(E`4toKU2Geq}+yK9+AFOzK?UMLJ;dIWv}c^(wCq&vf^A2 zRh*GDavYFTp&v+E4XJ`@mbxHD7VNqv(I7`_XE^&1V#@3m7Q=$~2I=+_-pa^5A!pVJ z{+uBshUMFo?wPW`i1QCLEx9CtTfCOheON#G#PAk+`?qf10^y$n5+OxK-~$-RwAlwuL&Ub33-AZ${uyH#uD)lm_*}za$VYWOapUi| zor^ggN1?VK4ZlwDZu9cD7<6_1hBrj7;CqUn&l?vN)fZI`jgY)aT{axENkAfyA;7^; zIBm8GB}A5C?jFz8=E_W*-RS`n>lFvC$XhY42+(+H@-44IpAQb&nBU zcR9^-XrGMmaAovG_ZIr9?4~zf!$S_w*jKeE&~5d>ukKRIyuVJqj~Kzj0LO>=rRS!k zW?IdBh9F*C+`^?v)QMb9vQ?f#BDr1gk$%uqxjDgf8>Y1*Sri`lUO<=<;Pco|>QGJ` zF;qdb*E)Zm)W`1=szJf%;d7&oB8wb$CHTygbg=Mv3KMby{KRp)d|u4Zmkbzu%7JeZ zS!V-IIdrzaqR132cP4MwHruW06<2I@AhR2ini=B9a#gb0{`#(k9-!_9HI8yNBUJY- z#W){ML?e@vH3AtZ`sA^NT=Bb2R5M|)-x#mWUI$sV^Ebj8=CE_d?x$O4%hikfh~9UC zf?2)=x&;_h5Z?c(Kg&}Hd?OnqFvgPI)pBKw9Q zw+p$+FrdE2MPf{y(9kOF9QE=_Yna-IkRWxh%ndVPbjJU82iyinSqlfY+IH}oEc>7J z*q%9brXA<^U;X zQ397GaQHtsuZ$L0?;~XEfY%bxoLT%%hHK!{y!V3)-TU$+wa%aB_&f0K1@AuHLj6i? z-~9+pBl5VG%`^Eh5epw|l1Hw1XN=y^@SrcF==Kg>oS3BfTOL6h1yxAE(ffDX=Rx~* zaiV%;lNAa@EeKOuz9&AmWe>+DXQ2tT`j^1$NX0&sAV6EE)}!-+eI~|@zbk$Mx^eS*1mj4SdYwT*I?X)y=TR_-jOrd86$^o=mToLf2uP)M!&+auREy! z9!=W*3^l~9fxPlEX+^_JTdOYzrB(fon&T54Bz=S2r?pHo%VTripG1etgse6Q6A}ao zeUxiNl>vB;ME4jw&nrP)0MzOgF3b$s?Tm@$;#6hC6U&iM&;TxDwDf0=(J8E)Gwd8j z;=F`HL4~~*r;Gz?c3+kL2rCx}6c26tx**VRq`%D}3c@NG#GfM%lvl<4gyz>UN^R8e z5gzRt30*mX3JpG&JdC`kNF7Tj*m>sz<>%8M<|%8oiQPNEYf^PzHLoi;&zrsF-bh@Q zCtOCK51+|IN-2puT&Hhuedd3DYw-I0e9jNywV{F5-x~N{&UJ>KW}=W)b&s^hkbpy3 zH+qlYtM0GX5=pjUG$3$bv;Lww9OCO!PqUlH>fS-UzIm#alP-ltYlr1YTcFuta+XT? z`XKiFQblV#--Cc?SUaq!^8r4v*9E5~dL#WPz4?>mbst%>(F1M6v})x(jg)C`qr5}> zCq)tqTZI|@>om8Jt9sSKy>H@U)6CLln9ptJ z9gQURUd1NAsVtDFGNFr)Jn+AH%Z;*~}z8o5_2fxQ494W0ELsRBW1`9L)SUVG`dMW`|Tjjy+JUc9`)O!%kB)+-GamZy@I;kh};%KvB)E@U3wY%Qo zX*CyX^+89f8QzdtFw@pt*OYP6H9v*XgvEG1=!I2$FEy9|=XYuYkG#4*d2A3~hPEy$ z1MnY-_Wx37AE6R$Ey93+P^JgdC4vI;8Gv4fiHl9YvnFojyL@c}APN}KF-Bz|$V9ZD z#9*W$z8Y!`kUdJlhNSUupzp=V>RIP46~WlpSaB^{pGrU!tk73aFLf<ejQ-GtaF@^Dx zxf9k27@|v$-<{2ty)i$VeI?M_1yLVlMHky9OcvY5Gx*i3TlM{H9ZURT`%}6>$ChmX z=c8d2eCYXcy%H%-gGpdr#rl4)R4W@XO33FFarY*;`~Xi%>k;?f?`8q1o#cRq{yIYe z&*;q@Db~4Ng(Dm6sBVD{4gheK$Q@@HI-iYy4G(p|PIRqxzsN!x2~olpyno7H%y=2yEDj7eF$?b%AuO;KwN?o+~IbzUfTEntVL5!y7NkR-6-duMN zQfrH|cGwWkKzWQOs~UFG z=W{`In|Dsu*x4+2=uU}xYiWsW)LCu$$EFO#(hA}OWYz7F0t!)k^XjZygi>5L{Tu+k3RUdu z4-exxA_pXun?yzBWv35Gca0VCJADcAQ0GkNET0;h3PsMe;lUwBHeYKZKOY=0VRu!5OFN2OHXPv&>Y;P#WH zOK(XTJ7|8C9znoi!<{phTw~%xFO0C3IzT6R+IQ!T<8ki#f&^+BDB25g?|qyN=+R{`0&pr&?x{4CMkDCN&5H}0IhFPz#ry(f(Lbs@tf<2507yEo>$-bJYU1Z?1 zuvI-x$;-hm->u%8s6dv<}xk5 zF<&i$6jB`!AGMl8o>od9^Ad4I@>#C#A?{sxtDYb@6n{nLLt1^YH8*YNsdt{nf9V_T zvw(MaLqaA?nBt_Iat?)Tqrfn|VxpA>wK8HLu1@md)r0?<@VTY}8f$Fhq(B?nZ4AY|m#vQ9#OVsG zK|qB2dH~6uDjp>v`%8+3JKjd!B`eo^W}VI^ue7y>`#Y?xePV`ZgO+o{S<`o%wj|d1 z-|$Rr^7bX|3=B+Svv7x64w^@_hR z6=iUSfijp_NF^!SH{xxY=zgOK&)@xxi}f|ft+5RTgqFm@+s2sY$5WxKER!qme&o+e zEhNAt&h5h%OR+BG*GpP1f4wo2?IMR`EJx5eY(*Yf~<*L`+%y&3Umo`)7X9%^SW1F6fl=g@#VL&c&FOW zf1WG`%`O;9+wr?%Bw_d<99h-6VD@7PSks6Csvv)+uX}Hs@iE4E2XvE2wVgzR{FohV zFfyMkFdr)jvzIJTK|ZAiV0do0wr`Z;tXkRiCGd=R*}M9s_bvzI$#_Lv65!eURG>72c#e?1yuPCThNBEJ@d#8IxCY&Auiw_t4z_CPxXL%a6;&=IXdXak@mVs~g&45Y z&gNcTdwq@IS)cc%z0Q8!Wlp_Y&EXdf*lQ4YIzmQKfb)gIvlhDljX_%a!03>kw*&M$ z)C{_5n75Gbd-xg}1N>Ee)p`+`9mT1i18I!1_%;AXmcfDauoBxMks+L*($z5j zeDQ#bcQ_?6yNdB=P7$rz-COI~rDOFOK%|rX0m=Rf_lI2P=eLhcT9?qOLL?bR`fE_@ zmPi8xXM#?zc|pM`{FT5cI=V@X{utl}_VGq>_VldhO{a!ZLJ{*usSuj(COZ7!6FQ}0 zAT$Fk&v6a+_RjhGM)4FI_fj2Ga@b;E0=Blzsu6wWyxdCaU{Lp=U&fv2=+%Jv*yz*O z(I_&3)@MHt*1%mHpIC3NcB?r=w@ilm40G$NSu0wt^m9w0(=D&?bz1i_?--yr6rX4~ zp|)90(fa~C1Azo@tQ1pqKhFvx3&Hu4u9F23vd_DT+@_(Q_wD%PPQG+VwUS{d5bsFI zr*Rf2oE%xJ&p7)xuZ-iYFmFvDXD?Y67Wj!Oor7|+?|x7OMl@J`rEw!Ce-4niS(>-e zx7Q#<^h2y?6_g{0^A3m|lQIAvfkJMUo%xplDAe7>O%)}-HyEi=XA!ot+)E_B@A^gF zK^`E-MsR=IEa?`pBp_)qxb)4qay}Ld{ppr>NtET^_Z7nV2{#O36{CeESzuk1sIK|@ z;Ow+l2V^K-dbs9o$8@2M`Gh%)aCU9kcwZ`}w3WKg5biH5Tk*ToX2lsGd$M<`EyjE=8J5*!7p-Cu>NN*Z2(5JyIE z%uqYMyyZU*Xe9-c$sRh0xBG>=GVVOPe@Mn?#`3=rOArVZuWk(+$nNfijhjjCFrNmL zoWOU4#{Y~u?8rHHRDo~-?hNThjX3Q>zSRSW1;SVww#QrUAypW>kXDasrr zN>l?o(#sT(@J`XFpi5^z2@KrTIG>sSX8rw;d$ff0GPo=f|~91=y1x`Y-kW?uWPr(_da{M z>R)_`Fom>gjUn4tKU1FRCw++1n5pNnCS9|O-Fz?#u}<$jRb4?jg9~42N!`%PP|0iH z1_fe%8c-vJTX&4v0=w_=iVtSCO_CTLj3ujx{L|y~Oe|$6KDD*f!%<*lZI%jr@$F`@Vpi|nwD$J?TEf>ts@Ycg1MP5*{!PUldnq-Id@@LigUZ>v z->z+L(I+1%vMjQ&B17v^i+;qci zpJ=d8{U~Uk1QgzLe?+>7<7j1|2|Xz@?k0R(7>}&kM{hCpXCG z0r4Inv91# zxWN>Np*fa4RA)!!#I_W_l zQm39N)iF{N=Vm_p@mgBRQeJY^@v-~i7$hy$gMU`x6=8!8Jg4h@O5qAWhlRc-fTZRm->%+8sMdqkE1JLZ9;VppWwypgkX!}Pa)WZ)+ zB)tOUZYb!!{+N&+z37a#rE;Gh*%*08>BtZfc$hC9xe zCjJI*gOW}N9(jlii7U(TC1xZ+@!biS_Q2|Gfe=}}2!B`#$xZ2s#7P-@s-&-X66H+qm?E^08fc&3t&@C9;n<3r!9s4YuQvK{JV{zxXx+c`!Vpa){%a)R_U#J%d}V^s|2?K^uIMF{g5oo;!iIY#`Ww{m zGA9SBw@8{QC3`ym=xxhLu;-A_ucB@D^f#buC*%^r#XO$bPKz^Qh&$ZoeY;|5ybJI( zn_Cjs;q%d9Yf4F}uX9I7*WLrUW>b5Q;5C}NCRVG(qUWvKu5_a18?mk3ST}uqW%f`3 zJfQajjz3~tI_Q+?bvY-v>ZQ4punG>7)9%&E-sLCGw+gsL`EqjMKtrHkFznc^td)ux zqWgvpi-$NymCy`!ifoQ37YS-^$TyZi1C!uA|B^Pa$Nhz+`e=>m0rU0VBEN5V zpj>SY!? zO%?ss&hLdO(>F{e!ja7R-|?WV$NftFaWwa02O(*}RqH!M$0^IR;~|ADwhQJ~y=fzB z|6^KrLAdtI>q9kOmsF<3HObqTTB^QLQ2KWk%Vk!fK&4L6Q!T^sH|s9Cf-{q|qlVaq zOG6;a;JJBsXlB+~jax=*%*=9i^oA(j38!yQx6+8e?1#0$GmDTw9Sr&HYTQ$DoUMF~VRPJ%m3lWMF;pDG`Yg%9N>Z)Ep68J*aNL-0y z8$`^VjVp8RG0=Arh!f(FKcssC1p^(z?zw}9T7nK*Y_5oOF~=_vzg(6*_(Lxj9QW98 zJwWQ?@(E+bl7tVR()(8$mT);_XnX(yZJp+((pEV=hL}&0ybXFkaQN*%z2pY|kmkr* zJQtZ#a{Tsks%()q+i)TTXXrRFR`fi_E11OgWX^8Bjp@zoP$*H^#w(aXz|k;-@3pss z7H@OpBC$2HHl@+^awXkgJl21}F9au(o5^Oughz*3y3}fYbi1qjjaAauej#ZG7?lPl z-|DimK{_ihFkfm6#cCXo#Zp+6Q0Pu&gKeLtgOApt6ZM?FqDFMVjowh*%Loi%vyqt2 zoOdbKYR_1FepD)CzUK6gN8?$pK?bgGz!TNY$_t!B z?Ghqs%*l|3Nb&I{Pigg7t+XgM3T@maehv-q2`yH3eN66W1_r4I|CM~8&+GIp>gx-2 ze#ER@_vDZH5I^7fD=XS1rIUw~%9RcBFGSw-ROr(uX@PYxVCurFz@MGzPvU(n564^G z2yd~DToyMt)2F3)QbNbKKrTKjY-f%k1Xa0!L&OKDJ}VT-D-MirpXQ_mg~wx-XCk4n zNj&*2J%!&}^F)0`Bwl&o%N#$63^YU`>4D>=;p_;=anZ7TfblOgLg-dOYtPx9Sox(j zdu8(?G3c<0meHo^B-(#@0cDy$=<_(3uI}C;}Qri;d+7O z_7b*Zo+B4uk^#`7s2Yss<`;LiAI4{K__vU zTWo=y9(eto-kr8JW**BL`vrxcW=Tmh>nT9#lAS=~`b%Jdqs_`khxck!_7n>` ziv&92s%WaRz|%{{s%|e?SYF!SYL>puPKyJ=(36Qe56^zMqW)+FE78(t0nweF_^7+s z0}mWPl9;(YN~n6}NJ+LJdfkt>#QmWUlBNk^{X7yK%6qXpP9bmb*@*^Ff$spr2p?rg zs3QVZ6S=ywvXo^?=vpWm75Z}{*s%BMslWIft2s#w0x@-83A27vWsBZ}8h4|1L8kjw zPZTrW1nCuTQOPHUfyku{D_p!p7{<|9{UTBD*G!FDiL8O|hZ{vhj(HaM`2c8Nrzb7q zF6l3_^h608u68^vM@APuN?kF!IBH4Rzwh(W7v)^7^}+WkPC)TFsNmAAM|{sz^_!wC zD%9RP9LP&*Q%Zvz%s$QImg-(wlZjn#=l@3`%QT0v4cutfzmk{L{QV+-Z+zJyI8uus4HQ@XS7C& z+i>o%bTrW&kx&Vl_JQXtRt?R@8C(jKEBdGgl02>^Rv_@?Bj>n`d}9;5FxSQ; zjiMv&S*Vgn$hOebrIW5bY^&%^xIoNe05Tgd$zN@;CsT`QQy;9H9in#GR%T(M3z;*eKH=U^Mf7-)z`+oW8iEdv96OwMTU)uIa5A5AWEnvA~5Z z2+3Cb+iiS2Rn>Od2}4-XLw`rzB|2q1GTX<8r-meH5a(cp7Uy&Kk#o0eV_{=HBY9O2 zEmLrB#17K`&K112VO-b1-T~qu$WAVxp5nqsEWyp13&{YpM>xAc^3n%L@-Ry& z4L0qsy)8x_DzNS6#pf8iC0Dk4k*KMq(9#PQz{G2X&xYER*Lm}4>ps(9r%|vNYt8Ld=L^bN_oqP(VEuFBuC}+x$CmDm0^3{xvgZcD*_IPi1B&C z)=~3QbK_`p>ef|ISxV69B&}3Zun}9$t|JTuQ;x;ghEYaQ7N3~fs5D-B=?O$xPZz}e z;A?f5I`MXyK_V)n>y+pppO?pSY|5(K?Zz-U>(1 zo7yvG0l7j07f_(QpInk#;`l+9FV1wBt_^rCYtPE9_A$9rJ&t)DrKB$hVbpyGa00@R z7De^OcFn5Qa|ARq(b*h}DS*`gu#mG{^*+ekvw|3}ALgDB@9StOASQvDb{B_C>*Ef! zOxYGo?pxgeQ2lIeL@0qdYelZG9L?dQPXDdg=J!v*_{D2H8U)fCtIbRD zGmQ*W-9A4+wE~RKgS6%c9l|d2&(Xr0GQY)spWeeAcppceN7Co`L<15x-%f9E$OPdb@sou8g;(|y3_JQG{ ztN~n_wC+h=aK_mxVu;I>BLk3YMq|S;n21TED#t=1OY+z7w7<#H^d3>3Xsds1D_uQ?`(giui}9-5e3kB z7xs66x$MG`As{&Z0*vDSDKj!Y>7O!Z{ThY#1{Ws;T%5lvw?qo68x{so?+%02>%qRD zcn}caHTeJ9GAO_RLXagnI*4Zk4e+#wcUTTiUI*{w{{@49*kJy1OC=^$P#74BX?ADJ43>;AWq^Pn{VT^{?*9X#fZT`B0RJoH`XB7I z>>psQ6!M?i{w=5Z9~cBgmck$4zr;21K&zu@fWMU*{~KJS^nYMX5YreM;NMbIV2KOX z|HeT;X#D}&k1;|2Bi91T8N+$UtoP^K#27u`Z(WT4z7o;!4~S-5_#My+#AHSViXNwb zw`B!t8z=i0P_@CTPrWwKX4&cx^@Z-1#w<>|ZK+SO=wiyM$e}a?#HHoF1W__;I=CK7YI=J z2Shs04EW1G=P!^>2^d5Uikim({O9Aw|5}RZH&E9+58&T38gQW*|H>t(55(L=4x;Tu z{>PjE|2qQw2U|J_vR#k^{4)alGeo?A3(otWRPd4SFleWjA8fhw?-WNt{EJMa|BMd* Z9uL3>a1ZxiTka?*Y>^)JW%QqO{|`erFw_75 delta 48711 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 JWZyq+{|_!t_?7?w diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties index 5c712baedf..e0b3fb8d70 100644 --- a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties +++ b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Jul 22 18:43:06 PDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip diff --git a/examples/multi-module/gradlew.bat b/examples/multi-module/gradlew.bat index e95643d6a2..f9553162f1 100644 --- a/examples/multi-module/gradlew.bat +++ b/examples/multi-module/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/multi-module/mvnw.cmd b/examples/multi-module/mvnw.cmd index 03d90e960b..830073a17e 100755 --- a/examples/multi-module/mvnw.cmd +++ b/examples/multi-module/mvnw.cmd @@ -1,161 +1,161 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..29953ea141f55e3b8fc691d31b5ca8816d89fa87 GIT binary patch literal 56177 zcmagFV{~WVwk?_pE4FRhwr$(CRk3Z`c2coz+fFL^#m=jD_df5v|GoR1_hGCxKaAPt z?5)i;2YO!$(jcHHKtMl#0s#RD{xu*V;Q#dm0)qVemK9YIq?MEtqXz*}_=jUJ`nb5z zUkCNS_ILXK>nJNICn+YXtU@O%b}u_MDI-lwHxDaKOEoh!+oZ&>#JqQWH$^)pIW0R) zElKkO>LS!6^{7~jvK^hY^r+ZqY@j9c3=``N6W|1J`tiT5`FENBXLF!`$M#O<|Hr=m zzdq3a_Az%dG_f)LA6=3E>FVxe=-^=L^nXkt;*h0g0|Nr0hXMkk{m)Z`?Co8gUH;CO zHMF!-b}@8vF?FIdwlQ>ej#1NgUlc?5LYq`G68Sj-$su4QLEuKmR+5|=T>6WUWDgWe zxE!*C;%NhMOo?hz$E$blz1#Poh2GazA4f~>{M`DT`i=e#G$*Bc4?Fwhs9KG=iTU1_ znfp#3-rpN&56JH)Q82UMm6+B@cJwQOmm^!avj=B5n8}b6-%orx(1!3RBhL~LO~Q_) z08-2}(`c{;%({toq#^5eD&g&LhE&rdu6Xo6?HW)dn#nW17y(4VDNRo}2Tz*KZeOJ=Gqg{aO>;;JnlqFiMVA+byk#lYskJf)bJ=Q) z8Z9b3bI9$rE-t9r5=Uhh={6sj%B;jj)M&G`lVH9Y*O*|2Qx{g3u&tETV~m)LwKEm7 zT}U%CvR7RA&X0<;L?i24Vi<+zU^$IbDbi|324Qk)pPH={pEwumUun5Zs*asDRPM8b z5ubzmua81PTymsv=oD9C!wsc%ZNy20pg(ci)Tela^>YG-p}A()CDp}KyJLp7^&ZEd z**kfem_(nl!mG9(IbD|-i?9@BbLa{R>y-AA+MIlrS7eH44qYo%1exzFTa1p>+K&yc z<5=g{WTI8(vJWa!Sw-MdwH~r;vJRyX}8pFLp7fEWHIe2J+N;mJkW0t*{qs_wO51nKyo;a zyP|YZy5it}{-S^*v_4Sp4{INs`_%Apd&OFg^iaJ;-~2_VAN?f}sM9mX+cSn-j1HMPHM$PPC&s>99#34a9HUk3;Bwf6BZG%oLAS*cq*)yqNs=7}gqn^ZKvuW^kN+x2qym zM_7hv4BiTDMj#<>Ax_0g^rmq=`4NbKlG1@CWh%_u&rx`9Xrlr0lDw zf}|C`$ey5IS3?w^Y#iZ!*#khIx8Vm+0msFN>$B~cD~;%#iqV|mP#EHY@t_VV77_@I zK@x`ixdjvu=j^jTc%;iiW`jIptKpX09b9LV{(vPu1o0LcG)50H{Wg{1_)cPq9rH+d zP?lSPp;sh%n^>~=&T533yPxuXFcTNvT&eGl9NSt8qTD5{5Z`zt1|RV%1_>;odK2QV zT=PT^2>(9iMtVP==YMXX#=dxN{~Z>=I$ob}1m(es=ae^3`m5f}C~_YbB#3c1Bw&3lLRp(V)^ZestV)Xe{Yk3^ijWw@xM16StLG)O zvCxht23Raf)|5^E3Mjt+b+*U7O%RM$fX*bu|H5E{V^?l_z6bJ8jH^y2J@9{nu)yCK z$MXM!QNhXH!&A`J#lqCi#nRZ&#s1&1CPi7-9!U^|7bJPu)Y4J4enraGTDP)ssm_9d z4Aj_2NG8b&d9jRA#$ehl3??X9-{c^vXH5**{}=y+2ShoNl-71whx;GS=a~*?bN{cm zCy+j0p4J4h{?MSnkQ5ZV4UJ(fs7p#3tmo7i*sWH?FmuDj0o>4|CIYAj=g@ZbEmMgl z6J-XPr67r}Ke$)WkD)hVD2|tn{e!x-z)koN$iH!2AUD0#&3&3g8mHKMr%iUusrnOd>R?l~q-#lr2Ki zb)XkR$bT5#or!s~fN5(K@`VL)5=CrQDiLQE;KrxvC78a+BXkAL$!KCJ3m1g%n4o4Z z@+*qk1bK{*U#?bZ$>8-Syw@3dG~GF=)-`%bU56v^)3b7`EW+tkkrSA?osI4}*~X?i zWO^kL8*xM{x-Ix}u=$wq8=Nl5bzHhAT)N&dg{HA$_n!ys67s~R1r7)(4i^ZB@P9sF z|N4Y-G$9R8Rz1J`EL)hhVuCdsX)!cl)`ZIXF>D+$NazAcg3$y)N1g~`ibIxbdAOtE zb2!M7*~GEENaTc+x#hOFY_n0y3`1mnNGu&QTmNh~%X$^tdi_4%ZjQk{_O^$=mcm|! z%xAxO*?qsc`IPrL?xgPmHAvEdG5A>rJ{Lo;-uQf3`5I~EC(PPgq2@n1Wc}lV&2O~t z1{|U92JH6zB?#yX!M`}Ojw+L1Z8{Is0pe?^ZxzOe_ZQcPCXnEVCy;+Yugc`E!nA(I z%O%hk_^!(IZso}h@Qe3{Fwl3nztZ$&ipk?FSr2Mo@18#FM^=PCyaDZ35%7gPt-%35 z$P4|4J8DnNH{_l_z@JQPY07;`(!M-{9j2=y__fxmbp59aaV4d)Y=@N(iUgGm0K!28 zMp;Ig3KkNy9z>t5BvQWtMY82$c}}d6;1`IJ^~At0(2|*C(NG#SWoa2rs|hBM8+HW(P5TMki>=KRlE+dThLZkdG387dOSY2X zWHr}5+)x`9lO#fSD1v&fL&wqU@b&THBot8Z?V;E4ZA$y42=95pP3iW)%$=UW_xC3; zB6t^^vl~v5csW5=aiZLZt9JLP*ph4~Q*l96@9!R8?{~a#m)tdNxFzQaeCgYIBA1+o+4UMmZoUO9z?Owi@Z=9VeCI6_ z7DV)=*v<&VRY|hWLdn^Ps=+L2+#Yg9#5mHcf*s8xp4nbrtT-=ju6wO976JQ(L+r=)?sfT?!(-}k!y?)>5c}?GB-zU zS*r8)PVsD;^aVhf^57tq(S%&9a;}F}^{ir}y0W|0G_=U9#W6y2FV}8NTpXJX*ivt{ zwQLhX0sSB8J?bmh(eUKq#AVmTO{VudFZpsIn-|i-8WlsexQ<;@WNn)OF=UpDJ7BI= z%-95NYqOY#)S?LIW-+rfw84@6Me}ya4*ltE*R^fy&W7?rEggZBxN@BR6=0!WH%4x0 zXg7=Ws|9Em`0pAt8k0cyQlr+>htn8GYs)+o>)IIf)p+yR`>lvz>5xFt(ep7>no4?4 zA%SUJ=L2D=;wq*f8WFl|&57Apa1;cT?b?bfJc8h&vkBvm%#ypP{=`6RL#Tf-dCq`;$!eR%>29EqpIkV*9 zEZl_>P3&}hY7)~q6UYw?*cBCsuPi$TU zRe}A|5nl7L_#e`8W0Hcpd~NWjAaV#3ngl$CoE3dz!= z?$3`dPgn5I+Q8 z@Bk>MqB7;kQqnDK=buPc+DsEDP-S;8#I(_z!*u&%_%nqI3+srxxsf9-Qg6%$l$Rtl zK2Wn-OtsBE5<1d}1Hl!l-r8eqD+{%b5$jfxQZw`2%)f+_^HMfbWyW4@j!^9M({>e; zeqCfR5b?^xh7MhHfmDvoXm8Wq;Jl2RU;jY*+a&o*H02$`#5HsG9#HOR4{g9 z#2mgNt%ep|IWrmctj=e%3xV&o^@8%OrR6io()6^sr!nQ3WIyQ3)0Mn}w}p^&t*V0G z03mUjJXbSCUG!o#-x*;_v>N8n-`yh1%Dp(1P)vz$^`oevMVh?u3}mgh}Qr(jhy;-09o$EB6jjWR!2F&xz^66M!F z-g}JBWLcw=j&Vb>xW#PQ3vICRT_UZ@wllScxk@ZQe&h-y)4B5kUJptVO%U-Ff3Hka zEyLldFsaM5E5`k>m}||+u`11;)tG@FL6TGzoF`A{R}?RZ@Ba!AS(tqAf{a_wtnlv>p|+&EEs(x%d4eq*RQ;Pq;) za9*J(n&C2dmFcNXb`WJi&XPu>t+m)Qp}c;$^35-Fj6soilnd4=b;ZePF27IdjE6PZ zvx{|&5tApKU2=ItX*ilhDx-a2SqQVjcV40Yn})Kaz$=$+3ZK~XXtrzTlKbR7C9)?2 zJ<^|JKX!eG231Oo=94kd1jC49mqE6G0x!-Qd}UkEm)API zKEemM1b4u_4LRq9IGE3e8XJq0@;%BCr|;BYW_`3R2H86QfSzzDg8eA>L)|?UEAc$< zaHY&MN|V#{!8}cryR+ygu!HI#$^;fxT|rmDE0zx|;V!ER3yW@09`p#zt}4S?Eoqx8 zk3FxI12)>eTd+c0%38kZdNwB`{bXeqO;vNI>F-l3O%-{`<3pNVdCdwqYsvso!Fw($ z`@$1&U=XH|%FFs>nq#e0tnS_jHVZLaEmnK#Ci==~Q!%Vr?{K0b$dSu(S!2VjZ}316b_I5Uk*L!8cJd>6W67+#0>-1P0i{eI%`C(_FkwRC zm}5eHEb0v^w3Wkqv#biSHXBG4yPC=^E!@hV8J5*JYf73=BqO!Ps#sP0fx~&C9PMN= z+V%$50uI|KE4^LCUXI74-qw$aRG&3kN-aOzVpRS1AX(Ua;Ewy>SlDn@lV(<^W?t-x z%K2iVK+;lG_~XF&Glk7w4<=Z!@-qDLc7)$q!>H^AU{s6e7krRmr!AZLf?8~$rRuP) zc$@c*PhIA^Lsu;uR{^x2)9nvsm}-67I`+iFZkhfNASUD>*LqxD=sAtpn{zY0xMxFp z4@USzYjMULeKc1lBe*8vxJDGNiSTtq_b#zd+Vzdc%$~+xf0;s|LR{F$YKe7YJVR$U}jKOo6=D+|6vnryopFbmNXEo-~I z*nm(LHmEGwkB%h%tXF4r|5h2p%VnRLx5rRsFpPR|e)*)C`WG-Iz94xsO&>1k8g6W? zG6#40`>I=B^scgmt_6!uU}=b3HgE@Jhj-X3jP!w-y>81ZD*~9C6ZRN4vlAFJQwK&l zP9&CP4%l-eN@0>Ihb_UWtp2kcPnh+L(fFJfQLc0`qqFbCkzr`8y2%{@RNrQbx*;tj zKtW!BWJFR$9(9^!Y%I%@3p?0zX#;(G?}sRkL{U>2rH4Wc{3{0@MV+vEaFcD18KIy% z7OyQTp?-N_)i%g+O#h(eLt_3ZDo)2l4PwjVS#=FzUNVvW{kFijz-@Y9-66fQL=xoc zXfLAC8<-!nnpM87K#eT;D^sW^HL5kS))Qj`kxT`%OewTXS(FT^X~VlkkZJJ?3*R8J zR>c>6)9K+9lg_a7!#<`KC$oEk-!~2N)@V}eq4O2xP)~N-lc}vH8qSe7tmQ3p@$pPde;Xk30uHYJ+VXeA@=yordN?7_ zpGsTlLlI{(qgtjOIlbx8DI{Nczj!*I>_-3ahzG;Kt&~8G_4G8qqF6IDn&g+zo>^L< z@zeVTB`{B9S*@M2_7@_(iHTQMCdC3zDi3_pE2!Lsg`K)$SiZj2X>=b2U#h^?x0j$Y zYuRf9vtRT~dxvF2Onn>?FfYPan1uc&eKyfBOK(|g7}E)t7}?{4GI%_KoO#8;_{N6! zDAqx7%0J`PG@O{(_)9yAFF!7l zWy1|Utdlc)^&J3OKhPI+S|Fc3R7vMVdN?PgoiQzo200oGpcy;TjSQ^e$a}Kh&C~xm zsG!Pqpqt5T`1`X$yas7{1hk?-r(Um>%&@?P2#NMETeQYhvk~nZW#BApGOLS2hdH)d zn!sf)7DotO?tRXBE#UpfKk-s}6%TfS0|7#>Rgk z%Np7ln*SH#6tzufY<0|UT+M}zJ1)1ap_cE@;QZp)+e-;k24 z3lZG_EA?tM$Eg|x3CK3!k`T7!*0}{fh8#=t^2EJ>TTo`6!CUm(HFUl7fFIB9Zlt4a z!4=|s-ZSn!@6Yc&+r1w*?*2fxKX>Hz2(vBwgE*>E=`A?Y1W-;{d2$4B%$NFAI?v5e zmYT{blxWeHn2J(0Vbz%FDz9~baqE#)R2TMG24xMZjCLcPfc1mR?5H4L%GnMR7ua{B zCu=nN(vV)5dJ_B80WBCy`tJ#YH6GyltGBSQvsN#q0;6XU1&60$&PC$0r}FUdr@1I+ zINcU{Ow6t4Qzmyk=A6u*z_!A*$^hBXJeKQ96bnF2qD$46hN!?1C|io|<_u@g16@Wd z(Fg?1=p8)dkWz<^ml6Tj5gO$hpB1N5msV!#PB5pfwCOBu`cv__=7kQq*r#Tc7E@6z zdr}5qs*slXK39`Yn%?=rslQgOTH0x?@z|h%fI5Y7kQ{X00BcL#8Jae4Dc9M zR%ySU5qODGnM;n#&up^M+PIddhxizA9@V%@0QQMY#1n z%{E8NS=?1?d((9Bk_ZC|{^(juH!;Mih{pTo&tu<^$Twk1aF;#W$;gxw!3g-zy(iiM z^+8nFS<9DJfk4+}(_Nza@Ukw}!*svpqJ)Nkh^sd%oHva}7+y)|5_aZ=JOZ6jnoYHQ zE2$FAnQ2mILoK*+6&(O9=%_tfQCYO%#(4t_5xP~W%Yw7Y4wcK|Ynd#YB3`rxli+9(uIQcRuQW_2EFA@J_ae$<%!EbI9c5htL`8>3Myy)@^=J)4p@nB2*&sWCOmwH zwYi;-9HOboaw0ov-WBk89LqGY!{)>8KxU1g%%wMq9h@Aie^42!f9`?o32T4;!dly? z(N?67=yo%jNp;oIVu7;esQ$wG=Vr+`rqPB&RLzr@@v`H-KK6wTa=8b<;$yE1lQGy?A1;JX|2hSzg9`a{;-5oh|=bFSzv&b zst=xa%|xW;id+~(8Fj7hS5BPVD(@(`3t@HUu))Q{0ZrqE2Jg zm6Gv~A*$A7Q#MU25zXD)iEUbLML1b++l4fJvP^PYOSK~^;n$EzdTE(zW3F1OpKztF zharBT_Ym7Y%lt#=p2&$3gs=g4xkM8A%Cbm*xR)9BnI}5=Oxp4GEF*bjFF^87xkP4L z;StW)zkX!yzz5^Q4HfEicKi{8elkFQx|0TH5Mtzsln>TN2*5Nypl(7sj_UxoN|KSyOP0g{L+vTbHlOyIEJ@ zjfku4x;`_FLga2P{FJLrgpIt;A-ukDuPsuW4#ApWE7|&i85Frv()~gOM`v`YVsF0c zx|J0}YRtNo7DIl>N&+%c(o1^C?%>Zf5<-<(yVcj~p88d;@=(jtox_$Af#v4%=g4oD ziv4MKh%Uf}NHP$SqF6mZj>}_HfC-@2>S~<3qOIu*R^%7;`VGN{ay@0(xmKM^5g9H4 zaq4>^38z|jszHqa)d>j#7Ccxz$*DGEG9PtB(d31?a;2$u>bY`CigPsg$zpDTW?zKg z+Ye-wtTjYHi#Hs`5$aDA=5Gl4J>p1Xs3PJZWWgax9~(h;G{hDip2I=+bW1ng3BrMC za72TsJR+;*0fSYuVnHsA;BnH5x8yc5Z=Bno0CUc14%hAC=b4*&iEzgAB!L= z`hhC!k&WLZPFYJY4X1pELFsAnJ!}Y@cW6I~)S53UOve!$ECM^q8ZE{e{o}hoflqqy z1*ubPGaeqs1&92?_Z|pDIR*gw{Tf^KJV)G*JLdzktzF;w@W<(X2;}XY0Mlzs8J?$L z$HVp2*+(o8?*n6cqx3_k6 z_&05@yeYRSfWQk)=oa0v#3BHNBBd>{fP`)#O^*^0_#?tW5jf!vCBp<2W+WCTEYeSv z9x0#bu>tB9M0W%_p^S7&BHa{2hfNL5eUUq4dFsGvgW}38M#j+AdeC5Q0pg^g zVzX3vrRi^YI(~*BW_Jv^o?2;5SRY4UiQy4mO}td`T?9Cn>K+dHL)+V&T+H2e9cz36 z3w!e<82_a0Abraxx8?L{a%&###&w=O83@y6xz0Yz{8$Wp? zpRHDDFRKHe+@^Y7*&@z$+aA;ksdi7xdV}c(i1><3F00dIA(v8LW(^O*HX)5kc#IRw zqF;w9l3uQK5us~@YEWk+?*7*(7!*}^OBGk+&H=rcQ31wWiI7@}vU8P`@-3x85BGy25yPLiFcZ9Ix z&g>o*aIM5;Y#3A-9~8-WmTezK5V~98kP{j^ZZ|WDa{ZX{nzq*qy3?Lw?|D4hN>kzB|OT6-b>reho-)KPiAg^M6 z^V7T^-LL<$VK9OM_AsP21hWykSObS?gk4L=NQ@Wevk9nXUWk~lu4S>zqFX4H{cWCE z8{eF=%>j8Xll5o2)cdA;Gx}>chr}9ZPv2kT=8x~q=B4i_@+{8-#jh5lsK}aj>0zxd zIl8*E$!(}Vii%YIB_2V6>|Ove`W+f~dqsd+*K|~yHvkUoMukz^XnLgcXunf+E9#k| zU0yT>#IG*W)+6ue)vv=xfDT{9k$;BDL!duM&qpGVui6NbuaKa`h?7i(W~4YUu2O@t zV=FEUMaC0QAIZg2c%Yb_WFI$vZ0z*fj-GdWkVMt>lDy@w)qhCE7c^Vx0i34{@bnQJ zMhB3B>8stMqGsKyqUsN>cE5xczm}r!D&5+?zTtYl6!U!4nmiPv?E)Pe$l(A@E1T7dD)Px*$)#pB(Mccz%i%RKcuskizkH& zM^+m#S#sK2?f8;gH5BaXCfyI z=Mo5s;fHbBh@$hNB(!H7;BeU>q)!Z^jaCks!;!d2W7 zv{8hf2+z&R2zAS%9Tu1(dKX~*{rOT|yjLsg6Bx_1@bTy#0{R-?J}i!IObk@Tql*9w zzz?AV8Z)xiNz}%2zKEIZ6UoVuri+AT8vVZBot|VA=8|~z-!4-N@}@Bfq$~F4`^LO) z?K#tKQ7_DzB_Z%wfZ*v)GUASW0eOy}aw!V^?FkG?fcp7dg4lvM$f-%IEnIAQEx7dJ zjeQdmuCCRe*a?o*QD#kfEAsvNYaVL>s2?e^Vg|OK!_F0B;_5TuXF?H0Pn&9-qO85; zmDYsjdxHi?{3_Il0sibc3V2IAP74l2a#&X0f6EdwEb_ zCHuQC@Q$(2$$0W&FuxtPzZJ`{zM{%lcw)>^c&ZZe3{GU#x8ZmhC${E>XcP+}<0zKn z`!He406MT}e^f*=$WZoCHO>xt?AE)A6xB*54a+>4&{!W0*`Q93ibK&4*}N2!PdjOa z8?@WRHjyEXqa(1=JSuglKreLS>x>SiHMYiH7)EW4L&&HyJUh+>opC2p&vz)-)hLZx z$xgyMGH)3R3o|Ptu(n3@oM8uX^(hq+q=`-aC1BlQp2I$eKj1tJuqDUh( zDkDsZ^23iaH3;bn7U>k)AD&%$u4G55$I=scldY;vFs+SJmR6mE&8&=C%8}PL3Pz1e zQ8C!gVj0PV2ym8>BOJZh9EPGH7B0X&x$=hK?E>1-@+vYaj!Grfw5!*_$pLHotuVn@tVzDd6inT? zVRbufqa&mdvhz=1^!A^mshoYUOn2TjV3fhuz*2mdNqBX{nUrI%6StBzCpt&mPbl5F zvw_Cj$en(bhzY^UOim8~W)nxy)zWKuy$oSS;qRzt zGB#g+Xbic&C4Zo0-$ZvuXA7-ka&rf8*Kn)MO$ggardqZ=0LyU3(T};RwH9seBsgBc z$6-BI}BN*-yID>S62)&!|-r4rDIfw zn19#SN$JA4xngbeGE4txEV5qszS(EnvzvVfh08c;IO5>d^UpU#m~24P{^7AVO7JAS zXZ6RdAp5-_yL;j@AlsMp8N&HVwHV>9DfH4c81xmzCzVZ3fXAQ+=RnI0B<;YfHZuqa zH|&*09Aj{ZsDVS+5jB{XEkd)PR5JO&0q`JK;9>!6T7%b14rbcBtNiw}OPI9h?u#%^ z{#w3(2+S5shq7N4smmX#Ns_ayWl5jP^7M^2hVn&gl1y>C@BvQ$Ah*^_cgzF=iG z39Lr1x6KpDuS0W9tH%r}N=vnOgCk^E`0I|6X8%H)E5a1{r;Ooi{4RF@DssCC6!o~J zDpXb3^$sNds;bMqm6n#cJ8M2#j7A_?^(fYr0QA$GrTQV$n;9;Qkh~$WT|e1Yq}o;h zEk_Ww1Kf4%%?R!{!c91CSJ*2fr<8xHF)(7!_%EKZ*$KsDg&ALtP>P19z99^whu6ms z^F(P(PMjgfp#lXpZt(?04@z5J{`JHow@|N~KFN{8WLok3u$zxk=`cv$?EaF;?XU6*mT&GJ_`>Ma3MgI?U07^UN9N3Fe37d_Q@ z-K2Z>R)Wso&W%+APtaorr8H4bEP6FH4p7!F)=w=jfs{I20h3Vck4N=Y(~XC1-kIAd zy5x^LnlUYu)zXH(P}oXq?U#Bgp{4bf<(9x%vx;I>b+jS0&jtaYZ?(5Pfi=RUF`r58 zPQbIAX=tIC=*W@cR#+`*i)vPR-|p^(ORBp*UB+Ei6;0-CF@No`$y^MQ8{I(2`CNzye&0=Q^qYjw%}y zZk$+l#(MVftcugPvORxL+@7k(4XzR~ti3!@toSymCaI5}vo}ri9vdMZa)_TzEsCB^ zLAkET9Z0E*!fv>)%Z#tIxUhYw%QRE2;98~{O{W%9rXI<-_{I=y%%qwb%iNi=+!>Qf zK(HtaA|ze7afz`txb*_lkb0u$(ijK97^%;axfg0J0#7NIs61X5HEQ=zq4Zv>VMu>$ z2~v10H$A`~ZB}6dK%@F2UgC9sMoSgd@q}!<7mY~z+C3H5tBW}xeKN&KIXP_?N=ed~ zFv^}TDs}$Eb(JDOQ;H7ZUNrivfKib({Ix|*X$AZawRj(j{g<^=Frb3--rEyv z6xZd8uQqr-K=@KuDrN*E`gfQ`mxKf_5w*!nJcKf(S=suW%7rFjx+s2> zi#9ouh%>Rl2Ch+}ie_3lybm-tkHbTSJILVkcjl~h@Q}u~N~u`668%(zQ9>9i7C#5$ zx{s(#H|$tR^Isy#9Q9XsY<1MHT-F7OyLQJdGEvzDtP8S6C2h^jU=C=>>*UM{Ijd1dNe~wr z+2V*%W+RpfrPRjc)E0!+gT^{TN*3CN1C}}95a1F4XwxwLS9A^ttvzq%M4HJ+$y?4I z`yKD+?Z?h%Uf%Z`@?6k*M1Nf&Cz(V^NgBygk_J*oqqX3`NcK^Lkg7rqVHhw@z>zv- z%X}I!;8!nQ^_RTCBos2Bl+SVD9Fa##0@yip*+{E)wPQxv$$hRA!c&QWLoLFG2$U zYDR(@dUI1w4`Zyv?%zhHwZ){BfpG(vq}!Y;6q(jI@xnbko7P(N3{;tEgWTp9X{GP3 z8Eh9fNgec!7)M?OE!e8wyw>Gtn}5IO|5~^)!F(*STx1KCRz?o>7RZbDJd>Dg##z!; zo}rG4d{6=c-pIFA4k|&90#~oqAIhkOeb6poAgkn^-%j66XICvZs}RA0IXj6u*rG#zR07|(JUt8bvX^$La@O#!;a) ziCtKmEDwgAp}1=mhU`6(nvaz%KG1c@?X8FbZK*QU*6mn${cWs15OGLA-803ZO-?=7 zah4u9yUPx8iI^Q~Bc7;DSaf@k0S@+p?!2(*$4}3v|?Nx~swkjwTmia)C!dVfht zzo1E-1vmsM(nC);|(Kp4yaPusRKec@I0b0J(n9k*tg>E zC-M)?LH%OLASR6}G-`?oyQ%KJ3(+KfS;-Rndh?ku8frhoZdKm<$0bj0e4I_lCX`7S#zIYBZ*s)i1dsNx5wX6~IDx z(Oz=(Bo4-fnzObxxiw~v`H}FuI<4v9nlM*7QryonD7aNenD4Iivwde7(TYd34Y|)E zZ;|i*$m}OZEsYWN9Xn+cJ?tl$HcJt&tK#m5)0pE@XV}gwcJV80^2W;>rR>%lUXzzrnFRHk2?0nQST``j1g;Rr}E@4Bo##q3%WJ3kW9`oLwIq zA0vY(vUKK{!(xz~Aai`k?GLCg(L^>jk7c19wzM!kci)KXbo`HMF5|jVUqOh5zPHx~ z7u)Wv`L*($bdq$~K@z$=!D+{HF@qBwO~Iv@@Nxw?Fyp2O5_#Ys8J$}5^H>J%`@CS{ zt-hYIu7NOhv0I=tr-?4EH2w4i=#_UUmFjs z%A-veHM(n~V=b%q0^_6lN0yt~Pi!0-4-LyFFewUhvZI$BFGs7)rVm2-{L|9h^f~Z)eyKyr z7?*u`rR)t7ZJ=8!I1#4|5kHXDmljgsWr(i6WPJ0eCg9K=mNGR7`F@<9Y)ptr=d(G2 zyFZ6ui;z7lu4{L3aCARB69KtaMekNz59bzEC8)@)F`W`q&hnF!@hlaZlivmQh~9 z8R-`kyDt3>Is4#t4`YaCAl(Y_9rDyTs1KYE_5gKHl-~>Ih(L@+s?${L`>}yrDEr-q zaZJ6`3Uhb_efWr)4dESDe#xM2C-gvCth%+_s@(-6U(RvIlv?Ex6v_UD{5h)9b*>N7 zzip!Gp<%x}c#!@x5`?mLYygtk7JG(HNpnAPnU%2^Gmjs75I>IS^yb*`pyeYn!J7D^ z_Z#@1;rrh7(T48tPjx2LKtKflO``Iz@cr-po+gBW$}#TuxAUQHEQAn2AEUg92@)F; z3M`=n3n&Q;h^mjIUSbe7;14c|RaJ{dweE`QJlDm5psETI1Mo@!_NG-@iUZ5tf+VTP5naWV2+Jq7qEv=`|Y`Kg-zESx3Ez zQ)3pq8v?(5LV8cnz-rlKv&6J}4*g7EdUU6RwAv#hOEPPngAzg>(I@$3kIb+#Z%^>q zC6ClJv0EE@{7Gk%QkBdOEd0}w2A}A(xKmF(szcN4$yDCezH)ILk`wx*R!dqa012KxWj{K;{m4IE$*u6C-i^Xn@6TimgZXs~mpQrA%YziFDYm9%33^x>MsMr{K`bk4 zmTYOFO0uD{fWnFuXf{4lKEGfjCSAEiBcUh~-RK~vwagYh%d^zqS*rgiNnc4TX!3<4FL7tr3;DA>RcYrMt3 z7h~TlyR(x;>v|5s1e#?b~H|Pqc=q};~YvHmKp(4Zk9bYF9IcEMmW{Q;%denJT?l4 z70{bSJ{{dIb)jJC54M+j%am#jwFugdb8V~47)xgJ;{uA!=Zs?&88BQVhSI&P+}(>q_==| z7JnM15Q4kwb~Px<@LEs%cxdZlH`{A~E3?IKpfJGR2rv7%N}=c)V?JJ@W7AH|AkZUh zvi2w)>RY)$6mkHQRo9L;PYl3PPg~?S(CX$-5+P!2B}GqIGEw- z3&}?!>|j7^Vh!EMc2U!gsDhS&8#Pq)SlamRXJ#FxX`caWHH_RW3%~WsoF&WECP$2g z3vaHqsO>V7k2xZwX3!-T2cj>VPidn8C|_4c?CyU;gpnaO(?YGO=a)9=Sc(n>Zb)C_ z>8fRKP6=d9Wg?&2G&5nNVU7Xk_8F-TmDrM6uNLZNK!U|gEn(vb`sw~_Q7LRLhitWE zJ{DBl&v1l}uTVoMM*y8$1{W*UIP`Ju*BeYbo`gJO3-K_tZ&4g%BSpS&lGf9 zD<3|fTK@&&<9U(QZ?zOW4zHKQXw`?v;uSZJ3ZIAji)F;jrOD;GeX1VSR+>@*5?@>z zVUfy2G!UmbDU$F&S&~3{;e=EUs{9uU^x(oT)!;)yX4Es>NE-7X%5^brZcL7_$KhIv zr5CGYP6|tw9`3$Cz3Myl8 znbJvOI4#W@<>Cyg>1I0>WiZtflPr-GM&DAaVv>AI;InpOh-5usQbSpOmTKY9e3EKR z;Hno1gPK2lJj!r+UKn9Zp#3yQStL5eP+`n?y*fm?v zA84*u&xPM4%6OaA%lsEMxp<}G&L4b#3zXfT`Q&U=2$xO!&?4X~_EUw`E}jd$70B`D z%VO!*-NSxZ=hz=*vGi#2+0DPI?Nr{|cA-Xm?8(IBQT5razQXk&(-b@ZJgwDKQH#!m zNC}wPd|`LEdw{jkq}>P?kLv_l`1H;`3Ypo z<=~^h)h>9lcSp#~`+8{d*nkO{Q57=hcqST+<>@KCkjsY4-m!~JrSs!7e3YBf5+gie z@3YxN5s{0Nw97uJlOQ$kM!sMpu6~+PJ9*Ym^Ru?p*)mlo*nLP}tQcyY@^-0%KE==U z9_PrE;U|ZK{=rZX`6#d#514_!C+5->pSvmgNS}EpK($i?)6CZ!Huf)`&x;5Z1A(&Q z@DlP6YDZ(sbd(>nxM#=4mhsQA4E;<+v`Q%cvx`xmNiP4h>WvTUPJ22uWaL49LZe&$ zu1$oP!=mMt@SLsRR9nk&V1bN$rN33*%D|rhd|xC)oT5}P_9ccwLRy4*EnFy#-VG|7&>jsJ2#RpDz#r@68GuOAE*sQSmL#Re$ z8y$k2M}GP&w8RPob)Z+eZez0hGJ6;ig$hoS`OMO5oKKR#YtoGWNpHT|{A-<2v@r9k zdHaj`SnX5h4E^0M=!*2hM>m9i#hdJD+AEofPeP$bAN9B`?Qin)0|4sWhwTizniPlA$1E6xG?)-y`KbWVB#R7|wk*IeoeRw}# zv0XV|5pzw9*e0TCxIsLcdLNFOYX4Y^gpD&=N$!;WMK)%4;Wh80b>{oPy}ot6_RYmF zZFlk2_X|kWVuVY)O#Vf9iHpmhr1G2no4g{P?=gJ_UpU}HpD|jo+qJb=ynu~|cc+v- z;x`}SwQprny~&aqm;cD>#RsRo_#Tf(pEw{Z8_{2^g#CKVen}EUK}tsX@2GvX6kFB{ zz@BgZBarBKocTk%rxxP`3yE^XTF~#~>G?6S_kr*M-OA&x38`~(+>=FcD7CF1Zzp~R z`rhZwkz2j21wH7{BU2yzTYRZMGS+cNw5Qs<(MJzN+PcO{SFY&&dRNlj2{vylsOs_+ zxNOcD(t>RX?HVbjT||`Df>@!92R)`K$w3^9!FYA7Zh8->KU!x)e?ztv$;IVrH@|W@fd8 z7BiE@%*;%u*_qv$`FHN(BD$hGqB^>w>&yBw^JV6HC=#GpjX!WQ(zeKjLwM3%)TCMT z#xyLTD8e|^YTKwg=Vv1|?|13o6!&U$_A}W2wWMcD^#DSn@g(5GbsHO6W$I9JNSxoCmsH}pFn8j_Wxk~5^ zVhEXZ+s@i0YjOeagPLSQYoxR{i2biszj7RW*S<_0j2Dw-Ef7qqLN%~y`ZAHIINOP} zvmaSn7x|DlC&W$UxkMbbJ&xpGD97rRFi#}3H61(AYVcPN9YUF0n72Zo#a#jfh`6TX z7!Pw#0~N0S?BC*wDZ0l04tmB!J145jwS;Pci*%m~ID_r&x0H;>J>$x}okimL!WLb^ z%m!KzacfeEw#alud8ZbsYF& z1@a|GCQHDAcQ3iM5LfSbz{fwQEh%&k<8f6$Q`yJ~Y7aO&6=u1}-*Gqw6$crh2cZ*X zMJE4cPZcdI%GQ>e=U|%r7EWn5pWBsM{|l8thH#qb@2{EkxwMBgjvOdH_IVX`Hh3}l zHcZa5HIB;>NekQX)ukMQJ`DTqS}jZ#j|$iH=Y_~kA^2?d%gm$PmPGuA)POynhUyaK zegRG1n2fzKfWg9@a>C@^5M)xpFSicmIRz7$?!Cq3uh(hTvD(>sag!Yf5*aMvtv=^^ zleZUVg$1$=zDs9p6Q1CAH&);!jkC-ZJ{fW`hE2o0x^4F_jcyr4#!ggqbcMo}icm`y zQ_77P#ZDAzmQz~g1=4DW!t7IZa}Z7thh#dEqn7+`5Lf8=4OAj_>AZ3IGQlz5loU2V zh|Ok)*^>O^ITIz*6(a6LT46*2Z8qn|UEzXV(Cl(`t!NL2^RU)JQ5CwNXU<%q`gjnv zF8YRI{0Qs{HiYEeK^2%=T5HFvrq^)R3Z~s+&dp-ZNpWu25qg9QUYwJZRjYFp(D>*A=`$9U_~N!BjcnQhdaf0Wf4k~Wb-yz6v=9i4rRTbdv0 zO)%vr@`J~@XKn3Cmo;jazVHe{VYoA-^m4ZO7VwZ~TARsMO7PY(!ck&QGkAgY9Q9RJ zLr}6J8cX!W%WFefwo9}P-hOjJJd>||gfOKNQ$xEbxDL$!N<$66h}w{A$tdnEEUq5; zQB17>Yh#_2o^GIeLQ`D^c**S1E;}*EAjaUHZAmh>Q~WW`RrCigz!CK>NF|IY`w>Yt zHl!vK+Cf`LljiFI=u=(p3$f!)&jk0aE{~>@e!_NZAc2Omti-mkw)JiJbz_^F-VP%u zQ&y+sQ5}T;hcIKT?jPxfEv!MA!t{oa;sV+#hIQ7_qx8Lz5Sulr_iep}MwMTaYYHyE z;th6PF7kKkE$1mPSGQC0?W9DiI&FS zPw(Wqb7k(snDvn6ol!D7!#GhJjH2M&gJc}C(-vuZ?+cGXPm&H#hftWUx3POg66a6n zfN##yl=25{SXg!9w>RJsk>cLGe2X4*AU?QPz|qi6XRQfR&>EZ1ay72<=1iIAao!gl z=iXCdaqY-04x%}=Y(<*>tlU_^(VrHIH)W}5({50@Pf_Emkvmy1_vz}FN4%!arFz{@ zGv%Z<%-w_KloV$v=!Z~|Z<%S|Y2a7~>BkxgdN}R+5+GE`KL1&xvnC1ZF`O&)@+-)Gcq!xuuB9S0X>R-t2pteqfiBX18=s!G>_Y z1xdnN_B)8}I9o<`n6y`b6?TV^e{iJi5!y5A8#Yc0miLEe zI33k{;HS8^<|IEkcVzjj#3rzLtPbmdq8r6_xeOf+1flw@2u{ z7ph8+9FzeiT#-P8tS?i#BdQ^$h{Ww*F=6X>5d^;jC>JrKa`a2vZCP4F`(r%|qT)+p z8I(A**}QO~>w_{AcjCG6S2(!)!0Q0koYHOqp0J7jIN>?pqxj+UPbG(ZzH%R7XM90` zj$jS22XlLiS_ef1-*ioM!Q*00STA}&18-3EN|(Q&<%b4;8@@tEm^uU}c!LZu9o`^A zX?d0=!n9~@Op+U(i2*`#N{3pe!XtMPb%k4>*#6S)3<-sC5x+);@IFHe;)vLac7gVb+ zVy%FX+y_#;fY94b0?IYZkO^Ow#D_#PU~5k6IsF|@9#PExC0GDbVu*%(SN5nu45KYs zKy!crklZl|C;1xq4#gk_`Nhg`S}5lC++i0e&GcafLxzk_hVLkBG5d2y{94=Z+|x=1 z%axSnz&LR0GB_NUJ02Lc;Ywvu?Q4ScA)Ezcg)!G2B1)N>;~wK=y{3lDg{gpiV|7Qn z#pOEzcxTd{r1`A7Q=fO{Wkuq(Nu{edMD>fb`0?+_%wU!>D5zX;AqW)-;3!Ex0vhNX zU(=77+{)#g(yr-uoy1;VzA7=eqw-JnGPqHOS9eh-G-@b?^PL|t*sa0#ONj?=tb;`? zl3AWgQ;F`_s;d-UQw4ap81^{HPK`38^=*#j0=$C|aKZrRIa{?amtPS#3sAyjQNNE= zMb?g$oC)nJIPC#jz%sw{QK8};07-+BdV^4n4PcL?xNe2Unx(ja7Qv=z_StA;h(t@` z(NNC7C@e%oWn=;U?G`?^0-gqzf+ur;K~}LsU5XJOUlJ1+>uC@)ch>nl zTSAKzE;N|>ob6G}%w)1smx;CC>fI+tlBydTE74*M`xWyfEVkhU0|-YvvQ@BS*=1*E z51c1H+!>B81O@#;EpxFY;eQ!72d*%yDa90owz9bww$P3P!PL8B1NB1>hZm6;z}(0;}OlhLJezvWPX0@NORT*jtJ!^cR@vI;g*o2t`ZiJwUsBg)gff zZE|OPnxbToa;liDWvy7?*;dfZj1DP^FbC{!haAw0nvpCY1``va4NgJN+5Q4oFCb0h zt^a99;!%c9Qzhh3JiTHZ?tWHR5Wz2sk&=FEtvf)LAVL}ekqCQE?nH=)#wWLp>@1CT zsg*%F!$+?0Z2>!V;;{xXE<^&RS}z%8PcOkF{p!LGufDBPhMPC^ zG$q{wZ z#Ja4}W6245crq5zje}Y@*c9{lc@AzpQqmGuXJ~LY$*{`hg&Gf3P11|WiFee_O|b}! zVRY5AG_P@)S3`T7$B`vU`zoGU;5|1#4QY$XU%4+;XJ0S*Gf z^`C83$;j1G*u}-n&e+z>nM}^X#K>0cbBxQ`${65k4P9l~vmH4wj!dK9Ds-qvw$pf(6VOiY2 zE?B}k{2zUxzM&EhG6jZ^@X=))R&lRCJ#H4rUE-D}<&<(5y_%LK&nIcv={%BK0e!`un#9Tp#Xwr-Fflcti3K={AE}6#+kt{Qie|AZ6 z6*&nr;n(wh^uhJE3@XxoOU#BJE&q;S)ux&^y%En`f>||6x$_bSMn;dC71xBhpU~E{ z5f2v|P{1Cv^jl+$^NJs3E!XibZM8w%4kl>uy8yA#xpwUfn$HvbVs|_LMy>AUN(Ar4 z6ZtLFzwcQpxj;zF&-MnRPYxT3{|`I(dzBso9p=4TUAQ4of#Wd3q@H-0Gz8C6U2uxl#VXmC}x+B`>D)ffK;%ZXO>H zPVvNavG%b4+j~NPJ?rVff87JMOM5lOQOltlI~`eXFb2A)9UhlOiw3q{Ke>OF<`kMl zD=jNgN&(C4hl51!cB-wzNNv$JDl%R#CFx^wJ8zI;*wqhcfv8FGOLzgs8B8@F<^2`p z%)SN|zLITOn%{T>nk3;{6-GYt$(;vrEOutbF+({n^elu<|244j+ z86+n$mOkc15>j*V=xfd1B$*G_jnCJcV9-J8EZ4((lhmZiNJw`_M7fwG&8pHy-Ke_I zrkS&<(%!(i9Q}xb&7WPk`{_kfquVmahoIG>3~7f7S+RSV+E92f8X9;%>e3J=Cr>x0 z&~#wS|C19#Hq^JQmKY}+yCL3daSWFY*=wp%?jSI5|8X-huuF_swuyAM*laABQv<nM&9OUnkdus9i3(4|D}`eMP1@}Y5Bb1U(z#8*%%$T>s4~qFx5>;H zHo2s5PKg@JpAq1ZZ4ryNp{ihW>z)*VLmyu=cWSVjU!#O$Av&KhM`<{OsHeT4W^L$D z{FjnPLb}b$BGoEeF$aDxO-llzmVFo67b$7hXg_8Tqtl11I(W(^t~3EMSd=YsUc-tL zeLEb+dK9(xLL!m2ow1)kliqtx)H+c?rCAXtFh}k)h<{do_@=OvP_jjD3nLJIHX;cA zVfvn9=>eu_t@R0_vlV-GJm~znRBf*`LeMt24Wb(uH5ag1#POrx5gcU1N=^GbQA zX9vONEw_HE$REtCE;n>zdhek^PUnZ};@#Hm_lec6sYLgf#WB9v_nsZ5KeZMY7auW5 z_kJ*q9eK)**B@+THL8Vch#NR9ncS;4qP#j6})Vi(T4b#5_y$z z7?C9%S=An`M&>9nt=_&CMr#bKi5!PK%Oi^X!xk~)OE$*!pzhBbDl|3c_cJ?Jt|od% zuYTxQifMN~M*;jbwvtdar!}ipi6*ul!tJ)0=`QptvVjiLWO?Ld6ii1euZ#(56TeW0VKXYA zO;JSEAuLdOhiOC(zo^YHO>63rTdS-vZ#(9539=q3ZSysm;qjs%@UoRNo1fD+cYOcer$pT%eNH6nAI) zF#HH}KZtL)Sp+0rH3lrc-tc*6T!UfgJ4jfcO4jby`$s!NkCaEoshYG5Jo6~Z904c_ zN@%e>N*~A}l2(TI*J0P&&ek!u&;b12$=W|DWJ0HN04;s(4eX5ydQQ`7)_VOrV%JU| zAsp{6!;B$uFYtT>M{r;b#P62;8PhsNPB~ zDoO@&p=doKv4mZP-D#zF_D~qc8PYJQJ|xuo%cr(3q7)B2GZMPwDGIJ&zZi;fUEyQ^ zlcs~)j^o>q<<~(~Ioj!$ZboT%dYqkYXq&vL*WDjLt_ESAA*A_+)v9X4Z~1?D*Gu@I zNYE?q&aC%8EUc1@Gw-PszuMQ!Erq`S#kHQj5KwM@PRZ4NlK(ROXVva0&c~E!#qtJ0ujV8(>y;aKR3G#1Mf43 zs*c3YkGCB~5XCJWkhOHBOJ@*-bm(s=s<7LjkA==WAdsxiSCN_HG*VRQs+ZOv^y!x- z2C;A|nMuaXAm|6=uTAFdv78xK6bw>VseGo>i1Y#EWJOx3B56}m<5I*`T}qD9x%_qM z>9{{znOJ%GMVUDWcqR9C$0bwpMbQjd+S2r_HA|s-X~_nZcDoQ?DCv38rI(hSCE_ZV zbvPUoTrAj=%zqNQ7P^-Fp>bqVgI}m6*^!WlyGKv+92^oWZlrs7 zLP%PeYC`}14V}Z>{6=9~EdATJEHiIgFI)OD3;bRds~f#P3rA87s!!-^uI1br2CapZ z`1v@|yHda{pTH)AkuX@Swr8a=g6N?>VNRM z7dRL!$B(sDymlKemGkMDPE2d*y(`$P4}_OZoiG2^U!|m)OKnsrH$J?=XL-5>htARqAgN!n1k0v0x4yHek#IorCFRo7^?-1;kV#W$fYQ!QZ- zomxY^(n$ZyZEU3bRd(Qmx=%pGu6}>mQ28S?VS|^mSzr&Wfbtc!fa(?ZZ>1~p-zrz^ zzm3k-e4;KOo(bR9U`{KmT>prvOF+)a;9Ml_ou|vL{IM=Wwe`oeC6zehu8qmGfVHua z1Y$@hbgk2??zN>r8?u<}nJOl7GDqOU+A)^>wkuZ=$Y+0?aq+`izt9p#hof!8mlE^O zf~Gi`+8)>#I!~O!_k0@}6j5)Cw87lr9N9gq4%B4BC9m4se#V(Ln8hzIpyRB}YGS^g zuNz)bukTc4-C-cH9TGtxvp~CV=`XTDd&4S2E=a~QX zH34ta32)bdsH=6WJ#2@#8V6}tbI48DGdKfUvU_^LA8y+nb4GUQkR}LPxm+CNd1|r_ z1{{kl@@K!{B?`H_fqa2bMp=P_xGQl3^UVQO)zE&*>6|fd0-ij2&(}+rzuIf z5BCVJgPeH`_W2=)_-9p+r-e~Ku;noOyq)`Rpluve)JTNOUH0EkxO#^Pz8g7A>2|Gu zo_MJ?scrYD45&6ToEltGJj8>3)|>Uy;dJZ@3c-Eg_+sB9D&U1|zG;L97$k}{!5VLm zZTG>$Pkz}N1Z_+lLxbHRQ6so1{TgU- zNgLZjHZh}%$P)p3^Gekk&O5Tieo9&&cDwA6`Vp6H4v$08e1lb0n7X`!_x6ZQd5Ncr z-1or8K7tmVoT%EEwQD=~7Pr?K#Q{0Fu|sSC$>>4Wb1Msgv(Z1Z(3m7U zMO0y=!H*S-W8oYSQ1PnB#xO?}$Q)^p(#SI7QlV{J=a2?GYE5VN`98&>h?oe*R}ep{ zozpe2vsQT@R#sltkEM-?rp}MoSIFEzNh`e`A6Ph1sa~lqf`_P8wdR(|ad7+8L@kAF z;vhFm@833@Jipi6uq3Pp_bF!`={6RZ)_q3e&#G#EWcSA-dg~O=vK_0rWH@i|&I%f1 zoygC}jg8DWcewP#zZ&O+CV8OUQ)Dm2p4Bjk$?oZgE_%JhAOFZW({kXYL>TpT;Lzz_ zI|FZMvT5ZIj4~Y)tmhAPt~%q0DYhX1((N?ZWM}JC*I_>20dJ=5-SmxUPm+W65rj^`Sjpw$s`^3 zE*(gDcZAiVe8og}D*eTK{{60Jzb!|N-s5|xL@(8VWewvmO-}3iw=6G!_s9I7pXH&* zrdXkqzmYytJaFoVEQefFHzj&&L-8Ck-zIBhH1+A6Dx7TbAE^RAhyx%HXL5skx89S4{#ET7{&c zmPoAZzn~8EGBAIa)Vb6MJ!#GZi5MYbm5C>b(F_nXi)XRA1togzy^M087T#tVYDd`x z;*c=}(IpnMfRND&nI{v8vJ54n?8f4lN`3K^%b)}oat1TifJuxO&ZZTXv5pUhub0Va z0wwYURnZ6}Gm9@r5z`F%e3zeTCje1FB69h@e{T5iwyiaFBF^|31@L?}B2xY5NZ=o~ zE$(4v0{AEMu;!Eh>^}AfO&zIZILKE}6cHN{5EEVqDy8a~1SAO{o{UWYu(Q(T`PAts5V>@5aLwuP6?A4V6(t8AZ*csoO|B$?XQ9mzToari6>M0&(#_q-@sf0G2g@us?RlnK?i5>!_})FfdEnul&4?fFyZ!m znCK()B;nqc9yH<3(+;1HNFSx>BO2|cmH9_>Fz+Q=1y^syP5ZMgbdJd#BU7(9as%Ha z^HX%VEDCVvM$S*Chwpb+?xd6lMjE*fvLWo&C>YLzd&w85R^HGrZ7(kpVPCu?l0Gs1 z>hIk~pj+7mBThy96}uG6s>OMG6mD=@i)9C}#fhwl)Jyp^xn=OVCWhssK}rg8=eT@_ z#MM-!#b3{H*Xr$FEUim5yRH+?cP*`J{c|f&rbWvFlCDFuH4#)*;lNUt$}#2XSF&9v zrQcdn7C`A`pBI)gGu9`(w@al@TAb`ex0c_we6RkY{rql>Q9pi>PGM8b2KT7qFnaxV5b zmoEvhO^tU`ABvOe!>+KynhALJ%$E>t)0)=h(O|==6SCC1QdZFZD5R7X(TTm*Q7_hO z7=l`B@tJOngSoFD`AxA6D{dmf-hq?o<*Jej1-3o?L1`s6?+mT&LguymtaBrJyuUnZ z?rVkLYMuzew?h6~WR}&&rjgWu%Ol0zRpK~!e`c9{nSB|I6c>-U%w~d<3Pru2oslnD z!7N9~Pvko?^+^eupC}q1Sey*kNzo2lD|DB`-Rbj%!6@17B|U@DbT%ss`OK13)V3c zBwneSClO9vQ^N*Z%RXYO`Wr~pe)sPVHe|_LFY!-A<-IfJFyW4DQ`-%WQ$+9`xjvG( zpQ|w~wLPi9e&l?tir%<7e!wa+NTIeV($?_M8K9Ok9K|eg(1Gw$>)_r!@~1mMWch?I zlu47XEEFQ?B*b6E2Mn(`k^R%I5MNchehcs$@A>Qon=44fmd(0d!g;b+#n@O=a#iwYWb+LEvPA@*#Kw4&DzJnYfh;LQnC6!87g zdeW^0s%^91PAO0q`>$Mb==p<41NxthJ-IB>>x%WSPot3rFI* zMf_9_Wl1cS$EV%`sC?Jhn@_2EIcHtJ_h7LBu5E^=&na;`bMz8S&E_6(zjFs3RZeiQ zuRTJN2!tO#0FHtOBj@_b2Se=SHmzr0Tt=WHWsm zPs9+a0tP&xdv8i{VnZqpkkTa`J-)KLAX(5g`{CFP0HkK9R?;p};94=j88#urqEf@h zNp86`#tPiH=peJZ1GkQ~j!|~G>DtG7jQ3c|>9GN9;LJVY1=w~3+AxFB$^Eo!vtkY< z^lHsv3=oH=6dYkZUJB8!gnGuu>Mpma_%KKAHQD%Qw+A~YE zE7L`H=rT?lQtq`I0KgG}wsC>BEIza!{njtF{Q`O>%)n&}o3jSMpQUFP%j1UC+HN<| z%(W?wu*JQbLVt+3ZDuiiDA#YyF+Ybg*l!h`SyN{^k0hQeu)8@TkKFQCrJXjud)K0> zE{25F{XD-Q59a5JYP&@17qn_&5_&P?3hqsnwKyDL`c}1=5ZJU0UskWz3a|b_9B++G zN)j91j2Rf7HbdQc&*p52&{LV;l9GveK^#X>?Yyoup(pf4w|r>&$=OG@Y_VMwA6hl! zIwQFIwy79_k(kp+&XQW7iS%nnfT|GF1~u@KPe&}8SiTJ;%RF2cz}~XJ6NDb<=rK#j zVHko2=aA8x+I!P%vZ!O9)e9UMJ0?eeR#JpbX0d512u#wxBlv;hf62v?LqwumZ%wcg zHVp25KY-e>DBPKKKy-JtDgj!RZ(S-1&dd=Xfl&QQQBJ6^qysCBFAbkG_9f#dv+)s1 z-L3APDR&JQ*PJ&s9> zB@&43RN*^1zQA-|GKN~I4qBYTZiMEPc`j3U596%W1rSO;yzSV-svR6&RH9>mD7B=u z8}eph-j#vh0v4B6McTDb$}TryMb+$sTV5 zi}_AlY6U+=R!x+it_{Fws^cQRi&m1^#pnUclQP{S=|M!jX6e!UuBpP(5qVg`=VuE5 zSpDtgx;0OGi1AVvVZScV;hZR4>PKLNj0j~Daguy8P6p8aJ#Wk2&=#n`iu={^&Cuoy z-OsacXUkkO&0G=_vb3pgg0D+_3b#{KW7s4b3?1@R)oPF<|d zG_ke%UusA5tAf>hpXrV2XKnZ|oQZ$?y0G!zbdF41MIG$yJ~1FUD|@rgG{@}|75Z;9 zC`IibDim;0C(9(jCO=WZUxP;=Hp0PKO>Q?1=4@jTW27?wUSwYJ5=htt-^akbm08Acywa z?nLL@sHAx-9N~vRRHk5`7W$g&)+fS=7KXruHCEE+=h`IRE~j?$(+$Nuv|ud;8rc|h zjdgESU_~0ZjvT}PN$$DBE25Xd!H!-qq-$f;-@rXwG-;l9#g7}!%cbSj%7`g-jyxA_ z0$^z@B zu8A=6hEd*PVO0if!FvNKOXTxHr=b0u@#o{$PVZQee5{z+S>bCizS`MmieM)ykX4gZhRpUGL6F zOkE$%^Gm`Lbd9qfXKCCp+^1dWmdg-NcoY+kwC`Rb+&@P{ix_T1_FL9HZn=tICT|&< z$H{Fd^@RXGa-_mGD1nN-V{GI0VrHfZ-iIa5NBVY7d=2t7+GO%A8@~x-5WU&2kH3_D zqk`_7tUqx{tWQlZ-v4d6|80u@L?!?4Mp>n?rirVL^s#1|6k-NPhJuub9zPdcC}t;X zlSfrFHxP;_4{1f~)}Y-ZvKZ5b3;!(mc+UO%q3O5S6&}Cuz2Hp2pO&BT6t;!bgS)$a zV_9(B5LMlN&4d5ZT`tN%!FUkZm!{_`EP1t|i5H*9W6l-hV^L zx!qJXeRAxC%aOh`>VU)L$Lc!pX&4TJA|Y^ok|g zGfQh;Rq}&N2EcF_JpyGSyGxM67#h+Ah=vdzPjUHZ_san!2g91j89&82?co8PbaI{{V*nJH-6oY-Z7TN1S54VidmMQ1IuCPAZY34*eyYOy*dkm= zWBmKt^*?yxjMko^(;OB+>mxwSTDg_&Nl3kTd_i5(x1YIH)T#2#9z=oU?&C~X&VJh* zC&dao)x@Os%2go&Td7bn6)YQM?7DCgOVd$hW<_kcf^{WhDRMGkvZ{&qjlF;(tv{(W z7$>A%gQ_qOYF&LitAX_s zomK?d5dU)Ok%o9z@e`X9dtYzo3)In;lfq*F;iGLslrQFTj^L#bFN^{P8Tk8zAsf z#keSh$;y9iM*Sqr_l1wz=EFXba$=NjYTWp-_yIAkN(S$eb$CC-PN#PoowN+o!DMey z#1(8Z4#=6dGYIRbLJMW+NVx09_`a_oo2N5P6Z`Tkkoz#_$XUhstzb@kZOA5N-Y!&% zw`TU0oGR(@E?u*=*M7z>?Wu^u7Z1R*c26GLw>%x<^sLJa@s8Z>F+cnGE%Ai`xC$d^wpgSo<>ze4WIAUE6Lvdxh;telK?xt9P)*x!)dTu6T=j*xL zkiLe*hoAV9l5hLoLxsK<7T_|lg=&wrp z*p>*BX3Uskrs5!gzfdod;X7^vSzcbzyR-0=!S>ltmUOBo(|z6E{s8j`iup7Rq~vE7 zRnWHm0f!Stlaf!zjvNbv9ylRrAYS{z{=tAs9k;ZNLce>*n4SX8jOywN_%rLNaG}t~ z3h7z*K+BU_xjdJ`t2JLTP$_d_le(Q74H##t9LWR}SnS@N19=Bkcl~6^qYRq5j{F_{(HdqNhjv^v)WoRlgkB#D!dh)d)H`V7AzDMv^$;{C4^ z(Dq~@#uN*gj+&HwR7MHYDiPnX`kXeGWIfJ9eqj8bvQ2arlrH)hxXo0QSh5|MBTKeE zn5cG-Uw&+L!y!~bvoll=Czr{~1HZ_c!tHx2zp8bUQBFMx795^CHcZ}?I3aiRZ8Jt@ z_{Hn+8>RJw9-4C{0#Rp|wR+54)ebE0`@9tpTE5X1Xwi_`zv5^+*X5_|WJ80m%iU#! zT$4bGhj}sl7l<6Z0^tq*6CTg}-@Q72iy{Bz{wn^9sb^_OyU%K%z3+0RnnaOdp-_&A zQpL(UuCU2T_aYTHVh0pT!zd})&LdL+6U;(qJd1Bq<=yFVF^WpMKADb6Dj1$ITTdnr zkEq|WD~GPtoLj?PH)h*5-p)HVd?zkG0du&3gDZJxTqlEp5F{V2jX(sCDo9KxX{~aP zv9JUY9(aVBC`pL{5iA~t(Polf=)9)gCaTKHT4&*1Q6EEeIM(pMN8<=dWxi^di<509 z(Sc7PN2z!hPuWQ`IF#i9hKhwb)9IO*-DGnF8Ot9ttlIN585zN6DTZM(vZCYWiK?k( z7OX+Nw@PZPs(N$ve{RS5vNXIEVz8|9x=3v*9zwT!STp~?Qmg(NmI|Nik%c~5QgbqB zYEC2?PcR%9L%(TgZ6eC+%rKl7BV#Sj;Ak`*nMxvU=@)1JNif^6T!`Pdk1J#2sVZBR znwpA)HPg__PDhM$6HM5|rkcgs*u9Po^PZrmgIYu~Cg$X1z*^GJDa@6o5`#TI*T1|3 zznkgm;}!R_d3@?ilQRYNV-;l9{Kma&PfC-Er}SYZ{KO0|#PQyAu1iHR9Xr5GZ+xX1 z$YVe3p(Ocvf+RYOR}K zqi8EWh=!!)B@I*IE%9u;V<-m1N_NcrdL8g z?a`g{d?N z(w+7w)4f1)n_7Zi9{9NXYDO>am#{o);@PlG(P+lnkeTc2M^U1R`+n3=5-SaTeBM0) z%kNRG@}o6-%AToQ(590ntVT?F6@U)=&6Isy2)}N*L1f4m5LPgamROcTYv*(iPyZ7c z#oWFCg`-d6eUw=UClhNO#vmqk7d}WW7zq;B057V=1_yWz^`sQ|iCPKK-*76K4e|ht!@`_yeX!1BAATkU7xFeYV z1PZo?&s`Us8+@fNYnk8(bz&7v_8NI9_DcEqlA8O-SC!D9g9; ze)c@z0tWx5DPDXxE&%#5N?4|>b4aw8>yRvSSEiX0?vLOiRHB=2|NhsXiZGo^5&B@< zeI31A+X0#Tx|c~iFv?`0v!=blr=KbwgLb78Gt8U_OIAAE2z9eNK&!s5F3F0>=8W!r zKT;oYg44jC_`bW%@*i!jZbKwGRx%8gdl9{Hbb1jDI`x3IjAJZW5Ei6(S>l@9E&B&0 zB3*=O@#A7@kk#)a|5-MdEKD-rCeGj6t~5#M&W2oS;K0izF)(Eg#omlB(Rx#OB)aoT z#GwXoK_5A|4xhFvu3CMq($#~xb8~18q6z}|Mk(d{j*7ZYQanRcz1UwW+(Xbs<`luO zHb8f`LI0u?3T)Otb_0X6$!xt|`V&k)`37wFO)&S%>7x!C60RXywvpkR*hEEuATHLB zx@Mc;`Zkyu+td&XI? zbu%d4p@UVsAW5iTL@C%3XR+Bptl=TbDEL_lvW3tV3l)rQ*yEL9_5{2}*ri^pn2SG} zR+-zw0QeD)q(v=8w55$|>$m^`e=SRmAT^m5fBNae&*Lv;slWJ>PpPj@Hs}8)xC)6D z{+kM@_=jba4xHOwYq(92K^_%!WFTeunUd}dMB?$5o(Bjbd2zGrme0Pwz*zf#={HE= zk-#G(=Qp%0W&TPr?xACqCk52iu;mm2Y}17p~)Pp;4!j)g8pxkGAfftTfDxEj~L%JS-YlQ79DmS zN^OP@{~`ohPv?81{MqY#@>z!a4@vL8_|AX)S7Gx{=taWH*~L{AVEm8Me{X*6*Emr? zRYrPOpr*5hLko^{?~9y*>xc*tZ&YiM%KMfA@nN^p#E|?c8W35t>GBAcZmA?4{UPUr zmeY-OaEd_%oDz|Gb=lAS!M&m9W`6(rdUJ;x06jy(gJfSoPLhvmgsi*@_=ffX5ej3s65C6K;Qq$m8<98QKQ&(2=PnxU-p zy1o$8j9+3oDY6_(6~00AZvJDQX{iOaWATzEh(B-7G*n?ii^k5}^sObC8mWZ$GqLO` zFQk3dGhc3LgXh1}46U4`@|u=PV=ro6Gk-U&3KzERYKq8iQ&`M{ z66z)|kDF*;2!t0`h2%3jtiMmCM!^ZbbEazf%%%b%rN^OWL#s=lwAd}0e;=qX?usTA z9(Zn-UmlKH6$@~yBkPop@gA+{^6&}OC$4EF1IHAN{w%|uvsCbY>|1Y3+n*y}m=gfM_MD2y2ybg5Ee#G4-0q!EQiw8pk8 zajMzrRw<+V4n|~tR*qNe&{ACV!QlqG+Tu_laOhYoqD#AJ;#RB7epfO@XP3?5L=4w| zHUPUmS;`H7X9qE!R2UvMsm6A;@=1O#5XSU1sWSQI@4a zZGFgOeXx}tmJs?=@*}5@_Cw*EWqjMYiP;ArX6+xYip?F}`38=k++5@zfoItr7BvNp zF4AQz;o;d5e2Pd(OFTD+j|Q|942$uF+L(@u_{M20MhtWi8oj``eZXbdJ;tUMbs@T5 z2y5LW6wZ&jO#>UCoMKMSy6g6DP)D&BF@YE9UtKg?xrubeFm**3WxIPdoUuJm6|>fa+?m%l%uRVj9gvr3LL<9h zzwJCHAAzE&-HEze3O~GobD}0Q8+EwwOWusWqu$p8zx0Xc)rsjG`nO_2#mkonxKUW8 zdT^tvODb;w?|v&f4=o3rG4P^EMVhblocIjZ`>hvC`9QX&{`gG;d5Q(*;i-d2Xpw&Q z(C@{o(K1N_^R@FKtK=F!$oRG`ANJ|~1L!u@kE-(fHSnoz^B9DTIMV%qFHDsLJLx;a z{kiDL9o$beEYbKDFhRicb1(FhJbGP|=3Wa8j344(w4YiN#2MMp;ozg{ZV|3@nlHrC zW^uW#Wd@qdwly%Kn#Y-3@(E1S1%~fg$8y?v55Ejv(DaH8Mi2lDLbwD&5!bxl1li;o z(LdPNVw+uqJe!`sO+I-1;BEVZO!%Dz_O@S66!?*QN}cGHJ0w6VOK24*rD{2LcnT6} z?;~uSqXzkQdoCHMAs~sk5Ds?W8B0!Ldi>wV}UtY5jdD4LGbGekgSgCxr;tWYlL{X}jf-~Z+7*=_Z1Km-EIkFnc0w}d*@k;T?0~RO(X-cMt?gUsdi*&sn>-7~!6{jts1NIoIy~YrX86%dgI}?$~|o75S{0+o3V$9hED;=AC2cw%Uuz zn%c_kE}cfHoSWej)Zc!aoh-n&ZK3_#(~$eJS8R2BuOn~A=IX3_35k7z6YhpHcdy?T zKih&CDm+TZQ+|d2B7GxKmyr)L^LpH%>r{7P+NA>@T2c_uw_wh}K= z{~#_+Nj<<2q>=ewjhBlt2DB&B#;NNHLLb&fj9u06uW|Ud5K!YyMi_OJ%*>q>C92EM z;>IlY(CJs-@UI?NF>1~-TU(XGwu|5~DS1{Lf9-8?OV3s@sIuccBOP*vKf>i@a+@$VGIzJD@${J?%^ zbWR$Kh@|3gAi3o+$wOkin1d7AoX>tYxR^ft5(7R*bJfR)v>mbg6-;nitLx>KfB0b0 z^R~_tVhPem2#B0P>L0Ca+st1MG&OmIKG0GA=mB{yop&crMUe&u{f>E@M9R(+e8Ni% z*kG=uijDODHo=eQsQfCP4ijs#+ve{s^Ck58tsW-rT2IDABK( zeZdFd?BB}%F6P((0YEmP3v&Vnlj%yt>UUG<0=6c-yY4qn()-Z5_dBePVW5rSoXDv6 zv8I!H;5&?F&m}_q9}C63GW9WD8U(lJ|8ioI7FNCX;8Vp}8QfcR?|g8Q>Enk2oF z%&lWU`bbvMjQq9e!|U7LrSj=juRk{#iT|GsM%2i~OxoVX%-+Sy^;6eO^>gme-r_S3 zb~O5Iyma_Si+Yi&yu<7#aChR<4D%Ji3O83tM<(wnUtt6^PYoRjhFS$ys_g$z_7+fi zC0Q3J1h?Ss?(QDk-3jjQuEE{i-Q6L$JA~kF!GaT9-`9W7yzXXt`pv7g?&7i*wd+#% zRNYfm=j`pVNwQiy*i_M^bg6a^-)2XN1Tm228%TlQ(5#}Y2#Ex7J~7qh&TQN9^zalC z1H^Vo0E6t>kUAp;eRo}NlV8|xjI4spihPIp{qy&vUN)h8%} zz?D7T5Tc;y#e*q4HO2E?Jtj9&@8CVOJCW6!pyTmRco8Kv0Xe@6$Aa0@irX*O@&*?;0Xf=JVLq>VInqATRQrg0KFw6m) zYg7;|g=VSrv)PxGi8one{g1!M%v@sL?hdjIV?Y@vbPGfEogW)9_IE1kkDEfOO9HE> zYwdcQW>QETgH6=aL}R#kOEDiOF+E%)Fg#=%8_Y}-im<;Z@9{>u{=gWSNna4S1xp!i zAp$Z{_|iqq(#N5J$R*J%UzJ5r*LjUrR#bPJU>Hs&SnMxaTLXxHH(F*_2V~o8hA|nc zp3>%Gs8VfFxr5*6ZDUmI(nJcX0m( zYBNX@GlF#qx-^JPA^N33M@fAMI*Z(nd!S}V)@;#^^kg&FUafSD$R=LIXP^A9zF-U( zH$4Wx4}3%f0^fE3yj8TPNFT;nA0(Zw3*4 zrB&9mN&Yb5^O_1&=JFLH13`qCvwlv+Q_`9U>}z+ZaViQ51E_P&%67bG!@m8FJg-oA z(H`d$B-%*g$70WK@hf+v7$rs^YtUhvm zHNWOcwjm+ukW6e!ptxSP#z>z}0xX0Yz%+@Algwn)EqKbBhT=UeQ#cuNu`WYx%-Bnl zt29^>_UO?mZfPJheZdvvf?K5wkq2;ys>AL{1du4}apz}9PKeB>gLKFs8-Lt6Bk{L$ z6_P1=jn$8sIE!1$aC+3U=C6J{O}hRGCFHD#Mp>QK-1+@Uwp=uSp5GOs!tv3$z4&y3 z{EkQOEa__=H|_`ig#*(ZW0Wi69Q?y&zvXY_2!~9&feRWFNHTC%-zzibWhC+w#U@hI zPn2l0y1fm)%pjF&8K(9JAIvA3Rgav1vQg+`Gs4PJC1TCRjP9AgS>CotwJrypkL;^-V)FCwm@eg^K46Nze^kOIrx>Xm8;V1!@~5 zjePDRBu#2!$$GR&S@dX{ss-0edeZ{El>0Y0=SODhhkB;oX$+_ui6vV77$DHsXMPfE zpR*zx19U6vU42UUQy!XKeNK4v%ToprR+MHPX5+y|OJ~`bF`8_&k6Do)wI~fqtGDKL z{2q{jPaA2Ru{ZfTn&gIx)Cmg^tC&`5m5aL?rH34}hzcMS{Dx+q5~oU3J{zXzfQ~<( z?vtESZ-7w3vlkP#kfY<$ZR{|F~eYQaL!%@WRn^)=9Suhl8TN zY)-M#liNT`Tnt;$%w(1( zg}2^JS8f-j6fSZtO&|A5Gw6M zYKO*RxVR%@k##Du;j)qW1$B2tW+d5e%ZiNjk+~9>xOq3Pbf*7D8PDDd&M9 z{!%^(kHTc$I_nSki$=X~yO&{Vq0%Nb4HI))Tv@YL8z`rpSTGZ5f&_?C*bE^|NvfX3 zwMCad0|fcQ`mPfyF!t6C%~Ym3r?Se{+nAksT#IeQYvRYvw7-mxkF^GUjR#v(Fh8Jr zTnQ4)2a?$yLPQB1#DMN6M^NVv&PPNE$q*$7$`C_<;SDb$IjIQ4L_m1M7!}bdpV_h~lgB{l{?ze1J5!l0w-9X3U zGyVmIb>DbJScwTXf=NEc-JS0U+GF7EKz<#3I)kF(Jx)UwuESdYv3k?^F;{QYK(j_* z;Le43=8!W~vmPBsWDrleZqHsB`lL4#S-mw|pYQ2VnS7rKVF!7K3tGhMCss1ANZ0nU zwoV>GTsCu8lS_IU<>BWi2ILHb;)FaX5dqz}t>FN2dc{E6-B)bGb_nMLt(z~EV^Bs= zzW8EIrp^ij$lM_t>IEE&+E%bQl0vl{xQV1~0Zg(GqH?nwQ-%$wjU2jL*jfnIR(K+l z+rFvcKjtjLmwaD+YVNR18KQj~A*&|TsN58f?N z`sBJk#VpbL3`tzVbfI_ekY8p*s6phlB-CGkhdUCw=pot+$OIls^wlm-E)yp{;YHQ{ zvOn$l)r#42pH>%Ie~Pjoe#jk!1actbgIwzI}$(lrU6Co)9xQL(kItc^-ug$3N+ zN)toZeqHnQ(ill$2%O4%yV~Y1LUIV#M`5&emYxdJwM}HOB1(RpS}(zpFc=NJ*nq0z z)Jzl-ea6fF%bWXhv}Ne7YPtg2fMEJL#9LbfE;mTtdt!+AFU!-vZNJkH0I@(B28pvLecY{H*DArFRNkf%@R`Pa}@rm?Qm zZlL8~M%iA^0(N482GD(g_!BSJnkRszhLXunIa>~%rwmsBVQVko3=ycfP$*6$3exc` zRdX3!im3{wq@+o^sZqOV0sB^-$;3OUh8P~(qW?EyPRz80IZ54jFgA+9}W-3;&y@QUu8Qnb3`fPU#*+ymcX zqURlh7>E(hjLDVwT-mLb4{!7;te)HK;$drFN%uKLHbuLbg&+i%WY4j#~h|Vxt1INLW8So(L_McXXgO7AHCm2>eK`_a_wgl+^ zMCpgZ%Bo%K$Nm1|XS-Sqtu%Gh!SHo6Jgb}iE*?>$2Eadh8obE?;t(Mgun@J&I3 zf$2cf`-~vn#gk`p^&#{;hvUtgRhBktk9~HNoIsR(L^wB@LWC_5V)}=fBL}Ro}t*KOD{~mH*p@^f^;qsG_zZ znn3sJWi+zt(UXit*ZmSoD9e(j;lFv-%tifK%7%L;XNUeG0-ptuHU76ChapF)-ndDW zFkO!`&V#mTM~~^Y(`nsJUmywt)?khymcv#;wOuS;0Qp$#Z0vAhI3*kvG?fXe3Ckmf86&t4znPfK40DOkk2q9Y>{k6doM4N=0G z@nYkzu9$cx0o%P-$f)4PlhsOfP?$?rE#<*(LlrXNu!$#FwyLcRMduKx8gxQGN24uQ z7RKn%yEK>g==N^l#+e2*6S$)VT7!D1m^;%BwG(Jxn=N9=*Fa$V<(sd=yZ3|0TCjrZ zsiiCGSS~XOCq#tM){+X7mllexaghdMP}^4`=vsGnjc;f3n_p7T-N=7L`KdOq=9^Sz zTn#8{gU%`{i+zy5HD#$Tl!;Mf^tgGDpSUTzGH(1$W2UlkUJxtqD;ghak ztEOJQZkWo2dC(iD0DmK^=CEd(%5VG`lk9EJO{J3Ii$0Ir3Uk8-iV^(6nKu$i<`Di9r@K zFQ!;FXBGi`FBD|75XU1tFz*`bYRQEMc1qG@Y5 zVvZ@gH(q(_QzV1JO`P#2f_umu-yH4HD69&ecgz5v!RM|D@9Pa!3yXL^8N#t*Zl?&b zuOhm4TvaN8LwIH4$VPM2Tmdjfj>@8$ulxr|2)I^wizpB1V}|JnjP(s9Ok!xGhqiwm z3e4s^PrZPlPz4wY?ElN!>-VAXev2UK--BRbMu82ZX3R^#ehfO2=@UXY`W^~>E;c`Y4<6|DZq~W?QzYtE)dOD zkUxtF%5{VozKQV!Wh_HYZYUUL1XD5!$sk{tF(&ngSK*=ZNLEZPq3N&Y8L!|%JT+%b z;-scI%&^MR8Mf@$o@?HQCmMyAelx#@(; ztyb4)HG&W91!+`qTB_%@4L5f*Cz)9L*kC<%1Kq7#@mw8KI4RiM7FHB;)gGuJKgjW7 zxKT?n4Jd?ciIyc1750xn;*Tz0nVGNst; zRbA|!Qy@zaJb;pCFgVf_mU_|3OMd(o5$o6n;h7UNgVJi7b8=(Pg~3WRmp*$vT9r8aMf`?_kijY9*qyhS?hiFHQmAhqx4k zWTMe7LXER#MdLvO*OUhM5~2F3*}Q_IUHXAPl!1CEYy`E0EEEo({YH=)>83LYe87)r zxkYx6J*Eh4r(H@H3Ykd;yIL6NvOaNkg)YQ!Ao>n7Jo!=HHlR9F>U}JLK0>o;VbU1F zjSoBkSsMg>ke%s0iz6{^rf7fCccC^S)F~`6otj~ndP6RZuHi7?f=ov2))KFmw4|wo zKi0{q1G0-V{{Vj(dO}3+H!WmcHQOq1OfpXs^}*d(f=<4Y#2k7ql*Zcu+AZ?r-KfZh zx!NxU#JCmzCvVo@pHBUk&4?sL?caE_cpEetj>v{c=Eb|M=1>YkD|R9ZA=%_LAvMJ> z^K280mSmSE#!d?F(VscJsjhng@%%{VRv!e222OY~xm~AuQ#{Ys_@BE$>>}m(n3gWK z4f=&9`^kiE8W9b3_L%3NJB9m;|k zUY9SQ0b_4C<$S0gLHJfUt#9bsb*-epuUg281#OJc#j*nO8Ulf+rvHsmv%I#g)_@UZ zA6u@t+-Se15m7})tPc_%;M**jPb~6TtjKV%hrr&X)Rrlb;~iz+Q=KZ7GiQQu>jO)T zc$6~Z(04%xf1fKFKl^lTHu55(Ww4aa4=rSkH(E7=?4sXIgTsy7_H%}ofFz=>@eY1U z7aHe>V*JeuS`7tVB-BM6Y-=N1qEh9Sb9jZiRGq~y(s3_lM1E2yvYiw6%b%$XXmSND zZYjx~au4{Wyc8*UzYyIQhoSYu?6MGw)`@S=2L)%H^LZG=HL5;&!u7@O3TB(wp+0q+qbWt(23#?l3&o1 zdu)^dCgS(B6leE^YS)++mSC*+R?77Tl(TwZdpiYkMz<*piGX(~65AxVH>ir2dH4 zw!4eGy*tK=6W}CKV6qad6P!YA&$_h0&g zCdw1q=PKJc`EAprZSd~;!o5J>Qzd_uE_ZPLB(0ds0}nCsyIg7>zItBRcMgg1Fv{7q z_%0m}M{gtR_@vy1VGhB*RIX3oQ~7{aQ_5bLXeG`QUI~kH6G&tAC17KHS!DYOs(}@e zjZ^1@34@$gL>r_jto3H@gN^8%L!;?2UV)u|L7MBk#OKV|L!MFxN7H|u(mGM_5p?*8 zpe~)nbB)n5x(n`2l^E7SW%GS-1PVAo7BQ9SW8Qg|6FTuxNvtBHqN)?$g0xP-R|!8W zX&HQhW&VulO{VowAzAQzgAPsvRCi8b!b?(yFr9%LzR{&q_LdS=}sc%(-pEdt>W z`Q(=fEI0z`M?D~qeEY%h z%M|A(CwGf(SLYj~9%2R8W87@sxR8*JkU~hf*j4JH-k4=P43;Do8fN@)vtyNSeN?d7f@_Ht)J~b(8)&nLa!yS6wtuvge+wlA38{lW$mYA|j@a zO+xlW(qgSL%%aKdybn}^ZVJuuMw?)*9mztFA9?sma6BLS32e*p!iOrzcUospllr(l zLsW@rTs^N;;G|$fFLy+P zQ@)8@UQ9V)`f<6HE-w);J%yLot%V^850q`D3`0W2E1`#Q`w+krMzhG!{}j8+CFunu z#e<5d86DvQDRGKsBSz9<7s4X@Bbgz%J&`%We2rL!6b>beg>6|4gNEt=`D#6a_F9udtCDAgC| zxg}dx+7r~enD`(xecQC#)^=YIuAe!c0jYMi&p)76BQn}mY1YB-7|<@aq;nBqU(~ zohC}+GxO*aO3n#t4h>#jd?BywPK$lU9vPFDVt=@~qbQuKhD}{y!W+zA%_n zRyKgcE&l(-tW<0)|KVt>Q$X`bTscPqxp5f~6#Q9Zu8N*PgS#zBahO zJ)Lp`xv!}r^tbwdly>??MLto;ptM6!qld+;pcS=)6`*z7S|Y|cjNm)4UVl~{1{Cnv z)9mcJyt7xYW0IxkA8 zwU&O6-Yg(?*+-bHe^1dctyH;7E^gG@C}SHZAct>iCHqb1GR-;oqF$+R=c~w=MNwl} zd(1;|Q3N_Cm`#=ABFYm1#%*>w$@d=Qr?%6MMtmFhV#7C5Qy9`r(BcDE%&)FFDJfb7 zir=kc=44FSC{C6Vw>|woBNy*OGwWMuv?G_`z!^Fo z;o+>ZdH2{gRB|Pe4CsX0j_c#(R*GYqlH|qX)A`Hw-4N8%a&_ zRT2d`|4<_nrg|zKT|@ES`7}E;wAPldMw1uL4Rgwn;nV(y!pc+Pt9{6OPh9nCKl)fE zl?xpABa#bv{LFH)IUSPS{5K-9A?{p_LL7S$!Bx^G7sM5@#7wV|Qb@F0Wc%BS>O$e9 zB(Cof#Zkt?@I5Zk$~V2k)5?w(DuZ^U-#CM30K|shyQU11F1d;ICrrol z6P_7Fc2a||(B4uTIAm0Gh++aUGBmW{seRw&UXPFpwH6@(0Vz=Z2Wjo!F2a8Iyt6di z^%Ccs-m)gHWV*bp{D2B*5RpbDfd~cFL4?61fCBW?2M8a;!GqH{m=SlPrL-;b7K*?u zEzMcyEsjNj3YMs~MN$+-cFd?Ic-CR2+u}j1O5s$#@P~MM#DRKH6jMuni=T>o7{E?l8wu zw*{w?1xx83{0~A~n!#sP1YEsY&rzNcgl~nRQ%RgU;E)DUJ~RK)*?ACjm9MQn_DhKDok6 zvF6(5V$|ZsGm6kshJ~^>Wt1VhFitFY!Xh3?XyM_9gYlvV@@L}!EbZ+Cvc0URVypPc zVyif6?|K#UzF)0liC?UKNi=9$F%F=8(yM|DIX$eGCqQd3^slQ}-R%``WyFIE{+uG> z(gcz3=SE^N;?n!W*e|t{2&bXHPLIbeYCT7s;rq7ifhB5WH%|vM&N8kG+9GH^Blijh z{D8I4O6zWssRj(RsBzi`Aw?;){=M((#5~y4v^>F@<{o5fHx-g~l|>Y|rl5<8BZYcWt+fh+75CVbu5enxhdg;B zS8uzR^?19KPi)^m@aEX-Xkls><`b9u(!vjYSQTW;I@Cshh1iV%t&abG^Wm;uJfiCQ zKo$_<-rT`ELLBtNtYxI0o+g;5}Z<-WB!e^q9=7I@Z$hA?}Ge1+_0ZljRpD2ub4x14Mz zs7Ucar1@!l0-|Inr6`w7SahQ)8VqQJOGT!OSVFam+PtvKaYH{a>oG$`3y zMAJ%f@crm8;m;>#Ov{-XMY^7I8`aY!oXkuz-73AQipx#2XCxh3$dJxF9p~rK3ahQi?VPCCNpUK2z1|1{~C=jNsdCcTxe&jfy znt}=LFkqw81hQfG1W>h*HB$a0cs!;;7-FeND(S0Zg{h~A^|Pd|JNignb+El_m__!fl2 z+Qw*S$5TPf&5|o`e&)}J&&5L|e%}Qz7H62tuNO0047f6u>LP-m;Vi|uj6G@jQE^pE zs+;gc`@mH?One2m(?J@N*!T*;K~PHjQ0x_vq=|N~EO4bd1Y8rb!UnI-;27$xy7?sR zey1?cV&Oet0hoR>`7Z=2HnkmW~*tApcum_s%BG zL$t$I!c`*aW)eB?1o9`Y8=s}7ufvcbp1 zubAR>eS(8}qlihCh7CeFgkq>KjA$_CO-KS&tOy1&D|HdB#^pLDa6eLYII1|W^%^3fZmmW+cU%|O@fZhQHglOrY=~QiDD-A{L(!joMUy?i{di-Wt%SbW;usj$Zw~C=kWj*P8Pxo1jB;w z?hT2c^q$5xJ#WiHHom=Wt45b`{O9oFWS4o7dKpbGzyj9KlYedl;Jw^q#TsRn!yZUo$%Vf7B9h4YgHnTY9M-UJZk?{K6;Cm;FVxW{htB)QqiR?#>r-XUN-w1j26pdz zXWR&lUJRIwjXnm9MiTP0K6$$`_-~_m#(225n}3IP&ZMr-FtNCpF{e;ZKQ-e!-f$0F zrEn?pi1q;C5(>lCFwQCZSb(9+6YqhNVx;2jR)K5EJ6qCqG$%;-c{`EaDCG05HJ9|! zmk#k(LL^zdEpeGNmIB$M0}GXJ4nECG<7i8C8xyeE3uc7{-a_)H2|3v}KZ*Ur8_Wa9 zor#E^{6w!7W-WDWRI#DGq3aoVrLkf?{9?w$bq^APuNED+7jWRnx{I4CO5WCJ$lzz7 zHnLnwM1O31N8AAK!N!EMe_b!>7Bs`cZ_z#X%D8Yi6b||2oOh0!<b_~5R!$;2kxcsIITT^RU^G~Pi_}lxBBYK07*XZ|rS1TJ z(vpT}U!Vhh2s)6hUe5BLdlX{4$%OYEc$@wFT^ToS-9N>m)nd3`@kFusikCNrb)~j< zLdT88w&;%iN{%2qLgIc!?sw#z+9?7#ZVhQgj@WMlzt-d6@r2ShY>v0w0V`6w!z>@v zPSaBJLldlq?gIUU>qZmf|kw*@C@A4IGmWgF}&U99xR~zeB_**D8O)qcgXP2 zV@u+V$ut~6#_@9o?f>b?&{0QiXUjx~)=?z-|3h@J%bqw7Lzrd0w$w!WT z2q(7WIs4h)CX)9{952RVq53ep(`bL@t?OxNJ?=Xt@zHJ&N(byV@RpI)i$7&mzNfHaRwbVn9q9~{9 zE<`zqXl+D6&&!owK6tN}@_g~?rZ=Zk>0P(*@CYd3Y9UZ-tNe+u|DEbp(FJuOHH~O8 zP@I|6!K2^0?fblEK1@VeL}5jS`nlkxo(Cn768>^za5XbCRXbzDjyWzNRd%)r*lH8T zv~X&;=$rwr>W)M6F=7w+$pGr1FtSabXmLN;(7FjvIISC=+7850IQ}lxb9f@Y9`)4(v? z!S}$knJ+s0`b!vwKe=w7nD5Hw1s2Sz_b&9rDb1adpk*0p`S|~GknJ1S*X-i1bxzzh zbRz_ob>t{u=%;YR53Z<$mz0LXe=-|-W#M5$GJ!O02#*COIx7f$Y6xA5!0R{+jg?%n zv9oCq%qC7%(cO@D?^ro4zeRC_UJFT`1IyN6-3T{w(TNp8HaXDix5hK+c|sj#5c?*7 z)Pp#rLiVjxQ(swxo$lo4OKBy2dC5h`r|$d11PS3D%##ZDa7#>5Y`34-m|&8dlRTFa zkt7FNGW&f}!t&_bUqOc@4u&XDeg(qM^feW_rG5SiHH~~z*4`LM@@QkiM{#|_=&I9O zaV>pSnU#i|sbI>BdZrV8gXK2aa}2(rNA0vaOuzYa=-3!78~1Uffqfbw`}Kb7vgTVAvYk_m!c|woPx# z;oQ(i_jORr9?CTjnmTc5F|NcIKQOL49@)mXdXpzuN;}*KoLFpKq9SoplDj4xt7@Hu zRnp89#SH~T6<5T&Da5`|9Sgj^u|!>!njWVgYqFZ1zlF%R>WNfq;fEqjl>d-TWr4si zs`y(iStaPun&V&W9HQ<_BN=N@VIK|8c_SC8vn2+9Hbs6yAa@8u@yQpav^PLAG=-ZX z>S| z)1UD@yv2xpBl*QmOs7BQhfD|cIRasV_#;8`u60mEYuZw^0e6Zge{{D#4))p$Uq=8w zQ#8LIqL1)bturpfbBk!!xuS@Tt95VQfeRWzl$T_CRnUzJ(n@5P9QH_`!hl&F%Uw2$$5xrg|YA zAosxu7#3bR#C%EMK#k#&!LD5T*(U<44bA!HHPYV27@tg5jX)6p z>Ciag6<4-9GJlimunzNDg>_>XX=7Ka%pR9-uC6Y0MY(qB8S+h5?uk=&&7~6Y738hV z-j?(=g1k!JhSDc$(<~yHf$z3x(NvW4ZM@QGrJ&{^ddk^m=f{PkTtLePkwez+_qS-5+mGxLRRa|BEPyr-P zFB_TBc1Tu^Di@A;CFSM@}5c4wSMEw4G-a+7F*HY$+#?UTn zn)I$BNL75_P*bFGgjn(6b4!N4sVNAuo);3_Bcz!e2{yvyfVOypHm z7h7+0Q%0}IwAdq=vu|+;Sr5CF+~Wu?#kPDByvr6h&~{U1Cx=6_8;oakt=iN27Cwg* zF1!%!=a>7+oQ|oq^DAQ4&$Xm|qY3Fh=*<=x`26KNg^tz7UoE;Q3r-AA4jN(_&h>oZ z22V}8Lo%~YYMe7#qhD?^@rPf*Z`td+!;brxHz$1PpFXc~wkEw;7j|d89Ei7QcHDoq zJ$rkXwcbE;2J-^gA~pnUc9H$(Hu3+RH5mOXIsG@zz<(Vvs~zj&sA2k;&`;D$L(0?n zksXok)ze6QBUu5WO!_tu2n0}XBAGu7%%Vx4<2G_d6S9=~T%~#LDpR#s?iQ9l2P%1a zE92{P_qqEfN8a}VEXUErWyv@MynCYKVB(4Iz&q#8!R5{U{Ina0Ba~lc#vcqdCz9w( zkOhgo%Af&?zUgJA8&A!Sl7ccfH~rk!Y^!Pj`enRZN97JP6(6<;E?WLln3}}}r9crpBED>xpqWg3=UtWLP&^z{^p_ahC7Rw7tz3 z#oRE2>Atgt5NCPdD7rDSGNsz}d?C?aJl4O*%?BZwo5^TOi$Mury3lHIaJ{Ydl|jtQ zW-e(fG7UiI*JW-Ab5dSlvd|cU(l{W6BD*Xq+nve?-abtU8Kq7ssYMbo-zONfJcx*IkSvFubJA6=28~V^^CZY%cW9YEg#0diCV% zB%99)q36QH)1m5?l3G)EBl{y`VQyPy@ZbXxs+iYx%*G~fTrzG#Gv6;7OL@V%RF!Ap zLAk7CMTWzaN^60LKvAoTCHSaIn{FI)HRxn(SW~5fWXh{8U2LCZ6?b$E=fDnenci&r zC1_1**l5%V=`n;fwaI5F=9H3T2OW|PdY+sQ`%7EG3U*GbXk9vL(?1^!W>^QQS-&1B ztyi9*?Q4|aN+3@LH$;exFStpl#Hgo5G7@W`FK{!fdQ7M@FzFz(KT%VQ-}@}(`+B}i zU&FsVljVocSa(nUoDKH&n!PZmSdc%uKdM|>Bl?2tK}Cu32L@nwz3~6lnf@r! zM}L2~(GB$)W5;TGg*JU$iXqN-c+JXXj_SZX1f?YHw-0>}(q|4QcEODFRp7e>FaLP- z;w4G>YHuC4>P84<|CjasMtO#liCo^ zY0hJ5iYOr{NgbclRCT*cfpb#4DVupU+s_a1gH9%D-amPx3;7@vEJaD2_(gTPVZv{t z4%{>Q;zxhqApxmZh!A58q|*9?j@KV@FJ=@U+Rq`{p|BIPWgq+snVqN$;{O3>80wQG zK3TZGQX*?tR+fTf31tg$qila}I3wyV71L1e8L?5sD^Y@xe^#_h=M1fyN^ zN8)cDSm_n7k;zAT{;;LgORSu@NCr_T{eqE@m$Z!=i46W9hZ}{04>{&{xo{8yrYB8f z&#BI`w1u!6F1FmvMn>m8iC@q-+Nq1%eC+eo5n@@c^~Cfnj)(Kyt6p)a=y z;Q~%c9@P;65}#?~e@buO&}@*wDoe7Y1FtK_;bdt3vc3gJ&pr7=Em0G@Z9}elWz+~= z14WFybXGKEz%T#YQ0LOs^USHgr>K4ho!dOc9!XxqEgs( z_T?66y$W0I6}Nri8{_&n%=n^B;&M+gZC{!2K4{5BY@-Rv+iHOar1k71n_-+DBy`*% z3r;9uF^ED-L<-lLL9!ny<8BMa^>R!wfg--vXT{PI>_OUYDnQ^5mEC{i-WXlSDj-;=LKdg zesdllPgSy-wnyTZbJf{Wag0hCkI44)osR$e#Q^-p!%qR#tP-7 z_rOGa?0RZn0!uwbd8#s&=!f@ zROV>B9%OFObFdYv=r{!myU8WFC3b95T(L&Olx@D3QZ@|i%Ab-uRbuH@;Y#{)phjJ` zaE=m?B!u8SP@S@Bwe4`4X(=rag=GO6D=4s8PTFiTHVg?gm-pYFpzrD^h=C^6tk3po zSI2E@X|qiiTsFFK66$Aa!$Yu47%Fo4rOEdnH2bfG*MA5UOO?fZnw@T@n!mvKg@s0v zH}i&lPMMf=BcnqIzbY3Kd=^RV^5Hz$yl8t&frec-C^xY(`g@NiII2%VS4E$8`Fy9f zR-P|~6h8)>^jGn7IxdlKQ5>hE4x04xMjsVcfR}gp5_brRET2MsL{1uVyyH|Kbp5Fe zlxM}bX-9@hub=KgT5$|c1J!2-Z9~uVPZ7eJGQY%SNP)xqiOgU3 z+ifY+PuCOD=v*DDn?sUkfuHg{@=A9{wNC`RjKW++>4ZPR%6{a{N|+3izHZdT2IAw` z_=kls__3-{xFmH!7-TC7Lobqy3;?eXxy@RPVK50-PM4e<1iLw~`&;tCeeERN`4y{5 zXIG%zOE%aEWKAfy)t5Yo%_H)F)X z*237(>3^X^&We|k>-&TfGz|tS?8PtNpMTN=nvUVTORNw{olk;sC&Zo1XdMCz0`(@T zMn?CW4DK#UIpdP>F3s6dCg1s&0BjCvG(kmvO6v57Q2( zVh%|crSI2B6Ok9dqmeG7gQ9V$LUhAQ_d5A+7DBlwh(dV$Rss!tCFi4Vq0n)wtCqr@ zu1t<~sHE;%=W(Gon~LGoRW>fLR6B7a3)ajT@ECnZEaCckeLqIoaRg+!LTJ`)aws#H zp7CR0%3tdjPi3T8Cq_=4@&;s22tk7>H6T0U!W5&G02f3cdqIseYQ=0{YyPwcr}Y+^ z)jgE_ke)3v9(HK)Aw5lm8mjccmAvfcofJ3pGzaf*@AMfk_i_H`JAJRa_opS)J8IIb z_;JbpPbk6DOBL2l%?lRuB5SOI$npb0=&@+%iuCeFKIwR~aU{rOvw|CvYW^_zJt0Ws z<_Kj10~(pkzoy?NGut|RJGy{-fUQyp;G>AFQ1UbaCqG!B=86#bj`5I9Lm90+#(ruZ z9~RGDF~!@EUPlb~%X5~5OPksYYato_oXkOQ;Y2!_jTrumT>LZ4u!6M0RH z5EESc?CTu1ScFR(yAn}2@&{IIV*_Yg@6lGV+?j=^7$;Gg5RYcgSbz8C`eq+>PYOy$ zJ83<3W4c;UDODP{du4UE(fsh6?nDz|Fy&kzkq?Dpxi|yz!)hpgyTFpx)n-2RRYUkJ zoC2p7ZdFY)wQyClj{Ro06L6+;Y56t?9M8k7Wvkk`bfSJJbMf7dwGf;)TMFYJ!lv?f z>ao(Okdqvr=s#tvm_kWX?Hks8G)AR%3>c$k?1G*LJtMIz?z(RL!q%OaM(;!mHc6Au zU1kRONtdq)UCw8DqWSiYT^9bWUk#w21O!+L|DU@0zxezC0U!U&<-hly!5@fLjA+b1NfS2V+BHb33O$s{%;TQcX=v|Dv9hk)*9>ondDA#{2;gkpcl}`P7z# z2B`VlW64Vae?a-|?oa3dEBoDMjsUu1pKiY;Q9^rk3tE! z{eP>;2*^r^iYO`5$%wv3_^rmj8wLa|{;6aE?thah_@^2G{-HmW-hb8jm$1P;Ww3A6od` zUwaSd?kAm}2Y?v^T)&ZI|526!=Kc?Gfaf)JFm`m52B^Io+x%OA;ypa2M`3>lpew^* zf6s;Z1AY|qZ{YzH+*Zzx04^C(b1P#3Lqk9dGWs_9rvI&htlLpg4?u?p13LUSMZiDG z0>R%lAm*SCP)}6>Fjb1%S{qB-+FCl>{e9PvZ4aY80Bo)U&=G(bvOkp!fUW#Z*ZdBx z1~5E;QtNNF_xHGuI~e=r0JK%WMf4|BAfPq6zr~gKx7GbU9``Cak1xQw*b(024blHS zo{giEzLnK~v*BOHH&%3jX~l>d2#DY>&ldzp@%x+q8^8ec8{XeP-9eLe z{$J28rT!L8+Sc^HzU@GBexQ25pjQQWVH|$}%aZ+DFnNG>i-4n}v9$p}F_%Qz)==L{ z7+|mt<_6Ax@Vvh_+V^tze>7Ai|Nq^}-*>}%o!>t&fzO6ZBt23g4r?*WLL8)z|!gQsH?I_!|Jg%KoqXrnK`% z*#H3k$!LFz{d`~fz3$E*mEkP@qw>F{PyV|*_#XbfmdYRSsaF3L{(o6Yyl?2e;=vyc zeYXFPhW_;Y|3&}cJ^Xv>{y*R^9sUXaowxiR_B~_$AFv8e{{;KzZHV`n?^%ogz|8ab zC(PdyGydDm_?{p5|Ec8cRTBuJD7=ktkw-{nV;#0k5o;S?!9D>&LLkM0AP6Feg`f{0 zDQpB`k<`JrvB<<-J;OKd%+1!z`DQP}{M_XnsTQvW)#kKd4xjO+0(FK~P*t8f?34gT zNeb{dG5{jMk|Z%xPNd?)Kr$uFk;z0bG4oFYGnNlV6q8Vd`WhQhkz5p#m^vZSc48n^ z)8XlE1_e=c^$WG1no(|j8Tc`PgwP}{$Z2MV1V$=SXvP)gXKtqW)?5PUcJu&?e*#h! zqs>gH(jDQk$9cz8;-w$cc*dE1}qLepfsBCXA@(bAJ66ft0aCq$Wrcq)WXX{0nm+#w=uBj1o9rLyA i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/spring-boot-draft/gradlew.bat b/examples/spring-boot-draft/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/examples/spring-boot-draft/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49492 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 RFWVll$ONUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/spring-boot-kubernetes/mvnw.cmd b/examples/spring-boot-kubernetes/mvnw.cmd index 03d90e960b..830073a17e 100755 --- a/examples/spring-boot-kubernetes/mvnw.cmd +++ b/examples/spring-boot-kubernetes/mvnw.cmd @@ -1,161 +1,161 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/jib-core/build.gradle b/jib-core/build.gradle index b151bcf71f..dd6bd1c7bf 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -64,10 +64,6 @@ jar { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.6' -} - // Integration tests must be run explicitly task integrationTest(type: Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs diff --git a/jib-core/gradle/wrapper/gradle-wrapper.jar b/jib-core/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49492 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 RFWVll$ONUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index cea2a0f450..f856a8ebf1 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -78,10 +78,6 @@ dependencies { errorprone 'com.google.errorprone:error_prone_core:2.2.0' } -task wrapper(type: Wrapper) { - gradleVersion = '4.6' -} - // Integration tests must be run explicitly task integrationTest(type: Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs diff --git a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 50157 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 *SW$u63w9|gi(C$5HMvX&ZbF|8 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 HXWjn;W&S@( delta 48671 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 p-JqCdbEt2u3x<%@f8MG(Y}9d{||T4^I-r0 diff --git a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties index 32a96c59be..e0b3fb8d70 100644 --- a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jib-gradle-plugin/gradlew.bat b/jib-gradle-plugin/gradlew.bat index e95643d6a2..f9553162f1 100644 --- a/jib-gradle-plugin/gradlew.bat +++ b/jib-gradle-plugin/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jib-maven-plugin/mvnw.cmd b/jib-maven-plugin/mvnw.cmd index 4f0b068a03..6a6eec39ba 100755 --- a/jib-maven-plugin/mvnw.cmd +++ b/jib-maven-plugin/mvnw.cmd @@ -1,145 +1,145 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 250df6233d..686e1917c3 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -44,10 +44,6 @@ dependencies { errorprone 'com.google.errorprone:error_prone_core:2.2.0' } -task wrapper(type: Wrapper) { - gradleVersion = '4.6' -} - test { testLogging { showStandardStreams = true diff --git a/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar b/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar index 0d4a9516871afd710a9d84d89e31ba77745607bd..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49463 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

Uses the default cache storage engine ({@link DefaultCacheStorage}), layer entries as the - * selector ({@link LayerEntriesSelector}), and last modified time as the metadata. + *

Uses the default cache storage engine ({@link DefaultCacheStorage}) with layer entries as the + * selector ({@link LayerEntriesSelector}). * *

This class is immutable and safe to use across threads. */ @@ -59,7 +58,7 @@ private Cache(CacheStorage cacheStorage) { /** * Saves a cache entry with a compressed layer {@link Blob}. Use {@link * #writeUncompressedLayer(Blob, ImmutableList)} to save a cache entry with an uncompressed layer - * {@link Blob} and include a selector and metadata. + * {@link Blob} and include a selector. * * @param compressedLayerBlob the compressed layer {@link Blob} * @return the {@link CacheEntry} for the written layer @@ -70,28 +69,23 @@ public CacheEntry writeCompressedLayer(Blob compressedLayerBlob) throws IOExcept } /** - * Saves a cache entry with an uncompressed layer {@link Blob}, an additional selector digest, and - * a metadata {@link Blob}. Use {@link #writeCompressedLayer(Blob)} to save a compressed layer - * {@link Blob}. + * Saves a cache entry with an uncompressed layer {@link Blob} and an additional selector digest. + * Use {@link #writeCompressedLayer(Blob)} to save a compressed layer {@link Blob}. * * @param uncompressedLayerBlob the layer {@link Blob} * @param layerEntries the layer entries that make up the layer - * @return the {@link CacheEntry} for the written layer and metadata + * @return the {@link CacheEntry} for the written layer * @throws IOException if an I/O exception occurs */ public CacheEntry writeUncompressedLayer( Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { return cacheStorage.write( new UncompressedCacheWrite( - uncompressedLayerBlob, - LayerEntriesSelector.generateSelector(layerEntries), - LastModifiedTimeMetadata.generateMetadata(layerEntries))); + uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries))); } /** - * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. The last - * modified time of the {@code layerEntries} must match the last modified time as stored by the - * metadata of the {@link CacheEntry}. + * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. * * @param layerEntries the layer entries to match against * @return a {@link CacheEntry} that was built from {@code layerEntries}, if found @@ -106,28 +100,7 @@ public Optional retrieve(ImmutableList layerEntries) return Optional.empty(); } - Optional optionalCacheEntry = - cacheStorage.retrieve(optionalSelectedLayerDigest.get()); - if (!optionalCacheEntry.isPresent()) { - return Optional.empty(); - } - - CacheEntry cacheEntry = optionalCacheEntry.get(); - - Optional optionalRetrievedLastModifiedTime = - LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry); - if (!optionalRetrievedLastModifiedTime.isPresent()) { - return Optional.empty(); - } - - FileTime retrievedLastModifiedTime = optionalRetrievedLastModifiedTime.get(); - FileTime expectedLastModifiedTime = LastModifiedTimeMetadata.getLastModifiedTime(layerEntries); - - if (!expectedLastModifiedTime.equals(retrievedLastModifiedTime)) { - return Optional.empty(); - } - - return Optional.of(cacheEntry); + return cacheStorage.retrieve(optionalSelectedLayerDigest.get()); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java index f139702dda..a4cb263c84 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.util.Optional; /** * Represents a cache entry for a layer stored in the cache. Implementations must be @@ -55,13 +54,4 @@ public interface CacheEntry { * @return the layer {@link Blob} */ Blob getLayerBlob(); - - /** - * Gets the optional metadata blob for the layer. The metadata is in the same format as supplied - * when writing to the cache with {@link UncompressedCacheWrite}. This {@link Blob} should be able - * to be used multiple times. - * - * @return the metadata {@link Blob} - */ - Optional getMetadataBlob(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java index 00fa173868..75f07ca48f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java @@ -25,9 +25,8 @@ /** * Interface for queries to a cache storage engine. * - *

The cache storage engine stores layer data in compressed form, along with optional metadata. - * These entries are read out as {@link CacheEntry}s. Cache entries can be retrieved by the layer - * digest. + *

The cache storage engine stores layer data in compressed form. These entries are read out as + * {@link CacheEntry}s. Cache entries can be retrieved by the layer digest. * *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). * The selectors do not need to be unique. An example of a selector could be the digest of the list diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java index dc2f00214e..a3ea2948c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.base.Preconditions; -import java.util.Optional; import javax.annotation.Nullable; /** Default implementation of {@link CacheEntry}. */ @@ -32,7 +31,6 @@ static class Builder { @Nullable private DescriptorDigest layerDiffId; private long layerSize = -1; @Nullable private Blob layerBlob; - @Nullable private Blob metadataBlob; private Builder() {} @@ -56,26 +54,16 @@ Builder setLayerBlob(Blob layerBlob) { return this; } - Builder setMetadataBlob(@Nullable Blob metadataBlob) { - this.metadataBlob = metadataBlob; - return this; - } - boolean hasLayerBlob() { return layerBlob != null; } - boolean hasMetadataBlob() { - return metadataBlob != null; - } - CacheEntry build() { return new DefaultCacheEntry( Preconditions.checkNotNull(layerDigest, "layerDigest required"), Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), layerSize, - Preconditions.checkNotNull(layerBlob, "layerBlob required"), - metadataBlob); + Preconditions.checkNotNull(layerBlob, "layerBlob required")); } } @@ -92,19 +80,13 @@ static Builder builder() { private final DescriptorDigest layerDiffId; private final long layerSize; private final Blob layerBlob; - @Nullable private final Blob metadataBlob; private DefaultCacheEntry( - DescriptorDigest layerDigest, - DescriptorDigest layerDiffId, - long layerSize, - Blob layerBlob, - @Nullable Blob metadataBlob) { + DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, Blob layerBlob) { this.layerDigest = layerDigest; this.layerDiffId = layerDiffId; this.layerSize = layerSize; this.layerBlob = layerBlob; - this.metadataBlob = metadataBlob; } @Override @@ -126,9 +108,4 @@ public long getLayerSize() { public Blob getLayerBlob() { return layerBlob; } - - @Override - public Optional getMetadataBlob() { - return Optional.ofNullable(metadataBlob); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java index 1c76eaf014..c3c5f91657 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java @@ -31,7 +31,6 @@ * layers/ * / * - * metadata * ... * selectors/ * @@ -40,7 +39,7 @@ * * Layers entries are stored in their own directories under the {@code layers/} directory. Each * layer directory is named by the layer digest. Inside each layer directory, the layer contents - * file is named by the layer diff ID, and the metadata is the {@code metadata} file. + * file is named by the layer diff ID. * *

Selectors are stored in the {@code selectors/} directory. Each selector file is named by the * selector digest. The contents of a selector file is the digest of the layer it selects. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java index e7dde41c2d..5323bfbd13 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java @@ -24,7 +24,6 @@ class DefaultCacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; - private static final String METADATA_FILENAME = "metadata"; private static final String SELECTORS_DIRECTORY = "selectors"; private static final String TEMPORARY_DIRECTORY = "tmp"; private static final String TEMPORARY_LAYER_FILE_NAME = ".tmp.layer"; @@ -39,16 +38,6 @@ static boolean isLayerFile(Path file) { return file.getFileName().toString().length() == DescriptorDigest.HASH_LENGTH; } - /** - * Returns whether or not {@code file} is a metadata file. - * - * @param file the file to check - * @return {@code true} if {@code file} is a metadata file; {@code false} otherwise - */ - static boolean isMetadataFile(Path file) { - return METADATA_FILENAME.equals(file.getFileName().toString()); - } - /** * Gets the diff ID portion of the layer filename. * @@ -95,25 +84,6 @@ String getLayerFilename(DescriptorDigest layerDiffId) { return layerDiffId.getHash(); } - /** - * Resolves the layer metadata file. - * - * @param layerDigest the layer digest - * @return the layer metadata file - */ - Path getMetadataFile(DescriptorDigest layerDigest) { - return getLayerDirectory(layerDigest).resolve(METADATA_FILENAME); - } - - /** - * Gets the filename for the metadata file. - * - * @return the filename for the metadata file - */ - String getMetadataFilename() { - return METADATA_FILENAME; - } - /** * Resolves a selector file. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java index e4f7ceaf72..da4ad71a6d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java @@ -95,16 +95,6 @@ Optional retrieve(DescriptorDigest layerDigest) .setLayerBlob(Blobs.from(fileInLayerDirectory)) .setLayerDiffId(DefaultCacheStorageFiles.getDiffId(fileInLayerDirectory)) .setLayerSize(Files.size(fileInLayerDirectory)); - - } else if (DefaultCacheStorageFiles.isMetadataFile(fileInLayerDirectory)) { - if (cacheEntryBuilder.hasMetadataBlob()) { - throw new CacheCorruptedException( - "Multiple metadata files found for layer with digest " - + layerDigest.getHash() - + " in directory: " - + layerDirectory); - } - cacheEntryBuilder.setMetadataBlob(Blobs.from(fileInLayerDirectory)); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java index ed8bcf3a14..246c41d41b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java @@ -173,9 +173,8 @@ CacheEntry write(Blob compressedLayerBlob) throws IOException { *

The {@link UncompressedCacheWrite} is written out to the cache directory in the form: * *

@@ -200,12 +199,6 @@ CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOExcepti writeUncompressedLayerBlobToDirectory( uncompressedCacheWrite.getUncompressedLayerBlob(), temporaryLayerDirectory); - // Writes the metadata to the temporary directory. - if (uncompressedCacheWrite.getMetadataBlob().isPresent()) { - writeMetadataBlobToDirectory( - uncompressedCacheWrite.getMetadataBlob().get(), temporaryLayerDirectory); - } - // Moves the temporary directory to the final location. moveIfDoesNotExist( temporaryLayerDirectory, @@ -220,10 +213,6 @@ CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOExcepti .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) .setLayerBlob(Blobs.from(layerFile)); - if (uncompressedCacheWrite.getMetadataBlob().isPresent()) { - Path metadataFile = defaultCacheStorageFiles.getMetadataFile(writtenLayer.layerDigest); - cacheEntryBuilder.setMetadataBlob(Blobs.from(metadataFile)).build(); - } // Write the selector file. if (uncompressedCacheWrite.getSelector().isPresent()) { @@ -300,19 +289,6 @@ private WrittenLayer writeUncompressedLayerBlobToDirectory( } } - /** - * Writes the {@code metadataBlob} to the {@code layerDirectory}. - * - * @param metadataBlob the metadata {@link Blob} - * @param layerDirectory the directory for the layer the metadata is for - * @throws IOException if an I/O exception occurs - */ - private void writeMetadataBlobToDirectory(Blob metadataBlob, Path layerDirectory) - throws IOException { - Path metadataFile = layerDirectory.resolve(defaultCacheStorageFiles.getMetadataFilename()); - Blobs.writeToFileWithLock(metadataBlob, metadataFile); - } - /** * Writes the {@code selector} to a file in the selectors directory, with contents {@code * layerDigest}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java deleted file mode 100644 index fdc57d965c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.Optional; - -/** - * Serializes/deserializes metadata storing the latest last modified time of all the source files in - * {@link LayerEntry}s for a layer. - * - *

Use {@link #generateMetadata} to serialize the latest last modified time of all the source - * files in {@link LayerEntry}s for a layer into a {@link Blob} containing the serialized last - * modified time. Use {@link #getLastModifiedTime(CacheEntry)} to deserialize the metadata in a - * {@link CacheEntry} into a last modified time. - */ -class LastModifiedTimeMetadata { - - /** - * Generates the metadata {@link Blob} for the list of {@link LayerEntry}s. The metadata is the - * latest last modified time of all the source files in the list of {@link LayerEntry}s serialized - * using {@link Instant#toString}. - * - * @param layerEntries the list of {@link LayerEntry}s - * @return the generated metadata - */ - static Blob generateMetadata(ImmutableList layerEntries) throws IOException { - return Blobs.from(getLastModifiedTime(layerEntries).toInstant().toString()); - } - - /** - * Gets the latest last modified time of all the source files in the list of {@link LayerEntry}s. - * - * @param layerEntries the list of {@link LayerEntry}s - * @return the last modified time - */ - static FileTime getLastModifiedTime(ImmutableList layerEntries) throws IOException { - FileTime maxLastModifiedTime = FileTime.from(Instant.MIN); - - for (LayerEntry layerEntry : layerEntries) { - FileTime lastModifiedTime = Files.getLastModifiedTime(layerEntry.getSourceFile()); - if (lastModifiedTime.compareTo(maxLastModifiedTime) > 0) { - maxLastModifiedTime = lastModifiedTime; - } - } - - return maxLastModifiedTime; - } - - /** - * Gets the last modified time from the metadata of a {@link CacheEntry}. - * - * @param cacheEntry the {@link CacheEntry} - * @return the last modified time, if the metadata is present - * @throws IOException if deserialization of the metadata failed - */ - static Optional getLastModifiedTime(CacheEntry cacheEntry) throws IOException { - if (!cacheEntry.getMetadataBlob().isPresent()) { - return Optional.empty(); - } - - Blob metadataBlob = cacheEntry.getMetadataBlob().get(); - return Optional.of(FileTime.from(Instant.parse(Blobs.writeToString(metadataBlob)))); - } - - private LastModifiedTimeMetadata() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index 55f3c1ae08..51bf45049d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -24,6 +24,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import java.io.IOException; +import java.nio.file.Files; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,10 +42,12 @@ * { * "sourceFile": "source/file/for/layer/entry/1", * "extractionPath": "/extraction/path/for/layer/entry/1" + * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" * }, * { * "sourceFile": "source/file/for/layer/entry/2", * "extractionPath": "/extraction/path/for/layer/entry/2" + * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" * } * ] * }

Gauk6VzRKaGELdj)1bl0ug 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 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From c5a2a1afcebf4a6b6d092cc982e5a95d376fbac5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 5 Oct 2018 15:51:27 -0400 Subject: [PATCH 0274/2020] Add WAR integration tests for exportDockerContext (#1106) * Add exportDockerContext Maven WAR integration test * Add Gradle integration test --- .../jib/gradle/WarProjectIntegrationTest.java | 60 ++++++++++++++-- .../maven/BuildImageMojoIntegrationTest.java | 24 +------ .../DockerContextMojoIntegrationTest.java | 68 +++++++++++++++++++ .../tools/jib/maven/HttpGetVerifier.java | 38 +++++++++++ .../tools/jib/maven/SkippedGoalVerifier.java | 2 +- 5 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 3e3087aa66..7665ba0993 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -23,6 +23,8 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import javax.annotation.Nullable; import org.junit.After; import org.junit.Assert; @@ -61,25 +63,69 @@ public void tearDown() throws IOException, InterruptedException { } @Test - public void testBuild_jettyServlet25Project() throws IOException, InterruptedException { - buildAndRunDetached(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); + public void testBuild_jettyServlet25() throws IOException, InterruptedException { + verifyBuildAndRun(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); + } - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + @Test + public void testBuild_tomcatServlet25() throws IOException, InterruptedException { + verifyBuildAndRun(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); } @Test - public void testBuild_tomcatServlet25Project() throws IOException, InterruptedException { - buildAndRunDetached(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); + public void testDockerContext_jettyServlet25() throws IOException, InterruptedException { + String expectedDockerContext = + "FROM gcr.io/distroless/java/jetty\n" + + "\n" + + "COPY libs /\n" + + "COPY resources /\n" + + "COPY classes /\n" + + "\n" + + "ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]\n" + + "CMD []"; + verifyDockerContextBuildAndRun(expectedDockerContext, "build.gradle"); + } - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + @Test + public void testDockerContext_tomcatServlet25() throws IOException, InterruptedException { + String expectedDockerContext = + "FROM tomcat:8.5-jre8-alpine\n" + + "\n" + + "COPY libs /\n" + + "COPY resources /\n" + + "COPY classes /\n" + + "\n" + + "ENTRYPOINT [\"catalina.sh\",\"run\"]\n" + + "CMD []"; + verifyDockerContextBuildAndRun(expectedDockerContext, "build-tomcat.gradle"); } - private void buildAndRunDetached(TestProject project, String label, String gradleBuildFile) + private void verifyBuildAndRun(TestProject project, String label, String gradleBuildFile) throws IOException, InterruptedException { String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; String targetImage = nameBase + label + System.nanoTime(); String output = JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); containerName = output.trim(); + + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + } + + private void verifyDockerContextBuildAndRun(String expectedDockerfile, String gradleBuildFile) + throws IOException, InterruptedException { + servlet25Project.build("clean", "jibExportDockerContext", "-b=" + gradleBuildFile); + + Path dockerContext = + servlet25Project.getProjectRoot().resolve("build").resolve("jib-docker-context"); + Assert.assertTrue(Files.exists(dockerContext)); + String dockerfile = String.join("\n", Files.readAllLines(dockerContext.resolve("Dockerfile"))); + Assert.assertEquals(expectedDockerfile, dockerfile); + + String imageName = "jib/integration-test" + System.nanoTime(); + new Command("docker", "build", "-t", imageName, dockerContext.toString()).run(); + containerName = + new Command("docker", "run", "--rm", "--detach", "-p8080:8080", imageName).run().trim(); + + Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 834354c66a..e47f3d0e76 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,13 +18,10 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -78,23 +75,6 @@ private static String getGcrImageReference(String label) { return nameBase + label + System.nanoTime(); } - @Nullable - private static String getContent(URL url) throws InterruptedException { - for (int i = 0; i < 40; i++) { - Thread.sleep(500); - try { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { - try (InputStream in = connection.getInputStream()) { - return Blobs.writeToString(Blobs.from(in)); - } - } - } catch (IOException ex) { - } - } - return null; - } - /** * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. @@ -362,14 +342,14 @@ public void testExecute_skipJibGoal() throws VerificationException, IOException public void testExecute_jettyServlet25() throws VerificationException, IOException, InterruptedException { buildAndRunWar("jetty-servlet25:maven", "pom.xml"); - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); } @Test public void testExecute_tomcatServlet25() throws VerificationException, IOException, InterruptedException { buildAndRunWar("tomcat-servlet25:maven", "pom-tomcat.xml"); - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); } private void buildAndRunWar(String label, String pomXml) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 4003444432..90dfac576d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -18,11 +18,15 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import javax.annotation.Nullable; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; +import org.junit.After; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -41,6 +45,18 @@ public class DockerContextMojoIntegrationTest { @ClassRule public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + @ClassRule + public static final TestProject servlet25Project = new TestProject(testPlugin, "war_servlet25"); + + @Nullable private String detachedContainerName; + + @After + public void tearDown() throws IOException, InterruptedException { + if (detachedContainerName != null) { + new Command("docker", "stop", detachedContainerName).run(); + } + } + @Test public void testExecute() throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); @@ -77,7 +93,59 @@ public void testExecute() throws VerificationException, IOException, Interrupted new Command("docker", "run", "--rm", imageName).run()); } + @Test public void testExecute_skipJibGoal() throws VerificationException, IOException { SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildDockerMojo.GOAL_NAME); } + + @Test + public void testExecute_jettyServlet25() + throws VerificationException, IOException, InterruptedException { + String expectedDockerfile = + "FROM gcr.io/distroless/java/jetty\n" + + "\n" + + "COPY libs /\n" + + "COPY resources /\n" + + "COPY classes /\n" + + "\n" + + "ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]\n" + + "CMD []"; + verifyWarBuildAndRun(expectedDockerfile, "pom.xml"); + } + + @Test + public void testExecute_tomcatServlet25() + throws VerificationException, IOException, InterruptedException { + String expectedDockerfile = + "FROM tomcat:8.5-jre8-alpine\n" + + "\n" + + "COPY libs /\n" + + "COPY resources /\n" + + "COPY classes /\n" + + "\n" + + "ENTRYPOINT [\"catalina.sh\",\"run\"]\n" + + "CMD []"; + verifyWarBuildAndRun(expectedDockerfile, "pom-tomcat.xml"); + } + + private void verifyWarBuildAndRun(String expectedDockerfile, String pomXml) + throws VerificationException, IOException, InterruptedException { + Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); + verifier.setAutoclean(false); + verifier.addCliOption("--file=" + pomXml); + verifier.executeGoals(Arrays.asList("clean", "package", "jib:exportDockerContext")); + + Path dockerContext = + servlet25Project.getProjectRoot().resolve("target").resolve("jib-docker-context"); + Assert.assertTrue(Files.exists(dockerContext)); + String dockerfile = String.join("\n", Files.readAllLines(dockerContext.resolve("Dockerfile"))); + Assert.assertEquals(expectedDockerfile, dockerfile); + + String imageName = "jib/integration-test" + System.nanoTime(); + new Command("docker", "build", "-t", imageName, dockerContext.toString()).run(); + detachedContainerName = + new Command("docker", "run", "--rm", "--detach", "-p8080:8080", imageName).run().trim(); + + HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java new file mode 100644 index 0000000000..5d051eb226 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java @@ -0,0 +1,38 @@ +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.blob.Blobs; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import javax.annotation.Nullable; +import org.junit.Assert; + +/** Verifies the response of HTTP GET. */ +class HttpGetVerifier { + + /** + * Verifies the response body. Repeatedly tries {@code url} at the interval of .5 seconds for up + * to 20 seconds until getting OK HTTP response code. + */ + static void verifyBody(String expectedBody, URL url) throws InterruptedException { + Assert.assertEquals(expectedBody, getContent(url)); + } + + @Nullable + private static String getContent(URL url) throws InterruptedException { + for (int i = 0; i < 40; i++) { + Thread.sleep(500); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream in = connection.getInputStream()) { + return Blobs.writeToString(Blobs.from(in)); + } + } + } catch (IOException ex) { + } + } + return null; + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java index 426ef839c4..f98b7f99ec 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java @@ -29,7 +29,7 @@ /** A simple verifier utility to test goal skipping accross all our jib goals. */ class SkippedGoalVerifier { - /** Verify that a jib goal is skipped */ + /** Verifies that a Jib goal is skipped. */ static void verifyGoalIsSkipped(TestProject testProject, String goal) throws VerificationException, IOException { Verifier verifier = new Verifier(testProject.getProjectRoot().toString()); From bcfafec09c0e4b0eb05b2e1006f36b671abafa33 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 5 Oct 2018 16:51:42 -0400 Subject: [PATCH 0275/2020] Add copyright header (#1108) --- .../cloud/tools/jib/maven/HttpGetVerifier.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java index 5d051eb226..9230a727b9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.blob.Blobs; From f474e4731dc86fe4bbf826537a19a17729ff522a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 8 Oct 2018 09:41:50 -0400 Subject: [PATCH 0276/2020] Adds JibContainerBuilder#containerize. (#1076) --- .../tools/jib/api/JibIntegrationTest.java | 86 +++++++++++++++++++ .../cloud/tools/jib/api/Containerizer.java | 16 ++++ .../tools/jib/api/DockerDaemonImage.java | 14 +-- .../cloud/tools/jib/api/JibContainer.java | 38 ++++++++ .../tools/jib/api/JibContainerBuilder.java | 59 +++++++++++-- .../cloud/tools/jib/api/RegistryImage.java | 12 +-- .../google/cloud/tools/jib/api/TarImage.java | 12 +-- .../cloud/tools/jib/api/TargetImage.java | 15 ++++ .../tools/jib/builder/steps/StepsRunner.java | 3 +- .../jib/configuration/BuildConfiguration.java | 31 ++++++- .../frontend/CredentialRetrieverFactory.java | 41 ++++++--- .../tools/jib/api/ContainerizerTest.java | 78 +++++++++++++++++ .../jib/api/JibContainerBuilderTest.java | 37 ++++++-- kokoro/presubmit.sh | 5 ++ proposals/jib_core_library.md | 1 + 15 files changed, 403 insertions(+), 45 deletions(-) create mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java new file mode 100644 index 0000000000..abe0ccead0 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.LocalRegistry; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for {@link Jib}. */ +public class JibIntegrationTest { + + @ClassRule + public static final LocalRegistry localRegistry = new LocalRegistry(5000, "username", "password"); + + /** + * Pulls a built image and attempts to run it. + * + * @param imageReference the image reference of the built image + * @return the container output + * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the process was interrupted + */ + private static String pullAndRunBuiltImage(String imageReference) + throws IOException, InterruptedException { + localRegistry.pull(imageReference); + return new Command("docker", "run", "--rm", imageReference).run(); + } + + @Before + public void setUp() { + System.setProperty("sendCredentialsOverHttp", "true"); + } + + @After + public void tearDown() { + System.clearProperty("sendCredentialsOverHttp"); + } + + @Test + public void testBasic_helloWorld() + throws InvalidImageReferenceException, InterruptedException, ExecutionException, + CacheDirectoryCreationException, IOException { + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld"); + JibContainer jibContainer = + Jib.from("busybox") + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.basic("username", "password")))) + .setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); + Assert.assertEquals( + "Hello World\n", + pullAndRunBuiltImage( + targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 20f6e5254a..e4c9a160d8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -76,6 +76,7 @@ public static Containerizer to(TarImage tarImage) { private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; @Nullable private Path applicationLayersCacheDirectory; @Nullable private EventHandlers eventHandlers; + private boolean allowInsecureRegistries = false; /** Instantiate with {@link #to}. */ private Containerizer(TargetImage targetImage) { @@ -131,6 +132,17 @@ public Containerizer setEventHandlers(EventHandlers eventHandlers) { return this; } + /** + * Sets whether or not to allow communication over HTTP/insecure HTTPS. + * + * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed + * @return this + */ + public Containerizer setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries = allowInsecureRegistries; + return this; + } + TargetImage getTargetImage() { return targetImage; } @@ -161,4 +173,8 @@ Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException Optional getEventHandlers() { return Optional.ofNullable(eventHandlers); } + + boolean getAllowInsecureRegistries() { + return allowInsecureRegistries; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index 6070268543..9cf6c3e542 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -17,7 +17,10 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.BuildSteps; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; @@ -70,16 +73,17 @@ public DockerDaemonImage setDockerExecutable(Path dockerExecutable) { return this; } - /** - * Converts into an {@link ImageConfiguration}. For internal use only. - * - * @return an {@link ImageConfiguration} - */ @Override public ImageConfiguration toImageConfiguration() { return ImageConfiguration.builder(imageReference).build(); } + @Override + public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { + return BuildSteps.forBuildToDockerDaemon( + DockerClient.newClient(dockerExecutable), buildConfiguration); + } + /** * Gets the path to the {@code docker} CLI. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java new file mode 100644 index 0000000000..7912fbf1c2 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.image.DescriptorDigest; + +/** The container built by Jib. */ +public class JibContainer { + + private final DescriptorDigest imageDigest; + + JibContainer(DescriptorDigest imageDigest) { + this.imageDigest = imageDigest; + } + + /** + * Gets the digest of the container image built by Jib. + * + * @return the image digest + */ + public DescriptorDigest getDigest() { + return imageDigest; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 381cb85cf6..f75aae5951 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -18,10 +18,13 @@ // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -31,6 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; /** @@ -301,20 +305,63 @@ public JibContainerBuilder addLabel(String key, String value) { return this; } - @VisibleForTesting - BuildConfiguration.Builder toBuildConfiguration(TargetImage targetImage) { - BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder(); + /** + * Builds the container(s). + * + * @param containerizer the {@link Containerizer} that configures how to containerize + * @return the built container(s) + * @throws CacheDirectoryCreationException if a directory to be used for the cache could not be + * created + * @throws ExecutionException if an exception occurred during execution + * @throws InterruptedException if the execution was interrupted + * @throws IOException if an I/O exception occurs + */ + public JibContainer containerize(Containerizer containerizer) + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + BuildConfiguration buildConfiguration = + toBuildConfiguration(BuildConfiguration.builder(), containerizer); + DescriptorDigest imageDigest = + containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); + + return new JibContainer(imageDigest); + } + /** + * Builds a {@link BuildConfiguration} using this and a {@link Containerizer}. + * + * @param buildConfigurationBuilder the {@link BuildConfiguration.Builder} to use + * @param containerizer the {@link Containerizer} + * @return the {@link BuildConfiguration} + * @throws CacheDirectoryCreationException if a cache directory could not be created + * @throws IOException if an I/O exception occurs + */ + @VisibleForTesting + BuildConfiguration toBuildConfiguration( + BuildConfiguration.Builder buildConfigurationBuilder, Containerizer containerizer) + throws CacheDirectoryCreationException, IOException { buildConfigurationBuilder .setBaseImageConfiguration(baseImage.toImageConfiguration()) - .setTargetImageConfiguration(targetImage.toImageConfiguration()) + .setTargetImageConfiguration(containerizer.getTargetImage().toImageConfiguration()) + .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) + .setApplicationLayersCacheDirectory(containerizer.getApplicationLayersCacheDirectory()) .setContainerConfiguration(toContainerConfiguration()) - .setLayerConfigurations(layerConfigurations); + .setLayerConfigurations(layerConfigurations) + .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()); + + containerizer.getExecutorService().ifPresent(buildConfigurationBuilder::setExecutorService); + + containerizer + .getEventHandlers() + .ifPresent( + eventHandlers -> + buildConfigurationBuilder.setEventDispatcher( + new DefaultEventDispatcher(eventHandlers))); // TODO: Allow users to configure this. buildConfigurationBuilder.setToolName("jib-core"); - return buildConfigurationBuilder; + return buildConfigurationBuilder.build(); } @VisibleForTesting diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 45a2c792ab..332c786c8a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.BuildSteps; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; @@ -115,15 +117,15 @@ public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetrie return this; } - /** - * Converts into an {@link ImageConfiguration}. For internal use only. - * - * @return an {@link ImageConfiguration} - */ @Override public ImageConfiguration toImageConfiguration() { return ImageConfiguration.builder(imageReference) .setCredentialRetrievers(credentialRetrievers) .build(); } + + @Override + public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { + return BuildSteps.forBuildToDockerRegistry(buildConfiguration); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index 124abfa61e..44f64a0fbf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.BuildSteps; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -86,16 +88,16 @@ private TarImage(ImageReference imageReference, Path outputFile) { this.outputFile = outputFile; } - /** - * Converts into an {@link ImageConfiguration}. For internal use only. - * - * @return an {@link ImageConfiguration} - */ @Override public ImageConfiguration toImageConfiguration() { return ImageConfiguration.builder(imageReference).build(); } + @Override + public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { + return BuildSteps.forBuildToTar(outputFile, buildConfiguration); + } + /** * Gets the output file to save the tarball archive to. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java index 4f52152268..b0eebdffda 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java @@ -17,10 +17,25 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.BuildSteps; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; /** Represents a destination for the Jib-built image. */ interface TargetImage { + /** + * Converts into an {@link ImageConfiguration}. For internal use only. + * + * @return an {@link ImageConfiguration} + */ ImageConfiguration toImageConfiguration(); + + /** + * Converts into {@link BuildSteps}. For internal use only. + * + * @param buildConfiguration the {@link BuildConfiguration} to use + * @return {@link BuildSteps} + */ + BuildSteps toBuildSteps(BuildConfiguration buildConfiguration); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 63e73baaa6..7608d22d56 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -30,7 +30,6 @@ import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Nullable; /** @@ -66,7 +65,7 @@ public StepsRunner(BuildConfiguration buildConfiguration) { ExecutorService executorService = JibSystemProperties.isSerializedExecutionEnabled() ? MoreExecutors.newDirectExecutorService() - : Executors.newCachedThreadPool(); + : buildConfiguration.getExecutorService(); listeningExecutorService = MoreExecutors.listeningDecorator(executorService); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 484f705af8..dc8d2b4e7c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.annotation.Nullable; /** Immutable configuration options for the builder process. */ @@ -62,6 +64,7 @@ public static class Builder { jibEvent -> { /* No-op EventDispatcher. */ }; + @Nullable private ExecutorService executorService; private Builder() {} @@ -187,6 +190,18 @@ public Builder setEventDispatcher(EventDispatcher eventDispatcher) { return this; } + /** + * Sets the {@link ExecutorService} Jib executes on. By default, Jib uses {@link + * Executors#newCachedThreadPool}. + * + * @param executorService the {@link ExecutorService} + * @return this + */ + public Builder setExecutorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + /** * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. * @@ -219,6 +234,10 @@ public BuildConfiguration build() throws IOException { + "' does not use a specific image digest - build may not be reproducible")); } + if (executorService == null) { + executorService = Executors.newCachedThreadPool(); + } + return new BuildConfiguration( baseImageConfiguration, Preconditions.checkNotNull(targetImageConfiguration), @@ -230,7 +249,8 @@ public BuildConfiguration build() throws IOException { allowInsecureRegistries, layerConfigurations, toolName, - eventDispatcher); + eventDispatcher, + executorService); case 1: throw new IllegalStateException(missingFields.get(0) + " is required but not set"); @@ -282,6 +302,7 @@ public static Builder builder() { private final ImmutableList layerConfigurations; private final String toolName; private final EventDispatcher eventDispatcher; + private final ExecutorService executorService; /** Instantiate with {@link #builder}. */ private BuildConfiguration( @@ -295,7 +316,8 @@ private BuildConfiguration( boolean allowInsecureRegistries, ImmutableList layerConfigurations, String toolName, - EventDispatcher eventDispatcher) { + EventDispatcher eventDispatcher, + ExecutorService executorService) { this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; this.additionalTargetImageTags = additionalTargetImageTags; @@ -307,6 +329,7 @@ private BuildConfiguration( this.layerConfigurations = layerConfigurations; this.toolName = toolName; this.eventDispatcher = eventDispatcher; + this.executorService = executorService; } public ImageConfiguration getBaseImageConfiguration() { @@ -342,6 +365,10 @@ public EventDispatcher getEventDispatcher() { return eventDispatcher; } + public ExecutorService getExecutorService() { + return executorService; + } + /** * Gets the {@link Cache} for base image layers. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index e711ecdc6b..67514c7cf7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; @@ -61,18 +62,25 @@ interface DockerCredentialHelperFactory { */ public static CredentialRetrieverFactory forImage( ImageReference imageReference, EventDispatcher eventDispatcher) { - return new CredentialRetrieverFactory(imageReference, eventDispatcher); + return new CredentialRetrieverFactory( + imageReference, eventDispatcher, DockerCredentialHelper::new); + } + + /** + * Creates a new {@link CredentialRetrieverFactory} for an image. + * + * @param imageReference the image the credential are for + * @return a new {@link CredentialRetrieverFactory} + */ + public static CredentialRetrieverFactory forImage(ImageReference imageReference) { + return new CredentialRetrieverFactory( + imageReference, ignored -> {}, DockerCredentialHelper::new); } private final EventDispatcher eventDispatcher; private final ImageReference imageReference; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; - private CredentialRetrieverFactory( - ImageReference imageReference, EventDispatcher eventDispatcher) { - this(imageReference, eventDispatcher, DockerCredentialHelper::new); - } - @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, @@ -119,13 +127,13 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - eventDispatcher.dispatch(LogEvent.info("Checking credentials from " + credentialHelper)); + dispatchEvent(LogEvent.info("Checking credentials from " + credentialHelper)); try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { - eventDispatcher.dispatch( + dispatchEvent( LogEvent.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); return Optional.empty(); @@ -169,9 +177,9 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - eventDispatcher.dispatch(LogEvent.warn(ex.getMessage())); + dispatchEvent(LogEvent.warn(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - eventDispatcher.dispatch(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); + dispatchEvent(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } } @@ -214,14 +222,14 @@ CredentialRetriever dockerConfig( try { Optional dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); if (dockerConfigCredentials.isPresent()) { - eventDispatcher.dispatch( + dispatchEvent( LogEvent.info( "Using credentials from Docker config for " + imageReference.getRegistry())); return dockerConfigCredentials; } } catch (IOException ex) { - eventDispatcher.dispatch(LogEvent.info("Unable to parse Docker config")); + dispatchEvent(LogEvent.info("Unable to parse Docker config")); } return Optional.empty(); }; @@ -239,7 +247,14 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) } private void logGotCredentialsFrom(String credentialSource) { - eventDispatcher.dispatch( + dispatchEvent( LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); } + + private void dispatchEvent(JibEvent jibEvent) { + if (eventDispatcher == null) { + return; + } + eventDispatcher.dispatch(jibEvent); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java new file mode 100644 index 0000000000..02fe1003c6 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.ImageReference; +import java.nio.file.Paths; +import java.util.concurrent.ExecutorService; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link Containerizer}. */ +@RunWith(MockitoJUnitRunner.class) +public class ContainerizerTest { + + @Mock private ExecutorService mockExecutorService; + @Mock private EventHandlers mockEventHandlers; + + @Test + public void testTo() throws CacheDirectoryCreationException { + RegistryImage registryImage = RegistryImage.named(ImageReference.of(null, "repository", null)); + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named(ImageReference.of(null, "repository", null)); + TarImage tarImage = + TarImage.named(ImageReference.of(null, "repository", null)).saveTo(Paths.get("ignored")); + + verifyTo(Containerizer.to(registryImage), registryImage); + verifyTo(Containerizer.to(dockerDaemonImage), dockerDaemonImage); + verifyTo(Containerizer.to(tarImage), tarImage); + } + + private void verifyTo(Containerizer containerizer, TargetImage expectedTargetImage) + throws CacheDirectoryCreationException { + Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); + Assert.assertFalse(containerizer.getExecutorService().isPresent()); + Assert.assertFalse(containerizer.getEventHandlers().isPresent()); + Assert.assertEquals( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, + containerizer.getBaseImageLayersCacheDirectory()); + Assert.assertNotEquals( + Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, + containerizer.getApplicationLayersCacheDirectory()); + + containerizer + .setExecutorService(mockExecutorService) + .setEventHandlers(mockEventHandlers) + .setBaseImageLayersCache(Paths.get("base/image/layers")) + .setApplicationLayersCache(Paths.get("application/layers")); + + Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); + Assert.assertTrue(containerizer.getExecutorService().isPresent()); + Assert.assertEquals(mockExecutorService, containerizer.getExecutorService().get()); + Assert.assertTrue(containerizer.getEventHandlers().isPresent()); + Assert.assertEquals(mockEventHandlers, containerizer.getEventHandlers().get()); + Assert.assertEquals( + Paths.get("base/image/layers"), containerizer.getBaseImageLayersCacheDirectory()); + Assert.assertEquals( + Paths.get("application/layers"), containerizer.getApplicationLayersCacheDirectory()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 2703333cc6..6901a0252e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -17,11 +17,14 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -29,19 +32,27 @@ import java.io.IOException; import java.nio.file.Paths; import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link JibContainerBuilder}. */ @RunWith(MockitoJUnitRunner.class) public class JibContainerBuilderTest { + @Spy private BuildConfiguration.Builder spyBuildConfigurationBuilder; @Mock private LayerConfiguration mockLayerConfiguration1; @Mock private LayerConfiguration mockLayerConfiguration2; @Mock private CredentialRetriever mockCredentialRetriever; + @Mock private ExecutorService mockExecutorService; + @Mock private Consumer mockJibEventConsumer; + @Mock private JibEvent mockJibEvent; @Test public void testToContainerConfiguration_set() throws InvalidImageReferenceException { @@ -93,23 +104,25 @@ public void testToContainerConfiguration_add() { @Test public void testToBuildConfiguration() - throws InvalidImageReferenceException, CredentialRetrievalException, IOException { + throws InvalidImageReferenceException, CredentialRetrievalException, IOException, + CacheDirectoryCreationException { RegistryImage baseImage = RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); RegistryImage targetImage = RegistryImage.named(ImageReference.of("gcr.io", "my-project/my-app", null)) .addCredential("username", "password"); + Containerizer containerizer = + Containerizer.to(targetImage) + .setBaseImageLayersCache(Paths.get("base/image/layers")) + .setApplicationLayersCache(Paths.get("application/layers")) + .setExecutorService(mockExecutorService) + .setEventHandlers(new EventHandlers().add(mockJibEventConsumer)); JibContainerBuilder jibContainerBuilder = Jib.from(baseImage) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); BuildConfiguration buildConfiguration = - jibContainerBuilder - .toBuildConfiguration(targetImage) - // Fills in defaults. - .setBaseImageLayersCacheDirectory(Paths.get("ignored")) - .setApplicationLayersCacheDirectory(Paths.get("ignored")) - .build(); + jibContainerBuilder.toBuildConfiguration(spyBuildConfigurationBuilder, containerizer); Assert.assertEquals( jibContainerBuilder.toContainerConfiguration(), @@ -138,10 +151,20 @@ public void testToBuildConfiguration() .retrieve() .orElseThrow(AssertionError::new)); + Mockito.verify(spyBuildConfigurationBuilder) + .setBaseImageLayersCacheDirectory(Paths.get("base/image/layers")); + Mockito.verify(spyBuildConfigurationBuilder) + .setApplicationLayersCacheDirectory(Paths.get("application/layers")); + Assert.assertEquals( Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2), buildConfiguration.getLayerConfigurations()); + Assert.assertEquals(mockExecutorService, buildConfiguration.getExecutorService()); + + buildConfiguration.getEventDispatcher().dispatch(mockJibEvent); + Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); + Assert.assertEquals("jib-core", buildConfiguration.getToolName()); } } diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index 14e29594db..674f8f4ecf 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -12,6 +12,11 @@ docker kill $(docker ps --all --quiet) || true cd github/jib +# Workaround for issue with calling 'docker login'. It defaults to using docker-credential-osxkeychain and errors with: +# Error saving credentials: error storing credentials - err: exit status 1, out: `User interaction is not allowed.` +# TODO: Follow-up with Kokoro about why this is happening. +rm /usr/local/bin/docker-credential-osxkeychain || true + (cd jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd jib-plugins-common; ./gradlew clean build --info --stacktrace) (cd jib-maven-plugin; ./mvnw clean install -B -U -X) diff --git a/proposals/jib_core_library.md b/proposals/jib_core_library.md index 082d64f8e7..dab30c98d7 100644 --- a/proposals/jib_core_library.md +++ b/proposals/jib_core_library.md @@ -51,6 +51,7 @@ Three `TargetImage` types (`RegistryImage`, `DockerDaemonImage`, and `TarImage`) - `Containerizer setExecutorService(ExecutorService)` - `Containerizer setCacheConfiguration(CacheConfiguration)` - `Containerizer setEventHandlers(EventHandlers)` +- `Containerizer setAllowInsecureRegistries(boolean)` ## For Java containers From b14173fbd386d2e6995296dcd46a784fd9e4d8fa Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 8 Oct 2018 11:46:45 -0400 Subject: [PATCH 0277/2020] Adds release scripts for Jib Core. (#1101) --- jib-core/build.gradle | 114 +++++++++++++++++++++++++--- jib-core/kokoro/release_build.sh | 9 +++ jib-core/kokoro/release_publish.sh | 67 ++++++++++++++++ jib-core/kokoro/release_sign.sh | 35 +++++++++ jib-core/scripts/prepare_release.sh | 58 ++++++++++++++ 5 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 jib-core/kokoro/release_build.sh create mode 100644 jib-core/kokoro/release_publish.sh create mode 100644 jib-core/kokoro/release_sign.sh create mode 100644 jib-core/scripts/prepare_release.sh diff --git a/jib-core/build.gradle b/jib-core/build.gradle index dd6bd1c7bf..d6dd2171fc 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -1,10 +1,14 @@ plugins { - id 'java' + id 'java-library' id 'checkstyle' id 'com.github.sherter.google-java-format' version '0.7.1' id 'net.ltgt.apt' version '0.13' id 'net.ltgt.errorprone' version '0.0.13' id 'biz.aQute.bnd.builder' version '4.0.0' + + // For release. + id 'maven' + id 'net.researchgate.release' version '2.7.0' } group 'com.google.cloud.tools' @@ -29,21 +33,21 @@ sourceSets { } configurations { - integrationTestCompile.extendsFrom testCompile + integrationTestImplementation.extendsFrom testImplementation integrationTestRuntime.extendsFrom testRuntime } dependencies { // Make sure these are consistent with jib-maven-plugin. - compile 'com.google.http-client:google-http-client:1.23.0' - compile 'org.apache.commons:commons-compress:1.17' - compile 'com.google.guava:guava:23.5-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.javassist:javassist:3.22.0-GA' + implementation 'com.google.http-client:google-http-client:1.23.0' + implementation 'org.apache.commons:commons-compress:1.17' + implementation 'com.google.guava:guava:23.5-jre' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + implementation 'org.javassist:javassist:3.22.0-GA' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.12.0' - testCompile 'org.slf4j:slf4j-api:1.7.25' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.12.0' + testImplementation 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin apt 'com.uber.nullaway:nullaway:0.4.2' @@ -120,3 +124,93 @@ checkstyle { maxWarnings = 0 } /* CHECKSTYLE */ + +/* RELEASE */ +task sourceJar(type: Jar) { + from sourceSets.main.allJava + classifier 'sources' +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + from javadoc.destinationDir + classifier 'javadoc' +} + +task writePom { + project.afterEvaluate { + def outputFile = file("${project.buildDir}/pom/${project.name}-${project.version}.pom") + outputs.file outputFile + + doLast { + pom { + project { + name 'Jib Core' + description 'Build container images.' + url 'https://github.com/GoogleContainerTools/jib' + inceptionYear '2018' + + scm { + url 'https://github.com/GoogleContainerTools/jib' + connection 'scm:https://github.com/GoogleContainerTools/jib.git' + developerConnection 'scm:git://github.com/GoogleContainerTools/jib.git' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'coollog' + name 'Qingyang Chen' + email 'qingyangc@google.com' + } + developer { + id 'loosebazooka' + name 'Appu Goundan' + email 'appu@google.com' + } + developer { + id 'TadCordle' + name 'Tad Cordle' + email 'tcordle@google.com' + } + developer { + id 'chanseokoh' + name 'Chanseok Oh' + email 'chanseok@google.com' + } + developer { + id 'briandealwis' + name 'Brian de Alwis' + email 'bdealwis@google.com' + } + } + } + }.writeTo(outputFile) + } + } +} + +// For Kokoro sign and releases +task prepareRelease(type: Copy) { + from jar + from sourceJar + from javadocJar + from writePom + into "${project.buildDir}/release-artifacts" + dependsOn build + dependsOn cleanPrepareRelease +} +// Release plugin (git release commits and version updates) +release { + tagTemplate = 'v$version-core' + git { + requireBranch = /^core_release_v\d+.*$/ //regex + } +} +/* RELEASE */ diff --git a/jib-core/kokoro/release_build.sh b/jib-core/kokoro/release_build.sh new file mode 100644 index 0000000000..44d19b9b0f --- /dev/null +++ b/jib-core/kokoro/release_build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Fail on any error. +set -e +# Display commands to stderr. +set -x + +cd github/jib/jib-core +./gradlew prepareRelease diff --git a/jib-core/kokoro/release_publish.sh b/jib-core/kokoro/release_publish.sh new file mode 100644 index 0000000000..48f0ed6017 --- /dev/null +++ b/jib-core/kokoro/release_publish.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Fail on any error. +set -e + +SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/72743_sonatype_username) +SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/72743_sonatype_password) +CREDENTIALS="$SONATYPE_USERNAME:$SONATYPE_PASSWORD" + +# Display commands to stderr. +set -x + +# Goes to the GCS directory. +cd ${KOKORO_GFILE_DIR} + +# Finds the latest ubuntu/release-sign build directory. +LAST_BUILD=$(ls prod/jib/core/ubuntu/release-sign/ | sort -rV | head -1) + +# Finds the bundled jar file in the latest signed artifact directory. +BUNDLED_JAR_FILE=$(find `pwd`/prod/jib/core/ubuntu/release-sign/${LAST_BUILD}/* -type f \( -iname \*-bundle.jar \)) + +# Usage: GetSessionID +# Uses the environment variable CREDENTIALS. +# Stores the Nexus session ID in the given variable. +GetSessionID() { + local __resultvar=$1 + + # Makes a temporary file to store the login cookies. + local cookies_temp=$(mktemp /tmp/sonatype_cookies.XXXXXXX) + + # Sends a login request. + { local login_response=$(curl 'https://oss.sonatype.org/service/local/authentication/login' -X 'GET' -u "$CREDENTIALS" -c ${cookies_temp} 2> /dev/null); } 2> /dev/null + + # Checks if login was successful. + echo ${login_response} | grep -q 'true' + + local login_check=$(echo -n $?) + + if [ "$login_check" -eq "1" ]; then + return 1 + fi + + # Extracts the session ID from the cookies. + local nxsessionid + local nxsessionid_line=$(cat ${cookies_temp} | grep 'NXSESSIONID') + nxsessionid=$(echo -n ${nxsessionid_line} | awk '{print $NF}') + + eval ${__resultvar}="'$nxsessionid'" +} + +# Usage: UploadJAR +# Uploads the bundled JAR file to the Nexus Staging Repository. +UploadJAR() { + curl 'https://oss.sonatype.org/service/local/staging/bundle_upload' -H "Cookie: NXSESSIONID=$1" -H 'Content-Type: multipart/form-data' --compressed -F "file=@$2" +} + +# Gets the session ID. +GetSessionID NXSESSIONID +if [ $? -eq 1 ]; then + echo 'Login failed!' + exit 1 +fi +echo 'Login successful.' + +# Uploads the bundled JAR file. +echo 'Uploading artifact...' +UploadJAR ${NXSESSIONID} ${BUNDLED_JAR_FILE} diff --git a/jib-core/kokoro/release_sign.sh b/jib-core/kokoro/release_sign.sh new file mode 100644 index 0000000000..f858f04331 --- /dev/null +++ b/jib-core/kokoro/release_sign.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Fail on any error. +set -e +# Display commands being run. +set -x + +cd ${KOKORO_GFILE_DIR} +mkdir signed && chmod 777 signed + +# find the latest directory under prod/jib/core/gcp_ubuntu/release-build/ +LAST_BUILD=$(ls prod/jib/core/gcp_ubuntu/release-build/ | sort -rV | head -1) + +# find the jars and the pom in the latest build artifact directory +FILES=$(find `pwd`/prod/jib/core/gcp_ubuntu/release-build/${LAST_BUILD}/* -type f \( -iname \*.jar -o -iname \*.pom \)) + +for f in ${FILES} +do + echo "Processing $f file..." + filename=$(basename "$f") + mv ${f} signed/$filename + if /escalated_sign/escalated_sign.py -j /escalated_sign_jobs -t linux_gpg_sign \ + `pwd`/signed/${filename} + then echo "Signed $filename" + else + echo "Could not sign $filename" + exit 1 + fi +done + +# bundle the artifacts for manual deploy to the Maven staging repository +cd signed +POM_NAME=$(ls *.pom) +BUNDLE_NAME=${POM_NAME%.pom}-bundle.jar +jar -cvf ${BUNDLE_NAME} * diff --git a/jib-core/scripts/prepare_release.sh b/jib-core/scripts/prepare_release.sh new file mode 100644 index 0000000000..95cb0ec463 --- /dev/null +++ b/jib-core/scripts/prepare_release.sh @@ -0,0 +1,58 @@ +#!/bin/bash - +# Usage: ./scripts/prepare_release.sh + +set -e + +Colorize() { + echo "$(tput setff $2)$1$(tput sgr0)" +} + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +DieUsage() { + Die "Usage: ./scripts/prepare_release.sh " +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." +} + +[ $# -ne 2 ] || DieUsage + +EchoGreen '===== RELEASE SETUP SCRIPT =====' + +VERSION=$1 +CheckVersion ${VERSION} + +if [[ $(git status -uno --porcelain) ]]; then + Die 'There are uncommitted changes.' +fi + +# Runs integration tests. +./gradlew integrationTest --info --stacktrace + +# Checks out a new branch for this version release (eg. 1.5.7). +BRANCH=core_release_v${VERSION} +git checkout -b ${BRANCH} + +# Changes the version for release and creates the commits/tags. +echo | ./gradlew release -PreleaseVersion=${VERSION} + +# Pushes the release branch and tag to Github. +git push origin ${BRANCH} +git push origin v${VERSION}-core + +# File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. +EchoGreen 'File a PR for the new release branch:' +echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} From 7614fedec5e9b66b755a01fe423c8d58ea4b5478 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 8 Oct 2018 15:05:53 -0400 Subject: [PATCH 0278/2020] Updates CHANGELOG for v0.9.12. (#1112) --- jib-gradle-plugin/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 173f46813d..45fd38e5cd 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -14,6 +14,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.9.12 + +### Fixed + +- `Cannot access 'image': it is public in ` error ([#1060](https://github.com/GoogleContainerTools/jib/issues/1060)) + ## 0.9.11 ### Added From 29b35f8543e39582ee83eb4e46bc06786f937251 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 9 Oct 2018 11:25:19 -0400 Subject: [PATCH 0279/2020] Rename Image.javaArguments to Image.programArguments (#1114) --- .../jib/builder/steps/BuildImageStep.java | 2 +- .../frontend/JavaDockerContextGenerator.java | 18 ++++++------- .../google/cloud/tools/jib/image/Image.java | 25 ++++++++++--------- .../jib/image/json/ImageToJsonTranslator.java | 2 +- .../jib/image/json/JsonToImageTranslator.java | 2 +- .../configuration/BuildConfigurationTest.java | 6 ++--- .../JavaDockerContextGeneratorTest.java | 4 +-- .../cloud/tools/jib/image/ImageTest.java | 4 +-- .../image/json/ImageToJsonTranslatorTest.java | 2 +- .../tools/jib/gradle/DockerContextTask.java | 2 +- .../tools/jib/maven/DockerContextMojo.java | 2 +- 11 files changed, 35 insertions(+), 34 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 43147e2338..64e07f5f90 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -176,7 +176,7 @@ private Image afterCacheEntrySteps() imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); imageBuilder.setCreated(containerConfiguration.getCreationTime()); imageBuilder.setEntrypoint(containerConfiguration.getEntrypoint()); - imageBuilder.setJavaArguments(containerConfiguration.getProgramArguments()); + imageBuilder.setProgramArguments(containerConfiguration.getProgramArguments()); imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); imageBuilder.addLabels(containerConfiguration.getLabels()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index ffe91b8684..10aaa89af9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -133,7 +133,7 @@ private static String mapToDockerfileString(Map map, String comm @Nullable private String baseImage; private List entrypoint = Collections.emptyList(); - private List javaArguments = Collections.emptyList(); + private List programArguments = Collections.emptyList(); private Map environment = Collections.emptyMap(); private List exposedPorts = Collections.emptyList(); private Map labels = Collections.emptyMap(); @@ -172,7 +172,7 @@ public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfiguration * Sets the base image for the {@code FROM} directive. This must be called before {@link * #generate}. * - * @param baseImage the base image. + * @param baseImage the base image * @return this */ public JavaDockerContextGenerator setBaseImage(String baseImage) { @@ -183,7 +183,7 @@ public JavaDockerContextGenerator setBaseImage(String baseImage) { /** * Sets the entrypoint to be used as the {@code ENTRYPOINT}. * - * @param entrypoint the entrypoint. + * @param entrypoint the entrypoint * @return this */ public JavaDockerContextGenerator setEntrypoint(List entrypoint) { @@ -194,16 +194,16 @@ public JavaDockerContextGenerator setEntrypoint(List entrypoint) { /** * Sets the arguments used in the {@code CMD}. * - * @param javaArguments the list of arguments to pass into main. + * @param programArguments the list of arguments to append to {@code ENTRYPOINT} * @return this */ - public JavaDockerContextGenerator setJavaArguments(List javaArguments) { - this.javaArguments = javaArguments; + public JavaDockerContextGenerator setProgramArguments(List programArguments) { + this.programArguments = programArguments; return this; } /** - * Sets the environment variables + * Sets the environment variables. * * @param environment map from the environment variable name to value * @return this @@ -225,7 +225,7 @@ public JavaDockerContextGenerator setExposedPorts(List exposedPorts) { } /** - * Sets the labels + * Sets the labels. * * @param labels the map of labels * @return this @@ -330,7 +330,7 @@ String makeDockerfile() throws JsonProcessingException { .append("\nENTRYPOINT ") .append(objectMapper.writeValueAsString(entrypoint)) .append("\nCMD ") - .append(objectMapper.writeValueAsString(javaArguments)); + .append(objectMapper.writeValueAsString(programArguments)); return dockerfile.toString(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index f4b99da095..0ce4220cb3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -38,7 +38,7 @@ public static class Builder { @Nullable private Instant created; @Nullable private ImmutableList entrypoint; - @Nullable private ImmutableList javaArguments; + @Nullable private ImmutableList programArguments; @Nullable private ImmutableList exposedPorts; @Nullable private String workingDirectory; @@ -90,13 +90,14 @@ public Builder setEntrypoint(@Nullable List entrypoint) { } /** - * Sets the items in the "Cmd" field in the container configuration (i.e. the main args). + * Sets the items in the "Cmd" field in the container configuration. * - * @param javaArguments the list of main args to add + * @param programArguments the list of arguments to append to the image entrypoint * @return this */ - public Builder setJavaArguments(@Nullable List javaArguments) { - this.javaArguments = (javaArguments == null) ? null : ImmutableList.copyOf(javaArguments); + public Builder setProgramArguments(@Nullable List programArguments) { + this.programArguments = + (programArguments == null) ? null : ImmutableList.copyOf(programArguments); return this; } @@ -177,7 +178,7 @@ public Image build() { historyBuilder.build(), environmentBuilder.build(), entrypoint, - javaArguments, + programArguments, exposedPorts, labelsBuilder.build(), workingDirectory); @@ -203,8 +204,8 @@ public static Builder builder() { /** Initial command to run when running the image. */ @Nullable private final ImmutableList entrypoint; - /** Arguments to pass into main when running the image. */ - @Nullable private final ImmutableList javaArguments; + /** Arguments to append to the image entrypoint when running the image. */ + @Nullable private final ImmutableList programArguments; /** Ports that the container listens on. */ @Nullable private final ImmutableList exposedPorts; @@ -221,7 +222,7 @@ private Image( ImmutableList history, @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, - @Nullable ImmutableList javaArguments, + @Nullable ImmutableList programArguments, @Nullable ImmutableList exposedPorts, @Nullable ImmutableMap labels, @Nullable String workingDirectory) { @@ -230,7 +231,7 @@ private Image( this.history = history; this.environment = environment; this.entrypoint = entrypoint; - this.javaArguments = javaArguments; + this.programArguments = programArguments; this.exposedPorts = exposedPorts; this.labels = labels; this.workingDirectory = workingDirectory; @@ -252,8 +253,8 @@ public ImmutableList getEntrypoint() { } @Nullable - public ImmutableList getJavaArguments() { - return javaArguments; + public ImmutableList getProgramArguments() { + return programArguments; } @Nullable diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index bf8e4987d5..5677fdd6f2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -130,7 +130,7 @@ public Blob getContainerConfigurationBlob() { template.setContainerEntrypoint(image.getEntrypoint()); // Sets the main method arguments. - template.setContainerCmd(image.getJavaArguments()); + template.setContainerCmd(image.getProgramArguments()); // Sets the exposed ports. template.setContainerExposedPorts(portListToMap(image.getExposedPorts())); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index b89447520a..6a6b41453d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -142,7 +142,7 @@ public static Image toImage( } if (containerConfigurationTemplate.getContainerCmd() != null) { - imageBuilder.setJavaArguments(containerConfigurationTemplate.getContainerCmd()); + imageBuilder.setProgramArguments(containerConfigurationTemplate.getContainerCmd()); } if (containerConfigurationTemplate.getContainerExposedPorts() != null) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index e8f23d2d15..550468ff63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -57,7 +57,7 @@ public void testBuilder() throws Exception { Collections.singletonList(() -> Optional.of(Credential.basic("username", "password"))); Instant expectedCreationTime = Instant.ofEpochSecond(10000); List expectedEntrypoint = Arrays.asList("some", "entrypoint"); - List expectedJavaArguments = Arrays.asList("arg1", "arg2"); + List expectedProgramArguments = Arrays.asList("arg1", "arg2"); Map expectedEnvironment = ImmutableMap.of("key", "value"); ImmutableList expectedExposedPorts = ImmutableList.of(Port.tcp(1000), Port.tcp(2000)); Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); @@ -86,7 +86,7 @@ public void testBuilder() throws Exception { ContainerConfiguration.builder() .setCreationTime(expectedCreationTime) .setEntrypoint(expectedEntrypoint) - .setProgramArguments(expectedJavaArguments) + .setProgramArguments(expectedProgramArguments) .setEnvironment(expectedEnvironment) .setExposedPorts(expectedExposedPorts) .setLabels(expectedLabels) @@ -133,7 +133,7 @@ public void testBuilder() throws Exception { .retrieve() .orElseThrow(AssertionError::new)); Assert.assertEquals( - expectedJavaArguments, + expectedProgramArguments, buildConfiguration.getContainerConfiguration().getProgramArguments()); Assert.assertEquals( expectedEnvironment, buildConfiguration.getContainerConfiguration().getEnvironmentMap()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index 47d0d0b296..cdb74a2c09 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -122,7 +122,7 @@ public void testMakeDockerfile() throws IOException { String expectedBaseImage = "somebaseimage"; List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); String expectedMainClass = "SomeMainClass"; - List expectedJavaArguments = Arrays.asList("arg1", "arg2"); + List expectedProgramArguments = Arrays.asList("arg1", "arg2"); Map expectedEnv = ImmutableMap.of("key1", "value1", "key2", "value2"); List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); Map expectedLabels = @@ -154,7 +154,7 @@ public void testMakeDockerfile() throws IOException { .setEntrypoint( JavaEntrypointConstructor.makeDefaultEntrypoint( AbsoluteUnixPath.get("/app"), expectedJvmFlags, expectedMainClass)) - .setJavaArguments(expectedJavaArguments) + .setProgramArguments(expectedProgramArguments) .setEnvironment(expectedEnv) .setExposedPorts(exposedPorts) .setLabels(expectedLabels) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 543d068a48..8a214d74df 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -51,7 +51,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .addEnvironmentVariable("crepecake", "is great") .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) - .setJavaArguments(Arrays.asList("arg1", "arg2")) + .setProgramArguments(Arrays.asList("arg1", "arg2")) .setExposedPorts(ImmutableList.of(Port.tcp(1000), Port.tcp(2000))) .addLayer(mockLayer) .build(); @@ -62,7 +62,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Assert.assertEquals( ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); - Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments()); + Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getProgramArguments()); Assert.assertEquals(ImmutableList.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index edc8e40bb8..3fdf292710 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -58,7 +58,7 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { testImageBuilder.addEnvironmentVariable("VAR1", "VAL1"); testImageBuilder.addEnvironmentVariable("VAR2", "VAL2"); testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); - testImageBuilder.setJavaArguments(Arrays.asList("arg1", "arg2")); + testImageBuilder.setProgramArguments(Arrays.asList("arg1", "arg2")); testImageBuilder.setExposedPorts( ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 8020938e30..9bf766cf37 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -125,7 +125,7 @@ public void generateDockerContext() { new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) .setBaseImage(jibExtension.getFrom().getImage()) .setEntrypoint(entrypoint) - .setJavaArguments(jibExtension.getContainer().getArgs()) + .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(jibExtension.getContainer().getPorts()) .setLabels(jibExtension.getContainer().getLabels()) .generate(Paths.get(targetDir)); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index bbf051dab5..9b0d5ffe89 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -86,7 +86,7 @@ public void execute() throws MojoExecutionException { new JavaDockerContextGenerator(mavenProjectProperties.getJavaLayerConfigurations()) .setBaseImage(PluginConfigurationProcessor.getBaseImage(this)) .setEntrypoint(entrypoint) - .setJavaArguments(getArgs()) + .setProgramArguments(getArgs()) .setExposedPorts(getExposedPorts()) .setLabels(getLabels()) .generate(Paths.get(targetDir)); From 1ce9c0b6c9ec73d362002ce1e0f10b6e33c62921 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 9 Oct 2018 15:53:37 -0400 Subject: [PATCH 0280/2020] Include last modified time in cache selector digests instead of metadata (#1107) --- .../google/cloud/tools/jib/cache/Cache.java | 45 ++----- .../cloud/tools/jib/cache/CacheEntry.java | 10 -- .../cloud/tools/jib/cache/CacheStorage.java | 5 +- .../tools/jib/cache/DefaultCacheEntry.java | 27 +--- .../tools/jib/cache/DefaultCacheStorage.java | 3 +- .../jib/cache/DefaultCacheStorageFiles.java | 30 ----- .../jib/cache/DefaultCacheStorageReader.java | 10 -- .../jib/cache/DefaultCacheStorageWriter.java | 28 +--- .../jib/cache/LastModifiedTimeMetadata.java | 88 ------------ .../tools/jib/cache/LayerEntriesSelector.java | 28 +++- .../jib/cache/UncompressedCacheWrite.java | 18 +-- .../jib/builder/steps/BuildImageStepTest.java | 6 - .../cloud/tools/jib/cache/CacheTest.java | 63 +++------ .../jib/cache/DefaultCacheEntryTest.java | 23 +--- .../cache/DefaultCacheStorageFilesTest.java | 28 ---- .../cache/DefaultCacheStorageReaderTest.java | 16 --- .../cache/DefaultCacheStorageWriterTest.java | 12 +- .../cache/LastModifiedTimeMetadataTest.java | 126 ------------------ .../jib/cache/LayerEntriesSelectorTest.java | 91 +++++++++---- ...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz | 0 .../src/test/resources/cache/metadata-v2.json | 34 ----- .../src/test/resources/cache/metadata-v3.json | 34 ----- .../src/test/resources/cache/metadata.json | 29 ---- 23 files changed, 118 insertions(+), 636 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadata.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java delete mode 100644 jib-core/src/test/resources/cache/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz delete mode 100644 jib-core/src/test/resources/cache/metadata-v2.json delete mode 100644 jib-core/src/test/resources/cache/metadata-v3.json delete mode 100644 jib-core/src/test/resources/cache/metadata.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 1778244ed7..561b40c4dd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -23,15 +23,14 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.FileTime; import java.util.Optional; import javax.annotation.concurrent.Immutable; /** * Cache for storing data to be shared between Jib executions. * - *

@@ -56,11 +60,13 @@ static class LayerEntryTemplate implements JsonTemplate, Comparable toSortedJsonTemplates(List layerEntries) { + static List toSortedJsonTemplates(List layerEntries) + throws IOException { List jsonTemplates = new ArrayList<>(); for (LayerEntry entry : layerEntries) { jsonTemplates.add(new LayerEntryTemplate(entry)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java index d8dfc2c2c9..1252e99e15 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java @@ -22,8 +22,7 @@ import javax.annotation.concurrent.Immutable; /** - * Represents uncompressed layer data to write to the cache, along with an additional selector - * digest and metadata. + * Represents uncompressed layer data to write to the cache with an additional selector digest. * *

Implementation is immutable and thread-safe. */ @@ -32,12 +31,10 @@ class UncompressedCacheWrite { private final Blob uncompressedLayerBlob; private final DescriptorDigest selector; - private final Blob metadataBlob; - UncompressedCacheWrite(Blob uncompressedLayerBlob, DescriptorDigest selector, Blob metadataBlob) { + UncompressedCacheWrite(Blob uncompressedLayerBlob, DescriptorDigest selector) { this.uncompressedLayerBlob = uncompressedLayerBlob; this.selector = selector; - this.metadataBlob = metadataBlob; } /** @@ -61,15 +58,4 @@ Blob getUncompressedLayerBlob() { Optional getSelector() { return Optional.ofNullable(selector); } - - /** - * Gets the optional {@link Blob} to write as the arbitrary layer metadata. - * - *

For example, the metadata could contain last modified time, layer types, layer sources, etc. - * - * @return the metadata {@link Blob} - */ - Optional getMetadataBlob() { - return Optional.ofNullable(metadataBlob); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index d437d0e4ed..53db24a2fc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -32,7 +32,6 @@ import com.google.common.util.concurrent.MoreExecutors; import java.security.DigestException; import java.time.Instant; -import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.junit.Assert; @@ -85,11 +84,6 @@ public long getLayerSize() { public Blob getLayerBlob() { return Blobs.from("ignored"); } - - @Override - public Optional getMetadataBlob() { - return Optional.empty(); - } }; Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 1a4b5e0f1c..d40cf93d81 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -158,8 +158,8 @@ public void testWriteCompressed_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1NoMetadata(cache.writeCompressedLayer(compress(layerBlob1))); - verifyIsLayer1NoMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.writeCompressedLayer(compress(layerBlob1))); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } @@ -168,8 +168,8 @@ public void testWriteUncompressedWithLayerEntries_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); - verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } @@ -178,8 +178,8 @@ public void testWriteUncompressedWithLayerEntries_retrieveByLayerEntries() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); - verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer1(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); // A source file modification results in the cached layer to be out-of-date and not retrieved. @@ -192,44 +192,16 @@ public void testWriteUncompressedWithLayerEntries_retrieveByLayerEntries() public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1WithMetadata(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); - verifyIsLayer2WithMetadata(cache.writeUncompressedLayer(layerBlob2, layerEntries2)); - verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); - verifyIsLayer2WithMetadata(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); - verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); - verifyIsLayer2WithMetadata(cache.retrieve(layerEntries2).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); + verifyIsLayer2(cache.writeUncompressedLayer(layerBlob2, layerEntries2)); + verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); + verifyIsLayer2(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new)); + verifyIsLayer1(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); + verifyIsLayer2(cache.retrieve(layerEntries2).orElseThrow(AssertionError::new)); } /** - * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}, and - * that no metadata was written. - * - * @param cacheEntry the {@link CacheEntry} to verify - * @throws IOException if an I/O exception occurs - */ - private void verifyIsLayer1NoMetadata(CacheEntry cacheEntry) throws IOException { - verifyIsLayer1(cacheEntry); - Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); - } - - /** - * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}, and - * that its metadata was written. - * - * @param cacheEntry the {@link CacheEntry} to verify - * @throws IOException if an I/O exception occurs - */ - private void verifyIsLayer1WithMetadata(CacheEntry cacheEntry) throws IOException { - verifyIsLayer1(cacheEntry); - Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); - Assert.assertEquals( - Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries1)), - Blobs.writeToString(cacheEntry.getMetadataBlob().get())); - } - - /** - * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}. Does - * not check the metadata. + * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}. * * @param cacheEntry the {@link CacheEntry} to verify * @throws IOException if an I/O exception occurs @@ -242,20 +214,15 @@ private void verifyIsLayer1(CacheEntry cacheEntry) throws IOException { } /** - * Verifies that {@code cacheEntry} corresponds to the second fake layer in {@link #setUp}, and - * that its metadata was written. + * Verifies that {@code cacheEntry} corresponds to the second fake layer in {@link #setUp}. * * @param cacheEntry the {@link CacheEntry} to verify * @throws IOException if an I/O exception occurs */ - private void verifyIsLayer2WithMetadata(CacheEntry cacheEntry) throws IOException { + private void verifyIsLayer2(CacheEntry cacheEntry) throws IOException { Assert.assertEquals("layerBlob2", Blobs.writeToString(decompress(cacheEntry.getLayerBlob()))); Assert.assertEquals(layerDigest2, cacheEntry.getLayerDigest()); Assert.assertEquals(layerDiffId2, cacheEntry.getLayerDiffId()); Assert.assertEquals(layerSize2, cacheEntry.getLayerSize()); - Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); - Assert.assertEquals( - Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries2)), - Blobs.writeToString(cacheEntry.getMetadataBlob().get())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java index 1999801faf..893868a1b7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java @@ -64,7 +64,7 @@ public void testBuilder_fail() { } @Test - public void testBuilder_noMetadataBlob() throws IOException { + public void testBuilder_pass() throws IOException { DefaultCacheEntry.Builder cacheEntryBuilder = DefaultCacheEntry.builder() .setLayerDigest(mockLayerDigest) @@ -78,26 +78,5 @@ public void testBuilder_noMetadataBlob() throws IOException { Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); Assert.assertEquals(1337, cacheEntry.getLayerSize()); Assert.assertEquals("layerBlob", Blobs.writeToString(cacheEntry.getLayerBlob())); - Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); - } - - @Test - public void testBuilder_withMetadataBlob() throws IOException { - DefaultCacheEntry.Builder cacheEntryBuilder = - DefaultCacheEntry.builder() - .setLayerDigest(mockLayerDigest) - .setLayerDiffId(mockLayerDiffId) - .setLayerSize(1337) - .setLayerBlob(Blobs.from("layerBlob")); - Assert.assertFalse(cacheEntryBuilder.hasMetadataBlob()); - cacheEntryBuilder.setMetadataBlob(Blobs.from("metadataBlob")); - Assert.assertTrue(cacheEntryBuilder.hasMetadataBlob()); - CacheEntry cacheEntry = cacheEntryBuilder.build(); - Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); - Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); - Assert.assertEquals(1337, cacheEntry.getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheEntry.getLayerBlob())); - Assert.assertEquals( - "metadataBlob", Blobs.writeToString(cacheEntry.getMetadataBlob().orElse(null))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java index d3e52c82bc..5fc21292a2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java @@ -40,13 +40,6 @@ public void testIsLayerFile() { Assert.assertFalse(DefaultCacheStorageFiles.isLayerFile(Paths.get("is.not.layer.file"))); } - @Test - public void testIsMetadataFile() { - Assert.assertTrue(DefaultCacheStorageFiles.isMetadataFile(Paths.get("metadata"))); - Assert.assertTrue(DefaultCacheStorageFiles.isMetadataFile(Paths.get("is", "metadata"))); - Assert.assertFalse(DefaultCacheStorageFiles.isMetadataFile(Paths.get("not.metadata"))); - } - @Test public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( @@ -121,27 +114,6 @@ public void testGetLayerFilename() throws DigestException { testDefaultCacheStorageFiles.getLayerFilename(diffId)); } - @Test - public void testGetMetadataFile() throws DigestException { - DescriptorDigest layerDigest = - DescriptorDigest.fromHash( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - - Assert.assertEquals( - Paths.get( - "cache", - "directory", - "layers", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "metadata"), - testDefaultCacheStorageFiles.getMetadataFile(layerDigest)); - } - - @Test - public void testGetMetadataFilename() { - Assert.assertEquals("metadata", testDefaultCacheStorageFiles.getMetadataFilename()); - } - @Test public void testGetSelectorFile() throws DigestException { DescriptorDigest selector = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java index 5ffadae803..1fb88e6cb3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java @@ -105,22 +105,6 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Assert.assertEquals(layerDiffId, optionalCacheEntry.get().getLayerDiffId()); Assert.assertEquals("layerBlob".length(), optionalCacheEntry.get().getLayerSize()); Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCacheEntry.get().getLayerBlob())); - Assert.assertFalse(optionalCacheEntry.get().getMetadataBlob().isPresent()); - - // Adds a metadata file. - Blobs.writeToFileWithLock( - Blobs.from("metadata"), defaultCacheStorageFiles.getMetadataFile(layerDigest)); - - // Checks that the CacheEntry is retrieved correctly with the metadata. - optionalCacheEntry = defaultCacheStorageReader.retrieve(layerDigest); - Assert.assertTrue(optionalCacheEntry.isPresent()); - Assert.assertEquals(layerDigest, optionalCacheEntry.get().getLayerDigest()); - Assert.assertEquals(layerDiffId, optionalCacheEntry.get().getLayerDiffId()); - Assert.assertEquals("layerBlob".length(), optionalCacheEntry.get().getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCacheEntry.get().getLayerBlob())); - Assert.assertTrue(optionalCacheEntry.get().getMetadataBlob().isPresent()); - Assert.assertEquals( - "metadata", Blobs.writeToString(optionalCacheEntry.get().getMetadataBlob().get())); // Checks that multiple .layer files means the cache is corrupted. Files.createFile(defaultCacheStorageFiles.getLayerFile(layerDigest, layerDigest)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java index 086e17e717..8da7aab6d9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java @@ -79,7 +79,6 @@ public void testWrite_compressed() throws IOException { .write(compress(uncompressedLayerBlob)); verifyCacheEntry(cacheEntry, uncompressedLayerBlob); - Assert.assertFalse(cacheEntry.getMetadataBlob().isPresent()); } @Test @@ -87,26 +86,17 @@ public void testWrite_uncompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); DescriptorDigest layerDigest = getCompressedBlobDescriptor(uncompressedLayerBlob).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")); - Blob metadataBlob = Blobs.from("metadata"); CacheEntry cacheEntry = new DefaultCacheStorageWriter(defaultCacheStorageFiles) - .write(new UncompressedCacheWrite(uncompressedLayerBlob, selector, metadataBlob)); + .write(new UncompressedCacheWrite(uncompressedLayerBlob, selector)); verifyCacheEntry(cacheEntry, uncompressedLayerBlob); - // Verifies cacheEntry is correct. - Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent()); - Assert.assertArrayEquals( - Blobs.writeToByteArray(metadataBlob), - Blobs.writeToByteArray(cacheEntry.getMetadataBlob().get())); - // Verifies that the files are present. Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); Assert.assertTrue(Files.exists(selectorFile)); Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); - Assert.assertTrue( - Files.exists(defaultCacheStorageFiles.getMetadataFile(cacheEntry.getLayerDigest()))); } private void verifyCacheEntry(CacheEntry cacheEntry, Blob uncompressedLayerBlob) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java deleted file mode 100644 index 349efc7d6b..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LastModifiedTimeMetadataTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -/** Tests for {@link LastModifiedTimeMetadata}. */ -public class LastModifiedTimeMetadataTest { - - private static LayerEntry copyFile(Path source, Path destination, FileTime newLastModifiedTime) - throws IOException { - Files.createDirectories(destination.getParent()); - Files.copy(source, destination); - Files.setLastModifiedTime(destination, newLastModifiedTime); - return new LayerEntry(destination, AbsoluteUnixPath.get("/ignored")); - } - - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private List layerEntries = new ArrayList<>(); - - @Before - public void setUp() throws IOException, URISyntaxException { - Path originalDirectory = Paths.get(Resources.getResource("layer").toURI()); - Path directory = temporaryFolder.newFolder().toPath(); - - layerEntries.add( - copyFile( - originalDirectory.resolve("a/b/bar"), - directory.resolve("a/b/bar"), - FileTime.fromMillis(1000))); - layerEntries.add( - copyFile( - originalDirectory.resolve("c/cat"), - directory.resolve("c/cat"), - FileTime.fromMillis(2000))); - layerEntries.add( - copyFile( - originalDirectory.resolve("foo"), directory.resolve("foo"), FileTime.fromMillis(1500))); - } - - @Test - public void testGetLastModifiedTime_layerEntries() throws IOException { - Assert.assertEquals( - FileTime.fromMillis(2000), - LastModifiedTimeMetadata.getLastModifiedTime(ImmutableList.copyOf(layerEntries))); - } - - @Test - public void testGetLastModifiedTime_noEntries() throws IOException { - Assert.assertEquals( - FileTime.from(Instant.MIN), - LastModifiedTimeMetadata.getLastModifiedTime(ImmutableList.of())); - } - - @Test - public void testGetLastModifiedTime_cacheEntry() throws IOException { - DescriptorDigest ignored = Mockito.mock(DescriptorDigest.class); - CacheEntry cacheEntry = - DefaultCacheEntry.builder() - .setLayerDigest(ignored) - .setLayerDiffId(ignored) - .setLayerSize(0) - .setLayerBlob(Blobs.from("ignored")) - .setMetadataBlob(Blobs.from(Instant.ofEpochMilli(1000).toString())) - .build(); - Assert.assertEquals( - FileTime.from(Instant.ofEpochMilli(1000)), - LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry).orElseThrow(AssertionError::new)); - } - - @Test - public void testGetLastModifiedTime_cacheEntry_noMetadata() throws IOException { - DescriptorDigest ignored = Mockito.mock(DescriptorDigest.class); - CacheEntry cacheEntry = - DefaultCacheEntry.builder() - .setLayerDigest(ignored) - .setLayerDiffId(ignored) - .setLayerSize(0) - .setLayerBlob(Blobs.from("ignored")) - .build(); - Assert.assertFalse(LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry).isPresent()); - } - - @Test - public void testGenerateMetadata() throws IOException { - Assert.assertEquals( - Instant.ofEpochMilli(2000).toString(), - Blobs.writeToString( - LastModifiedTimeMetadata.generateMetadata(ImmutableList.copyOf(layerEntries)))); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 989f3bd339..4e1f3e6b68 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -24,49 +24,62 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import java.io.IOException; -import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Tests for {@link LayerEntriesSelector}. */ public class LayerEntriesSelectorTest { - private static final LayerEntry TEST_LAYER_ENTRY_1 = - new LayerEntry(Paths.get("source", "file"), AbsoluteUnixPath.get("/extraction/path")); - private static final LayerEntry TEST_LAYER_ENTRY_2 = - new LayerEntry(Paths.get("source", "file", "two"), AbsoluteUnixPath.get("/extraction/path")); - private static final LayerEntry TEST_LAYER_ENTRY_3 = - new LayerEntry(Paths.get("source", "gile"), AbsoluteUnixPath.get("/extraction/path")); - private static final LayerEntry TEST_LAYER_ENTRY_4 = - new LayerEntry(Paths.get("source", "gile"), AbsoluteUnixPath.get("/extraction/patha")); - - private static final ImmutableList OUT_OF_ORDER_LAYER_ENTRIES = - ImmutableList.of( - TEST_LAYER_ENTRY_4, TEST_LAYER_ENTRY_2, TEST_LAYER_ENTRY_3, TEST_LAYER_ENTRY_1); - private static final ImmutableList IN_ORDER_LAYER_ENTRIES = - ImmutableList.of( - TEST_LAYER_ENTRY_1, TEST_LAYER_ENTRY_2, TEST_LAYER_ENTRY_3, TEST_LAYER_ENTRY_4); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private ImmutableList outOfOrderLayerEntries; + private ImmutableList inOrderLayerEntries; private static ImmutableList toLayerEntryTemplates( - ImmutableList layerEntries) { - return layerEntries - .stream() - .map(LayerEntryTemplate::new) - .collect(ImmutableList.toImmutableList()); + ImmutableList layerEntries) throws IOException { + ImmutableList.Builder builder = ImmutableList.builder(); + for (LayerEntry layerEntry : layerEntries) { + builder.add(new LayerEntryTemplate(layerEntry)); + } + return builder.build(); + } + + @Before + public void setUp() throws IOException { + Path folder = temporaryFolder.newFolder().toPath(); + Path file1 = Files.createDirectory(folder.resolve("files")); + Path file2 = Files.createFile(folder.resolve("files").resolve("two")); + Path file3 = Files.createFile(folder.resolve("gile")); + + LayerEntry testLayerEntry1 = new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry2 = new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry3 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry4 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); + outOfOrderLayerEntries = + ImmutableList.of(testLayerEntry4, testLayerEntry2, testLayerEntry3, testLayerEntry1); + inOrderLayerEntries = + ImmutableList.of(testLayerEntry1, testLayerEntry2, testLayerEntry3, testLayerEntry4); } @Test - public void testLayerEntryTemplate_compareTo() { + public void testLayerEntryTemplate_compareTo() throws IOException { Assert.assertEquals( - toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES), - ImmutableList.sortedCopyOf(toLayerEntryTemplates(OUT_OF_ORDER_LAYER_ENTRIES))); + toLayerEntryTemplates(inOrderLayerEntries), + ImmutableList.sortedCopyOf(toLayerEntryTemplates(outOfOrderLayerEntries))); } @Test - public void testToSortedJsonTemplates() { + public void testToSortedJsonTemplates() throws IOException { Assert.assertEquals( - toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES), - LayerEntriesSelector.toSortedJsonTemplates(OUT_OF_ORDER_LAYER_ENTRIES)); + toLayerEntryTemplates(inOrderLayerEntries), + LayerEntriesSelector.toSortedJsonTemplates(outOfOrderLayerEntries)); } @Test @@ -82,10 +95,30 @@ public void testGenerateSelector_empty() throws IOException { @Test public void testGenerateSelector() throws IOException { DescriptorDigest expectedSelector = - JsonTemplateMapper.toBlob(toLayerEntryTemplates(IN_ORDER_LAYER_ENTRIES)) + JsonTemplateMapper.toBlob(toLayerEntryTemplates(inOrderLayerEntries)) .writeTo(ByteStreams.nullOutputStream()) .getDigest(); Assert.assertEquals( - expectedSelector, LayerEntriesSelector.generateSelector(OUT_OF_ORDER_LAYER_ENTRIES)); + expectedSelector, LayerEntriesSelector.generateSelector(outOfOrderLayerEntries)); + } + + @Test + public void testGenerateSelector_fileModified() throws IOException { + Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); + Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); + LayerEntry layerEntry = new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); + DescriptorDigest expectedSelector = + LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry)); + + // Verify that changing modified time generates a different selector + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.ofEpochSecond(1))); + Assert.assertNotEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry))); + + // Verify that changing modified time back generates same selector + Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); + Assert.assertEquals( + expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry))); } } diff --git a/jib-core/src/test/resources/cache/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz b/jib-core/src/test/resources/cache/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/jib-core/src/test/resources/cache/metadata-v2.json b/jib-core/src/test/resources/cache/metadata-v2.json deleted file mode 100644 index cdb6049a7b..0000000000 --- a/jib-core/src/test/resources/cache/metadata-v2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "layers": [ - { - "reference": { - "size": 1000, - "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" - } - }, - { - "reference": { - "size": 2000, - "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", - "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" - } - }, - { - "reference": { - "size": 0, - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "properties": { - "layerEntries": [ - { - "sourceFiles": ["some/source/directory"], - "extractionPath": "some/extraction/path" - } - ], - "lastModifiedTime": 1000 - } - } - ] -} \ No newline at end of file diff --git a/jib-core/src/test/resources/cache/metadata-v3.json b/jib-core/src/test/resources/cache/metadata-v3.json deleted file mode 100644 index 03985f307c..0000000000 --- a/jib-core/src/test/resources/cache/metadata-v3.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "layers": [ - { - "reference": { - "size": 1000, - "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" - } - }, - { - "reference": { - "size": 2000, - "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", - "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" - } - }, - { - "reference": { - "size": 0, - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "properties": { - "layerEntries": [ - { - "sourceFile": "/some/source/path", - "extractionPath": "/some/extraction/path" - } - ], - "lastModifiedTime": 1000 - } - } - ] -} \ No newline at end of file diff --git a/jib-core/src/test/resources/cache/metadata.json b/jib-core/src/test/resources/cache/metadata.json deleted file mode 100644 index fb2ed7fa1b..0000000000 --- a/jib-core/src/test/resources/cache/metadata.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "layers": [ - { - "reference": { - "size": 1000, - "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - "diffId": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647" - } - }, - { - "reference": { - "size": 2000, - "digest": "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", - "diffId": "sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372" - } - }, - { - "reference": { - "size": 0, - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "diffId": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "properties": { - "sourceFiles": ["some/source/directory"], - "lastModifiedTime": 1000 - } - } - ] -} \ No newline at end of file From 2b92fa8b9109be870d87292dc3c3e096bfbd4443 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 9 Oct 2018 16:54:07 -0400 Subject: [PATCH 0281/2020] Adds some more required parameters for Jib Core. (#1116) * Adds JibContainerBuilder#setFormat. * Adds JibContainerBuilder#setCreationTime. * Adds Containerizer#setToolName. * Adds Containerizer#withAdditionalTag. * Updates Jib Core proposal. --- .../cloud/tools/jib/api/Containerizer.java | 47 +++++++++++++++++++ .../tools/jib/api/JibContainerBuilder.java | 36 ++++++++++++-- .../cloud/tools/jib/api/RegistryImage.java | 2 +- .../cloud/tools/jib/image/ImageFormat.java | 6 ++- .../tools/jib/api/ContainerizerTest.java | 13 ++++- .../jib/api/JibContainerBuilderTest.java | 31 +++++++++++- proposals/jib_core_library.md | 4 ++ 7 files changed, 131 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index e4c9a160d8..a98ea7f4ae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -20,10 +20,14 @@ import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.UserCacheHome; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Nullable; @@ -40,6 +44,8 @@ public class Containerizer { public static final Path DEFAULT_BASE_CACHE_DIRECTORY = UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); + private static final String DEFAULT_TOOL_NAME = "jib-core"; + /** * Gets a new {@link Containerizer} that containerizes to a container registry. * @@ -72,17 +78,37 @@ public static Containerizer to(TarImage tarImage) { } private final TargetImage targetImage; + private final Set additionalTags = new HashSet<>(); @Nullable private ExecutorService executorService; private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; @Nullable private Path applicationLayersCacheDirectory; @Nullable private EventHandlers eventHandlers; private boolean allowInsecureRegistries = false; + private String toolName = DEFAULT_TOOL_NAME; /** Instantiate with {@link #to}. */ private Containerizer(TargetImage targetImage) { this.targetImage = targetImage; } + /** + * Adds an additional tag to tag the target image with. For example, the following would + * containerize to both {@code gcr.io/my-project/my-image:tag} and {@code + * gcr.io/my-project/my-image:tag2}: + * + *

{@code
+   * Containerizer.to(RegistryImage.named("gcr.io/my-project/my-image:tag")).withAdditionalTag("tag2");
+   * }
+ * + * @param tag the additional tag to push to + * @return this + */ + public Containerizer withAdditionalTag(String tag) { + Preconditions.checkArgument(ImageReference.isValidTag(tag)); + additionalTags.add(tag); + return this; + } + /** * Sets the {@link ExecutorService} Jib executes on. Jib, by default, uses {@link * Executors#newCachedThreadPool}. @@ -143,10 +169,27 @@ public Containerizer setAllowInsecureRegistries(boolean allowInsecureRegistries) return this; } + /** + * Sets the name of the tool that is using Jib Core. The tool name is sent as part of the {@code + * User-Agent} in registry requests and set as the {@code created_by} in the container layer + * history. Defaults to {@code jib-core}. + * + * @param toolName the name of the tool using this library + * @return this + */ + public Containerizer setToolName(String toolName) { + this.toolName = toolName; + return this; + } + TargetImage getTargetImage() { return targetImage; } + Set getAdditionalTags() { + return additionalTags; + } + Optional getExecutorService() { return Optional.ofNullable(executorService); } @@ -177,4 +220,8 @@ Optional getEventHandlers() { boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } + + String getToolName() { + return toolName; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index f75aae5951..e7f9736ace 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -25,10 +25,12 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageFormat; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -64,6 +66,8 @@ public class JibContainerBuilder { private Map labels = new HashMap<>(); @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; + private ImageFormat imageFormat = ImageFormat.Docker; + private Instant creationTime = Instant.EPOCH; /** Instantiate with {@link Jib#from}. */ JibContainerBuilder(SourceImage baseImage) { @@ -305,6 +309,29 @@ public JibContainerBuilder addLabel(String key, String value) { return this; } + /** + * Sets the format to build the container image as. Use {@link ImageFormat#Docker} for Docker V2.2 + * or {@link ImageFormat#OCI} for OCI. + * + * @param imageFormat the {@link ImageFormat} + * @return this + */ + public JibContainerBuilder setFormat(ImageFormat imageFormat) { + this.imageFormat = imageFormat; + return this; + } + + /** + * Sets the container image creation time. The default is {@link Instant#EPOCH}. + * + * @param creationTime the container image creation time + * @return this + */ + public JibContainerBuilder setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + return this; + } + /** * Builds the container(s). * @@ -343,11 +370,14 @@ BuildConfiguration toBuildConfiguration( buildConfigurationBuilder .setBaseImageConfiguration(baseImage.toImageConfiguration()) .setTargetImageConfiguration(containerizer.getTargetImage().toImageConfiguration()) + .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) .setApplicationLayersCacheDirectory(containerizer.getApplicationLayersCacheDirectory()) .setContainerConfiguration(toContainerConfiguration()) .setLayerConfigurations(layerConfigurations) - .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()); + .setTargetFormat(imageFormat.getManifestTemplateClass()) + .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) + .setToolName(containerizer.getToolName()); containerizer.getExecutorService().ifPresent(buildConfigurationBuilder::setExecutorService); @@ -358,9 +388,6 @@ BuildConfiguration toBuildConfiguration( buildConfigurationBuilder.setEventDispatcher( new DefaultEventDispatcher(eventHandlers))); - // TODO: Allow users to configure this. - buildConfigurationBuilder.setToolName("jib-core"); - return buildConfigurationBuilder.build(); } @@ -372,6 +399,7 @@ ContainerConfiguration toContainerConfiguration() { .setEnvironment(environment) .setExposedPorts(ports) .setLabels(labels) + .setCreationTime(creationTime) .build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 332c786c8a..52f5912c03 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -66,7 +66,7 @@ public static RegistryImage named(String imageReference) throws InvalidImageRefe } private final ImageReference imageReference; - private List credentialRetrievers = new ArrayList<>(); + private final List credentialRetrievers = new ArrayList<>(); /** Instantiate with {@link #named}. */ private RegistryImage(ImageReference imageReference) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java index b5ff8dcb4a..129d25a3e1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java @@ -20,9 +20,13 @@ import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -/** Enumeration of {@link BuildableManifestTemplate}s. */ +/** Enumeration of {@link BuildableManifestTemplate}s that indicates the format of the image. */ public enum ImageFormat { + + /** @see
Docker V2.2 */ Docker(V22ManifestTemplate.class), + + /** @see OCI */ OCI(OCIManifestTemplate.class); private final Class manifestTemplateClass; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index 02fe1003c6..ecc36e71d2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; import java.util.concurrent.ExecutorService; import org.junit.Assert; @@ -50,6 +51,7 @@ public void testTo() throws CacheDirectoryCreationException { private void verifyTo(Containerizer containerizer, TargetImage expectedTargetImage) throws CacheDirectoryCreationException { Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); + Assert.assertTrue(containerizer.getAdditionalTags().isEmpty()); Assert.assertFalse(containerizer.getExecutorService().isPresent()); Assert.assertFalse(containerizer.getEventHandlers().isPresent()); Assert.assertEquals( @@ -58,14 +60,21 @@ private void verifyTo(Containerizer containerizer, TargetImage expectedTargetIma Assert.assertNotEquals( Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, containerizer.getApplicationLayersCacheDirectory()); + Assert.assertFalse(containerizer.getAllowInsecureRegistries()); + Assert.assertEquals("jib-core", containerizer.getToolName()); containerizer + .withAdditionalTag("tag1") + .withAdditionalTag("tag2") .setExecutorService(mockExecutorService) .setEventHandlers(mockEventHandlers) .setBaseImageLayersCache(Paths.get("base/image/layers")) - .setApplicationLayersCache(Paths.get("application/layers")); + .setApplicationLayersCache(Paths.get("application/layers")) + .setAllowInsecureRegistries(true) + .setToolName("tool"); Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); + Assert.assertEquals(ImmutableSet.of("tag1", "tag2"), containerizer.getAdditionalTags()); Assert.assertTrue(containerizer.getExecutorService().isPresent()); Assert.assertEquals(mockExecutorService, containerizer.getExecutorService().get()); Assert.assertTrue(containerizer.getEventHandlers().isPresent()); @@ -74,5 +83,7 @@ private void verifyTo(Containerizer containerizer, TargetImage expectedTargetIma Paths.get("base/image/layers"), containerizer.getBaseImageLayersCacheDirectory()); Assert.assertEquals( Paths.get("application/layers"), containerizer.getApplicationLayersCacheDirectory()); + Assert.assertTrue(containerizer.getAllowInsecureRegistries()); + Assert.assertEquals("tool", containerizer.getToolName()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 6901a0252e..6a52caef0f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -25,12 +25,15 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.nio.file.Paths; +import java.time.Instant; import java.util.Arrays; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; @@ -62,7 +65,8 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(Arrays.asList(Port.tcp(1234), Port.udp(5678))) .setLabels(ImmutableMap.of("key", "value")) - .setProgramArguments(Arrays.asList("program", "arguments")); + .setProgramArguments(Arrays.asList("program", "arguments")) + .setCreationTime(Instant.ofEpochMilli(1000)); ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); @@ -73,6 +77,7 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep Assert.assertEquals(ImmutableMap.of("key", "value"), containerConfiguration.getLabels()); Assert.assertEquals( Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + Assert.assertEquals(Instant.ofEpochMilli(1000), containerConfiguration.getCreationTime()); } @Test @@ -100,6 +105,7 @@ public void testToContainerConfiguration_add() { ImmutableMap.of("key", "value", "added", "label"), containerConfiguration.getLabels()); Assert.assertEquals( Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); + Assert.assertEquals(Instant.EPOCH, containerConfiguration.getCreationTime()); } @Test @@ -151,6 +157,8 @@ public void testToBuildConfiguration() .retrieve() .orElseThrow(AssertionError::new)); + Assert.assertEquals(ImmutableSet.of("latest"), buildConfiguration.getAllTargetImageTags()); + Mockito.verify(spyBuildConfigurationBuilder) .setBaseImageLayersCacheDirectory(Paths.get("base/image/layers")); Mockito.verify(spyBuildConfigurationBuilder) @@ -166,5 +174,26 @@ public void testToBuildConfiguration() Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); Assert.assertEquals("jib-core", buildConfiguration.getToolName()); + + Assert.assertSame( + ImageFormat.Docker.getManifestTemplateClass(), buildConfiguration.getTargetFormat()); + + Assert.assertEquals("jib-core", buildConfiguration.getToolName()); + + // Changes jibContainerBuilder. + buildConfiguration = + jibContainerBuilder + .setFormat(ImageFormat.OCI) + .toBuildConfiguration( + spyBuildConfigurationBuilder, + containerizer + .withAdditionalTag("tag1") + .withAdditionalTag("tag2") + .setToolName("toolName")); + Assert.assertSame( + ImageFormat.OCI.getManifestTemplateClass(), buildConfiguration.getTargetFormat()); + Assert.assertEquals( + ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); + Assert.assertEquals("toolName", buildConfiguration.getToolName()); } } diff --git a/proposals/jib_core_library.md b/proposals/jib_core_library.md index dab30c98d7..08bdab6dbc 100644 --- a/proposals/jib_core_library.md +++ b/proposals/jib_core_library.md @@ -26,6 +26,8 @@ Design for Jib Core as a Java library for building container images. - `JibContainerBuilder addExposedPort(Port port)` - `JibContainerBuilder setLabels(Map labelMap)` - `JibContainerBuilder addLabel(String key, String value)` +- `JibContainerBuilder setFormat(ImageFormat)` +- `JibContainerBuilder setCreationTime(Instant creationTime)` - `JibContainer containerize(Containerizer)` Three `TargetImage` types (`RegistryImage`, `DockerDaemonImage`, and `TarImage`) define the 3 different targets Jib can build to. @@ -48,10 +50,12 @@ Three `TargetImage` types (`RegistryImage`, `DockerDaemonImage`, and `TarImage`) - `static Containerizer to(RegistryImage)` - `static Containerizer to(DockerDaemonImage)` - `static Containerizer to(TarImage)` +- `Containerizer withAdditionalTag(String tag)` - `Containerizer setExecutorService(ExecutorService)` - `Containerizer setCacheConfiguration(CacheConfiguration)` - `Containerizer setEventHandlers(EventHandlers)` - `Containerizer setAllowInsecureRegistries(boolean)` +- `Containerizer setToolName(String)` ## For Java containers From a0bf21078df9b1bf0a7b803b0fdd764743253763 Mon Sep 17 00:00:00 2001 From: Cyrille HEIT Date: Wed, 10 Oct 2018 17:43:02 +0200 Subject: [PATCH 0282/2020] Adds container.user configuration (#1109) --- .../jib/builder/steps/BuildImageStep.java | 1 + .../configuration/ContainerConfiguration.java | 31 +++++++- .../frontend/JavaDockerContextGenerator.java | 16 ++++ .../google/cloud/tools/jib/image/Image.java | 27 ++++++- .../json/ContainerConfigurationTemplate.java | 15 +++- .../jib/image/json/ImageToJsonTranslator.java | 3 + .../jib/image/json/JsonToImageTranslator.java | 1 + .../jib/builder/steps/BuildImageStepTest.java | 2 + .../ContainerConfigurationTest.java | 6 ++ .../cloud/tools/jib/image/ImageTest.java | 2 + .../ContainerConfigurationTemplateTest.java | 1 + .../image/json/ImageToJsonTranslatorTest.java | 1 + .../image/json/JsonToImageTranslatorTest.java | 1 + .../test/resources/json/containerconfig.json | 2 +- .../json/translated_ocimanifest.json | 2 +- .../json/translated_v22manifest.json | 2 +- .../tools/jib/gradle/ContainerParameters.java | 15 ++++ .../tools/jib/gradle/DockerContextTask.java | 1 + .../gradle/PluginConfigurationProcessor.java | 3 +- .../jib/gradle/DockerContextTaskTest.java | 27 +++++++ .../tools/jib/gradle/JibExtensionTest.java | 3 + .../PluginConfigurationProcessorTest.java | 22 ++++++ .../tools/jib/maven/DockerContextMojo.java | 1 + .../jib/maven/JibPluginConfiguration.java | 15 ++++ .../maven/PluginConfigurationProcessor.java | 3 +- .../jib/maven/DockerContextMojoTest.java | 74 +++++++++++++++++++ .../jib/maven/JibPluginConfigurationTest.java | 3 + .../PluginConfigurationProcessorTest.java | 22 ++++++ .../jib/plugins/common/PropertyNames.java | 1 + 29 files changed, 291 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 64e07f5f90..25c788541d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -175,6 +175,7 @@ private Image afterCacheEntrySteps() if (containerConfiguration != null) { imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); imageBuilder.setCreated(containerConfiguration.getCreationTime()); + imageBuilder.setUser(containerConfiguration.getUser()); imageBuilder.setEntrypoint(containerConfiguration.getEntrypoint()); imageBuilder.setProgramArguments(containerConfiguration.getProgramArguments()); imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 00cdf3926e..f6325bd687 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -42,6 +42,7 @@ public static class Builder { @Nullable private ImmutableMap environmentMap; @Nullable private ImmutableList exposedPorts; @Nullable private ImmutableMap labels; + @Nullable private String user; /** * Sets the image creation time. @@ -136,6 +137,19 @@ public Builder setEntrypoint(@Nullable List entrypoint) { return this; } + /** + * Sets the user and group to run the container as. {@code user} can be a username or UID along + * with an optional groupname or GID. The following are all valid: {@code user}, {@code uid}, + * {@code user:group}, {@code uid:gid}, {@code uid:group}, {@code user:gid}. + * + * @param user the username/UID and optionally the groupname/GID + * @return this + */ + public Builder setUser(@Nullable String user) { + this.user = user; + return this; + } + /** * Builds the {@link ContainerConfiguration}. * @@ -143,7 +157,7 @@ public Builder setEntrypoint(@Nullable List entrypoint) { */ public ContainerConfiguration build() { return new ContainerConfiguration( - creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels); + creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels, user); } private Builder() {} @@ -164,6 +178,7 @@ public static Builder builder() { @Nullable private final ImmutableMap environmentMap; @Nullable private final ImmutableList exposedPorts; @Nullable private final ImmutableMap labels; + @Nullable private final String user; private ContainerConfiguration( Instant creationTime, @@ -171,13 +186,15 @@ private ContainerConfiguration( @Nullable ImmutableList programArguments, @Nullable ImmutableMap environmentMap, @Nullable ImmutableList exposedPorts, - @Nullable ImmutableMap labels) { + @Nullable ImmutableMap labels, + @Nullable String user) { this.creationTime = creationTime; this.entrypoint = entrypoint; this.programArguments = programArguments; this.environmentMap = environmentMap; this.exposedPorts = exposedPorts; this.labels = labels; + this.user = user; } public Instant getCreationTime() { @@ -204,6 +221,11 @@ public ImmutableList getExposedPorts() { return exposedPorts; } + @Nullable + public String getUser() { + return user; + } + @Nullable public ImmutableMap getLabels() { return labels; @@ -224,13 +246,14 @@ public boolean equals(Object other) { && Objects.equals(programArguments, otherContainerConfiguration.programArguments) && Objects.equals(environmentMap, otherContainerConfiguration.environmentMap) && Objects.equals(exposedPorts, otherContainerConfiguration.exposedPorts) - && Objects.equals(labels, otherContainerConfiguration.labels); + && Objects.equals(labels, otherContainerConfiguration.labels) + && Objects.equals(user, otherContainerConfiguration.user); } @Override @VisibleForTesting public int hashCode() { return Objects.hash( - creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels); + creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels, user); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 10aaa89af9..162034ffcf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -133,6 +133,7 @@ private static String mapToDockerfileString(Map map, String comm @Nullable private String baseImage; private List entrypoint = Collections.emptyList(); + @Nullable private String user; private List programArguments = Collections.emptyList(); private Map environment = Collections.emptyMap(); private List exposedPorts = Collections.emptyList(); @@ -191,6 +192,17 @@ public JavaDockerContextGenerator setEntrypoint(List entrypoint) { return this; } + /** + * Sets the user for the {@code USER} directive. + * + * @param user the username/UID and optionally the groupname/GID + * @return this + */ + public JavaDockerContextGenerator setUser(@Nullable String user) { + this.user = user; + return this; + } + /** * Sets the arguments used in the {@code CMD}. * @@ -299,6 +311,7 @@ public void generate(Path targetDirectory) throws IOException { * LABEL [key1]="[value1]" \ * [key2]="[value2]" \ * [...] + * USER [user name (or UID) and optionally user group (or GID)] * ENTRYPOINT java [jvm flags] -cp [classpaths] [main class] * CMD [main class args] * } @@ -331,6 +344,9 @@ String makeDockerfile() throws JsonProcessingException { .append(objectMapper.writeValueAsString(entrypoint)) .append("\nCMD ") .append(objectMapper.writeValueAsString(programArguments)); + if (user != null) { + dockerfile.append("\nUSER ").append(user); + } return dockerfile.toString(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 0ce4220cb3..e36f962db0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -41,6 +41,7 @@ public static class Builder { @Nullable private ImmutableList programArguments; @Nullable private ImmutableList exposedPorts; @Nullable private String workingDirectory; + @Nullable private String user; /** * Sets the image creation time. @@ -89,6 +90,17 @@ public Builder setEntrypoint(@Nullable List entrypoint) { return this; } + /** + * Sets the user/group to run the container as. + * + * @param user the username/UID and optionally the groupname/GID + * @return this + */ + public Builder setUser(@Nullable String user) { + this.user = user; + return this; + } + /** * Sets the items in the "Cmd" field in the container configuration. * @@ -181,7 +193,8 @@ public Image build() { programArguments, exposedPorts, labelsBuilder.build(), - workingDirectory); + workingDirectory, + user); } } @@ -216,6 +229,9 @@ public static Builder builder() { /** Working directory on the container configuration */ @Nullable private final String workingDirectory; + /** User on the container configuration */ + @Nullable private final String user; + private Image( @Nullable Instant created, ImageLayers layers, @@ -225,7 +241,8 @@ private Image( @Nullable ImmutableList programArguments, @Nullable ImmutableList exposedPorts, @Nullable ImmutableMap labels, - @Nullable String workingDirectory) { + @Nullable String workingDirectory, + @Nullable String user) { this.created = created; this.layers = layers; this.history = history; @@ -235,6 +252,7 @@ private Image( this.exposedPorts = exposedPorts; this.labels = labels; this.workingDirectory = workingDirectory; + this.user = user; } @Nullable @@ -272,6 +290,11 @@ public String getWorkingDirectory() { return workingDirectory; } + @Nullable + public String getUser() { + return user; + } + public ImmutableList getLayers() { return layers.getLayers(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index e1115bbcc0..bc9cd0fd64 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -41,7 +41,8 @@ * "Cmd": ["arg1", "arg2"], * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} }, * "Labels": { "com.example.label": "value" }, - * "WorkingDir": "/home/user/workspace" + * "WorkingDir": "/home/user/workspace", + * "User": "me" * }, * "history": [ * { @@ -110,6 +111,9 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { /** Working directory. */ @Nullable private String WorkingDir; + + /** User. */ + @Nullable private String User; } /** @@ -156,6 +160,10 @@ public void setContainerWorkingDir(@Nullable String workingDirectory) { config.WorkingDir = workingDirectory; } + public void setContainerUser(@Nullable String user) { + config.User = user; + } + public void addLayerDiffId(DescriptorDigest diffId) { rootfs.diff_ids.add(diffId); } @@ -207,6 +215,11 @@ String getContainerWorkingDir() { return config.WorkingDir; } + @Nullable + String getContainerUser() { + return config.User; + } + @VisibleForTesting DescriptorDigest getLayerDiffId(int index) { return rootfs.diff_ids.get(index); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 5677fdd6f2..c90944f2c2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -141,6 +141,9 @@ public Blob getContainerConfigurationBlob() { // Sets the working directory. template.setContainerWorkingDir(image.getWorkingDirectory()); + // Sets the user. + template.setContainerUser(image.getUser()); + // Serializes into JSON. return JsonTemplateMapper.toBlob(template); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 6a6b41453d..034965211e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -162,6 +162,7 @@ public static Image toImage( } imageBuilder.setWorkingDirectory(containerConfigurationTemplate.getContainerWorkingDir()); + imageBuilder.setUser(containerConfigurationTemplate.getContainerUser()); return imageBuilder.build(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 53db24a2fc..a7b50a69d8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -95,6 +95,7 @@ public Blob getLayerBlob() { Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getUser()).thenReturn("root"); nonEmptyLayerHistory = HistoryEntry.builder() @@ -178,6 +179,7 @@ public void test_propagateBaseImageConfiguration() ImmutableMap.of("base.label", "base.label.value", "my.label", "my.label.value"), image.getLabels()); Assert.assertEquals("/base/working/directory", image.getWorkingDirectory()); + Assert.assertEquals("root", image.getUser()); Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index 46d5baee41..c02368430c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -98,4 +98,10 @@ public void testBuilder_environmentMapTypes() { ContainerConfiguration.builder().setEnvironment(new TreeMap<>()); ContainerConfiguration.builder().setEnvironment(new Hashtable<>()); } + + @Test + public void testBuilder_user() { + ContainerConfiguration configuration = ContainerConfiguration.builder().setUser("john").build(); + Assert.assertEquals("john", configuration.getUser()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 8a214d74df..5419be278d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -53,6 +53,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .setEntrypoint(Arrays.asList("some", "command")) .setProgramArguments(Arrays.asList("arg1", "arg2")) .setExposedPorts(ImmutableList.of(Port.tcp(1000), Port.tcp(2000))) + .setUser("john") .addLayer(mockLayer) .build(); @@ -64,5 +65,6 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getProgramArguments()); Assert.assertEquals(ImmutableList.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); + Assert.assertEquals("john", image.getUser()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 5029b13990..14dac4065f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -61,6 +61,7 @@ public void testToJson() throws IOException, URISyntaxException, DigestException ImmutableMap.of())); containerConfigJson.setContainerLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); containerConfigJson.setContainerWorkingDir("/some/workspace"); + containerConfigJson.setContainerUser("tomcat"); containerConfigJson.addLayerDiffId( DescriptorDigest.fromDigest( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 3fdf292710..ea60a9ab50 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -63,6 +63,7 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); testImageBuilder.setWorkingDirectory("/some/workspace"); + testImageBuilder.setUser("tomcat"); DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index ffa8e036ad..827b919a00 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -183,5 +183,6 @@ private void testToImage_buildable( Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); Assert.assertEquals( ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); + Assert.assertEquals("tomcat", image.getUser()); } } diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index 4cb079a8f3..d4fe8ced9e 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace"},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat"},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/json/translated_ocimanifest.json index 5403c875db..239aa3ab56 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:64b29673c04ae315eb40e66ac8b0898c62ae6e75a09a45c1b89b7ab6adfba8e1","size":610},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:f200c357a04f3deee6422664f36fa5e1f7ac1ab09b4e1f5f6ab1a2bff56bdab4","size":626},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index a817387134..d9b83ead76 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:64b29673c04ae315eb40e66ac8b0898c62ae6e75a09a45c1b89b7ab6adfba8e1","size":610},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:f200c357a04f3deee6422664f36fa5e1f7ac1ab09b4e1f5f6ab1a2bff56bdab4","size":626},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 7cc1a1a685..434d2da6a2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -44,6 +44,7 @@ public class ContainerParameters { private List ports = Collections.emptyList(); private Map labels = Collections.emptyMap(); private String appRoot = ""; + @Nullable private String user; @Input @Optional @@ -182,4 +183,18 @@ public String getAppRoot() { public void setAppRoot(String appRoot) { this.appRoot = appRoot; } + + @Input + @Nullable + @Optional + public String getUser() { + if (System.getProperty(PropertyNames.CONTAINER_USER) != null) { + return System.getProperty(PropertyNames.CONTAINER_USER); + } + return user; + } + + public void setUser(String user) { + this.user = user; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 9bf766cf37..7d15184e21 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -128,6 +128,7 @@ public void generateDockerContext() { .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(jibExtension.getContainer().getPorts()) .setLabels(jibExtension.getContainer().getLabels()) + .setUser(jibExtension.getContainer().getUser()) .generate(Paths.get(targetDir)); getLogger().lifecycle("Created Docker context at " + targetDir); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 4612c11655..35a7735df1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -133,7 +133,8 @@ static PluginConfigurationProcessor processCommonConfiguration( .setEnvironment(jibExtension.getContainer().getEnvironment()) .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) - .setLabels(jibExtension.getContainer().getLabels()); + .setLabels(jibExtension.getContainer().getLabels()) + .setUser(jibExtension.getContainer().getUser()); if (jibExtension.getContainer().getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index fb9fbc15b1..45f4ac9f67 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -20,6 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.NoSuchElementException; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; @@ -95,6 +96,26 @@ public void testEntrypoint_defaultWebAppRoot() throws IOException { Assert.assertEquals("ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]", getEntrypoint()); } + @Test + public void testUser() throws IOException { + Mockito.when(containerParameters.getUser()).thenReturn("tomcat"); + task.generateDockerContext(); + + Assert.assertEquals("USER tomcat", getUser()); + } + + @Test + public void testUser_null() throws IOException { + Mockito.when(containerParameters.getUser()).thenReturn(null); + task.generateDockerContext(); + try { + getUser(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } + } + @Test public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { Mockito.when(containerParameters.getAppRoot()).thenReturn("relative/path"); @@ -140,4 +161,10 @@ private String getEntrypoint() throws IOException { List lines = Files.readAllLines(dockerfile); return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); } + + private String getUser() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("USER")).findFirst().get(); + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 595ca8d214..18638208a1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -52,6 +52,7 @@ private static void clearProperties() { System.clearProperty("jib.container.mainClass"); System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); + System.clearProperty("jib.container.user"); } @Before @@ -187,6 +188,8 @@ public void testProperties() { ImmutableList.of("port1", "port2", "port3"), testJibExtension.getContainer().getPorts()); System.setProperty("jib.container.useCurrentTimestamp", "true"); Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); + System.setProperty("jib.container.user", "myUser"); + Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 911b1aa8c2..c608962a11 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -86,6 +86,28 @@ public void testEntrypoint() throws InvalidImageReferenceException { Mockito.verifyZeroInteractions(mockLogger); } + @Test + public void testUser() throws InvalidImageReferenceException { + Mockito.doReturn("customUser").when(mockContainerParameters).getUser(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals("customUser", configuration.getUser()); + } + + @Test + public void testUser_null() throws InvalidImageReferenceException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLogger, mockJibExtension, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertNull(configuration.getUser()); + } + @Test public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException { Mockito.doReturn(Arrays.asList("custom", "entrypoint")) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 9b0d5ffe89..252a416d0c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -89,6 +89,7 @@ public void execute() throws MojoExecutionException { .setProgramArguments(getArgs()) .setExposedPorts(getExposedPorts()) .setLabels(getLabels()) + .setUser(getUser()) .generate(Paths.get(targetDir)); getLog().info("Created Docker context at " + targetDir); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index f1e22b4cb3..65991bffd9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -139,6 +139,8 @@ public static class ContainerParameters { @Parameter private Map labels = Collections.emptyMap(); @Parameter private String appRoot = ""; + + @Nullable @Parameter private String user; } @Nullable @@ -338,6 +340,19 @@ String getMainClass() { return container.mainClass; } + /** + * Gets the username or UID which the process in the container should run as. + * + * @return the configured main class name + */ + @Nullable + String getUser() { + if (System.getProperty(PropertyNames.CONTAINER_USER) != null) { + return System.getProperty(PropertyNames.CONTAINER_USER); + } + return container.user; + } + /** * Gets the configured main arguments. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index fd0d864876..dbc93f267b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -174,7 +174,8 @@ static PluginConfigurationProcessor processCommonConfiguration( .setProgramArguments(jibPluginConfiguration.getArgs()) .setEnvironment(jibPluginConfiguration.getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())) - .setLabels(jibPluginConfiguration.getLabels()); + .setLabels(jibPluginConfiguration.getLabels()) + .setUser(jibPluginConfiguration.getUser()); if (jibPluginConfiguration.getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 783f408a29..5c323a72f1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -22,6 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.NoSuchElementException; import org.apache.maven.model.Build; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; @@ -248,6 +249,79 @@ String getAppRoot() { Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getEntrypoint()); } + @Test + public void testUser() throws IOException, MojoExecutionException { + mojo = + new DockerContextMojo() { + @Override + MavenProject getProject() { + return project; + } + + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } + + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + String getBaseImage() { + return "tomcat:8.5-jre8-alpine"; + } + + @Override + String getUser() { + return "tomcat"; + } + }; + mojo.targetDir = outputFolder.toString(); + mojo.execute(); + Assert.assertEquals("USER tomcat", getUser()); + } + + @Test + public void testUser_null() throws IOException, MojoExecutionException { + mojo = + new DockerContextMojo() { + @Override + MavenProject getProject() { + return project; + } + + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } + + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + String getBaseImage() { + return "tomcat:8.5-jre8-alpine"; + } + }; + mojo.targetDir = outputFolder.toString(); + mojo.execute(); + try { + getUser(); + Assert.fail(); + } catch (NoSuchElementException ex) { + } + } + + private String getUser() throws IOException { + Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); + List lines = Files.readAllLines(dockerfile); + return lines.stream().filter(line -> line.startsWith("USER")).findFirst().get(); + } + private String getEntrypoint() throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); List lines = Files.readAllLines(dockerfile); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 137cfde198..b63dd66371 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -46,6 +46,7 @@ private static void clearProperties() { System.clearProperty("jib.container.mainClass"); System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); + System.clearProperty("jib.container.user"); } @Before @@ -125,5 +126,7 @@ public void testSystemProperties() { ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); System.setProperty("jib.container.useCurrentTimestamp", "true"); Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); + System.setProperty("jib.container.user", "myUser"); + Assert.assertEquals("myUser", testPluginConfiguration.getUser()); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index 0f24393fd6..f91de61df2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -198,6 +198,28 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws MojoExecutionExce "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2)); } + @Test + public void testUser() throws MojoExecutionException { + Mockito.doReturn("customUser").when(mockJibPluginConfiguration).getUser(); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLog, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertEquals("customUser", configuration.getUser()); + } + + @Test + public void testUser_null() throws MojoExecutionException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + mockLog, mockJibPluginConfiguration, mockProjectProperties); + ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + + Assert.assertNull(configuration.getUser()); + } + @Test public void testGetAppRootChecked() throws MojoExecutionException { Mockito.doReturn("/some/root").when(mockJibPluginConfiguration).getAppRoot(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index baf10329b6..85bd87c3c1 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -36,6 +36,7 @@ public class PropertyNames { public static final String CONTAINER_JVM_FLAGS = "jib.container.jvmFlags"; public static final String CONTAINER_LABELS = "jib.container.labels"; public static final String CONTAINER_MAIN_CLASS = "jib.container.mainClass"; + public static final String CONTAINER_USER = "jib.container.user"; public static final String CONTAINER_PORTS = "jib.container.ports"; public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; From 0fedb9f0a05dc54d6bef2d4b066cf1e4b37e55b3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 10 Oct 2018 14:39:46 -0400 Subject: [PATCH 0283/2020] Adds JibContainerBuilder#setUser. (#1126) --- .../tools/jib/api/JibContainerBuilder.java | 25 +++++++++++++++++++ .../jib/api/JibContainerBuilderTest.java | 4 ++- proposals/jib_core_library.md | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index e7f9736ace..97bf75a29a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -68,6 +68,7 @@ public class JibContainerBuilder { @Nullable private ImmutableList programArguments; private ImageFormat imageFormat = ImageFormat.Docker; private Instant creationTime = Instant.EPOCH; + @Nullable private String user; /** Instantiate with {@link Jib#from}. */ JibContainerBuilder(SourceImage baseImage) { @@ -332,6 +333,29 @@ public JibContainerBuilder setCreationTime(Instant creationTime) { return this; } + /** + * Sets the user and group to run the container as. {@code user} can be a username or UID along + * with an optional groupname or GID. + * + *

The following are valid formats for {@code user} + * + *

    + *
  • {@code user} + *
  • {@code uid} + *
  • {@code user:group} + *
  • {@code uid:gid} + *
  • {@code uid:group} + *
  • {@code user:gid} + *
+ * + * @param user the user to run the container as + * @return this + */ + public JibContainerBuilder setUser(@Nullable String user) { + this.user = user; + return this; + } + /** * Builds the container(s). * @@ -400,6 +424,7 @@ ContainerConfiguration toContainerConfiguration() { .setExposedPorts(ports) .setLabels(labels) .setCreationTime(creationTime) + .setUser(user) .build(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 6a52caef0f..98fac4d559 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -66,7 +66,8 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep .setExposedPorts(Arrays.asList(Port.tcp(1234), Port.udp(5678))) .setLabels(ImmutableMap.of("key", "value")) .setProgramArguments(Arrays.asList("program", "arguments")) - .setCreationTime(Instant.ofEpochMilli(1000)); + .setCreationTime(Instant.ofEpochMilli(1000)) + .setUser("user"); ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); @@ -78,6 +79,7 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep Assert.assertEquals( Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); Assert.assertEquals(Instant.ofEpochMilli(1000), containerConfiguration.getCreationTime()); + Assert.assertEquals("user", containerConfiguration.getUser()); } @Test diff --git a/proposals/jib_core_library.md b/proposals/jib_core_library.md index 08bdab6dbc..091756c8a1 100644 --- a/proposals/jib_core_library.md +++ b/proposals/jib_core_library.md @@ -28,6 +28,7 @@ Design for Jib Core as a Java library for building container images. - `JibContainerBuilder addLabel(String key, String value)` - `JibContainerBuilder setFormat(ImageFormat)` - `JibContainerBuilder setCreationTime(Instant creationTime)` +- `JibContainerBuilder setUser(String user)` - `JibContainer containerize(Containerizer)` Three `TargetImage` types (`RegistryImage`, `DockerDaemonImage`, and `TarImage`) define the 3 different targets Jib can build to. From 4bd31f030f1aee4bb4b58caf19d1dd6f4083f427 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 10 Oct 2018 22:11:22 -0400 Subject: [PATCH 0284/2020] Remove unthrown exceptions (#1128) --- .../com/google/cloud/tools/jib/docker/DockerClientTest.java | 3 +-- .../google/cloud/tools/jib/json/JsonTemplateMapperTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 36c249c126..6e71e0d7b0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -182,8 +182,7 @@ public void testTag() throws InterruptedException, IOException, InvalidImageRefe } @Test - public void testTag_fail() - throws InterruptedException, IOException, InvalidImageReferenceException { + public void testTag_fail() throws InterruptedException, InvalidImageReferenceException { DockerClient testDockerClient = new DockerClient( subcommand -> { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index a123370729..307e4afe6e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -149,7 +149,7 @@ public void testReadListOfJson() throws IOException, URISyntaxException, DigestE } @Test - public void testToBlob_listOfJson() throws IOException, URISyntaxException, DigestException { + public void testToBlob_listOfJson() throws IOException, URISyntaxException { Path jsonFile = Paths.get(Resources.getResource("json/basic_list.json").toURI()); String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); From e942c1a2f8e302e915702091b01aef63365df1b2 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 11 Oct 2018 12:37:15 -0400 Subject: [PATCH 0285/2020] Updates jib-core release script to accept version suffixes. (#1129) --- jib-core/scripts/prepare_release.sh | 26 +++++++++---------- jib-maven-plugin/scripts/prepare_release.sh | 28 +++++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) mode change 100644 => 100755 jib-core/scripts/prepare_release.sh diff --git a/jib-core/scripts/prepare_release.sh b/jib-core/scripts/prepare_release.sh old mode 100644 new mode 100755 index 95cb0ec463..120cc81eb0 --- a/jib-core/scripts/prepare_release.sh +++ b/jib-core/scripts/prepare_release.sh @@ -3,40 +3,40 @@ set -e -Colorize() { - echo "$(tput setff $2)$1$(tput sgr0)" -} - EchoRed() { - echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" } EchoGreen() { - echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" } Die() { - EchoRed "$1" - exit 1 + EchoRed "$1" + exit 1 } DieUsage() { - Die "Usage: ./scripts/prepare_release.sh " + Die "Usage: ./prepare_release.sh []" } # Usage: CheckVersion CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." } -[ $# -ne 2 ] || DieUsage +[ $# -ne 1 ] && [ $# -ne 2 ] && DieUsage EchoGreen '===== RELEASE SETUP SCRIPT =====' VERSION=$1 CheckVersion ${VERSION} +if [ -n "$2" ]; then + POST_RELEASE_VERSION=$2 + CheckVersion ${POST_RELEASE_VERSION} +fi if [[ $(git status -uno --porcelain) ]]; then - Die 'There are uncommitted changes.' + Die 'There are uncommitted changes.' fi # Runs integration tests. @@ -47,7 +47,7 @@ BRANCH=core_release_v${VERSION} git checkout -b ${BRANCH} # Changes the version for release and creates the commits/tags. -echo | ./gradlew release -PreleaseVersion=${VERSION} +echo | ./gradlew release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} # Pushes the release branch and tag to Github. git push origin ${BRANCH} diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index a738fe8c54..64b78ac0ff 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -3,40 +3,36 @@ set -e -Colorize() { - echo "$(tput setff $2)$1$(tput sgr0)" -} - EchoRed() { - echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" } EchoGreen() { - echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" } Die() { - EchoRed "$1" - exit 1 + EchoRed "$1" + exit 1 } DieUsage() { - Die "Usage: ./scripts/prepare_release.sh " + Die "Usage: ./scripts/prepare_release.sh []" } # Usage: CheckVersion CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." } # Usage: IncrementVersion IncrementVersion() { - local version=$1 - local minorVersion=$(echo $version | sed 's/[0-9][0-9]*\.[0-9][0-9]*\.\([0-9][0-9]\)*/\1/') - local nextMinorVersion=$((minorVersion+1)) - echo $version | sed "s/\([0-9][0-9]*\.[0-9][0-9]*\)\.[0-9][0-9]*/\1.$nextMinorVersion/" + local version=$1 + local minorVersion=$(echo $version | sed 's/[0-9][0-9]*\.[0-9][0-9]*\.\([0-9][0-9]\)*/\1/') + local nextMinorVersion=$((minorVersion+1)) + echo $version | sed "s/\([0-9][0-9]*\.[0-9][0-9]*\)\.[0-9][0-9]*/\1.$nextMinorVersion/" } -[ $# -ne 2 ] || DieUsage +[ $# -ne 1 ] && [ $# -ne 2 ] && DieUsage EchoGreen '===== RELEASE SETUP SCRIPT =====' @@ -47,7 +43,7 @@ NEXT_VERSION=$(IncrementVersion $VERSION) CheckVersion ${NEXT_VERSION} if [[ $(git status -uno --porcelain) ]]; then - Die 'There are uncommitted changes.' + Die 'There are uncommitted changes.' fi # Runs integration tests. From 8a5d0cf4daa72f1897d21bcc77dd0555d3100521 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 11 Oct 2018 13:15:40 -0400 Subject: [PATCH 0286/2020] Jib Core release v0.1.0 (#1131) * [Gradle Release Plugin] - pre tag commit: 'v0.1.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.1.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index da3fb3f1a2..29ea34fd9d 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.0-SNAPSHOT +version = 0.1.1-SNAPSHOT From 4f752042fbc0a9fa890cd3481686143aa5f133af Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 11 Oct 2018 13:27:36 -0400 Subject: [PATCH 0287/2020] Changes jib-gradle-plugin to use new Jib Core API. (#1119) --- .../tools/jib/gradle/BuildDockerTask.java | 49 +-- .../tools/jib/gradle/BuildImageTask.java | 58 ++-- .../cloud/tools/jib/gradle/BuildTarTask.java | 51 ++-- .../tools/jib/gradle/ContainerParameters.java | 8 +- .../jib/gradle/GradleProjectProperties.java | 27 +- .../gradle/PluginConfigurationProcessor.java | 95 +++--- .../gradle/GradleProjectPropertiesTest.java | 5 +- .../tools/jib/gradle/JibExtensionTest.java | 9 +- .../PluginConfigurationProcessorTest.java | 161 ++++++---- .../tools/jib/maven/BuildDockerMojo.java | 3 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 9 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../jib/maven/MavenProjectProperties.java | 23 +- .../maven/PluginConfigurationProcessor.java | 10 +- .../jib/maven/MavenProjectPropertiesTest.java | 5 +- .../PluginConfigurationProcessorTest.java | 34 +-- .../jib/plugins/common/MainClassResolver.java | 15 +- .../jib/plugins/common/NBuildStepsRunner.java | 270 +++++++++++++++++ .../jib/plugins/common/ProjectProperties.java | 4 +- .../api/JibContainerBuilderTestHelper.java | 35 +++ .../plugins/common/MainClassResolverTest.java | 25 +- .../plugins/common/NBuildStepsRunnerTest.java | 283 ++++++++++++++++++ 22 files changed, 923 insertions(+), 259 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 1042708b67..984836e98c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,16 +16,20 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.base.Preconditions; import java.io.IOException; import javax.annotation.Nullable; @@ -68,7 +72,8 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildDocker() - throws InvalidImageReferenceException, IOException, BuildStepsExecutionException { + throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, + CacheDirectoryCreationException { if (!DOCKER_CLIENT.isDockerInstalled()) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); @@ -84,39 +89,43 @@ public void buildDocker() GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - ImageReference targetImage = + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); + ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTo().getImage(), - gradleProjectProperties.getEventDispatcher(), + eventDispatcher, getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); + DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); + PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( getLogger(), jibExtension, gradleProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setAdditionalTargetImageTags(jibExtension.getTo().getTags()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, jibExtension, gradleProjectProperties); HelpfulSuggestions helpfulSuggestions = gradleHelpfulSuggestionsBuilder - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageReference(targetImageReference) .build(); - BuildStepsRunner.forBuildToDockerDaemon(DOCKER_CLIENT, buildConfiguration) - .build(helpfulSuggestions); + NBuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index f3ad912051..11234485cf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,18 +16,22 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -71,7 +75,8 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildImage() - throws InvalidImageReferenceException, IOException, BuildStepsExecutionException { + throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, + CacheDirectoryCreationException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); @@ -88,15 +93,16 @@ public void buildImage() "gradle jib --image ")); } - ImageReference targetImage = ImageReference.parse(jibExtension.getTo().getImage()); + ImageReference targetImageReference = ImageReference.parse(jibExtension.getTo().getImage()); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage( - targetImage, gradleProjectProperties.getEventDispatcher())); + CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - gradleProjectProperties.getEventDispatcher(), + eventDispatcher, PropertyNames.TO_AUTH_USERNAME, PropertyNames.TO_AUTH_PASSWORD, jibExtension.getTo().getAuth()); @@ -105,37 +111,39 @@ public void buildImage() defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getTo().getCredHelper()); - ImageConfiguration targetImageConfiguration = - ImageConfiguration.builder(targetImage) - .setCredentialRetrievers(defaultCredentialRetrievers.asList()) - .build(); + RegistryImage targetImage = RegistryImage.named(targetImageReference); + defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( getLogger(), jibExtension, gradleProjectProperties); - BuildConfiguration buildConfiguration = + JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(targetImageConfiguration) - .setAdditionalTargetImageTags(jibExtension.getTo().getTags()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .setTargetFormat(jibExtension.getContainer().getFormat()) - .build(); + .getJibContainerBuilder() + // Only uses possibly non-Docker formats for build to registry. + .setFormat(jibExtension.getContainer().getFormat()); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, jibExtension, gradleProjectProperties); HelpfulSuggestions helpfulSuggestions = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageReference(targetImageReference) .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); - BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); + NBuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 0e65d37b4c..12a87fbac4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,17 +16,22 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.base.Preconditions; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -97,7 +102,8 @@ private String getTargetPath() { @TaskAction public void buildTar() - throws InvalidImageReferenceException, BuildStepsExecutionException, IOException { + throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, + CacheDirectoryCreationException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); @@ -107,38 +113,45 @@ public void buildTar() GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - ImageReference targetImage = + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); + ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( jibExtension.getTo().getImage(), - gradleProjectProperties.getEventDispatcher(), + eventDispatcher, getProject().getName(), getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); + Path tarOutputPath = Paths.get(getTargetPath()); + TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); + PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( getLogger(), jibExtension, gradleProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, jibExtension, gradleProjectProperties); HelpfulSuggestions helpfulSuggestions = gradleHelpfulSuggestionsBuilder - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageReference(targetImageReference) .build(); - BuildStepsRunner.forBuildTar(Paths.get(getTargetPath()), buildConfiguration) - .build(helpfulSuggestions); + NBuildStepsRunner.forBuildTar(tarOutputPath) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 434d2da6a2..ec8592d243 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; @@ -131,12 +130,11 @@ public void setArgs(List args) { @Input @Optional - public Class getFormat() { + public ImageFormat getFormat() { if (System.getProperty(PropertyNames.CONTAINER_FORMAT) != null) { - return ImageFormat.valueOf(System.getProperty(PropertyNames.CONTAINER_FORMAT)) - .getManifestTemplateClass(); + return ImageFormat.valueOf(System.getProperty(PropertyNames.CONTAINER_FORMAT)); } - return Preconditions.checkNotNull(format).getManifestTemplateClass(); + return Preconditions.checkNotNull(format); } public void setFormat(ImageFormat format) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 13c3d9e06b..fa022d22a7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -63,7 +61,7 @@ static GradleProjectProperties getForProject( try { return new GradleProjectProperties( project, - makeEventDispatcher(logger), + makeEventHandlers(logger), GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot)); } catch (IOException ex) { @@ -71,14 +69,13 @@ static GradleProjectProperties getForProject( } } - private static EventDispatcher makeEventDispatcher(Logger logger) { + private static EventHandlers makeEventHandlers(Logger logger) { LogEventHandler logEventHandler = new LogEventHandler(logger); - return new DefaultEventDispatcher( - new EventHandlers() - .add(JibEventType.LOGGING, logEventHandler) - .add( - JibEventType.TIMING, - new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))))); + return new EventHandlers() + .add(JibEventType.LOGGING, logEventHandler) + .add( + JibEventType.TIMING, + new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message)))); } @Nullable @@ -96,16 +93,16 @@ static Path getExplodedWarDirectory(Project project) { } private final Project project; - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( Project project, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -115,8 +112,8 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { } @Override - public EventDispatcher getEventDispatcher() { - return eventDispatcher; + public EventHandlers getEventHandlers() { + return eventHandlers; } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index 35a7735df1..c50ac59bea 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -18,10 +18,13 @@ import com.google.api.client.http.HttpTransport; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -31,6 +34,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.time.Instant; @@ -100,7 +104,7 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided disableHttpLogging(); - ImageReference baseImage = + ImageReference baseImageReference = ImageReference.parse(Preconditions.checkNotNull(jibExtension.getFrom().getImage())); if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { @@ -108,12 +112,14 @@ static PluginConfigurationProcessor processCommonConfiguration( "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " + "this on a public network!"); } + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventDispatcher())); + CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - projectProperties.getEventDispatcher(), + eventDispatcher, PropertyNames.FROM_AUTH_USERNAME, PropertyNames.FROM_AUTH_PASSWORD, jibExtension.getFrom().getAuth()); @@ -122,46 +128,44 @@ static PluginConfigurationProcessor processCommonConfiguration( defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth")); defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getFrom().getCredHelper()); - ImageConfiguration.Builder baseImageConfigurationBuilder = - ImageConfiguration.builder(baseImage) - .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - List entrypoint = computeEntrypoint(logger, jibExtension, projectProperties); - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() + + RegistryImage baseImage = RegistryImage.named(baseImageReference); + defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + + JibContainerBuilder jibContainerBuilder = + Jib.from(baseImage) + .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) .setEntrypoint(entrypoint) .setEnvironment(jibExtension.getContainer().getEnvironment()) - .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) + .setProgramArguments(jibExtension.getContainer().getArgs()) .setLabels(jibExtension.getContainer().getLabels()) .setUser(jibExtension.getContainer().getUser()); if (jibExtension.getContainer().getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); + jibContainerBuilder.setCreationTime(Instant.now()); } - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder() - .setToolName(GradleProjectProperties.TOOL_NAME) - .setEventDispatcher(projectProperties.getEventDispatcher()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setLayerConfigurations( - projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); - buildConfigurationBuilder.setApplicationLayersCacheDirectory( - projectProperties.getCacheDirectory()); - buildConfigurationBuilder.setBaseImageLayersCacheDirectory( - Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); + return new PluginConfigurationProcessor( + jibContainerBuilder, baseImageReference, optionalFromCredential.isPresent()); + } + + static void configureContainerizer( + Containerizer containerizer, JibExtension jibExtension, ProjectProperties projectProperties) { + containerizer + .setToolName(GradleProjectProperties.TOOL_NAME) + .setEventHandlers(projectProperties.getEventHandlers()) + .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) + .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) + .setApplicationLayersCache(projectProperties.getCacheDirectory()); + + jibExtension.getTo().getTags().forEach(containerizer::withAdditionalTag); + if (jibExtension.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheDirectory( - projectProperties.getCacheDirectory()); + containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); } - - return new PluginConfigurationProcessor( - buildConfigurationBuilder, - baseImageConfigurationBuilder, - containerConfigurationBuilder, - optionalFromCredential.isPresent()); } /** @@ -197,32 +201,25 @@ static List computeEntrypoint( AbsoluteUnixPath.get(parameters.getAppRoot()), parameters.getJvmFlags(), mainClass); } - private final BuildConfiguration.Builder buildConfigurationBuilder; - private final ImageConfiguration.Builder baseImageConfigurationBuilder; - private final ContainerConfiguration.Builder containerConfigurationBuilder; + private final JibContainerBuilder jibContainerBuilder; + private final ImageReference baseImageReference; private final boolean isBaseImageCredentialPresent; private PluginConfigurationProcessor( - BuildConfiguration.Builder buildConfigurationBuilder, - ImageConfiguration.Builder baseImageConfigurationBuilder, - ContainerConfiguration.Builder containerConfigurationBuilder, + JibContainerBuilder jibContainerBuilder, + ImageReference baseImageReference, boolean isBaseImageCredentialPresent) { - this.buildConfigurationBuilder = buildConfigurationBuilder; - this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; - this.containerConfigurationBuilder = containerConfigurationBuilder; + this.jibContainerBuilder = jibContainerBuilder; + this.baseImageReference = baseImageReference; this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; } - BuildConfiguration.Builder getBuildConfigurationBuilder() { - return buildConfigurationBuilder; - } - - ImageConfiguration.Builder getBaseImageConfigurationBuilder() { - return baseImageConfigurationBuilder; + JibContainerBuilder getJibContainerBuilder() { + return jibContainerBuilder; } - ContainerConfiguration.Builder getContainerConfigurationBuilder() { - return containerConfigurationBuilder; + ImageReference getBaseImageReference() { + return baseImageReference; } boolean isBaseImageCredentialPresent() { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 9f81d8df2a..f1c82dcaff 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -49,7 +49,6 @@ public class GradleProjectPropertiesTest { @Mock private Convention mockConvention; @Mock private WarPluginConvention mockWarPluginConvection; @Mock private TaskContainer mockTaskContainer; - @Mock private EventDispatcher mockEventDispatcher; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; private Manifest manifest; @@ -67,7 +66,7 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties(mockProject, mockEventDispatcher, mockJavaLayerConfigurations); + new GradleProjectProperties(mockProject, new EventHandlers(), mockJavaLayerConfigurations); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 18638208a1..9e7a0c75c1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -112,7 +110,7 @@ public void testContainer() { Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getEnvironment()); Assert.assertNull(testJibExtension.getContainer().getMainClass()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getArgs()); - Assert.assertEquals(V22ManifestTemplate.class, testJibExtension.getContainer().getFormat()); + Assert.assertSame(ImageFormat.Docker, testJibExtension.getContainer().getFormat()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); Assert.assertEquals("", testJibExtension.getContainer().getAppRoot()); @@ -139,7 +137,7 @@ public void testContainer() { Assert.assertEquals(Arrays.asList("1000", "2000-2010", "3000"), container.getPorts()); Assert.assertEquals( ImmutableMap.of("label1", "value1", "label2", "value2"), container.getLabels()); - Assert.assertEquals(OCIManifestTemplate.class, container.getFormat()); + Assert.assertSame(ImageFormat.OCI, container.getFormat()); Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); } @@ -172,8 +170,7 @@ public void testProperties() { ImmutableMap.of("env1", "val1", "env2", "val2"), testJibExtension.getContainer().getEnvironment()); System.setProperty("jib.container.format", "OCI"); - Assert.assertEquals( - ImageFormat.OCI.getManifestTemplateClass(), testJibExtension.getContainer().getFormat()); + Assert.assertSame(ImageFormat.OCI, testJibExtension.getContainer().getFormat()); System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); Assert.assertEquals( ImmutableList.of("flag1", "flag2", "flag3"), testJibExtension.getContainer().getJvmFlags()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index c608962a11..5c28ec8b1f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -16,11 +16,18 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import org.gradle.api.GradleException; @@ -37,6 +44,14 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { + private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + return JibContainerBuilderTestHelper.toBuildConfiguration( + jibContainerBuilder, + BuildConfiguration.builder(), + Containerizer.to(RegistryImage.named("ignored"))); + } + @Mock private Logger mockLogger; @Mock private JibExtension mockJibExtension; @Mock private BaseImageParameters mockBaseImageParameters; @@ -45,134 +60,168 @@ public class PluginConfigurationProcessorTest { @Before public void setUp() { - Mockito.doReturn("gcr.io/distroless/java").when(mockBaseImageParameters).getImage(); - Mockito.doReturn(mockBaseImageParameters).when(mockJibExtension).getFrom(); - Mockito.doReturn(new AuthParameters("mock")).when(mockBaseImageParameters).getAuth(); - Mockito.doReturn(mockContainerParameters).when(mockJibExtension).getContainer(); - Mockito.doReturn(Collections.emptyList()).when(mockContainerParameters).getEntrypoint(); - Mockito.doReturn("/app").when(mockContainerParameters).getAppRoot(); - - Mockito.doReturn(JavaLayerConfigurations.builder().build()) - .when(mockProjectProperties) - .getJavaLayerConfigurations(); - Mockito.doReturn("java.lang.Object").when(mockProjectProperties).getMainClass(mockJibExtension); + Mockito.when(mockBaseImageParameters.getImage()).thenReturn("gcr.io/distroless/java"); + Mockito.when(mockJibExtension.getFrom()).thenReturn(mockBaseImageParameters); + Mockito.when(mockBaseImageParameters.getAuth()).thenReturn(new AuthParameters("mock")); + Mockito.when(mockJibExtension.getContainer()).thenReturn(mockContainerParameters); + Mockito.when(mockContainerParameters.getEntrypoint()).thenReturn(Collections.emptyList()); + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/app"); + + Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) + .thenReturn(JavaLayerConfigurations.builder().build()); + Mockito.when(mockProjectProperties.getMainClass(mockJibExtension)) + .thenReturn("java.lang.Object"); + Mockito.when(mockProjectProperties.getEventHandlers()).thenReturn(new EventHandlers()); } /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ @Test - public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException { + public void testPluginConfigurationProcessor_defaults() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - configuration.getEntrypoint()); + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLogger); } @Test - public void testEntrypoint() throws InvalidImageReferenceException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockContainerParameters) - .getEntrypoint(); + public void testEntrypoint() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockContainerParameters.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLogger); } @Test - public void testUser() throws InvalidImageReferenceException { - Mockito.doReturn("customUser").when(mockContainerParameters).getUser(); + public void testUser() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockContainerParameters.getUser()).thenReturn("customUser"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals("customUser", configuration.getUser()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); } @Test - public void testUser_null() throws InvalidImageReferenceException { + public void testUser_null() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertNull(configuration.getUser()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); } @Test - public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockContainerParameters) - .getEntrypoint(); - Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockContainerParameters).getJvmFlags(); + public void testEntrypoint_warningOnJvmFlags() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockContainerParameters.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); + Mockito.when(mockContainerParameters.getJvmFlags()) + .thenReturn(Collections.singletonList("jvmFlag")); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(mockLogger) .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } @Test - public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockContainerParameters) - .getEntrypoint(); - Mockito.doReturn("java.util.Object").when(mockContainerParameters).getMainClass(); + public void testEntrypoint_warningOnMainclass() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockContainerParameters.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); + Mockito.when(mockContainerParameters.getMainClass()).thenReturn("java.util.Object"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(mockLogger) .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } @Test - public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException { - Mockito.doReturn("/my/app").when(mockContainerParameters).getAppRoot(); + public void testEntrypointClasspath_nonDefaultAppRoot() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals("java", configuration.getEntrypoint().get(0)); - Assert.assertEquals("-cp", configuration.getEntrypoint().get(1)); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals( + "java", buildConfiguration.getContainerConfiguration().getEntrypoint().get(0)); + Assert.assertEquals( + "-cp", buildConfiguration.getContainerConfiguration().getEntrypoint().get(1)); Assert.assertEquals( - "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2)); + "/my/app/resources:/my/app/classes:/my/app/libs/*", + buildConfiguration.getContainerConfiguration().getEntrypoint().get(2)); } @Test - public void testWebAppEntrypoint_default() throws InvalidImageReferenceException { - Mockito.doReturn(true).when(mockProjectProperties).isWarProject(); + public void testWebAppEntrypoint_default() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + Mockito.when(mockProjectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLogger, mockJibExtension, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( - ImmutableList.of("java", "-jar", "/jetty/start.jar"), configuration.getEntrypoint()); + ImmutableList.of("java", "-jar", "/jetty/start.jar"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); } @Test public void testGetAppRootChecked() { - Mockito.doReturn("/some/root").when(mockContainerParameters).getAppRoot(); + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/some/root"); Assert.assertEquals( AbsoluteUnixPath.get("/some/root"), @@ -181,7 +230,7 @@ public void testGetAppRootChecked() { @Test public void testGetAppRootChecked_errorOnNonAbsolutePath() { - Mockito.doReturn("relative/path").when(mockContainerParameters).getAppRoot(); + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("relative/path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); @@ -194,7 +243,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() { @Test public void testGetAppRootChecked_errorOnWindowsPath() { - Mockito.doReturn("\\windows\\path").when(mockContainerParameters).getAppRoot(); + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("\\windows\\path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); @@ -207,7 +256,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() { @Test public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { - Mockito.doReturn("C:\\windows\\path").when(mockContainerParameters).getAppRoot(); + Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 005f56d6bf..7524a2aecc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -71,7 +72,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenProjectProperties.getEventDispatcher(), + new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index cbdf0dac7f..cef166a4c7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageFormat; @@ -93,13 +95,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { ImageReference targetImage = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage( - targetImage, mavenProjectProperties.getEventDispatcher())); + CredentialRetrieverFactory.forImage(targetImage, eventDispatcher)); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - mavenProjectProperties.getEventDispatcher(), + eventDispatcher, PropertyNames.TO_AUTH_USERNAME, PropertyNames.TO_AUTH_PASSWORD, getTargetImageAuth()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 78070f4472..0648d85043 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -66,7 +67,7 @@ public void execute() throws MojoExecutionException { ImageReference targetImage = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - mavenProjectProperties.getEventDispatcher(), + new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()), getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 3541fcf3ca..eb21c35e44 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -66,7 +64,7 @@ static MavenProjectProperties getForProject( try { return new MavenProjectProperties( project, - makeEventDispatcher(log), + makeEventHandlers(log), MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); } catch (IOException ex) { @@ -78,24 +76,23 @@ static MavenProjectProperties getForProject( } } - private static EventDispatcher makeEventDispatcher(Log log) { - return new DefaultEventDispatcher( - new EventHandlers() - .add(JibEventType.LOGGING, new LogEventHandler(log)) - .add(JibEventType.TIMING, new TimerEventHandler(log::debug))); + private static EventHandlers makeEventHandlers(Log log) { + return new EventHandlers() + .add(JibEventType.LOGGING, new LogEventHandler(log)) + .add(JibEventType.TIMING, new TimerEventHandler(log::debug)); } private final MavenProject project; - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( MavenProject project, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.javaLayerConfigurations = javaLayerConfigurations; } @@ -105,8 +102,8 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { } @Override - public EventDispatcher getEventDispatcher() { - return eventDispatcher; + public EventHandlers getEventHandlers() { + return eventHandlers; } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index dbc93f267b..34af59e1a6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -141,12 +143,14 @@ static PluginConfigurationProcessor processCommonConfiguration( Preconditions.checkNotNull(jibPluginConfiguration.getSession()).getSettings(), jibPluginConfiguration.getSettingsDecrypter(), logger); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImage, projectProperties.getEventDispatcher())); + CredentialRetrieverFactory.forImage(baseImage, eventDispatcher)); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( - projectProperties.getEventDispatcher(), + eventDispatcher, PropertyNames.FROM_AUTH_USERNAME, PropertyNames.FROM_AUTH_PASSWORD, jibPluginConfiguration.getBaseImageAuth()); @@ -185,7 +189,7 @@ static PluginConfigurationProcessor processCommonConfiguration( BuildConfiguration.Builder buildConfigurationBuilder = BuildConfiguration.builder() .setToolName(MavenProjectProperties.TOOL_NAME) - .setEventDispatcher(projectProperties.getEventDispatcher()) + .setEventDispatcher(eventDispatcher) .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) .setLayerConfigurations( projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 5cfcb5f2a0..30559b5fc3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import org.apache.maven.model.Plugin; import org.apache.maven.project.MavenProject; @@ -34,7 +34,6 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; - @Mock private EventDispatcher mockEventDispatcher; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @@ -49,7 +48,7 @@ public class MavenProjectPropertiesTest { public void setup() { mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockEventDispatcher, mockJavaLayerConfigurations); + mockMavenProject, new EventHandlers(), mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index f91de61df2..c6e62820fa 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; @@ -51,23 +52,22 @@ public class PluginConfigurationProcessorTest { @Before public void setUp() throws Exception { - Mockito.doReturn(mockMavenSession).when(mockJibPluginConfiguration).getSession(); - Mockito.doReturn(mockMavenSettings).when(mockMavenSession).getSettings(); - - Mockito.doReturn(new AuthConfiguration()).when(mockJibPluginConfiguration).getBaseImageAuth(); - Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getEntrypoint(); - Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getJvmFlags(); - Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getArgs(); - Mockito.doReturn(Collections.emptyList()).when(mockJibPluginConfiguration).getExposedPorts(); - Mockito.doReturn("/app").when(mockJibPluginConfiguration).getAppRoot(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - - Mockito.doReturn(JavaLayerConfigurations.builder().build()) - .when(mockProjectProperties) - .getJavaLayerConfigurations(); - Mockito.doReturn("java.lang.Object") - .when(mockProjectProperties) - .getMainClass(mockJibPluginConfiguration); + Mockito.when(mockJibPluginConfiguration.getSession()).thenReturn(mockMavenSession); + Mockito.when(mockMavenSession.getSettings()).thenReturn(mockMavenSettings); + + Mockito.when(mockJibPluginConfiguration.getBaseImageAuth()).thenReturn(new AuthConfiguration()); + Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(Collections.emptyList()); + Mockito.when(mockJibPluginConfiguration.getJvmFlags()).thenReturn(Collections.emptyList()); + Mockito.when(mockJibPluginConfiguration.getArgs()).thenReturn(Collections.emptyList()); + Mockito.when(mockJibPluginConfiguration.getExposedPorts()).thenReturn(Collections.emptyList()); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/app"); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + + Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) + .thenReturn(JavaLayerConfigurations.builder().build()); + Mockito.when(mockProjectProperties.getMainClass(mockJibPluginConfiguration)) + .thenReturn("java.lang.Object"); + Mockito.when(mockProjectProperties.getEventHandlers()).thenReturn(new EventHandlers()); } /** Test with our default mocks, which try to mimic the bare Maven configuration. */ diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 08c29d35e3..6c8b224711 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.image.LayerEntry; @@ -72,8 +73,7 @@ public static String resolveMainClass( Preconditions.checkNotNull(mainClass); if (!isValidJavaClass(mainClass)) { - projectProperties - .getEventDispatcher() + new DefaultEventDispatcher(projectProperties.getEventHandlers()) .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); } @@ -96,8 +96,7 @@ static boolean isValidJavaClass(String className) { @Nullable private static String getMainClassFromJar(ProjectProperties projectProperties) { - projectProperties - .getEventDispatcher() + new DefaultEventDispatcher(projectProperties.getEventHandlers()) .dispatch( LogEvent.info( "Searching for main class... Add a 'mainClass' configuration to '" @@ -108,8 +107,7 @@ private static String getMainClassFromJar(ProjectProperties projectProperties) { private static String findMainClassInClassFiles(ProjectProperties projectProperties) throws MainClassInferenceException { - projectProperties - .getEventDispatcher() + new DefaultEventDispatcher(projectProperties.getEventHandlers()) .dispatch( LogEvent.debug( "Could not find a valid main class specified in " @@ -125,7 +123,10 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert .collect(ImmutableList.toImmutableList()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(classesSourceFiles, projectProperties.getEventDispatcher()).find(); + new MainClassFinder( + classesSourceFiles, + new DefaultEventDispatcher(projectProperties.getEventHandlers())) + .find(); switch (mainClassFinderResult.getType()) { case MAIN_CLASS_FOUND: diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java new file mode 100644 index 0000000000..2a96810cfd --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java @@ -0,0 +1,270 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +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.builder.BuildSteps; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.registry.InsecureRegistryException; +import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; +import com.google.cloud.tools.jib.registry.RegistryErrorException; +import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.ExecutionException; +import org.apache.http.conn.HttpHostConnectException; + +// TODO: This should replace BuildStepsRunner once jib-maven-plugin is changed to use this. +/** Runs a {@link BuildSteps} and builds helpful error messages. */ +public class NBuildStepsRunner { + + private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = + "Containerizing application to "; + private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = + "Built and pushed image as "; + + private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = + "Containerizing application to Docker daemon as "; + private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = + "Built image to Docker daemon as "; + + private static final String STARTUP_MESSAGE_FORMAT_FOR_TARBALL = + "Containerizing application to file at '%s'..."; + private static final String SUCCESS_MESSAGE_FORMAT_FOR_TARBALL = + "Built image tarball at \u001B[36m%s\u001B[0m"; + + private static CharSequence colorCyan(CharSequence innerText) { + return new StringBuilder().append("\u001B[36m").append(innerText).append("\u001B[0m"); + } + + private static String buildMessageWithTargetImageReferences( + ImageReference targetImageReference, + Set additionalTags, + String prefix, + String suffix) { + StringJoiner successMessageBuilder = new StringJoiner(", ", prefix, suffix); + successMessageBuilder.add(colorCyan(targetImageReference.toString())); + for (String tag : additionalTags) { + successMessageBuilder.add(colorCyan(targetImageReference.withTag(tag).toString())); + } + return successMessageBuilder.toString(); + } + + /** + * Creates a runner to build an image. Creates a directory for the cache, if needed. + * + * @param targetImageReference the target image reference + * @param additionalTags additional tags to push to + * @return a {@link NBuildStepsRunner} for building to a registry + */ + public static NBuildStepsRunner forBuildImage( + ImageReference targetImageReference, Set additionalTags) { + return new NBuildStepsRunner( + buildMessageWithTargetImageReferences( + targetImageReference, + additionalTags, + STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, + "..."), + buildMessageWithTargetImageReferences( + targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "")); + } + + /** + * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. + * + * @param targetImageReference the target image reference + * @param additionalTags additional tags to push to + * @return a {@link NBuildStepsRunner} for building to a Docker daemon + */ + public static NBuildStepsRunner forBuildToDockerDaemon( + ImageReference targetImageReference, Set additionalTags) { + return new NBuildStepsRunner( + buildMessageWithTargetImageReferences( + targetImageReference, additionalTags, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), + buildMessageWithTargetImageReferences( + targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "")); + } + + /** + * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. + * + * @param outputPath the path to output the tarball to + * @return a {@link NBuildStepsRunner} for building a tarball + */ + public static NBuildStepsRunner forBuildTar(Path outputPath) { + return new NBuildStepsRunner( + String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), + String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); + } + + private static void handleRegistryUnauthorizedException( + RegistryUnauthorizedException registryUnauthorizedException, + HelpfulSuggestions helpfulSuggestions) + throws BuildStepsExecutionException { + if (registryUnauthorizedException.getHttpResponseException().getStatusCode() + == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + // No permissions for registry/repository. + throw new BuildStepsExecutionException( + helpfulSuggestions.forHttpStatusCodeForbidden( + registryUnauthorizedException.getImageReference()), + registryUnauthorizedException); + + } else { + throw new BuildStepsExecutionException( + helpfulSuggestions.forNoCredentialsDefined( + registryUnauthorizedException.getRegistry(), + registryUnauthorizedException.getRepository()), + registryUnauthorizedException); + } + } + + private static String capitalizeFirstLetter(String string) { + if (string.length() == 0) { + return string; + } + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + + private final String startupMessage; + private final String successMessage; + + @VisibleForTesting + NBuildStepsRunner(String startupMessage, String successMessage) { + this.startupMessage = startupMessage; + this.successMessage = successMessage; + } + + /** + * Runs the {@link BuildSteps}. + * + * @param jibContainerBuilder the {@link JibContainerBuilder} + * @param containerizer the {@link Containerizer} + * @param eventDispatcher the {@link EventDispatcher} + * @param layerConfigurations the list of {@link LayerConfiguration}s + * @param helpfulSuggestions suggestions to use in help messages for exceptions + * @return the built {@link JibContainer} + * @throws BuildStepsExecutionException if another exception is thrown during the build + * @throws IOException if an I/O exception occurs + * @throws CacheDirectoryCreationException if the cache directory could not be created + */ + public JibContainer build( + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + EventDispatcher eventDispatcher, + List layerConfigurations, + HelpfulSuggestions helpfulSuggestions) + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { + try { + eventDispatcher.dispatch(LogEvent.lifecycle("")); + eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); + + // Logs the different source files used. + eventDispatcher.dispatch( + LogEvent.info("Containerizing application with the following files:")); + + for (LayerConfiguration layerConfiguration : layerConfigurations) { + if (layerConfiguration.getLayerEntries().isEmpty()) { + continue; + } + + eventDispatcher.dispatch( + LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); + + for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { + eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); + } + } + + JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); + + eventDispatcher.dispatch(LogEvent.lifecycle("")); + eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); + + return jibContainer; + + } catch (ExecutionException executionException) { + Throwable exceptionDuringBuildSteps = executionException.getCause(); + + if (exceptionDuringBuildSteps instanceof HttpHostConnectException) { + // Failed to connect to registry. + throw new BuildStepsExecutionException( + helpfulSuggestions.forHttpHostConnect(), exceptionDuringBuildSteps); + + } else if (exceptionDuringBuildSteps instanceof RegistryUnauthorizedException) { + handleRegistryUnauthorizedException( + (RegistryUnauthorizedException) exceptionDuringBuildSteps, helpfulSuggestions); + + } else if (exceptionDuringBuildSteps instanceof RegistryCredentialsNotSentException) { + throw new BuildStepsExecutionException( + helpfulSuggestions.forCredentialsNotSent(), exceptionDuringBuildSteps); + + } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException + && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { + RegistryAuthenticationFailedException failureException = + (RegistryAuthenticationFailedException) exceptionDuringBuildSteps; + handleRegistryUnauthorizedException( + new RegistryUnauthorizedException( + failureException.getServerUrl(), + failureException.getImageName(), + (HttpResponseException) exceptionDuringBuildSteps.getCause()), + helpfulSuggestions); + + } else if (exceptionDuringBuildSteps instanceof UnknownHostException) { + throw new BuildStepsExecutionException( + helpfulSuggestions.forUnknownHost(), exceptionDuringBuildSteps); + + } else if (exceptionDuringBuildSteps instanceof InsecureRegistryException) { + throw new BuildStepsExecutionException( + helpfulSuggestions.forInsecureRegistry(), exceptionDuringBuildSteps); + + } else if (exceptionDuringBuildSteps instanceof RegistryErrorException) { + // RegistryErrorExceptions have good messages + RegistryErrorException registryErrorException = + (RegistryErrorException) exceptionDuringBuildSteps; + String message = + Verify.verifyNotNull(registryErrorException.getMessage()); // keep null-away happy + throw new BuildStepsExecutionException(message, exceptionDuringBuildSteps); + + } else { + throw new BuildStepsExecutionException( + helpfulSuggestions.none(), executionException.getCause()); + } + + } catch (InterruptedException ex) { + // TODO: Add more suggestions for various build failures. + throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); + } + + throw new IllegalStateException("unreachable"); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 1e2f47173b..c2d26ba5d3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.nio.file.Path; import javax.annotation.Nullable; @@ -33,7 +33,7 @@ public interface ProjectProperties { */ String EXPLODED_WAR_DIRECTORY_NAME = "jib-exploded-war"; - EventDispatcher getEventDispatcher(); + EventHandlers getEventHandlers(); String getPluginName(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java new file mode 100644 index 0000000000..83c1afc13f --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import java.io.IOException; + +/** Test helper to expose package-private members of {@link JibContainerBuilder}. */ +public class JibContainerBuilderTestHelper { + + public static BuildConfiguration toBuildConfiguration( + JibContainerBuilder jibContainerBuilder, + BuildConfiguration.Builder buildConfigurationBuilder, + Containerizer containerizer) + throws IOException, CacheDirectoryCreationException { + return jibContainerBuilder.toBuildConfiguration(buildConfigurationBuilder, containerizer); + } + + private JibContainerBuilderTestHelper() {} +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 33d298cba5..f514802cb3 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; @@ -28,6 +29,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.function.Consumer; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -41,15 +43,16 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassResolverTest { - @Mock private EventDispatcher mockEventDispatcher; + private static final Path FAKE_CLASSES_PATH = Paths.get("a/b/c"); + + @Mock private Consumer mockJibEventConsumer; @Mock private ProjectProperties mockProjectProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; - private final Path FAKE_CLASSES_PATH = Paths.get("a/b/c"); - @Before public void setup() { - Mockito.when(mockProjectProperties.getEventDispatcher()).thenReturn(mockEventDispatcher); + Mockito.when(mockProjectProperties.getEventHandlers()) + .thenReturn(new EventHandlers().add(mockJibEventConsumer)); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) @@ -73,8 +76,8 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventDispatcher) - .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockJibEventConsumer) + .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -91,8 +94,8 @@ public void testResolveMainClass_multipleInferredWithBackup() .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventDispatcher) - .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockJibEventConsumer) + .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -128,8 +131,8 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockEventDispatcher) - .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockJibEventConsumer) + .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java new file mode 100644 index 0000000000..de3f9a0d44 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.registry.InsecureRegistryException; +import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; +import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import org.apache.http.conn.HttpHostConnectException; +import org.junit.Assert; +import org.junit.Before; +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link NBuildStepsRunner}. */ +@RunWith(MockitoJUnitRunner.class) +public class NBuildStepsRunnerTest { + + private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = + new HelpfulSuggestions( + "messagePrefix", + "clearCacheCommand", + ImageReference.of("someregistry", "somerepository", null), + false, + "baseImageCredHelperConfiguration", + registry -> "baseImageAuthConfiguration " + registry, + ImageReference.of("toRegistry", "torepository", null), + false, + "targetImageCredHelperConfiguration", + registry -> "targetImageAuthConfiguration " + registry, + "toConfig", + "toFlag", + "buildFile"); + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private JibContainerBuilder mockJibContainerBuilder; + @Mock private Containerizer mockContainerizer; + @Mock private EventDispatcher mockEventDispatcher; + @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; + @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; + @Mock private HttpResponseException mockHttpResponseException; + @Mock private ExecutionException mockExecutionException; + + private NBuildStepsRunner testBuildImageStepsRunner; + + @Before + public void setUpMocks() { + testBuildImageStepsRunner = new NBuildStepsRunner("ignored", "ignored"); + } + + @Test + public void testBuildImage_pass() + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + } + + @Test + public void testBuildImage_executionException_httpHostConnectException() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + HttpHostConnectException mockHttpHostConnectException = + Mockito.mock(HttpHostConnectException.class); + Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect(), ex.getMessage()); + Assert.assertEquals(mockHttpHostConnectException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_unknownHostException() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); + Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forUnknownHost(), ex.getMessage()); + Assert.assertEquals(mockUnknownHostException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_insecureRegistryException() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + InsecureRegistryException mockInsecureRegistryException = + Mockito.mock(InsecureRegistryException.class); + Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage()); + Assert.assertEquals(mockInsecureRegistryException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) + .thenReturn(mockHttpResponseException); + Mockito.when(mockRegistryUnauthorizedException.getImageReference()) + .thenReturn("someregistry/somerepository"); + Mockito.when(mockHttpResponseException.getStatusCode()) + .thenReturn(HttpStatusCodes.STATUS_CODE_FORBIDDEN); + + Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals( + TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"), + ex.getMessage()); + Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) + .thenReturn(mockHttpResponseException); + Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); + Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); + Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown + + Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals( + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry", "somerepository"), + ex.getMessage()); + Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_registryCredentialsNotSentException() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + Mockito.when(mockExecutionException.getCause()) + .thenReturn(mockRegistryCredentialsNotSentException); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage()); + Assert.assertEquals(mockRegistryCredentialsNotSentException, ex.getCause()); + } + } + + @Test + public void testBuildImage_executionException_other() + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + Throwable throwable = new Throwable(); + Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); + + try { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); + Assert.fail("buildImage should have thrown an exception"); + + } catch (BuildStepsExecutionException ex) { + Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); + Assert.assertEquals(throwable, ex.getCause()); + } + } +} From 5f118c00aa6bdc88d259eaaf0ac105d4984d7f55 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 11 Oct 2018 16:25:35 -0400 Subject: [PATCH 0288/2020] Change jib-maven-plugin to use new Jib Core API (#1123) --- .../tools/jib/maven/BuildDockerMojo.java | 46 ++-- .../cloud/tools/jib/maven/BuildImageMojo.java | 68 +++--- .../cloud/tools/jib/maven/BuildTarMojo.java | 50 +++-- .../jib/maven/JibPluginConfiguration.java | 1 - .../maven/PluginConfigurationProcessor.java | 88 +++++--- .../PluginConfigurationProcessorTest.java | 197 +++++++++++------- 6 files changed, 272 insertions(+), 178 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 7524a2aecc..4bde2d8415 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,17 +16,20 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; @@ -69,42 +72,45 @@ public void execute() throws MojoExecutionException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - ImageReference targetImage = + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); + ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()), + eventDispatcher, getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); + DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( getLog(), this, mavenProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor.getJibContainerBuilder(); + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, this, mavenProjectProperties); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageReference(targetImageReference) .build(); - BuildStepsRunner.forBuildToDockerDaemon(DOCKER_CLIENT, buildConfiguration) - .build(helpfulSuggestions); + NBuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); getLog().info(""); - } catch (InvalidImageReferenceException | IOException ex) { + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index cef166a4c7..c2442788cf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -26,10 +28,10 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; @@ -92,14 +94,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.processCommonConfiguration( getLog(), this, mavenProjectProperties); - ImageReference targetImage = + ImageReference targetImageReference = PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); EventDispatcher eventDispatcher = new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImage, eventDispatcher)); + CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( eventDispatcher, @@ -113,7 +115,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { optionalToCredential = pluginConfigurationProcessor .getMavenSettingsServerCredentials() - .retrieve(targetImage.getRegistry()); + .retrieve(targetImageReference.getRegistry()); optionalToCredential.ifPresent( toCredential -> defaultCredentialRetrievers.setInferredCredential( @@ -121,37 +123,39 @@ public void execute() throws MojoExecutionException, MojoFailureException { } defaultCredentialRetrievers.setCredentialHelperSuffix(getTargetImageCredentialHelperName()); - ImageConfiguration targetImageConfiguration = - ImageConfiguration.builder(targetImage) - .setCredentialRetrievers(defaultCredentialRetrievers.asList()) + RegistryImage targetImage = RegistryImage.named(targetImageReference); + defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); + + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor + .getJibContainerBuilder() + // Only uses possibly non-Docker formats for build to registry. + .setFormat(ImageFormat.valueOf(getFormat())); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, this, mavenProjectProperties); + + HelpfulSuggestions helpfulSuggestions = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(targetImageReference) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); try { - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(targetImageConfiguration) - .setAdditionalTargetImageTags(getTargetImageAdditionalTags()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .setTargetFormat(ImageFormat.valueOf(getFormat()).getManifestTemplateClass()) - .build(); - - HelpfulSuggestions helpfulSuggestions = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) - .build(); - - BuildStepsRunner.forBuildImage(buildConfiguration).build(helpfulSuggestions); + NBuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); getLog().info(""); - } catch (IOException ex) { + } catch (IOException | CacheDirectoryCreationException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 0648d85043..ffc213c98b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,18 +16,22 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -64,43 +68,47 @@ public void execute() throws MojoExecutionException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - ImageReference targetImage = + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); + ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), - new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()), + eventDispatcher, getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); + Path tarOutputPath = + Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"); + TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( getLog(), this, mavenProjectProperties); - BuildConfiguration buildConfiguration = - pluginConfigurationProcessor - .getBuildConfigurationBuilder() - .setBaseImageConfiguration( - pluginConfigurationProcessor.getBaseImageConfigurationBuilder().build()) - .setTargetImageConfiguration(ImageConfiguration.builder(targetImage).build()) - .setContainerConfiguration( - pluginConfigurationProcessor.getContainerConfigurationBuilder().build()) - .build(); + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor.getJibContainerBuilder(); + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, this, mavenProjectProperties); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder - .setBaseImageReference(buildConfiguration.getBaseImageConfiguration().getImage()) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(buildConfiguration.getTargetImageConfiguration().getImage()) + .setTargetImageReference(targetImageReference) .build(); - BuildStepsRunner.forBuildTar( - Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"), - buildConfiguration) - .build(helpfulSuggestions); + NBuildStepsRunner.forBuildTar(tarOutputPath) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); getLog().info(""); - } catch (InvalidImageReferenceException | IOException ex) { + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 65991bffd9..e33b693013 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -318,7 +318,6 @@ List getJvmFlags() { * * @return the configured environment variables */ - @Nullable Map getEnvironment() { if (System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT) != null) { return ConfigurationPropertyValidator.parseMapProperty( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 34af59e1a6..f457dea091 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -17,9 +17,10 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -33,6 +34,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import java.time.Instant; @@ -130,7 +132,8 @@ static PluginConfigurationProcessor processCommonConfiguration( // TODO: Instead of disabling logging, have authentication credentials be provided disableHttpLogging(); - ImageReference baseImage = parseImageReference(getBaseImage(jibPluginConfiguration), "from"); + ImageReference baseImageReference = + parseImageReference(getBaseImage(jibPluginConfiguration), "from"); // Checks Maven settings for registry credentials. if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { @@ -147,7 +150,7 @@ static PluginConfigurationProcessor processCommonConfiguration( new DefaultEventDispatcher(projectProperties.getEventHandlers()); DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImage, eventDispatcher)); + CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); Optional optionalFromCredential = ConfigurationPropertyValidator.getImageCredential( eventDispatcher, @@ -158,7 +161,8 @@ static PluginConfigurationProcessor processCommonConfiguration( defaultCredentialRetrievers.setKnownCredential( optionalFromCredential.get(), "jib-maven-plugin configuration"); } else { - optionalFromCredential = mavenSettingsServerCredentials.retrieve(baseImage.getRegistry()); + optionalFromCredential = + mavenSettingsServerCredentials.retrieve(baseImageReference.getRegistry()); optionalFromCredential.ifPresent( fromCredential -> defaultCredentialRetrievers.setInferredCredential( @@ -167,13 +171,14 @@ static PluginConfigurationProcessor processCommonConfiguration( defaultCredentialRetrievers.setCredentialHelperSuffix( jibPluginConfiguration.getBaseImageCredentialHelperName()); - ImageConfiguration.Builder baseImageConfiguration = - ImageConfiguration.builder(baseImage) - .setCredentialRetrievers(defaultCredentialRetrievers.asList()); - List entrypoint = computeEntrypoint(logger, jibPluginConfiguration, projectProperties); - ContainerConfiguration.Builder containerConfigurationBuilder = - ContainerConfiguration.builder() + + RegistryImage baseImage = RegistryImage.named(baseImageReference); + defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + + JibContainerBuilder jibContainerBuilder = + Jib.from(baseImage) + .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) .setEntrypoint(entrypoint) .setProgramArguments(jibPluginConfiguration.getArgs()) .setEnvironment(jibPluginConfiguration.getEnvironment()) @@ -183,7 +188,7 @@ static PluginConfigurationProcessor processCommonConfiguration( if (jibPluginConfiguration.getUseCurrentTimestamp()) { logger.warn( "Setting image creation time to current time; your image may not be reproducible."); - containerConfigurationBuilder.setCreationTime(Instant.now()); + jibContainerBuilder.setCreationTime(Instant.now()); } BuildConfiguration.Builder buildConfigurationBuilder = @@ -203,13 +208,38 @@ static PluginConfigurationProcessor processCommonConfiguration( } return new PluginConfigurationProcessor( - buildConfigurationBuilder, - baseImageConfiguration, - containerConfigurationBuilder, + jibContainerBuilder, + baseImageReference, mavenSettingsServerCredentials, optionalFromCredential.isPresent()); } + /** + * Configures a {@link Containerizer} with values pulled from project properties/build + * configuration. + * + * @param containerizer the {@link Containerizer} to configure + * @param jibPluginConfiguration the build configuration + * @param projectProperties the project properties + */ + static void configureContainerizer( + Containerizer containerizer, + JibPluginConfiguration jibPluginConfiguration, + ProjectProperties projectProperties) { + containerizer + .setToolName(MavenProjectProperties.TOOL_NAME) + .setEventHandlers(projectProperties.getEventHandlers()) + .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) + .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) + .setApplicationLayersCache(projectProperties.getCacheDirectory()); + + jibPluginConfiguration.getTargetImageAdditionalTags().forEach(containerizer::withAdditionalTag); + + if (jibPluginConfiguration.getUseOnlyProjectCache()) { + containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); + } + } + /** * @param image the image reference string to parse. * @param type name of the parameter being parsed (e.g. "to" or "from"). @@ -262,35 +292,29 @@ static List computeEntrypoint( getAppRootChecked(jibPluginConfiguration), jibPluginConfiguration.getJvmFlags(), mainClass); } - private final BuildConfiguration.Builder buildConfigurationBuilder; - private final ImageConfiguration.Builder baseImageConfigurationBuilder; - private final ContainerConfiguration.Builder containerConfigurationBuilder; + private final JibContainerBuilder jibContainerBuilder; + private final ImageReference baseImageReference; + private final MavenSettingsServerCredentials mavenSettingsServerCredentials; private final boolean isBaseImageCredentialPresent; private PluginConfigurationProcessor( - BuildConfiguration.Builder buildConfigurationBuilder, - ImageConfiguration.Builder baseImageConfigurationBuilder, - ContainerConfiguration.Builder containerConfigurationBuilder, + JibContainerBuilder jibContainerBuilder, + ImageReference baseImageReference, MavenSettingsServerCredentials mavenSettingsServerCredentials, boolean isBaseImageCredentialPresent) { - this.buildConfigurationBuilder = buildConfigurationBuilder; - this.baseImageConfigurationBuilder = baseImageConfigurationBuilder; - this.containerConfigurationBuilder = containerConfigurationBuilder; + this.jibContainerBuilder = jibContainerBuilder; + this.baseImageReference = baseImageReference; this.mavenSettingsServerCredentials = mavenSettingsServerCredentials; this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; } - BuildConfiguration.Builder getBuildConfigurationBuilder() { - return buildConfigurationBuilder; - } - - ImageConfiguration.Builder getBaseImageConfigurationBuilder() { - return baseImageConfigurationBuilder; + JibContainerBuilder getJibContainerBuilder() { + return jibContainerBuilder; } - ContainerConfiguration.Builder getContainerConfigurationBuilder() { - return containerConfigurationBuilder; + ImageReference getBaseImageReference() { + return baseImageReference; } MavenSettingsServerCredentials getMavenSettingsServerCredentials() { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index c6e62820fa..40396b74c1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -16,6 +16,12 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -24,6 +30,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import org.apache.maven.execution.MavenSession; @@ -43,6 +50,14 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { + private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + return JibContainerBuilderTestHelper.toBuildConfiguration( + jibContainerBuilder, + BuildConfiguration.builder(), + Containerizer.to(RegistryImage.named("ignored"))); + } + @Mock private Log mockLog; @Mock private JibPluginConfiguration mockJibPluginConfiguration; @Mock private MavenProjectProperties mockProjectProperties; @@ -73,25 +88,28 @@ public void setUp() throws Exception { /** Test with our default mocks, which try to mimic the bare Maven configuration. */ @Test public void testPluginConfigurationProcessor_defaults() - throws MojoExecutionException, InvalidImageReferenceException { + throws MojoExecutionException, InvalidImageReferenceException, IOException, + CacheDirectoryCreationException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - configuration.getEntrypoint()); + buildConfiguration.getContainerConfiguration().getEntrypoint()); Assert.assertEquals( ImageReference.parse("gcr.io/distroless/java").toString(), - processor.getBaseImageConfigurationBuilder().build().getImage().toString()); + processor.getBaseImageReference().toString()); Mockito.verifyZeroInteractions(mockLog); } @Test public void testPluginConfigurationProcessor_warPackaging() throws MojoExecutionException, InvalidImageReferenceException { - Mockito.doReturn("war").when(mavenProject).getPackaging(); + Mockito.when(mavenProject.getPackaging()).thenReturn("war"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( @@ -99,130 +117,165 @@ public void testPluginConfigurationProcessor_warPackaging() Assert.assertEquals( ImageReference.parse("gcr.io/distroless/java/jetty").toString(), - processor.getBaseImageConfigurationBuilder().build().getImage().toString()); + processor.getBaseImageReference().toString()); Mockito.verifyZeroInteractions(mockLog); } @Test - public void testEntrypoint() throws MojoExecutionException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockJibPluginConfiguration) - .getEntrypoint(); + public void testEntrypoint() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); - - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLog); } @Test - public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException { - Mockito.doReturn(ImmutableList.of()).when(mockJibPluginConfiguration).getEntrypoint(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn("war").when(mavenProject).getPackaging(); + public void testEntrypoint_defaultWarPackaging() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn("war"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); - + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( - Arrays.asList("java", "-jar", "/jetty/start.jar"), configuration.getEntrypoint()); + Arrays.asList("java", "-jar", "/jetty/start.jar"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLog); } @Test - public void testEntrypoint_defaulNonWarPackaging() throws MojoExecutionException { - Mockito.doReturn(ImmutableList.of()).when(mockJibPluginConfiguration).getEntrypoint(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn(null).when(mavenProject).getPackaging(); + public void testEntrypoint_defaulNonWarPackaging() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn(null); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); - + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - configuration.getEntrypoint()); + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLog); } @Test - public void testEntrypoint_warningOnJvmFlags() throws MojoExecutionException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockJibPluginConfiguration) - .getEntrypoint(); - Mockito.doReturn(Arrays.asList("jvmFlag")).when(mockJibPluginConfiguration).getJvmFlags(); + public void testEntrypoint_warningOnJvmFlags() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); + Mockito.when(mockJibPluginConfiguration.getJvmFlags()) + .thenReturn(Collections.singletonList("jvmFlag")); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); - - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(mockLog) .warn(" and are ignored when is specified"); } @Test - public void testEntrypoint_warningOnMainclass() throws MojoExecutionException { - Mockito.doReturn(Arrays.asList("custom", "entrypoint")) - .when(mockJibPluginConfiguration) - .getEntrypoint(); - Mockito.doReturn("java.util.Object").when(mockJibPluginConfiguration).getMainClass(); + public void testEntrypoint_warningOnMainclass() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getEntrypoint()) + .thenReturn(Arrays.asList("custom", "entrypoint")); + Mockito.when(mockJibPluginConfiguration.getMainClass()).thenReturn("java.util.Object"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); - - Assert.assertEquals(Arrays.asList("custom", "entrypoint"), configuration.getEntrypoint()); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(mockLog) .warn(" and are ignored when is specified"); } @Test - public void testEntrypointClasspath_nonDefaultAppRoot() throws MojoExecutionException { - Mockito.doReturn("/my/app").when(mockJibPluginConfiguration).getAppRoot(); + public void testEntrypointClasspath_nonDefaultAppRoot() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + ContainerConfiguration containerConfiguration = + getBuildConfiguration(jibContainerBuilder).getContainerConfiguration(); + Assert.assertNotNull(containerConfiguration); + Assert.assertNotNull(containerConfiguration.getEntrypoint()); Assert.assertEquals( - "/my/app/resources:/my/app/classes:/my/app/libs/*", configuration.getEntrypoint().get(2)); + "/my/app/resources:/my/app/classes:/my/app/libs/*", + containerConfiguration.getEntrypoint().get(2)); } @Test - public void testUser() throws MojoExecutionException { - Mockito.doReturn("customUser").when(mockJibPluginConfiguration).getUser(); + public void testUser() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(mockJibPluginConfiguration.getUser()).thenReturn("customUser"); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertEquals("customUser", configuration.getUser()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); } @Test - public void testUser_null() throws MojoExecutionException { + public void testUser_null() + throws MojoExecutionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); - ContainerConfiguration configuration = processor.getContainerConfigurationBuilder().build(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertNull(configuration.getUser()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); } @Test public void testGetAppRootChecked() throws MojoExecutionException { - Mockito.doReturn("/some/root").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/some/root"); Assert.assertEquals( AbsoluteUnixPath.get("/some/root"), @@ -231,7 +284,7 @@ public void testGetAppRootChecked() throws MojoExecutionException { @Test public void testGetAppRootChecked_errorOnNonAbsolutePath() { - Mockito.doReturn("relative/path").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("relative/path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); @@ -245,7 +298,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() { @Test public void testGetAppRootChecked_errorOnWindowsPath() { - Mockito.doReturn("\\windows\\path").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("\\windows\\path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); @@ -259,7 +312,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() { @Test public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { - Mockito.doReturn("C:\\windows\\path").when(mockJibPluginConfiguration).getAppRoot(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("C:\\windows\\path"); try { PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); @@ -273,9 +326,9 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { @Test public void testGetAppRootChecked_defaultNonWarPackaging() throws MojoExecutionException { - Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn(null).when(mavenProject).getPackaging(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn(null); Assert.assertEquals( AbsoluteUnixPath.get("/app"), @@ -284,9 +337,9 @@ public void testGetAppRootChecked_defaultNonWarPackaging() throws MojoExecutionE @Test public void testGetAppRootChecked_defaultJarPackaging() throws MojoExecutionException { - Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn("jar").when(mavenProject).getPackaging(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn("jar"); Assert.assertEquals( AbsoluteUnixPath.get("/app"), @@ -295,9 +348,9 @@ public void testGetAppRootChecked_defaultJarPackaging() throws MojoExecutionExce @Test public void testGetAppRootChecked_defaultWarPackaging() throws MojoExecutionException { - Mockito.doReturn("").when(mockJibPluginConfiguration).getAppRoot(); - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn("war").when(mavenProject).getPackaging(); + Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn("war"); Assert.assertEquals( AbsoluteUnixPath.get("/jetty/webapps/ROOT"), @@ -306,8 +359,8 @@ public void testGetAppRootChecked_defaultWarPackaging() throws MojoExecutionExce @Test public void testGetBaseImage_defaultWarPackaging() { - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn("war").when(mavenProject).getPackaging(); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn("war"); Assert.assertEquals( "gcr.io/distroless/java/jetty", @@ -316,8 +369,8 @@ public void testGetBaseImage_defaultWarPackaging() { @Test public void testGetBaseImage_defaultNonWarPackaging() { - Mockito.doReturn(mavenProject).when(mockJibPluginConfiguration).getProject(); - Mockito.doReturn(null).when(mavenProject).getPackaging(); + Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); + Mockito.when(mavenProject.getPackaging()).thenReturn(null); Assert.assertEquals( "gcr.io/distroless/java", @@ -326,7 +379,7 @@ public void testGetBaseImage_defaultNonWarPackaging() { @Test public void testGetBaseImage_nonDefault() { - Mockito.doReturn("tomcat").when(mockJibPluginConfiguration).getBaseImage(); + Mockito.when(mockJibPluginConfiguration.getBaseImage()).thenReturn("tomcat"); Assert.assertEquals( "tomcat", PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); From 6c9802ad36ed82518249e21afe60ea11e204d0a3 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 12 Oct 2018 11:21:35 -0400 Subject: [PATCH 0289/2020] Rename NBuildStepsRunner to BuildStepsRunner and remove original class (#1124) --- .../tools/jib/gradle/BuildDockerTask.java | 4 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 +- .../tools/jib/maven/BuildDockerMojo.java | 4 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 4 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 4 +- .../jib/plugins/common/BuildStepsRunner.java | 83 +++-- .../jib/plugins/common/NBuildStepsRunner.java | 270 ----------------- .../plugins/common/BuildStepsRunnerTest.java | 140 ++++++--- .../plugins/common/NBuildStepsRunnerTest.java | 283 ------------------ 10 files changed, 155 insertions(+), 645 deletions(-) delete mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java delete mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 984836e98c..afdf2b5a08 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -27,9 +27,9 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.base.Preconditions; import java.io.IOException; import javax.annotation.Nullable; @@ -119,7 +119,7 @@ public void buildDocker() .setTargetImageReference(targetImageReference) .build(); - NBuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) + BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) .build( jibContainerBuilder, containerizer, diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 11234485cf..e126d34eef 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -28,10 +28,10 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -137,7 +137,7 @@ public void buildImage() .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) .build(); - NBuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) .build( jibContainerBuilder, containerizer, diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 12a87fbac4..914c0c55fc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -26,9 +26,9 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -145,7 +145,7 @@ public void buildTar() .setTargetImageReference(targetImageReference) .build(); - NBuildStepsRunner.forBuildTar(tarOutputPath) + BuildStepsRunner.forBuildTar(tarOutputPath) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 4bde2d8415..0d3504d23e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -27,9 +27,9 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; @@ -101,7 +101,7 @@ public void execute() throws MojoExecutionException { .setTargetImageReference(targetImageReference) .build(); - NBuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) + BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c2442788cf..263e8066fb 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -28,10 +28,10 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; @@ -146,7 +146,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build(); try { - NBuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) + BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index ffc213c98b..2d4c36ae0f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -26,9 +26,9 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; +import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.NBuildStepsRunner; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -99,7 +99,7 @@ public void execute() throws MojoExecutionException { .setTargetImageReference(targetImageReference) .build(); - NBuildStepsRunner.forBuildTar(tarOutputPath) + BuildStepsRunner.forBuildTar(tarOutputPath) .build( jibContainerBuilder, containerizer, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 9cad3d09a0..8010b659fb 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -18,10 +18,12 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +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.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; @@ -33,8 +35,11 @@ import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; +import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.Path; +import java.util.List; +import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; @@ -62,14 +67,14 @@ private static CharSequence colorCyan(CharSequence innerText) { } private static String buildMessageWithTargetImageReferences( - BuildConfiguration buildConfiguration, String prefix, String suffix) { - String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); - String targetRepository = buildConfiguration.getTargetImageConfiguration().getImageRepository(); - + ImageReference targetImageReference, + Set additionalTags, + String prefix, + String suffix) { StringJoiner successMessageBuilder = new StringJoiner(", ", prefix, suffix); - for (String tag : buildConfiguration.getAllTargetImageTags()) { - successMessageBuilder.add( - colorCyan(ImageReference.of(targetRegistry, targetRepository, tag).toString())); + successMessageBuilder.add(colorCyan(targetImageReference.toString())); + for (String tag : additionalTags) { + successMessageBuilder.add(colorCyan(targetImageReference.withTag(tag).toString())); } return successMessageBuilder.toString(); } @@ -77,46 +82,46 @@ private static String buildMessageWithTargetImageReferences( /** * Creates a runner to build an image. Creates a directory for the cache, if needed. * - * @param buildConfiguration the configuration parameters for the build + * @param targetImageReference the target image reference + * @param additionalTags additional tags to push to * @return a {@link BuildStepsRunner} for building to a registry */ - public static BuildStepsRunner forBuildImage(BuildConfiguration buildConfiguration) { + public static BuildStepsRunner forBuildImage( + ImageReference targetImageReference, Set additionalTags) { return new BuildStepsRunner( - BuildSteps.forBuildToDockerRegistry(buildConfiguration), buildMessageWithTargetImageReferences( - buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "..."), + targetImageReference, + additionalTags, + STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, + "..."), buildMessageWithTargetImageReferences( - buildConfiguration, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "")); + targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "")); } /** * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. * - * @param dockerClient the {@link DockerClient} for running {@code docker} commands - * @param buildConfiguration the configuration parameters for the build + * @param targetImageReference the target image reference + * @param additionalTags additional tags to push to * @return a {@link BuildStepsRunner} for building to a Docker daemon */ public static BuildStepsRunner forBuildToDockerDaemon( - DockerClient dockerClient, BuildConfiguration buildConfiguration) { + ImageReference targetImageReference, Set additionalTags) { return new BuildStepsRunner( - BuildSteps.forBuildToDockerDaemon(dockerClient, buildConfiguration), buildMessageWithTargetImageReferences( - buildConfiguration, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), + targetImageReference, additionalTags, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), buildMessageWithTargetImageReferences( - buildConfiguration, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "")); + targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "")); } /** * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. * * @param outputPath the path to output the tarball to - * @param buildConfiguration the configuration parameters for the build * @return a {@link BuildStepsRunner} for building a tarball */ - public static BuildStepsRunner forBuildTar( - Path outputPath, BuildConfiguration buildConfiguration) { + public static BuildStepsRunner forBuildTar(Path outputPath) { return new BuildStepsRunner( - BuildSteps.forBuildToTar(outputPath, buildConfiguration), String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); } @@ -149,13 +154,11 @@ private static String capitalizeFirstLetter(String string) { return Character.toUpperCase(string.charAt(0)) + string.substring(1); } - private final BuildSteps buildSteps; private final String startupMessage; private final String successMessage; @VisibleForTesting - BuildStepsRunner(BuildSteps buildSteps, String startupMessage, String successMessage) { - this.buildSteps = buildSteps; + BuildStepsRunner(String startupMessage, String successMessage) { this.startupMessage = startupMessage; this.successMessage = successMessage; } @@ -163,13 +166,24 @@ private static String capitalizeFirstLetter(String string) { /** * Runs the {@link BuildSteps}. * + * @param jibContainerBuilder the {@link JibContainerBuilder} + * @param containerizer the {@link Containerizer} + * @param eventDispatcher the {@link EventDispatcher} + * @param layerConfigurations the list of {@link LayerConfiguration}s * @param helpfulSuggestions suggestions to use in help messages for exceptions + * @return the built {@link JibContainer} * @throws BuildStepsExecutionException if another exception is thrown during the build + * @throws IOException if an I/O exception occurs + * @throws CacheDirectoryCreationException if the cache directory could not be created */ - public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException { + public JibContainer build( + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + EventDispatcher eventDispatcher, + List layerConfigurations, + HelpfulSuggestions helpfulSuggestions) + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { try { - EventDispatcher eventDispatcher = buildSteps.getBuildConfiguration().getEventDispatcher(); - eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); @@ -177,8 +191,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut eventDispatcher.dispatch( LogEvent.info("Containerizing application with the following files:")); - for (LayerConfiguration layerConfiguration : - buildSteps.getBuildConfiguration().getLayerConfigurations()) { + for (LayerConfiguration layerConfiguration : layerConfigurations) { if (layerConfiguration.getLayerEntries().isEmpty()) { continue; } @@ -191,11 +204,13 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut } } - buildSteps.run(); + JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); + return jibContainer; + } catch (ExecutionException executionException) { Throwable exceptionDuringBuildSteps = executionException.getCause(); @@ -248,5 +263,7 @@ public void build(HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecut // TODO: Add more suggestions for various build failures. throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); } + + throw new IllegalStateException("unreachable"); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java deleted file mode 100644 index 2a96810cfd..0000000000 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunner.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpStatusCodes; -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.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; -import com.google.cloud.tools.jib.registry.RegistryErrorException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Verify; -import java.io.IOException; -import java.net.UnknownHostException; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; -import java.util.StringJoiner; -import java.util.concurrent.ExecutionException; -import org.apache.http.conn.HttpHostConnectException; - -// TODO: This should replace BuildStepsRunner once jib-maven-plugin is changed to use this. -/** Runs a {@link BuildSteps} and builds helpful error messages. */ -public class NBuildStepsRunner { - - private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = - "Containerizing application to "; - private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = - "Built and pushed image as "; - - private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = - "Containerizing application to Docker daemon as "; - private static final String SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON = - "Built image to Docker daemon as "; - - private static final String STARTUP_MESSAGE_FORMAT_FOR_TARBALL = - "Containerizing application to file at '%s'..."; - private static final String SUCCESS_MESSAGE_FORMAT_FOR_TARBALL = - "Built image tarball at \u001B[36m%s\u001B[0m"; - - private static CharSequence colorCyan(CharSequence innerText) { - return new StringBuilder().append("\u001B[36m").append(innerText).append("\u001B[0m"); - } - - private static String buildMessageWithTargetImageReferences( - ImageReference targetImageReference, - Set additionalTags, - String prefix, - String suffix) { - StringJoiner successMessageBuilder = new StringJoiner(", ", prefix, suffix); - successMessageBuilder.add(colorCyan(targetImageReference.toString())); - for (String tag : additionalTags) { - successMessageBuilder.add(colorCyan(targetImageReference.withTag(tag).toString())); - } - return successMessageBuilder.toString(); - } - - /** - * Creates a runner to build an image. Creates a directory for the cache, if needed. - * - * @param targetImageReference the target image reference - * @param additionalTags additional tags to push to - * @return a {@link NBuildStepsRunner} for building to a registry - */ - public static NBuildStepsRunner forBuildImage( - ImageReference targetImageReference, Set additionalTags) { - return new NBuildStepsRunner( - buildMessageWithTargetImageReferences( - targetImageReference, - additionalTags, - STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, - "..."), - buildMessageWithTargetImageReferences( - targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY, "")); - } - - /** - * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. - * - * @param targetImageReference the target image reference - * @param additionalTags additional tags to push to - * @return a {@link NBuildStepsRunner} for building to a Docker daemon - */ - public static NBuildStepsRunner forBuildToDockerDaemon( - ImageReference targetImageReference, Set additionalTags) { - return new NBuildStepsRunner( - buildMessageWithTargetImageReferences( - targetImageReference, additionalTags, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), - buildMessageWithTargetImageReferences( - targetImageReference, additionalTags, SUCCESS_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "")); - } - - /** - * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. - * - * @param outputPath the path to output the tarball to - * @return a {@link NBuildStepsRunner} for building a tarball - */ - public static NBuildStepsRunner forBuildTar(Path outputPath) { - return new NBuildStepsRunner( - String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), - String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); - } - - private static void handleRegistryUnauthorizedException( - RegistryUnauthorizedException registryUnauthorizedException, - HelpfulSuggestions helpfulSuggestions) - throws BuildStepsExecutionException { - if (registryUnauthorizedException.getHttpResponseException().getStatusCode() - == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { - // No permissions for registry/repository. - throw new BuildStepsExecutionException( - helpfulSuggestions.forHttpStatusCodeForbidden( - registryUnauthorizedException.getImageReference()), - registryUnauthorizedException); - - } else { - throw new BuildStepsExecutionException( - helpfulSuggestions.forNoCredentialsDefined( - registryUnauthorizedException.getRegistry(), - registryUnauthorizedException.getRepository()), - registryUnauthorizedException); - } - } - - private static String capitalizeFirstLetter(String string) { - if (string.length() == 0) { - return string; - } - return Character.toUpperCase(string.charAt(0)) + string.substring(1); - } - - private final String startupMessage; - private final String successMessage; - - @VisibleForTesting - NBuildStepsRunner(String startupMessage, String successMessage) { - this.startupMessage = startupMessage; - this.successMessage = successMessage; - } - - /** - * Runs the {@link BuildSteps}. - * - * @param jibContainerBuilder the {@link JibContainerBuilder} - * @param containerizer the {@link Containerizer} - * @param eventDispatcher the {@link EventDispatcher} - * @param layerConfigurations the list of {@link LayerConfiguration}s - * @param helpfulSuggestions suggestions to use in help messages for exceptions - * @return the built {@link JibContainer} - * @throws BuildStepsExecutionException if another exception is thrown during the build - * @throws IOException if an I/O exception occurs - * @throws CacheDirectoryCreationException if the cache directory could not be created - */ - public JibContainer build( - JibContainerBuilder jibContainerBuilder, - Containerizer containerizer, - EventDispatcher eventDispatcher, - List layerConfigurations, - HelpfulSuggestions helpfulSuggestions) - throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { - try { - eventDispatcher.dispatch(LogEvent.lifecycle("")); - eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); - - // Logs the different source files used. - eventDispatcher.dispatch( - LogEvent.info("Containerizing application with the following files:")); - - for (LayerConfiguration layerConfiguration : layerConfigurations) { - if (layerConfiguration.getLayerEntries().isEmpty()) { - continue; - } - - eventDispatcher.dispatch( - LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); - - for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); - } - } - - JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); - - eventDispatcher.dispatch(LogEvent.lifecycle("")); - eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); - - return jibContainer; - - } catch (ExecutionException executionException) { - Throwable exceptionDuringBuildSteps = executionException.getCause(); - - if (exceptionDuringBuildSteps instanceof HttpHostConnectException) { - // Failed to connect to registry. - throw new BuildStepsExecutionException( - helpfulSuggestions.forHttpHostConnect(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof RegistryUnauthorizedException) { - handleRegistryUnauthorizedException( - (RegistryUnauthorizedException) exceptionDuringBuildSteps, helpfulSuggestions); - - } else if (exceptionDuringBuildSteps instanceof RegistryCredentialsNotSentException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forCredentialsNotSent(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException - && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { - RegistryAuthenticationFailedException failureException = - (RegistryAuthenticationFailedException) exceptionDuringBuildSteps; - handleRegistryUnauthorizedException( - new RegistryUnauthorizedException( - failureException.getServerUrl(), - failureException.getImageName(), - (HttpResponseException) exceptionDuringBuildSteps.getCause()), - helpfulSuggestions); - - } else if (exceptionDuringBuildSteps instanceof UnknownHostException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forUnknownHost(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof InsecureRegistryException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forInsecureRegistry(), exceptionDuringBuildSteps); - - } else if (exceptionDuringBuildSteps instanceof RegistryErrorException) { - // RegistryErrorExceptions have good messages - RegistryErrorException registryErrorException = - (RegistryErrorException) exceptionDuringBuildSteps; - String message = - Verify.verifyNotNull(registryErrorException.getMessage()); // keep null-away happy - throw new BuildStepsExecutionException(message, exceptionDuringBuildSteps); - - } else { - throw new BuildStepsExecutionException( - helpfulSuggestions.none(), executionException.getCause()); - } - - } catch (InterruptedException ex) { - // TODO: Add more suggestions for various build failures. - throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); - } - - throw new IllegalStateException("unreachable"); - } -} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 5b1f82ed48..f39611ddb0 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -18,18 +18,17 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.net.UnknownHostException; -import java.nio.file.Paths; +import java.util.Collections; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; import org.junit.Assert; @@ -64,51 +63,50 @@ public class BuildStepsRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Mock private BuildSteps mockBuildSteps; + @Mock private JibContainerBuilder mockJibContainerBuilder; + @Mock private Containerizer mockContainerizer; @Mock private EventDispatcher mockEventDispatcher; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @Mock private ExecutionException mockExecutionException; - @Mock private BuildConfiguration mockBuildConfiguration; private BuildStepsRunner testBuildImageStepsRunner; @Before public void setUpMocks() { - testBuildImageStepsRunner = new BuildStepsRunner(mockBuildSteps, "ignored", "ignored"); - - Mockito.when(mockBuildSteps.getBuildConfiguration()).thenReturn(mockBuildConfiguration); - Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); - Mockito.when(mockBuildConfiguration.getLayerConfigurations()) - .thenReturn( - ImmutableList.of( - LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) - .build(), - LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) - .build(), - LayerConfiguration.builder() - .addEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")) - .build())); + testBuildImageStepsRunner = new BuildStepsRunner("ignored", "ignored"); } @Test - public void testBuildImage_pass() throws BuildStepsExecutionException { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + public void testBuildImage_pass() + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); } @Test public void testBuildImage_executionException_httpHostConnectException() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { HttpHostConnectException mockHttpHostConnectException = Mockito.mock(HttpHostConnectException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -119,13 +117,21 @@ public void testBuildImage_executionException_httpHostConnectException() @Test public void testBuildImage_executionException_unknownHostException() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -136,14 +142,22 @@ public void testBuildImage_executionException_unknownHostException() @Test public void testBuildImage_executionException_insecureRegistryException() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { InsecureRegistryException mockInsecureRegistryException = Mockito.mock(InsecureRegistryException.class); Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -154,7 +168,8 @@ public void testBuildImage_executionException_insecureRegistryException() @Test public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getImageReference()) @@ -163,10 +178,17 @@ public void testBuildImage_executionException_registryUnauthorizedException_stat .thenReturn(HttpStatusCodes.STATUS_CODE_FORBIDDEN); Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -179,7 +201,8 @@ public void testBuildImage_executionException_registryUnauthorizedException_stat @Test public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); @@ -187,10 +210,17 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -203,13 +233,21 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr @Test public void testBuildImage_executionException_registryCredentialsNotSentException() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { Mockito.when(mockExecutionException.getCause()) .thenReturn(mockRegistryCredentialsNotSentException); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -220,13 +258,21 @@ public void testBuildImage_executionException_registryCredentialsNotSentExceptio @Test public void testBuildImage_executionException_other() - throws InterruptedException, ExecutionException { + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { Throwable throwable = new Throwable(); Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); - Mockito.doThrow(mockExecutionException).when(mockBuildSteps).run(); + Mockito.doThrow(mockExecutionException) + .when(mockJibContainerBuilder) + .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build(TEST_HELPFUL_SUGGESTIONS); + testBuildImageStepsRunner.build( + mockJibContainerBuilder, + mockContainerizer, + mockEventDispatcher, + Collections.emptyList(), + TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java deleted file mode 100644 index de3f9a0d44..0000000000 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/NBuildStepsRunnerTest.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; -import java.io.IOException; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.concurrent.ExecutionException; -import org.apache.http.conn.HttpHostConnectException; -import org.junit.Assert; -import org.junit.Before; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link NBuildStepsRunner}. */ -@RunWith(MockitoJUnitRunner.class) -public class NBuildStepsRunnerTest { - - private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = - new HelpfulSuggestions( - "messagePrefix", - "clearCacheCommand", - ImageReference.of("someregistry", "somerepository", null), - false, - "baseImageCredHelperConfiguration", - registry -> "baseImageAuthConfiguration " + registry, - ImageReference.of("toRegistry", "torepository", null), - false, - "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry, - "toConfig", - "toFlag", - "buildFile"); - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private JibContainerBuilder mockJibContainerBuilder; - @Mock private Containerizer mockContainerizer; - @Mock private EventDispatcher mockEventDispatcher; - @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; - @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; - @Mock private HttpResponseException mockHttpResponseException; - @Mock private ExecutionException mockExecutionException; - - private NBuildStepsRunner testBuildImageStepsRunner; - - @Before - public void setUpMocks() { - testBuildImageStepsRunner = new NBuildStepsRunner("ignored", "ignored"); - } - - @Test - public void testBuildImage_pass() - throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - } - - @Test - public void testBuildImage_executionException_httpHostConnectException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - HttpHostConnectException mockHttpHostConnectException = - Mockito.mock(HttpHostConnectException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect(), ex.getMessage()); - Assert.assertEquals(mockHttpHostConnectException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_unknownHostException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forUnknownHost(), ex.getMessage()); - Assert.assertEquals(mockUnknownHostException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_insecureRegistryException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - InsecureRegistryException mockInsecureRegistryException = - Mockito.mock(InsecureRegistryException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage()); - Assert.assertEquals(mockInsecureRegistryException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getImageReference()) - .thenReturn("someregistry/somerepository"); - Mockito.when(mockHttpResponseException.getStatusCode()) - .thenReturn(HttpStatusCodes.STATUS_CODE_FORBIDDEN); - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"), - ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) - .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); - Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry", "somerepository"), - ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_registryCredentialsNotSentException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Mockito.when(mockExecutionException.getCause()) - .thenReturn(mockRegistryCredentialsNotSentException); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage()); - Assert.assertEquals(mockRegistryCredentialsNotSentException, ex.getCause()); - } - } - - @Test - public void testBuildImage_executionException_other() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Throwable throwable = new Throwable(); - Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); - Mockito.doThrow(mockExecutionException) - .when(mockJibContainerBuilder) - .containerize(mockContainerizer); - - try { - testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); - - } catch (BuildStepsExecutionException ex) { - Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); - Assert.assertEquals(throwable, ex.getCause()); - } - } -} From ed2c7c5ea266794d2d1a19344734112a8ccabade Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 12 Oct 2018 14:12:19 -0400 Subject: [PATCH 0290/2020] Set file timestamps in image to epoch + 1 second (#1133) * Set file timestamps in image to epoch + 1 sec * Reword CHANGELOG --- .../jib/image/ReproducibleLayerBuilder.java | 3 ++- .../image/ReproducibleLayerBuilderTest.java | 23 +++++++++++++++++++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index f30db1aaff..2b1aec456c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -114,7 +114,8 @@ public Blob build() { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); for (TarArchiveEntry entry : sortedFilesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. - entry.setModTime(0); + // 1 second since the epoch (https://github.com/GoogleContainerTools/jib/issues/1079) + entry.setModTime(1000); entry.setGroupId(0); entry.setUserId(0); entry.setUserName(""); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 829739ccaa..f3a8a8e69f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -34,6 +34,8 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Date; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.hamcrest.CoreMatchers; @@ -180,6 +182,27 @@ public void testToBlob_reproducibility() throws IOException { Assert.assertThat(layerContent, CoreMatchers.is(reproducedLayerContent)); } + @Test + public void testBuild_timestamp() throws IOException { + Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA")))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + // Reads the file back. + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + Assert.assertEquals( + Date.from(Instant.EPOCH.plusSeconds(1)), in.getNextEntry().getLastModifiedDate()); + } + } + private Path createFile(Path root, String filename, String content, long lastModifiedTime) throws IOException { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 45fd38e5cd..cfb8957211 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jibExportDockerContext` generates different directory layout and `Dockerfile`. +- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 73f4d5cdb9..9736e65ea3 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jib:exportDockerContext` generates different directory layout and `Dockerfile`. +- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) ### Fixed From 6977036d2f0d667a5698b204b80b8e4e531d3b64 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 12 Oct 2018 16:38:05 -0400 Subject: [PATCH 0291/2020] Add integration tests for container.user parameter / update CHANGELOG (#1136) --- jib-gradle-plugin/CHANGELOG.md | 3 +- .../gradle/EmptyProjectIntegrationTest.java | 18 ++++++++ .../cloud/tools/jib/gradle/JibRunHelper.java | 14 +++++- .../projects/empty/build-broken-user.gradle | 25 +++++++++++ .../resources/projects/empty/build.gradle | 1 + jib-maven-plugin/CHANGELOG.md | 3 +- .../maven/BuildDockerMojoIntegrationTest.java | 43 ++++++++++++++----- .../projects/empty/pom-broken-user.xml | 43 +++++++++++++++++++ .../src/test/resources/projects/empty/pom.xml | 1 + 9 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle create mode 100644 jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index cfb8957211..56fc3094a7 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,8 +10,9 @@ All notable changes to this project will be documented in this file. ### Changed - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jibExportDockerContext` generates different directory layout and `Dockerfile`. +- `jibExportDockerContext` generates different directory layout and `Dockerfile` - File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index 189b866696..6cb9755173 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -92,4 +92,22 @@ public void testDockerDaemon_empty() throws IOException, InterruptedException { new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); assertDockerInspect(targetImage); } + + @Test + public void testDockerDaemon_userNumeric() throws IOException, InterruptedException { + String targetImage = "emptyimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build.gradle"); + Assert.assertEquals( + "12345:54321", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } + + @Test + public void testDockerDaemon_userNames() throws IOException, InterruptedException { + String targetImage = "brokenuserimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build-broken-user.gradle"); + Assert.assertEquals( + "myuser:mygroup", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 4460cea302..cca59df9e7 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -75,15 +75,25 @@ static void buildAndRunAdditionalTag( assertCreationTimeEpoch(additionalImageReference); } - static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) + static void buildToDockerDaemon( + TestProject testProject, String imageReference, String gradleBuildFile) throws IOException, InterruptedException { BuildResult buildResult = - testProject.build("clean", "jibDockerBuild", "-D_TARGET_IMAGE=" + imageReference); + testProject.build( + "clean", + "jibDockerBuild", + "-D_TARGET_IMAGE=" + imageReference, + "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + } + + static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) + throws IOException, InterruptedException { + buildToDockerDaemon(testProject, imageReference, "build.gradle"); return new Command("docker", "run", "--rm", imageReference).run(); } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle new file mode 100644 index 0000000000..054212b5fb --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle @@ -0,0 +1,25 @@ +plugins { + id 'com.google.cloud.tools.jib' +} + +// Applies the java plugin after Jib to make sure it works in this order. +apply plugin: 'java' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + user = 'myuser:mygroup' + } + // Does not have tests use user-level cache for base image layers. + useOnlyProjectCache = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index 7226c1588f..f7b6cec689 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -18,6 +18,7 @@ jib { credHelper = 'gcr' } container { + user = '12345:54321' ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9736e65ea3..a5a2258d34 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,8 +10,9 @@ All notable changes to this project will be documented in this file. ### Changed - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jib:exportDockerContext` generates different directory layout and `Dockerfile`. +- `jib:exportDockerContext` generates different directory layout and `Dockerfile` - File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 69cd1ee18f..a40a55bc5e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -42,8 +42,17 @@ public class BuildDockerMojoIntegrationTest { public static final TestProject defaultTargetTestProject = new TestProject(testPlugin, "default-target"); - @ClassRule - public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + private static void buildToDockerDaemon(Path projectRoot, String imageReference, String pomXml) + throws VerificationException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + verifier.setAutoclean(false); + verifier.addCliOption("--file=" + pomXml); + verifier.executeGoal("package"); + + verifier.executeGoal("jib:dockerBuild"); + verifier.verifyErrorFreeLog(); + } /** * Builds and runs jib:buildDocker on a project at {@code projectRoot} pushing to {@code @@ -51,13 +60,7 @@ public class BuildDockerMojoIntegrationTest { */ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageReference) throws VerificationException, IOException, InterruptedException { - Verifier verifier = new Verifier(projectRoot.toString()); - verifier.setSystemProperty("_TARGET_IMAGE", imageReference); - verifier.setAutoclean(false); - verifier.executeGoal("package"); - - verifier.executeGoal("jib:" + BuildDockerMojo.GOAL_NAME); - verifier.verifyErrorFreeLog(); + buildToDockerDaemon(projectRoot, imageReference, "pom.xml"); String dockerInspect = new Command("docker", "inspect", imageReference).run(); Assert.assertThat( @@ -116,6 +119,26 @@ public void testExecute_defaultTarget() @Test public void testExecute_skipJibGoal() throws VerificationException, IOException { - SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildDockerMojo.GOAL_NAME); + SkippedGoalVerifier.verifyGoalIsSkipped(emptyTestProject, BuildDockerMojo.GOAL_NAME); + } + + @Test + public void testExecute_userNumeric() + throws VerificationException, IOException, InterruptedException { + String targetImage = "emptyimage:maven" + System.nanoTime(); + buildToDockerDaemon(emptyTestProject.getProjectRoot(), targetImage, "pom.xml"); + Assert.assertEquals( + "12345:54321", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); + } + + @Test + public void testExecute_userNames() + throws VerificationException, IOException, InterruptedException { + String targetImage = "brokenuserimage:maven" + System.nanoTime(); + buildToDockerDaemon(emptyTestProject.getProjectRoot(), targetImage, "pom-broken-user.xml"); + Assert.assertEquals( + "myuser:mygroup", + new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); } } diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml new file mode 100644 index 0000000000..5ae90666e2 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + com.test + empty + 1 + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + myuser:mygroup + + + true + + + + + diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 37b971bb13..5f73a8ebe5 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -33,6 +33,7 @@ ${_ADDITIONAL_TAG}

This is similar to {@code + * ENTRYPOINT} in Dockerfiles or {@code command} in the Kubernetes + * Container spec. + * + * @param entrypoint a list of the entrypoint command + * @return this + */ + public JibContainerBuilder setEntrypoint(List entrypoint) { + this.entrypoint = ImmutableList.copyOf(entrypoint); + return this; + } + + /** + * Sets the container entrypoint. + * + * @param entrypoint the entrypoint command + * @return this + * @see #setEntrypoint(List) for more details + */ + public JibContainerBuilder setEntrypoint(String... entrypoint) { + return setEntrypoint(Arrays.asList(entrypoint)); + } + + /** + * Sets the container entrypoint program arguments. These are additional tokens added to the end + * of the entrypoint command. + * + *

b60>uA3w?Llhxx!a=-x=Dfr-^ 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%0D+PR6Co1>#F)=TO`C3w}q5v$fEk0VG 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 P{?~(#I^&Rm0`C0}ufuT; literal 0 HcmV?d00001 diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.jar b/examples/micronaut/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

+ 12345:54321 1000/tcp 2000-2003/udp From 7d11c8be10ac37765c7fc7305fa2f9bf5707e9e9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 12 Oct 2018 18:11:19 -0400 Subject: [PATCH 0292/2020] Update CHANGELOG (#1137) --- jib-gradle-plugin/CHANGELOG.md | 4 ++-- jib-maven-plugin/CHANGELOG.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 56fc3094a7..a586cb9281 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,8 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jibExportDockerContext` generates different directory layout and `Dockerfile` -- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a5a2258d34..0dc630c398 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,8 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jib:exportDockerContext` generates different directory layout and `Dockerfile` -- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed From 9f091d538f498cf685a101fc29345fc4a1a0f248 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Sat, 13 Oct 2018 00:02:02 -0400 Subject: [PATCH 0293/2020] Add example of downloading and configuring the Stackdriver Debugger Java Agent (#1127) Uses SparkJava as a base, which listens on port 4567. Stackdriver Debugger agent is configured to log to stderr. Generates git.properties to enable locating the source. --- examples/README.md | 4 + examples/java-agent/README.md | 12 ++ examples/java-agent/build.gradle | 58 ++++++ examples/java-agent/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/java-agent/gradlew | 172 ++++++++++++++++++ examples/java-agent/gradlew.bat | 84 +++++++++ examples/java-agent/pom.xml | 119 ++++++++++++ .../src/main/java/example/HelloWorld.java | 25 +++ examples/java-agent/src/main/resources/world | 1 + 11 files changed, 481 insertions(+) create mode 100644 examples/java-agent/README.md create mode 100644 examples/java-agent/build.gradle create mode 100644 examples/java-agent/gradle.properties create mode 100644 examples/java-agent/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/java-agent/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/java-agent/gradlew create mode 100644 examples/java-agent/gradlew.bat create mode 100644 examples/java-agent/pom.xml create mode 100644 examples/java-agent/src/main/java/example/HelloWorld.java create mode 100644 examples/java-agent/src/main/resources/world diff --git a/examples/README.md b/examples/README.md index 9c6c506234..ef0c3116b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,3 +23,7 @@ See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Sprin ### Micronaut example See [micronaut](micronaut) for containerizing a [Micronaut framework](https://micronaut.io/) Groovy/Java application. + +### Java agents example + +See [java-agent](java-agent) for launching with the Stackdriver Debugger Java agent. diff --git a/examples/java-agent/README.md b/examples/java-agent/README.md new file mode 100644 index 0000000000..737773ba9b --- /dev/null +++ b/examples/java-agent/README.md @@ -0,0 +1,12 @@ +This SparkJava-based example builds a container image that includes the [Stackdriver Debugger Java Agent](https://cloud.google.com/debugger/docs/). +This project assumes the resulting image will be run inside Google Cloud Platform. + +To build the image: + +1. Replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project in `pom.xml` or `build.gradle`. + +1. Run `mvn package` or `./gradlew` to build the image. + +SparkJava listens on port 4567 by default. + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/java-agent)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle new file mode 100644 index 0000000000..3362361a2c --- /dev/null +++ b/examples/java-agent/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' version '0.9.11' + id 'de.undercouch.download' version '3.4.0' + id "com.gorylenko.gradle-git-properties" version "1.5.2" +} + +ext { + // where to download the Stackdriver Debugger agent https://cloud.google.com/debugger/docs/setup/java + stackdriverDebuggerAgentUrl = 'https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz' + + // where to place the Cloud Debugger agent in the container + stackdriverDebuggerLocation = '/opt/cdbg' + + // location for jib extras, including the Java agent + jibExtraDirectory = "${buildDir}/jib-agents" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile "com.sparkjava:spark-core:2.7.2" + compile "org.slf4j:slf4j-simple:1.7.21" +} + +// Download and extract the Cloud Debugger Java Agent +task downloadAgent(type: Download) { + src stackdriverDebuggerAgentUrl + dest "${buildDir}/cdbg_java_agent_gce.tar.gz" +} +task extractAgent(dependsOn: downloadAgent, type: Copy) { + from tarTree(downloadAgent.dest) + into "${jibExtraDirectory}/${stackdriverDebuggerLocation}" +} + +jib { + to { + image = 'gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib' + } + extraDirectory = file(jibExtraDirectory) + container { + ports = ['4567'] + jvmFlags = [ + '-agentpath:' + stackdriverDebuggerLocation + '/cdbg_java_agent.so=--logtostderr=1', + '-Dcom.google.cdbg.module=' + project.name, + '-Dcom.google.cdbg.version=' + version] + } +} + +tasks.jib.dependsOn extractAgent +tasks.jibDockerBuild.dependsOn extractAgent +tasks.jibBuildTar.dependsOn extractAgent +defaultTasks 'jib' diff --git a/examples/java-agent/gradle.properties b/examples/java-agent/gradle.properties new file mode 100644 index 0000000000..ac2e54ee6d --- /dev/null +++ b/examples/java-agent/gradle.properties @@ -0,0 +1 @@ +version = 0.0.1-SNAPSHOT diff --git a/examples/java-agent/gradle/wrapper/gradle-wrapper.jar b/examples/java-agent/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..29953ea141f55e3b8fc691d31b5ca8816d89fa87 GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/java-agent/gradlew.bat b/examples/java-agent/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/examples/java-agent/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml new file mode 100644 index 0000000000..0f9d9feb86 --- /dev/null +++ b/examples/java-agent/pom.xml @@ -0,0 +1,119 @@ + + 4.0.0 + + example + java-agent + 1 + + + UTF-8 + 0.9.11 + 3.8.0 + 1.4.1 + 2.2.5 + + + ${project.build.directory}/jib-agents + + + https://storage.googleapis.com/cloud-debugger/compute-java/debian-wheezy/cdbg_java_agent_gce.tar.gz + + + /opt/cdbg + + + + + com.sparkjava + spark-core + 2.7.2 + + + org.slf4j + slf4j-simple + 1.7.21 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + + revision + + + initialize + + true + + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib + + ${agent-extraction-root} + + 4567 + + -agentpath:${stackdriver-debugger-agent-location}/cdbg_java_agent.so=--logtostderr=1 + -Dcom.google.cdbg.module=${project.artifactId} + -Dcom.google.cdbg.version=${project.version} + + + + + + package + + build + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + ${download-maven-plugin.version} + + + install-stackdriver-debugger + prepare-package + + wget + + + ${stackdriver-debugger-agent-url} + true + ${agent-extraction-root}/${stackdriver-debugger-agent-location} + + + + + + + diff --git a/examples/java-agent/src/main/java/example/HelloWorld.java b/examples/java-agent/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..8cf08fec54 --- /dev/null +++ b/examples/java-agent/src/main/java/example/HelloWorld.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import static spark.Spark.get; + +public class HelloWorld { + public static void main(String[] args) { + get("/hello", (req, res) -> "Hello World"); + } +} diff --git a/examples/java-agent/src/main/resources/world b/examples/java-agent/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/examples/java-agent/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file From 4af5907a840309a37f8738d1bce21d62e8e46554 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Sat, 13 Oct 2018 22:14:53 -0400 Subject: [PATCH 0294/2020] Update examples to use jib-gradle-plugin 0.9.12 (#1140) --- examples/helloworld/build.gradle | 2 +- examples/java-agent/build.gradle | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 1147bbab0f..cf56e74407 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.9.12' } sourceCompatibility = 1.8 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 3362361a2c..a8d3cf5906 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.9.12' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index e21d9ea7c2..7b7d96a535 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.9.12' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 805d21b1ec..edc342f6ff 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.11' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.12' } } diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 27d05d5915..a35cc171a1 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.9.12' } apply plugin: 'java' diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index f6cf949683..eabe0072c0 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.9.12' } repositories { From 362f11bac2f6b8c10d80679a373dfa2177d1c564 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 15 Oct 2018 11:40:38 -0400 Subject: [PATCH 0295/2020] Allow using path or suffix in cred helper config (#1132) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../tools/jib/gradle/BuildImageTask.java | 2 +- .../gradle/PluginConfigurationProcessor.java | 5 ++- jib-maven-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/maven/BuildImageMojo.java | 9 +++- .../maven/PluginConfigurationProcessor.java | 9 +++- .../common/DefaultCredentialRetrievers.java | 41 ++++++++++++------ .../DefaultCredentialRetrieversTest.java | 42 +++++++++++++++++-- 8 files changed, 88 insertions(+), 22 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index a586cb9281..8984a95fcb 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changed +- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index e126d34eef..aa2a1b6f32 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -109,7 +109,7 @@ public void buildImage() optionalToCredential.ifPresent( toCredential -> defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); - defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getTo().getCredHelper()); + defaultCredentialRetrievers.setCredentialHelper(jibExtension.getTo().getCredHelper()); RegistryImage targetImage = RegistryImage.named(targetImageReference); defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index c50ac59bea..b89c52134b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -37,6 +37,7 @@ import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; +import java.io.FileNotFoundException; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -99,7 +100,7 @@ static void disableHttpLogging() { */ static PluginConfigurationProcessor processCommonConfiguration( Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) - throws InvalidImageReferenceException, NumberFormatException { + throws InvalidImageReferenceException, NumberFormatException, FileNotFoundException { JibSystemProperties.checkHttpTimeoutProperty(); // TODO: Instead of disabling logging, have authentication credentials be provided @@ -126,7 +127,7 @@ static PluginConfigurationProcessor processCommonConfiguration( optionalFromCredential.ifPresent( fromCredential -> defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth")); - defaultCredentialRetrievers.setCredentialHelperSuffix(jibExtension.getFrom().getCredHelper()); + defaultCredentialRetrievers.setCredentialHelper(jibExtension.getFrom().getCredHelper()); List entrypoint = computeEntrypoint(logger, jibExtension, projectProperties); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 0dc630c398..11845c4e19 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changed +- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 263e8066fb..8910eec289 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -35,6 +35,7 @@ import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; import java.util.Optional; @@ -121,10 +122,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { defaultCredentialRetrievers.setInferredCredential( toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); } - defaultCredentialRetrievers.setCredentialHelperSuffix(getTargetImageCredentialHelperName()); + defaultCredentialRetrievers.setCredentialHelper(getTargetImageCredentialHelperName()); RegistryImage targetImage = RegistryImage.named(targetImageReference); - defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); + try { + defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); + } catch (FileNotFoundException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index f457dea091..2bffaf18cf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -37,6 +37,7 @@ import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; +import java.io.FileNotFoundException; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -168,13 +169,17 @@ static PluginConfigurationProcessor processCommonConfiguration( defaultCredentialRetrievers.setInferredCredential( fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); } - defaultCredentialRetrievers.setCredentialHelperSuffix( + defaultCredentialRetrievers.setCredentialHelper( jibPluginConfiguration.getBaseImageCredentialHelperName()); List entrypoint = computeEntrypoint(logger, jibPluginConfiguration, projectProperties); RegistryImage baseImage = RegistryImage.named(baseImageReference); - defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + try { + defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + } catch (FileNotFoundException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } JibContainerBuilder jibContainerBuilder = Jib.from(baseImage) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index e12602f48a..7ac9a34ae7 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -20,6 +20,10 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; +import java.io.FileNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; @@ -56,7 +60,7 @@ public static DefaultCredentialRetrievers init( @Nullable private CredentialRetriever knownCredentialRetriever; @Nullable private CredentialRetriever inferredCredentialRetriever; - @Nullable private String credentialHelperSuffix; + @Nullable private String credentialHelper; private DefaultCredentialRetrievers(CredentialRetrieverFactory credentialRetrieverFactory) { this.credentialRetrieverFactory = credentialRetrieverFactory; @@ -90,15 +94,15 @@ public DefaultCredentialRetrievers setInferredCredential( } /** - * Sets the suffix for a known credential helper. + * Sets the known credential helper. May either be a path to a credential helper executable, or a + * credential helper suffix (following {@code docker-credential-}). * - * @param credentialHelperSuffix the known credential helper suffix (following {@code - * docker-credential-}) + * @param credentialHelper the path to a credential helper, or a credential helper suffix + * (following {@code docker-credential-}). * @return this */ - public DefaultCredentialRetrievers setCredentialHelperSuffix( - @Nullable String credentialHelperSuffix) { - this.credentialHelperSuffix = credentialHelperSuffix; + public DefaultCredentialRetrievers setCredentialHelper(@Nullable String credentialHelper) { + this.credentialHelper = credentialHelper; return this; } @@ -106,16 +110,29 @@ public DefaultCredentialRetrievers setCredentialHelperSuffix( * Makes a list of {@link CredentialRetriever}s. * * @return the list of {@link CredentialRetriever}s + * @throws FileNotFoundException if a credential helper path is specified, but the file doesn't + * exist */ - public List asList() { + public List asList() throws FileNotFoundException { List credentialRetrievers = new ArrayList<>(); if (knownCredentialRetriever != null) { credentialRetrievers.add(knownCredentialRetriever); } - if (credentialHelperSuffix != null) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); + if (credentialHelper != null) { + // If credential helper contains file separator, treat as path; otherwise treat as suffix + if (credentialHelper.contains(FileSystems.getDefault().getSeparator())) { + if (Files.exists(Paths.get(credentialHelper))) { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelper)); + } else { + throw new FileNotFoundException( + "Specified credential helper was not found: " + credentialHelper); + } + } else { + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper( + DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + credentialHelper)); + } } if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index b90f095ead..c98c724446 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -19,11 +19,17 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Before; +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.Mockito; @@ -33,6 +39,8 @@ @RunWith(MockitoJUnitRunner.class) public class DefaultCredentialRetrieversTest { + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private CredentialRetrieverFactory mockCredentialRetrieverFactory; @Mock private CredentialRetriever mockDockerCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockKnownCredentialRetriever; @@ -59,7 +67,7 @@ public void setUp() { } @Test - public void testInitAsList() { + public void testInitAsList() throws FileNotFoundException { List credentialRetrievers = DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); Assert.assertEquals( @@ -69,12 +77,12 @@ public void testInitAsList() { } @Test - public void testInitAsList_all() { + public void testInitAsList_all() throws FileNotFoundException { List credentialRetrievers = DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) .setKnownCredential(knownCredential, "credentialSource") .setInferredCredential(inferredCredential, "inferredCredentialSource") - .setCredentialHelperSuffix("credentialHelperSuffix") + .setCredentialHelper("credentialHelperSuffix") .asList(); Assert.assertEquals( Arrays.asList( @@ -91,4 +99,32 @@ public void testInitAsList_all() { Mockito.verify(mockCredentialRetrieverFactory) .dockerCredentialHelper("docker-credential-credentialHelperSuffix"); } + + @Test + public void testInitAsList_credentialHelperPath() throws IOException { + Path fakeCredentialHelperPath = temporaryFolder.newFile("fake-credHelper").toPath(); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) + .setCredentialHelper(fakeCredentialHelperPath.toString()); + + List credentialRetrievers = defaultCredentialRetrievers.asList(); + Assert.assertEquals( + Arrays.asList( + mockDockerCredentialHelperCredentialRetriever, + mockInferCredentialHelperCredentialRetriever, + mockDockerConfigCredentialRetriever), + credentialRetrievers); + Mockito.verify(mockCredentialRetrieverFactory) + .dockerCredentialHelper(fakeCredentialHelperPath.toString()); + + Files.delete(fakeCredentialHelperPath); + try { + defaultCredentialRetrievers.asList(); + Assert.fail("Expected FileNotFoundException"); + } catch (FileNotFoundException ex) { + Assert.assertEquals( + "Specified credential helper was not found: " + fakeCredentialHelperPath, + ex.getMessage()); + } + } } From 04cf15b5b3fe8ed813e7ec28de89a7bfd1e5e70f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Oct 2018 15:00:34 -0400 Subject: [PATCH 0296/2020] Refactor layer construction code for Maven and Gradle (#1117) --- .../jib/frontend/JavaLayerConfigurations.java | 120 ++---- .../frontend/JavaLayerConfigurationsTest.java | 352 +++++++++++++----- .../jib/gradle/GradleLayerConfigurations.java | 189 ++-------- .../jib/gradle/DockerContextTaskTest.java | 3 +- .../gradle/GradleLayerConfigurationsTest.java | 25 +- .../jib/maven/MavenLayerConfigurations.java | 137 +------ .../maven/MavenLayerConfigurationsTest.java | 167 ++------- .../common/JavaLayerConfigurationsHelper.java | 76 ++++ .../JavaLayerConfigurationsHelperTest.java | 132 +++++++ .../exploded-war/META-INF/context.xml | 0 .../src/test/resources/exploded-war/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 1 + .../WEB-INF/classes/package/Other.class | 1 + .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../resources/exploded-war/WEB-INF/web.xml | 0 17 files changed, 621 insertions(+), 582 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java rename jib-core/src/test/resources/exploded-war/file => jib-plugins-common/src/test/resources/exploded-war/META-INF/context.xml (100%) create mode 100644 jib-plugins-common/src/test/resources/exploded-war/Test.jsp create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/test.properties create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependency-1.0.0.jar create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-plugins-common/src/test/resources/exploded-war/WEB-INF/web.xml diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 00abd34a20..9191325a00 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -18,22 +18,25 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.util.EnumMap; import java.util.Map; +import java.util.function.Predicate; /** Builds {@link LayerConfiguration}s for a Java application. */ public class JavaLayerConfigurations { /** Represents the different types of layers for a Java application. */ - @VisibleForTesting - enum LayerType { + public static enum LayerType { DEPENDENCIES("dependencies"), SNAPSHOT_DEPENDENCIES("snapshot dependencies"), RESOURCES("resources"), @@ -47,7 +50,7 @@ enum LayerType { * * @param name name to set for the layer; does not affect the contents of the layer */ - LayerType(String name) { + private LayerType(String name) { this.name = name; } @@ -70,94 +73,56 @@ private Builder() { } /** - * Adds a file to the dependency layer. If the source file is a directory, the directory and its - * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive} - * for concrete examples about how the file will be placed in the image. + * Adds a file to a layer. Only adds the single source file to the exact path in the container + * file system. (If the source file is a directory, does not copy its contents but creates only + * the directory.) See {@link LayerConfiguration.Builder#addEntry} for concrete examples about + * how the file will be placed in the image. * + * @param layerType the layer to add files into * @param sourceFile the source file to add to the layer * @param pathInContainer the path in the container file system corresponding to the {@code * sourceFile} * @return this - * @throws IOException if an exception occurred when recursively listing the directory + * @see LayerConfiguration.Builder#addEntry(Path, AbsoluteUnixPath) */ - public Builder addDependencyFile(Path sourceFile, AbsoluteUnixPath pathInContainer) - throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.DEPENDENCIES)) - .addEntryRecursive(sourceFile, pathInContainer); + public Builder addFile(LayerType layerType, Path sourceFile, AbsoluteUnixPath pathInContainer) { + Preconditions.checkNotNull(layerBuilders.get(layerType)) + .addEntry(sourceFile, pathInContainer); return this; } /** - * Adds a file to the snapshot dependency layer. If the source file is a directory, the - * directory and its contents will be added recursively. See {@link - * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will - * be placed in the image. + * Adds directory contents to a layer selectively (via {@code pathFilter}) and recursively. + * {@code sourceRoot} must be a directory. Empty directories will always be added regardless of + * {@code pathFilter}, except that {@code sourceRoot} is never added. * - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @return this - * @throws IOException if an exception occurred when recursively listing the directory - */ - public Builder addSnapshotDependencyFile(Path sourceFile, AbsoluteUnixPath pathInContainer) - throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.SNAPSHOT_DEPENDENCIES)) - .addEntryRecursive(sourceFile, pathInContainer); - return this; - } - - /** - * Adds a file to the resource layer. If the source file is a directory, the directory and its - * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive} - * for concrete examples about how the file will be placed in the image. + *

The contents of {@code sourceRoot} will be placed into {@code basePathInContainer}. For + * example, if {@code sourceRoot} is {@code /usr/home}, {@code /usr/home/passwd} exists locally, + * and {@code basePathInContainer} is {@code /etc}, then the image will have {@code + * /etc/passwd}. * - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} + * @param layerType the layer to add files into + * @param sourceRoot root directory whose contents will be added + * @param pathFilter filter that determines which files (not directories) should be added + * @param basePathInContainer directory in the layer into which the source contents are added * @return this - * @throws IOException if an exception occurred when recursively listing the directory + * @throws IOException error while listing directories + * @throws NotDirectoryException if {@code sourceRoot} is not a directory */ - public Builder addResourceFile(Path sourceFile, AbsoluteUnixPath pathInContainer) + public Builder addDirectoryContents( + LayerType layerType, + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer) throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.RESOURCES)) - .addEntryRecursive(sourceFile, pathInContainer); - return this; - } - - /** - * Adds a file to the classes layer. If the source file is a directory, the directory and its - * contents will be added recursively. See {@link LayerConfiguration.Builder#addEntryRecursive} - * for concrete examples about how the file will be placed in the image. - * - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @return this - * @throws IOException if an exception occurred when recursively listing the directory - */ - public Builder addClassFile(Path sourceFile, AbsoluteUnixPath pathInContainer) - throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.CLASSES)) - .addEntryRecursive(sourceFile, pathInContainer); - return this; - } - - /** - * Adds a file to the extra files layer. If the source file is a directory, the directory and - * its contents will be added recursively. See {@link - * LayerConfiguration.Builder#addEntryRecursive} for concrete examples about how the file will - * be placed in the image. - * - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @return this - * @throws IOException if an exception occurred when recursively listing the directory - */ - public Builder addExtraFile(Path sourceFile, AbsoluteUnixPath pathInContainer) - throws IOException { - Preconditions.checkNotNull(layerBuilders.get(LayerType.EXTRA_FILES)) - .addEntryRecursive(sourceFile, pathInContainer); + LayerConfiguration.Builder builder = Preconditions.checkNotNull(layerBuilders.get(layerType)); + + new DirectoryWalker(sourceRoot) + .filterRoot() + .filter(path -> Files.isDirectory(path) || pathFilter.test(path)) + .walk( + path -> + builder.addEntry(path, basePathInContainer.resolve(sourceRoot.relativize(path)))); return this; } @@ -188,9 +153,6 @@ public static Builder builder() { */ public static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; - /** The filename suffix for a maven/gradle snapshot dependency */ - public static final String SNAPSHOT_FILENAME_SUFFIX = "SNAPSHOT"; - private final ImmutableMap layerConfigurationMap; private JavaLayerConfigurations( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index b5b5b4d27e..b1560b9c16 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -2,81 +2,74 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.function.Supplier; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; +import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Tests for {@link JavaLayerConfigurations}. */ public class JavaLayerConfigurationsTest { - private static JavaLayerConfigurations createFakeConfigurations() throws IOException { - return JavaLayerConfigurations.builder() - .addDependencyFile(Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path")) - .addSnapshotDependencyFile( - Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots")) - .addResourceFile(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here")) - .addClassFile(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here")) - .addExtraFile(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras")) - .build(); - } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private static List layerEntriesToSourceFiles(List entries) { - return entries.stream().map(LayerEntry::getSourceFile).collect(Collectors.toList()); + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); } - private static List layerEntriesToExtractionPaths(List entries) { - return entries.stream().map(LayerEntry::getExtractionPath).collect(Collectors.toList()); + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); } - private static List toSortedStrings(List paths) { - return paths.stream().map(T::toString).sorted().collect(Collectors.toList()); + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); } - private static void verifyRecursiveAdd( - Supplier> layerEntriesSupplier, Path sourceRoot, String extractionRoot) { - AbsoluteUnixPath extractionRootPath = AbsoluteUnixPath.get(extractionRoot); - List expectedPaths = - Arrays.asList( - "", - "file1", - "file2", - "sub-directory", - "sub-directory/file3", - "sub-directory/file4", - "sub-directory/leaf", - "sub-directory/leaf/file5", - "sub-directory/leaf/file6"); - - List expectedSourcePaths = - expectedPaths.stream().map(sourceRoot::resolve).collect(Collectors.toList()); - List expectedTargetPaths = - expectedPaths.stream().map(extractionRootPath::resolve).collect(Collectors.toList()); - - List sourcePaths = layerEntriesToSourceFiles(layerEntriesSupplier.get()); - Assert.assertEquals(toSortedStrings(expectedSourcePaths), toSortedStrings(sourcePaths)); - - List targetPaths = layerEntriesToExtractionPaths(layerEntriesSupplier.get()); - Assert.assertEquals(toSortedStrings(expectedTargetPaths), toSortedStrings(targetPaths)); + private static JavaLayerConfigurations createFakeConfigurations() { + return JavaLayerConfigurations.builder() + .addFile( + LayerType.DEPENDENCIES, + Paths.get("dependency"), + AbsoluteUnixPath.get("/dependency/path")) + .addFile( + LayerType.SNAPSHOT_DEPENDENCIES, + Paths.get("snapshot dependency"), + AbsoluteUnixPath.get("/snapshots")) + .addFile( + LayerType.RESOURCES, Paths.get("resource"), AbsoluteUnixPath.get("/resources/here")) + .addFile(LayerType.CLASSES, Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here")) + .addFile( + LayerType.EXTRA_FILES, Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras")) + .build(); } @Test - public void testLabels() throws IOException { + public void testLabels() { JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); List expectedLabels = new ArrayList<>(); - for (JavaLayerConfigurations.LayerType layerType : JavaLayerConfigurations.LayerType.values()) { + for (LayerType layerType : LayerType.values()) { expectedLabels.add(layerType.getName()); } List actualLabels = new ArrayList<>(); @@ -87,59 +80,78 @@ public void testLabels() throws IOException { } @Test - public void testAddFile() throws IOException { + public void testAddFile() { JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); - List> expectedFiles = + Assert.assertEquals( Arrays.asList( - Collections.singletonList(Paths.get("dependency")), - Collections.singletonList(Paths.get("snapshot dependency")), - Collections.singletonList(Paths.get("resource")), - Collections.singletonList(Paths.get("class")), - Collections.singletonList(Paths.get("extra file"))); - List> actualFiles = - javaLayerConfigurations - .getLayerConfigurations() - .stream() - .map(LayerConfiguration::getLayerEntries) - .map(JavaLayerConfigurationsTest::layerEntriesToSourceFiles) - .collect(Collectors.toList()); - Assert.assertEquals(expectedFiles, actualFiles); + new LayerEntry(Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path"))), + javaLayerConfigurations.getDependencyLayerEntries()); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots"))), + javaLayerConfigurations.getSnapshotDependencyLayerEntries()); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here"))), + javaLayerConfigurations.getResourceLayerEntries()); + Assert.assertEquals( + Arrays.asList(new LayerEntry(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here"))), + javaLayerConfigurations.getClassLayerEntries()); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras"))), + javaLayerConfigurations.getExtraFilesLayerEntries()); } @Test - public void testAddFile_directories() throws IOException, URISyntaxException { + public void testAddFile_directories() throws URISyntaxException { Path sourceDirectory = Paths.get(Resources.getResource("random-contents").toURI()); JavaLayerConfigurations configurations = JavaLayerConfigurations.builder() - .addDependencyFile(sourceDirectory, AbsoluteUnixPath.get("/libs/dir")) - .addSnapshotDependencyFile(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target")) - .addResourceFile(sourceDirectory, AbsoluteUnixPath.get("/resources")) - .addClassFile(sourceDirectory, AbsoluteUnixPath.get("/classes/here")) - .addExtraFile(sourceDirectory, AbsoluteUnixPath.get("/extra/files")) + .addFile(LayerType.DEPENDENCIES, sourceDirectory, AbsoluteUnixPath.get("/libs/dir")) + .addFile( + LayerType.SNAPSHOT_DEPENDENCIES, + sourceDirectory, + AbsoluteUnixPath.get("/snapshots/target")) + .addFile(LayerType.RESOURCES, sourceDirectory, AbsoluteUnixPath.get("/resources")) + .addFile(LayerType.CLASSES, sourceDirectory, AbsoluteUnixPath.get("/classes/here")) + .addFile(LayerType.EXTRA_FILES, sourceDirectory, AbsoluteUnixPath.get("/extra/files")) .build(); - verifyRecursiveAdd(configurations::getDependencyLayerEntries, sourceDirectory, "/libs/dir"); - verifyRecursiveAdd( - configurations::getSnapshotDependencyLayerEntries, sourceDirectory, "/snapshots/target"); - verifyRecursiveAdd(configurations::getResourceLayerEntries, sourceDirectory, "/resources"); - verifyRecursiveAdd(configurations::getClassLayerEntries, sourceDirectory, "/classes/here"); - verifyRecursiveAdd(configurations::getExtraFilesLayerEntries, sourceDirectory, "/extra/files"); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/libs/dir"))), + configurations.getDependencyLayerEntries()); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target"))), + configurations.getSnapshotDependencyLayerEntries()); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/resources"))), + configurations.getResourceLayerEntries()); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/classes/here"))), + configurations.getClassLayerEntries()); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/extra/files"))), + configurations.getExtraFilesLayerEntries()); } @Test - public void testAddFile_regularFiles() throws IOException, URISyntaxException { + public void testAddFile_regularFiles() throws URISyntaxException { Path sourceFile = Paths.get(Resources.getResource("random-contents/sub-directory/leaf/file6").toURI()); JavaLayerConfigurations configurations = JavaLayerConfigurations.builder() - .addDependencyFile(sourceFile, AbsoluteUnixPath.get("/libs/file")) - .addSnapshotDependencyFile(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file")) - .addResourceFile(sourceFile, AbsoluteUnixPath.get("/resources-file")) - .addClassFile(sourceFile, AbsoluteUnixPath.get("/classes/file")) - .addExtraFile(sourceFile, AbsoluteUnixPath.get("/some/file")) + .addFile(LayerType.DEPENDENCIES, sourceFile, AbsoluteUnixPath.get("/libs/file")) + .addFile( + LayerType.SNAPSHOT_DEPENDENCIES, + sourceFile, + AbsoluteUnixPath.get("/snapshots/target/file")) + .addFile(LayerType.RESOURCES, sourceFile, AbsoluteUnixPath.get("/resources-file")) + .addFile(LayerType.CLASSES, sourceFile, AbsoluteUnixPath.get("/classes/file")) + .addFile(LayerType.EXTRA_FILES, sourceFile, AbsoluteUnixPath.get("/some/file")) .build(); Assert.assertEquals( @@ -160,20 +172,33 @@ public void testAddFile_regularFiles() throws IOException, URISyntaxException { } @Test - public void testWebApp() throws IOException { + public void testAddFile_webAppSample() { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); JavaLayerConfigurations configurations = JavaLayerConfigurations.builder() - .addResourceFile(Paths.get("test.jsp"), appRoot.resolve("test.jsp")) - .addResourceFile(Paths.get("META-INF/"), appRoot.resolve("META-INF/")) - .addResourceFile(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml")) - .addResourceFile(Paths.get("sub_dir/"), appRoot.resolve("WEB-INF/sub_dir/")) - .addDependencyFile(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar")) - .addSnapshotDependencyFile( - Paths.get("my-SNAPSHOT.jar"), appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) - .addClassFile(Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class")) - .addExtraFile(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file")) + .addFile(LayerType.RESOURCES, Paths.get("test.jsp"), appRoot.resolve("test.jsp")) + .addFile(LayerType.RESOURCES, Paths.get("META-INF/"), appRoot.resolve("META-INF/")) + .addFile( + LayerType.RESOURCES, + Paths.get("context.xml"), + appRoot.resolve("WEB-INF/context.xml")) + .addFile( + LayerType.RESOURCES, Paths.get("sub_dir/"), appRoot.resolve("WEB-INF/sub_dir/")) + .addFile( + LayerType.DEPENDENCIES, + Paths.get("myLib.jar"), + appRoot.resolve("WEB-INF/lib/myLib.jar")) + .addFile( + LayerType.SNAPSHOT_DEPENDENCIES, + Paths.get("my-SNAPSHOT.jar"), + appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) + .addFile( + LayerType.CLASSES, + Paths.get("test.class"), + appRoot.resolve("WEB-INF/classes/test.class")) + .addFile( + LayerType.EXTRA_FILES, Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file")) .build(); ImmutableList expectedDependenciesLayer = @@ -203,4 +228,155 @@ public void testWebApp() throws IOException { Assert.assertEquals(expectedClassesLayer, configurations.getClassLayerEntries()); Assert.assertEquals(expectedExtraLayer, configurations.getExtraFilesLayerEntries()); } + + @Test + public void testAddDirectoryContents_file() throws IOException { + temporaryFolder.newFile("file"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, path -> true, basePath) + .build(); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceRoot.resolve("file"), basePath.resolve("file"))), + configurations.getExtraFilesLayerEntries()); + } + + @Test + public void testAddDirectoryContents_emptyDirectory() throws IOException { + temporaryFolder.newFolder("leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.CLASSES, sourceRoot, path -> true, basePath) + .build(); + Assert.assertEquals( + Arrays.asList(new LayerEntry(sourceRoot.resolve("leaf"), basePath.resolve("leaf"))), + configurations.getClassLayerEntries()); + } + + @Test + public void testAddDirectoryContents_directoriesAdded() throws IOException { + temporaryFolder.newFolder("non-empty", "leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.RESOURCES, sourceRoot, path -> true, basePath) + .build(); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(sourceRoot.resolve("non-empty"), basePath.resolve("non-empty")), + new LayerEntry( + sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf"))), + configurations.getResourceLayerEntries()); + } + + @Test + public void testAddDirectoryContents_filter() throws IOException { + temporaryFolder.newFile("non-target"); + temporaryFolder.newFolder("sub"); + temporaryFolder.newFile("sub/target"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + + Predicate nameIsTarget = path -> "target".equals(path.getFileName().toString()); + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.DEPENDENCIES, sourceRoot, nameIsTarget, basePath) + .build(); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub")), + new LayerEntry(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target"))), + configurations.getDependencyLayerEntries()); + } + + @Test + public void testAddDirectoryContents_directoriesForced() throws IOException { + temporaryFolder.newFolder("sub", "leaf"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); + + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, path -> false, basePath) + .build(); + Assert.assertEquals( + Arrays.asList( + new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub")), + new LayerEntry(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf"))), + configurations.getExtraFilesLayerEntries()); + } + + @Test + public void testAddDirectoryContents_fileAsSourceRoot() throws IOException { + Path sourceFile = temporaryFolder.newFile("foo").toPath(); + + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + JavaLayerConfigurations.Builder builder = JavaLayerConfigurations.builder(); + try { + builder.addDirectoryContents(LayerType.DEPENDENCIES, sourceFile, path -> true, basePath); + Assert.fail(); + } catch (NotDirectoryException ex) { + Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("foo is not a directory")); + } + } + + @Test + public void testAddDirectoryContents_complex() throws IOException { + temporaryFolder.newFile("A.class"); + temporaryFolder.newFile("B.java"); + temporaryFolder.newFolder("example", "dir"); + temporaryFolder.newFile("example/dir/C.class"); + temporaryFolder.newFile("example/C.class"); + temporaryFolder.newFolder("test", "resources", "leaf"); + temporaryFolder.newFile("test/resources/D.java"); + temporaryFolder.newFile("test/D.class"); + + Path sourceRoot = temporaryFolder.getRoot().toPath(); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/base"); + + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, isClassFile, basePath) + .build(); + + assertSourcePathsUnordered( + Arrays.asList( + sourceRoot.resolve("A.class"), + sourceRoot.resolve("example"), + sourceRoot.resolve("example/dir"), + sourceRoot.resolve("example/dir/C.class"), + sourceRoot.resolve("example/C.class"), + sourceRoot.resolve("test"), + sourceRoot.resolve("test/resources"), + sourceRoot.resolve("test/resources/leaf"), + sourceRoot.resolve("test/D.class")), + configurations.getExtraFilesLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/base/A.class", + "/base/example", + "/base/example/dir", + "/base/example/dir/C.class", + "/base/example/C.class", + "/base/test", + "/base/test/resources", + "/base/test/resources/leaf", + "/base/test/D.class"), + configurations.getExtraFilesLayerEntries()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 4ecf340167..1f238f5df5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -17,18 +17,15 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; @@ -75,18 +72,28 @@ static JavaLayerConfigurations getForProject( private static JavaLayerConfigurations getForNonWarProject( Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + AbsoluteUnixPath dependenciesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); + AbsoluteUnixPath resourcesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); + AbsoluteUnixPath classesExtractionPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + + Builder layerBuilder = JavaLayerConfigurations.builder(); + JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); - List dependenciesFiles = new ArrayList<>(); - List snapshotDependenciesFiles = new ArrayList<>(); - List classesFiles = new ArrayList<>(); - List extraFiles = new ArrayList<>(); - - // Adds each file in each classes output directory to the classes files list. FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); + Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); + FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); + FileCollection dependencyFiles = + allFiles + .minus(classesOutputDirectories) + .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); + + // Adds class files. logger.info("Adding corresponding output directories of source sets to image"); for (File classesOutputDirectory : classesOutputDirectories) { if (Files.notExists(classesOutputDirectory.toPath())) { @@ -94,68 +101,40 @@ private static JavaLayerConfigurations getForNonWarProject( continue; } logger.info("\t'" + classesOutputDirectory + "'"); - try (Stream classFileStream = Files.list(classesOutputDirectory.toPath())) { - classFileStream.forEach(classesFiles::add); - } + layerBuilder.addDirectoryContents( + LayerType.CLASSES, classesOutputDirectory.toPath(), path -> true, classesExtractionPath); } - if (classesFiles.isEmpty()) { + if (classesOutputDirectories.filter(File::exists).isEmpty()) { logger.warn("No classes files were found - did you compile your project?"); } - Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); + // Adds resource files. + if (Files.exists(resourcesOutputDirectory)) { + layerBuilder.addDirectoryContents( + LayerType.RESOURCES, resourcesOutputDirectory, path -> true, resourcesExtractionPath); + } - // Adds all other files to the dependencies files list. - FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); - // Removes the classes output directories. - allFiles = allFiles.minus(classesOutputDirectories); - for (File dependencyFile : allFiles) { - // Removes the resources output directory. - if (resourcesOutputDirectory.equals(dependencyFile.toPath())) { - continue; - } - if (dependencyFile.getName().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX)) { - snapshotDependenciesFiles.add(dependencyFile.toPath()); - } else { - dependenciesFiles.add(dependencyFile.toPath()); - } + // Adds dependency files. + for (File dependencyFile : dependencyFiles) { + boolean isSnapshot = dependencyFile.getName().contains("SNAPSHOT"); + LayerType layerType = isSnapshot ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; + layerBuilder.addFile( + layerType, + dependencyFile.toPath(), + dependenciesExtractionPath.resolve(dependencyFile.getName())); } // Adds all the extra files. if (Files.exists(extraDirectory)) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { - extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); - } + layerBuilder.addDirectoryContents( + LayerType.EXTRA_FILES, extraDirectory, path -> true, AbsoluteUnixPath.get("/")); } - AbsoluteUnixPath dependenciesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - AbsoluteUnixPath resourcesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - AbsoluteUnixPath classesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - - Builder layerBuilder = JavaLayerConfigurations.builder(); - for (Path file : dependenciesFiles) { - layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName())); - } - for (Path file : snapshotDependenciesFiles) { - layerBuilder.addSnapshotDependencyFile( - file, dependenciesExtractionPath.resolve(file.getFileName())); - } - if (Files.exists(resourcesOutputDirectory)) { - layerBuilder.addResourceFile(resourcesOutputDirectory, resourcesExtractionPath); - } - for (Path file : classesFiles) { - layerBuilder.addClassFile(file, classesExtractionPath.resolve(file.getFileName())); - } - for (Path file : extraFiles) { - layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName())); - } return layerBuilder.build(); } /** - * Resolves the {@link JavaLayerConfigurations} for a War Gradle {@link Project}. + * Resolves the {@link JavaLayerConfigurations} for a WAR Gradle {@link Project}. * * @param project the Gradle {@link Project} * @param logger the build logger for providing feedback about the resolution @@ -167,98 +146,8 @@ private static JavaLayerConfigurations getForNonWarProject( private static JavaLayerConfigurations getForWarProject( Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { - List dependenciesFiles = new ArrayList<>(); - List snapshotDependenciesFiles = new ArrayList<>(); - List resourcesFiles = new ArrayList<>(); - List resourcesWebInfFiles = new ArrayList<>(); - List extraFiles = new ArrayList<>(); - Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); - - Path libOutputDirectory = explodedWarPath.resolve("WEB-INF/lib"); - if (Files.exists(libOutputDirectory)) { - try (Stream dependencyFileStream = Files.list(libOutputDirectory)) { - dependencyFileStream.forEach( - path -> { - if (path.toString().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX)) { - snapshotDependenciesFiles.add(path); - } else { - dependenciesFiles.add(path); - } - }); - } - } - - // First, all files except WEB-INF go into the resources layer. - try (Stream fileStream = Files.list(explodedWarPath)) { - fileStream.filter(path -> !path.endsWith("WEB-INF")).forEach(resourcesFiles::add); - } - // Some files in WEB-INF/ (e.g. web.xml, ...) need to go into the resources layer. However, - // don't add or look into WEB-INF/classes and WEB-INF/lib. - Path webInfOutputDirectory = explodedWarPath.resolve("WEB-INF"); - if (Files.exists(webInfOutputDirectory)) { - try (Stream fileStream = Files.list(webInfOutputDirectory)) { - fileStream - .filter(path -> !path.endsWith("classes") && !path.endsWith("lib")) - .forEach(resourcesWebInfFiles::add); - } - } - - // Adds all the extra files. - if (Files.exists(extraDirectory)) { - try (Stream extraFilesLayerDirectoryFiles = Files.list(extraDirectory)) { - extraFiles = extraFilesLayerDirectoryFiles.collect(Collectors.toList()); - } - } - - Builder layerBuilder = JavaLayerConfigurations.builder(); - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib"); - AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes"); - AbsoluteUnixPath webInfExtractionPath = appRoot.resolve("WEB-INF"); - - // For "WEB-INF/classes", *.class go into the class layer. All other files and empty directories - // go into the resource layer. - Path webInfClasses = explodedWarPath.resolve("WEB-INF/classes"); - if (Files.exists(webInfClasses)) { - new DirectoryWalker(webInfClasses) - .walk( - path -> { - AbsoluteUnixPath pathInContainer = - classesExtractionPath.resolve(webInfClasses.relativize(path)); - - if (path.getFileName().toString().endsWith(".class")) { - layerBuilder.addClassFile(path, pathInContainer); - - } else if (Files.isDirectory(path)) { - try (Stream fileStream = Files.list(path)) { - if (!fileStream.findAny().isPresent()) { - // The directory is empty - layerBuilder.addResourceFile(path, pathInContainer); - } - } - } else { - layerBuilder.addResourceFile(path, pathInContainer); - } - }); - } - - for (Path file : dependenciesFiles) { - layerBuilder.addDependencyFile(file, dependenciesExtractionPath.resolve(file.getFileName())); - } - for (Path file : snapshotDependenciesFiles) { - layerBuilder.addSnapshotDependencyFile( - file, dependenciesExtractionPath.resolve(file.getFileName())); - } - for (Path file : resourcesFiles) { - layerBuilder.addResourceFile(file, appRoot.resolve(file.getFileName())); - } - for (Path file : resourcesWebInfFiles) { - layerBuilder.addResourceFile(file, webInfExtractionPath.resolve(file.getFileName())); - } - for (Path file : extraFiles) { - layerBuilder.addExtraFile(file, AbsoluteUnixPath.get("/").resolve(file.getFileName())); - } - return layerBuilder.build(); + return JavaLayerConfigurationsHelper.fromExplodedWar(explodedWarPath, appRoot, extraDirectory); } private GradleLayerConfigurations() {} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 45f4ac9f67..279dba11b1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -54,7 +54,8 @@ public void setUp() throws IOException { JibExtension jibExtension = Mockito.mock(JibExtension.class); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); - Mockito.when(jibExtension.getExtraDirectoryPath()).thenReturn(projectRoot.getRoot().toPath()); + Mockito.when(jibExtension.getExtraDirectoryPath()) + .thenReturn(projectRoot.newFolder("src", "main", "jib").toPath()); Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 3493de32b1..2104b155f7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -164,7 +164,6 @@ public void test_correctFiles() throws URISyntaxException, IOException { applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); ImmutableList expectedResourcesFiles = ImmutableList.of( - applicationDirectory.resolve("resources"), applicationDirectory.resolve("resources/resourceA"), applicationDirectory.resolve("resources/resourceB"), applicationDirectory.resolve("resources/world")); @@ -243,7 +242,6 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException { configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( - "/my/app/resources", "/my/app/resources/resourceA", "/my/app/resources/resourceB", "/my/app/resources/world"), @@ -274,10 +272,7 @@ public void testGetForProject_defaultAppRoot() throws IOException { configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( - "/app/resources", - "/app/resources/resourceA", - "/app/resources/resourceB", - "/app/resources/world"), + "/app/resources/resourceA", "/app/resources/resourceB", "/app/resources/world"), configuration.getResourceLayerEntries()); assertExtractionPathsUnordered( Arrays.asList("/app/classes/HelloWorld.class", "/app/classes/some.class"), @@ -306,12 +301,18 @@ public void testWebApp() throws URISyntaxException, IOException { webAppDirectory.resolve("jib-exploded-war/META-INF"), webAppDirectory.resolve("jib-exploded-war/META-INF/context.xml"), webAppDirectory.resolve("jib-exploded-war/Test.jsp"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes"), webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib"), webAppDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")); ImmutableList expectedClassesFiles = ImmutableList.of( webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -341,13 +342,19 @@ public void testWebApp() throws URISyntaxException, IOException { "/my/app/META-INF", "/my/app/META-INF/context.xml", "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", "/my/app/WEB-INF/web.xml"), configuration.getResourceLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/Other.class"), configuration.getClassLayerEntries()); assertExtractionPathsUnordered( @@ -377,13 +384,19 @@ public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOExceptio "/jetty/webapps/ROOT/META-INF", "/jetty/webapps/ROOT/META-INF/context.xml", "/jetty/webapps/ROOT/Test.jsp", + "/jetty/webapps/ROOT/WEB-INF", + "/jetty/webapps/ROOT/WEB-INF/classes", "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", + "/jetty/webapps/ROOT/WEB-INF/classes/package", "/jetty/webapps/ROOT/WEB-INF/classes/package/test.properties", + "/jetty/webapps/ROOT/WEB-INF/lib", "/jetty/webapps/ROOT/WEB-INF/web.xml"), configuration.getResourceLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/jetty/webapps/ROOT/WEB-INF/classes/HelloWorld.class", + "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", + "/jetty/webapps/ROOT/WEB-INF/classes/package", "/jetty/webapps/ROOT/WEB-INF/classes/package/Other.class"), configuration.getClassLayerEntries()); assertExtractionPathsUnordered( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 292d0b5639..91f57da0fc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -17,18 +17,16 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; -import com.google.common.annotations.VisibleForTesting; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.function.Predicate; -import java.util.stream.Stream; import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; @@ -77,33 +75,27 @@ private static JavaLayerConfigurations getForNonWarProject( // Gets all the dependencies. for (Artifact artifact : project.getArtifacts()) { Path artifactPath = artifact.getFile().toPath(); - if (artifact.isSnapshot()) { - layerBuilder.addSnapshotDependencyFile( - artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); - } else { - layerBuilder.addDependencyFile( - artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); - } + LayerType layerType = + artifact.isSnapshot() ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; + layerBuilder.addFile( + layerType, artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); } Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); // Gets the classes files in the 'classes' output directory. - Predicate isClassFile = path -> path.toString().endsWith(".class"); - addFilesToLayer( - classesOutputDirectory, isClassFile, classesExtractionPath, layerBuilder::addClassFile); + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + layerBuilder.addDirectoryContents( + LayerType.CLASSES, classesOutputDirectory, isClassFile, classesExtractionPath); // Gets the resources files in the 'classes' output directory. - addFilesToLayer( - classesOutputDirectory, - isClassFile.negate(), - resourcesExtractionPath, - layerBuilder::addResourceFile); + layerBuilder.addDirectoryContents( + LayerType.RESOURCES, classesOutputDirectory, isClassFile.negate(), resourcesExtractionPath); // Adds all the extra files. if (Files.exists(extraDirectory)) { - AbsoluteUnixPath extractionBase = AbsoluteUnixPath.get("/"); - addFilesToLayer(extraDirectory, path -> true, extractionBase, layerBuilder::addExtraFile); + layerBuilder.addDirectoryContents( + LayerType.EXTRA_FILES, extraDirectory, path -> true, AbsoluteUnixPath.get("/")); } return layerBuilder.build(); @@ -127,108 +119,7 @@ private static JavaLayerConfigurations getForWarProject( // at build.getFinalName(). Path explodedWarPath = Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - Path webInfClasses = explodedWarPath.resolve("WEB-INF/classes"); - Path webInfLib = explodedWarPath.resolve("WEB-INF/lib"); - - AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve("WEB-INF/lib"); - AbsoluteUnixPath classesExtractionPath = appRoot.resolve("WEB-INF/classes"); - - Builder layerBuilder = JavaLayerConfigurations.builder(); - - // Gets all the dependencies. - Predicate isSnapshotDependency = - path -> path.toString().contains(JavaLayerConfigurations.SNAPSHOT_FILENAME_SUFFIX); - if (Files.exists(webInfLib)) { - addFilesToLayer( - webInfLib, - isSnapshotDependency, - dependenciesExtractionPath, - layerBuilder::addSnapshotDependencyFile); - addFilesToLayer( - webInfLib, - isSnapshotDependency.negate(), - dependenciesExtractionPath, - layerBuilder::addDependencyFile); - } - - // Gets the classes files in the 'WEB-INF/classes' output directory. - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - if (Files.exists(webInfClasses)) { - addFilesToLayer( - webInfClasses, isClassFile, classesExtractionPath, layerBuilder::addClassFile); - } - - // Gets the resources. - Predicate isResource = - path -> { - boolean inWebInfClasses = path.startsWith(webInfClasses); - boolean inWebInfLib = path.startsWith(webInfLib); - - return (!inWebInfClasses && !inWebInfLib) || (inWebInfClasses && !isClassFile.test(path)); - }; - addFilesToLayer(explodedWarPath, isResource, appRoot, layerBuilder::addResourceFile); - - // Adds all the extra files. - if (Files.exists(extraDirectory)) { - AbsoluteUnixPath extractionBase = AbsoluteUnixPath.get("/"); - addFilesToLayer(extraDirectory, path -> true, extractionBase, layerBuilder::addExtraFile); - } - - return layerBuilder.build(); - } - - @FunctionalInterface - @VisibleForTesting - static interface FileToLayerAdder { - - void add(Path sourcePath, AbsoluteUnixPath pathInContainer) throws IOException; - } - - @VisibleForTesting - static boolean isEmptyDirectory(Path path) throws IOException { - if (Files.isDirectory(path)) { - try (Stream stream = Files.list(path)) { - return !stream.findAny().isPresent(); - } - } - return false; - } - - /** - * Adds files to a layer selectively and recursively. {@code sourceRoot} must be a directory. - * Non-empty directories will be ignored, while empty directories will always be added. Note that - * empty directories will always be added regardless of {@code pathFilter}. - * - *

The contents of {@code sourceRoot} will be placed into {@code basePathInContainer}. For - * example, if {@code sourceRoot} is {@code /usr/home}, {@code /usr/home/passwd} exists locally, - * and {@code basePathInContainer} is {@code /etc}, then the image will have {@code /etc/passwd}. - * - * @param sourceRoot root directory whose contents will be added - * @param pathFilter only the files satisfying the filter will be added, unless the files are - * empty directories - * @param basePathInContainer directory in the layer into which the source contents are added - * @param addFileToLayer function that should add the file to the layer; the function gets the - * path of the source file (may be a directory) and the final destination path in the layer - * @throws IOException error while listing directories - * @throws NotDirectoryException if {@code sourceRoot} is not a directory - */ - @VisibleForTesting - static void addFilesToLayer( - Path sourceRoot, - Predicate pathFilter, - AbsoluteUnixPath basePathInContainer, - FileToLayerAdder addFileToLayer) - throws IOException { - - new DirectoryWalker(sourceRoot) - .walk( - path -> { - // Always add empty directories. However, ignore non-empty directories because - // otherwise JavaLayerConfigurations will add files recursively. - if (isEmptyDirectory(path) || (!Files.isDirectory(path) && pathFilter.test(path))) { - addFileToLayer.add(path, basePathInContainer.resolve(sourceRoot.relativize(path))); - } - }); + return JavaLayerConfigurationsHelper.fromExplodedWar(explodedWarPath, appRoot, extraDirectory); } private MavenLayerConfigurations() {} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 6cdc49bae0..ffa786e1a5 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -25,19 +25,16 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Build; import org.apache.maven.project.MavenProject; -import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -82,7 +79,9 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( + "/my/app/resources/directory", "/my/app/resources/directory/somefile", + "/my/app/resources/package", "/my/app/resources/resourceA", "/my/app/resources/resourceB", "/my/app/resources/world"), @@ -90,11 +89,14 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio assertExtractionPathsUnordered( Arrays.asList( "/my/app/classes/HelloWorld.class", + "/my/app/classes/directory", + "/my/app/classes/package", "/my/app/classes/package/some.class", "/my/app/classes/some.class"), configuration.getClassLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); } @Rule public final TestRepository testRepository = new TestRepository(); @@ -103,8 +105,6 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio @Mock private MavenProject mockMavenProject; @Mock private Build mockBuild; - @Mock private MavenLayerConfigurations.FileToLayerAdder fileToLayerAdder; - private Path extraFilesDirectory; @Before @@ -145,13 +145,17 @@ public void test_correctFiles() throws URISyntaxException, IOException { Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); ImmutableList expectedResourcesFiles = ImmutableList.of( + applicationDirectory.resolve("output/directory"), applicationDirectory.resolve("output/directory/somefile"), + applicationDirectory.resolve("output/package"), applicationDirectory.resolve("output/resourceA"), applicationDirectory.resolve("output/resourceB"), applicationDirectory.resolve("output/world")); ImmutableList expectedClassesFiles = ImmutableList.of( applicationDirectory.resolve("output/HelloWorld.class"), + applicationDirectory.resolve("output/directory"), + applicationDirectory.resolve("output/package"), applicationDirectory.resolve("output/package/some.class"), applicationDirectory.resolve("output/some.class")); @@ -177,7 +181,10 @@ public void test_extraFiles() throws IOException { ImmutableList expectedExtraFiles = ImmutableList.of( + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), extraFilesDirectory.resolve("c/cat"), extraFilesDirectory.resolve("foo")); @@ -194,132 +201,6 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException { assertNonDefaultAppRoot(configuration); } - @Test - public void testIsEmptyDirectory() throws IOException { - Assert.assertTrue( - MavenLayerConfigurations.isEmptyDirectory(temporaryFolder.getRoot().toPath())); - } - - @Test - public void testIsEmptyDirectory_file() throws IOException { - Assert.assertFalse( - MavenLayerConfigurations.isEmptyDirectory(temporaryFolder.newFile().toPath())); - } - - @Test - public void testIsEmptyDirectory_nonExistent() throws IOException { - Assert.assertFalse(MavenLayerConfigurations.isEmptyDirectory(Paths.get("non/existent"))); - } - - @Test - public void testAddFilesToLayer_file() throws IOException { - temporaryFolder.newFile("file"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder).add(sourceRoot.resolve("file"), basePath.resolve("file")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - - @Test - public void testAddFilesToLayer_emptyDirectory() throws IOException { - temporaryFolder.newFolder("leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - - MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder).add(sourceRoot.resolve("leaf"), basePath.resolve("leaf")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - - @Test - public void testAddFilesToLayer_nonEmptyDirectoryIgnored() throws IOException { - temporaryFolder.newFolder("non-empty", "leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> true, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - - @Test - public void testAddFilesToLayer_filter() throws IOException { - temporaryFolder.newFile("non-target"); - temporaryFolder.newFolder("sub"); - temporaryFolder.newFile("sub/target"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - - Predicate nameIsTarget = path -> "target".equals(path.getFileName().toString()); - MavenLayerConfigurations.addFilesToLayer(sourceRoot, nameIsTarget, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - - @Test - public void testAddFilesToLayer_emptyDirectoryForced() throws IOException { - temporaryFolder.newFolder("sub", "leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - MavenLayerConfigurations.addFilesToLayer(sourceRoot, path -> false, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - - @Test - public void testAddFilesToLayer_fileAsSource() throws IOException { - Path sourceFile = temporaryFolder.newFile("foo").toPath(); - - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - try { - MavenLayerConfigurations.addFilesToLayer( - sourceFile, path -> true, basePath, fileToLayerAdder); - } catch (NotDirectoryException ex) { - Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("foo is not a directory")); - } - } - - @Test - public void testAddFilesToLayer_complex() throws IOException { - temporaryFolder.newFile("A.class"); - temporaryFolder.newFile("B.java"); - temporaryFolder.newFolder("example", "dir"); - temporaryFolder.newFile("example/dir/C.class"); - temporaryFolder.newFile("example/C.class"); - temporaryFolder.newFolder("test", "resources", "leaf"); - temporaryFolder.newFile("test/resources/D.java"); - temporaryFolder.newFile("test/D.class"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/base"); - - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - - MavenLayerConfigurations.addFilesToLayer(sourceRoot, isClassFile, basePath, fileToLayerAdder); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("A.class"), basePath.resolve("A.class")); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("example/dir/C.class"), basePath.resolve("example/dir/C.class")); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("example/C.class"), basePath.resolve("example/C.class")); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("test/resources/leaf"), basePath.resolve("test/resources/leaf")); - Mockito.verify(fileToLayerAdder) - .add(sourceRoot.resolve("test/D.class"), basePath.resolve("test/D.class")); - Mockito.verifyNoMoreInteractions(fileToLayerAdder); - } - private Artifact makeArtifact(Path path) { Artifact artifact = Mockito.mock(Artifact.class); Mockito.when(artifact.getFile()).thenReturn(path.toFile()); @@ -344,19 +225,28 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); ImmutableList expectedResourcesFiles = ImmutableList.of( + outputPath.resolve("final-name/META-INF"), outputPath.resolve("final-name/META-INF/context.xml"), outputPath.resolve("final-name/Test.jsp"), + outputPath.resolve("final-name/WEB-INF"), + outputPath.resolve("final-name/WEB-INF/classes"), outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), + outputPath.resolve("final-name/WEB-INF/classes/package"), outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), + outputPath.resolve("final-name/WEB-INF/lib"), outputPath.resolve("final-name/WEB-INF/web.xml")); ImmutableList expectedClassesFiles = ImmutableList.of( outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), - outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), // Not sure about that + outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), + outputPath.resolve("final-name/WEB-INF/classes/package"), outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")); ImmutableList expectedExtraFiles = ImmutableList.of( + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), extraFilesDirectory.resolve("c/cat"), extraFilesDirectory.resolve("foo")); @@ -376,20 +266,27 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( + "/my/app/META-INF", "/my/app/META-INF/context.xml", "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", "/my/app/WEB-INF/web.xml"), configuration.getResourceLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/WEB-INF/classes/HelloWorld.class", - "/my/app/WEB-INF/classes/empty_dir", // Not sure about that + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/Other.class"), configuration.getClassLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/a/b/bar", "/c/cat", "/foo"), configuration.getExtraFilesLayerEntries()); + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); } @Test diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java new file mode 100644 index 0000000000..ab9238c9c0 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Predicate; + +/** Helper for constructing {@link JavaLayerConfigurations}. */ +public class JavaLayerConfigurationsHelper { + + public static JavaLayerConfigurations fromExplodedWar( + Path explodedWar, AbsoluteUnixPath appRoot, Path extraFilesDirectory) throws IOException { + Path webInfLib = explodedWar.resolve("WEB-INF/lib"); + Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); + + Predicate nameHasSnapshot = path -> path.getFileName().toString().contains("SNAPSHOT"); + Predicate isSnapshotDependency = + path -> path.startsWith(webInfLib) && nameHasSnapshot.test(path); + Predicate isNonSnapshotDependency = + path -> path.startsWith(webInfLib) && !nameHasSnapshot.test(path); + Predicate isClassFile = + path -> path.startsWith(webInfClasses) && path.getFileName().toString().endsWith(".class"); + Predicate isResource = + (isSnapshotDependency.or(isNonSnapshotDependency).or(isClassFile)).negate(); + + Builder layerBuilder = JavaLayerConfigurations.builder(); + + // Gets all the dependencies. + if (Files.exists(webInfLib)) { + AbsoluteUnixPath basePathInContainer = appRoot.resolve("WEB-INF/lib"); + layerBuilder.addDirectoryContents( + LayerType.DEPENDENCIES, webInfLib, isNonSnapshotDependency, basePathInContainer); + layerBuilder.addDirectoryContents( + LayerType.SNAPSHOT_DEPENDENCIES, webInfLib, isSnapshotDependency, basePathInContainer); + } + + // Gets the classes files in the 'WEB-INF/classes' output directory. + if (Files.exists(webInfClasses)) { + layerBuilder.addDirectoryContents( + LayerType.CLASSES, webInfClasses, isClassFile, appRoot.resolve("WEB-INF/classes")); + } + + // Gets the resources. + layerBuilder.addDirectoryContents(LayerType.RESOURCES, explodedWar, isResource, appRoot); + + // Adds all the extra files. + if (Files.exists(extraFilesDirectory)) { + layerBuilder.addDirectoryContents( + LayerType.EXTRA_FILES, extraFilesDirectory, path -> true, AbsoluteUnixPath.get("/")); + } + + return layerBuilder.build(); + } + + private JavaLayerConfigurationsHelper() {} +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java new file mode 100644 index 0000000000..46b16bee57 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link JavaLayerConfigurationsHelper}. */ +public class JavaLayerConfigurationsHelperTest { + + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); + } + + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); + } + + @Test + public void testFromExplodedWar() throws URISyntaxException, IOException { + Path explodedWar = Paths.get(Resources.getResource("exploded-war").toURI()); + Files.createDirectories(explodedWar.resolve("WEB-INF/classes/empty_dir")); + Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + + JavaLayerConfigurations configuration = + JavaLayerConfigurationsHelper.fromExplodedWar( + explodedWar, AbsoluteUnixPath.get("/my/app"), extraFilesDirectory); + + assertSourcePathsUnordered( + Arrays.asList(explodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), + configuration.getDependencyLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList(explodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + configuration.getSnapshotDependencyLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList( + explodedWar.resolve("META-INF"), + explodedWar.resolve("META-INF/context.xml"), + explodedWar.resolve("Test.jsp"), + explodedWar.resolve("WEB-INF"), + explodedWar.resolve("WEB-INF/classes"), + explodedWar.resolve("WEB-INF/classes/empty_dir"), + explodedWar.resolve("WEB-INF/classes/package"), + explodedWar.resolve("WEB-INF/classes/package/test.properties"), + explodedWar.resolve("WEB-INF/lib"), + explodedWar.resolve("WEB-INF/web.xml")), + configuration.getResourceLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList( + explodedWar.resolve("WEB-INF/classes/HelloWorld.class"), + explodedWar.resolve("WEB-INF/classes/empty_dir"), + explodedWar.resolve("WEB-INF/classes/package"), + explodedWar.resolve("WEB-INF/classes/package/Other.class")), + configuration.getClassLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList( + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")), + configuration.getExtraFilesLayerEntries()); + + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + configuration.getDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + configuration.getSnapshotDependencyLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/META-INF", + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", + "/my/app/WEB-INF/web.xml"), + configuration.getResourceLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/Other.class"), + configuration.getClassLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), + configuration.getExtraFilesLayerEntries()); + } +} diff --git a/jib-core/src/test/resources/exploded-war/file b/jib-plugins-common/src/test/resources/exploded-war/META-INF/context.xml similarity index 100% rename from jib-core/src/test/resources/exploded-war/file rename to jib-plugins-common/src/test/resources/exploded-war/META-INF/context.xml diff --git a/jib-plugins-common/src/test/resources/exploded-war/Test.jsp b/jib-plugins-common/src/test/resources/exploded-war/Test.jsp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class @@ -0,0 +1 @@ + diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class new file mode 100644 index 0000000000..8d1c8b69c3 --- /dev/null +++ b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class @@ -0,0 +1 @@ + diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/test.properties b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/test.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependency-1.0.0.jar b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/web.xml b/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/web.xml new file mode 100644 index 0000000000..e69de29bb2 From 5b31da0c92d4f999895b53c2e0959295eafd6dfd Mon Sep 17 00:00:00 2001 From: Cyrille HEIT Date: Mon, 15 Oct 2018 21:05:30 +0200 Subject: [PATCH 0297/2020] Inherit ENTRYPOINT and CMD from base image for WAR projects (#1071) * Support WARs : inherit entrypoint from base images (Jetty / Tomcat / ? / ... ) * Improves UX * Inherits entrypoint when entrypoint is null * Some refactor * Refactor * Fixes rebasing + changes program arguments * Fixes review comments * Fixes rebase problems Removes "CMD []" when entrypoint is present in Dockerfile Changes a TODO description --- .../tools/jib/api/JibContainerBuilder.java | 12 ++-- .../cloud/tools/jib/builder/BuildSteps.java | 16 +++-- .../jib/builder/steps/BuildImageStep.java | 62 +++++++++++++++- .../frontend/JavaDockerContextGenerator.java | 20 +++--- .../frontend/JavaEntrypointConstructor.java | 12 ---- .../jib/builder/steps/BuildImageStepTest.java | 70 +++++++++++++++++++ .../JavaEntrypointConstructorTest.java | 6 -- .../tools/jib/gradle/ContainerParameters.java | 3 +- .../gradle/PluginConfigurationProcessor.java | 7 +- .../jib/gradle/DockerContextTaskTest.java | 39 +++++++++-- .../tools/jib/gradle/JibExtensionTest.java | 2 +- .../PluginConfigurationProcessorTest.java | 7 +- .../jib/maven/JibPluginConfiguration.java | 3 +- .../maven/PluginConfigurationProcessor.java | 7 +- .../jib/maven/DockerContextMojoTest.java | 51 +++++++++++--- .../PluginConfigurationProcessorTest.java | 10 ++- 16 files changed, 255 insertions(+), 72 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 97bf75a29a..2205491657 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -162,8 +162,10 @@ public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { * @param entrypoint a list of the entrypoint command * @return this */ - public JibContainerBuilder setEntrypoint(List entrypoint) { - this.entrypoint = ImmutableList.copyOf(entrypoint); + public JibContainerBuilder setEntrypoint(@Nullable List entrypoint) { + if (entrypoint != null) { + this.entrypoint = ImmutableList.copyOf(entrypoint); + } return this; } @@ -194,8 +196,10 @@ public JibContainerBuilder setEntrypoint(String... entrypoint) { * @param programArguments a list of program argument tokens * @return this */ - public JibContainerBuilder setProgramArguments(List programArguments) { - this.programArguments = ImmutableList.copyOf(programArguments); + public JibContainerBuilder setProgramArguments(@Nullable List programArguments) { + if (programArguments != null) { + this.programArguments = ImmutableList.copyOf(programArguments); + } return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index da90882128..25a3ebacf3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -158,12 +158,16 @@ public DescriptorDigest run() throws InterruptedException, ExecutionException { if (buildConfiguration.getContainerConfiguration() != null) { buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.lifecycle( - "Container entrypoint set to " - + buildConfiguration.getContainerConfiguration().getEntrypoint())); + // TODO refactor code to also log ENTRYPOINT and CMD when inheriting them in this code, + // instead of logging them elsewhere. + if (buildConfiguration.getContainerConfiguration().getEntrypoint() != null) { + buildConfiguration + .getEventDispatcher() + .dispatch( + LogEvent.lifecycle( + "Container entrypoint set to " + + buildConfiguration.getContainerConfiguration().getEntrypoint())); + } } return imageDigest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 25c788541d..4314e0c554 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -40,6 +41,7 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; /** Builds a model {@link Image}. */ class BuildImageStep @@ -176,8 +178,9 @@ private Image afterCacheEntrySteps() imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); imageBuilder.setCreated(containerConfiguration.getCreationTime()); imageBuilder.setUser(containerConfiguration.getUser()); - imageBuilder.setEntrypoint(containerConfiguration.getEntrypoint()); - imageBuilder.setProgramArguments(containerConfiguration.getProgramArguments()); + imageBuilder.setEntrypoint(computeEntrypoint(baseImage, containerConfiguration)); + imageBuilder.setProgramArguments( + computeProgramArguments(baseImage, containerConfiguration)); imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); imageBuilder.addLabels(containerConfiguration.getLabels()); } @@ -186,4 +189,59 @@ private Image afterCacheEntrySteps() return imageBuilder.build(); } } + + /** + * Computes the image entrypoint. If {@link ContainerConfiguration#getEntrypoint()} is null, the + * entrypoint is inherited from the base image. Otherwise {@link + * ContainerConfiguration#getEntrypoint()} is returned. + * + * @param baseImage the base image + * @param containerConfiguration the container configuration + * @return the container entrypoint + */ + @Nullable + private ImmutableList computeEntrypoint( + Image baseImage, ContainerConfiguration containerConfiguration) { + if (baseImage.getEntrypoint() == null || containerConfiguration.getEntrypoint() != null) { + return containerConfiguration.getEntrypoint(); + } + + buildConfiguration + .getEventDispatcher() + .dispatch( + LogEvent.lifecycle( + "Container entrypoint set to " + + baseImage.getEntrypoint() + + " (inherited from base image)")); + return baseImage.getEntrypoint(); + } + + /** + * Computes the image program arguments. If {@link ContainerConfiguration#getEntrypoint()} and + * {@link ContainerConfiguration#getProgramArguments()} are null, the program arguments are + * inherited from the base image. Otherwise {@link ContainerConfiguration#getProgramArguments()} + * is returned. + * + * @param baseImage the base image + * @param containerConfiguration the container configuration + * @return the container program arguments + */ + @Nullable + private ImmutableList computeProgramArguments( + Image baseImage, ContainerConfiguration containerConfiguration) { + if (baseImage.getProgramArguments() == null + || containerConfiguration.getEntrypoint() != null + || containerConfiguration.getProgramArguments() != null) { + return containerConfiguration.getProgramArguments(); + } + + buildConfiguration + .getEventDispatcher() + .dispatch( + LogEvent.lifecycle( + "Container program arguments set to " + + baseImage.getProgramArguments() + + " (inherited from base image)")); + return baseImage.getProgramArguments(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 162034ffcf..200a153e8b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -89,7 +89,6 @@ private CopyDirective( * @param listBuilder the {@link ImmutableList.Builder} to add to * @param layerEntries the layer entries * @param directoryInContext the directory in the context to put the source files for the layer - * @param extractionPath the extraction path to extract the directory to */ private static void addIfNotEmpty( ImmutableList.Builder listBuilder, @@ -132,9 +131,9 @@ private static String mapToDockerfileString(Map map, String comm private final ImmutableList copyDirectives; @Nullable private String baseImage; - private List entrypoint = Collections.emptyList(); + @Nullable private List entrypoint = Collections.emptyList(); + @Nullable private List programArguments = Collections.emptyList(); @Nullable private String user; - private List programArguments = Collections.emptyList(); private Map environment = Collections.emptyMap(); private List exposedPorts = Collections.emptyList(); private Map labels = Collections.emptyMap(); @@ -187,7 +186,7 @@ public JavaDockerContextGenerator setBaseImage(String baseImage) { * @param entrypoint the entrypoint * @return this */ - public JavaDockerContextGenerator setEntrypoint(List entrypoint) { + public JavaDockerContextGenerator setEntrypoint(@Nullable List entrypoint) { this.entrypoint = entrypoint; return this; } @@ -209,7 +208,7 @@ public JavaDockerContextGenerator setUser(@Nullable String user) { * @param programArguments the list of arguments to append to {@code ENTRYPOINT} * @return this */ - public JavaDockerContextGenerator setProgramArguments(List programArguments) { + public JavaDockerContextGenerator setProgramArguments(@Nullable List programArguments) { this.programArguments = programArguments; return this; } @@ -339,11 +338,12 @@ String makeDockerfile() throws JsonProcessingException { dockerfile.append(mapToDockerfileString(environment, "ENV")); dockerfile.append(mapToDockerfileString(labels, "LABEL")); - dockerfile - .append("\nENTRYPOINT ") - .append(objectMapper.writeValueAsString(entrypoint)) - .append("\nCMD ") - .append(objectMapper.writeValueAsString(programArguments)); + if (entrypoint != null) { + dockerfile.append("\nENTRYPOINT ").append(objectMapper.writeValueAsString(entrypoint)); + } + if (programArguments != null) { + dockerfile.append("\nCMD ").append(objectMapper.writeValueAsString(programArguments)); + } if (user != null) { dockerfile.append("\nUSER ").append(user); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 6b0dea0b70..4605085845 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -43,18 +43,6 @@ public static List makeDefaultEntrypoint( mainClass); } - /** - * Constructs the container entrypoint for the gcr.io/distroless/jetty base image. - * - * @return ["java", "-jar", "/jetty/start.jar"] - * @see - * https://github.com/GoogleContainerTools/distroless/blob/master/java/jetty/BUILD - */ - // TODO: inherit CMD and ENTRYPOINT from the base image and remove this. - public static List makeDistrolessJettyEntrypoint() { - return Arrays.asList("java", "-jar", "/jetty/start.jar"); - } - /** * Constructs the container entrypoint. * diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index a7b50a69d8..ef2a45f525 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -116,6 +116,8 @@ public Blob getLayerBlob() { .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE")) .addLabel("base.label", "base.label.value") .setWorkingDirectory("/base/working/directory") + .setEntrypoint(ImmutableList.of("baseImageEntrypoint")) + .setProgramArguments(ImmutableList.of("catalina.sh", "run")) .addHistory(nonEmptyLayerHistory) .addHistory(emptyLayerHistory) .addHistory(emptyLayerHistory) @@ -184,6 +186,74 @@ public void test_propagateBaseImageConfiguration() Assert.assertEquals(image.getHistory().get(0), nonEmptyLayerHistory); Assert.assertEquals(image.getHistory().get(1), emptyLayerHistory); Assert.assertEquals(image.getHistory().get(2), emptyLayerHistory); + Assert.assertEquals(ImmutableList.of(), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of(), image.getProgramArguments()); + } + + @Test + public void test_inheritedEntrypoint() throws ExecutionException, InterruptedException { + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); + Mockito.when(mockContainerConfiguration.getProgramArguments()) + .thenReturn(ImmutableList.of("test")); + + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + + Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of("test"), image.getProgramArguments()); + } + + @Test + public void test_inheritedEntrypointAndProgramArguments() + throws ExecutionException, InterruptedException { + Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); + + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + + Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); + Assert.assertEquals(ImmutableList.of("catalina.sh", "run"), image.getProgramArguments()); + } + + @Test + public void test_notInheritedProgramArguments() throws ExecutionException, InterruptedException { + Mockito.when(mockContainerConfiguration.getEntrypoint()) + .thenReturn(ImmutableList.of("myEntrypoint")); + Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); + + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep, + mockBuildAndCacheApplicationLayerStep)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + + Assert.assertEquals(ImmutableList.of("myEntrypoint"), image.getEntrypoint()); + Assert.assertNull(image.getProgramArguments()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index e97c0f9558..ea0fcd9f5d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -73,10 +73,4 @@ public void testMakeDefaultEntrypoint_classpathStringWithNonDefaultAppRoot() { AbsoluteUnixPath.get("/my/app"), Collections.emptyList(), "Main"); Assert.assertEquals("/my/app/resources:/my/app/classes:/my/app/libs/*", entrypoint.get(2)); } - - @Test - public void testMakeDistrolessJettyEntrypoint() { - List expected = Arrays.asList("java", "-jar", "/jetty/start.jar"); - Assert.assertEquals(expected, JavaEntrypointConstructor.makeDistrolessJettyEntrypoint()); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index ec8592d243..e39fd5f2d9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -38,7 +38,7 @@ public class ContainerParameters { private Map environment = Collections.emptyMap(); private List entrypoint = Collections.emptyList(); @Nullable private String mainClass; - private List args = Collections.emptyList(); + @Nullable private List args; private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); private Map labels = Collections.emptyMap(); @@ -115,6 +115,7 @@ public void setMainClass(String mainClass) { } @Input + @Nullable @Optional public List getArgs() { if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index b89c52134b..ba7f314d10 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -42,6 +42,7 @@ import java.util.List; import java.util.Optional; import java.util.logging.Level; +import javax.annotation.Nullable; import org.gradle.api.GradleException; import org.gradle.api.logging.Logger; import org.gradle.internal.logging.events.LogEvent; @@ -139,6 +140,7 @@ static PluginConfigurationProcessor processCommonConfiguration( .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) .setEntrypoint(entrypoint) .setEnvironment(jibExtension.getContainer().getEnvironment()) + .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) .setProgramArguments(jibExtension.getContainer().getArgs()) .setLabels(jibExtension.getContainer().getLabels()) @@ -174,7 +176,7 @@ static void configureContainerizer( * *

    *
  1. the user specified one, if set - *
  2. for a WAR project, the Jetty default one + *
  3. for a WAR project, null (it must be inherited from base image) *
  4. for a non-WAR project, by resolving the main class *
* @@ -183,6 +185,7 @@ static void configureContainerizer( * @param projectProperties used for providing additional information * @return the entrypoint */ + @Nullable static List computeEntrypoint( Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) { ContainerParameters parameters = jibExtension.getContainer(); @@ -194,7 +197,7 @@ static List computeEntrypoint( } if (projectProperties.isWarProject()) { - return JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); + return null; } String mainClass = projectProperties.getMainClass(jibExtension); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 279dba11b1..e2741bf79a 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -60,6 +60,7 @@ public void setUp() throws IOException { Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); Mockito.when(containerParameters.getAppRoot()).thenReturn("/app"); + Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); project.getPluginManager().apply("java"); @@ -85,16 +86,34 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException { Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", getEntrypoint()); + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test - public void testEntrypoint_defaultWebAppRoot() throws IOException { + public void testEntrypoint_inheritedEntrypoint() throws IOException { Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); + Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); project.getPluginManager().apply("war"); task.generateDockerContext(); - Assert.assertEquals("ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]", getEntrypoint()); + try { + getEntrypoint(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test @@ -157,15 +176,21 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } } + private String getUser() throws IOException { + return getDockerfileLine("USER"); + } + private String getEntrypoint() throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); + return getDockerfileLine("ENTRYPOINT"); } - private String getUser() throws IOException { + private String getCmd() throws IOException { + return getDockerfileLine("CMD"); + } + + private String getDockerfileLine(String command) throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("USER")).findFirst().get(); + return lines.stream().filter(line -> line.startsWith(command)).findFirst().get(); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 9e7a0c75c1..313dc56cb6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -109,7 +109,7 @@ public void testContainer() { Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getJvmFlags()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getEnvironment()); Assert.assertNull(testJibExtension.getContainer().getMainClass()); - Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getArgs()); + Assert.assertNull(testJibExtension.getContainer().getArgs()); Assert.assertSame(ImageFormat.Docker, testJibExtension.getContainer().getFormat()); Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java index 5c28ec8b1f..85e910a23d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -203,7 +202,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() } @Test - public void testWebAppEntrypoint_default() + public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { Mockito.when(mockProjectProperties.isWarProject()).thenReturn(true); @@ -214,9 +213,7 @@ public void testWebAppEntrypoint_default() getBuildConfiguration(processor.getJibContainerBuilder()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - ImmutableList.of("java", "-jar", "/jetty/start.jar"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index e33b693013..83cc73356b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -128,7 +128,7 @@ public static class ContainerParameters { @Nullable @Parameter private String mainClass; - @Parameter private List args = Collections.emptyList(); + @Nullable @Parameter private List args; @Nullable @Parameter(required = true) @@ -357,6 +357,7 @@ String getUser() { * * @return the configured main arguments */ + @Nullable List getArgs() { if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { return ConfigurationPropertyValidator.parseListProperty( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 2bffaf18cf..f9fa7fb131 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -41,6 +41,7 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; @@ -263,7 +264,7 @@ static ImageReference parseImageReference(String image, String type) { * *
    *
  1. the user specified one, if set - *
  2. for a WAR project, the Jetty default one + *
  3. for a WAR project, null (it must be inherited from base image) *
  4. for a non-WAR project, by resolving the main class *
* @@ -275,6 +276,7 @@ static ImageReference parseImageReference(String image, String type) { * @throws MojoExecutionException if resolving the main class fails or the app root parameter is * not an absolute path in Unix-style */ + @Nullable static List computeEntrypoint( Log logger, JibPluginConfiguration jibPluginConfiguration, @@ -289,7 +291,7 @@ static List computeEntrypoint( } if (isWarPackaging(jibPluginConfiguration)) { - return JavaEntrypointConstructor.makeDistrolessJettyEntrypoint(); + return null; } String mainClass = projectProperties.getMainClass(jibPluginConfiguration); @@ -299,7 +301,6 @@ static List computeEntrypoint( private final JibContainerBuilder jibContainerBuilder; private final ImageReference baseImageReference; - private final MavenSettingsServerCredentials mavenSettingsServerCredentials; private final boolean isBaseImageCredentialPresent; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 5c323a72f1..ba25f8f776 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -86,6 +86,12 @@ public void testEntrypoint() throws MojoExecutionException, IOException { Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]", getEntrypoint()); + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test @@ -96,6 +102,12 @@ public void testEntrypoint_nonDefaultAppRoot() throws MojoExecutionException, IO Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", getEntrypoint()); + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test @@ -206,7 +218,18 @@ public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException, Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); mojo.execute(); - Assert.assertEquals("ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]", getEntrypoint()); + try { + getEntrypoint(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test @@ -245,8 +268,13 @@ String getAppRoot() { }; mojo.targetDir = outputFolder.toString(); mojo.execute(); - Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getEntrypoint()); + try { + getCmd(); + Assert.fail(); + } catch (NoSuchElementException ex) { + // pass + } } @Test @@ -313,24 +341,29 @@ String getBaseImage() { getUser(); Assert.fail(); } catch (NoSuchElementException ex) { + // pass } } private String getUser() throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("USER")).findFirst().get(); + return getDockerfileLine("USER"); } private String getEntrypoint() throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("ENTRYPOINT")).findFirst().get(); + return getDockerfileLine("ENTRYPOINT"); } private String getBaseImage() throws IOException { + return getDockerfileLine("FROM"); + } + + private String getCmd() throws IOException { + return getDockerfileLine("CMD"); + } + + private String getDockerfileLine(String command) throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith("FROM")).findFirst().get(); + return lines.stream().filter(line -> line.startsWith(command)).findFirst().get(); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index 40396b74c1..d2f8e579ec 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -131,9 +131,13 @@ public void testEntrypoint() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); Assert.assertEquals( Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); @@ -151,12 +155,12 @@ public void testEntrypoint_defaultWarPackaging() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( mockLog, mockJibPluginConfiguration, mockProjectProperties); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("java", "-jar", "/jetty/start.jar"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(mockLog); } From 1a9d33a0928038eb94d12f9830991173832dfb18 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Oct 2018 16:12:36 -0400 Subject: [PATCH 0298/2020] Fix integration tests after #1071 (inheriting ENTRYPOINT and CMD) (#1141) --- .../jib/gradle/WarProjectIntegrationTest.java | 10 +- .../war_servlet25/build-tomcat.gradle | 5 +- .../jib/gradle/DockerContextTaskTest.java | 51 +---- .../DockerContextMojoIntegrationTest.java | 10 +- .../jib/maven/DockerContextMojoTest.java | 202 ++++-------------- .../projects/war_servlet25/pom-tomcat.xml | 4 - 6 files changed, 54 insertions(+), 228 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 7665ba0993..6f6ffbc60c 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -79,10 +79,7 @@ public void testDockerContext_jettyServlet25() throws IOException, InterruptedEx + "\n" + "COPY libs /\n" + "COPY resources /\n" - + "COPY classes /\n" - + "\n" - + "ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]\n" - + "CMD []"; + + "COPY classes /"; verifyDockerContextBuildAndRun(expectedDockerContext, "build.gradle"); } @@ -93,10 +90,7 @@ public void testDockerContext_tomcatServlet25() throws IOException, InterruptedE + "\n" + "COPY libs /\n" + "COPY resources /\n" - + "COPY classes /\n" - + "\n" - + "ENTRYPOINT [\"catalina.sh\",\"run\"]\n" - + "CMD []"; + + "COPY classes /"; verifyDockerContextBuildAndRun(expectedDockerContext, "build-tomcat.gradle"); } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle index a71aa65fc3..ad6dcb00cf 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle @@ -32,10 +32,7 @@ jib { image = System.getProperty("_TARGET_IMAGE") credHelper = 'gcr' } - container { - appRoot = '/usr/local/tomcat/webapps/ROOT' - entrypoint = ['catalina.sh', 'run'] - } + container.appRoot = '/usr/local/tomcat/webapps/ROOT' // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index e2741bf79a..1c2d1e82bf 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -20,7 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.NoSuchElementException; +import javax.annotation.Nullable; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; @@ -75,7 +75,7 @@ public void testEntrypoint() throws IOException { Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]", - getEntrypoint()); + getDockerfileLine("ENTRYPOINT")); } @Test @@ -85,13 +85,8 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException { Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", - getEntrypoint()); - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test @@ -102,18 +97,8 @@ public void testEntrypoint_inheritedEntrypoint() throws IOException { task.generateDockerContext(); - try { - getEntrypoint(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + Assert.assertNull(getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test @@ -121,19 +106,14 @@ public void testUser() throws IOException { Mockito.when(containerParameters.getUser()).thenReturn("tomcat"); task.generateDockerContext(); - Assert.assertEquals("USER tomcat", getUser()); + Assert.assertEquals("USER tomcat", getDockerfileLine("USER")); } @Test public void testUser_null() throws IOException { Mockito.when(containerParameters.getUser()).thenReturn(null); task.generateDockerContext(); - try { - getUser(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + Assert.assertNull(getDockerfileLine("USER")); } @Test @@ -176,21 +156,10 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } } - private String getUser() throws IOException { - return getDockerfileLine("USER"); - } - - private String getEntrypoint() throws IOException { - return getDockerfileLine("ENTRYPOINT"); - } - - private String getCmd() throws IOException { - return getDockerfileLine("CMD"); - } - + @Nullable private String getDockerfileLine(String command) throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith(command)).findFirst().get(); + return lines.stream().filter(line -> line.startsWith(command)).findFirst().orElse(null); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 90dfac576d..24c686a346 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -106,10 +106,7 @@ public void testExecute_jettyServlet25() + "\n" + "COPY libs /\n" + "COPY resources /\n" - + "COPY classes /\n" - + "\n" - + "ENTRYPOINT [\"java\",\"-jar\",\"/jetty/start.jar\"]\n" - + "CMD []"; + + "COPY classes /"; verifyWarBuildAndRun(expectedDockerfile, "pom.xml"); } @@ -121,10 +118,7 @@ public void testExecute_tomcatServlet25() + "\n" + "COPY libs /\n" + "COPY resources /\n" - + "COPY classes /\n" - + "\n" - + "ENTRYPOINT [\"catalina.sh\",\"run\"]\n" - + "CMD []"; + + "COPY classes /"; verifyWarBuildAndRun(expectedDockerfile, "pom-tomcat.xml"); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index ba25f8f776..61f3c83f9b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -22,7 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.NoSuchElementException; +import javax.annotation.Nullable; import org.apache.maven.model.Build; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; @@ -54,28 +54,7 @@ public void setUp() throws IOException { Mockito.when(project.getBuild()).thenReturn(build); Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); - mojo = - new DockerContextMojo() { - @Override - MavenProject getProject() { - return project; - } - - @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); - } - - @Override - String getMainClass() { - return "MainClass"; - } - - @Override - String getAppRoot() { - return appRoot; - } - }; + mojo = new BaseDockerContextMojo(); mojo.targetDir = outputFolder.toString(); } @@ -85,13 +64,8 @@ public void testEntrypoint() throws MojoExecutionException, IOException { Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]", - getEntrypoint()); - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test @@ -101,13 +75,8 @@ public void testEntrypoint_nonDefaultAppRoot() throws MojoExecutionException, IO Assert.assertEquals( "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", - getEntrypoint()); - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test @@ -153,7 +122,7 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { public void testBaseImage_nonWarPackaging() throws MojoExecutionException, IOException { mojo.execute(); - Assert.assertEquals("FROM gcr.io/distroless/java", getBaseImage()); + Assert.assertEquals("FROM gcr.io/distroless/java", getDockerfileLine("FROM")); } @Test @@ -165,7 +134,7 @@ public void testBaseImage_warPackaging() throws MojoExecutionException, IOExcept Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); mojo.execute(); - Assert.assertEquals("FROM gcr.io/distroless/java/jetty", getBaseImage()); + Assert.assertEquals("FROM gcr.io/distroless/java/jetty", getDockerfileLine("FROM")); } @Test @@ -173,31 +142,11 @@ public void testBaseImage_nonDefault() throws MojoExecutionException, IOExceptio Mockito.doReturn("war").when(project).getPackaging(); Mockito.doReturn("final-name").when(build).getFinalName(); mojo = - new DockerContextMojo() { - @Override - MavenProject getProject() { - return project; - } - - @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); - } - - @Override - String getMainClass() { - return "MainClass"; - } - + new BaseDockerContextMojo() { @Override String getBaseImage() { return "tomcat:8.5-jre8-alpine"; } - - @Override - String getAppRoot() { - return appRoot; - } }; mojo.targetDir = outputFolder.toString(); @@ -206,7 +155,7 @@ String getAppRoot() { Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); mojo.execute(); - Assert.assertEquals("FROM tomcat:8.5-jre8-alpine", getBaseImage()); + Assert.assertEquals("FROM tomcat:8.5-jre8-alpine", getDockerfileLine("FROM")); } @Test @@ -218,18 +167,8 @@ public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException, Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); mojo.execute(); - try { - getEntrypoint(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + Assert.assertNull(getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test @@ -240,67 +179,22 @@ public void testEntrypoint_warPackaging() throws MojoExecutionException, IOExcep projectRoot.newFolder("final-name", "WEB-INF", "classes"); Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); mojo = - new DockerContextMojo() { - @Override - MavenProject getProject() { - return project; - } - - @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); - } - - @Override - String getMainClass() { - return "MainClass"; - } - + new BaseDockerContextMojo() { @Override List getEntrypoint() { return ImmutableList.of("catalina.sh", "run"); } - - @Override - String getAppRoot() { - return appRoot; - } }; mojo.targetDir = outputFolder.toString(); mojo.execute(); - Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getEntrypoint()); - try { - getCmd(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getDockerfileLine("ENTRYPOINT")); + Assert.assertNull(getDockerfileLine("CMD")); } @Test public void testUser() throws IOException, MojoExecutionException { mojo = - new DockerContextMojo() { - @Override - MavenProject getProject() { - return project; - } - - @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); - } - - @Override - String getMainClass() { - return "MainClass"; - } - - @Override - String getBaseImage() { - return "tomcat:8.5-jre8-alpine"; - } - + new BaseDockerContextMojo() { @Override String getUser() { return "tomcat"; @@ -308,62 +202,44 @@ String getUser() { }; mojo.targetDir = outputFolder.toString(); mojo.execute(); - Assert.assertEquals("USER tomcat", getUser()); + Assert.assertEquals("USER tomcat", getDockerfileLine("USER")); } @Test public void testUser_null() throws IOException, MojoExecutionException { - mojo = - new DockerContextMojo() { - @Override - MavenProject getProject() { - return project; - } - - @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); - } - - @Override - String getMainClass() { - return "MainClass"; - } - - @Override - String getBaseImage() { - return "tomcat:8.5-jre8-alpine"; - } - }; + mojo = new BaseDockerContextMojo(); mojo.targetDir = outputFolder.toString(); mojo.execute(); - try { - getUser(); - Assert.fail(); - } catch (NoSuchElementException ex) { - // pass - } + Assert.assertNull(getDockerfileLine("USER")); } - private String getUser() throws IOException { - return getDockerfileLine("USER"); - } + private class BaseDockerContextMojo extends DockerContextMojo { - private String getEntrypoint() throws IOException { - return getDockerfileLine("ENTRYPOINT"); - } + @Override + MavenProject getProject() { + return project; + } - private String getBaseImage() throws IOException { - return getDockerfileLine("FROM"); - } + @Override + Path getExtraDirectory() { + return projectRoot.getRoot().toPath(); + } - private String getCmd() throws IOException { - return getDockerfileLine("CMD"); + @Override + String getMainClass() { + return "MainClass"; + } + + @Override + String getAppRoot() { + return appRoot; + } } + @Nullable private String getDockerfileLine(String command) throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith(command)).findFirst().get(); + return lines.stream().filter(line -> line.startsWith(command)).findFirst().orElse(null); } } diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml index 7c2bbbbb3b..576fad6506 100644 --- a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml @@ -51,10 +51,6 @@
/usr/local/tomcat/webapps/ROOT - - catalina.sh - run - true From d1edaad3608fdc1bef454ac3a14e970781f5dd37 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Oct 2018 16:42:27 -0400 Subject: [PATCH 0299/2020] Make entrypoint parameter nullable (#1144) --- .../google/cloud/tools/jib/api/JibContainerBuilder.java | 9 +++------ .../tools/jib/frontend/JavaDockerContextGenerator.java | 4 ++-- jib-core/src/test/resources/webAppSampleDockerfile | 3 +-- .../cloud/tools/jib/gradle/ContainerParameters.java | 3 ++- .../tools/jib/gradle/PluginConfigurationProcessor.java | 5 ++--- .../cloud/tools/jib/maven/JibPluginConfiguration.java | 3 ++- .../tools/jib/maven/PluginConfigurationProcessor.java | 5 +++-- 7 files changed, 15 insertions(+), 17 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 2205491657..6a50019079 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -163,9 +163,7 @@ public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { * @return this */ public JibContainerBuilder setEntrypoint(@Nullable List entrypoint) { - if (entrypoint != null) { - this.entrypoint = ImmutableList.copyOf(entrypoint); - } + this.entrypoint = entrypoint == null ? null : ImmutableList.copyOf(entrypoint); return this; } @@ -197,9 +195,8 @@ public JibContainerBuilder setEntrypoint(String... entrypoint) { * @return this */ public JibContainerBuilder setProgramArguments(@Nullable List programArguments) { - if (programArguments != null) { - this.programArguments = ImmutableList.copyOf(programArguments); - } + this.programArguments = + programArguments == null ? null : ImmutableList.copyOf(programArguments); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index 200a153e8b..a4ee7a2a27 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -131,8 +131,8 @@ private static String mapToDockerfileString(Map map, String comm private final ImmutableList copyDirectives; @Nullable private String baseImage; - @Nullable private List entrypoint = Collections.emptyList(); - @Nullable private List programArguments = Collections.emptyList(); + @Nullable private List entrypoint; + @Nullable private List programArguments; @Nullable private String user; private Map environment = Collections.emptyMap(); private List exposedPorts = Collections.emptyList(); diff --git a/jib-core/src/test/resources/webAppSampleDockerfile b/jib-core/src/test/resources/webAppSampleDockerfile index f8771a9a5b..3667c9feec 100644 --- a/jib-core/src/test/resources/webAppSampleDockerfile +++ b/jib-core/src/test/resources/webAppSampleDockerfile @@ -6,5 +6,4 @@ COPY resources / COPY classes / COPY root / -ENTRYPOINT ["catalina.sh","run"] -CMD [] \ No newline at end of file +ENTRYPOINT ["catalina.sh","run"] \ No newline at end of file diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index e39fd5f2d9..3dd4968989 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -36,7 +36,7 @@ public class ContainerParameters { private boolean useCurrentTimestamp = false; private List jvmFlags = Collections.emptyList(); private Map environment = Collections.emptyMap(); - private List entrypoint = Collections.emptyList(); + @Nullable private List entrypoint; @Nullable private String mainClass; @Nullable private List args; private ImageFormat format = ImageFormat.Docker; @@ -59,6 +59,7 @@ public void setUseCurrentTimestamp(boolean useCurrentTimestamp) { } @Input + @Nullable @Optional public List getEntrypoint() { if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java index ba7f314d10..e4159374d5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java @@ -139,10 +139,9 @@ static PluginConfigurationProcessor processCommonConfiguration( Jib.from(baseImage) .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) .setEntrypoint(entrypoint) - .setEnvironment(jibExtension.getContainer().getEnvironment()) .setProgramArguments(jibExtension.getContainer().getArgs()) + .setEnvironment(jibExtension.getContainer().getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) - .setProgramArguments(jibExtension.getContainer().getArgs()) .setLabels(jibExtension.getContainer().getLabels()) .setUser(jibExtension.getContainer().getUser()); if (jibExtension.getContainer().getUseCurrentTimestamp()) { @@ -189,7 +188,7 @@ static void configureContainerizer( static List computeEntrypoint( Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) { ContainerParameters parameters = jibExtension.getContainer(); - if (!parameters.getEntrypoint().isEmpty()) { + if (parameters.getEntrypoint() != null && !parameters.getEntrypoint().isEmpty()) { if (parameters.getMainClass() != null || !parameters.getJvmFlags().isEmpty()) { logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 83cc73356b..bfe73996c3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -120,7 +120,7 @@ public static class ContainerParameters { @Parameter private boolean useCurrentTimestamp = false; - @Parameter private List entrypoint = Collections.emptyList(); + @Nullable @Parameter private List entrypoint; @Parameter private List jvmFlags = Collections.emptyList(); @@ -292,6 +292,7 @@ boolean getUseCurrentTimestamp() { * * @return the configured entrypoint */ + @Nullable List getEntrypoint() { if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { return ConfigurationPropertyValidator.parseListProperty( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index f9fa7fb131..b4b5695bf6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -282,12 +282,13 @@ static List computeEntrypoint( JibPluginConfiguration jibPluginConfiguration, MavenProjectProperties projectProperties) throws MojoExecutionException { - if (!jibPluginConfiguration.getEntrypoint().isEmpty()) { + List entrypointParameter = jibPluginConfiguration.getEntrypoint(); + if (entrypointParameter != null && !entrypointParameter.isEmpty()) { if (jibPluginConfiguration.getMainClass() != null || !jibPluginConfiguration.getJvmFlags().isEmpty()) { logger.warn(" and are ignored when is specified"); } - return jibPluginConfiguration.getEntrypoint(); + return entrypointParameter; } if (isWarPackaging(jibPluginConfiguration)) { From 7388a3ab5047ce6ee1662df46f6af874dc472889 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Oct 2018 17:38:52 -0400 Subject: [PATCH 0300/2020] Refactor code (#1145) --- .../cloud/tools/jib/builder/BuildSteps.java | 32 +++++++++---------- jib-gradle-plugin/CHANGELOG.md | 2 +- jib-maven-plugin/CHANGELOG.md | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 25a3ebacf3..f7ff49c4a0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -31,20 +31,18 @@ public class BuildSteps { private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; - /** Accepts {@link StepsRunner} by running the appropriate steps. */ + /** Runs appropriate steps to build an image. */ @FunctionalInterface - private interface StepsRunnerConsumer { + private interface ImageBuildRunnable { /** - * Runs a {@link StepsRunner}. + * Builds an image and returns its digest. * - * @param stepsRunner the {@link StepsRunner} to run * @return the digest of the built image * @throws ExecutionException if an exception occurs during execution * @throws InterruptedException if the execution is interrupted */ - DescriptorDigest accept(StepsRunner stepsRunner) - throws ExecutionException, InterruptedException; + DescriptorDigest build() throws ExecutionException, InterruptedException; } /** @@ -57,8 +55,8 @@ public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfig return new BuildSteps( DESCRIPTION_FOR_DOCKER_REGISTRY, buildConfiguration, - stepsRunner -> - stepsRunner + () -> + new StepsRunner(buildConfiguration) .runRetrieveTargetRegistryCredentialsStep() .runAuthenticatePushStep() .runPullBaseImageStep() @@ -85,8 +83,8 @@ public static BuildSteps forBuildToDockerDaemon( return new BuildSteps( DESCRIPTION_FOR_DOCKER_DAEMON, buildConfiguration, - stepsRunner -> - stepsRunner + () -> + new StepsRunner(buildConfiguration) .runPullBaseImageStep() .runPullAndCacheBaseImageLayersStep() .runBuildAndCacheApplicationLayerSteps() @@ -107,8 +105,8 @@ public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration build return new BuildSteps( DESCRIPTION_FOR_TARBALL, buildConfiguration, - stepsRunner -> - stepsRunner + () -> + new StepsRunner(buildConfiguration) .runPullBaseImageStep() .runPullAndCacheBaseImageLayersStep() .runBuildAndCacheApplicationLayerSteps() @@ -120,20 +118,20 @@ public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration build private final String description; private final BuildConfiguration buildConfiguration; - private final StepsRunnerConsumer stepsRunnerConsumer; + private final ImageBuildRunnable imageBuildRunnable; /** * @param description a description of what the steps do * @param buildConfiguration the configuration parameters for the build - * @param stepsRunnerConsumer accepts a {@link StepsRunner} by running the necessary steps + * @param imageBuildRunnable runs the necessary steps to build an image */ private BuildSteps( String description, BuildConfiguration buildConfiguration, - StepsRunnerConsumer stepsRunnerConsumer) { + ImageBuildRunnable imageBuildRunnable) { this.description = description; this.buildConfiguration = buildConfiguration; - this.stepsRunnerConsumer = stepsRunnerConsumer; + this.imageBuildRunnable = imageBuildRunnable; } public BuildConfiguration getBuildConfiguration() { @@ -153,7 +151,7 @@ public DescriptorDigest run() throws InterruptedException, ExecutionException { DescriptorDigest imageDigest; try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { - imageDigest = stepsRunnerConsumer.accept(new StepsRunner(buildConfiguration)); + imageDigest = imageBuildRunnable.build(); } if (buildConfiguration.getContainerConfiguration() != null) { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 8984a95fcb..f992385916 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. - `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) -- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 11845c4e19..1e91d943d9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. - `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) -- File timestamps in the build image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed From d219046f5d6dfa525785fba2f323e0b22ae41c7b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Oct 2018 11:20:50 -0400 Subject: [PATCH 0301/2020] Preliminary WAR support in CHANGELOG (#1148) --- jib-gradle-plugin/CHANGELOG.md | 5 +++-- jib-maven-plugin/CHANGELOG.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index f992385916..ee57e9963d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,14 +6,15 @@ All notable changes to this project will be documented in this file. ### Added - Properties for each configuration parameter, allowing any parameter to be set via commandline ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) +- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) ### Changed -- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) -- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 1e91d943d9..74f42cc59d 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,14 +6,15 @@ All notable changes to this project will be documented in this file. ### Added - Properties for each configuration parameter, allowing any parameter to be set via commandline ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) +- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) ### Changed -- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) -- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) ### Fixed From f32cdafa8b1540fad7764aadc439e1505d09ecd6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 16 Oct 2018 12:24:52 -0400 Subject: [PATCH 0302/2020] Changes from Gradle v0.9.13 patch (#1151) --- jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-gradle-plugin/gradle.properties | 2 +- .../google/cloud/tools/jib/gradle/DockerContextTask.java | 1 + .../cloud/tools/jib/gradle/DockerContextTaskTest.java | 9 +++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index ee57e9963d..e530069b3a 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -18,6 +18,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.9.13 + +### Fixed + +- Adds environment variable configuration to Docker context generator ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) + ## 0.9.12 ### Fixed diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 7210bfc265..92180d0637 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.12-SNAPSHOT +version = 0.9.14-SNAPSHOT diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 7d15184e21..2186c78b38 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -127,6 +127,7 @@ public void generateDockerContext() { .setEntrypoint(entrypoint) .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(jibExtension.getContainer().getPorts()) + .setEnvironment(jibExtension.getContainer().getEnvironment()) .setLabels(jibExtension.getContainer().getLabels()) .setUser(jibExtension.getContainer().getUser()) .generate(Paths.get(targetDir)); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index 1c2d1e82bf..edbd43ce22 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -54,6 +55,8 @@ public void setUp() throws IOException { JibExtension jibExtension = Mockito.mock(JibExtension.class); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); + Mockito.when(containerParameters.getEnvironment()) + .thenReturn(ImmutableMap.of("envKey", "envVal")); Mockito.when(jibExtension.getExtraDirectoryPath()) .thenReturn(projectRoot.newFolder("src", "main", "jib").toPath()); Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); @@ -156,6 +159,12 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } } + @Test + public void testGenerateDockerContext_env() throws IOException { + task.generateDockerContext(); + Assert.assertEquals("ENV envKey=\"envVal\"", getDockerfileLine("ENV")); + } + @Nullable private String getDockerfileLine(String command) throws IOException { Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); From 055440c2b030e5ad93da7c314a4a152d9d03e712 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Oct 2018 13:25:40 -0400 Subject: [PATCH 0303/2020] Log CMD and move code that logs ENTRYPOINT (#1146) * Refactor code * Log entrypoint and cmd * Do not log if values are null --- .../cloud/tools/jib/builder/BuildSteps.java | 22 +------- .../jib/builder/steps/BuildImageStep.java | 50 +++++++++++-------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index f7ff49c4a0..3fbad4abb8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; import java.util.concurrent.ExecutionException; @@ -146,28 +145,9 @@ public BuildConfiguration getBuildConfiguration() { * @throws ExecutionException if an exception occurs during execution */ public DescriptorDigest run() throws InterruptedException, ExecutionException { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); - - DescriptorDigest imageDigest; try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { - imageDigest = imageBuildRunnable.build(); - } - - if (buildConfiguration.getContainerConfiguration() != null) { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); - // TODO refactor code to also log ENTRYPOINT and CMD when inheriting them in this code, - // instead of logging them elsewhere. - if (buildConfiguration.getContainerConfiguration().getEntrypoint() != null) { - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.lifecycle( - "Container entrypoint set to " - + buildConfiguration.getContainerConfiguration().getEntrypoint())); - } + return imageBuildRunnable.build(); } - - return imageDigest; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 4314e0c554..24c2c5b4a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -202,18 +202,20 @@ private Image afterCacheEntrySteps() @Nullable private ImmutableList computeEntrypoint( Image baseImage, ContainerConfiguration containerConfiguration) { - if (baseImage.getEntrypoint() == null || containerConfiguration.getEntrypoint() != null) { - return containerConfiguration.getEntrypoint(); + boolean shouldInherit = + baseImage.getEntrypoint() != null && containerConfiguration.getEntrypoint() == null; + + ImmutableList entrypointToUse = + shouldInherit ? baseImage.getEntrypoint() : containerConfiguration.getEntrypoint(); + + if (entrypointToUse != null) { + String logSuffix = shouldInherit ? " (inherited from base image)" : ""; + String message = "Container entrypoint set to " + entrypointToUse + logSuffix; + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(message)); } - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.lifecycle( - "Container entrypoint set to " - + baseImage.getEntrypoint() - + " (inherited from base image)")); - return baseImage.getEntrypoint(); + return entrypointToUse; } /** @@ -229,19 +231,23 @@ private ImmutableList computeEntrypoint( @Nullable private ImmutableList computeProgramArguments( Image baseImage, ContainerConfiguration containerConfiguration) { - if (baseImage.getProgramArguments() == null - || containerConfiguration.getEntrypoint() != null - || containerConfiguration.getProgramArguments() != null) { - return containerConfiguration.getProgramArguments(); + boolean shouldInherit = + baseImage.getProgramArguments() != null + // Inherit CMD only when inheriting ENTRYPOINT. + && containerConfiguration.getEntrypoint() == null + && containerConfiguration.getProgramArguments() == null; + + ImmutableList programArgumentsToUse = + shouldInherit + ? baseImage.getProgramArguments() + : containerConfiguration.getProgramArguments(); + + if (programArgumentsToUse != null) { + String logSuffix = shouldInherit ? " (inherited from base image)" : ""; + String message = "Container program arguments set to " + programArgumentsToUse + logSuffix; + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(message)); } - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.lifecycle( - "Container program arguments set to " - + baseImage.getProgramArguments() - + " (inherited from base image)")); - return baseImage.getProgramArguments(); + return programArgumentsToUse; } } From 695e8159352f9a6e2c76fa5bfa443f0458e8fbd5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 16 Oct 2018 13:33:03 -0400 Subject: [PATCH 0304/2020] Maven release v0.9.13 (#1153) * Fix missing env in maven docker context generator * preparing release 0.9.13 * 0.9.14-SNAPSHOT * Make exposed ports and labels nullable in JavaDockerContextGenerator --- .../frontend/JavaDockerContextGenerator.java | 27 +++++++++++-------- jib-maven-plugin/CHANGELOG.md | 6 +++++ jib-maven-plugin/pom.xml | 2 +- .../tools/jib/maven/DockerContextMojo.java | 1 + .../jib/maven/DockerContextMojoTest.java | 12 +++++++++ 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java index a4ee7a2a27..61fcb2f125 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java @@ -29,7 +29,6 @@ import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -133,10 +132,10 @@ private static String mapToDockerfileString(Map map, String comm @Nullable private String baseImage; @Nullable private List entrypoint; @Nullable private List programArguments; + @Nullable private Map environment; @Nullable private String user; - private Map environment = Collections.emptyMap(); - private List exposedPorts = Collections.emptyList(); - private Map labels = Collections.emptyMap(); + @Nullable private List exposedPorts; + @Nullable private Map labels; /** * Constructs a Docker context generator for a Java application. @@ -219,7 +218,7 @@ public JavaDockerContextGenerator setProgramArguments(@Nullable List pro * @param environment map from the environment variable name to value * @return this */ - public JavaDockerContextGenerator setEnvironment(Map environment) { + public JavaDockerContextGenerator setEnvironment(@Nullable Map environment) { this.environment = environment; return this; } @@ -230,7 +229,7 @@ public JavaDockerContextGenerator setEnvironment(Map environment * @param exposedPorts the list of port numbers/port ranges to expose * @return this */ - public JavaDockerContextGenerator setExposedPorts(List exposedPorts) { + public JavaDockerContextGenerator setExposedPorts(@Nullable List exposedPorts) { this.exposedPorts = exposedPorts; return this; } @@ -241,7 +240,7 @@ public JavaDockerContextGenerator setExposedPorts(List exposedPorts) { * @param labels the map of labels * @return this */ - public JavaDockerContextGenerator setLabels(Map labels) { + public JavaDockerContextGenerator setLabels(@Nullable Map labels) { this.labels = labels; return this; } @@ -332,12 +331,18 @@ String makeDockerfile() throws JsonProcessingException { } dockerfile.append("\n"); - for (String port : exposedPorts) { - dockerfile.append("\nEXPOSE ").append(port); + if (exposedPorts != null) { + for (String port : exposedPorts) { + dockerfile.append("\nEXPOSE ").append(port); + } } - dockerfile.append(mapToDockerfileString(environment, "ENV")); - dockerfile.append(mapToDockerfileString(labels, "LABEL")); + if (environment != null) { + dockerfile.append(mapToDockerfileString(environment, "ENV")); + } + if (labels != null) { + dockerfile.append(mapToDockerfileString(labels, "LABEL")); + } if (entrypoint != null) { dockerfile.append("\nENTRYPOINT ").append(objectMapper.writeValueAsString(entrypoint)); } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 74f42cc59d..900ce8eb1b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -18,6 +18,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.9.13 + +### Fixed + +- Adds environment variable configuration to Docker context generator ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) + ## 0.9.11 ### Added diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index a1194392a9..1f1572f106 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.12-SNAPSHOT + 0.9.14-SNAPSHOT maven-plugin Jib diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 252a416d0c..47c978139c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -88,6 +88,7 @@ public void execute() throws MojoExecutionException { .setEntrypoint(entrypoint) .setProgramArguments(getArgs()) .setExposedPorts(getExposedPorts()) + .setEnvironment(getEnvironment()) .setLabels(getLabels()) .setUser(getUser()) .generate(Paths.get(targetDir)); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 61f3c83f9b..bf73130422 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -17,11 +17,13 @@ package com.google.cloud.tools.jib.maven; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import org.apache.maven.model.Build; import org.apache.maven.plugin.MojoExecutionException; @@ -119,6 +121,11 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } @Test + public void testGeneratedDockerContext_env() throws MojoExecutionException, IOException { + mojo.execute(); + Assert.assertEquals("ENV envKey=\"envVal\"", getDockerfileLine("ENV")); + } + public void testBaseImage_nonWarPackaging() throws MojoExecutionException, IOException { mojo.execute(); @@ -234,6 +241,11 @@ String getMainClass() { String getAppRoot() { return appRoot; } + + @Override + Map getEnvironment() { + return ImmutableMap.of("envKey", "envVal"); + } } @Nullable From fc74d6b9976ec2cec9968901e48076742df0ee33 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 18 Oct 2018 11:58:38 -0400 Subject: [PATCH 0305/2020] Set tag to latest if version is unspecified in jibDockerBuild (#1159) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/gradle/BuildDockerTask.java | 4 +++- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 +++- .../common/ConfigurationPropertyValidator.java | 14 +++++++------- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e530069b3a..86fc318812 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) - `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- Sets tag to "latest" instead of "unspecified" if `jib.to.image` and project version are both unspecified when running `jibDockerBuild` or `jibBuildTar` ([#1096](https://github.com/GoogleContainerTools/jib/issues/1096)) ### Fixed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index afdf2b5a08..22bda0354e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -96,7 +96,9 @@ public void buildDocker() jibExtension.getTo().getImage(), eventDispatcher, getProject().getName(), - getProject().getVersion().toString(), + getProject().getVersion().toString().equals("unspecified") + ? "latest" + : getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 914c0c55fc..72cb89f03d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -121,7 +121,9 @@ public void buildTar() jibExtension.getTo().getImage(), eventDispatcher, getProject().getName(), - getProject().getVersion().toString(), + getProject().getVersion().toString().equals("unspecified") + ? "latest" + : getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); Path tarOutputPath = Paths.get(getTargetPath()); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index e54453a024..708a2dfce1 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -110,8 +110,8 @@ public static Optional getImageCredential( * * @param targetImage the configured target image reference * @param eventDispatcher the {@link EventDispatcher} used to dispatch log events - * @param projectName the project name, as determined by the plugin - * @param projectVersion the project version, as determined by the plugin + * @param generatedName the image name to use if {@code targetImage} is {@code null} + * @param generatedTag the tag to use if {@code targetImage} is {@code null} * @param helpfulSuggestions used for generating the message notifying the user of the generated * tag * @return an {@link ImageReference} parsed from the configured target image, or one of the form @@ -122,18 +122,18 @@ public static Optional getImageCredential( public static ImageReference getGeneratedTargetDockerTag( @Nullable String targetImage, EventDispatcher eventDispatcher, - String projectName, - String projectVersion, + String generatedName, + String generatedTag, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { if (Strings.isNullOrEmpty(targetImage)) { eventDispatcher.dispatch( - LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(projectName, projectVersion))); + LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(generatedName, generatedTag))); // Try to parse generated tag to verify that project name and version are valid (throws an // exception if parse fails) - ImageReference.parse(projectName + ":" + projectVersion); - return ImageReference.of(null, projectName, projectVersion); + ImageReference.parse(generatedName + ":" + generatedTag); + return ImageReference.of(null, generatedName, generatedTag); } else { return ImageReference.parse(targetImage); } From 10f7db22f820ef2fa03773b3a30f53669b9d59b3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 18 Oct 2018 17:39:09 -0400 Subject: [PATCH 0306/2020] Remove obsolete code (#1162) --- .../jib/maven/PluginConfigurationProcessor.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index b4b5695bf6..659a454259 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -197,22 +197,6 @@ static PluginConfigurationProcessor processCommonConfiguration( jibContainerBuilder.setCreationTime(Instant.now()); } - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder() - .setToolName(MavenProjectProperties.TOOL_NAME) - .setEventDispatcher(eventDispatcher) - .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) - .setLayerConfigurations( - projectProperties.getJavaLayerConfigurations().getLayerConfigurations()); - buildConfigurationBuilder.setApplicationLayersCacheDirectory( - projectProperties.getCacheDirectory()); - buildConfigurationBuilder.setBaseImageLayersCacheDirectory( - Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); - if (jibPluginConfiguration.getUseOnlyProjectCache()) { - buildConfigurationBuilder.setBaseImageLayersCacheDirectory( - projectProperties.getCacheDirectory()); - } - return new PluginConfigurationProcessor( jibContainerBuilder, baseImageReference, From fd6428999346a30939706423daebe2aad46df108 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 19 Oct 2018 14:02:40 -0400 Subject: [PATCH 0307/2020] Add FilePermissions class (#1166) --- .../jib/configuration/FilePermissions.java | 109 ++++++++++++++++++ .../configuration/FilePermissionsTest.java | 69 +++++++++++ 2 files changed, 178 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java new file mode 100644 index 0000000000..1b800b2c5b --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +/** Represents read/write/execute file permissions for owner, group, and others. */ +public class FilePermissions { + + /** + * Matches an octal string representation of file permissions. From left to right, each digit + * represents permissions for owner, group, and other. + */ + private static final String OCTAL_PATTERN = "[0-7][0-7][0-7]"; + + /** Maps from a {@link PosixFilePermission} to its corresponding file permission bit. */ + private static final ImmutableMap PERMISSION_MAP = + ImmutableMap.builder() + .put(PosixFilePermission.OWNER_READ, 0400) + .put(PosixFilePermission.OWNER_WRITE, 0200) + .put(PosixFilePermission.OWNER_EXECUTE, 0100) + .put(PosixFilePermission.GROUP_READ, 040) + .put(PosixFilePermission.GROUP_WRITE, 020) + .put(PosixFilePermission.GROUP_EXECUTE, 010) + .put(PosixFilePermission.OTHERS_READ, 04) + .put(PosixFilePermission.OTHERS_WRITE, 02) + .put(PosixFilePermission.OTHERS_EXECUTE, 01) + .build(); + + /** + * Creates a new {@link FilePermissions} from an octal string representation (e.g. "123", "644", + * "755", etc). + * + * @param octalPermissions the octal string representation of the permissions + * @return a new {@link FilePermissions} with the given permissions + */ + public static FilePermissions fromOctalString(String octalPermissions) { + Preconditions.checkArgument( + octalPermissions.matches(OCTAL_PATTERN), + "octalPermissions must be a 3-digit octal number (000-777)"); + return new FilePermissions(Integer.parseInt(octalPermissions, 8)); + } + + /** + * Creates a new {@link FilePermissions} from a set of {@link PosixFilePermission}. + * + * @param posixFilePermissions the set of {@link PosixFilePermission} + * @return a new {@link FilePermissions} with the given permissions + */ + public static FilePermissions fromPosixFilePermissions( + Set posixFilePermissions) { + int permissionBits = 0; + for (PosixFilePermission permission : posixFilePermissions) { + permissionBits |= PERMISSION_MAP.get(permission); + } + return new FilePermissions(permissionBits); + } + + private final int permissionBits; + + @VisibleForTesting + FilePermissions(int permissionBits) { + this.permissionBits = permissionBits; + } + + /** + * Gets the corresponding permissions bits specified by the {@link FilePermissions} + * + * @return the permission bits + */ + public int getPermissionBits() { + return permissionBits; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof FilePermissions)) { + return false; + } + FilePermissions otherFilePermissions = (FilePermissions) other; + return permissionBits == otherFilePermissions.permissionBits; + } + + @Override + public int hashCode() { + return permissionBits; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java new file mode 100644 index 0000000000..ab5efd09cc --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.nio.file.attribute.PosixFilePermission; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link FilePermissions}. */ +public class FilePermissionsTest { + + @Test + public void testFromOctalString() { + Assert.assertEquals(new FilePermissions(0777), FilePermissions.fromOctalString("777")); + Assert.assertEquals(new FilePermissions(0000), FilePermissions.fromOctalString("000")); + Assert.assertEquals(new FilePermissions(0123), FilePermissions.fromOctalString("123")); + Assert.assertEquals(new FilePermissions(0755), FilePermissions.fromOctalString("755")); + Assert.assertEquals(new FilePermissions(0644), FilePermissions.fromOctalString("644")); + + ImmutableList badStrings = ImmutableList.of("abc", "-123", "777444333", "987", "3"); + for (String badString : badStrings) { + try { + FilePermissions.fromOctalString(badString); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "octalPermissions must be a 3-digit octal number (000-777)", ex.getMessage()); + } + } + } + + @Test + public void testFromPosixFilePermissions() { + Assert.assertEquals( + new FilePermissions(0000), FilePermissions.fromPosixFilePermissions(ImmutableSet.of())); + Assert.assertEquals( + new FilePermissions(0110), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_EXECUTE))); + Assert.assertEquals( + new FilePermissions(0202), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.OWNER_WRITE, PosixFilePermission.OTHERS_WRITE))); + Assert.assertEquals( + new FilePermissions(0044), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.OTHERS_READ))); + Assert.assertEquals( + new FilePermissions(0777), + FilePermissions.fromPosixFilePermissions( + ImmutableSet.copyOf(PosixFilePermission.values()))); + } +} From 07ff66d2c8b46c8352c8cedd125974c2a9e6707e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 19 Oct 2018 15:46:39 -0400 Subject: [PATCH 0308/2020] Core changes for setting file permissions (#1156) --- .../jib/configuration/FilePermissions.java | 4 + .../jib/configuration/LayerConfiguration.java | 21 ++- .../jib/frontend/JavaLayerConfigurations.java | 66 +++++++- .../cloud/tools/jib/image/LayerEntry.java | 29 +++- .../jib/image/ReproducibleLayerBuilder.java | 10 +- .../cloud/tools/jib/cache/CacheTest.java | 5 +- .../jib/cache/LayerEntriesSelectorTest.java | 15 +- .../configuration/LayerConfigurationTest.java | 20 ++- .../JavaDockerContextGeneratorTest.java | 28 ++-- .../frontend/JavaLayerConfigurationsTest.java | 151 ++++++++++++++---- .../image/ReproducibleLayerBuilderTest.java | 49 +++++- .../plugins/common/MainClassResolverTest.java | 11 +- 12 files changed, 327 insertions(+), 82 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java index 1b800b2c5b..2756a31e4f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java @@ -25,6 +25,10 @@ /** Represents read/write/execute file permissions for owner, group, and others. */ public class FilePermissions { + public static final FilePermissions DEFAULT_FILE_PERMISSIONS = new FilePermissions(0644); + + public static final FilePermissions DEFAULT_FOLDER_PERMISSIONS = new FilePermissions(0755); + /** * Matches an octal string representation of file permissions. From left to right, each digit * represents permissions for owner, group, and other. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index 0f8d33ed5c..7ccdf63976 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; /** Configures how to build a layer in the container image. Instantiate with {@link #builder}. */ public class LayerConfiguration { @@ -66,7 +67,25 @@ public Builder setName(String name) { * @return this */ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { - layerEntries.add(new LayerEntry(sourceFile, pathInContainer)); + return addEntry(sourceFile, pathInContainer, null); + } + + /** + * Adds an entry to the layer with the given permissions. Only adds the single source file to + * the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container. If null, then default permissions + * are used (644 for files, 755 for directories) + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, @Nullable FilePermissions permissions) { + layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions)); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 9191325a00..de7a206ff3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; @@ -31,12 +32,13 @@ import java.util.EnumMap; import java.util.Map; import java.util.function.Predicate; +import javax.annotation.Nullable; /** Builds {@link LayerConfiguration}s for a Java application. */ public class JavaLayerConfigurations { /** Represents the different types of layers for a Java application. */ - public static enum LayerType { + public enum LayerType { DEPENDENCIES("dependencies"), SNAPSHOT_DEPENDENCIES("snapshot dependencies"), RESOURCES("resources"), @@ -50,7 +52,7 @@ public static enum LayerType { * * @param name name to set for the layer; does not affect the contents of the layer */ - private LayerType(String name) { + LayerType(String name) { this.name = name; } @@ -86,8 +88,31 @@ private Builder() { * @see LayerConfiguration.Builder#addEntry(Path, AbsoluteUnixPath) */ public Builder addFile(LayerType layerType, Path sourceFile, AbsoluteUnixPath pathInContainer) { + return addFile(layerType, sourceFile, pathInContainer, null); + } + + /** + * Adds a file to a layer. Only adds the single source file to the exact path in the container + * file system. (If the source file is a directory, does not copy its contents but creates only + * the directory.) See {@link LayerConfiguration.Builder#addEntry} for concrete examples about + * how the file will be placed in the image. + * + * @param layerType the layer to add files into + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container. Use {@code null} for defaults (644 + * for files, 755 for directories) + * @return this + * @see LayerConfiguration.Builder#addEntry(Path, AbsoluteUnixPath, FilePermissions) + */ + public Builder addFile( + LayerType layerType, + Path sourceFile, + AbsoluteUnixPath pathInContainer, + @Nullable FilePermissions permissions) { Preconditions.checkNotNull(layerBuilders.get(layerType)) - .addEntry(sourceFile, pathInContainer); + .addEntry(sourceFile, pathInContainer, permissions); return this; } @@ -115,14 +140,45 @@ public Builder addDirectoryContents( Predicate pathFilter, AbsoluteUnixPath basePathInContainer) throws IOException { + return addDirectoryContents( + layerType, sourceRoot, pathFilter, basePathInContainer, ImmutableMap.of()); + } + + /** + * Adds directory contents to a layer selectively (via {@code pathFilter}) and recursively. + * {@code sourceRoot} must be a directory. Empty directories will always be added regardless of + * {@code pathFilter}, except that {@code sourceRoot} is never added. Permissions are specified + * via {@code permissionsMap}, which maps from extraction path on the container to file + * permissions. + * + * @param layerType the layer to add files into + * @param sourceRoot root directory whose contents will be added + * @param pathFilter filter that determines which files (not directories) should be added + * @param basePathInContainer directory in the layer into which the source contents are added + * @param permissionsMap the map from absolute path on container to file permission + * @return this + * @throws IOException error while listing directories + * @throws NotDirectoryException if {@code sourceRoot} is not a directory + */ + // TODO: Use in plugins + public Builder addDirectoryContents( + LayerType layerType, + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer, + Map permissionsMap) + throws IOException { LayerConfiguration.Builder builder = Preconditions.checkNotNull(layerBuilders.get(layerType)); new DirectoryWalker(sourceRoot) .filterRoot() .filter(path -> Files.isDirectory(path) || pathFilter.test(path)) .walk( - path -> - builder.addEntry(path, basePathInContainer.resolve(sourceRoot.relativize(path)))); + path -> { + AbsoluteUnixPath pathOnContainer = + basePathInContainer.resolve(sourceRoot.relativize(path)); + builder.addEntry(path, pathOnContainer, permissionsMap.get(pathOnContainer)); + }); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index b251065d27..65a942e73c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; +import javax.annotation.Nullable; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar @@ -36,6 +39,7 @@ public class LayerEntry { private final Path sourceFile; private final AbsoluteUnixPath extractionPath; + private final FilePermissions permissions; /** * Instantiates with a source file and the path to place the source file in the container file @@ -52,10 +56,19 @@ public class LayerEntry { * @param sourceFile the source file to add to the layer * @param extractionPath the path in the container file system corresponding to the {@code * sourceFile} + * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 + * for files, 755 for directories) */ - public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath) { + public LayerEntry( + Path sourceFile, AbsoluteUnixPath extractionPath, @Nullable FilePermissions permissions) { this.sourceFile = sourceFile; this.extractionPath = extractionPath; + this.permissions = + permissions == null + ? Files.isDirectory(sourceFile) + ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS + : FilePermissions.DEFAULT_FILE_PERMISSIONS + : permissions; } /** @@ -81,6 +94,15 @@ public AbsoluteUnixPath getExtractionPath() { return extractionPath; } + /** + * Gets the file permissions on the container. + * + * @return the file permissions on the container + */ + public FilePermissions getPermissions() { + return permissions; + } + // TODO: Remove these get...String methods. /** * Get the source file as an absolute path in Unix form. The path is made absolute first, if not @@ -111,11 +133,12 @@ public boolean equals(Object other) { } LayerEntry otherLayerEntry = (LayerEntry) other; return sourceFile.equals(otherLayerEntry.sourceFile) - && extractionPath.equals(otherLayerEntry.extractionPath); + && extractionPath.equals(otherLayerEntry.extractionPath) + && Objects.equals(permissions, otherLayerEntry.permissions); } @Override public int hashCode() { - return Objects.hash(sourceFile, extractionPath); + return Objects.hash(sourceFile, extractionPath, permissions); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 2b1aec456c..05f760cd41 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -100,9 +100,15 @@ public Blob build() { for (LayerEntry layerEntry : layerEntries) { // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and // adds parent directories for each extraction path. - uniqueTarArchiveEntries.add( + TarArchiveEntry entry = new TarArchiveEntry( - layerEntry.getSourceFile().toFile(), layerEntry.getAbsoluteExtractionPathString())); + layerEntry.getSourceFile().toFile(), layerEntry.getAbsoluteExtractionPathString()); + + // Sets the entry's permissions by masking out the permission bits from the entry's mode (the + // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. + entry.setMode((entry.getMode() & ~0777) | layerEntry.getPermissions().getPermissionBits()); + + uniqueTarArchiveEntries.add(entry); } // Gets the entries sorted by extraction path. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index d40cf93d81..669827c8ce 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -128,10 +128,11 @@ public void setUp() throws IOException { layerEntries1 = ImmutableList.of( new LayerEntry( - directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")), + directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path"), null), new LayerEntry( directory.resolve("another/source/file"), - AbsoluteUnixPath.get("/another/extraction/path"))); + AbsoluteUnixPath.get("/another/extraction/path"), + null)); layerBlob2 = Blobs.from("layerBlob2"); layerDigest2 = digestOf(compress(layerBlob2)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 4e1f3e6b68..a1d748375b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -58,10 +58,14 @@ public void setUp() throws IOException { Path file2 = Files.createFile(folder.resolve("files").resolve("two")); Path file3 = Files.createFile(folder.resolve("gile")); - LayerEntry testLayerEntry1 = new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); - LayerEntry testLayerEntry2 = new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); - LayerEntry testLayerEntry3 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); - LayerEntry testLayerEntry4 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); + LayerEntry testLayerEntry1 = + new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path"), null); + LayerEntry testLayerEntry2 = + new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path"), null); + LayerEntry testLayerEntry3 = + new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path"), null); + LayerEntry testLayerEntry4 = + new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha"), null); outOfOrderLayerEntries = ImmutableList.of(testLayerEntry4, testLayerEntry2, testLayerEntry3, testLayerEntry1); inOrderLayerEntries = @@ -107,7 +111,8 @@ public void testGenerateSelector_fileModified() throws IOException { Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); - LayerEntry layerEntry = new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry layerEntry = + new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path"), null); DescriptorDigest expectedSelector = LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java index 11f804cd8b..ec38245641 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -43,16 +43,20 @@ public void testAddEntryRecursive() throws IOException, URISyntaxException { ImmutableSet expectedLayerEntries = ImmutableSet.of( - new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), - new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), - new LayerEntry(testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), + new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/"), null), + new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/"), null), new LayerEntry( - testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/")), - new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), + testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/"), null), new LayerEntry( - testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat")), - new LayerEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), - new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); + testDirectory.resolve("a/b/bar"), + AbsoluteUnixPath.get("/app/layer/a/b/bar/"), + null), + new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c"), null), + new LayerEntry( + testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat"), null), + new LayerEntry( + testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo"), null), + new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"), null)); Assert.assertEquals( expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java index cdb74a2c09..546e25e978 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java @@ -73,7 +73,7 @@ private ImmutableList filesToLayerEntries(Path directory, String ext .map( sourceFile -> new LayerEntry( - sourceFile, extractionRoot.resolve(directory.relativize(sourceFile)))) + sourceFile, extractionRoot.resolve(directory.relativize(sourceFile)), null)) .collect(ImmutableList.toImmutableList()); } @@ -136,17 +136,19 @@ public void testMakeDockerfile() throws IOException { Path ignored = Paths.get("ignored"); Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/app/libs")))); + .thenReturn( + ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/app/libs"), null))); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/snapshots")))); + .thenReturn( + ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/snapshots"), null))); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/resources")))); + ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/resources"), null))); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/classes/")))); + ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/classes/"), null))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/")))); + .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/"), null))); String dockerfile = new JavaDockerContextGenerator(mockJavaLayerConfigurations) @@ -174,20 +176,24 @@ public void testMakeDockerfileWithWebApp() throws IOException { Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib")))); + new LayerEntry( + Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib"), null))); Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib")))); + new LayerEntry( + Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib"), null))); Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), exepectedAppRoot))); + .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), exepectedAppRoot, null))); Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/classes")))); + new LayerEntry( + Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/classes"), null))); Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/")))); + ImmutableList.of( + new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/"), null))); String dockerfile = new JavaDockerContextGenerator(mockJavaLayerConfigurations) .setBaseImage(expectedBaseImage) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index b1560b9c16..34f57e0c11 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -1,10 +1,12 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -13,7 +15,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -84,23 +88,26 @@ public void testAddFile() { JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); Assert.assertEquals( - Arrays.asList( - new LayerEntry(Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path"))), + Collections.singletonList( + new LayerEntry( + Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path"), null)), javaLayerConfigurations.getDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList( - new LayerEntry(Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots"))), + Collections.singletonList( + new LayerEntry( + Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots"), null)), javaLayerConfigurations.getSnapshotDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList( - new LayerEntry(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here"))), + Collections.singletonList( + new LayerEntry(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here"), null)), javaLayerConfigurations.getResourceLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here"))), + Collections.singletonList( + new LayerEntry(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here"), null)), javaLayerConfigurations.getClassLayerEntries()); Assert.assertEquals( - Arrays.asList( - new LayerEntry(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras"))), + Collections.singletonList( + new LayerEntry(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras"), null)), javaLayerConfigurations.getExtraFilesLayerEntries()); } @@ -121,19 +128,24 @@ public void testAddFile_directories() throws URISyntaxException { .build(); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/libs/dir"))), + Collections.singletonList( + new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/libs/dir"), null)), configurations.getDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target"))), + Collections.singletonList( + new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target"), null)), configurations.getSnapshotDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/resources"))), + Collections.singletonList( + new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/resources"), null)), configurations.getResourceLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/classes/here"))), + Collections.singletonList( + new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/classes/here"), null)), configurations.getClassLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/extra/files"))), + Collections.singletonList( + new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/extra/files"), null)), configurations.getExtraFilesLayerEntries()); } @@ -155,19 +167,24 @@ public void testAddFile_regularFiles() throws URISyntaxException { .build(); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/libs/file"))), + Collections.singletonList( + new LayerEntry(sourceFile, AbsoluteUnixPath.get("/libs/file"), null)), configurations.getDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file"))), + Collections.singletonList( + new LayerEntry(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file"), null)), configurations.getSnapshotDependencyLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/resources-file"))), + Collections.singletonList( + new LayerEntry(sourceFile, AbsoluteUnixPath.get("/resources-file"), null)), configurations.getResourceLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/classes/file"))), + Collections.singletonList( + new LayerEntry(sourceFile, AbsoluteUnixPath.get("/classes/file"), null)), configurations.getClassLayerEntries()); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceFile, AbsoluteUnixPath.get("/some/file"))), + Collections.singletonList( + new LayerEntry(sourceFile, AbsoluteUnixPath.get("/some/file"), null)), configurations.getExtraFilesLayerEntries()); } @@ -203,23 +220,26 @@ public void testAddFile_webAppSample() { ImmutableList expectedDependenciesLayer = ImmutableList.of( - new LayerEntry(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar"))); + new LayerEntry(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar"), null)); ImmutableList expectedSnapshotDependenciesLayer = ImmutableList.of( new LayerEntry( - Paths.get("my-SNAPSHOT.jar"), appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"))); + Paths.get("my-SNAPSHOT.jar"), + appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"), + null)); ImmutableList expectedResourcesLayer = ImmutableList.of( - new LayerEntry(Paths.get("test.jsp"), appRoot.resolve("test.jsp")), - new LayerEntry(Paths.get("META-INF"), appRoot.resolve("META-INF")), - new LayerEntry(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml")), - new LayerEntry(Paths.get("sub_dir"), appRoot.resolve("WEB-INF/sub_dir"))); + new LayerEntry(Paths.get("test.jsp"), appRoot.resolve("test.jsp"), null), + new LayerEntry(Paths.get("META-INF"), appRoot.resolve("META-INF"), null), + new LayerEntry(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml"), null), + new LayerEntry(Paths.get("sub_dir"), appRoot.resolve("WEB-INF/sub_dir"), null)); ImmutableList expectedClassesLayer = ImmutableList.of( - new LayerEntry(Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class"))); + new LayerEntry( + Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class"), null)); ImmutableList expectedExtraLayer = ImmutableList.of( - new LayerEntry(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file"))); + new LayerEntry(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file"), null)); Assert.assertEquals(expectedDependenciesLayer, configurations.getDependencyLayerEntries()); Assert.assertEquals( @@ -241,7 +261,8 @@ public void testAddDirectoryContents_file() throws IOException { .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, path -> true, basePath) .build(); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceRoot.resolve("file"), basePath.resolve("file"))), + Collections.singletonList( + new LayerEntry(sourceRoot.resolve("file"), basePath.resolve("file"), null)), configurations.getExtraFilesLayerEntries()); } @@ -257,7 +278,8 @@ public void testAddDirectoryContents_emptyDirectory() throws IOException { .addDirectoryContents(LayerType.CLASSES, sourceRoot, path -> true, basePath) .build(); Assert.assertEquals( - Arrays.asList(new LayerEntry(sourceRoot.resolve("leaf"), basePath.resolve("leaf"))), + Collections.singletonList( + new LayerEntry(sourceRoot.resolve("leaf"), basePath.resolve("leaf"), null)), configurations.getClassLayerEntries()); } @@ -274,9 +296,9 @@ public void testAddDirectoryContents_directoriesAdded() throws IOException { .build(); Assert.assertEquals( Arrays.asList( - new LayerEntry(sourceRoot.resolve("non-empty"), basePath.resolve("non-empty")), + new LayerEntry(sourceRoot.resolve("non-empty"), basePath.resolve("non-empty"), null), new LayerEntry( - sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf"))), + sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf"), null)), configurations.getResourceLayerEntries()); } @@ -296,8 +318,8 @@ public void testAddDirectoryContents_filter() throws IOException { .build(); Assert.assertEquals( Arrays.asList( - new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub")), - new LayerEntry(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target"))), + new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub"), null), + new LayerEntry(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target"), null)), configurations.getDependencyLayerEntries()); } @@ -314,8 +336,8 @@ public void testAddDirectoryContents_directoriesForced() throws IOException { .build(); Assert.assertEquals( Arrays.asList( - new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub")), - new LayerEntry(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf"))), + new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub"), null), + new LayerEntry(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf"), null)), configurations.getExtraFilesLayerEntries()); } @@ -379,4 +401,63 @@ public void testAddDirectoryContents_complex() throws IOException { "/base/test/D.class"), configurations.getExtraFilesLayerEntries()); } + + @Test + public void testAddExtraDirectoryContents() throws IOException { + temporaryFolder.newFolder("src", "main", "jib", "folder", "folder2"); + temporaryFolder.newFile("src/main/jib/fileA"); + temporaryFolder.newFile("src/main/jib/fileB"); + temporaryFolder.newFile("src/main/jib/fileC"); + temporaryFolder.newFile("src/main/jib/folder/fileD"); + temporaryFolder.newFile("src/main/jib/folder/fileE"); + + Map permissions = + ImmutableMap.of( + AbsoluteUnixPath.get("/fileA"), + FilePermissions.fromOctalString("123"), + AbsoluteUnixPath.get("/fileB"), + FilePermissions.fromOctalString("456"), + AbsoluteUnixPath.get("/folder"), + FilePermissions.fromOctalString("111"), + AbsoluteUnixPath.get("/folder/fileD"), + FilePermissions.fromOctalString("222")); + + Path extraDirectory = + temporaryFolder.getRoot().toPath().resolve("src").resolve("main").resolve("jib"); + AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); + JavaLayerConfigurations configurations = + JavaLayerConfigurations.builder() + .addDirectoryContents( + LayerType.EXTRA_FILES, extraDirectory, path -> true, basePath, permissions) + .build(); + + Assert.assertEquals( + Arrays.asList( + new LayerEntry( + extraDirectory.resolve("fileA"), + basePath.resolve("fileA"), + FilePermissions.fromOctalString("123")), + new LayerEntry( + extraDirectory.resolve("fileB"), + basePath.resolve("fileB"), + FilePermissions.fromOctalString("456")), + new LayerEntry(extraDirectory.resolve("fileC"), basePath.resolve("fileC"), null), + new LayerEntry( + extraDirectory.resolve("folder"), + basePath.resolve("folder"), + FilePermissions.fromOctalString("111")), + new LayerEntry( + extraDirectory.resolve("folder").resolve("fileD"), + basePath.resolve("folder").resolve("fileD"), + FilePermissions.fromOctalString("222")), + new LayerEntry( + extraDirectory.resolve("folder").resolve("fileE"), + basePath.resolve("folder").resolve("fileE"), + null), + new LayerEntry( + extraDirectory.resolve("folder").resolve("folder2"), + basePath.resolve("folder").resolve("folder2"), + null)), + configurations.getExtraFilesLayerEntries()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index f3a8a8e69f..9dffc527ae 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.collect.ImmutableList; @@ -166,14 +167,14 @@ public void testToBlob_reproducibility() throws IOException { Blob layer = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), - new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) + new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA"), null), + new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB"), null))) .build(); Blob reproduced = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), - new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) + new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB"), null), + new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA"), null))) .build(); byte[] layerContent = Blobs.writeToByteArray(layer); @@ -188,7 +189,7 @@ public void testBuild_timestamp() throws IOException { Blob blob = new ReproducibleLayerBuilder( - ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA")))) + ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA"), null))) .build(); Path tarFile = temporaryFolder.newFile().toPath(); @@ -203,6 +204,44 @@ public void testBuild_timestamp() throws IOException { } } + @Test + public void testBuild_permissions() throws IOException { + Path testRoot = temporaryFolder.getRoot().toPath(); + Path folder = Files.createDirectories(testRoot.resolve("files1")); + Path fileA = createFile(testRoot, "fileA", "abc", 54321); + Path fileB = createFile(testRoot, "fileB", "def", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of( + new LayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA"), null), + new LayerEntry( + fileB, + AbsoluteUnixPath.get("/somewhere/fileB"), + FilePermissions.fromOctalString("123")), + new LayerEntry( + folder, + AbsoluteUnixPath.get("/somewhere/folder"), + FilePermissions.fromOctalString("456")))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + // Root folder (default folder permissions) + Assert.assertEquals(040755, in.getNextTarEntry().getMode()); + // fileA (default file permissions) + Assert.assertEquals(0100644, in.getNextTarEntry().getMode()); + // fileB (custom file permissions) + Assert.assertEquals(0100123, in.getNextTarEntry().getMode()); + // folder (custom folder permissions) + Assert.assertEquals(040456, in.getNextTarEntry().getMode()); + } + } + private Path createFile(Path root, String filename, String content, long lastModifiedTime) throws IOException { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index f514802cb3..79d31d8568 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -73,7 +73,8 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( - ImmutableList.of(new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored")))); + ImmutableList.of( + new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored"), null))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockJibEventConsumer) @@ -90,7 +91,7 @@ public void testResolveMainClass_multipleInferredWithBackup() Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) .walk() .stream() - .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"))) + .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) .collect(ImmutableList.toImmutableList())); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); @@ -108,7 +109,7 @@ public void testResolveMainClass_multipleInferredWithoutBackup() Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) .walk() .stream() - .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"))) + .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) .collect(ImmutableList.toImmutableList())); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); @@ -128,7 +129,7 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); + new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored"), null))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockJibEventConsumer) @@ -140,7 +141,7 @@ public void testResolveMainClass_noneInferredWithoutBackup() { Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) .thenReturn( ImmutableList.of( - new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored")))); + new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored"), null))); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); From 563160b192f6926b98fe628fa6630bc031e5fb8a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 19 Oct 2018 16:11:23 -0400 Subject: [PATCH 0309/2020] Update commons-compress to 1.18 (#1170) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index d6dd2171fc..20db8e0a78 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -40,7 +40,7 @@ configurations { dependencies { // Make sure these are consistent with jib-maven-plugin. implementation 'com.google.http-client:google-http-client:1.23.0' - implementation 'org.apache.commons:commons-compress:1.17' + implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:23.5-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' implementation 'org.javassist:javassist:3.22.0-GA' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index f856a8ebf1..41c0cf0e99 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -62,7 +62,7 @@ configurations { dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. compile 'com.google.http-client:google-http-client:1.23.0' - compile 'org.apache.commons:commons-compress:1.17' + compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' compile 'org.javassist:javassist:3.22.0-GA' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 1f1572f106..02244a2777 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -56,7 +56,7 @@ org.apache.commons commons-compress - 1.17 + 1.18 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 686e1917c3..90235b5d40 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -30,7 +30,7 @@ sourceSets { dependencies { // Make sure these are consistent with jib-maven-plugin. compile 'com.google.http-client:google-http-client:1.23.0' - compile 'org.apache.commons:commons-compress:1.17' + compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' compile 'org.javassist:javassist:3.22.0-GA' From bd8c4544f2f70ee407e1a3fc89cc286e3ff486d9 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 22 Oct 2018 16:22:30 -0400 Subject: [PATCH 0310/2020] Update CONTRIBUTING.md for style guide and jib-plugins-common (#1176) --- CONTRIBUTING.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 305c0755d4..9a79ae0b7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,9 @@ again. Jib comes as 3 components: - `jib-core`: a library - - `jib-maven-plugin`: a Maven plugin that uses `jib-core` - - `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` + - `jib-plugins-common`: a library with helpers for plugin builders + - `jib-maven-plugin`: a Maven plugin that uses `jib-core` and `jib-plugins-common` + - `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` and `jib-plugins-common` To build, use the provided `build.sh` which builds and tests each of the components into your local `~/.m2/repository`. Note that this script does not run integration tests. @@ -34,10 +35,12 @@ To build, use the provided `build.sh` which builds and tests each of the compone If you're a Googler or other corporate contributor, use your corporate email address here, not your personal address. 2. Fork the repository into your own Github account. +3. We follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) 3. Please include unit tests (and integration tests if applicable) for all new code. 4. Make sure all existing tests pass (but see the note below about integration tests). * In `jib-core`, run `./gradlew clean goJF build integrationTest` * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` + * In `jib-plugins-common`, run `./gradlew clean goJF build` * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -Pintegration-tests` 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! From 374685f7c01e525deceda3693fb593e5582c645c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 22 Oct 2018 17:05:50 -0400 Subject: [PATCH 0311/2020] Make CacheEntry extend Layer (#1171) --- .../BuildAndCacheApplicationLayerStep.java | 22 +++---- .../jib/builder/steps/BuildImageStep.java | 36 ++--------- .../steps/PullAndCacheBaseImageLayerStep.java | 16 ++--- .../jib/builder/steps/PushLayersStep.java | 28 ++++----- .../google/cloud/tools/jib/cache/Cache.java | 20 +++---- .../cloud/tools/jib/cache/CacheStorage.java | 16 ++--- .../{CacheEntry.java => CachedLayer.java} | 24 ++------ .../tools/jib/cache/DefaultCacheStorage.java | 6 +- .../jib/cache/DefaultCacheStorageReader.java | 16 ++--- .../jib/cache/DefaultCacheStorageWriter.java | 20 +++---- ...acheEntry.java => DefaultCachedLayer.java} | 38 ++++++------ .../google/cloud/tools/jib/image/Layer.java | 1 + ...BuildAndCacheApplicationLayerStepTest.java | 59 ++++++++----------- .../jib/builder/steps/BuildImageStepTest.java | 24 +++++--- .../cloud/tools/jib/cache/CacheTest.java | 28 ++++----- .../cache/DefaultCacheStorageReaderTest.java | 14 ++--- .../cache/DefaultCacheStorageWriterTest.java | 22 +++---- ...yTest.java => DefaultCachedLayerTest.java} | 30 +++++----- 18 files changed, 190 insertions(+), 230 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/cache/{CacheEntry.java => CachedLayer.java} (62%) rename jib-core/src/main/java/com/google/cloud/tools/jib/cache/{DefaultCacheEntry.java => DefaultCachedLayer.java} (75%) rename jib-core/src/test/java/com/google/cloud/tools/jib/cache/{DefaultCacheEntryTest.java => DefaultCachedLayerTest.java} (70%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 91d996bb5a..0694d9cc5c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.cache.CacheEntry; +import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -34,7 +34,7 @@ import java.util.concurrent.Callable; /** Builds and caches application layers. */ -class BuildAndCacheApplicationLayerStep implements AsyncStep, Callable { +class BuildAndCacheApplicationLayerStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Building application layers"; @@ -69,7 +69,7 @@ static ImmutableList makeList( private final BuildConfiguration buildConfiguration; private final LayerConfiguration layerConfiguration; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; private BuildAndCacheApplicationLayerStep( String layerType, @@ -84,12 +84,12 @@ private BuildAndCacheApplicationLayerStep( } @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public CacheEntry call() throws IOException, CacheCorruptedException { + public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); @@ -99,21 +99,21 @@ public CacheEntry call() throws IOException, CacheCorruptedException { Cache cache = buildConfiguration.getApplicationLayersCache(); // Don't build the layer if it exists already. - Optional optionalCacheEntry = + Optional optionalCachedLayer = cache.retrieve(layerConfiguration.getLayerEntries()); - if (optionalCacheEntry.isPresent()) { - return optionalCacheEntry.get(); + if (optionalCachedLayer.isPresent()) { + return optionalCachedLayer.get(); } Blob layerBlob = new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries()).build(); - CacheEntry cacheEntry = + CachedLayer cachedLayer = cache.writeUncompressedLayer(layerBlob, layerConfiguration.getLayerEntries()); buildConfiguration .getEventDispatcher() - .dispatch(LogEvent.debug(description + " built " + cacheEntry.getLayerDigest())); + .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); - return cacheEntry; + return cachedLayer; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 24c2c5b4a1..c2d69bee2c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -19,19 +19,14 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.cache.CacheEntry; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -49,27 +44,6 @@ class BuildImageStep private static final String DESCRIPTION = "Building container configuration"; - @VisibleForTesting - static Layer cacheEntryToLayer(CacheEntry cacheEntry) { - return new Layer() { - - @Override - public Blob getBlob() throws LayerPropertyNotFoundException { - return cacheEntry.getLayerBlob(); - } - - @Override - public BlobDescriptor getBlobDescriptor() throws LayerPropertyNotFoundException { - return new BlobDescriptor(cacheEntry.getLayerSize(), cacheEntry.getLayerDigest()); - } - - @Override - public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { - return cacheEntry.getLayerDiffId(); - } - }; - } - private final BuildConfiguration buildConfiguration; private final PullBaseImageStep pullBaseImageStep; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; @@ -115,11 +89,11 @@ public AsyncStep> call() throws ExecutionException { } ListenableFuture> future = Futures.whenAllSucceed(dependencies) - .call(this::afterCacheEntrySteps, listeningExecutorService); + .call(this::afterCachedLayerSteps, listeningExecutorService); return () -> future; } - private Image afterCacheEntrySteps() + private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { @@ -133,8 +107,7 @@ private Image afterCacheEntrySteps() List baseImageLayers = NonBlockingSteps.get(pullAndCacheBaseImageLayersStep); for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : baseImageLayers) { - imageBuilder.addLayer( - cacheEntryToLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep))); + imageBuilder.addLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)); } // Passthrough config and count non-empty history entries @@ -165,8 +138,7 @@ private Image afterCacheEntrySteps() // Add built layers/configuration for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - imageBuilder.addLayer( - cacheEntryToLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep))); + imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); imageBuilder.addHistory( HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 75c34b476a..0235fc0cd2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.cache.CacheEntry; +import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -33,7 +33,7 @@ import javax.annotation.Nullable; /** Pulls and caches a single base image layer. */ -class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable { +class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Pulling base image layer %s"; @@ -41,7 +41,7 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable< private final DescriptorDigest layerDigest; private final @Nullable Authorization pullAuthorization; - private final ListenableFuture listenableFuture; + private final ListenableFuture listenableFuture; PullAndCacheBaseImageLayerStep( ListeningExecutorService listeningExecutorService, @@ -56,21 +56,21 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable< } @Override - public ListenableFuture getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public CacheEntry call() throws IOException, CacheCorruptedException { + public CachedLayer call() throws IOException, CacheCorruptedException { try (TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { Cache cache = buildConfiguration.getBaseImageLayersCache(); // Checks if the layer already exists in the cache. - Optional optionalCacheEntry = cache.retrieve(layerDigest); - if (optionalCacheEntry.isPresent()) { - return optionalCacheEntry.get(); + Optional optionalCachedLayer = cache.retrieve(layerDigest); + if (optionalCachedLayer.isPresent()) { + return optionalCachedLayer.get(); } RegistryClient registryClient = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 9c25dd4949..5ee6bf46ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.cache.CacheEntry; +import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -37,8 +37,8 @@ class PushLayersStep private final BuildConfiguration buildConfiguration; private final AuthenticatePushStep authenticatePushStep; - private final AsyncStep>> - cacheEntryStep; + private final AsyncStep>> + cachedLayerStep; private final ListeningExecutorService listeningExecutorService; private final ListenableFuture>> listenableFuture; @@ -47,15 +47,15 @@ class PushLayersStep ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, AuthenticatePushStep authenticatePushStep, - AsyncStep>> - cacheEntryStep) { + AsyncStep>> + cachedLayerStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; this.authenticatePushStep = authenticatePushStep; - this.cacheEntryStep = cacheEntryStep; + this.cachedLayerStep = cachedLayerStep; listenableFuture = - Futures.whenAllSucceed(cacheEntryStep.getFuture()).call(this, listeningExecutorService); + Futures.whenAllSucceed(cachedLayerStep.getFuture()).call(this, listeningExecutorService); } @Override @@ -67,12 +67,12 @@ public ListenableFuture>> getFuture() { public ImmutableList> call() throws ExecutionException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { - ImmutableList> cacheEntry = - NonBlockingSteps.get(cacheEntryStep); + ImmutableList> cachedLayer = + NonBlockingSteps.get(cachedLayerStep); // Constructs a PushBlobStep for each layer. ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); - for (AsyncStep cachedLayerStep : cacheEntry) { + for (AsyncStep cachedLayerStep : cachedLayer) { ListenableFuture pushBlobStepFuture = Futures.whenAllSucceed(cachedLayerStep.getFuture()) .call(() -> makePushBlobStep(cachedLayerStep), listeningExecutorService); @@ -83,14 +83,14 @@ public ImmutableList> call() throws ExecutionException { } } - private PushBlobStep makePushBlobStep(AsyncStep cacheEntryStep) + private PushBlobStep makePushBlobStep(AsyncStep cachedLayerStep) throws ExecutionException { - CacheEntry cacheEntry = NonBlockingSteps.get(cacheEntryStep); + CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep); return new PushBlobStep( listeningExecutorService, buildConfiguration, authenticatePushStep, - new BlobDescriptor(cacheEntry.getLayerSize(), cacheEntry.getLayerDigest()), - cacheEntry.getLayerBlob()); + new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), + cachedLayer.getBlob()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 561b40c4dd..dc5d382986 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -61,10 +61,10 @@ private Cache(CacheStorage cacheStorage) { * {@link Blob} and include a selector. * * @param compressedLayerBlob the compressed layer {@link Blob} - * @return the {@link CacheEntry} for the written layer + * @return the {@link CachedLayer} for the written layer * @throws IOException if an I/O exception occurs */ - public CacheEntry writeCompressedLayer(Blob compressedLayerBlob) throws IOException { + public CachedLayer writeCompressedLayer(Blob compressedLayerBlob) throws IOException { return cacheStorage.write(compressedLayerBlob); } @@ -74,10 +74,10 @@ public CacheEntry writeCompressedLayer(Blob compressedLayerBlob) throws IOExcept * * @param uncompressedLayerBlob the layer {@link Blob} * @param layerEntries the layer entries that make up the layer - * @return the {@link CacheEntry} for the written layer + * @return the {@link CachedLayer} for the written layer * @throws IOException if an I/O exception occurs */ - public CacheEntry writeUncompressedLayer( + public CachedLayer writeUncompressedLayer( Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { return cacheStorage.write( new UncompressedCacheWrite( @@ -85,14 +85,14 @@ public CacheEntry writeUncompressedLayer( } /** - * Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. + * Retrieves the {@link CachedLayer} that was built from the {@code layerEntries}. * * @param layerEntries the layer entries to match against - * @return a {@link CacheEntry} that was built from {@code layerEntries}, if found + * @return a {@link CachedLayer} that was built from {@code layerEntries}, if found * @throws IOException if an I/O exception occurs * @throws CacheCorruptedException if the cache is corrupted */ - public Optional retrieve(ImmutableList layerEntries) + public Optional retrieve(ImmutableList layerEntries) throws IOException, CacheCorruptedException { Optional optionalSelectedLayerDigest = cacheStorage.select(LayerEntriesSelector.generateSelector(layerEntries)); @@ -104,14 +104,14 @@ public Optional retrieve(ImmutableList layerEntries) } /** - * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. + * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheEntry} referenced by the layer digest, if found + * @return the {@link CachedLayer} referenced by the layer digest, if found * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - public Optional retrieve(DescriptorDigest layerDigest) + public Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { return cacheStorage.retrieve(layerDigest); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java index 75f07ca48f..7956ebc9d3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java @@ -26,7 +26,7 @@ * Interface for queries to a cache storage engine. * *

The cache storage engine stores layer data in compressed form. These entries are read out as - * {@link CacheEntry}s. Cache entries can be retrieved by the layer digest. + * {@link CachedLayer}s. Cache entries can be retrieved by the layer digest. * *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). * The selectors do not need to be unique. An example of a selector could be the digest of the list @@ -40,19 +40,19 @@ interface CacheStorage { * Saves the {@link UncompressedCacheWrite}. * * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} - * @return the {@link CacheEntry} for the written {@link UncompressedCacheWrite} + * @return the {@link CachedLayer} for the written {@link UncompressedCacheWrite} * @throws IOException if an I/O exception occurs */ - CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException; + CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException; /** * Saves a compressed layer {@link Blob}. * * @param compressedLayerBlob the compressed layer {@link Blob} - * @return the {@link CacheEntry} for the written layer + * @return the {@link CachedLayer} for the written layer * @throws IOException if an I/O exception occurs */ - CacheEntry write(Blob compressedLayerBlob) throws IOException; + CachedLayer write(Blob compressedLayerBlob) throws IOException; /** * Fetches all the layer digests stored. @@ -64,14 +64,14 @@ interface CacheStorage { Set fetchDigests() throws IOException, CacheCorruptedException; /** - * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. + * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheEntry} referenced by the layer digest, if found + * @return the {@link CachedLayer} referenced by the layer digest, if found * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - Optional retrieve(DescriptorDigest layerDigest) + Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException; /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java similarity index 62% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index a4cb263c84..6b5c515624 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -16,42 +16,26 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.Layer; /** * Represents a cache entry for a layer stored in the cache. Implementations must be * immutable. */ -public interface CacheEntry { +public interface CachedLayer extends Layer { /** * Gets the digest of the layer. * * @return the layer digest */ - DescriptorDigest getLayerDigest(); - - /** - * Gets the diff ID of the layer. The diff ID is the digest of the uncompressed layer contents, - * whereas the {@link #getLayerDigest} is the digest of the compressed layer contents. - * - * @return the layer diff ID - */ - DescriptorDigest getLayerDiffId(); + DescriptorDigest getDigest(); /** * Gets the size of the layer, in bytes. * * @return the layer size */ - long getLayerSize(); - - /** - * Gets the {@link Blob} for the layer. This {@link Blob} should be able to be used multiple - * times. - * - * @return the layer {@link Blob} - */ - Blob getLayerBlob(); + long getSize(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java index c3c5f91657..c63d1ce96e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java @@ -65,12 +65,12 @@ private DefaultCacheStorage(DefaultCacheStorageFiles defaultCacheStorageFiles) { } @Override - public CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { + public CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { return defaultCacheStorageWriter.write(uncompressedCacheWrite); } @Override - public CacheEntry write(Blob compressedLayerBlob) throws IOException { + public CachedLayer write(Blob compressedLayerBlob) throws IOException { return defaultCacheStorageWriter.write(compressedLayerBlob); } @@ -80,7 +80,7 @@ public Set fetchDigests() throws IOException, CacheCorruptedEx } @Override - public Optional retrieve(DescriptorDigest layerDigest) + public Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { return defaultCacheStorageReader.retrieve(layerDigest); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java index da4ad71a6d..c97ca0b235 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java @@ -64,34 +64,34 @@ Set fetchDigests() throws IOException, CacheCorruptedException } /** - * Retrieves the {@link CacheEntry} for the layer with digest {@code layerDigest}. + * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. * * @param layerDigest the layer digest - * @return the {@link CacheEntry} referenced by the layer digest, if found + * @return the {@link CachedLayer} referenced by the layer digest, if found * @throws CacheCorruptedException if the cache was found to be corrupted * @throws IOException if an I/O exception occurs */ - Optional retrieve(DescriptorDigest layerDigest) + Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { Path layerDirectory = defaultCacheStorageFiles.getLayerDirectory(layerDigest); if (!Files.exists(layerDirectory)) { return Optional.empty(); } - DefaultCacheEntry.Builder cacheEntryBuilder = - DefaultCacheEntry.builder().setLayerDigest(layerDigest); + DefaultCachedLayer.Builder cachedLayerBuilder = + DefaultCachedLayer.builder().setLayerDigest(layerDigest); try (Stream filesInLayerDirectory = Files.list(layerDirectory)) { for (Path fileInLayerDirectory : filesInLayerDirectory.collect(Collectors.toList())) { if (DefaultCacheStorageFiles.isLayerFile(fileInLayerDirectory)) { - if (cacheEntryBuilder.hasLayerBlob()) { + if (cachedLayerBuilder.hasLayerBlob()) { throw new CacheCorruptedException( "Multiple layer files found for layer with digest " + layerDigest.getHash() + " in directory: " + layerDirectory); } - cacheEntryBuilder + cachedLayerBuilder .setLayerBlob(Blobs.from(fileInLayerDirectory)) .setLayerDiffId(DefaultCacheStorageFiles.getDiffId(fileInLayerDirectory)) .setLayerSize(Files.size(fileInLayerDirectory)); @@ -99,7 +99,7 @@ Optional retrieve(DescriptorDigest layerDigest) } } - return Optional.of(cacheEntryBuilder.build()); + return Optional.of(cachedLayerBuilder.build()); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java index 246c41d41b..78372c3962 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java @@ -133,10 +133,10 @@ private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile * corresponding to the layer blob. * * @param compressedLayerBlob the compressed layer {@link Blob} to write out - * @return the {@link CacheEntry} representing the written entry + * @return the {@link CachedLayer} representing the written entry * @throws IOException if an I/O exception occurs */ - CacheEntry write(Blob compressedLayerBlob) throws IOException { + CachedLayer write(Blob compressedLayerBlob) throws IOException { // Creates the layers directory if it doesn't exist. Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); @@ -155,10 +155,10 @@ CacheEntry write(Blob compressedLayerBlob) throws IOException { temporaryLayerDirectory, defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); - // Updates cacheEntry with the blob information. + // Updates cachedLayer with the blob information. Path layerFile = defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); - return DefaultCacheEntry.builder() + return DefaultCachedLayer.builder() .setLayerDigest(writtenLayer.layerDigest) .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) @@ -180,10 +180,10 @@ CacheEntry write(Blob compressedLayerBlob) throws IOException { * * * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} to write out - * @return the {@link CacheEntry} representing the written entry + * @return the {@link CachedLayer} representing the written entry * @throws IOException if an I/O exception occurs */ - CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { + CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { // Creates the layers directory if it doesn't exist. Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); @@ -204,11 +204,11 @@ CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOExcepti temporaryLayerDirectory, defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); - // Updates cacheEntry with the blob information. + // Updates cachedLayer with the blob information. Path layerFile = defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); - DefaultCacheEntry.Builder cacheEntryBuilder = - DefaultCacheEntry.builder() + DefaultCachedLayer.Builder cachedLayerBuilder = + DefaultCachedLayer.builder() .setLayerDigest(writtenLayer.layerDigest) .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) @@ -219,7 +219,7 @@ CacheEntry write(UncompressedCacheWrite uncompressedCacheWrite) throws IOExcepti writeSelector(uncompressedCacheWrite.getSelector().get(), writtenLayer.layerDigest); } - return cacheEntryBuilder.build(); + return cachedLayerBuilder.build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java similarity index 75% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java index a3ea2948c3..47bd107f0a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java @@ -17,14 +17,15 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.base.Preconditions; import javax.annotation.Nullable; -/** Default implementation of {@link CacheEntry}. */ -class DefaultCacheEntry implements CacheEntry { +/** Default implementation of {@link CachedLayer}. */ +class DefaultCachedLayer implements CachedLayer { - /** Builds a {@link CacheEntry}. */ + /** Builds a {@link CachedLayer}. */ static class Builder { @Nullable private DescriptorDigest layerDigest; @@ -58,8 +59,8 @@ boolean hasLayerBlob() { return layerBlob != null; } - CacheEntry build() { - return new DefaultCacheEntry( + CachedLayer build() { + return new DefaultCachedLayer( Preconditions.checkNotNull(layerDigest, "layerDigest required"), Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), layerSize, @@ -68,7 +69,7 @@ CacheEntry build() { } /** - * Creates a new {@link Builder} for a {@link CacheEntry}. + * Creates a new {@link Builder} for a {@link CachedLayer}. * * @return the new {@link Builder} */ @@ -76,36 +77,39 @@ static Builder builder() { return new Builder(); } - private final DescriptorDigest layerDigest; private final DescriptorDigest layerDiffId; - private final long layerSize; + private final BlobDescriptor blobDescriptor; private final Blob layerBlob; - private DefaultCacheEntry( + private DefaultCachedLayer( DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, Blob layerBlob) { - this.layerDigest = layerDigest; this.layerDiffId = layerDiffId; - this.layerSize = layerSize; this.layerBlob = layerBlob; + this.blobDescriptor = new BlobDescriptor(layerSize, layerDigest); } @Override - public DescriptorDigest getLayerDigest() { - return layerDigest; + public DescriptorDigest getDigest() { + return blobDescriptor.getDigest(); } @Override - public DescriptorDigest getLayerDiffId() { + public DescriptorDigest getDiffId() { return layerDiffId; } @Override - public long getLayerSize() { - return layerSize; + public long getSize() { + return blobDescriptor.getSize(); } @Override - public Blob getLayerBlob() { + public Blob getBlob() { return layerBlob; } + + @Override + public BlobDescriptor getBlobDescriptor() { + return blobDescriptor; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java index d60f4274e2..05fccf9b89 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java @@ -56,6 +56,7 @@ public interface Layer { */ Blob getBlob() throws LayerPropertyNotFoundException; + // TODO: Remove this /** * @return the layer's content {@link BlobDescriptor} * @throws LayerPropertyNotFoundException if not available diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 3aa7df0c59..213dd510b7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.cache.CacheEntry; +import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -133,9 +133,7 @@ private ImageLayers buildFakeLayersToCache() throws ExecutionException { for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - applicationLayersBuilder.add( - BuildImageStep.cacheEntryToLayer( - NonBlockingSteps.get(buildAndCacheApplicationLayerStep))); + applicationLayersBuilder.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); } return applicationLayersBuilder.build(); @@ -170,41 +168,38 @@ public void testRun() ImmutableList extraFilesLayerEntries = fakeLayerConfigurations.get(4).getLayerEntries(); - CacheEntry dependenciesCacheEntry = + CachedLayer dependenciesCachedLayer = cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry snapshotDependenciesCacheEntry = + CachedLayer snapshotDependenciesCachedLayer = cache.retrieve(snapshotDependenciesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry resourcesCacheEntry = + CachedLayer resourcesCachedLayer = cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry classesCacheEntry = + CachedLayer classesCachedLayer = cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry extraFilesCacheEntry = + CachedLayer extraFilesCachedLayer = cache.retrieve(extraFilesLayerEntries).orElseThrow(AssertionError::new); // Verifies that the cached layers are up-to-date. Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor().getDigest(), - dependenciesCacheEntry.getLayerDigest()); + dependenciesCachedLayer.getDigest()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor().getDigest(), - snapshotDependenciesCacheEntry.getLayerDigest()); + snapshotDependenciesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(2).getBlobDescriptor().getDigest(), - resourcesCacheEntry.getLayerDigest()); + applicationLayers.get(2).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(3).getBlobDescriptor().getDigest(), - classesCacheEntry.getLayerDigest()); + applicationLayers.get(3).getBlobDescriptor().getDigest(), classesCachedLayer.getDigest()); Assert.assertEquals( applicationLayers.get(4).getBlobDescriptor().getDigest(), - extraFilesCacheEntry.getLayerDigest()); + extraFilesCachedLayer.getDigest()); // Verifies that the cache reader gets the same layers as the newest application layers. - assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCacheEntry.getLayerBlob()); - assertBlobsEqual( - applicationLayers.get(1).getBlob(), snapshotDependenciesCacheEntry.getLayerBlob()); - assertBlobsEqual(applicationLayers.get(2).getBlob(), resourcesCacheEntry.getLayerBlob()); - assertBlobsEqual(applicationLayers.get(3).getBlob(), classesCacheEntry.getLayerBlob()); - assertBlobsEqual(applicationLayers.get(4).getBlob(), extraFilesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(1).getBlob(), snapshotDependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), resourcesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(3).getBlob(), classesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(4).getBlob(), extraFilesCachedLayer.getBlob()); } @Test @@ -231,27 +226,25 @@ public void testRun_emptyLayersIgnored() ImmutableList classesLayerEntries = fakeLayerConfigurations.get(3).getLayerEntries(); - CacheEntry dependenciesCacheEntry = + CachedLayer dependenciesCachedLayer = cache.retrieve(dependenciesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry resourcesCacheEntry = + CachedLayer resourcesCachedLayer = cache.retrieve(resourcesLayerEntries).orElseThrow(AssertionError::new); - CacheEntry classesCacheEntry = + CachedLayer classesCachedLayer = cache.retrieve(classesLayerEntries).orElseThrow(AssertionError::new); // Verifies that the cached layers are up-to-date. Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor().getDigest(), - dependenciesCacheEntry.getLayerDigest()); + dependenciesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(1).getBlobDescriptor().getDigest(), - resourcesCacheEntry.getLayerDigest()); + applicationLayers.get(1).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( - applicationLayers.get(2).getBlobDescriptor().getDigest(), - classesCacheEntry.getLayerDigest()); + applicationLayers.get(2).getBlobDescriptor().getDigest(), classesCachedLayer.getDigest()); // Verifies that the cache reader gets the same layers as the newest application layers. - assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCacheEntry.getLayerBlob()); - assertBlobsEqual(applicationLayers.get(1).getBlob(), resourcesCacheEntry.getLayerBlob()); - assertBlobsEqual(applicationLayers.get(2).getBlob(), classesCacheEntry.getLayerBlob()); + assertBlobsEqual(applicationLayers.get(0).getBlob(), dependenciesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(1).getBlob(), resourcesCachedLayer.getBlob()); + assertBlobsEqual(applicationLayers.get(2).getBlob(), classesCachedLayer.getBlob()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index ef2a45f525..81f9df5638 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -17,8 +17,9 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.cache.CacheEntry; +import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -63,27 +64,32 @@ public void setUp() throws DigestException { testDescriptorDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - CacheEntry testCacheEntry = - new CacheEntry() { + CachedLayer testCachedLayer = + new CachedLayer() { @Override - public DescriptorDigest getLayerDigest() { + public DescriptorDigest getDigest() { return testDescriptorDigest; } @Override - public DescriptorDigest getLayerDiffId() { + public DescriptorDigest getDiffId() { return testDescriptorDigest; } @Override - public long getLayerSize() { + public long getSize() { return 0; } @Override - public Blob getLayerBlob() { + public Blob getBlob() { return Blobs.from("ignored"); } + + @Override + public BlobDescriptor getBlobDescriptor() { + return new BlobDescriptor(0, testDescriptorDigest); + } }; Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); @@ -123,7 +129,7 @@ public Blob getLayerBlob() { .addHistory(emptyLayerHistory) .build(); Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCacheEntry)); + .thenReturn(Futures.immediateFuture(testCachedLayer)); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) .thenReturn( Futures.immediateFuture( @@ -136,7 +142,7 @@ public Blob getLayerBlob() { Futures.immediateFuture( new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null))); Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCacheEntry)); + .thenReturn(Futures.immediateFuture(testCachedLayer)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 669827c8ce..224d4baefe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -202,28 +202,28 @@ public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorrupt } /** - * Verifies that {@code cacheEntry} corresponds to the first fake layer in {@link #setUp}. + * Verifies that {@code cachedLayer} corresponds to the first fake layer in {@link #setUp}. * - * @param cacheEntry the {@link CacheEntry} to verify + * @param cachedLayer the {@link CachedLayer} to verify * @throws IOException if an I/O exception occurs */ - private void verifyIsLayer1(CacheEntry cacheEntry) throws IOException { - Assert.assertEquals("layerBlob1", Blobs.writeToString(decompress(cacheEntry.getLayerBlob()))); - Assert.assertEquals(layerDigest1, cacheEntry.getLayerDigest()); - Assert.assertEquals(layerDiffId1, cacheEntry.getLayerDiffId()); - Assert.assertEquals(layerSize1, cacheEntry.getLayerSize()); + private void verifyIsLayer1(CachedLayer cachedLayer) throws IOException { + Assert.assertEquals("layerBlob1", Blobs.writeToString(decompress(cachedLayer.getBlob()))); + Assert.assertEquals(layerDigest1, cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId1, cachedLayer.getDiffId()); + Assert.assertEquals(layerSize1, cachedLayer.getSize()); } /** - * Verifies that {@code cacheEntry} corresponds to the second fake layer in {@link #setUp}. + * Verifies that {@code cachedLayer} corresponds to the second fake layer in {@link #setUp}. * - * @param cacheEntry the {@link CacheEntry} to verify + * @param cachedLayer the {@link CachedLayer} to verify * @throws IOException if an I/O exception occurs */ - private void verifyIsLayer2(CacheEntry cacheEntry) throws IOException { - Assert.assertEquals("layerBlob2", Blobs.writeToString(decompress(cacheEntry.getLayerBlob()))); - Assert.assertEquals(layerDigest2, cacheEntry.getLayerDigest()); - Assert.assertEquals(layerDiffId2, cacheEntry.getLayerDiffId()); - Assert.assertEquals(layerSize2, cacheEntry.getLayerSize()); + private void verifyIsLayer2(CachedLayer cachedLayer) throws IOException { + Assert.assertEquals("layerBlob2", Blobs.writeToString(decompress(cachedLayer.getBlob()))); + Assert.assertEquals(layerDigest2, cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId2, cachedLayer.getDiffId()); + Assert.assertEquals(layerSize2, cachedLayer.getSize()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java index 1fb88e6cb3..6d852d50ae 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java @@ -98,13 +98,13 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Blobs.writeToFileWithLock( layerBlob, defaultCacheStorageFiles.getLayerFile(layerDigest, layerDiffId)); - // Checks that the CacheEntry is retrieved correctly. - Optional optionalCacheEntry = defaultCacheStorageReader.retrieve(layerDigest); - Assert.assertTrue(optionalCacheEntry.isPresent()); - Assert.assertEquals(layerDigest, optionalCacheEntry.get().getLayerDigest()); - Assert.assertEquals(layerDiffId, optionalCacheEntry.get().getLayerDiffId()); - Assert.assertEquals("layerBlob".length(), optionalCacheEntry.get().getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCacheEntry.get().getLayerBlob())); + // Checks that the CachedLayer is retrieved correctly. + Optional optionalCachedLayer = defaultCacheStorageReader.retrieve(layerDigest); + Assert.assertTrue(optionalCachedLayer.isPresent()); + Assert.assertEquals(layerDigest, optionalCachedLayer.get().getDigest()); + Assert.assertEquals(layerDiffId, optionalCachedLayer.get().getDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCachedLayer.get().getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCachedLayer.get().getBlob())); // Checks that multiple .layer files means the cache is corrupted. Files.createFile(defaultCacheStorageFiles.getLayerFile(layerDigest, layerDigest)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java index 8da7aab6d9..58b1c382b0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java @@ -74,11 +74,11 @@ public void setUp() throws IOException { public void testWrite_compressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); - CacheEntry cacheEntry = + CachedLayer cachedLayer = new DefaultCacheStorageWriter(defaultCacheStorageFiles) .write(compress(uncompressedLayerBlob)); - verifyCacheEntry(cacheEntry, uncompressedLayerBlob); + verifyCachedLayer(cachedLayer, uncompressedLayerBlob); } @Test @@ -87,11 +87,11 @@ public void testWrite_uncompressed() throws IOException { DescriptorDigest layerDigest = getCompressedBlobDescriptor(uncompressedLayerBlob).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")); - CacheEntry cacheEntry = + CachedLayer cachedLayer = new DefaultCacheStorageWriter(defaultCacheStorageFiles) .write(new UncompressedCacheWrite(uncompressedLayerBlob, selector)); - verifyCacheEntry(cacheEntry, uncompressedLayerBlob); + verifyCachedLayer(cachedLayer, uncompressedLayerBlob); // Verifies that the files are present. Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); @@ -99,23 +99,23 @@ public void testWrite_uncompressed() throws IOException { Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); } - private void verifyCacheEntry(CacheEntry cacheEntry, Blob uncompressedLayerBlob) + private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(uncompressedLayerBlob); DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob); - // Verifies cacheEntry is correct. - Assert.assertEquals(layerBlobDescriptor.getDigest(), cacheEntry.getLayerDigest()); - Assert.assertEquals(layerDiffId, cacheEntry.getLayerDiffId()); - Assert.assertEquals(layerBlobDescriptor.getSize(), cacheEntry.getLayerSize()); + // Verifies cachedLayer is correct. + Assert.assertEquals(layerBlobDescriptor.getDigest(), cachedLayer.getDigest()); + Assert.assertEquals(layerDiffId, cachedLayer.getDiffId()); + Assert.assertEquals(layerBlobDescriptor.getSize(), cachedLayer.getSize()); Assert.assertArrayEquals( Blobs.writeToByteArray(uncompressedLayerBlob), - Blobs.writeToByteArray(decompress(cacheEntry.getLayerBlob()))); + Blobs.writeToByteArray(decompress(cachedLayer.getBlob()))); // Verifies that the files are present. Assert.assertTrue( Files.exists( defaultCacheStorageFiles.getLayerFile( - cacheEntry.getLayerDigest(), cacheEntry.getLayerDiffId()))); + cachedLayer.getDigest(), cachedLayer.getDiffId()))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java similarity index 70% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java index 893868a1b7..1565b6cd1f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheEntryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java @@ -26,9 +26,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DefaultCacheEntry}. */ +/** Tests for {@link DefaultCachedLayer}. */ @RunWith(MockitoJUnitRunner.class) -public class DefaultCacheEntryTest { +public class DefaultCachedLayerTest { @Mock private DescriptorDigest mockLayerDigest; @Mock private DescriptorDigest mockLayerDiffId; @@ -36,7 +36,7 @@ public class DefaultCacheEntryTest { @Test public void testBuilder_fail() { try { - DefaultCacheEntry.builder().build(); + DefaultCachedLayer.builder().build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -44,7 +44,7 @@ public void testBuilder_fail() { } try { - DefaultCacheEntry.builder().setLayerDigest(mockLayerDigest).build(); + DefaultCachedLayer.builder().setLayerDigest(mockLayerDigest).build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -52,7 +52,7 @@ public void testBuilder_fail() { } try { - DefaultCacheEntry.builder() + DefaultCachedLayer.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .build(); @@ -65,18 +65,18 @@ public void testBuilder_fail() { @Test public void testBuilder_pass() throws IOException { - DefaultCacheEntry.Builder cacheEntryBuilder = - DefaultCacheEntry.builder() + DefaultCachedLayer.Builder cachedLayerBuilder = + DefaultCachedLayer.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .setLayerSize(1337); - Assert.assertFalse(cacheEntryBuilder.hasLayerBlob()); - cacheEntryBuilder.setLayerBlob(Blobs.from("layerBlob")); - Assert.assertTrue(cacheEntryBuilder.hasLayerBlob()); - CacheEntry cacheEntry = cacheEntryBuilder.build(); - Assert.assertEquals(mockLayerDigest, cacheEntry.getLayerDigest()); - Assert.assertEquals(mockLayerDiffId, cacheEntry.getLayerDiffId()); - Assert.assertEquals(1337, cacheEntry.getLayerSize()); - Assert.assertEquals("layerBlob", Blobs.writeToString(cacheEntry.getLayerBlob())); + Assert.assertFalse(cachedLayerBuilder.hasLayerBlob()); + cachedLayerBuilder.setLayerBlob(Blobs.from("layerBlob")); + Assert.assertTrue(cachedLayerBuilder.hasLayerBlob()); + CachedLayer cachedLayer = cachedLayerBuilder.build(); + Assert.assertEquals(mockLayerDigest, cachedLayer.getDigest()); + Assert.assertEquals(mockLayerDiffId, cachedLayer.getDiffId()); + Assert.assertEquals(1337, cachedLayer.getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(cachedLayer.getBlob())); } } From 37ed046247c85ed6e91f4732a0fd688080139de9 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 22 Oct 2018 17:13:02 -0400 Subject: [PATCH 0312/2020] Add Travis build configuration (#1120) * Unset _JAVA_OPTIONS to avoid `Picked up _JAVA_OPTIONS: ...` message * Narrow gradle cache as per Gradle recommendations --- .travis.yml | 31 +++++++++++++++++++++++++++++++ build.sh | 7 +++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..145fc2dcc2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +sudo: false +git: + depth: 3 +language: java +# using `openjdk8` leads to javadoc errors +jdk: oraclejdk8 +env: + global: + - TERM=dumb # for gradle + +install: true +script: +- (cd jib-core; ./gradlew --stacktrace build) +- (cd jib-plugins-common; ./gradlew --stacktrace build) +- (cd jib-gradle-plugin; ./gradlew --stacktrace build) +# unset _JAVA_OPTIONS to avoid spurious test failures +- (cd jib-maven-plugin; unset _JAVA_OPTIONS; ./mvnw -B verify -U) + +# cobbled together from +# https://docs.travis-ci.com/user/languages/java/#projects-using-gradle +# https://github.com/blox/blox/pull/338#issuecomment-358556137 +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + - rm -fr $HOME/.gradle/caches/*/fileHashes/ + - rm -fr $HOME/.gradle/caches/jars-3/ +cache: + directories: + - $HOME/.m2 + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ diff --git a/build.sh b/build.sh index 00fee12d02..05c8bd46cd 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,7 @@ quickMode=false mavenOptions="" gradleOptions="" +dryRun="" usage() { @@ -13,6 +14,7 @@ usage() echo " providing no target is the same as 'core plugins gradle maven'" echo " -q quick mode: skip tests, formatting" echo " -e show error information (mvn: -e, gradle: --stacktrace --info)" + echo " -n dry run: show what will be performed" exit 1 } @@ -20,15 +22,16 @@ usage() # $1 = directory # $2... = build command doBuild() { - (directory="$1"; shift; echo ">>> (cd $directory; $*)"; cd "$directory" && eval '"$@"') + (directory="$1"; shift; echo ">>> (cd $directory; $*)"; [ -z "$dryRun" ] && cd "$directory" && eval '"$@"') } -while getopts qe c; do +while getopts qen c; do case $c in q) quickMode=true;; e) mavenOptions="$mavenOptions -e" gradleOptions="$gradleOptions --stacktrace --info" ;; + n) dryRun=true;; \?) usage;; esac done From 90a0b66dfa51c73ee93aaa738efcd3bc55709399 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 23 Oct 2018 16:09:47 -0400 Subject: [PATCH 0313/2020] Update gradle extraDirectory config parameter to include path and permissions field (#1178) --- jib-gradle-plugin/CHANGELOG.md | 5 +- .../gradle/SingleProjectIntegrationTest.java | 12 +-- .../projects/simple/complex-build.gradle | 5 +- .../src/main/java/com/test/HelloWorld.java | 5 ++ .../tools/jib/gradle/BuildDockerTask.java | 6 +- .../tools/jib/gradle/BuildImageTask.java | 6 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 9 +- .../tools/jib/gradle/DockerContextTask.java | 9 +- .../jib/gradle/ExtraDirectoryParameters.java | 88 +++++++++++++++++++ .../cloud/tools/jib/gradle/FilesTask.java | 4 +- .../jib/gradle/GradleLayerConfigurations.java | 36 ++++++-- .../jib/gradle/GradleProjectProperties.java | 15 +++- .../cloud/tools/jib/gradle/JibExtension.java | 58 ++++++------ .../jib/gradle/DockerContextTaskTest.java | 14 +-- .../gradle/GradleLayerConfigurationsTest.java | 32 +++++-- .../tools/jib/gradle/JibExtensionTest.java | 26 +++++- .../complex-service/build.gradle | 4 +- .../resources/projects/simple/build.gradle | 4 +- .../jib/maven/JibPluginConfiguration.java | 2 +- .../jib/maven/MavenLayerConfigurations.java | 5 +- .../ConfigurationPropertyValidator.java | 21 +++++ .../common/JavaLayerConfigurationsHelper.java | 24 ++++- .../jib/plugins/common/PropertyNames.java | 3 +- .../ConfigurationPropertyValidatorTest.java | 22 +++++ .../JavaLayerConfigurationsHelperTest.java | 15 ++-- 25 files changed, 349 insertions(+), 81 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 86fc318812..891464b9a8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,7 +9,10 @@ All notable changes to this project will be documented in this file. - `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) - Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) - +- `jib.extraDirectory` closure with a `path` and `permissions` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) + - `jib.extraDirectory.path` configures the extra layer directory (still also configurable via `jib.extraDirectory = file(...)`) + - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string) + ### Changed - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index ffa4e27dbe..e530fdb925 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -142,7 +142,7 @@ public void testBuild_simple() throws IOException, InterruptedException { Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", JibRunHelper.buildAndRun(simpleTestProject, targetImage)); assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); @@ -153,7 +153,7 @@ public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -163,7 +163,7 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); } @@ -173,7 +173,7 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertDockerInspect(targetImage); @@ -201,7 +201,7 @@ public void testDockerContext() throws IOException, InterruptedException { assertDockerInspect(imageName); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", new Command("docker", "run", "--rm", imageName).run()); // Checks that generating the Docker context again is skipped. @@ -249,7 +249,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { new Command("docker", "load", "--input", outputPath).run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", new Command("docker", "run", "--rm", targetImage).run()); assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index 0fd21b3dd4..b12d6940fd 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -38,8 +38,11 @@ jib { ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } + extraDirectory { + path = file('src/main/custom-extra-dir') + permissions = ['/foo':'755', '/bar/cat':'777'] + } allowInsecureRegistries = true - extraDirectory = file('src/main/custom-extra-dir') // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 733648707f..cf30f12cdf 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; /** Example class that uses a dependency and a resource file. */ public class HelloWorld { @@ -41,6 +42,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { // Prints the contents of the extra files. if (Files.exists(Paths.get("/foo"))) { + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); System.out.println(new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 22bda0354e..6bac5250e5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -84,7 +84,11 @@ public void buildDocker() AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index aa2a1b6f32..26cb572ec1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -82,7 +82,11 @@ public void buildImage() AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 72cb89f03d..053fdaf7fc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -78,7 +78,8 @@ public void setTargetImage(String targetImage) { @InputFiles public FileCollection getInputFiles() { return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectoryPath().toFile(), getProject()); + Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPath().toFile(), + getProject()); } /** @@ -109,7 +110,11 @@ public void buildTar() AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 2186c78b38..538ebbfbe6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -61,7 +61,8 @@ public JibExtension getJib() { @InputFiles public FileCollection getInputFiles() { return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectoryPath().toFile(), getProject()); + Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPath().toFile(), + getProject()); } /** @@ -110,7 +111,11 @@ public void generateDockerContext() { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); GradleProjectProperties gradleProjectProperties = GradleProjectProperties.getForProject( - getProject(), getLogger(), jibExtension.getExtraDirectoryPath(), appRoot); + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); String targetDir = getTargetDir(); List entrypoint = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java new file mode 100644 index 0000000000..8ff93b188f --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import javax.inject.Inject; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Object in {@link JibExtension} that configures the extra directory. */ +public class ExtraDirectoryParameters { + + private static Path resolveDefaultExtraDirectory(Path projectDirectory) { + return projectDirectory.resolve("src").resolve("main").resolve("jib"); + } + + private Path path; + private Map permissions = Collections.emptyMap(); + + @Inject + public ExtraDirectoryParameters(Path projectDirectory) { + path = resolveDefaultExtraDirectory(projectDirectory); + } + + @Input + public String getPathString() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { + return System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + } + return path.toString(); + } + + @Internal + public Path getPath() { + // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a + // String to make them go away. + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { + return Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH)); + } + return path; + } + + public void setPath(File path) { + this.path = path.toPath(); + } + + /** + * Gets the permissions for files in the extra layer on the container. Maps from absolute path on + * the container to a 3-digit octal string representation of the file permission bits (e.g. {@code + * "/path/on/container" -> "755"}). + * + * @return the permissions map from path on container to file permissions + */ + @Input + public Map getPermissions() { + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS)); + } + return permissions; + } + + public void setPermissions(Map permissions) { + this.permissions = permissions; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java index 9a4e5347c1..fde1cf0456 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java @@ -149,8 +149,8 @@ public void listFiles() { printProjectFiles(project); // Print extra layer - if (Files.exists(jibExtension.getExtraDirectoryPath())) { - System.out.println(jibExtension.getExtraDirectoryPath()); + if (Files.exists(jibExtension.getExtraDirectory().getPath())) { + System.out.println(jibExtension.getExtraDirectory().getPath()); } // Find project dependencies diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 1f238f5df5..b508435784 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; @@ -26,6 +27,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; @@ -44,18 +46,23 @@ class GradleLayerConfigurations { * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution * @param extraDirectory path to the source directory for the extra files layer + * @param permissions map from path on container to file permissions for extra-layer files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ static JavaLayerConfigurations getForProject( - Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + Project project, + Logger logger, + Path extraDirectory, + Map permissions, + AbsoluteUnixPath appRoot) throws IOException { if (GradleProjectProperties.getWarTask(project) != null) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); - return getForWarProject(project, logger, extraDirectory, appRoot); + return getForWarProject(project, extraDirectory, permissions, appRoot); } else { - return getForNonWarProject(project, logger, extraDirectory, appRoot); + return getForNonWarProject(project, logger, extraDirectory, permissions, appRoot); } } @@ -65,12 +72,17 @@ static JavaLayerConfigurations getForProject( * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution * @param extraDirectory path to the source directory for the extra files layer + * @param permissions map from path on container to file permissions for extra-layer files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ private static JavaLayerConfigurations getForNonWarProject( - Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + Project project, + Logger logger, + Path extraDirectory, + Map permissions, + AbsoluteUnixPath appRoot) throws IOException { AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); @@ -127,7 +139,11 @@ private static JavaLayerConfigurations getForNonWarProject( // Adds all the extra files. if (Files.exists(extraDirectory)) { layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, extraDirectory, path -> true, AbsoluteUnixPath.get("/")); + LayerType.EXTRA_FILES, + extraDirectory, + path -> true, + AbsoluteUnixPath.get("/"), + permissions); } return layerBuilder.build(); @@ -137,17 +153,21 @@ private static JavaLayerConfigurations getForNonWarProject( * Resolves the {@link JavaLayerConfigurations} for a WAR Gradle {@link Project}. * * @param project the Gradle {@link Project} - * @param logger the build logger for providing feedback about the resolution * @param extraDirectory path to the source directory for the extra files layer + * @param permissions map from path on container to file permissions for extra-layer files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution */ private static JavaLayerConfigurations getForWarProject( - Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) + Project project, + Path extraDirectory, + Map permissions, + AbsoluteUnixPath appRoot) throws IOException { Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); - return JavaLayerConfigurationsHelper.fromExplodedWar(explodedWarPath, appRoot, extraDirectory); + return JavaLayerConfigurationsHelper.fromExplodedWar( + explodedWarPath, appRoot, extraDirectory, permissions); } private GradleLayerConfigurations() {} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index fa022d22a7..8512629205 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; @@ -32,6 +33,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.GradleException; @@ -57,12 +59,21 @@ class GradleProjectProperties implements ProjectProperties { /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, Logger logger, Path extraDirectory, AbsoluteUnixPath appRoot) { + Project project, + Logger logger, + Path extraDirectory, + Map permissions, + AbsoluteUnixPath appRoot) { try { return new GradleProjectProperties( project, makeEventHandlers(logger), - GradleLayerConfigurations.getForProject(project, logger, extraDirectory, appRoot)); + GradleLayerConfigurations.getForProject( + project, + logger, + extraDirectory, + ConfigurationPropertyValidator.convertPermissionsMap(permissions), + appRoot)); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 8d37d461c9..2a68dea65f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -18,14 +18,11 @@ import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; @@ -52,6 +49,15 @@ * format = OCI * appRoot = "/app"; * } + * extraDirectory { + * path = file('path/to/extra/dir') + * permissions = [ + * '/path/on/container/file1': 744, + * '/path/on/container/file2': 123 + * ] + * } + * allowInsecureRegistries = false + * useOnlyProjectCache = false * } * } */ @@ -61,16 +67,13 @@ public class JibExtension { private static final boolean DEFAULT_USE_ONLY_PROJECT_CACHE = false; private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; - private static Path resolveDefaultExtraDirectory(Path projectDirectory) { - return projectDirectory.resolve("src").resolve("main").resolve("jib"); - } - private final BaseImageParameters from; private final TargetImageParameters to; private final ContainerParameters container; + private final ExtraDirectoryParameters extraDirectory; + private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; - private final Property extraDirectory; public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); @@ -78,15 +81,15 @@ public JibExtension(Project project) { from = objectFactory.newInstance(BaseImageParameters.class, "jib.from"); to = objectFactory.newInstance(TargetImageParameters.class, "jib.to"); container = objectFactory.newInstance(ContainerParameters.class); + extraDirectory = + objectFactory.newInstance(ExtraDirectoryParameters.class, project.getProjectDir().toPath()); useOnlyProjectCache = objectFactory.property(Boolean.class); allowInsecureRegistries = objectFactory.property(Boolean.class); - extraDirectory = objectFactory.property(Path.class); // Sets defaults. useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); - extraDirectory.set(resolveDefaultExtraDirectory(project.getProjectDir().toPath())); } public void from(Action action) { @@ -101,12 +104,16 @@ public void container(Action action) { action.execute(container); } - public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { - this.allowInsecureRegistries.set(allowInsecureRegistries); + public void extraDirectory(Action action) { + action.execute(extraDirectory); } public void setExtraDirectory(File extraDirectory) { - this.extraDirectory.set(extraDirectory.toPath()); + this.extraDirectory.setPath(extraDirectory); + } + + public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { + this.allowInsecureRegistries.set(allowInsecureRegistries); } void setUseOnlyProjectCache(boolean useOnlyProjectCache) { @@ -131,6 +138,12 @@ public ContainerParameters getContainer() { return container; } + @Nested + @Optional + public ExtraDirectoryParameters getExtraDirectory() { + return extraDirectory; + } + @Input @Optional boolean getUseOnlyProjectCache() { @@ -148,23 +161,4 @@ boolean getAllowInsecureRegistries() { } return allowInsecureRegistries.get(); } - - @Input - String getExtraDirectory() { - // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a - // String to make them go away. - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY) != null) { - return System.getProperty(PropertyNames.EXTRA_DIRECTORY); - } - return extraDirectory.get().toString(); - } - - @Internal - Path getExtraDirectoryPath() { - // TODO: Should inform user about nonexistent directory if using custom directory. - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY) != null) { - return Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY)); - } - return extraDirectory.get(); - } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index edbd43ce22..a768010fa7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -43,6 +43,7 @@ public class DockerContextTaskTest { @Mock private ContainerParameters containerParameters; @Mock private BaseImageParameters baseImageParameters; + @Mock private ExtraDirectoryParameters extraDirectoryParameters; private DockerContextTask task; private Project project; @@ -57,14 +58,17 @@ public void setUp() throws IOException { Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); Mockito.when(containerParameters.getEnvironment()) .thenReturn(ImmutableMap.of("envKey", "envVal")); - Mockito.when(jibExtension.getExtraDirectoryPath()) - .thenReturn(projectRoot.newFolder("src", "main", "jib").toPath()); - Mockito.when(jibExtension.getContainer().getMainClass()).thenReturn("MainClass"); - Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); - Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); + Mockito.when(containerParameters.getMainClass()).thenReturn("MainClass"); Mockito.when(containerParameters.getAppRoot()).thenReturn("/app"); Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); + Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); + Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); + + Mockito.when(jibExtension.getExtraDirectory()).thenReturn(extraDirectoryParameters); + Mockito.when(extraDirectoryParameters.getPath()) + .thenReturn(projectRoot.newFolder("src", "main", "jib").toPath()); + project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); project.getPluginManager().apply("java"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 2104b155f7..d0de719443 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -29,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -176,7 +177,11 @@ public void test_correctFiles() throws URISyntaxException, IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockLogger, Paths.get("nonexistent/path"), appRoot); + mockProject, + mockLogger, + Paths.get("nonexistent/path"), + Collections.emptyMap(), + appRoot); assertSourcePathsUnordered( expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); assertSourcePathsUnordered( @@ -198,7 +203,7 @@ public void test_noClassesFiles() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); GradleLayerConfigurations.getForProject( - mockProject, mockLogger, Paths.get("nonexistent/path"), appRoot); + mockProject, mockLogger, Paths.get("nonexistent/path"), Collections.emptyMap(), appRoot); Mockito.verify(mockLogger) .info("Adding corresponding output directories of source sets to image"); @@ -210,7 +215,11 @@ public void test_noClassesFiles() throws IOException { public void test_extraFiles() throws IOException { JavaLayerConfigurations javaLayerConfigurations = GradleLayerConfigurations.getForProject( - mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/app")); + mockProject, + mockLogger, + extraFilesDirectory, + Collections.emptyMap(), + AbsoluteUnixPath.get("/app")); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -229,7 +238,11 @@ public void test_extraFiles() throws IOException { public void testGetForProject_nonDefaultAppRoot() throws IOException { JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( - mockProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); + mockProject, + mockLogger, + extraFilesDirectory, + Collections.emptyMap(), + AbsoluteUnixPath.get("/my/app")); assertExtractionPathsUnordered( Arrays.asList( @@ -261,6 +274,7 @@ public void testGetForProject_defaultAppRoot() throws IOException { mockProject, mockLogger, extraFilesDirectory, + Collections.emptyMap(), AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_APP_ROOT)); assertExtractionPathsUnordered( @@ -289,7 +303,11 @@ public void testWebApp() throws URISyntaxException, IOException { JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( - mockWebAppProject, mockLogger, extraFilesDirectory, AbsoluteUnixPath.get("/my/app")); + mockWebAppProject, + mockLogger, + extraFilesDirectory, + Collections.emptyMap(), + AbsoluteUnixPath.get("/my/app")); ImmutableList expectedDependenciesFiles = ImmutableList.of( webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); @@ -371,6 +389,7 @@ public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOExceptio mockWebAppProject, mockLogger, extraFilesDirectory, + Collections.emptyMap(), AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); assertExtractionPathsUnordered( @@ -413,6 +432,7 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOE mockWebAppProject, mockLogger, extraFilesDirectory, + Collections.emptyMap(), AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } @@ -425,6 +445,7 @@ public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOExcep mockWebAppProject, mockLogger, extraFilesDirectory, + Collections.emptyMap(), AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } @@ -437,6 +458,7 @@ public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOExceptio mockWebAppProject, mockLogger, extraFilesDirectory, + Collections.emptyMap(), AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 313dc56cb6..f95f080387 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import org.gradle.api.Project; @@ -33,6 +34,7 @@ public class JibExtensionTest { private JibExtension testJibExtension; + private Project fakeProject; private static void clearProperties() { System.clearProperty("jib.from.image"); @@ -51,12 +53,14 @@ private static void clearProperties() { System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); System.clearProperty("jib.container.user"); + System.clearProperty("jib.extraDirectory.path"); + System.clearProperty("jib.extraDirectory.permissions"); } @Before public void setUp() { clearProperties(); - Project fakeProject = ProjectBuilder.builder().build(); + fakeProject = ProjectBuilder.builder().build(); testJibExtension = fakeProject .getExtensions() @@ -141,6 +145,26 @@ public void testContainer() { Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); } + @Test + public void testExtraDirectory() { + Assert.assertEquals( + fakeProject.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib"), + testJibExtension.getExtraDirectory().getPath()); + Assert.assertEquals( + Collections.emptyMap(), testJibExtension.getExtraDirectory().getPermissions()); + + testJibExtension.extraDirectory( + extraDirectory -> { + extraDirectory.setPath(Paths.get("test").resolve("path").toFile()); + extraDirectory.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); + }); + Assert.assertEquals( + Paths.get("test").resolve("path"), testJibExtension.getExtraDirectory().getPath()); + Assert.assertEquals( + ImmutableMap.of("file1", "123", "file2", "456"), + testJibExtension.getExtraDirectory().getPermissions()); + } + @Test public void testProperties() { System.setProperty("jib.from.image", "fromImage"); diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle index 28762e824c..92ca5a3e66 100644 --- a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle +++ b/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle @@ -17,7 +17,9 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") } - extraDirectory = file('src/main/other-jib') + extraDirectory { + path = file('src/main/other-jib') + } } sourceSets { diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle index 4832a01f69..52d0f7cab5 100644 --- a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle @@ -18,5 +18,7 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") } - extraDirectory = file('src/main/custom-extra-dir') + extraDirectory { + path = file('src/main/custom-extra-dir') + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index bfe73996c3..21ff0a7484 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -174,7 +174,7 @@ public static class ContainerParameters { @Parameter( defaultValue = "${project.basedir}/src/main/jib", required = true, - property = PropertyNames.EXTRA_DIRECTORY) + property = PropertyNames.EXTRA_DIRECTORY_PATH) private File extraDirectory; @Parameter(defaultValue = "false", property = PropertyNames.SKIP) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index 91f57da0fc..d6b4904120 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.function.Predicate; import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; @@ -119,7 +120,9 @@ private static JavaLayerConfigurations getForWarProject( // at build.getFinalName(). Path explodedWarPath = Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - return JavaLayerConfigurationsHelper.fromExplodedWar(explodedWarPath, appRoot, extraDirectory); + // TODO: Replace Collections.emptyMap() with configured permissions + return JavaLayerConfigurationsHelper.fromExplodedWar( + explodedWarPath, appRoot, extraDirectory, Collections.emptyMap()); } private MavenLayerConfigurations() {} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 708a2dfce1..31ecab43b5 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -16,9 +16,11 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -29,6 +31,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -184,5 +187,23 @@ public static List parseListProperty(String property) { return ImmutableList.copyOf(items); } + /** + * Validates and converts a {@code String->String} file-path-to-file-permissions map to an + * equivalent {@code AbsoluteUnixPath->FilePermission} map. + * + * @param inputMap the map to convert + * @return the converted map + */ + public static Map convertPermissionsMap( + Map inputMap) { + ImmutableMap.Builder permissionsMap = ImmutableMap.builder(); + for (Entry entry : inputMap.entrySet()) { + AbsoluteUnixPath key = AbsoluteUnixPath.get(entry.getKey()); + FilePermissions value = FilePermissions.fromOctalString(entry.getValue()); + permissionsMap.put(key, value); + } + return permissionsMap.build(); + } + private ConfigurationPropertyValidator() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java index ab9238c9c0..1c398790ac 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; @@ -23,13 +24,28 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import java.util.function.Predicate; /** Helper for constructing {@link JavaLayerConfigurations}. */ public class JavaLayerConfigurationsHelper { + /** + * Constructs a new {@link JavaLayerConfigurations} for a WAR project. + * + * @param explodedWar the exploded WAR directory + * @param appRoot root directory in the image where the app will be placed + * @param extraFilesDirectory path to the source directory for the extra files layer + * @param permissions map from path on container to file permissions + * @return {@link JavaLayerConfigurations} for the layers for the exploded WAR + * @throws IOException if adding layer contents fails + */ public static JavaLayerConfigurations fromExplodedWar( - Path explodedWar, AbsoluteUnixPath appRoot, Path extraFilesDirectory) throws IOException { + Path explodedWar, + AbsoluteUnixPath appRoot, + Path extraFilesDirectory, + Map permissions) + throws IOException { Path webInfLib = explodedWar.resolve("WEB-INF/lib"); Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); @@ -66,7 +82,11 @@ public static JavaLayerConfigurations fromExplodedWar( // Adds all the extra files. if (Files.exists(extraFilesDirectory)) { layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, extraFilesDirectory, path -> true, AbsoluteUnixPath.get("/")); + LayerType.EXTRA_FILES, + extraFilesDirectory, + path -> true, + AbsoluteUnixPath.get("/"), + permissions); } return layerBuilder.build(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 85bd87c3c1..3c8876f1d9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -41,6 +41,7 @@ public class PropertyNames { public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; - public static final String EXTRA_DIRECTORY = "jib.extraDirectory"; + public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; + public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; public static final String SKIP = "jib.skip"; } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index e539f21e73..2a7f2a135d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -16,9 +16,11 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableList; @@ -172,4 +174,24 @@ public void testParseMapProperty() { // pass } } + + @Test + public void testConvertPermissionsMap() { + Assert.assertEquals( + ImmutableMap.of( + AbsoluteUnixPath.get("/test/folder/file1"), + FilePermissions.fromOctalString("123"), + AbsoluteUnixPath.get("/test/file2"), + FilePermissions.fromOctalString("456")), + ConfigurationPropertyValidator.convertPermissionsMap( + ImmutableMap.of("/test/folder/file1", "123", "/test/file2", "456"))); + + try { + ConfigurationPropertyValidator.convertPermissionsMap( + ImmutableMap.of("a path", "not valid permission")); + Assert.fail(); + } catch (IllegalArgumentException ignored) { + // pass + } + } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java index 46b16bee57..3ed758c562 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java @@ -26,6 +26,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @@ -61,13 +62,17 @@ public void testFromExplodedWar() throws URISyntaxException, IOException { JavaLayerConfigurations configuration = JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWar, AbsoluteUnixPath.get("/my/app"), extraFilesDirectory); + explodedWar, + AbsoluteUnixPath.get("/my/app"), + extraFilesDirectory, + Collections.emptyMap()); assertSourcePathsUnordered( - Arrays.asList(explodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), + Collections.singletonList(explodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), configuration.getDependencyLayerEntries()); assertSourcePathsUnordered( - Arrays.asList(explodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + Collections.singletonList( + explodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), configuration.getSnapshotDependencyLayerEntries()); assertSourcePathsUnordered( Arrays.asList( @@ -100,10 +105,10 @@ public void testFromExplodedWar() throws URISyntaxException, IOException { configuration.getExtraFilesLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( From 9375a24168492827c68055e15cdc9f478772161a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 24 Oct 2018 12:02:29 -0400 Subject: [PATCH 0314/2020] Ignore file permissions in docker context integration test, add warning (#1187) --- .../tools/jib/gradle/SingleProjectIntegrationTest.java | 6 +++--- .../google/cloud/tools/jib/gradle/DockerContextTask.java | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index e530fdb925..e24e5ace5a 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -200,9 +200,9 @@ public void testDockerContext() throws IOException, InterruptedException { .run(); assertDockerInspect(imageName); - Assert.assertEquals( - "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", - new Command("docker", "run", "--rm", imageName).run()); + String output = new Command("docker", "run", "--rm", imageName).run(); + Assert.assertThat(output, CoreMatchers.startsWith("Hello, world. An argument.\n")); + Assert.assertThat(output, CoreMatchers.endsWith("foo\ncat\n")); // Checks that generating the Docker context again is skipped. BuildTask upToDateJibDockerContextTask = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 538ebbfbe6..3e21fcdd47 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -105,6 +105,13 @@ public void generateDockerContext() { Preconditions.checkNotNull(jibExtension.getFrom().getImage()); JibSystemProperties.checkHttpTimeoutProperty(); + if (!jibExtension.getExtraDirectory().getPermissions().isEmpty()) { + getLogger() + .warn( + "'jib.extraDirectory.permissions' configuration is not supported by Jib Docker " + + "context generator - building using Docker may produce unexpected results."); + } + // TODO: Instead of disabling logging, have authentication credentials be provided PluginConfigurationProcessor.disableHttpLogging(); From fb6f963a5dd9e4bd3ed8e9848936c599bf0297d4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 24 Oct 2018 13:49:11 -0400 Subject: [PATCH 0315/2020] Add file permissions to cache selector (#1182) --- .../tools/jib/cache/LayerEntriesSelector.java | 16 ++++-- .../jib/configuration/FilePermissions.java | 11 ++++- .../jib/cache/LayerEntriesSelectorTest.java | 49 ++++++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index 51bf45049d..4ae0c93ff0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -43,11 +43,13 @@ * "sourceFile": "source/file/for/layer/entry/1", * "extractionPath": "/extraction/path/for/layer/entry/1" * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" + * "permissions": "777" * }, * { * "sourceFile": "source/file/for/layer/entry/2", * "extractionPath": "/extraction/path/for/layer/entry/2" * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" + * "permissions": "777" * } * ] * } @@ -61,12 +63,14 @@ static class LayerEntryTemplate implements JsonTemplate, Comparable Date: Wed, 24 Oct 2018 16:58:29 -0400 Subject: [PATCH 0316/2020] Remove LayerEntry#getAbsolute...String methods (#1190) --- .../tools/jib/cache/LayerEntriesSelector.java | 4 +-- .../frontend/JavaDockerContextGenerator.java | 2 +- .../cloud/tools/jib/image/LayerEntry.java | 29 ++----------------- .../jib/image/ReproducibleLayerBuilder.java | 2 +- .../frontend/JavaLayerConfigurationsTest.java | 2 +- .../gradle/GradleLayerConfigurationsTest.java | 2 +- .../maven/MavenLayerConfigurationsTest.java | 2 +- .../JavaLayerConfigurationsHelperTest.java | 2 +- 8 files changed, 11 insertions(+), 34 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index 4ae0c93ff0..b47011fff9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -67,8 +67,8 @@ static class LayerEntryTemplate implements JsonTemplate, ComparableDo not call {@link Path#toString} on this - use {@link #getAbsoluteSourceFileString} - * instead. This path can be relative or absolute, but {@link #getAbsoluteSourceFileString} can - * only be absolute. Callers should rely on {@link #getAbsoluteSourceFileString} for the - * serialized form since the serialization could change independently of the path representation. + * Gets the source file. The source file may be relative or absolute, so the caller should use + * {@code getSourceFile().toAbsolutePath().toString()} for the serialized form since the + * serialization could change independently of the path representation. * * @return the source file */ @@ -103,26 +100,6 @@ public FilePermissions getPermissions() { return permissions; } - // TODO: Remove these get...String methods. - /** - * Get the source file as an absolute path in Unix form. The path is made absolute first, if not - * already absolute. - * - * @return the source file path - */ - public String getAbsoluteSourceFileString() { - return AbsoluteUnixPath.fromPath(sourceFile.toAbsolutePath()).toString(); - } - - /** - * Gets the extraction path as an absolute path in Unix form. - * - * @return the extraction path - */ - public String getAbsoluteExtractionPathString() { - return extractionPath.toString(); - } - @Override public boolean equals(Object other) { if (this == other) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 05f760cd41..e436583ccf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -102,7 +102,7 @@ public Blob build() { // adds parent directories for each extraction path. TarArchiveEntry entry = new TarArchiveEntry( - layerEntry.getSourceFile().toFile(), layerEntry.getAbsoluteExtractionPathString()); + layerEntry.getSourceFile().toFile(), layerEntry.getExtractionPath().toString()); // Sets the entry's permissions by masking out the permission bits from the entry's mode (the // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 34f57e0c11..636b599c36 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -47,7 +47,7 @@ private static void assertSourcePathsUnordered( private static void assertExtractionPathsUnordered( List expectedPaths, List entries) { assertLayerEntriesUnordered( - expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } private static JavaLayerConfigurations createFakeConfigurations() { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index d0de719443..aaeffc6802 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -96,7 +96,7 @@ private static void assertSourcePathsUnordered( private static void assertExtractionPathsUnordered( List expectedPaths, List entries) { assertLayerEntriesUnordered( - expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index ffa786e1a5..130518b695 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -64,7 +64,7 @@ private static void assertSourcePathsUnordered( private static void assertExtractionPathsUnordered( List expectedPaths, List entries) { assertLayerEntriesUnordered( - expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuration) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java index 3ed758c562..54a3652f99 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java @@ -51,7 +51,7 @@ private static void assertSourcePathsUnordered( private static void assertExtractionPathsUnordered( List expectedPaths, List entries) { assertLayerEntriesUnordered( - expectedPaths, entries, LayerEntry::getAbsoluteExtractionPathString); + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } @Test From 792c5c068ab72335337790ec6f48b24eeff49c7d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 25 Oct 2018 11:20:52 -0400 Subject: [PATCH 0317/2020] Remove dependence of filesTask on classes and assembly (#1189) --- .../main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index d71e23d0ef..732ec900d1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -152,7 +152,6 @@ public void apply(Project project) { dockerContextTask.dependsOn(dependsOnTask); buildDockerTask.dependsOn(dependsOnTask); buildTarTask.dependsOn(dependsOnTask); - filesTask.dependsOn(dependsOnTask); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble @@ -170,7 +169,6 @@ public void apply(Project project) { dockerContextTask.dependsOn(assembleTask); buildDockerTask.dependsOn(assembleTask); buildTarTask.dependsOn(assembleTask); - filesTask.dependsOn(assembleTask); }); } } catch (UnknownTaskException ex) { From 0174a49e09c71ca7829814f96a2b9681fc3c323a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 29 Oct 2018 14:31:34 -0400 Subject: [PATCH 0318/2020] Update maven extraDirectory config parameter to include path and permissions field (#1195) --- .../jib/gradle/GradleLayerConfigurations.java | 24 ++-- .../jib/gradle/GradleProjectProperties.java | 29 ++++- .../gradle/GradleProjectPropertiesTest.java | 22 ++++ jib-maven-plugin/CHANGELOG.md | 3 + .../tools/jib/maven/BuildDockerMojo.java | 7 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 7 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 7 +- .../tools/jib/maven/DockerContextMojo.java | 7 +- .../jib/maven/JibPluginConfiguration.java | 105 ++++++++++++++---- .../jib/maven/MavenLayerConfigurations.java | 40 +++++-- .../jib/maven/MavenProjectProperties.java | 11 +- .../maven/PluginConfigurationProcessor.java | 49 ++++++++ .../tools/jib/maven/skaffold/FilesMojo.java | 23 +++- .../maven/BuildDockerMojoIntegrationTest.java | 2 +- .../maven/BuildImageMojoIntegrationTest.java | 6 +- .../maven/BuildTarMojoIntegrationTest.java | 2 +- .../DockerContextMojoIntegrationTest.java | 6 +- .../jib/maven/DockerContextMojoTest.java | 6 +- .../jib/maven/JibPluginConfigurationTest.java | 16 +++ .../maven/MavenLayerConfigurationsTest.java | 24 ++-- .../PluginConfigurationProcessorTest.java | 25 +++++ .../jib/maven/skaffold/FilesMojoTest.java | 2 +- .../resources/projects/simple/pom-complex.xml | 13 +++ .../test/resources/projects/simple/pom.xml | 1 + .../src/main/java/com/test/HelloWorld.java | 5 + .../src/main/{jib => jib-custom}/bar/cat | 0 .../simple/src/main/{jib => jib-custom}/foo | 0 .../ConfigurationPropertyValidator.java | 21 ---- .../common/JavaLayerConfigurationsHelper.java | 6 +- .../ConfigurationPropertyValidatorTest.java | 22 ---- 30 files changed, 370 insertions(+), 121 deletions(-) rename jib-maven-plugin/src/test/resources/projects/simple/src/main/{jib => jib-custom}/bar/cat (100%) rename jib-maven-plugin/src/test/resources/projects/simple/src/main/{jib => jib-custom}/foo (100%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index b508435784..f20173859e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -46,7 +46,8 @@ class GradleLayerConfigurations { * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution * @param extraDirectory path to the source directory for the extra files layer - * @param permissions map from path on container to file permissions for extra-layer files + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution @@ -55,14 +56,15 @@ static JavaLayerConfigurations getForProject( Project project, Logger logger, Path extraDirectory, - Map permissions, + Map extraDirectoryPermissions, AbsoluteUnixPath appRoot) throws IOException { if (GradleProjectProperties.getWarTask(project) != null) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); - return getForWarProject(project, extraDirectory, permissions, appRoot); + return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); } else { - return getForNonWarProject(project, logger, extraDirectory, permissions, appRoot); + return getForNonWarProject( + project, logger, extraDirectory, extraDirectoryPermissions, appRoot); } } @@ -72,7 +74,8 @@ static JavaLayerConfigurations getForProject( * @param project the Gradle {@link Project} * @param logger the logger for providing feedback about the resolution * @param extraDirectory path to the source directory for the extra files layer - * @param permissions map from path on container to file permissions for extra-layer files + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution @@ -81,7 +84,7 @@ private static JavaLayerConfigurations getForNonWarProject( Project project, Logger logger, Path extraDirectory, - Map permissions, + Map extraDirectoryPermissions, AbsoluteUnixPath appRoot) throws IOException { AbsoluteUnixPath dependenciesExtractionPath = @@ -143,7 +146,7 @@ private static JavaLayerConfigurations getForNonWarProject( extraDirectory, path -> true, AbsoluteUnixPath.get("/"), - permissions); + extraDirectoryPermissions); } return layerBuilder.build(); @@ -154,7 +157,8 @@ private static JavaLayerConfigurations getForNonWarProject( * * @param project the Gradle {@link Project} * @param extraDirectory path to the source directory for the extra files layer - * @param permissions map from path on container to file permissions for extra-layer files + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} * @throws IOException if an I/O exception occurred during resolution @@ -162,12 +166,12 @@ private static JavaLayerConfigurations getForNonWarProject( private static JavaLayerConfigurations getForWarProject( Project project, Path extraDirectory, - Map permissions, + Map extraDirectoryPermissions, AbsoluteUnixPath appRoot) throws IOException { Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); return JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWarPath, appRoot, extraDirectory, permissions); + explodedWarPath, appRoot, extraDirectory, extraDirectoryPermissions); } private GradleLayerConfigurations() {} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 8512629205..680bc82f7a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; @@ -32,8 +32,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.GradleException; @@ -69,11 +71,7 @@ static GradleProjectProperties getForProject( project, makeEventHandlers(logger), GradleLayerConfigurations.getForProject( - project, - logger, - extraDirectory, - ConfigurationPropertyValidator.convertPermissionsMap(permissions), - appRoot)); + project, logger, extraDirectory, convertPermissionsMap(permissions), appRoot)); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); @@ -193,4 +191,23 @@ static FileCollection getInputFiles(File extraDirectory, Project project) { return project.files(dependencyFileCollections); } } + + /** + * Validates and converts a {@code String->String} file-path-to-file-permissions map to an + * equivalent {@code AbsoluteUnixPath->FilePermission} map. + * + * @param stringMap the map to convert (example entry: {@code "/path/on/container" -> "755"}) + * @return the converted map + */ + @VisibleForTesting + static Map convertPermissionsMap( + Map stringMap) { + Map permissionsMap = new HashMap<>(); + for (Entry entry : stringMap.entrySet()) { + AbsoluteUnixPath key = AbsoluteUnixPath.get(entry.getKey()); + FilePermissions value = FilePermissions.fromOctalString(entry.getValue()); + permissionsMap.put(key, value); + } + return permissionsMap; + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index f1c82dcaff..97000fe5d3 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,7 +16,9 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -113,4 +115,24 @@ public void testGetWar_noWarTask() { Assert.assertNull(GradleProjectProperties.getWarTask(mockProject)); } + + @Test + public void testConvertPermissionsMap() { + Assert.assertEquals( + ImmutableMap.of( + AbsoluteUnixPath.get("/test/folder/file1"), + FilePermissions.fromOctalString("123"), + AbsoluteUnixPath.get("/test/file2"), + FilePermissions.fromOctalString("456")), + GradleProjectProperties.convertPermissionsMap( + ImmutableMap.of("/test/folder/file1", "123", "/test/file2", "456"))); + + try { + GradleProjectProperties.convertPermissionsMap( + ImmutableMap.of("a path", "not valid permission")); + Assert.fail(); + } catch (IllegalArgumentException ignored) { + // pass + } + } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 900ce8eb1b..f9f995acd9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to this project will be documented in this file. - `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) - Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) +- `` object with a `` and `` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) + - `` configures the extra layer directory (still also configurable via `...`) + - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 0d3504d23e..cc4ebf6f72 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -66,7 +66,12 @@ public void execute() throws MojoExecutionException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject( + getProject(), + getLog(), + PluginConfigurationProcessor.getExtraDirectoryPath(this), + PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 8910eec289..e549283174 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -89,7 +89,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject( + getProject(), + getLog(), + PluginConfigurationProcessor.getExtraDirectoryPath(this), + PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 2d4c36ae0f..c9486512d4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -62,7 +62,12 @@ public void execute() throws MojoExecutionException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject( + getProject(), + getLog(), + PluginConfigurationProcessor.getExtraDirectoryPath(this), + PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 47c978139c..c98182f7db 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -73,7 +73,12 @@ public void execute() throws MojoExecutionException { AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject(getProject(), getLog(), getExtraDirectory(), appRoot); + MavenProjectProperties.getForProject( + getProject(), + getLog(), + PluginConfigurationProcessor.getExtraDirectoryPath(this), + PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); List entrypoint = PluginConfigurationProcessor.computeEntrypoint(getLog(), this, mavenProjectProperties); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 21ff0a7484..6ec0be3ab8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -24,11 +24,14 @@ import com.google.common.collect.ImmutableSet; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; @@ -39,7 +42,7 @@ import org.apache.maven.settings.crypto.SettingsDecrypter; /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ -abstract class JibPluginConfiguration extends AbstractMojo { +public abstract class JibPluginConfiguration extends AbstractMojo { /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ public static class AuthConfiguration implements AuthProperty { @@ -87,6 +90,30 @@ private void setPropertyDescriptors(String descriptorPrefix) { } } + /** Used to configure {@code extraDirectory.permissions} parameter. */ + public static class PermissionConfiguration { + + @Nullable @Parameter private String file; + @Nullable @Parameter private String mode; + + // Need default constructor for Maven + public PermissionConfiguration() {} + + @VisibleForTesting + PermissionConfiguration(String file, String mode) { + this.file = file; + this.mode = mode; + } + + Optional getFile() { + return Optional.ofNullable(file); + } + + Optional getMode() { + return Optional.ofNullable(mode); + } + } + /** Configuration for {@code from} parameter, */ public static class FromConfiguration { @@ -143,6 +170,29 @@ public static class ContainerParameters { @Nullable @Parameter private String user; } + /** Configuration for the {@code extraDirectory} parameter. */ + public static class ExtraDirectoryParameters { + + @Nullable @Parameter private File path; + + @Parameter private List permissions = Collections.emptyList(); + + /** + * Allows users to configure {@code path} using just {@code } instead of {@code + * }. + * + * @param path the value to set {@code path} to + */ + public void set(File path) { + this.path = path; + } + + @Nullable + public File getPath() { + return path; + } + } + @Nullable @Parameter(defaultValue = "${session}", readonly = true) MavenSession session; @@ -157,6 +207,9 @@ public static class ContainerParameters { @Parameter private ContainerParameters container = new ContainerParameters(); + // this parameter is cloned in FilesMojo + @Parameter private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); + @Parameter( defaultValue = "false", required = true, @@ -169,14 +222,6 @@ public static class ContainerParameters { property = PropertyNames.ALLOW_INSECURE_REGISTRIES) private boolean allowInsecureRegistries; - // this parameter is cloned in FilesMojo - @Nullable - @Parameter( - defaultValue = "${project.basedir}/src/main/jib", - required = true, - property = PropertyNames.EXTRA_DIRECTORY_PATH) - private File extraDirectory; - @Parameter(defaultValue = "false", property = PropertyNames.SKIP) private boolean skip; @@ -417,6 +462,38 @@ String getFormat() { return Preconditions.checkNotNull(container.format); } + /** + * Gets the configured extra directory path. + * + * @return the configured extra directory path + */ + Optional getExtraDirectoryPath() { + // TODO: Should inform user about nonexistent directory if using custom directory. + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { + return Optional.of(Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH))); + } + return extraDirectory.path == null + ? Optional.empty() + : Optional.of(extraDirectory.path.toPath()); + } + + /** + * Gets the configured extra layer file permissions. + * + * @return the configured extra layer file permissions + */ + List getExtraDirectoryPermissions() { + if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS)) + .entrySet() + .stream() + .map(entry -> new PermissionConfiguration(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + return extraDirectory.permissions; + } + boolean getUseOnlyProjectCache() { return useOnlyProjectCache; } @@ -425,11 +502,6 @@ boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } - Path getExtraDirectory() { - // TODO: Should inform user about nonexistent directory if using custom directory. - return Preconditions.checkNotNull(extraDirectory).toPath(); - } - boolean isSkipped() { return skip; } @@ -442,9 +514,4 @@ SettingsDecrypter getSettingsDecrypter() { void setProject(MavenProject project) { this.project = project; } - - @VisibleForTesting - void setExtraDirectory(File extraDirectory) { - this.extraDirectory = extraDirectory; - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index d6b4904120..c3a3f7abc8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; @@ -26,7 +27,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; +import java.util.Map; import java.util.function.Predicate; import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; @@ -39,16 +40,22 @@ class MavenLayerConfigurations { * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the project * @throws IOException if collecting the project files fails */ static JavaLayerConfigurations getForProject( - MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + MavenProject project, + Path extraDirectory, + Map extraDirectoryPermissions, + AbsoluteUnixPath appRoot) + throws IOException { if ("war".equals(project.getPackaging())) { - return getForWarProject(project, extraDirectory, appRoot); + return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); } else { - return getForNonWarProject(project, extraDirectory, appRoot); + return getForNonWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); } } @@ -57,12 +64,18 @@ static JavaLayerConfigurations getForProject( * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the project * @throws IOException if collecting the project files fails */ private static JavaLayerConfigurations getForNonWarProject( - MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + MavenProject project, + Path extraDirectory, + Map extraDirectoryPermissions, + AbsoluteUnixPath appRoot) + throws IOException { AbsoluteUnixPath dependenciesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); @@ -96,7 +109,11 @@ private static JavaLayerConfigurations getForNonWarProject( // Adds all the extra files. if (Files.exists(extraDirectory)) { layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, extraDirectory, path -> true, AbsoluteUnixPath.get("/")); + LayerType.EXTRA_FILES, + extraDirectory, + path -> true, + AbsoluteUnixPath.get("/"), + extraDirectoryPermissions); } return layerBuilder.build(); @@ -107,12 +124,18 @@ private static JavaLayerConfigurations getForNonWarProject( * * @param project the {@link MavenProject} * @param extraDirectory path to the directory for the extra files layer + * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer + * files * @param appRoot root directory in the image where the app will be placed * @return a {@link JavaLayerConfigurations} for the project * @throws IOException if collecting the project files fails */ private static JavaLayerConfigurations getForWarProject( - MavenProject project, Path extraDirectory, AbsoluteUnixPath appRoot) throws IOException { + MavenProject project, + Path extraDirectory, + Map extraDirectoryPermissions, + AbsoluteUnixPath appRoot) + throws IOException { // TODO explode the WAR file rather than using this directory. The contents of the final WAR may // be different from this directory (it's possible to include or exclude files when packaging a @@ -120,9 +143,8 @@ private static JavaLayerConfigurations getForWarProject( // at build.getFinalName(). Path explodedWarPath = Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - // TODO: Replace Collections.emptyMap() with configured permissions return JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWarPath, appRoot, extraDirectory, Collections.emptyMap()); + explodedWarPath, appRoot, extraDirectory, extraDirectoryPermissions); } private MavenLayerConfigurations() {} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index eb21c35e44..e8d74fd241 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -28,6 +29,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; @@ -54,18 +56,23 @@ public class MavenProjectProperties implements ProjectProperties { * @param project the {@link MavenProject} for the plugin. * @param log the Maven {@link Log} to log messages during Jib execution * @param extraDirectory path to the directory for the extra files layer + * @param permissions map from path on container to file permissions for extra-layer files * @param appRoot root directory in the image where the app will be placed * @return a MavenProjectProperties from the given project and logger. * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, Log log, Path extraDirectory, AbsoluteUnixPath appRoot) + MavenProject project, + Log log, + Path extraDirectory, + Map permissions, + AbsoluteUnixPath appRoot) throws MojoExecutionException { try { return new MavenProjectProperties( project, makeEventHandlers(log), - MavenLayerConfigurations.getForProject(project, extraDirectory, appRoot)); + MavenLayerConfigurations.getForProject(project, extraDirectory, permissions, appRoot)); } catch (IOException ex) { throw new MojoExecutionException( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java index 659a454259..d9e7fea55f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -32,14 +33,19 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.FileNotFoundException; +import java.nio.file.Path; import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; @@ -102,6 +108,49 @@ static String getBaseImage(JibPluginConfiguration jibPluginConfiguration) { return baseImage; } + /** + * Gets the extra directory path from a {@link JibPluginConfiguration}. Returns {@code (project + * dir)/src/main/jib} if null. + * + * @param jibPluginConfiguration the build configuration + * @return the resolved extra directory + */ + static Path getExtraDirectoryPath(JibPluginConfiguration jibPluginConfiguration) { + return jibPluginConfiguration + .getExtraDirectoryPath() + .orElse( + Preconditions.checkNotNull(jibPluginConfiguration.getProject()) + .getBasedir() + .toPath() + .resolve("src") + .resolve("main") + .resolve("jib")); + } + + /** + * Validates and converts a list of {@link PermissionConfiguration} to an equivalent {@code + * AbsoluteUnixPath->FilePermission} map. + * + * @param permissionList the list to convert + * @return the resulting map + */ + @VisibleForTesting + static Map convertPermissionsList( + List permissionList) { + Map permissionsMap = new HashMap<>(); + for (PermissionConfiguration permission : permissionList) { + if (!permission.getFile().isPresent() || !permission.getMode().isPresent()) { + throw new IllegalArgumentException( + "Incomplete configuration; requires and fields to both be " + + "non-null."); + } + AbsoluteUnixPath key = AbsoluteUnixPath.get(permission.getFile().get()); + FilePermissions value = FilePermissions.fromOctalString(permission.getMode().get()); + permissionsMap.put(key, value); + } + return permissionsMap; + } + /** Disables annoying Apache HTTP client logging. */ static void disableHttpLogging() { System.setProperty( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java index 5148f7e648..f923cfdacd 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -16,11 +16,12 @@ package com.google.cloud.tools.jib.maven.skaffold; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.ExtraDirectoryParameters; import com.google.cloud.tools.jib.maven.MavenProjectProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; -import java.io.File; +import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -74,16 +75,13 @@ public class FilesMojo extends AbstractMojo { @Nullable @Component private ProjectDependenciesResolver projectDependenciesResolver; // This parameter is cloned from JibPluginConfiguration - @Nullable - @Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true) - private File extraDirectory; + @Parameter private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); @Override public void execute() throws MojoExecutionException, MojoFailureException { Preconditions.checkNotNull(project); Preconditions.checkNotNull(projects); Preconditions.checkNotNull(session); - Preconditions.checkNotNull(extraDirectory); Preconditions.checkNotNull(projectDependenciesResolver); // print out pom configuration files @@ -107,7 +105,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { // and is expected to run on all projects irrespective of their configuring of the jib plugin). if (project.getPlugin(MavenProjectProperties.PLUGIN_KEY) != null) { // print out extra directory - System.out.println(extraDirectory.getAbsoluteFile().toPath()); + System.out.println(resolveExtraDirectory()); } // Grab non-project SNAPSHOT dependencies for this project @@ -151,4 +149,17 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException("Failed to resolve dependencies", ex); } } + + private Path resolveExtraDirectory() { + if (extraDirectory.getPath() == null) { + return Preconditions.checkNotNull(project) + .getBasedir() + .getAbsoluteFile() + .toPath() + .resolve("src") + .resolve("main") + .resolve("jib"); + } + return extraDirectory.getPath().getAbsoluteFile().toPath(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index a40a55bc5e..7d65ea0462 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -88,7 +88,7 @@ public void testExecute_simple() throws VerificationException, IOException, Inte Instant before = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", buildToDockerDaemonAndRun(simpleTestProject.getProjectRoot(), targetImage)); Instant buildTime = Instant.parse( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index e47f3d0e76..fd75e3a96c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -269,7 +269,7 @@ public void testExecute_simple() throws VerificationException, IOException, Inte before.toString().getBytes(StandardCharsets.UTF_8)); Assert.assertEquals( - "Hello, " + before + ". An argument.\nfoo\ncat\n", + "Hello, " + before + ". An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, true)); Instant buildTime = @@ -320,7 +320,7 @@ public void testExecute_complex() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); } @@ -329,7 +329,7 @@ public void testExecute_complex_sameFromAndToRegistry() throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 25288e905b..856fc0294a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -63,7 +63,7 @@ public void testExecute_simple() throws VerificationException, IOException, Inte .toString()) .run(); Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", new Command("docker", "run", "--rm", targetImage).run()); Instant buildTime = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java index 24c686a346..2a3849a1f4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java @@ -88,9 +88,9 @@ public void testExecute() throws VerificationException, IOException, Interrupted + " \"key2\": \"value2\"\n" + " }")); - Assert.assertEquals( - "Hello, world. An argument.\nfoo\ncat\n", - new Command("docker", "run", "--rm", imageName).run()); + String output = new Command("docker", "run", "--rm", imageName).run(); + Assert.assertThat(output, CoreMatchers.startsWith("Hello, world. An argument.\n")); + Assert.assertThat(output, CoreMatchers.endsWith("foo\ncat\n")); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index bf73130422..90b9a8a756 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; import org.apache.maven.model.Build; import org.apache.maven.plugin.MojoExecutionException; @@ -54,6 +55,7 @@ public class DockerContextMojoTest { public void setUp() throws IOException { outputFolder = projectRoot.newFolder("target"); Mockito.when(project.getBuild()).thenReturn(build); + Mockito.when(project.getBasedir()).thenReturn(projectRoot.getRoot()); Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); mojo = new BaseDockerContextMojo(); @@ -228,8 +230,8 @@ MavenProject getProject() { } @Override - Path getExtraDirectory() { - return projectRoot.getRoot().toPath(); + Optional getExtraDirectoryPath() { + return Optional.of(projectRoot.getRoot().toPath()); } @Override diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index b63dd66371..8cb69fb8d4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.nio.file.Paths; +import java.util.List; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -47,6 +50,8 @@ private static void clearProperties() { System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); System.clearProperty("jib.container.user"); + System.clearProperty("jib.extraDirectory.path"); + System.clearProperty("jib.extraDirectory.permissions"); } @Before @@ -128,5 +133,16 @@ public void testSystemProperties() { Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); + + System.setProperty("jib.extraDirectory.path", "custom-jib"); + Assert.assertEquals( + Paths.get("custom-jib"), testPluginConfiguration.getExtraDirectoryPath().get()); + System.setProperty("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); + List permissions = + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); + Assert.assertEquals("123", permissions.get(0).getMode().get()); + Assert.assertEquals("/another/file", permissions.get(1).getFile().get()); + Assert.assertEquals("456", permissions.get(1).getMode().get()); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 130518b695..127e8cf90e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -28,6 +28,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -161,7 +162,10 @@ public void test_correctFiles() throws URISyntaxException, IOException { JavaLayerConfigurations javaLayerConfigurations = MavenLayerConfigurations.getForProject( - mockMavenProject, Paths.get("nonexistent/path"), AbsoluteUnixPath.get("/app")); + mockMavenProject, + Paths.get("nonexistent/path"), + Collections.emptyMap(), + AbsoluteUnixPath.get("/app")); assertSourcePathsUnordered( expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); assertSourcePathsUnordered( @@ -177,7 +181,8 @@ public void test_correctFiles() throws URISyntaxException, IOException { public void test_extraFiles() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); JavaLayerConfigurations javaLayerConfigurations = - MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); ImmutableList expectedExtraFiles = ImmutableList.of( @@ -196,7 +201,8 @@ public void test_extraFiles() throws IOException { public void testGetForProject_nonDefaultAppRoot() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); assertNonDefaultAppRoot(configuration); } @@ -216,7 +222,8 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); ImmutableList expectedDependenciesFiles = ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")); @@ -296,7 +303,8 @@ public void testGetForJarProject_nonDefaultAppRoot() throws IOException { AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject(mockMavenProject, extraFilesDirectory, appRoot); + MavenLayerConfigurations.getForProject( + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); assertNonDefaultAppRoot(configuration); } @@ -311,7 +319,7 @@ public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOExceptio AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, appRoot); // should pass + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass } @Test @@ -324,7 +332,7 @@ public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOExcep AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, appRoot); // should pass + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass } @Test @@ -337,6 +345,6 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOE AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, appRoot); // should pass + mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java index d2f8e579ec..96133e0367 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java @@ -23,13 +23,16 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -389,5 +392,27 @@ public void testGetBaseImage_nonDefault() { "tomcat", PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); } + @Test + public void testConvertPermissionsList() { + Assert.assertEquals( + ImmutableMap.of( + AbsoluteUnixPath.get("/test/folder/file1"), + FilePermissions.fromOctalString("123"), + AbsoluteUnixPath.get("/test/file2"), + FilePermissions.fromOctalString("456")), + PluginConfigurationProcessor.convertPermissionsList( + ImmutableList.of( + new PermissionConfiguration("/test/folder/file1", "123"), + new PermissionConfiguration("/test/file2", "456")))); + + try { + PluginConfigurationProcessor.convertPermissionsList( + ImmutableList.of(new PermissionConfiguration("a path", "not valid permission"))); + Assert.fail(); + } catch (IllegalArgumentException ignored) { + // pass + } + } + // TODO should test other behaviours } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index 9cdd30e831..e6e5aa6950 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -78,7 +78,7 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti projectRoot.resolve("pom.xml"), projectRoot.resolve("src/main/java"), projectRoot.resolve("src/main/resources"), - projectRoot.resolve("src/main/jib"))); + projectRoot.resolve("src/main/jib-custom"))); } @Test diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index 02ebc126ce..36e375a6b8 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -76,6 +76,19 @@ Docker + + ${project.basedir}/src/main/jib-custom + + + /foo + 755 + + + /bar/cat + 777 + + + true true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index 1f9485fd48..e5c4e478c1 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -53,6 +53,7 @@ value2 + ${project.basedir}/src/main/jib-custom true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java index 733648707f..cf30f12cdf 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; /** Example class that uses a dependency and a resource file. */ public class HelloWorld { @@ -41,6 +42,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { // Prints the contents of the extra files. if (Files.exists(Paths.get("/foo"))) { + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); System.out.println(new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/bar/cat b/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/bar/cat similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/bar/cat rename to jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/bar/cat diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/foo b/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/foo similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib/foo rename to jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/foo diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 31ecab43b5..708a2dfce1 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -16,11 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -31,7 +29,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -187,23 +184,5 @@ public static List parseListProperty(String property) { return ImmutableList.copyOf(items); } - /** - * Validates and converts a {@code String->String} file-path-to-file-permissions map to an - * equivalent {@code AbsoluteUnixPath->FilePermission} map. - * - * @param inputMap the map to convert - * @return the converted map - */ - public static Map convertPermissionsMap( - Map inputMap) { - ImmutableMap.Builder permissionsMap = ImmutableMap.builder(); - for (Entry entry : inputMap.entrySet()) { - AbsoluteUnixPath key = AbsoluteUnixPath.get(entry.getKey()); - FilePermissions value = FilePermissions.fromOctalString(entry.getValue()); - permissionsMap.put(key, value); - } - return permissionsMap.build(); - } - private ConfigurationPropertyValidator() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java index 1c398790ac..3abbcf08dc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java @@ -36,7 +36,7 @@ public class JavaLayerConfigurationsHelper { * @param explodedWar the exploded WAR directory * @param appRoot root directory in the image where the app will be placed * @param extraFilesDirectory path to the source directory for the extra files layer - * @param permissions map from path on container to file permissions + * @param extraDirectoryPermissions map from path on container to file permissions * @return {@link JavaLayerConfigurations} for the layers for the exploded WAR * @throws IOException if adding layer contents fails */ @@ -44,7 +44,7 @@ public static JavaLayerConfigurations fromExplodedWar( Path explodedWar, AbsoluteUnixPath appRoot, Path extraFilesDirectory, - Map permissions) + Map extraDirectoryPermissions) throws IOException { Path webInfLib = explodedWar.resolve("WEB-INF/lib"); Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); @@ -86,7 +86,7 @@ public static JavaLayerConfigurations fromExplodedWar( extraFilesDirectory, path -> true, AbsoluteUnixPath.get("/"), - permissions); + extraDirectoryPermissions); } return layerBuilder.build(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 2a7f2a135d..e539f21e73 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -16,11 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableList; @@ -174,24 +172,4 @@ public void testParseMapProperty() { // pass } } - - @Test - public void testConvertPermissionsMap() { - Assert.assertEquals( - ImmutableMap.of( - AbsoluteUnixPath.get("/test/folder/file1"), - FilePermissions.fromOctalString("123"), - AbsoluteUnixPath.get("/test/file2"), - FilePermissions.fromOctalString("456")), - ConfigurationPropertyValidator.convertPermissionsMap( - ImmutableMap.of("/test/folder/file1", "123", "/test/file2", "456"))); - - try { - ConfigurationPropertyValidator.convertPermissionsMap( - ImmutableMap.of("a path", "not valid permission")); - Assert.fail(); - } catch (IllegalArgumentException ignored) { - // pass - } - } } From 75d310fa1da5cb0910e5d35c4a8cf6b745bf2ec8 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 29 Oct 2018 11:47:23 -0700 Subject: [PATCH 0319/2020] Adds README for Jib Core. (#1160) --- README.md | 11 +++-- jib-core/README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 121a739041..9cc83afda1 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,11 @@ ## What is Jib? -Jib builds Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications and is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin). +Jib builds Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. It is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin) and as a Java library. [Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin).\ -[Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin). - -*Jib as a container-building library for Java is work-in-progress. [Watch for updates.](https://github.com/GoogleContainerTools/jib/issues/337)* +[Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin).\ +[Jib Core](jib-core): A general-purpose container-building library for Java. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). @@ -39,6 +38,10 @@ See documentation for using [jib-maven-plugin](jib-maven-plugin#quickstart). See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). +### Jib Core + +See documentation for using [Jib Core](jib-core#adding-jib-core-to-your-build) + ## How Jib Works Whereas traditionally a Java application is built as a single image layer with the application JAR, Jib's build strategy separates the Java application into multiple layers for more granular incremental builds. When you change your code, only your changes are rebuilt, not your entire application. These layers, by default, are layered on top of a [distroless](https://github.com/GoogleCloudPlatform/distroless) base image. For more information, check out the [official blog post](https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html) or watch [this talk](https://www.youtube.com/watch?v=H6gR_Cv4yWI) ([slides](https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications)). diff --git a/jib-core/README.md b/jib-core/README.md index fe751f04e8..f32ab5a114 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -1,3 +1,120 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-core)](https://github.com/igrigorik/ga-beacon) +![experimental](https://img.shields.io/badge/stability-experimental-red.svg) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core) +[![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) + +# Jib Core - Java library for building containers + +Jib Core is a Java library for building Docker and [OCI](https://github.com/opencontainers/image-spec) container images. It implements a general-purpose container builder that can be used to build containers without a Docker daemon, for any application. The implementation is pure Java. + +*The API is currently in alpha and may change substantially.* + +Jib Core powers the popular Jib plugins for Maven and Gradle. The plugins build containers specifically for JVM languages and separate the application into multiple layers to optimize for fast rebuilds.\ +For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin).\ +For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). + +For information about the Jib project, see the [Jib project README](../README.md). + +## Upcoming features + +- Extensions to make building Java and other language-specific containers easier +- Structured events to react to parts of Jib Core's execution + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. + +## Adding Jib Core to your build + +Add Jib Core as a dependency using Maven: + +```xml + + com.google.cloud.tools + jib-core + 0.1.0 + +``` + +Add Jib Core as a dependency using Gradle: + +```groovy +dependencies { + compile 'com.google.cloud.tools:jib-core:0.1.0' +} +``` + +## Simple example + +```java +Jib.from("busybox") + .addLayer(Arrays.asList(Paths.get("helloworld.sh")), AbsoluteUnixPath.get("/")) + .setEntrypoint("sh", "/helloworld.sh") + .containerize( + Containerizer.to(RegistryImage.named("gcr.io/my-project/hello-from-jib") + .addCredential("myusername", "mypassword"))); +``` + +1. [`Jib.from("busybox")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. +1. [`.addLayer(...)`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. +1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. +1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. +1. [`.addCredential`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). +1. [`Containerizer.to`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. +1. [`.containerize`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. + +## Tutorials + +*None yet available. We welcome contributions for examples and tutorials!* + +## API overview + +[`Jib`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core -Jib core library. NOT intended for use outside of `jib-maven-plugin` or `jib-gradle-plugin` in its current state. We are working on making this into a library for Java. \ No newline at end of file +[`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build + +[`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to + +[`JibContainer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container + +Three `TargetImage` types define the 3 different targets Jib can build to: +- [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry +- [`DockerDaemonImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon +- [`TarImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive + +Other useful classes: + +- [`ImageReference`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references +- [`LayerConfiguration`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build +- [`CredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry +- [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers +- [`EventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution + +## API reference + +[API reference](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/package-summary.html) + +## How Jib Core works + +The Jib Core system consists 3 main parts: + +- an execution orchestrator that executes an asynchronous pipeline of containerization steps, +- an image manipulator capable of handling Docker and OCI image formats, and +- a registry client that implements the [Docker Registry V2 API](https://docs.docker.com/registry/spec/api/). + +Some other parts of Jib Core internals include: + +- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) +- an eventing system to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) +- support for fully-concurrent multi-threaded executions + +## How Jib Works + +See the [Jib project README](/../../#how-jib-works). + +## Frequently Asked Questions (FAQ) + +See the [Jib project FAQ](../docs/faq.md). + +## Community + +See the [Jib project README](/../../#community). + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-core)](https://github.com/igrigorik/ga-beacon) From 8f46e19a862876987d63561bcdbc4adbe376e640 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 29 Oct 2018 16:47:03 -0400 Subject: [PATCH 0320/2020] Write out image digest to /jib-image.digest (#1155) --- .../builder/BuildStepsIntegrationTest.java | 10 +-- .../cloud/tools/jib/api/JibContainer.java | 34 +++++++++- .../tools/jib/api/JibContainerBuilder.java | 12 ++-- .../cloud/tools/jib/builder/BuildSteps.java | 12 ++-- .../tools/jib/builder/steps/BuildResult.java | 58 ++++++++++++++++ .../jib/builder/steps/LoadDockerStep.java | 20 +++--- .../jib/builder/steps/PushImageStep.java | 26 ++++--- .../tools/jib/builder/steps/StepsRunner.java | 7 +- .../jib/builder/steps/WriteTarFileStep.java | 25 ++++--- .../cloud/tools/jib/api/JibContainerTest.java | 67 +++++++++++++++++++ .../jib/builder/steps/BuildResultTest.java | 62 +++++++++++++++++ jib-gradle-plugin/CHANGELOG.md | 1 + .../DefaultTargetProjectIntegrationTest.java | 4 +- .../gradle/EmptyProjectIntegrationTest.java | 13 ++-- .../cloud/tools/jib/gradle/JibRunHelper.java | 25 +++++-- .../gradle/SingleProjectIntegrationTest.java | 5 +- .../jib/gradle/WarProjectIntegrationTest.java | 8 ++- .../tools/jib/gradle/BuildDockerTask.java | 3 + .../tools/jib/gradle/BuildImageTask.java | 3 + .../cloud/tools/jib/gradle/BuildTarTask.java | 25 +++---- jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildImageMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildTarMojo.java | 5 +- .../maven/BuildDockerMojoIntegrationTest.java | 20 ++++-- .../maven/BuildImageMojoIntegrationTest.java | 49 +++++++++++--- .../maven/BuildTarMojoIntegrationTest.java | 8 ++- .../jib/plugins/common/BuildStepsRunner.java | 22 ++++++ 28 files changed, 432 insertions(+), 101 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index ae126d1730..5cf6b3ae65 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder; import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; @@ -130,13 +130,13 @@ public void testSteps_forBuildToDockerRegistry() .build()); long lastTime = System.nanoTime(); - DescriptorDigest imageDigest1 = buildImageSteps.run(); + BuildResult image1 = buildImageSteps.run(); logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); lastTime = System.nanoTime(); - DescriptorDigest imageDigest2 = buildImageSteps.run(); + BuildResult image2 = buildImageSteps.run(); logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); - Assert.assertEquals(imageDigest1, imageDigest2); + Assert.assertEquals(image1, image2); String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); @@ -144,7 +144,7 @@ public void testSteps_forBuildToDockerRegistry() Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); - String imageReferenceByDigest = "localhost:5000/testimage@" + imageDigest1; + String imageReferenceByDigest = "localhost:5000/testimage@" + image1.getImageDigest(); localRegistry.pull(imageReferenceByDigest); assertDockerInspect(imageReferenceByDigest); Assert.assertEquals( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java index 7912fbf1c2..b445880f13 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java @@ -17,22 +17,52 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.util.Objects; /** The container built by Jib. */ public class JibContainer { private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; - JibContainer(DescriptorDigest imageDigest) { + JibContainer(DescriptorDigest imageDigest, DescriptorDigest imageId) { this.imageDigest = imageDigest; + this.imageId = imageId; } /** - * Gets the digest of the container image built by Jib. + * Gets the digest of the registry image manifest built by Jib. This digest can be used to fetch a + * specific image from the registry in the form {@code myregistry/myimage@digest}. * * @return the image digest */ public DescriptorDigest getDigest() { return imageDigest; } + + /** + * Gets the digest of the container configuration built by Jib. + * + * @return the image ID + */ + public DescriptorDigest getImageId() { + return imageId; + } + + @Override + public int hashCode() { + return Objects.hash(imageDigest, imageId); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JibContainer)) { + return false; + } + JibContainer otherContainer = (JibContainer) other; + return imageDigest.equals(otherContainer.imageDigest) && imageId.equals(otherContainer.imageId); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 6a50019079..cb76d6e0d1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -358,10 +358,10 @@ public JibContainerBuilder setUser(@Nullable String user) { } /** - * Builds the container(s). + * Builds the container. * * @param containerizer the {@link Containerizer} that configures how to containerize - * @return the built container(s) + * @return the built container * @throws CacheDirectoryCreationException if a directory to be used for the cache could not be * created * @throws ExecutionException if an exception occurred during execution @@ -373,10 +373,8 @@ public JibContainer containerize(Containerizer containerizer) CacheDirectoryCreationException { BuildConfiguration buildConfiguration = toBuildConfiguration(BuildConfiguration.builder(), containerizer); - DescriptorDigest imageDigest = - containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); - - return new JibContainer(imageDigest); + BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); + return new JibContainer(result.getImageDigest(), result.getImageId()); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 3fbad4abb8..4039891578 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.builder; +import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.nio.file.Path; import java.util.concurrent.ExecutionException; @@ -35,13 +35,13 @@ public class BuildSteps { private interface ImageBuildRunnable { /** - * Builds an image and returns its digest. + * Builds an image. * - * @return the digest of the built image + * @return the built image * @throws ExecutionException if an exception occurs during execution * @throws InterruptedException if the execution is interrupted */ - DescriptorDigest build() throws ExecutionException, InterruptedException; + BuildResult build() throws ExecutionException, InterruptedException; } /** @@ -140,11 +140,11 @@ public BuildConfiguration getBuildConfiguration() { /** * Executes the build. * - * @return the built image digest + * @return the build result * @throws InterruptedException if the execution is interrupted * @throws ExecutionException if an exception occurs during execution */ - public DescriptorDigest run() throws InterruptedException, ExecutionException { + public BuildResult run() throws InterruptedException, ExecutionException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { return imageBuildRunnable.build(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java new file mode 100644 index 0000000000..e5b37b1398 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.util.Objects; + +/** Used to record the results of a build. */ +public class BuildResult { + + private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; + + BuildResult(DescriptorDigest imageDigest, DescriptorDigest imageId) { + this.imageDigest = imageDigest; + this.imageId = imageId; + } + + public DescriptorDigest getImageDigest() { + return imageDigest; + } + + public DescriptorDigest getImageId() { + return imageId; + } + + @Override + public int hashCode() { + return Objects.hash(imageDigest, imageId); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BuildResult)) { + return false; + } + BuildResult otherBuildResult = (BuildResult) other; + return imageDigest.equals(otherBuildResult.imageDigest) + && imageId.equals(otherBuildResult.imageId); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 5ff9f12db0..c124db5810 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -40,7 +40,7 @@ import java.util.concurrent.ExecutionException; /** Adds image layers to a tarball and loads into Docker daemon. */ -class LoadDockerStep implements AsyncStep, Callable { +class LoadDockerStep implements AsyncStep, Callable { private final DockerClient dockerClient; private final BuildConfiguration buildConfiguration; @@ -49,7 +49,7 @@ class LoadDockerStep implements AsyncStep, Callable listenableFuture; + private final ListenableFuture listenableFuture; LoadDockerStep( ListeningExecutorService listeningExecutorService, @@ -72,12 +72,12 @@ class LoadDockerStep implements AsyncStep, Callable getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public DescriptorDigest call() throws ExecutionException, InterruptedException { + public BuildResult call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { @@ -93,7 +93,7 @@ public DescriptorDigest call() throws ExecutionException, InterruptedException { .get(); } - private DescriptorDigest afterPushBaseImageLayerFuturesFuture() + private BuildResult afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = @@ -124,8 +124,12 @@ private DescriptorDigest afterPushBaseImageLayerFuturesFuture() BuildableManifestTemplate manifestTemplate = imageToJsonTranslator.getManifestTemplate( buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); - return JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest imageDigest = + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + + return new BuildResult(imageDigest, imageId); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 6beb0cc376..553cd46b17 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -38,7 +39,7 @@ import java.util.concurrent.ExecutionException; /** Pushes the final image. Outputs the pushed image digest. */ -class PushImageStep implements AsyncStep, Callable { +class PushImageStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Pushing new image"; @@ -51,7 +52,7 @@ class PushImageStep implements AsyncStep, Callable listenableFuture; + private final ListenableFuture listenableFuture; PushImageStep( ListeningExecutorService listeningExecutorService, @@ -79,12 +80,12 @@ class PushImageStep implements AsyncStep, Callable getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public DescriptorDigest call() throws ExecutionException, InterruptedException { + public BuildResult call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); dependenciesBuilder.add(authenticatePushStep.getFuture()); for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { @@ -103,7 +104,7 @@ public DescriptorDigest call() throws ExecutionException, InterruptedException { .get(); } - private ListenableFuture> afterPushSteps() + private ListenableFuture> afterPushSteps() throws ExecutionException { List> dependencies = new ArrayList<>(); for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { @@ -119,8 +120,7 @@ private ListenableFuture> afterPushSteps() .call(this::afterAllPushed, listeningExecutorService); } - private ListenableFuture afterAllPushed() - throws ExecutionException, IOException { + private ListenableFuture afterAllPushed() throws ExecutionException, IOException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { RegistryClient registryClient = @@ -134,11 +134,12 @@ private ListenableFuture afterAllPushed() new ImageToJsonTranslator(NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep))); // Gets the image manifest to push. + BlobDescriptor containerConfigurationBlobDescriptor = + NonBlockingSteps.get( + NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep))); BuildableManifestTemplate manifestTemplate = imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), - NonBlockingSteps.get( - NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)))); + buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); // Pushes to all target image tags. List> pushAllTagsFutures = new ArrayList<>(); @@ -158,8 +159,11 @@ private ListenableFuture afterAllPushed() JsonTemplateMapper.toBlob(manifestTemplate) .writeTo(ByteStreams.nullOutputStream()) .getDigest(); + DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + BuildResult result = new BuildResult(imageDigest, imageId); + return Futures.whenAllSucceed(pushAllTagsFutures) - .call(() -> imageDigest, listeningExecutorService); + .call(() -> result, listeningExecutorService); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 7608d22d56..934c43365f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; @@ -203,15 +202,15 @@ public StepsRunner runWriteTarFileStep(Path outputPath) { return this; } - public DescriptorDigest waitOnPushImageStep() throws ExecutionException, InterruptedException { + public BuildResult waitOnPushImageStep() throws ExecutionException, InterruptedException { return Preconditions.checkNotNull(pushImageStep).getFuture().get(); } - public DescriptorDigest waitOnLoadDockerStep() throws ExecutionException, InterruptedException { + public BuildResult waitOnLoadDockerStep() throws ExecutionException, InterruptedException { return Preconditions.checkNotNull(loadDockerStep).getFuture().get(); } - public DescriptorDigest waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { + public BuildResult waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { return Preconditions.checkNotNull(writeTarFileStep).getFuture().get(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 3f1f8ac889..a0b43db8e1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -42,7 +42,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -public class WriteTarFileStep implements AsyncStep, Callable { +public class WriteTarFileStep implements AsyncStep, Callable { private final Path outputPath; private final BuildConfiguration buildConfiguration; @@ -51,7 +51,7 @@ public class WriteTarFileStep implements AsyncStep, Callable listenableFuture; + private final ListenableFuture listenableFuture; WriteTarFileStep( ListeningExecutorService listeningExecutorService, @@ -74,12 +74,12 @@ public class WriteTarFileStep implements AsyncStep, Callable getFuture() { + public ListenableFuture getFuture() { return listenableFuture; } @Override - public DescriptorDigest call() throws ExecutionException, InterruptedException { + public BuildResult call() throws ExecutionException, InterruptedException { ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { @@ -91,12 +91,11 @@ public DescriptorDigest call() throws ExecutionException, InterruptedException { } dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::afterPushBaseImageLayerFuturesFuture, listeningExecutorService) + .call(this::writeTarFile, listeningExecutorService) .get(); } - private DescriptorDigest afterPushBaseImageLayerFuturesFuture() - throws ExecutionException, IOException { + private BuildResult writeTarFile() throws ExecutionException, IOException { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Builds the image to a tarball. @@ -111,7 +110,7 @@ private DescriptorDigest afterPushBaseImageLayerFuturesFuture() .writeTo(outputStream); } - // TODO: Consolide image digest generation with PushImageStep and LoadDockerStep. + // TODO: Consolidate image digest generation with PushImageStep and LoadDockerStep. // Gets the image manifest to generate the image digest. ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); BlobDescriptor containerConfigurationBlobDescriptor = @@ -121,8 +120,12 @@ private DescriptorDigest afterPushBaseImageLayerFuturesFuture() BuildableManifestTemplate manifestTemplate = imageToJsonTranslator.getManifestTemplate( buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); - return JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest imageDigest = + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + + return new BuildResult(imageDigest, imageId); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java new file mode 100644 index 0000000000..2d5950d00d --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link JibContainer}. */ +public class JibContainerTest { + + @Rule public TemporaryFolder temporaryDirectory = new TemporaryFolder(); + + private DescriptorDigest digest1; + private DescriptorDigest digest2; + private DescriptorDigest digest3; + + @Before + public void setUp() throws DigestException { + digest1 = + DescriptorDigest.fromDigest( + "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"); + digest2 = + DescriptorDigest.fromDigest( + "sha256:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); + digest3 = + DescriptorDigest.fromDigest( + "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"); + } + + @Test + public void testCreation() { + JibContainer container = new JibContainer(digest1, digest2); + + Assert.assertEquals(digest1, container.getDigest()); + Assert.assertEquals(digest2, container.getImageId()); + } + + @Test + public void testEquality() { + JibContainer container1 = new JibContainer(digest1, digest2); + JibContainer container2 = new JibContainer(digest1, digest2); + JibContainer container3 = new JibContainer(digest2, digest3); + + Assert.assertEquals(container1, container2); + Assert.assertEquals(container1.hashCode(), container2.hashCode()); + Assert.assertNotEquals(container1, container3); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java new file mode 100644 index 0000000000..8786a50d44 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.image.DescriptorDigest; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link BuildResult}. */ +public class BuildResultTest { + + private DescriptorDigest digest1; + private DescriptorDigest digest2; + private DescriptorDigest id; + + @Before + public void setUp() throws DigestException { + digest1 = + DescriptorDigest.fromDigest( + "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"); + digest2 = + DescriptorDigest.fromDigest( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + id = + DescriptorDigest.fromDigest( + "sha256:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); + } + + @Test + public void testCreated() { + BuildResult container = new BuildResult(digest1, id); + Assert.assertEquals(digest1, container.getImageDigest()); + Assert.assertEquals(id, container.getImageId()); + } + + @Test + public void testEquality() { + BuildResult container1 = new BuildResult(digest1, id); + BuildResult container2 = new BuildResult(digest1, id); + BuildResult container3 = new BuildResult(digest2, id); + + Assert.assertEquals(container1, container2); + Assert.assertEquals(container1.hashCode(), container2.hashCode()); + Assert.assertNotEquals(container1, container3); + } +} diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 891464b9a8..bb0a70c12c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - `jib.extraDirectory` closure with a `path` and `permissions` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `jib.extraDirectory.path` configures the extra layer directory (still also configurable via `jib.extraDirectory = file(...)`) - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string) +- Image digest is now written to `build/jib-image.digest` ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java index 6f8951f5b1..60eca3ae02 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.security.DigestException; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.hamcrest.CoreMatchers; import org.junit.Assert; @@ -75,7 +76,8 @@ public void testBuild_defaultTarget() { } @Test - public void testDockerDaemon_defaultTarget() throws IOException, InterruptedException { + public void testDockerDaemon_defaultTarget() + throws IOException, InterruptedException, DigestException { Assert.assertEquals( "Hello, world. An argument.\n", JibRunHelper.buildToDockerDaemonAndRun( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index 6cb9755173..8519413c0b 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.IntegrationTestingConfiguration; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.io.IOException; +import java.security.DigestException; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.ClassRule; @@ -59,7 +60,7 @@ private static void assertDockerInspect(String imageReference) } @Test - public void testBuild_empty() throws IOException, InterruptedException { + public void testBuild_empty() throws IOException, InterruptedException, DigestException { String targetImage = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() @@ -72,7 +73,7 @@ public void testBuild_empty() throws IOException, InterruptedException { @Test public void testBuild_multipleTags() - throws IOException, InterruptedException, InvalidImageReferenceException { + throws IOException, InterruptedException, InvalidImageReferenceException, DigestException { String targetImage = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() @@ -84,7 +85,7 @@ public void testBuild_multipleTags() } @Test - public void testDockerDaemon_empty() throws IOException, InterruptedException { + public void testDockerDaemon_empty() throws IOException, InterruptedException, DigestException { String targetImage = "emptyimage:gradle" + System.nanoTime(); Assert.assertEquals("", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage)); Assert.assertEquals( @@ -94,7 +95,8 @@ public void testDockerDaemon_empty() throws IOException, InterruptedException { } @Test - public void testDockerDaemon_userNumeric() throws IOException, InterruptedException { + public void testDockerDaemon_userNumeric() + throws IOException, InterruptedException, DigestException { String targetImage = "emptyimage:gradle" + System.nanoTime(); JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build.gradle"); Assert.assertEquals( @@ -103,7 +105,8 @@ public void testDockerDaemon_userNumeric() throws IOException, InterruptedExcept } @Test - public void testDockerDaemon_userNames() throws IOException, InterruptedException { + public void testDockerDaemon_userNames() + throws IOException, InterruptedException, DigestException { String targetImage = "brokenuserimage:gradle" + System.nanoTime(); JibRunHelper.buildToDockerDaemon(emptyTestProject, targetImage, "build-broken-user.gradle"); Assert.assertEquals( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index cca59df9e7..f22395ed98 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -17,9 +17,14 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; 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.ArrayList; import java.util.Arrays; import java.util.List; @@ -33,7 +38,7 @@ public class JibRunHelper { static String buildAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { + throws IOException, InterruptedException, DigestException { return buildAndRun(testProject, imageReference, "build.gradle"); } @@ -42,11 +47,12 @@ static String buildAndRun( String imageReference, String gradleBuildFile, String... extraRunArguments) - throws IOException, InterruptedException { + throws IOException, InterruptedException, DigestException { BuildResult buildResult = testProject.build( "clean", "jib", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertImageDigest(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); return pullAndRunBuiltImage(imageReference, extraRunArguments); @@ -54,7 +60,7 @@ static String buildAndRun( static void buildAndRunAdditionalTag( TestProject testProject, String imageReference, String additionalTag, String expectedOutput) - throws InvalidImageReferenceException, IOException, InterruptedException { + throws InvalidImageReferenceException, IOException, InterruptedException, DigestException { BuildResult buildResult = testProject.build( "clean", @@ -62,6 +68,7 @@ static void buildAndRunAdditionalTag( "-D_TARGET_IMAGE=" + imageReference, "-D_ADDITIONAL_TAG=" + additionalTag); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertImageDigest(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); String additionalImageReference = @@ -77,7 +84,7 @@ static void buildAndRunAdditionalTag( static void buildToDockerDaemon( TestProject testProject, String imageReference, String gradleBuildFile) - throws IOException, InterruptedException { + throws IOException, InterruptedException, DigestException { BuildResult buildResult = testProject.build( "clean", @@ -85,6 +92,7 @@ static void buildToDockerDaemon( "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); + assertImageDigest(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); String history = new Command("docker", "history", imageReference).run(); @@ -92,7 +100,7 @@ static void buildToDockerDaemon( } static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) - throws IOException, InterruptedException { + throws IOException, InterruptedException, DigestException { buildToDockerDaemon(testProject, imageReference, "build.gradle"); return new Command("docker", "run", "--rm", imageReference).run(); } @@ -122,6 +130,13 @@ static void assertCreationTimeEpoch(String imageReference) new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } + static void assertImageDigest(Path projectRoot) throws IOException, DigestException { + Path digestPath = projectRoot.resolve("build/jib-image.digest"); + Assert.assertTrue(Files.exists(digestPath)); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + DescriptorDigest.fromDigest(digest); + } + /** * Pulls a built image and attempts to run it. Also verifies the container configuration and * history of the built image. diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index e24e5ace5a..31ee5c0a59 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.file.Files; +import java.security.DigestException; import java.time.Instant; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; @@ -121,7 +122,7 @@ public void setup() throws IOException, InterruptedException { } @Test - public void testBuild_simple() throws IOException, InterruptedException { + public void testBuild_simple() throws IOException, InterruptedException, DigestException { String targetImage = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() @@ -169,7 +170,7 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr } @Test - public void testDockerDaemon_simple() throws IOException, InterruptedException { + public void testDockerDaemon_simple() throws IOException, InterruptedException, DigestException { String targetImage = "simpleimage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); Assert.assertEquals( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 6f6ffbc60c..3aaef472d1 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -25,6 +25,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.security.DigestException; import javax.annotation.Nullable; import org.junit.After; import org.junit.Assert; @@ -63,12 +64,13 @@ public void tearDown() throws IOException, InterruptedException { } @Test - public void testBuild_jettyServlet25() throws IOException, InterruptedException { + public void testBuild_jettyServlet25() throws IOException, InterruptedException, DigestException { verifyBuildAndRun(servlet25Project, "war_jetty_servlet25:gradle", "build.gradle"); } @Test - public void testBuild_tomcatServlet25() throws IOException, InterruptedException { + public void testBuild_tomcatServlet25() + throws IOException, InterruptedException, DigestException { verifyBuildAndRun(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); } @@ -95,7 +97,7 @@ public void testDockerContext_tomcatServlet25() throws IOException, InterruptedE } private void verifyBuildAndRun(TestProject project, String label, String gradleBuildFile) - throws IOException, InterruptedException { + throws IOException, InterruptedException, DigestException { String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; String targetImage = nameBase + label + System.nanoTime(); String output = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 6bac5250e5..4b4e916630 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -32,6 +32,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.base.Preconditions; import java.io.IOException; +import java.nio.file.Path; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -89,6 +90,7 @@ public void buildDocker() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + Path buildOutput = getProject().getBuildDir().toPath(); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); @@ -126,6 +128,7 @@ public void buildDocker() .build(); BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 26cb572ec1..2517c3228d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -36,6 +36,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; +import java.nio.file.Path; import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -87,6 +88,7 @@ public void buildImage() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + Path buildOutput = getProject().getBuildDir().toPath(); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( @@ -142,6 +144,7 @@ public void buildImage() .build(); BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 053fdaf7fc..9c9af92fe6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -32,7 +32,6 @@ import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.file.FileCollection; @@ -89,16 +88,7 @@ public FileCollection getInputFiles() { */ @OutputFile public String getOutputFile() { - return getTargetPath(); - } - - /** - * Returns the output directory for the tarball. By default, it is {@code build/jib-image.tar}. - * - * @return the output directory - */ - private String getTargetPath() { - return getProject().getBuildDir().toPath().resolve("jib-image.tar").toString(); + return getTargetPath().toString(); } @TaskAction @@ -115,6 +105,7 @@ public void buildTar() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + Path buildOutput = getProject().getBuildDir().toPath(); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); @@ -131,7 +122,7 @@ public void buildTar() : getProject().getVersion().toString(), gradleHelpfulSuggestionsBuilder.build()); - Path tarOutputPath = Paths.get(getTargetPath()); + Path tarOutputPath = getTargetPath(); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); PluginConfigurationProcessor pluginConfigurationProcessor = @@ -153,6 +144,7 @@ public void buildTar() .build(); BuildStepsRunner.forBuildTar(tarOutputPath) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, @@ -166,4 +158,13 @@ public BuildTarTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } + + /** + * Returns the output directory for the tarball. By default, it is {@code build/jib-image.tar}. + * + * @return the output directory + */ + private Path getTargetPath() { + return getProject().getBuildDir().toPath().resolve("jib-image.tar"); + } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index f9f995acd9..4a62b43fb0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - `` object with a `` and `` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `` configures the extra layer directory (still also configurable via `...`) - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) +- Image digest is now written to `build/jib-image.digest` ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index cc4ebf6f72..caf11a5b49 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -32,6 +32,8 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -72,6 +74,7 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor.getExtraDirectoryPath(this), PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = @@ -107,6 +110,7 @@ public void execute() throws MojoExecutionException { .build(); BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index e549283174..f00b6fb230 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -37,6 +37,8 @@ import com.google.common.base.Strings; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; @@ -95,6 +97,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.getExtraDirectoryPath(this), PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( @@ -157,6 +160,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index c9486512d4..7458903737 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -68,6 +68,7 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor.getExtraDirectoryPath(this), PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); try { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = @@ -82,8 +83,7 @@ public void execute() throws MojoExecutionException { getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); - Path tarOutputPath = - Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar"); + Path tarOutputPath = buildOutput.resolve("jib-image.tar"); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); PluginConfigurationProcessor pluginConfigurationProcessor = @@ -105,6 +105,7 @@ public void execute() throws MojoExecutionException { .build(); BuildStepsRunner.forBuildTar(tarOutputPath) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 7d65ea0462..b8c54a67aa 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; import java.nio.file.Path; +import java.security.DigestException; import java.time.Instant; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; @@ -43,7 +44,7 @@ public class BuildDockerMojoIntegrationTest { new TestProject(testPlugin, "default-target"); private static void buildToDockerDaemon(Path projectRoot, String imageReference, String pomXml) - throws VerificationException { + throws VerificationException, DigestException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); @@ -52,6 +53,8 @@ private static void buildToDockerDaemon(Path projectRoot, String imageReference, verifier.executeGoal("jib:dockerBuild"); verifier.verifyErrorFreeLog(); + + BuildImageMojoIntegrationTest.assertImageDigest(projectRoot); } /** @@ -59,7 +62,7 @@ private static void buildToDockerDaemon(Path projectRoot, String imageReference, * imageReference}. */ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageReference) - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { buildToDockerDaemon(projectRoot, imageReference, "pom.xml"); String dockerInspect = new Command("docker", "inspect", imageReference).run(); @@ -79,11 +82,13 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe + " \"key1\": \"value1\",\n" + " \"key2\": \"value2\"\n" + " }")); + return new Command("docker", "run", "--rm", imageReference).run(); } @Test - public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + public void testExecute_simple() + throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "simpleimage:maven" + System.nanoTime(); Instant before = Instant.now(); @@ -97,7 +102,8 @@ public void testExecute_simple() throws VerificationException, IOException, Inte } @Test - public void testExecute_empty() throws InterruptedException, IOException, VerificationException { + public void testExecute_empty() + throws InterruptedException, IOException, VerificationException, DigestException { String targetImage = "emptyimage:maven" + System.nanoTime(); Assert.assertEquals( @@ -109,7 +115,7 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi @Test public void testExecute_defaultTarget() - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { Assert.assertEquals( "Hello, world. An argument.\n", buildToDockerDaemonAndRun( @@ -124,7 +130,7 @@ public void testExecute_skipJibGoal() throws VerificationException, IOException @Test public void testExecute_userNumeric() - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "emptyimage:maven" + System.nanoTime(); buildToDockerDaemon(emptyTestProject.getProjectRoot(), targetImage, "pom.xml"); Assert.assertEquals( @@ -134,7 +140,7 @@ public void testExecute_userNumeric() @Test public void testExecute_userNames() - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "brokenuserimage:maven" + System.nanoTime(); buildToDockerDaemon(emptyTestProject.getProjectRoot(), targetImage, "pom-broken-user.xml"); Assert.assertEquals( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index fd75e3a96c..b884eae60f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; @@ -27,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.DigestException; import java.time.Instant; import java.util.Arrays; import java.util.regex.Matcher; @@ -75,12 +77,21 @@ private static String getGcrImageReference(String label) { return nameBase + label + System.nanoTime(); } + static String assertImageDigest(Path projectRoot) throws IOException, DigestException { + Path digestPath = projectRoot.resolve("target/jib-image.digest"); + Assert.assertTrue(Files.exists(digestPath)); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + return DescriptorDigest.fromDigest(digest).toString(); + } + /** * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. + * + * @throws DigestException */ private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); @@ -102,13 +113,24 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole verifier.verifyErrorFreeLog(); - return pullAndRunBuiltImage(imageReference); + String output = pullAndRunBuiltImage(imageReference); + + try { + String digest = assertImageDigest(projectRoot); + String imageReferenceWithDigest = + ImageReference.parse(imageReference).withTag(digest).toString(); + Assert.assertEquals(output, pullAndRunBuiltImage(imageReferenceWithDigest)); + } catch (InvalidImageReferenceException ex) { + throw new AssertionError("error replacing tag with digest"); + } + + return output; } private static String buildAndRunAdditionalTag( Path projectRoot, String imageReference, String additionalTag) throws VerificationException, InvalidImageReferenceException, IOException, - InterruptedException { + InterruptedException, DigestException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); @@ -124,6 +146,11 @@ private static String buildAndRunAdditionalTag( String additionalOutput = pullAndRunBuiltImage(additionalImageReference); Assert.assertEquals(output, additionalOutput); + String digest = assertImageDigest(projectRoot); + String digestImageReference = ImageReference.parse(imageReference).withTag(digest).toString(); + String digestOutput = pullAndRunBuiltImage(digestImageReference); + Assert.assertEquals(output, digestOutput); + assertCreationTimeEpoch(imageReference); assertCreationTimeEpoch(additionalImageReference); @@ -132,7 +159,7 @@ private static String buildAndRunAdditionalTag( private static String buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) - throws VerificationException, IOException, InterruptedException { + throws VerificationException, IOException, InterruptedException, DigestException { Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); @@ -145,6 +172,8 @@ private static String buildAndRunComplex( verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); verifier.verifyErrorFreeLog(); + assertImageDigest(simpleTestProject.getProjectRoot()); + // Verify output targetRegistry.pull(imageReference); assertDockerInspectParameters(imageReference); @@ -233,7 +262,8 @@ public void tearDown() throws IOException, InterruptedException { } @Test - public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + public void testExecute_simple() + throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = getGcrImageReference("simpleimage:maven"); // Test empty output error @@ -279,7 +309,8 @@ public void testExecute_simple() throws VerificationException, IOException, Inte } @Test - public void testExecute_empty() throws InterruptedException, IOException, VerificationException { + public void testExecute_empty() + throws InterruptedException, IOException, VerificationException, DigestException { String targetImage = getGcrImageReference("emptyimage:maven"); Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); assertCreationTimeEpoch(targetImage); @@ -288,7 +319,7 @@ public void testExecute_empty() throws InterruptedException, IOException, Verifi @Test public void testExecute_multipleTags() throws IOException, InterruptedException, InvalidImageReferenceException, - VerificationException { + VerificationException, DigestException { String targetImage = getGcrImageReference("multitag-image:maven"); Assert.assertEquals( "", @@ -317,7 +348,7 @@ public void testExecute_defaultTarget() throws IOException { @Test public void testExecute_complex() - throws IOException, InterruptedException, VerificationException { + throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", @@ -326,7 +357,7 @@ public void testExecute_complex() @Test public void testExecute_complex_sameFromAndToRegistry() - throws IOException, InterruptedException, VerificationException { + throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 856fc0294a..febe2d8830 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; +import java.security.DigestException; import java.time.Instant; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; @@ -38,9 +39,12 @@ public class BuildTarMojoIntegrationTest { /** * Builds and runs jib:buildTar on a project at {@code projectRoot} pushing to {@code * imageReference}. + * + * @throws DigestException */ @Test - public void testExecute_simple() throws VerificationException, IOException, InterruptedException { + public void testExecute_simple() + throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "simpleimage:maven" + System.nanoTime(); Instant before = Instant.now(); @@ -52,6 +56,8 @@ public void testExecute_simple() throws VerificationException, IOException, Inte verifier.executeGoal("jib:" + BuildTarMojo.GOAL_NAME); verifier.verifyErrorFreeLog(); + BuildImageMojoIntegrationTest.assertImageDigest(simpleTestProject.getProjectRoot()); + new Command( "docker", "load", diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 8010b659fb..90855ea288 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -37,11 +37,14 @@ import com.google.common.base.Verify; import java.io.IOException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; import org.apache.http.conn.HttpHostConnectException; /** Runs a {@link BuildSteps} and builds helpful error messages. */ @@ -156,6 +159,7 @@ private static String capitalizeFirstLetter(String string) { private final String startupMessage; private final String successMessage; + @Nullable private Path imageDigestOutputPath; @VisibleForTesting BuildStepsRunner(String startupMessage, String successMessage) { @@ -209,6 +213,12 @@ public JibContainer build( eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); + // when an image is built, write out the digest + if (imageDigestOutputPath != null) { + String imageDigest = jibContainer.getDigest().toString(); + Files.write(imageDigestOutputPath, imageDigest.getBytes(StandardCharsets.UTF_8)); + } + return jibContainer; } catch (ExecutionException executionException) { @@ -266,4 +276,16 @@ public JibContainer build( throw new IllegalStateException("unreachable"); } + + /** + * Set the location where the image digest will be saved. If {@code null} then digest is not + * saved. + * + * @param imageDigestOutputPath the location to write the image digest or {@code null} to skip + * @return this + */ + public BuildStepsRunner writeImageDigest(@Nullable Path imageDigestOutputPath) { + this.imageDigestOutputPath = imageDigestOutputPath; + return this; + } } From af00e2d91e29b29b7a55c20b90e5b1270bc1391f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 29 Oct 2018 17:29:53 -0400 Subject: [PATCH 0321/2020] Share PluginConfigurationProcessor between Gradle and Maven (#1163) --- .../tools/jib/gradle/AuthParameters.java | 16 +- .../tools/jib/gradle/BuildDockerTask.java | 124 +++--- .../tools/jib/gradle/BuildImageTask.java | 155 ++++--- .../cloud/tools/jib/gradle/BuildTarTask.java | 127 +++--- .../tools/jib/gradle/DockerContextTask.java | 50 ++- .../jib/gradle/GradleProjectProperties.java | 15 - .../jib/gradle/GradleRawConfiguration.java | 118 +++++ .../cloud/tools/jib/gradle/JibPlugin.java | 19 - .../gradle/PluginConfigurationProcessor.java | 231 ---------- .../cloud/tools/jib/gradle/TaskCommon.java | 79 ++++ .../jib/gradle/DockerContextTaskTest.java | 21 +- .../gradle/GradleRawConfigurationTest.java | 100 +++++ .../cloud/tools/jib/gradle/JibPluginTest.java | 28 +- .../PluginConfigurationProcessorTest.java | 269 ----------- .../tools/jib/maven/BuildDockerMojo.java | 47 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 155 ++++--- .../cloud/tools/jib/maven/BuildTarMojo.java | 47 +- .../tools/jib/maven/DockerContextMojo.java | 57 ++- .../jib/maven/JibPluginConfiguration.java | 21 +- .../jib/maven/MavenProjectProperties.java | 17 - .../jib/maven/MavenRawConfiguration.java | 134 ++++++ .../maven/MavenSettingsServerCredentials.java | 65 ++- .../cloud/tools/jib/maven/MojoCommon.java | 112 +++++ .../maven/PluginConfigurationProcessor.java | 367 --------------- .../jib/maven/DockerContextMojoTest.java | 17 +- .../jib/maven/MavenRawConfigurationTest.java | 121 +++++ .../MavenSettingsServerCredentialsTest.java | 66 +-- .../PluginConfigurationProcessorTest.java | 418 ------------------ .../common/AppRootInvalidException.java | 35 ++ .../jib/plugins/common/AuthProperty.java | 7 + .../InferredAuthRetrievalException.java | 29 ++ .../jib/plugins/common/MainClassResolver.java | 8 +- .../common/PluginConfigurationProcessor.java | 263 +++++++++++ .../jib/plugins/common/RawConfiguration.java | 62 +++ .../PluginConfigurationProcessorTest.java | 375 ++++++++++++++++ 35 files changed, 2009 insertions(+), 1766 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index e76aa7fca0..245a951426 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -32,8 +32,7 @@ public class AuthParameters implements AuthProperty { @Nullable private String username; @Nullable private String password; - private String usernameDescriptor; - private String passwordDescriptor; + private String descriptor; /** * Constructs a new {@link AuthParameters}. @@ -42,20 +41,25 @@ public class AuthParameters implements AuthProperty { */ @Inject public AuthParameters(String descriptor) { - this.usernameDescriptor = descriptor + ".username"; - this.passwordDescriptor = descriptor + ".password"; + this.descriptor = descriptor; + } + + @Internal + @Override + public String getPropertyDescriptor() { + return Preconditions.checkNotNull(descriptor); } @Internal @Override public String getUsernamePropertyDescriptor() { - return Preconditions.checkNotNull(usernameDescriptor); + return Preconditions.checkNotNull(descriptor) + ".username"; } @Internal @Override public String getPasswordPropertyDescriptor() { - return Preconditions.checkNotNull(passwordDescriptor); + return Preconditions.checkNotNull(descriptor) + ".password"; } @Input diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 4b4e916630..c755faa72b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -26,10 +26,15 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -74,7 +79,8 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildDocker() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException { + CacheDirectoryCreationException, MainClassInferenceException, + InferredAuthRetrievalException { if (!DOCKER_CLIENT.isDockerInstalled()) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); @@ -82,59 +88,69 @@ public void buildDocker() // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - Path buildOutput = getProject().getBuildDir().toPath(); - - GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTo().getImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion().toString().equals("unspecified") - ? "latest" - : getProject().getVersion().toString(), - gradleHelpfulSuggestionsBuilder.build()); - - DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); - - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - getLogger(), jibExtension, gradleProjectProperties); - - JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, jibExtension, gradleProjectProperties); - - HelpfulSuggestions helpfulSuggestions = - gradleHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .build(); - - BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .build( - jibContainerBuilder, - containerizer, - eventDispatcher, - gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); + TaskCommon.disableHttpLogging(); + + try { + AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); + RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + + GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + ImageReference targetImageReference = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + jibExtension.getTo().getImage(), + eventDispatcher, + getProject().getName(), + getProject().getVersion().toString().equals("unspecified") + ? "latest" + : getProject().getVersion().toString(), + gradleHelpfulSuggestionsBuilder.build()); + + DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); + + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor.getJibContainerBuilder(); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); + + HelpfulSuggestions helpfulSuggestions = + gradleHelpfulSuggestionsBuilder + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(targetImageReference) + .build(); + + Path buildOutput = getProject().getBuildDir().toPath(); + BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } catch (AppRootInvalidException ex) { + throw new GradleException( + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + } } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 2517c3228d..6c7977167c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -27,12 +27,17 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; @@ -77,80 +82,90 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildImage() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException { + CacheDirectoryCreationException, MainClassInferenceException, + InferredAuthRetrievalException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - Path buildOutput = getProject().getBuildDir().toPath(); - - if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { + TaskCommon.disableHttpLogging(); + + try { + AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); + RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + + if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { + throw new GradleException( + HelpfulSuggestions.forToNotConfigured( + "Missing target image parameter", + "'jib.to.image'", + "build.gradle", + "gradle jib --image ")); + } + + ImageReference targetImageReference = ImageReference.parse(jibExtension.getTo().getImage()); + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); + Optional optionalToCredential = + ConfigurationPropertyValidator.getImageCredential( + eventDispatcher, + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, + jibExtension.getTo().getAuth()); + optionalToCredential.ifPresent( + toCredential -> + defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); + defaultCredentialRetrievers.setCredentialHelper(jibExtension.getTo().getCredHelper()); + + RegistryImage targetImage = RegistryImage.named(targetImageReference); + defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); + + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor + .getJibContainerBuilder() + // Only uses possibly non-Docker formats for build to registry. + .setFormat(jibExtension.getContainer().getFormat()); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); + + HelpfulSuggestions helpfulSuggestions = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(targetImageReference) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) + .build(); + + Path buildOutput = getProject().getBuildDir().toPath(); + BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } catch (AppRootInvalidException ex) { throw new GradleException( - HelpfulSuggestions.forToNotConfigured( - "Missing target image parameter", - "'jib.to.image'", - "build.gradle", - "gradle jib --image ")); + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); } - - ImageReference targetImageReference = ImageReference.parse(jibExtension.getTo().getImage()); - - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); - Optional optionalToCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.TO_AUTH_USERNAME, - PropertyNames.TO_AUTH_PASSWORD, - jibExtension.getTo().getAuth()); - optionalToCredential.ifPresent( - toCredential -> - defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); - defaultCredentialRetrievers.setCredentialHelper(jibExtension.getTo().getCredHelper()); - - RegistryImage targetImage = RegistryImage.named(targetImageReference); - defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); - - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - getLogger(), jibExtension, gradleProjectProperties); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor - .getJibContainerBuilder() - // Only uses possibly non-Docker formats for build to registry. - .setFormat(jibExtension.getContainer().getFormat()); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, jibExtension, gradleProjectProperties); - - HelpfulSuggestions helpfulSuggestions = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) - .build(); - - BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .build( - jibContainerBuilder, - containerizer, - eventDispatcher, - gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 9c9af92fe6..150fde2daa 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -25,15 +25,21 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Nested; @@ -94,63 +100,74 @@ public String getOutputFile() { @TaskAction public void buildTar() throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, - CacheDirectoryCreationException { + CacheDirectoryCreationException, MainClassInferenceException, + InferredAuthRetrievalException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - Path buildOutput = getProject().getBuildDir().toPath(); - - GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(gradleProjectProperties.getEventHandlers()); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTo().getImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion().toString().equals("unspecified") - ? "latest" - : getProject().getVersion().toString(), - gradleHelpfulSuggestionsBuilder.build()); - - Path tarOutputPath = getTargetPath(); - TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); - - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - getLogger(), jibExtension, gradleProjectProperties); - - JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, jibExtension, gradleProjectProperties); - - HelpfulSuggestions helpfulSuggestions = - gradleHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .build(); - - BuildStepsRunner.forBuildTar(tarOutputPath) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .build( - jibContainerBuilder, - containerizer, - eventDispatcher, - gradleProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); + TaskCommon.disableHttpLogging(); + try { + AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); + RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + + GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = + new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + ImageReference targetImageReference = + ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + jibExtension.getTo().getImage(), + eventDispatcher, + getProject().getName(), + getProject().getVersion().toString().equals("unspecified") + ? "latest" + : getProject().getVersion().toString(), + gradleHelpfulSuggestionsBuilder.build()); + + Path tarOutputPath = getTargetPath(); + TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); + + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor.getJibContainerBuilder(); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); + + HelpfulSuggestions helpfulSuggestions = + gradleHelpfulSuggestionsBuilder + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(targetImageReference) + .build(); + + Path buildOutput = getProject().getBuildDir().toPath(); + BuildStepsRunner.forBuildTar(tarOutputPath) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .build( + jibContainerBuilder, + containerizer, + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } catch (AppRootInvalidException ex) { + throw new GradleException( + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + } } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index 3e21fcdd47..fa10f044de 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -20,7 +20,11 @@ import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; import java.io.IOException; @@ -100,10 +104,10 @@ public void setTargetDir(String targetDir) { } @TaskAction - public void generateDockerContext() { + public void generateDockerContext() throws MainClassInferenceException { Preconditions.checkNotNull(jibExtension); - Preconditions.checkNotNull(jibExtension.getFrom().getImage()); JibSystemProperties.checkHttpTimeoutProperty(); + TaskCommon.disableHttpLogging(); if (!jibExtension.getExtraDirectory().getPermissions().isEmpty()) { getLogger() @@ -112,30 +116,30 @@ public void generateDockerContext() { + "context generator - building using Docker may produce unexpected results."); } - // TODO: Instead of disabling logging, have authentication credentials be provided - PluginConfigurationProcessor.disableHttpLogging(); - - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(jibExtension); - GradleProjectProperties gradleProjectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - String targetDir = getTargetDir(); - - List entrypoint = - PluginConfigurationProcessor.computeEntrypoint( - getLogger(), jibExtension, gradleProjectProperties); - try { + AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject( + getProject(), + getLogger(), + jibExtension.getExtraDirectory().getPath(), + jibExtension.getExtraDirectory().getPermissions(), + appRoot); + RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + String targetDir = getTargetDir(); + + List entrypoint = + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties); + String baseImage = + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + // Validate port input, but don't save the output because we don't want the ranges expanded // here. ExposedPortsParser.parse(jibExtension.getContainer().getPorts()); - new JavaDockerContextGenerator(gradleProjectProperties.getJavaLayerConfigurations()) - .setBaseImage(jibExtension.getFrom().getImage()) + new JavaDockerContextGenerator(projectProperties.getJavaLayerConfigurations()) + .setBaseImage(baseImage) .setEntrypoint(entrypoint) .setProgramArguments(jibExtension.getContainer().getArgs()) .setExposedPorts(jibExtension.getContainer().getPorts()) @@ -161,6 +165,10 @@ public void generateDockerContext() { "Export Docker context failed", "check if the command-line option `--jibTargetDir` is set correctly"), ex); + + } catch (AppRootInvalidException ex) { + throw new GradleException( + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 680bc82f7a..13675b0965 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -22,8 +22,6 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; -import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; @@ -155,19 +153,6 @@ public boolean isWarProject() { return getWarTask(project) != null; } - /** - * Tries to resolve the main class. - * - * @throws GradleException if resolving the main class fails. - */ - String getMainClass(JibExtension jibExtension) { - try { - return MainClassResolver.resolveMainClass(jibExtension.getContainer().getMainClass(), this); - } catch (MainClassInferenceException ex) { - throw new GradleException(ex.getMessage(), ex); - } - } - /** * Returns the input files for a task. * diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java new file mode 100644 index 0000000000..0290a5a258 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Gradle-specific adapter for providing raw configuration parameter values. */ +class GradleRawConfiguration implements RawConfiguration { + + private final JibExtension jibExtension; + + GradleRawConfiguration(JibExtension jibExtension) { + this.jibExtension = jibExtension; + } + + @Override + public Optional getFromImage() { + return Optional.ofNullable(jibExtension.getFrom().getImage()); + } + + @Override + public AuthProperty getFromAuth() { + return jibExtension.getFrom().getAuth(); + } + + @Override + public Optional getFromCredHelper() { + return Optional.ofNullable(jibExtension.getFrom().getCredHelper()); + } + + @Override + public Iterable getToTags() { + return jibExtension.getTo().getTags(); + } + + @Override + public Optional> getEntrypoint() { + return Optional.ofNullable(jibExtension.getContainer().getEntrypoint()); + } + + @Override + public Optional> getProgramArguments() { + return Optional.ofNullable(jibExtension.getContainer().getArgs()); + } + + @Override + public Optional getMainClass() { + return Optional.ofNullable(jibExtension.getContainer().getMainClass()); + } + + @Override + public List getJvmFlags() { + return jibExtension.getContainer().getJvmFlags(); + } + + @Override + public String getAppRoot() { + return jibExtension.getContainer().getAppRoot(); + } + + @Override + public Map getEnvironment() { + return jibExtension.getContainer().getEnvironment(); + } + + @Override + public Map getLabels() { + return jibExtension.getContainer().getLabels(); + } + + @Override + public List getPorts() { + return jibExtension.getContainer().getPorts(); + } + + @Override + public Optional getUser() { + return Optional.ofNullable(jibExtension.getContainer().getUser()); + } + + @Override + public boolean getUseCurrentTimestamp() { + return jibExtension.getContainer().getUseCurrentTimestamp(); + } + + @Override + public boolean getAllowInsecureRegistries() { + return jibExtension.getAllowInsecureRegistries(); + } + + @Override + public boolean getUseOnlyProjectCache() { + return jibExtension.getUseOnlyProjectCache(); + } + + @Override + public Optional getInferredAuth(String authTarget) { + return Optional.empty(); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 732ec900d1..eba9879645 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.stream.Collectors; @@ -44,9 +43,6 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; - @VisibleForTesting static final String DEFAULT_FROM_IMAGE = "gcr.io/distroless/java"; - @VisibleForTesting static final String DEFAULT_WAR_FROM_IMAGE = "gcr.io/distroless/java/jetty"; - /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of * configuration [compile, runtime, etc]. It potentially will collect common test libraries in @@ -114,19 +110,10 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { - // TODO move this to a separate place try { War warTask = GradleProjectProperties.getWarTask(project); Task dependsOnTask; if (warTask != null) { - if (jibExtension.getFrom().getImage() == null) { - jibExtension.getFrom().setImage(DEFAULT_WAR_FROM_IMAGE); - } - if (jibExtension.getContainer().getAppRoot().isEmpty()) { - jibExtension - .getContainer() - .setAppRoot(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); - } ExplodedWarTask explodedWarTask = (ExplodedWarTask) project @@ -139,12 +126,6 @@ public void apply(Project project) { // Have all tasks depend on the 'jibExplodedWar' task. dependsOnTask = explodedWarTask; } else { - if (jibExtension.getFrom().getImage() == null) { - jibExtension.getFrom().setImage(DEFAULT_FROM_IMAGE); - } - if (jibExtension.getContainer().getAppRoot().isEmpty()) { - jibExtension.getContainer().setAppRoot(JavaLayerConfigurations.DEFAULT_APP_ROOT); - } // Have all tasks depend on the 'classes' task. dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java deleted file mode 100644 index e4159374d5..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessor.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.Jib; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; -import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; -import com.google.cloud.tools.jib.plugins.common.ProjectProperties; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import com.google.common.base.Preconditions; -import java.io.FileNotFoundException; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import javax.annotation.Nullable; -import org.gradle.api.GradleException; -import org.gradle.api.logging.Logger; -import org.gradle.internal.logging.events.LogEvent; -import org.gradle.internal.logging.events.OutputEventListener; -import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; -import org.slf4j.LoggerFactory; - -/** Configures and provides builders for the image building tasks. */ -class PluginConfigurationProcessor { - - /** - * Gets the value of the {@code container.appRoot} parameter. Throws {@link GradleException} if it - * is not an absolute path in Unix-style. - * - * @param jibExtension the {@link JibExtension} providing the configuration data - * @return the app root value - * @throws GradleException if the app root is not an absolute path in Unix-style - */ - static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension) { - String appRoot = jibExtension.getContainer().getAppRoot(); - try { - return AbsoluteUnixPath.get(appRoot); - } catch (IllegalArgumentException ex) { - throw new GradleException("container.appRoot is not an absolute Unix-style path: " + appRoot); - } - } - - /** Disables annoying Apache HTTP client logging. */ - static void disableHttpLogging() { - // Disables Apache HTTP client logging. - OutputEventListenerBackedLoggerContext context = - (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); - OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); - context.setOutputEventListener( - event -> { - LogEvent logEvent = (LogEvent) event; - if (!logEvent.getCategory().contains("org.apache")) { - defaultOutputEventListener.onOutput(event); - } - }); - - // Disables Google HTTP client logging. - java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); - } - - /** - * Sets up {@link BuildConfiguration} that is common among the image building tasks. This includes - * setting up the base image reference/authorization, container configuration, cache - * configuration, and layer configuration. - * - * @param logger the logger used to display messages - * @param jibExtension the {@link JibExtension} providing the configuration data - * @param projectProperties used for providing additional information - * @return a new {@link PluginConfigurationProcessor} containing pre-configured builders - * @throws InvalidImageReferenceException if parsing the base image configuration fails - */ - static PluginConfigurationProcessor processCommonConfiguration( - Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) - throws InvalidImageReferenceException, NumberFormatException, FileNotFoundException { - JibSystemProperties.checkHttpTimeoutProperty(); - - // TODO: Instead of disabling logging, have authentication credentials be provided - disableHttpLogging(); - ImageReference baseImageReference = - ImageReference.parse(Preconditions.checkNotNull(jibExtension.getFrom().getImage())); - - if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { - logger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); - Optional optionalFromCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.FROM_AUTH_USERNAME, - PropertyNames.FROM_AUTH_PASSWORD, - jibExtension.getFrom().getAuth()); - optionalFromCredential.ifPresent( - fromCredential -> - defaultCredentialRetrievers.setKnownCredential(fromCredential, "jib.from.auth")); - defaultCredentialRetrievers.setCredentialHelper(jibExtension.getFrom().getCredHelper()); - - List entrypoint = computeEntrypoint(logger, jibExtension, projectProperties); - - RegistryImage baseImage = RegistryImage.named(baseImageReference); - defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); - - JibContainerBuilder jibContainerBuilder = - Jib.from(baseImage) - .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) - .setEntrypoint(entrypoint) - .setProgramArguments(jibExtension.getContainer().getArgs()) - .setEnvironment(jibExtension.getContainer().getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(jibExtension.getContainer().getPorts())) - .setLabels(jibExtension.getContainer().getLabels()) - .setUser(jibExtension.getContainer().getUser()); - if (jibExtension.getContainer().getUseCurrentTimestamp()) { - logger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - jibContainerBuilder.setCreationTime(Instant.now()); - } - - return new PluginConfigurationProcessor( - jibContainerBuilder, baseImageReference, optionalFromCredential.isPresent()); - } - - static void configureContainerizer( - Containerizer containerizer, JibExtension jibExtension, ProjectProperties projectProperties) { - containerizer - .setToolName(GradleProjectProperties.TOOL_NAME) - .setEventHandlers(projectProperties.getEventHandlers()) - .setAllowInsecureRegistries(jibExtension.getAllowInsecureRegistries()) - .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) - .setApplicationLayersCache(projectProperties.getCacheDirectory()); - - jibExtension.getTo().getTags().forEach(containerizer::withAdditionalTag); - - if (jibExtension.getUseOnlyProjectCache()) { - containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); - } - } - - /** - * Compute the container entrypoint, in this order: - * - *

    - *
  1. the user specified one, if set - *
  2. for a WAR project, null (it must be inherited from base image) - *
  3. for a non-WAR project, by resolving the main class - *
- * - * @param logger the logger used to display messages - * @param jibExtension the {@link JibExtension} providing the configuration data - * @param projectProperties used for providing additional information - * @return the entrypoint - */ - @Nullable - static List computeEntrypoint( - Logger logger, JibExtension jibExtension, GradleProjectProperties projectProperties) { - ContainerParameters parameters = jibExtension.getContainer(); - if (parameters.getEntrypoint() != null && !parameters.getEntrypoint().isEmpty()) { - if (parameters.getMainClass() != null || !parameters.getJvmFlags().isEmpty()) { - logger.warn("mainClass and jvmFlags are ignored when entrypoint is specified"); - } - return parameters.getEntrypoint(); - } - - if (projectProperties.isWarProject()) { - return null; - } - - String mainClass = projectProperties.getMainClass(jibExtension); - return JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get(parameters.getAppRoot()), parameters.getJvmFlags(), mainClass); - } - - private final JibContainerBuilder jibContainerBuilder; - private final ImageReference baseImageReference; - private final boolean isBaseImageCredentialPresent; - - private PluginConfigurationProcessor( - JibContainerBuilder jibContainerBuilder, - ImageReference baseImageReference, - boolean isBaseImageCredentialPresent) { - this.jibContainerBuilder = jibContainerBuilder; - this.baseImageReference = baseImageReference; - this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; - } - - JibContainerBuilder getJibContainerBuilder() { - return jibContainerBuilder; - } - - ImageReference getBaseImageReference() { - return baseImageReference; - } - - boolean isBaseImageCredentialPresent() { - return isBaseImageCredentialPresent; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java new file mode 100644 index 0000000000..98f7347993 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; +import java.util.logging.Level; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.internal.logging.events.LogEvent; +import org.gradle.internal.logging.events.OutputEventListener; +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; +import org.slf4j.LoggerFactory; + +/** Collection of common methods to share between Gradle tasks. */ +class TaskCommon { + + /** + * Gets the value of the {@code container.appRoot} parameter. Throws {@link GradleException} if it + * is not an absolute path in Unix-style. + * + * @param jibExtension the {@link JibExtension} providing the configuration data + * @return the app root value + * @throws AppRootInvalidException if the app root is not an absolute path in Unix-style + */ + // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead + static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension, Project project) + throws AppRootInvalidException { + String appRoot = jibExtension.getContainer().getAppRoot(); + if (appRoot.isEmpty()) { + appRoot = + GradleProjectProperties.getWarTask(project) != null + ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT + : JavaLayerConfigurations.DEFAULT_APP_ROOT; + } + try { + return AbsoluteUnixPath.get(appRoot); + } catch (IllegalArgumentException ex) { + throw new AppRootInvalidException(appRoot, appRoot, ex); + } + } + + /** Disables annoying Apache HTTP client logging. */ + // TODO: Instead of disabling logging, have authentication credentials be provided + static void disableHttpLogging() { + // Disables Apache HTTP client logging. + OutputEventListenerBackedLoggerContext context = + (OutputEventListenerBackedLoggerContext) LoggerFactory.getILoggerFactory(); + OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); + context.setOutputEventListener( + event -> { + LogEvent logEvent = (LogEvent) event; + if (!logEvent.getCategory().contains("org.apache")) { + defaultOutputEventListener.onOutput(event); + } + }); + + // Disables Google HTTP client logging. + java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); + } + + private TaskCommon() {} +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java index a768010fa7..5751127b2c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Files; @@ -77,7 +78,7 @@ public void setUp() throws IOException { } @Test - public void testEntrypoint() throws IOException { + public void testEntrypoint() throws IOException, MainClassInferenceException { task.generateDockerContext(); Assert.assertEquals( @@ -86,7 +87,7 @@ public void testEntrypoint() throws IOException { } @Test - public void testEntrypoint_nonDefaultAppRoot() throws IOException { + public void testEntrypoint_nonDefaultAppRoot() throws IOException, MainClassInferenceException { Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); task.generateDockerContext(); @@ -97,7 +98,7 @@ public void testEntrypoint_nonDefaultAppRoot() throws IOException { } @Test - public void testEntrypoint_inheritedEntrypoint() throws IOException { + public void testEntrypoint_inheritedEntrypoint() throws IOException, MainClassInferenceException { Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); project.getPluginManager().apply("war"); @@ -109,7 +110,7 @@ public void testEntrypoint_inheritedEntrypoint() throws IOException { } @Test - public void testUser() throws IOException { + public void testUser() throws IOException, MainClassInferenceException { Mockito.when(containerParameters.getUser()).thenReturn("tomcat"); task.generateDockerContext(); @@ -117,14 +118,15 @@ public void testUser() throws IOException { } @Test - public void testUser_null() throws IOException { + public void testUser_null() throws IOException, MainClassInferenceException { Mockito.when(containerParameters.getUser()).thenReturn(null); task.generateDockerContext(); Assert.assertNull(getDockerfileLine("USER")); } @Test - public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { + public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() + throws MainClassInferenceException { Mockito.when(containerParameters.getAppRoot()).thenReturn("relative/path"); try { @@ -137,7 +139,7 @@ public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { } @Test - public void testGenerateDockerContext_errorOnWindowsAppRoot() { + public void testGenerateDockerContext_errorOnWindowsAppRoot() throws MainClassInferenceException { Mockito.when(containerParameters.getAppRoot()).thenReturn("\\windows\\path"); try { @@ -150,7 +152,8 @@ public void testGenerateDockerContext_errorOnWindowsAppRoot() { } @Test - public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { + public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() + throws MainClassInferenceException { Mockito.when(containerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); try { @@ -164,7 +167,7 @@ public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { } @Test - public void testGenerateDockerContext_env() throws IOException { + public void testGenerateDockerContext_env() throws IOException, MainClassInferenceException { task.generateDockerContext(); Assert.assertEquals("ENV envKey=\"envVal\"", getDockerfileLine("ENV")); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java new file mode 100644 index 0000000000..e57e472a37 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** Test for {@link GradleRawConfiguration}. */ +public class GradleRawConfigurationTest { + + @Test + public void testGetters() { + JibExtension jibExtension = Mockito.mock(JibExtension.class); + + AuthParameters authParameters = Mockito.mock(AuthParameters.class); + BaseImageParameters baseImageParameters = Mockito.mock(BaseImageParameters.class); + TargetImageParameters targetImageParameters = Mockito.mock(TargetImageParameters.class); + ContainerParameters containerParameters = Mockito.mock(ContainerParameters.class); + + Mockito.when(authParameters.getUsername()).thenReturn("user"); + Mockito.when(authParameters.getPassword()).thenReturn("password"); + Mockito.when(authParameters.getPropertyDescriptor()).thenReturn("auth source"); + Mockito.when(authParameters.getUsernamePropertyDescriptor()).thenReturn("from.auth.username"); + Mockito.when(authParameters.getPasswordPropertyDescriptor()).thenReturn(""); + + Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); + Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); + Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); + Mockito.when(jibExtension.getAllowInsecureRegistries()).thenReturn(true); + Mockito.when(jibExtension.getUseOnlyProjectCache()).thenReturn(true); + + Mockito.when(baseImageParameters.getCredHelper()).thenReturn("gcr"); + Mockito.when(baseImageParameters.getImage()).thenReturn("openjdk:15"); + Mockito.when(baseImageParameters.getAuth()).thenReturn(authParameters); + + Mockito.when(targetImageParameters.getTags()) + .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); + + Mockito.when(containerParameters.getAppRoot()).thenReturn("/app/root"); + Mockito.when(containerParameters.getArgs()).thenReturn(Arrays.asList("--log", "info")); + Mockito.when(containerParameters.getEntrypoint()).thenReturn(Arrays.asList("java", "Main")); + Mockito.when(containerParameters.getEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("currency", "dollar"))); + Mockito.when(containerParameters.getJvmFlags()).thenReturn(Arrays.asList("-cp", ".")); + Mockito.when(containerParameters.getLabels()) + .thenReturn(new HashMap<>(ImmutableMap.of("unit", "cm"))); + Mockito.when(containerParameters.getMainClass()).thenReturn("com.example.Main"); + Mockito.when(containerParameters.getPorts()).thenReturn(Arrays.asList("80/tcp", "0")); + Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); + Mockito.when(containerParameters.getUser()).thenReturn("admin:wheel"); + + GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); + + AuthProperty fromAuth = rawConfiguration.getFromAuth(); + Assert.assertEquals("user", fromAuth.getUsername()); + Assert.assertEquals("password", fromAuth.getPassword()); + Assert.assertEquals("auth source", fromAuth.getPropertyDescriptor()); + Assert.assertEquals("from.auth.username", fromAuth.getUsernamePropertyDescriptor()); + Assert.assertEquals("", fromAuth.getPasswordPropertyDescriptor()); + + Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); + Assert.assertEquals("/app/root", rawConfiguration.getAppRoot()); + Assert.assertEquals(Arrays.asList("java", "Main"), rawConfiguration.getEntrypoint().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("currency", "dollar")), rawConfiguration.getEnvironment()); + Assert.assertEquals("gcr", rawConfiguration.getFromCredHelper().get()); + Assert.assertEquals("openjdk:15", rawConfiguration.getFromImage().get()); + Assert.assertEquals(Arrays.asList("-cp", "."), rawConfiguration.getJvmFlags()); + Assert.assertEquals(new HashMap<>(ImmutableMap.of("unit", "cm")), rawConfiguration.getLabels()); + Assert.assertEquals("com.example.Main", rawConfiguration.getMainClass().get()); + Assert.assertEquals(Arrays.asList("80/tcp", "0"), rawConfiguration.getPorts()); + Assert.assertEquals( + Arrays.asList("--log", "info"), rawConfiguration.getProgramArguments().get()); + Assert.assertEquals( + new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); + Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); + Assert.assertTrue(rawConfiguration.getUseOnlyProjectCache()); + Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 8a6ca1fc01..ffdf955094 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -49,7 +48,8 @@ public class JibPluginTest { JibPlugin.BUILD_DOCKER_TASK_NAME, JibPlugin.DOCKER_CONTEXT_TASK_NAME, JibPlugin.BUILD_TAR_TASK_NAME); - @Rule public TemporaryFolder testProjectRoot = new TemporaryFolder(); + + @Rule public final TemporaryFolder testProjectRoot = new TemporaryFolder(); @Test public void testCheckGradleVersion_pass() { @@ -201,18 +201,6 @@ public void testWebAppProject() { .getDependsOn() .iterator() .next()); - Assert.assertEquals( - JibPlugin.DEFAULT_WAR_FROM_IMAGE, - ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) - .getJib() - .getFrom() - .getImage()); - Assert.assertEquals( - JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT, - ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) - .getJib() - .getContainer() - .getAppRoot()); } @Test @@ -222,18 +210,6 @@ public void testNonWebAppProject() { rootProject.getPluginManager().apply("java"); rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); ((ProjectInternal) rootProject).evaluate(); - Assert.assertEquals( - JibPlugin.DEFAULT_FROM_IMAGE, - ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) - .getJib() - .getFrom() - .getImage()); - Assert.assertEquals( - JavaLayerConfigurations.DEFAULT_APP_ROOT, - ((BuildImageTask) rootProject.getTasks().getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME)) - .getJib() - .getContainer() - .getAppRoot()); TaskContainer tasks = rootProject.getTasks(); try { tasks.getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java deleted file mode 100644 index 85e910a23d..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/PluginConfigurationProcessorTest.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; -import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import org.gradle.api.GradleException; -import org.gradle.api.logging.Logger; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link PluginConfigurationProcessor}. */ -@RunWith(MockitoJUnitRunner.class) -public class PluginConfigurationProcessorTest { - - private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - return JibContainerBuilderTestHelper.toBuildConfiguration( - jibContainerBuilder, - BuildConfiguration.builder(), - Containerizer.to(RegistryImage.named("ignored"))); - } - - @Mock private Logger mockLogger; - @Mock private JibExtension mockJibExtension; - @Mock private BaseImageParameters mockBaseImageParameters; - @Mock private ContainerParameters mockContainerParameters; - @Mock private GradleProjectProperties mockProjectProperties; - - @Before - public void setUp() { - Mockito.when(mockBaseImageParameters.getImage()).thenReturn("gcr.io/distroless/java"); - Mockito.when(mockJibExtension.getFrom()).thenReturn(mockBaseImageParameters); - Mockito.when(mockBaseImageParameters.getAuth()).thenReturn(new AuthParameters("mock")); - Mockito.when(mockJibExtension.getContainer()).thenReturn(mockContainerParameters); - Mockito.when(mockContainerParameters.getEntrypoint()).thenReturn(Collections.emptyList()); - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/app"); - - Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) - .thenReturn(JavaLayerConfigurations.builder().build()); - Mockito.when(mockProjectProperties.getMainClass(mockJibExtension)) - .thenReturn("java.lang.Object"); - Mockito.when(mockProjectProperties.getEventHandlers()).thenReturn(new EventHandlers()); - } - - /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ - @Test - public void testPluginConfigurationProcessor_defaults() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(mockLogger); - } - - @Test - public void testEntrypoint() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockContainerParameters.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(mockLogger); - } - - @Test - public void testUser() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockContainerParameters.getUser()).thenReturn("customUser"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); - } - - @Test - public void testUser_null() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); - } - - @Test - public void testEntrypoint_warningOnJvmFlags() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockContainerParameters.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - Mockito.when(mockContainerParameters.getJvmFlags()) - .thenReturn(Collections.singletonList("jvmFlag")); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(mockLogger) - .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); - } - - @Test - public void testEntrypoint_warningOnMainclass() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockContainerParameters.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - Mockito.when(mockContainerParameters.getMainClass()).thenReturn("java.util.Object"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(mockLogger) - .warn("mainClass and jvmFlags are ignored when entrypoint is specified"); - } - - @Test - public void testEntrypointClasspath_nonDefaultAppRoot() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/my/app"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertEquals( - "java", buildConfiguration.getContainerConfiguration().getEntrypoint().get(0)); - Assert.assertEquals( - "-cp", buildConfiguration.getContainerConfiguration().getEntrypoint().get(1)); - Assert.assertEquals( - "/my/app/resources:/my/app/classes:/my/app/libs/*", - buildConfiguration.getContainerConfiguration().getEntrypoint().get(2)); - } - - @Test - public void testWebAppEntrypoint_inheritedFromBaseImage() - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - Mockito.when(mockProjectProperties.isWarProject()).thenReturn(true); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLogger, mockJibExtension, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - } - - @Test - public void testGetAppRootChecked() { - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("/some/root"); - - Assert.assertEquals( - AbsoluteUnixPath.get("/some/root"), - PluginConfigurationProcessor.getAppRootChecked(mockJibExtension)); - } - - @Test - public void testGetAppRootChecked_errorOnNonAbsolutePath() { - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("relative/path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: relative/path", ex.getMessage()); - } - } - - @Test - public void testGetAppRootChecked_errorOnWindowsPath() { - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("\\windows\\path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: \\windows\\path", ex.getMessage()); - } - } - - @Test - public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { - Mockito.when(mockContainerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibExtension); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: C:\\windows\\path", - ex.getMessage()); - } - } - - // TODO should test other behaviours -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index caf11a5b49..dfea131995 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -26,10 +26,15 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -66,22 +71,23 @@ public void execute() throws MojoExecutionException { HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), - getLog(), - PluginConfigurationProcessor.getExtraDirectoryPath(this), - PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - + MojoCommon.disableHttpLogging(); try { + AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject( + getProject(), + getLog(), + MojoCommon.getExtraDirectoryPath(this), + MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); + MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), @@ -93,13 +99,13 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - getLog(), this, mavenProjectProperties); + rawConfiguration, projectProperties); JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); Containerizer containerizer = Containerizer.to(targetImage); PluginConfigurationProcessor.configureContainerizer( - containerizer, this, mavenProjectProperties); + containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder @@ -109,17 +115,26 @@ public void execute() throws MojoExecutionException { .setTargetImageReference(targetImageReference) .build(); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, eventDispatcher, - mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); getLog().info(""); - } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException ex) { + } catch (AppRootInvalidException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + + } catch (InvalidImageReferenceException + | IOException + | CacheDirectoryCreationException + | MainClassInferenceException + | InferredAuthRetrievalException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index f00b6fb230..dcf0c38e28 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -27,15 +27,22 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import java.io.FileNotFoundException; +import com.google.common.base.Verify; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -89,87 +96,95 @@ public void execute() throws MojoExecutionException, MojoFailureException { "mvn compile jib:build -Dimage=")); } - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), - getLog(), - PluginConfigurationProcessor.getExtraDirectoryPath(this), - PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - getLog(), this, mavenProjectProperties); - - ImageReference targetImageReference = - PluginConfigurationProcessor.parseImageReference(getTargetImage(), "to"); - - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); - Optional optionalToCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.TO_AUTH_USERNAME, - PropertyNames.TO_AUTH_PASSWORD, - getTargetImageAuth()); - if (optionalToCredential.isPresent()) { - defaultCredentialRetrievers.setKnownCredential( - optionalToCredential.get(), "jib-maven-plugin configuration"); - } else { - optionalToCredential = - pluginConfigurationProcessor - .getMavenSettingsServerCredentials() - .retrieve(targetImageReference.getRegistry()); - optionalToCredential.ifPresent( - toCredential -> - defaultCredentialRetrievers.setInferredCredential( - toCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); - } - defaultCredentialRetrievers.setCredentialHelper(getTargetImageCredentialHelperName()); - - RegistryImage targetImage = RegistryImage.named(targetImageReference); + MojoCommon.disableHttpLogging(); try { + AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject( + getProject(), + getLog(), + MojoCommon.getExtraDirectoryPath(this), + MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); + + PluginConfigurationProcessor pluginConfigurationProcessor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + ImageReference targetImageReference = ImageReference.parse(getTargetImage()); + + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); + Optional optionalToCredential = + ConfigurationPropertyValidator.getImageCredential( + eventDispatcher, + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, + getTargetImageAuth()); + if (optionalToCredential.isPresent()) { + defaultCredentialRetrievers.setKnownCredential( + optionalToCredential.get(), "jib-maven-plugin configuration"); + } else { + Optional optionalInferredAuth = + rawConfiguration.getInferredAuth(targetImageReference.getRegistry()); + if (optionalInferredAuth.isPresent()) { + AuthProperty auth = optionalInferredAuth.get(); + String username = Verify.verifyNotNull(auth.getUsername()); + String password = Verify.verifyNotNull(auth.getPassword()); + Credential credential = Credential.basic(username, password); + defaultCredentialRetrievers.setInferredCredential( + credential, auth.getPropertyDescriptor()); + } + } + defaultCredentialRetrievers.setCredentialHelper(getTargetImageCredentialHelperName()); + + RegistryImage targetImage = RegistryImage.named(targetImageReference); defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); - } catch (FileNotFoundException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor - .getJibContainerBuilder() - // Only uses possibly non-Docker formats for build to registry. - .setFormat(ImageFormat.valueOf(getFormat())); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, this, mavenProjectProperties); - - HelpfulSuggestions helpfulSuggestions = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) - .build(); - try { + JibContainerBuilder jibContainerBuilder = + pluginConfigurationProcessor + .getJibContainerBuilder() + // Only uses possibly non-Docker formats for build to registry. + .setFormat(ImageFormat.valueOf(getFormat())); + + Containerizer containerizer = Containerizer.to(targetImage); + PluginConfigurationProcessor.configureContainerizer( + containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); + + HelpfulSuggestions helpfulSuggestions = + new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) + .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) + .setBaseImageHasConfiguredCredentials( + pluginConfigurationProcessor.isBaseImageCredentialPresent()) + .setTargetImageReference(targetImageReference) + .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) + .build(); + + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( jibContainerBuilder, containerizer, eventDispatcher, - mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); getLog().info(""); - } catch (IOException | CacheDirectoryCreationException ex) { + } catch (AppRootInvalidException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + + } catch (InvalidImageReferenceException + | IOException + | CacheDirectoryCreationException + | MainClassInferenceException + | InferredAuthRetrievalException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 7458903737..5411349977 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -25,10 +25,15 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -60,22 +65,23 @@ public void execute() throws MojoExecutionException { return; } - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), - getLog(), - PluginConfigurationProcessor.getExtraDirectoryPath(this), - PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - + MojoCommon.disableHttpLogging(); try { + AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject( + getProject(), + getLog(), + MojoCommon.getExtraDirectoryPath(this), + MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); + MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(mavenProjectProperties.getEventHandlers()); ImageReference targetImageReference = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( getTargetImage(), @@ -83,18 +89,19 @@ public void execute() throws MojoExecutionException { getProject().getName(), getProject().getVersion(), mavenHelpfulSuggestionsBuilder.build()); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); Path tarOutputPath = buildOutput.resolve("jib-image.tar"); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfiguration( - getLog(), this, mavenProjectProperties); + rawConfiguration, projectProperties); JibContainerBuilder jibContainerBuilder = pluginConfigurationProcessor.getJibContainerBuilder(); Containerizer containerizer = Containerizer.to(targetImage); PluginConfigurationProcessor.configureContainerizer( - containerizer, this, mavenProjectProperties); + containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder @@ -110,11 +117,19 @@ public void execute() throws MojoExecutionException { jibContainerBuilder, containerizer, eventDispatcher, - mavenProjectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); getLog().info(""); - } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException ex) { + } catch (AppRootInvalidException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + + } catch (InvalidImageReferenceException + | IOException + | CacheDirectoryCreationException + | MainClassInferenceException + | InferredAuthRetrievalException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index c98182f7db..75ff22d920 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -16,11 +16,16 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.InsecureRecursiveDeleteException; @@ -51,6 +56,8 @@ public class DockerContextMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + Preconditions.checkNotNull(targetDir); + if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; @@ -62,34 +69,31 @@ public void execute() throws MojoExecutionException { try { JibSystemProperties.checkHttpTimeoutProperty(); - } catch (NumberFormatException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - - // TODO: Instead of disabling logging, have authentication credentials be provided - PluginConfigurationProcessor.disableHttpLogging(); - - Preconditions.checkNotNull(targetDir); + MojoCommon.disableHttpLogging(); + AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject( + getProject(), + getLog(), + MojoCommon.getExtraDirectoryPath(this), + MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), + appRoot); + DefaultEventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); + + List entrypoint = + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties); + String baseImage = + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); - AbsoluteUnixPath appRoot = PluginConfigurationProcessor.getAppRootChecked(this); - MavenProjectProperties mavenProjectProperties = - MavenProjectProperties.getForProject( - getProject(), - getLog(), - PluginConfigurationProcessor.getExtraDirectoryPath(this), - PluginConfigurationProcessor.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); - - List entrypoint = - PluginConfigurationProcessor.computeEntrypoint(getLog(), this, mavenProjectProperties); - - try { // Validate port input, but don't save the output because we don't want the ranges expanded // here. ExposedPortsParser.parse(getExposedPorts()); - new JavaDockerContextGenerator(mavenProjectProperties.getJavaLayerConfigurations()) - .setBaseImage(PluginConfigurationProcessor.getBaseImage(this)) + new JavaDockerContextGenerator(projectProperties.getJavaLayerConfigurations()) + .setBaseImage(baseImage) .setEntrypoint(entrypoint) .setProgramArguments(getArgs()) .setExposedPorts(getExposedPorts()) @@ -114,6 +118,13 @@ public void execute() throws MojoExecutionException { HelpfulSuggestions.suggest( "Export Docker context failed", "check if `targetDir` is set correctly"), ex); + + } catch (AppRootInvalidException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + + } catch (MainClassInferenceException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); } } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 6ec0be3ab8..28891aeb51 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -49,17 +49,21 @@ public static class AuthConfiguration implements AuthProperty { @Nullable @Parameter private String username; @Nullable @Parameter private String password; - @Nullable private String usernameDescriptor; - @Nullable private String passwordDescriptor; + @Nullable private String descriptor; + + @Override + public String getPropertyDescriptor() { + return Preconditions.checkNotNull(descriptor); + } @Override public String getUsernamePropertyDescriptor() { - return Preconditions.checkNotNull(usernameDescriptor); + return Preconditions.checkNotNull(descriptor) + ""; } @Override public String getPasswordPropertyDescriptor() { - return Preconditions.checkNotNull(passwordDescriptor); + return Preconditions.checkNotNull(descriptor) + ""; } @Override @@ -84,9 +88,8 @@ void setPassword(String password) { this.password = password; } - private void setPropertyDescriptors(String descriptorPrefix) { - usernameDescriptor = descriptorPrefix + ""; - passwordDescriptor = descriptorPrefix + ""; + private void setPropertyDescriptor(String descriptor) { + this.descriptor = descriptor; } } @@ -229,8 +232,8 @@ public File getPath() { /** Default constructor handles setting up auth property descriptors. */ JibPluginConfiguration() { - to.auth.setPropertyDescriptors(""); - from.auth.setPropertyDescriptors(""); + to.auth.setPropertyDescriptor(""); + from.auth.setPropertyDescriptor(""); } MavenSession getSession() { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index e8d74fd241..a4ad4e1298 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -21,8 +21,6 @@ import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; -import com.google.cloud.tools.jib.plugins.common.MainClassResolver; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; @@ -158,19 +156,4 @@ public String getJarPluginName() { public boolean isWarProject() { return "war".equals(project.getPackaging()); } - - /** - * Tries to resolve the main class. - * - * @param jibPluginConfiguration the mojo configuration properties. - * @return the configured main class, or the inferred main class if none is configured. - * @throws MojoExecutionException if resolving the main class fails. - */ - String getMainClass(JibPluginConfiguration jibPluginConfiguration) throws MojoExecutionException { - try { - return MainClassResolver.resolveMainClass(jibPluginConfiguration.getMainClass(), this); - } catch (MainClassInferenceException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java new file mode 100644 index 0000000000..01925fcd4e --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -0,0 +1,134 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Maven-specific adapter for providing raw configuration parameter values. */ +class MavenRawConfiguration implements RawConfiguration { + + private final JibPluginConfiguration jibPluginConfiguration; + private final MavenSettingsServerCredentials mavenSettingsServerCredentials; + + /** + * Creates a raw configuration instances. + * + * @param jibPluginConfiguration the Jib plugin configuration + * @param eventDispatcher Jib event dispatcher + */ + MavenRawConfiguration( + JibPluginConfiguration jibPluginConfiguration, EventDispatcher eventDispatcher) { + this.jibPluginConfiguration = jibPluginConfiguration; + mavenSettingsServerCredentials = + new MavenSettingsServerCredentials( + jibPluginConfiguration.getSession().getSettings(), + jibPluginConfiguration.getSettingsDecrypter(), + eventDispatcher); + } + + @Override + public Optional getFromImage() { + return Optional.ofNullable(jibPluginConfiguration.getBaseImage()); + } + + @Override + public AuthProperty getFromAuth() { + return jibPluginConfiguration.getBaseImageAuth(); + } + + @Override + public Optional getFromCredHelper() { + return Optional.ofNullable(jibPluginConfiguration.getBaseImageCredentialHelperName()); + } + + @Override + public Iterable getToTags() { + return jibPluginConfiguration.getTargetImageAdditionalTags(); + } + + @Override + public Optional> getEntrypoint() { + return Optional.ofNullable(jibPluginConfiguration.getEntrypoint()); + } + + @Override + public Optional> getProgramArguments() { + return Optional.ofNullable(jibPluginConfiguration.getArgs()); + } + + @Override + public Optional getMainClass() { + return Optional.ofNullable(jibPluginConfiguration.getMainClass()); + } + + @Override + public List getJvmFlags() { + return jibPluginConfiguration.getJvmFlags(); + } + + @Override + public String getAppRoot() { + return jibPluginConfiguration.getAppRoot(); + } + + @Override + public Map getEnvironment() { + return jibPluginConfiguration.getEnvironment(); + } + + @Override + public Map getLabels() { + return jibPluginConfiguration.getLabels(); + } + + @Override + public List getPorts() { + return jibPluginConfiguration.getExposedPorts(); + } + + @Override + public Optional getUser() { + return Optional.ofNullable(jibPluginConfiguration.getUser()); + } + + @Override + public boolean getUseCurrentTimestamp() { + return jibPluginConfiguration.getUseCurrentTimestamp(); + } + + @Override + public boolean getAllowInsecureRegistries() { + return jibPluginConfiguration.getAllowInsecureRegistries(); + } + + @Override + public boolean getUseOnlyProjectCache() { + return jibPluginConfiguration.getUseOnlyProjectCache(); + } + + @Override + public Optional getInferredAuth(String authTarget) + throws InferredAuthRetrievalException { + return mavenSettingsServerCredentials.retrieve(authTarget); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index b60dd85080..87cf7cb26e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -16,14 +16,15 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.common.annotations.VisibleForTesting; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -58,30 +59,32 @@ static boolean isEncrypted(String password) { private final Settings settings; @Nullable private final SettingsDecrypter settingsDecrypter; - private final Log log; + private final EventDispatcher eventDispatcher; /** * Create new instance. * * @param settings the Maven settings object * @param settingsDecrypter the Maven decrypter component - * @param log the Maven build logger + * @param eventDispatcher the Jib event dispatcher */ MavenSettingsServerCredentials( - Settings settings, @Nullable SettingsDecrypter settingsDecrypter, Log log) { + Settings settings, + @Nullable SettingsDecrypter settingsDecrypter, + EventDispatcher eventDispatcher) { this.settings = settings; this.settingsDecrypter = settingsDecrypter; - this.log = log; + this.eventDispatcher = eventDispatcher; } /** * Attempts to retrieve credentials for {@code registry} from Maven settings. * * @param registry the registry - * @return the credentials for the registry, or {@link Optional#empty} if none could be retrieved - * @throws MojoExecutionException if the credentials could not be retrieved + * @return the auth info for the registry, or {@link Optional#empty} if none could be retrieved + * @throws InferredAuthRetrievalException if the credentials could not be retrieved */ - Optional retrieve(@Nullable String registry) throws MojoExecutionException { + Optional retrieve(@Nullable String registry) throws InferredAuthRetrievalException { if (registry == null) { return Optional.empty(); } @@ -103,7 +106,7 @@ Optional retrieve(@Nullable String registry) throws MojoExecutionExc for (SettingsProblem problem : result.getProblems()) { if (problem.getSeverity() == SettingsProblem.Severity.ERROR || problem.getSeverity() == SettingsProblem.Severity.FATAL) { - throw new MojoExecutionException( + throw new InferredAuthRetrievalException( "Unable to decrypt password for " + registry + ": " + problem); } } @@ -111,13 +114,43 @@ Optional retrieve(@Nullable String registry) throws MojoExecutionExc registryServer = result.getServer(); } } else if (isEncrypted(registryServer.getPassword())) { - log.warn( - "Server password for registry " - + registry - + " appears to be encrypted, but there is no decrypter available"); + eventDispatcher.dispatch( + LogEvent.warn( + "Server password for registry " + + registry + + " appears to be encrypted, but there is no decrypter available")); } + String username = registryServer.getUsername(); + String password = registryServer.getPassword(); + return Optional.of( - Credential.basic(registryServer.getUsername(), registryServer.getPassword())); + new AuthProperty() { + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getPropertyDescriptor() { + return CREDENTIAL_SOURCE; + } + + @Override + public String getUsernamePropertyDescriptor() { + return CREDENTIAL_SOURCE; + } + + @Override + public String getPasswordPropertyDescriptor() { + return CREDENTIAL_SOURCE; + } + }); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java new file mode 100644 index 0000000000..574a8ba87c --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -0,0 +1,112 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; +import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Collection of common methods to share between Gradle tasks. */ +class MojoCommon { + + /** + * Gets the value of the {@code } parameter. If the parameter is empty, + * returns {@link JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for project with WAR packaging or + * {@link JavaLayerConfigurations#DEFAULT_APP_ROOT} for other packaging. + * + * @param jibPluginConfiguration the Jib plugin configuration + * @return the app root value + * @throws AppRootInvalidException if the app root is not an absolute path in Unix-style + */ + // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead + static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) + throws AppRootInvalidException { + String appRoot = jibPluginConfiguration.getAppRoot(); + if (appRoot.isEmpty()) { + boolean isWarProject = "war".equals(jibPluginConfiguration.getProject().getPackaging()); + appRoot = + isWarProject + ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT + : JavaLayerConfigurations.DEFAULT_APP_ROOT; + } + try { + return AbsoluteUnixPath.get(appRoot); + } catch (IllegalArgumentException ex) { + throw new AppRootInvalidException(appRoot, appRoot, ex); + } + } + + /** + * Gets the extra directory path from a {@link JibPluginConfiguration}. Returns {@code (project + * dir)/src/main/jib} if null. + * + * @param jibPluginConfiguration the build configuration + * @return the resolved extra directory + */ + static Path getExtraDirectoryPath(JibPluginConfiguration jibPluginConfiguration) { + return jibPluginConfiguration + .getExtraDirectoryPath() + .orElse( + Preconditions.checkNotNull(jibPluginConfiguration.getProject()) + .getBasedir() + .toPath() + .resolve("src") + .resolve("main") + .resolve("jib")); + } + + /** + * Validates and converts a list of {@link PermissionConfiguration} to an equivalent {@code + * AbsoluteUnixPath->FilePermission} map. + * + * @param permissionList the list to convert + * @return the resulting map + */ + @VisibleForTesting + static Map convertPermissionsList( + List permissionList) { + Map permissionsMap = new HashMap<>(); + for (PermissionConfiguration permission : permissionList) { + if (!permission.getFile().isPresent() || !permission.getMode().isPresent()) { + throw new IllegalArgumentException( + "Incomplete configuration; requires and fields to be set"); + } + AbsoluteUnixPath key = AbsoluteUnixPath.get(permission.getFile().get()); + FilePermissions value = FilePermissions.fromOctalString(permission.getMode().get()); + permissionsMap.put(key, value); + } + return permissionsMap; + } + + /** Disables annoying Apache HTTP client logging. */ + // TODO: Instead of disabling logging, have authentication credentials be provided + static void disableHttpLogging() { + System.setProperty( + "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); + } + + private MojoCommon() {} +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java deleted file mode 100644 index d9e7fea55f..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessor.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.Jib; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; -import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; -import com.google.cloud.tools.jib.plugins.common.ProjectProperties; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import java.io.FileNotFoundException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; - -/** Configures and provides builders for the image building goals. */ -class PluginConfigurationProcessor { - - /** - * Returns true if the Maven packaging type is "war". - * - * @param jibPluginConfiguration the Jib plugin configuration - * @return true if the Maven packaging type is "war" - */ - private static boolean isWarPackaging(JibPluginConfiguration jibPluginConfiguration) { - return "war".equals(jibPluginConfiguration.getProject().getPackaging()); - } - - /** - * Gets the value of the {@code } parameter. If the parameter is empty, - * returns {@link JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for project with WAR packaging or - * {@link JavaLayerConfigurations#DEFAULT_APP_ROOT} for other packaging. - * - * @param jibPluginConfiguration the Jib plugin configuration - * @return the app root value - * @throws MojoExecutionException if the app root is not an absolute path in Unix-style - */ - static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) - throws MojoExecutionException { - String appRoot = jibPluginConfiguration.getAppRoot(); - if (appRoot.isEmpty()) { - appRoot = - isWarPackaging(jibPluginConfiguration) - ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT - : JavaLayerConfigurations.DEFAULT_APP_ROOT; - } - try { - return AbsoluteUnixPath.get(appRoot); - } catch (IllegalArgumentException ex) { - throw new MojoExecutionException( - " is not an absolute Unix-style path: " + appRoot); - } - } - - /** - * Gets the value of the {@code } parameter. If the parameter is null, returns - * "gcr.io/distroless/java/jetty" for projects with WAR packaging or "gcr.io/distroless/java" for - * other packaging. - * - * @param jibPluginConfiguration the Jib plugin configuration - * @return the base image value - */ - static String getBaseImage(JibPluginConfiguration jibPluginConfiguration) { - String baseImage = jibPluginConfiguration.getBaseImage(); - if (baseImage == null) { - return isWarPackaging(jibPluginConfiguration) - ? "gcr.io/distroless/java/jetty" - : "gcr.io/distroless/java"; - } - return baseImage; - } - - /** - * Gets the extra directory path from a {@link JibPluginConfiguration}. Returns {@code (project - * dir)/src/main/jib} if null. - * - * @param jibPluginConfiguration the build configuration - * @return the resolved extra directory - */ - static Path getExtraDirectoryPath(JibPluginConfiguration jibPluginConfiguration) { - return jibPluginConfiguration - .getExtraDirectoryPath() - .orElse( - Preconditions.checkNotNull(jibPluginConfiguration.getProject()) - .getBasedir() - .toPath() - .resolve("src") - .resolve("main") - .resolve("jib")); - } - - /** - * Validates and converts a list of {@link PermissionConfiguration} to an equivalent {@code - * AbsoluteUnixPath->FilePermission} map. - * - * @param permissionList the list to convert - * @return the resulting map - */ - @VisibleForTesting - static Map convertPermissionsList( - List permissionList) { - Map permissionsMap = new HashMap<>(); - for (PermissionConfiguration permission : permissionList) { - if (!permission.getFile().isPresent() || !permission.getMode().isPresent()) { - throw new IllegalArgumentException( - "Incomplete configuration; requires and fields to both be " - + "non-null."); - } - AbsoluteUnixPath key = AbsoluteUnixPath.get(permission.getFile().get()); - FilePermissions value = FilePermissions.fromOctalString(permission.getMode().get()); - permissionsMap.put(key, value); - } - return permissionsMap; - } - - /** Disables annoying Apache HTTP client logging. */ - static void disableHttpLogging() { - System.setProperty( - "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); - } - - /** - * Sets up {@link BuildConfiguration} that is common among the image building goals. This includes - * setting up the base image reference/authorization, container configuration, cache - * configuration, and layer configuration. - * - * @param logger the logger used to display messages - * @param jibPluginConfiguration the {@link JibPluginConfiguration} providing the configuration - * data - * @param projectProperties used for providing additional information - * @return a new {@link PluginConfigurationProcessor} containing pre-configured builders - * @throws MojoExecutionException if the http timeout system property is misconfigured - */ - static PluginConfigurationProcessor processCommonConfiguration( - Log logger, - JibPluginConfiguration jibPluginConfiguration, - MavenProjectProperties projectProperties) - throws MojoExecutionException { - try { - JibSystemProperties.checkHttpTimeoutProperty(); - } catch (NumberFormatException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - - // TODO: Instead of disabling logging, have authentication credentials be provided - disableHttpLogging(); - ImageReference baseImageReference = - parseImageReference(getBaseImage(jibPluginConfiguration), "from"); - - // Checks Maven settings for registry credentials. - if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { - logger.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not enable " - + "this on a public network!"); - } - MavenSettingsServerCredentials mavenSettingsServerCredentials = - new MavenSettingsServerCredentials( - Preconditions.checkNotNull(jibPluginConfiguration.getSession()).getSettings(), - jibPluginConfiguration.getSettingsDecrypter(), - logger); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); - Optional optionalFromCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.FROM_AUTH_USERNAME, - PropertyNames.FROM_AUTH_PASSWORD, - jibPluginConfiguration.getBaseImageAuth()); - if (optionalFromCredential.isPresent()) { - defaultCredentialRetrievers.setKnownCredential( - optionalFromCredential.get(), "jib-maven-plugin configuration"); - } else { - optionalFromCredential = - mavenSettingsServerCredentials.retrieve(baseImageReference.getRegistry()); - optionalFromCredential.ifPresent( - fromCredential -> - defaultCredentialRetrievers.setInferredCredential( - fromCredential, MavenSettingsServerCredentials.CREDENTIAL_SOURCE)); - } - defaultCredentialRetrievers.setCredentialHelper( - jibPluginConfiguration.getBaseImageCredentialHelperName()); - - List entrypoint = computeEntrypoint(logger, jibPluginConfiguration, projectProperties); - - RegistryImage baseImage = RegistryImage.named(baseImageReference); - try { - defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); - } catch (FileNotFoundException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - - JibContainerBuilder jibContainerBuilder = - Jib.from(baseImage) - .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) - .setEntrypoint(entrypoint) - .setProgramArguments(jibPluginConfiguration.getArgs()) - .setEnvironment(jibPluginConfiguration.getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(jibPluginConfiguration.getExposedPorts())) - .setLabels(jibPluginConfiguration.getLabels()) - .setUser(jibPluginConfiguration.getUser()); - if (jibPluginConfiguration.getUseCurrentTimestamp()) { - logger.warn( - "Setting image creation time to current time; your image may not be reproducible."); - jibContainerBuilder.setCreationTime(Instant.now()); - } - - return new PluginConfigurationProcessor( - jibContainerBuilder, - baseImageReference, - mavenSettingsServerCredentials, - optionalFromCredential.isPresent()); - } - - /** - * Configures a {@link Containerizer} with values pulled from project properties/build - * configuration. - * - * @param containerizer the {@link Containerizer} to configure - * @param jibPluginConfiguration the build configuration - * @param projectProperties the project properties - */ - static void configureContainerizer( - Containerizer containerizer, - JibPluginConfiguration jibPluginConfiguration, - ProjectProperties projectProperties) { - containerizer - .setToolName(MavenProjectProperties.TOOL_NAME) - .setEventHandlers(projectProperties.getEventHandlers()) - .setAllowInsecureRegistries(jibPluginConfiguration.getAllowInsecureRegistries()) - .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) - .setApplicationLayersCache(projectProperties.getCacheDirectory()); - - jibPluginConfiguration.getTargetImageAdditionalTags().forEach(containerizer::withAdditionalTag); - - if (jibPluginConfiguration.getUseOnlyProjectCache()) { - containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); - } - } - - /** - * @param image the image reference string to parse. - * @param type name of the parameter being parsed (e.g. "to" or "from"). - * @return the {@link ImageReference} parsed from {@code from}. - */ - static ImageReference parseImageReference(String image, String type) { - try { - return ImageReference.parse(image); - } catch (InvalidImageReferenceException ex) { - throw new IllegalStateException("Parameter '" + type + "' is invalid", ex); - } - } - - /** - * Compute the container entrypoint, in this order: - * - *
    - *
  1. the user specified one, if set - *
  2. for a WAR project, null (it must be inherited from base image) - *
  3. for a non-WAR project, by resolving the main class - *
- * - * @param logger the logger used to display messages. - * @param jibPluginConfiguration the {@link JibPluginConfiguration} providing the configuration - * data - * @param projectProperties used for providing additional information - * @return the entrypoint - * @throws MojoExecutionException if resolving the main class fails or the app root parameter is - * not an absolute path in Unix-style - */ - @Nullable - static List computeEntrypoint( - Log logger, - JibPluginConfiguration jibPluginConfiguration, - MavenProjectProperties projectProperties) - throws MojoExecutionException { - List entrypointParameter = jibPluginConfiguration.getEntrypoint(); - if (entrypointParameter != null && !entrypointParameter.isEmpty()) { - if (jibPluginConfiguration.getMainClass() != null - || !jibPluginConfiguration.getJvmFlags().isEmpty()) { - logger.warn(" and are ignored when is specified"); - } - return entrypointParameter; - } - - if (isWarPackaging(jibPluginConfiguration)) { - return null; - } - - String mainClass = projectProperties.getMainClass(jibPluginConfiguration); - return JavaEntrypointConstructor.makeDefaultEntrypoint( - getAppRootChecked(jibPluginConfiguration), jibPluginConfiguration.getJvmFlags(), mainClass); - } - - private final JibContainerBuilder jibContainerBuilder; - private final ImageReference baseImageReference; - private final MavenSettingsServerCredentials mavenSettingsServerCredentials; - private final boolean isBaseImageCredentialPresent; - - private PluginConfigurationProcessor( - JibContainerBuilder jibContainerBuilder, - ImageReference baseImageReference, - MavenSettingsServerCredentials mavenSettingsServerCredentials, - boolean isBaseImageCredentialPresent) { - this.jibContainerBuilder = jibContainerBuilder; - this.baseImageReference = baseImageReference; - this.mavenSettingsServerCredentials = mavenSettingsServerCredentials; - this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; - } - - JibContainerBuilder getJibContainerBuilder() { - return jibContainerBuilder; - } - - ImageReference getBaseImageReference() { - return baseImageReference; - } - - MavenSettingsServerCredentials getMavenSettingsServerCredentials() { - return mavenSettingsServerCredentials; - } - - boolean isBaseImageCredentialPresent() { - return isBaseImageCredentialPresent; - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java index 90b9a8a756..c7d078e6e1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java @@ -26,9 +26,11 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.crypto.SettingsDecrypter; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -45,11 +47,12 @@ public class DockerContextMojoTest { @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); + @Mock private MavenProject project; + @Mock private Build build; + private DockerContextMojo mojo; private String appRoot = "/app"; private File outputFolder; - private @Mock MavenProject project; - private @Mock Build build; @Before public void setUp() throws IOException { @@ -248,6 +251,16 @@ String getAppRoot() { Map getEnvironment() { return ImmutableMap.of("envKey", "envVal"); } + + @Override + MavenSession getSession() { + return Mockito.mock(MavenSession.class); + } + + @Override + SettingsDecrypter getSettingsDecrypter() { + return Mockito.mock(SettingsDecrypter.class); + } } @Nullable diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java new file mode 100644 index 0000000000..8dffae9eaf --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** Test for {@link MavenRawConfiguration}. */ +public class MavenRawConfigurationTest { + + @Test + public void testGetters() throws InferredAuthRetrievalException { + JibPluginConfiguration jibPluginConfiguration = Mockito.mock(JibPluginConfiguration.class); + EventDispatcher eventDispatcher = Mockito.mock(EventDispatcher.class); + + Server server = Mockito.mock(Server.class); + Mockito.when(server.getUsername()).thenReturn("maven settings user"); + Mockito.when(server.getPassword()).thenReturn("maven settings password"); + + Settings mavenSettings = Mockito.mock(Settings.class); + Mockito.when(mavenSettings.getServer("base registry")).thenReturn(server); + + MavenSession mavenSession = Mockito.mock(MavenSession.class); + Mockito.when(mavenSession.getSettings()).thenReturn(mavenSettings); + + AuthConfiguration auth = Mockito.mock(AuthConfiguration.class); + Mockito.when(auth.getUsername()).thenReturn("user"); + Mockito.when(auth.getPassword()).thenReturn("password"); + Mockito.when(auth.getPropertyDescriptor()).thenReturn("auth source"); + Mockito.when(auth.getUsernamePropertyDescriptor()).thenReturn("from.auth.username"); + Mockito.when(auth.getPasswordPropertyDescriptor()).thenReturn(""); + + Mockito.when(jibPluginConfiguration.getSession()).thenReturn(mavenSession); + Mockito.when(jibPluginConfiguration.getBaseImageAuth()).thenReturn(auth); + + Mockito.when(jibPluginConfiguration.getAllowInsecureRegistries()).thenReturn(true); + Mockito.when(jibPluginConfiguration.getAppRoot()).thenReturn("/app/root"); + Mockito.when(jibPluginConfiguration.getArgs()).thenReturn(Arrays.asList("--log", "info")); + Mockito.when(jibPluginConfiguration.getBaseImage()).thenReturn("openjdk:15"); + Mockito.when(jibPluginConfiguration.getBaseImageCredentialHelperName()).thenReturn("gcr"); + Mockito.when(jibPluginConfiguration.getEntrypoint()).thenReturn(Arrays.asList("java", "Main")); + Mockito.when(jibPluginConfiguration.getEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("currency", "dollar"))); + Mockito.when(jibPluginConfiguration.getExposedPorts()).thenReturn(Arrays.asList("80/tcp", "0")); + Mockito.when(jibPluginConfiguration.getJvmFlags()).thenReturn(Arrays.asList("-cp", ".")); + Mockito.when(jibPluginConfiguration.getLabels()) + .thenReturn(new HashMap<>(ImmutableMap.of("unit", "cm"))); + Mockito.when(jibPluginConfiguration.getMainClass()).thenReturn("com.example.Main"); + Mockito.when(jibPluginConfiguration.getTargetImageAdditionalTags()) + .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); + Mockito.when(jibPluginConfiguration.getUseCurrentTimestamp()).thenReturn(true); + Mockito.when(jibPluginConfiguration.getUseOnlyProjectCache()).thenReturn(true); + Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); + + MavenRawConfiguration rawConfiguration = + new MavenRawConfiguration(jibPluginConfiguration, eventDispatcher); + + AuthProperty fromAuth = rawConfiguration.getFromAuth(); + Assert.assertEquals("user", fromAuth.getUsername()); + Assert.assertEquals("password", fromAuth.getPassword()); + Assert.assertEquals("auth source", fromAuth.getPropertyDescriptor()); + Assert.assertEquals("from.auth.username", fromAuth.getUsernamePropertyDescriptor()); + Assert.assertEquals("", fromAuth.getPasswordPropertyDescriptor()); + + AuthProperty mavenSettingsAuth = rawConfiguration.getInferredAuth("base registry").get(); + Assert.assertEquals("maven settings user", mavenSettingsAuth.getUsername()); + Assert.assertEquals("maven settings password", mavenSettingsAuth.getPassword()); + Assert.assertEquals("Maven settings", mavenSettingsAuth.getPropertyDescriptor()); + Assert.assertEquals("Maven settings", mavenSettingsAuth.getUsernamePropertyDescriptor()); + Assert.assertEquals("Maven settings", mavenSettingsAuth.getPasswordPropertyDescriptor()); + + Assert.assertFalse(rawConfiguration.getInferredAuth("unknown registry").isPresent()); + + Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); + Assert.assertEquals(Arrays.asList("java", "Main"), rawConfiguration.getEntrypoint().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("currency", "dollar")), rawConfiguration.getEnvironment()); + Assert.assertEquals("/app/root", rawConfiguration.getAppRoot()); + Assert.assertEquals("gcr", rawConfiguration.getFromCredHelper().get()); + Assert.assertEquals("openjdk:15", rawConfiguration.getFromImage().get()); + Assert.assertEquals(Arrays.asList("-cp", "."), rawConfiguration.getJvmFlags()); + Assert.assertEquals(new HashMap<>(ImmutableMap.of("unit", "cm")), rawConfiguration.getLabels()); + Assert.assertEquals("com.example.Main", rawConfiguration.getMainClass().get()); + Assert.assertEquals(Arrays.asList("80/tcp", "0"), rawConfiguration.getPorts()); + Assert.assertEquals( + Arrays.asList("--log", "info"), rawConfiguration.getProgramArguments().get()); + Assert.assertEquals( + new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); + Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); + Assert.assertTrue(rawConfiguration.getUseOnlyProjectCache()); + Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); + + Mockito.verifyNoMoreInteractions(eventDispatcher); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 54eb88dd92..23fa29c09c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -16,11 +16,12 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import java.util.Collections; import java.util.Optional; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -40,61 +41,60 @@ public class MavenSettingsServerCredentialsTest { @Mock private Settings mockSettings; @Mock private Server mockServer1; - @Mock private Log mockLog; + @Mock private EventDispatcher mockEventDispatcher; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; @Before public void setUp() { testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, null, mockLog); + new MavenSettingsServerCredentials(mockSettings, null, mockEventDispatcher); } @Test - public void testRetrieve_found() throws MojoExecutionException { + public void testRetrieve_found() throws InferredAuthRetrievalException { Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Optional optionalCredential = - testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertTrue(optionalCredential.isPresent()); - Assert.assertEquals( - Credential.basic("server1 username", "server1 password"), optionalCredential.get()); + Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("server1 username", auth.get().getUsername()); + Assert.assertEquals("server1 password", auth.get().getPassword()); + Assert.assertEquals("Maven settings", auth.get().getPropertyDescriptor()); - Mockito.verifyZeroInteractions(mockLog); + Mockito.verifyZeroInteractions(mockEventDispatcher); } @Test - public void testRetrieve_notFound() throws MojoExecutionException { + public void testRetrieve_notFound() throws InferredAuthRetrievalException { Assert.assertFalse(testMavenSettingsServerCredentials.retrieve("serverUnknown").isPresent()); } @Test - public void testRetrieve_withNullServer() throws MojoExecutionException { + public void testRetrieve_withNullServer() throws InferredAuthRetrievalException { Assert.assertFalse(testMavenSettingsServerCredentials.retrieve(null).isPresent()); } @Test - public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionException { + public void testRetrieve_withNullDecrypter_encrypted() throws InferredAuthRetrievalException { Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); - Optional optionalCredential = - testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertTrue(optionalCredential.isPresent()); - Assert.assertEquals( - Credential.basic("server1 username", "{COQLCE6DU6GtcS5P=}"), optionalCredential.get()); - Mockito.verify(mockLog) - .warn( - "Server password for registry server1 appears to be encrypted, " - + "but there is no decrypter available"); + Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("server1 username", auth.get().getUsername()); + Assert.assertEquals("{COQLCE6DU6GtcS5P=}", auth.get().getPassword()); + String expectedWarning = + "Server password for registry server1 appears to be encrypted, but there is no decrypter" + + " available"; + Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn(expectedWarning)); } @Test - public void testRetrieve_withDecrypter_success() throws MojoExecutionException { + public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalException { SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class); Mockito.when(mockResult.getProblems()).thenReturn(Collections.emptyList()); Mockito.when(mockResult.getServer()).thenReturn(mockServer1); @@ -103,18 +103,18 @@ public void testRetrieve_withDecrypter_success() throws MojoExecutionException { SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLog); + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockEventDispatcher); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Optional optionalCredential = - testMavenSettingsServerCredentials.retrieve("server1"); - Assert.assertTrue(optionalCredential.isPresent()); - Assert.assertEquals( - Credential.basic("server1 username", "server1 password"), optionalCredential.get()); + Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("server1 username", auth.get().getUsername()); + Assert.assertEquals("server1 password", auth.get().getPassword()); + Assert.assertEquals("Maven settings", auth.get().getPropertyDescriptor()); Mockito.verify(mockDecrypter).decrypt(Mockito.any()); Mockito.verify(mockResult).getProblems(); @@ -136,7 +136,7 @@ public void testRetrieve_withDecrypter_failure() { SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLog); + new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockEventDispatcher); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); @@ -144,7 +144,7 @@ public void testRetrieve_withDecrypter_failure() { try { testMavenSettingsServerCredentials.retrieve("server1"); Assert.fail("decryption should have failed"); - } catch (MojoExecutionException ex) { + } catch (InferredAuthRetrievalException ex) { Assert.assertEquals( ex.getMessage(), "Unable to decrypt password for server1: MockProblemText"); Mockito.verify(mockDecrypter).decrypt(Mockito.any()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java deleted file mode 100644 index 96133e0367..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/PluginConfigurationProcessorTest.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; -import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.Log; -import org.apache.maven.project.MavenProject; -import org.apache.maven.settings.Settings; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link PluginConfigurationProcessor}. */ -@RunWith(MockitoJUnitRunner.class) -public class PluginConfigurationProcessorTest { - - private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - return JibContainerBuilderTestHelper.toBuildConfiguration( - jibContainerBuilder, - BuildConfiguration.builder(), - Containerizer.to(RegistryImage.named("ignored"))); - } - - @Mock private Log mockLog; - @Mock private JibPluginConfiguration mockJibPluginConfiguration; - @Mock private MavenProjectProperties mockProjectProperties; - @Mock private MavenSession mockMavenSession; - @Mock private Settings mockMavenSettings; - @Mock private MavenProject mavenProject; - - @Before - public void setUp() throws Exception { - Mockito.when(mockJibPluginConfiguration.getSession()).thenReturn(mockMavenSession); - Mockito.when(mockMavenSession.getSettings()).thenReturn(mockMavenSettings); - - Mockito.when(mockJibPluginConfiguration.getBaseImageAuth()).thenReturn(new AuthConfiguration()); - Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(Collections.emptyList()); - Mockito.when(mockJibPluginConfiguration.getJvmFlags()).thenReturn(Collections.emptyList()); - Mockito.when(mockJibPluginConfiguration.getArgs()).thenReturn(Collections.emptyList()); - Mockito.when(mockJibPluginConfiguration.getExposedPorts()).thenReturn(Collections.emptyList()); - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/app"); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - - Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) - .thenReturn(JavaLayerConfigurations.builder().build()); - Mockito.when(mockProjectProperties.getMainClass(mockJibPluginConfiguration)) - .thenReturn("java.lang.Object"); - Mockito.when(mockProjectProperties.getEventHandlers()).thenReturn(new EventHandlers()); - } - - /** Test with our default mocks, which try to mimic the bare Maven configuration. */ - @Test - public void testPluginConfigurationProcessor_defaults() - throws MojoExecutionException, InvalidImageReferenceException, IOException, - CacheDirectoryCreationException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - - Assert.assertEquals( - ImageReference.parse("gcr.io/distroless/java").toString(), - processor.getBaseImageReference().toString()); - Mockito.verifyZeroInteractions(mockLog); - } - - @Test - public void testPluginConfigurationProcessor_warPackaging() - throws MojoExecutionException, InvalidImageReferenceException { - Mockito.when(mavenProject.getPackaging()).thenReturn("war"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - - Assert.assertEquals( - ImageReference.parse("gcr.io/distroless/java/jetty").toString(), - processor.getBaseImageReference().toString()); - Mockito.verifyZeroInteractions(mockLog); - } - - @Test - public void testEntrypoint() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(mockLog); - } - - @Test - public void testEntrypoint_defaultWarPackaging() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn("war"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(mockLog); - } - - @Test - public void testEntrypoint_defaulNonWarPackaging() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn(null); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(mockLog); - } - - @Test - public void testEntrypoint_warningOnJvmFlags() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - Mockito.when(mockJibPluginConfiguration.getJvmFlags()) - .thenReturn(Collections.singletonList("jvmFlag")); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(mockLog) - .warn(" and are ignored when is specified"); - } - - @Test - public void testEntrypoint_warningOnMainclass() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getEntrypoint()) - .thenReturn(Arrays.asList("custom", "entrypoint")); - Mockito.when(mockJibPluginConfiguration.getMainClass()).thenReturn("java.util.Object"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals( - Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(mockLog) - .warn(" and are ignored when is specified"); - } - - @Test - public void testEntrypointClasspath_nonDefaultAppRoot() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/my/app"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - - ContainerConfiguration containerConfiguration = - getBuildConfiguration(jibContainerBuilder).getContainerConfiguration(); - Assert.assertNotNull(containerConfiguration); - Assert.assertNotNull(containerConfiguration.getEntrypoint()); - Assert.assertEquals( - "/my/app/resources:/my/app/classes:/my/app/libs/*", - containerConfiguration.getEntrypoint().get(2)); - } - - @Test - public void testUser() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - Mockito.when(mockJibPluginConfiguration.getUser()).thenReturn("customUser"); - - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); - } - - @Test - public void testUser_null() - throws MojoExecutionException, IOException, InvalidImageReferenceException, - CacheDirectoryCreationException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - mockLog, mockJibPluginConfiguration, mockProjectProperties); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); - - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); - } - - @Test - public void testGetAppRootChecked() throws MojoExecutionException { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("/some/root"); - - Assert.assertEquals( - AbsoluteUnixPath.get("/some/root"), - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); - } - - @Test - public void testGetAppRootChecked_errorOnNonAbsolutePath() { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("relative/path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: relative/path", - ex.getMessage()); - } - } - - @Test - public void testGetAppRootChecked_errorOnWindowsPath() { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("\\windows\\path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: \\windows\\path", - ex.getMessage()); - } - } - - @Test - public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn("C:\\windows\\path"); - - try { - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: C:\\windows\\path", - ex.getMessage()); - } - } - - @Test - public void testGetAppRootChecked_defaultNonWarPackaging() throws MojoExecutionException { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn(null); - - Assert.assertEquals( - AbsoluteUnixPath.get("/app"), - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); - } - - @Test - public void testGetAppRootChecked_defaultJarPackaging() throws MojoExecutionException { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn("jar"); - - Assert.assertEquals( - AbsoluteUnixPath.get("/app"), - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); - } - - @Test - public void testGetAppRootChecked_defaultWarPackaging() throws MojoExecutionException { - Mockito.when(mockJibPluginConfiguration.getAppRoot()).thenReturn(""); - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn("war"); - - Assert.assertEquals( - AbsoluteUnixPath.get("/jetty/webapps/ROOT"), - PluginConfigurationProcessor.getAppRootChecked(mockJibPluginConfiguration)); - } - - @Test - public void testGetBaseImage_defaultWarPackaging() { - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn("war"); - - Assert.assertEquals( - "gcr.io/distroless/java/jetty", - PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); - } - - @Test - public void testGetBaseImage_defaultNonWarPackaging() { - Mockito.when(mockJibPluginConfiguration.getProject()).thenReturn(mavenProject); - Mockito.when(mavenProject.getPackaging()).thenReturn(null); - - Assert.assertEquals( - "gcr.io/distroless/java", - PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); - } - - @Test - public void testGetBaseImage_nonDefault() { - Mockito.when(mockJibPluginConfiguration.getBaseImage()).thenReturn("tomcat"); - - Assert.assertEquals( - "tomcat", PluginConfigurationProcessor.getBaseImage(mockJibPluginConfiguration)); - } - - @Test - public void testConvertPermissionsList() { - Assert.assertEquals( - ImmutableMap.of( - AbsoluteUnixPath.get("/test/folder/file1"), - FilePermissions.fromOctalString("123"), - AbsoluteUnixPath.get("/test/file2"), - FilePermissions.fromOctalString("456")), - PluginConfigurationProcessor.convertPermissionsList( - ImmutableList.of( - new PermissionConfiguration("/test/folder/file1", "123"), - new PermissionConfiguration("/test/file2", "456")))); - - try { - PluginConfigurationProcessor.convertPermissionsList( - ImmutableList.of(new PermissionConfiguration("a path", "not valid permission"))); - Assert.fail(); - } catch (IllegalArgumentException ignored) { - // pass - } - } - - // TODO should test other behaviours -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java new file mode 100644 index 0000000000..fce57f6bb0 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Indicates that the container.appRoot config value is invalid (i.e., the path is not in the + * absolute unix-path style. + */ +public class AppRootInvalidException extends Exception { + + private final String invalidAppRoot; + + public AppRootInvalidException(String message, String invalidAppRoot, Throwable ex) { + super(message, ex); + this.invalidAppRoot = invalidAppRoot; + } + + public String getInvalidAppRoot() { + return invalidAppRoot; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java index e62af16bc7..d81b6a6452 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java @@ -27,6 +27,13 @@ public interface AuthProperty { @Nullable String getPassword(); + /** + * Returns the full descriptor used to configure the {@link AuthProperty}. + * + * @return the descriptor used to configure the property (e.g. 'jib.to.auth') + */ + String getPropertyDescriptor(); + /** * Returns the full descriptor used to configure the {@link AuthProperty}'s username. * diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java new file mode 100644 index 0000000000..5198597dbc --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Exception when trying to retrieve an inferred auth info. + * + * @see RawConfiguration#getInferredAuth(String) + */ +public class InferredAuthRetrievalException extends Exception { + + public InferredAuthRetrievalException(String message) { + super(message); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 6c8b224711..1f9d7a9b88 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -43,10 +43,10 @@ public class MainClassResolver { *

Warns if main class provided by {@code projectProperties} is not valid, or throws an error * if no valid main class is found. * - * @param mainClass the explicitly configured main class ({@code null} if not configured). - * @param projectProperties properties containing plugin information and help messages. - * @return the name of the main class to be used for the container entrypoint. - * @throws MainClassInferenceException if no valid main class is configured or discovered. + * @param mainClass the explicitly configured main class ({@code null} if not configured) + * @param projectProperties properties containing plugin information and help messages + * @return the name of the main class to be used for the container entrypoint + * @throws MainClassInferenceException if no valid main class is configured or discovered */ public static String resolveMainClass( @Nullable String mainClass, ProjectProperties projectProperties) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java new file mode 100644 index 0000000000..6a2e71d027 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -0,0 +1,263 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.cloud.tools.jib.frontend.ExposedPortsParser; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.base.Verify; +import java.io.FileNotFoundException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * Configures and provides {@code JibContainerBuilder} for the image building tasks based on raw + * plugin configuration values and project properties. + */ +public class PluginConfigurationProcessor { + + /** + * Gets the value of the {@code appRoot} parameter. If the parameter is empty, returns {@link + * JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link + * JavaLayerConfigurations#DEFAULT_APP_ROOT} for other projects. + * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the app root value + * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path + */ + static AbsoluteUnixPath getAppRootChecked( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws AppRootInvalidException { + String appRoot = rawConfiguration.getAppRoot(); + if (appRoot.isEmpty()) { + appRoot = + projectProperties.isWarProject() + ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT + : JavaLayerConfigurations.DEFAULT_APP_ROOT; + } + try { + return AbsoluteUnixPath.get(appRoot); + } catch (IllegalArgumentException ex) { + throw new AppRootInvalidException(appRoot, appRoot, ex); + } + } + + /** + * Compute the container entrypoint, in this order: + * + *

    + *
  1. the user specified one, if set + *
  2. for a WAR project, null (it must be inherited from base image) + *
  3. for a non-WAR project, by resolving the main class + *
+ * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the entrypoint + * @throws MainClassInferenceException if no valid main class is configured or discovered + * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path + */ + @Nullable + public static List computeEntrypoint( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws MainClassInferenceException, AppRootInvalidException { + Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); + if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { + if (rawConfiguration.getMainClass().isPresent() + || !rawConfiguration.getJvmFlags().isEmpty()) { + new DefaultEventDispatcher(projectProperties.getEventHandlers()) + .dispatch( + LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + } + return rawEntrypoint.get(); + } + + if (projectProperties.isWarProject()) { + return null; + } + + AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); + String mainClass = + MainClassResolver.resolveMainClass( + rawConfiguration.getMainClass().orElse(null), projectProperties); + return JavaEntrypointConstructor.makeDefaultEntrypoint( + appRoot, rawConfiguration.getJvmFlags(), mainClass); + } + + /** + * Gets the suitable value for the base image. If the raw base image parameter is null, returns + * {@code "gcr.io/distroless/java/jetty"} for WAR projects or {@code "gcr.io/distroless/java"} for + * non-WAR. + * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the base image + */ + public static String getBaseImage( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) { + return rawConfiguration + .getFromImage() + .orElse( + projectProperties.isWarProject() + ? "gcr.io/distroless/java/jetty" + : "gcr.io/distroless/java"); + } + + public static PluginConfigurationProcessor processCommonConfiguration( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws InvalidImageReferenceException, NumberFormatException, FileNotFoundException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + JibSystemProperties.checkHttpTimeoutProperty(); + + // TODO: Instead of disabling logging, have authentication credentials be provided + ImageReference baseImageReference = + ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { + eventDispatcher.dispatch( + LogEvent.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not " + + "enable this on a public network!")); + } + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); + Optional optionalFromCredential = + ConfigurationPropertyValidator.getImageCredential( + eventDispatcher, + PropertyNames.FROM_AUTH_USERNAME, + PropertyNames.FROM_AUTH_PASSWORD, + rawConfiguration.getFromAuth()); + if (optionalFromCredential.isPresent()) { + // TODO: fix https://github.com/GoogleContainerTools/jib/issues/1177 + // rawConfiguration.getFromAuth().getPropertyDescriptor() may cause NPE, so using + // "from.auth/" as a compromise. + defaultCredentialRetrievers.setKnownCredential( + optionalFromCredential.get(), "from.auth/"); + } else { + // TODO: this is here only for getting values from Maven settings.xml. Consider passing a + // Supplier for an inferred credential as an additional argument, rather than + // having RawConfigurations.getInferredAuth(). + // https://github.com/GoogleContainerTools/jib/pull/1163#discussion_r228389684 + Optional optionalInferredAuth = + rawConfiguration.getInferredAuth(baseImageReference.getRegistry()); + if (optionalInferredAuth.isPresent()) { + AuthProperty auth = optionalInferredAuth.get(); + String username = Verify.verifyNotNull(auth.getUsername()); + String password = Verify.verifyNotNull(auth.getPassword()); + Credential credential = Credential.basic(username, password); + defaultCredentialRetrievers.setInferredCredential(credential, auth.getPropertyDescriptor()); + optionalFromCredential = Optional.of(credential); + } + } + defaultCredentialRetrievers.setCredentialHelper( + rawConfiguration.getFromCredHelper().orElse(null)); + + RegistryImage baseImage = RegistryImage.named(baseImageReference); + defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + + JibContainerBuilder jibContainerBuilder = + Jib.from(baseImage) + .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) + .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) + .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) + .setEnvironment(rawConfiguration.getEnvironment()) + .setExposedPorts(ExposedPortsParser.parse(rawConfiguration.getPorts())) + .setLabels(rawConfiguration.getLabels()) + .setUser(rawConfiguration.getUser().orElse(null)); + if (rawConfiguration.getUseCurrentTimestamp()) { + eventDispatcher.dispatch( + LogEvent.warn( + "Setting image creation time to current time; your image may not be reproducible.")); + jibContainerBuilder.setCreationTime(Instant.now()); + } + + return new PluginConfigurationProcessor( + jibContainerBuilder, baseImageReference, optionalFromCredential.isPresent()); + } + + /** + * Configures a {@link Containerizer} with values pulled from project properties/raw build + * configuration. + * + * @param containerizer the {@link Containerizer} to configure + * @param rawConfiguration the raw build configuration + * @param projectProperties the project properties + * @param toolName tool name to set + */ + public static void configureContainerizer( + Containerizer containerizer, + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + String toolName) { + containerizer + .setToolName(toolName) + .setEventHandlers(projectProperties.getEventHandlers()) + .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) + .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) + .setApplicationLayersCache(projectProperties.getCacheDirectory()); + + rawConfiguration.getToTags().forEach(containerizer::withAdditionalTag); + + if (rawConfiguration.getUseOnlyProjectCache()) { + containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); + } + } + + private final JibContainerBuilder jibContainerBuilder; + private final ImageReference baseImageReference; + private final boolean isBaseImageCredentialPresent; + + private PluginConfigurationProcessor( + JibContainerBuilder jibContainerBuilder, + ImageReference baseImageReference, + boolean isBaseImageCredentialPresent) { + this.jibContainerBuilder = jibContainerBuilder; + this.baseImageReference = baseImageReference; + this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; + } + + public JibContainerBuilder getJibContainerBuilder() { + return jibContainerBuilder; + } + + public ImageReference getBaseImageReference() { + return baseImageReference; + } + + public boolean isBaseImageCredentialPresent() { + return isBaseImageCredentialPresent; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java new file mode 100644 index 0000000000..3ddf837518 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Holds raw plugin configuration parameter values. Acts as a common adapter for heterogeneous + * plugin configuration models. + */ +public interface RawConfiguration { + + Optional getFromImage(); + + AuthProperty getFromAuth(); + + Optional getFromCredHelper(); + + Iterable getToTags(); + + Optional> getEntrypoint(); + + Optional> getProgramArguments(); + + Optional getMainClass(); + + List getJvmFlags(); + + String getAppRoot(); + + Map getEnvironment(); + + Map getLabels(); + + List getPorts(); + + Optional getUser(); + + boolean getUseCurrentTimestamp(); + + boolean getAllowInsecureRegistries(); + + boolean getUseOnlyProjectCache(); + + Optional getInferredAuth(String authTarget) throws InferredAuthRetrievalException; +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java new file mode 100644 index 0000000000..1a524252ad --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -0,0 +1,375 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PluginConfigurationProcessor}. */ +@RunWith(MockitoJUnitRunner.class) +public class PluginConfigurationProcessorTest { + + private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + return JibContainerBuilderTestHelper.toBuildConfiguration( + jibContainerBuilder, + BuildConfiguration.builder(), + Containerizer.to(RegistryImage.named("ignored"))); + } + + @Mock private RawConfiguration rawConfiguration; + @Mock private ProjectProperties projectProperties; + @Mock private AuthProperty authProperty; + @Mock private Consumer logger; + + @Before + public void setUp() { + Mockito.when(rawConfiguration.getFromAuth()).thenReturn(authProperty); + Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); + + Mockito.when(projectProperties.getJavaLayerConfigurations()) + .thenReturn(JavaLayerConfigurations.builder().build()); + Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); + Mockito.when(projectProperties.getEventHandlers()) + .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); + } + + /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ + @Test + public void testPluginConfigurationProcessor_defaults() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + + ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; + Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); + } + + @Test + public void testPluginConfigurationProcessor_warProjectBaseImage() + throws InvalidImageReferenceException, FileNotFoundException, MainClassInferenceException, + AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + Assert.assertEquals( + ImageReference.parse("gcr.io/distroless/java/jetty").toString(), + processor.getBaseImageReference().toString()); + Mockito.verifyNoMoreInteractions(logger); + } + + @Test + public void testEntrypoint() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getEntrypoint()) + .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verifyZeroInteractions(logger); + } + + @Test + public void testEntrypoint_defaultWarPackaging() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verifyZeroInteractions(logger); + } + + @Test + public void testEntrypoint_defaulNonWarPackaging() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); + Mockito.when(projectProperties.isWarProject()).thenReturn(false); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + + ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; + Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); + } + + @Test + public void testUser() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); + } + + @Test + public void testUser_null() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); + } + + @Test + public void testEntrypoint_warningOnJvmFlags() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getEntrypoint()) + .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); + Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verify(logger) + .accept(LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + } + + @Test + public void testEntrypoint_warningOnMainclass() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getEntrypoint()) + .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); + Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList("custom", "entrypoint"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verify(logger) + .accept(LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + } + + @Test + public void testEntrypointClasspath_nonDefaultAppRoot() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals( + "java", buildConfiguration.getContainerConfiguration().getEntrypoint().get(0)); + Assert.assertEquals( + "-cp", buildConfiguration.getContainerConfiguration().getEntrypoint().get(1)); + Assert.assertEquals( + "/my/app/resources:/my/app/classes:/my/app/libs/*", + buildConfiguration.getContainerConfiguration().getEntrypoint().get(2)); + } + + @Test + public void testWebAppEntrypoint_inheritedFromBaseImage() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + PluginConfigurationProcessor processor = + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + } + + @Test + public void testGetAppRootChecked() throws AppRootInvalidException { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/some/root"); + + Assert.assertEquals( + AbsoluteUnixPath.get("/some/root"), + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + } + + @Test + public void testGetAppRootChecked_errorOnNonAbsolutePath() { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("relative/path"); + + try { + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + Assert.fail(); + } catch (AppRootInvalidException ex) { + Assert.assertEquals("relative/path", ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPath() { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("\\windows\\path"); + + try { + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + Assert.fail(); + } catch (AppRootInvalidException ex) { + Assert.assertEquals("\\windows\\path", ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn("C:\\windows\\path"); + + try { + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + Assert.fail(); + } catch (AppRootInvalidException ex) { + Assert.assertEquals("C:\\windows\\path", ex.getMessage()); + } + } + + @Test + public void testGetAppRootChecked_defaultNonWarProject() throws AppRootInvalidException { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(projectProperties.isWarProject()).thenReturn(false); + + Assert.assertEquals( + AbsoluteUnixPath.get("/app"), + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + } + + @Test + public void testGetAppRootChecked_defaultWarProject() throws AppRootInvalidException { + Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + Assert.assertEquals( + AbsoluteUnixPath.get("/jetty/webapps/ROOT"), + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_defaultNonWarPackaging() { + Mockito.when(projectProperties.isWarProject()).thenReturn(false); + + Assert.assertEquals( + "gcr.io/distroless/java", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_defaultWarProject() { + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + Assert.assertEquals( + "gcr.io/distroless/java/jetty", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_nonDefault() { + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("tomcat")); + + Assert.assertEquals( + "tomcat", PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } +} From 2bb8a5263a66ae57abac165bc91ac91ead1962c9 Mon Sep 17 00:00:00 2001 From: Gustavo Silva Paiva Date: Tue, 30 Oct 2018 14:57:36 -0300 Subject: [PATCH 0322/2020] Add layer description to layer history (#1198) --- .../BuildAndCacheApplicationLayerStep.java | 4 + .../jib/builder/steps/BuildImageStep.java | 1 + .../jib/builder/steps/BuildImageStepTest.java | 104 +++++++++++++----- jib-gradle-plugin/CHANGELOG.md | 3 +- jib-maven-plugin/CHANGELOG.md | 1 + 5 files changed, 84 insertions(+), 29 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 0694d9cc5c..f4853e24d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -88,6 +88,10 @@ public ListenableFuture getFuture() { return listenableFuture; } + public String getLayerType() { + return layerType; + } + @Override public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index c2d69bee2c..be9048bd5b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -144,6 +144,7 @@ private Image afterCachedLayerSteps() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) + .setComment(buildAndCacheApplicationLayerStep.getLayerType()) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 81f9df5638..d1795fd99c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -35,6 +35,7 @@ import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -53,7 +54,10 @@ public class BuildImageStepTest { @Mock private PullBaseImageStep mockPullBaseImageStep; @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStep; + @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepDependencies; + @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepResources; + @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepClasses; + @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepExtraFiles; private DescriptorDigest testDescriptorDigest; private HistoryEntry nonEmptyLayerHistory; @@ -141,8 +145,24 @@ public BlobDescriptor getBlobDescriptor() { .thenReturn( Futures.immediateFuture( new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null))); - Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + + Stream.of( + mockBuildAndCacheApplicationLayerStepClasses, + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepExtraFiles, + mockBuildAndCacheApplicationLayerStepResources) + .forEach( + layerStep -> + Mockito.when(layerStep.getFuture()) + .thenReturn(Futures.immediateFuture(testCachedLayer))); + + Mockito.when(mockBuildAndCacheApplicationLayerStepClasses.getLayerType()).thenReturn("classes"); + Mockito.when(mockBuildAndCacheApplicationLayerStepDependencies.getLayerType()) + .thenReturn("dependencies"); + Mockito.when(mockBuildAndCacheApplicationLayerStepExtraFiles.getLayerType()) + .thenReturn("extra files"); + Mockito.when(mockBuildAndCacheApplicationLayerStepResources.getLayerType()) + .thenReturn("resources"); } @Test @@ -154,9 +174,9 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); @@ -176,9 +196,9 @@ public void test_propagateBaseImageConfiguration() mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE"), @@ -209,9 +229,9 @@ public void test_inheritedEntrypoint() throws ExecutionException, InterruptedExc mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); @@ -231,9 +251,9 @@ public void test_inheritedEntrypointAndProgramArguments() mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); @@ -253,9 +273,9 @@ public void test_notInheritedProgramArguments() throws ExecutionException, Inter mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("myEntrypoint"), image.getEntrypoint()); @@ -271,9 +291,10 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep, - mockBuildAndCacheApplicationLayerStep)); + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses, + mockBuildAndCacheApplicationLayerStepExtraFiles)); Image image = buildImageStep.getFuture().get().getFuture().get(); // Make sure history is as expected @@ -282,11 +303,37 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted .setCreationTimestamp(Instant.EPOCH) .setComment("auto-generated by Jib") .build(); - HistoryEntry expectedApplicationLayerHistory = + + HistoryEntry expectedApplicationLayerHistoryDependencies = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("dependencies") + .build(); + + HistoryEntry expectedApplicationLayerHistoryResources = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("resources") + .build(); + + HistoryEntry expectedApplicationLayerHistoryClasses = + HistoryEntry.builder() + .setCreationTimestamp(Instant.EPOCH) + .setAuthor("Jib") + .setCreatedBy("jib:null") + .setComment("classes") + .build(); + + HistoryEntry expectedApplicationLayerHistoryExtrafiles = HistoryEntry.builder() .setCreationTimestamp(Instant.EPOCH) .setAuthor("Jib") .setCreatedBy("jib:null") + .setComment("extra files") .build(); // Base layers (1 non-empty propagated, 2 empty propagated, 2 non-empty generated) @@ -296,12 +343,13 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(3)); Assert.assertEquals(expectedAddedBaseLayerHistory, image.getHistory().get(4)); - // Application layers (3 generated) - Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(5)); - Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(6)); - Assert.assertEquals(expectedApplicationLayerHistory, image.getHistory().get(7)); + // Application layers (4 generated) + Assert.assertEquals(expectedApplicationLayerHistoryDependencies, image.getHistory().get(5)); + Assert.assertEquals(expectedApplicationLayerHistoryResources, image.getHistory().get(6)); + Assert.assertEquals(expectedApplicationLayerHistoryClasses, image.getHistory().get(7)); + Assert.assertEquals(expectedApplicationLayerHistoryExtrafiles, image.getHistory().get(8)); - // Should be exactly 8 total - Assert.assertEquals(8, image.getHistory().size()); + // Should be exactly 9 total + Assert.assertEquals(9, image.getHistory().size()); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index bb0a70c12c..e3c2461c1b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,7 +13,8 @@ All notable changes to this project will be documented in this file. - `jib.extraDirectory.path` configures the extra layer directory (still also configurable via `jib.extraDirectory = file(...)`) - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string) - Image digest is now written to `build/jib-image.digest` ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) - +- Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) + ### Changed - Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4a62b43fb0..35891a5133 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - `` configures the extra layer directory (still also configurable via `...`) - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) - Image digest is now written to `build/jib-image.digest` ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) +- Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) ### Changed From e3cee296d6dd7f5887b103efc982b0781a167b34 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 30 Oct 2018 16:43:50 -0400 Subject: [PATCH 0323/2020] Configuration refactoring idea (#1197) --- .../builder/BuildStepsIntegrationTest.java | 4 +- .../tools/jib/api/JibContainerBuilder.java | 69 +++++++------------ .../configuration/ContainerConfiguration.java | 43 ++++++++++-- .../jib/api/JibContainerBuilderTest.java | 28 +++++--- .../api/JibContainerBuilderTestHelper.java | 6 +- .../PluginConfigurationProcessorTest.java | 4 +- 6 files changed, 82 insertions(+), 72 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 5cf6b3ae65..aebab6db26 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -96,8 +96,8 @@ private static void assertDockerInspect(String imageReference) + " }")); String dockerConfigEnv = new Command("docker", "inspect", "-f", "{{.Config.Env}}", imageReference).run(); - Assert.assertThat( - dockerConfigEnv, CoreMatchers.containsString("env1=envvalue1 env2=envvalue2")); + Assert.assertThat(dockerConfigEnv, CoreMatchers.containsString("env1=envvalue1")); + Assert.assertThat(dockerConfigEnv, CoreMatchers.containsString("env2=envvalue2")); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-integration-test")); Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index cb76d6e0d1..209591a2dc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -27,13 +27,11 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -58,21 +56,21 @@ // TODO: Add tests once containerize() is added. public class JibContainerBuilder { - private final SourceImage baseImage; + private final ContainerConfiguration.Builder containerConfigurationBuilder = + ContainerConfiguration.builder(); + private final BuildConfiguration.Builder buildConfigurationBuilder; private List layerConfigurations = new ArrayList<>(); - private Map environment = new HashMap<>(); - private List ports = new ArrayList<>(); - private Map labels = new HashMap<>(); - @Nullable private ImmutableList entrypoint; - @Nullable private ImmutableList programArguments; - private ImageFormat imageFormat = ImageFormat.Docker; - private Instant creationTime = Instant.EPOCH; - @Nullable private String user; /** Instantiate with {@link Jib#from}. */ JibContainerBuilder(SourceImage baseImage) { - this.baseImage = baseImage; + this(baseImage, BuildConfiguration.builder()); + } + + @VisibleForTesting + JibContainerBuilder(SourceImage baseImage, BuildConfiguration.Builder buildConfigurationBuilder) { + this.buildConfigurationBuilder = buildConfigurationBuilder; + buildConfigurationBuilder.setBaseImageConfiguration(baseImage.toImageConfiguration()); } /** @@ -163,7 +161,7 @@ public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { * @return this */ public JibContainerBuilder setEntrypoint(@Nullable List entrypoint) { - this.entrypoint = entrypoint == null ? null : ImmutableList.copyOf(entrypoint); + containerConfigurationBuilder.setEntrypoint(entrypoint); return this; } @@ -195,8 +193,7 @@ public JibContainerBuilder setEntrypoint(String... entrypoint) { * @return this */ public JibContainerBuilder setProgramArguments(@Nullable List programArguments) { - this.programArguments = - programArguments == null ? null : ImmutableList.copyOf(programArguments); + containerConfigurationBuilder.setProgramArguments(programArguments); return this; } @@ -225,7 +222,7 @@ public JibContainerBuilder setProgramArguments(String... programArguments) { * @return this */ public JibContainerBuilder setEnvironment(Map environmentMap) { - environment = new HashMap<>(environmentMap); + containerConfigurationBuilder.setEnvironment(environmentMap); return this; } @@ -238,7 +235,7 @@ public JibContainerBuilder setEnvironment(Map environmentMap) { * @see #setEnvironment for more details */ public JibContainerBuilder addEnvironmentVariable(String name, String value) { - environment.put(name, value); + containerConfigurationBuilder.addEnvironment(name, value); return this; } @@ -258,7 +255,7 @@ public JibContainerBuilder addEnvironmentVariable(String name, String value) { * @return this */ public JibContainerBuilder setExposedPorts(List ports) { - this.ports = new ArrayList<>(ports); + containerConfigurationBuilder.setExposedPorts(ports); return this; } @@ -281,7 +278,7 @@ public JibContainerBuilder setExposedPorts(Port... ports) { * @see #setExposedPorts(List) for more details */ public JibContainerBuilder addExposedPort(Port port) { - ports.add(port); + containerConfigurationBuilder.addExposedPort(port); return this; } @@ -295,7 +292,7 @@ public JibContainerBuilder addExposedPort(Port port) { * @return this */ public JibContainerBuilder setLabels(Map labelMap) { - labels = new HashMap<>(labelMap); + containerConfigurationBuilder.setLabels(labelMap); return this; } @@ -307,7 +304,7 @@ public JibContainerBuilder setLabels(Map labelMap) { * @return this */ public JibContainerBuilder addLabel(String key, String value) { - labels.put(key, value); + containerConfigurationBuilder.addLabel(key, value); return this; } @@ -319,7 +316,7 @@ public JibContainerBuilder addLabel(String key, String value) { * @return this */ public JibContainerBuilder setFormat(ImageFormat imageFormat) { - this.imageFormat = imageFormat; + buildConfigurationBuilder.setTargetFormat(imageFormat.getManifestTemplateClass()); return this; } @@ -330,7 +327,7 @@ public JibContainerBuilder setFormat(ImageFormat imageFormat) { * @return this */ public JibContainerBuilder setCreationTime(Instant creationTime) { - this.creationTime = creationTime; + containerConfigurationBuilder.setCreationTime(creationTime); return this; } @@ -353,7 +350,7 @@ public JibContainerBuilder setCreationTime(Instant creationTime) { * @return this */ public JibContainerBuilder setUser(@Nullable String user) { - this.user = user; + containerConfigurationBuilder.setUser(user); return this; } @@ -371,8 +368,7 @@ public JibContainerBuilder setUser(@Nullable String user) { public JibContainer containerize(Containerizer containerizer) throws InterruptedException, ExecutionException, IOException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = - toBuildConfiguration(BuildConfiguration.builder(), containerizer); + BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer); BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); return new JibContainer(result.getImageDigest(), result.getImageId()); } @@ -380,25 +376,21 @@ public JibContainer containerize(Containerizer containerizer) /** * Builds a {@link BuildConfiguration} using this and a {@link Containerizer}. * - * @param buildConfigurationBuilder the {@link BuildConfiguration.Builder} to use * @param containerizer the {@link Containerizer} * @return the {@link BuildConfiguration} * @throws CacheDirectoryCreationException if a cache directory could not be created * @throws IOException if an I/O exception occurs */ @VisibleForTesting - BuildConfiguration toBuildConfiguration( - BuildConfiguration.Builder buildConfigurationBuilder, Containerizer containerizer) + BuildConfiguration toBuildConfiguration(Containerizer containerizer) throws CacheDirectoryCreationException, IOException { buildConfigurationBuilder - .setBaseImageConfiguration(baseImage.toImageConfiguration()) .setTargetImageConfiguration(containerizer.getTargetImage().toImageConfiguration()) .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) .setApplicationLayersCacheDirectory(containerizer.getApplicationLayersCacheDirectory()) - .setContainerConfiguration(toContainerConfiguration()) + .setContainerConfiguration(containerConfigurationBuilder.build()) .setLayerConfigurations(layerConfigurations) - .setTargetFormat(imageFormat.getManifestTemplateClass()) .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) .setToolName(containerizer.getToolName()); @@ -413,17 +405,4 @@ BuildConfiguration toBuildConfiguration( return buildConfigurationBuilder.build(); } - - @VisibleForTesting - ContainerConfiguration toContainerConfiguration() { - return ContainerConfiguration.builder() - .setEntrypoint(entrypoint) - .setProgramArguments(programArguments) - .setEnvironment(environment) - .setExposedPorts(ports) - .setLabels(labels) - .setCreationTime(creationTime) - .setUser(user) - .build(); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index f6325bd687..ce44efca6a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -39,9 +41,9 @@ public static class Builder { private Instant creationTime = DEFAULT_CREATION_TIME; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; - @Nullable private ImmutableMap environmentMap; - @Nullable private ImmutableList exposedPorts; - @Nullable private ImmutableMap labels; + @Nullable private Map environmentMap; + @Nullable private List exposedPorts; + @Nullable private Map labels; @Nullable private String user; /** @@ -83,11 +85,18 @@ public Builder setEnvironment(@Nullable Map environmentMap) { } else { Preconditions.checkArgument(!Iterables.any(environmentMap.keySet(), Objects::isNull)); Preconditions.checkArgument(!Iterables.any(environmentMap.values(), Objects::isNull)); - this.environmentMap = ImmutableMap.copyOf(environmentMap); + this.environmentMap = new HashMap<>(environmentMap); } return this; } + public void addEnvironment(String name, String value) { + if (environmentMap == null) { + environmentMap = new HashMap<>(); + } + environmentMap.put(name, value); + } + /** * Sets the container's exposed ports. * @@ -99,11 +108,18 @@ public Builder setExposedPorts(@Nullable List exposedPorts) { this.exposedPorts = null; } else { Preconditions.checkArgument(!exposedPorts.contains(null)); - this.exposedPorts = ImmutableList.copyOf(exposedPorts); + this.exposedPorts = new ArrayList<>(exposedPorts); } return this; } + public void addExposedPort(Port port) { + if (exposedPorts == null) { + exposedPorts = new ArrayList<>(); + } + exposedPorts.add(port); + } + /** * Sets the container's labels. * @@ -116,11 +132,18 @@ public Builder setLabels(@Nullable Map labels) { } else { Preconditions.checkArgument(!Iterables.any(labels.keySet(), Objects::isNull)); Preconditions.checkArgument(!Iterables.any(labels.values(), Objects::isNull)); - this.labels = ImmutableMap.copyOf(labels); + this.labels = new HashMap<>(labels); } return this; } + public void addLabel(String key, String value) { + if (labels == null) { + labels = new HashMap<>(); + } + labels.put(key, value); + } + /** * Sets the container entrypoint. * @@ -157,7 +180,13 @@ public Builder setUser(@Nullable String user) { */ public ContainerConfiguration build() { return new ContainerConfiguration( - creationTime, entrypoint, programArguments, environmentMap, exposedPorts, labels, user); + creationTime, + entrypoint, + programArguments, + environmentMap == null ? null : ImmutableMap.copyOf(environmentMap), + exposedPorts == null ? null : ImmutableList.copyOf(exposedPorts), + labels == null ? null : ImmutableMap.copyOf(labels), + user); } private Builder() {} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 98fac4d559..496cb9a7d7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -58,9 +58,11 @@ public class JibContainerBuilderTest { @Mock private JibEvent mockJibEvent; @Test - public void testToContainerConfiguration_set() throws InvalidImageReferenceException { + public void testToBuildConfiguration_containerConfigurationSet() + throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { + RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - Jib.from("base/image/reference") + new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(Arrays.asList(Port.tcp(1234), Port.udp(5678))) @@ -69,7 +71,9 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep .setCreationTime(Instant.ofEpochMilli(1000)) .setUser("user"); - ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); + BuildConfiguration buildConfiguration = + jibContainerBuilder.toBuildConfiguration(Containerizer.to(baseImage)); + ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( ImmutableMap.of("name", "value"), containerConfiguration.getEnvironmentMap()); @@ -83,9 +87,11 @@ public void testToContainerConfiguration_set() throws InvalidImageReferenceExcep } @Test - public void testToContainerConfiguration_add() { + public void testToBuildConfiguration_containerConfigurationAdd() + throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { + RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - Jib.from(ImageReference.of("base", "image", "reference")) + new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setEntrypoint("entry", "point") .setEnvironment(ImmutableMap.of("name", "value")) .addEnvironmentVariable("environment", "variable") @@ -95,7 +101,9 @@ public void testToContainerConfiguration_add() { .addLabel("added", "label") .setProgramArguments("program", "arguments"); - ContainerConfiguration containerConfiguration = jibContainerBuilder.toContainerConfiguration(); + BuildConfiguration buildConfiguration = + jibContainerBuilder.toBuildConfiguration(Containerizer.to(baseImage)); + ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( ImmutableMap.of("name", "value", "environment", "variable"), @@ -127,13 +135,12 @@ public void testToBuildConfiguration() .setEventHandlers(new EventHandlers().add(mockJibEventConsumer)); JibContainerBuilder jibContainerBuilder = - Jib.from(baseImage) + new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); - BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration(spyBuildConfigurationBuilder, containerizer); + BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(containerizer); Assert.assertEquals( - jibContainerBuilder.toContainerConfiguration(), + spyBuildConfigurationBuilder.build().getContainerConfiguration(), buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -187,7 +194,6 @@ public void testToBuildConfiguration() jibContainerBuilder .setFormat(ImageFormat.OCI) .toBuildConfiguration( - spyBuildConfigurationBuilder, containerizer .withAdditionalTag("tag1") .withAdditionalTag("tag2") diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java index 83c1afc13f..b6116220d8 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -24,11 +24,9 @@ public class JibContainerBuilderTestHelper { public static BuildConfiguration toBuildConfiguration( - JibContainerBuilder jibContainerBuilder, - BuildConfiguration.Builder buildConfigurationBuilder, - Containerizer containerizer) + JibContainerBuilder jibContainerBuilder, Containerizer containerizer) throws IOException, CacheDirectoryCreationException { - return jibContainerBuilder.toBuildConfiguration(buildConfigurationBuilder, containerizer); + return jibContainerBuilder.toBuildConfiguration(containerizer); } private JibContainerBuilderTestHelper() {} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 1a524252ad..6d9510cb39 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -51,9 +51,7 @@ public class PluginConfigurationProcessorTest { private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { return JibContainerBuilderTestHelper.toBuildConfiguration( - jibContainerBuilder, - BuildConfiguration.builder(), - Containerizer.to(RegistryImage.named("ignored"))); + jibContainerBuilder, Containerizer.to(RegistryImage.named("ignored"))); } @Mock private RawConfiguration rawConfiguration; From 64f20d80e0171c9695f5d0e7e4b82185a734865b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 30 Oct 2018 16:51:37 -0400 Subject: [PATCH 0324/2020] Add jib-core changelog (#1207) --- jib-core/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 jib-core/CHANGELOG.md diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md new file mode 100644 index 0000000000..68e44c3231 --- /dev/null +++ b/jib-core/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [unreleased] + +### Added + +### Changed + +### Fixed \ No newline at end of file From fe9fb3df0d6dbded3c0b16eb48d4c7e780d5d647 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 31 Oct 2018 12:59:58 -0400 Subject: [PATCH 0325/2020] Log inferred cred helper failure at info instead of warn (#1210) --- .../cloud/tools/jib/frontend/CredentialRetrieverFactory.java | 2 +- .../tools/jib/frontend/CredentialRetrieverFactoryTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 67514c7cf7..3a9fc29192 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -177,7 +177,7 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - dispatchEvent(LogEvent.warn(ex.getMessage())); + dispatchEvent(LogEvent.info(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { dispatchEvent(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index d9abf4da35..0cf1ecfeb1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -123,7 +123,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { } @Test - public void testInferCredentialHelper_warn() throws CredentialRetrievalException { + public void testInferCredentialHelper_info() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.amazonaws.com", "repository", null), @@ -137,7 +137,7 @@ public void testInferCredentialHelper_warn() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); - Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn("warning")); + Mockito.verify(mockEventDispatcher).dispatch(LogEvent.info("warning")); Mockito.verify(mockEventDispatcher).dispatch(LogEvent.info(" Caused by: the root cause")); } From c3dd090a35b014132192b91ac0862ef26beb1f4d Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 31 Oct 2018 16:38:59 -0400 Subject: [PATCH 0326/2020] Share more code between tasks, Maven and Gradle (#1205) --- .../tools/jib/gradle/BuildDockerTask.java | 37 +-- .../tools/jib/gradle/BuildImageTask.java | 53 +--- .../cloud/tools/jib/gradle/BuildTarTask.java | 42 +-- .../jib/gradle/GradleProjectProperties.java | 15 + .../jib/gradle/GradleRawConfiguration.java | 21 ++ .../tools/jib/maven/BuildDockerMojo.java | 28 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 62 +--- .../cloud/tools/jib/maven/BuildTarMojo.java | 33 +- .../jib/maven/MavenProjectProperties.java | 15 + .../jib/maven/MavenRawConfiguration.java | 21 ++ .../common/PluginConfigurationProcessor.java | 281 +++++++++++++----- .../jib/plugins/common/ProjectProperties.java | 6 + .../jib/plugins/common/RawConfiguration.java | 12 + .../PluginConfigurationProcessorTest.java | 43 ++- 14 files changed, 372 insertions(+), 297 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index c755faa72b..fe8cfbb6a4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,20 +16,15 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.DockerDaemonImage; -import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -104,31 +99,11 @@ public void buildDocker() GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTo().getImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion().toString().equals("unspecified") - ? "latest" - : getProject().getVersion().toString(), - gradleHelpfulSuggestionsBuilder.build()); - - DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor.getJibContainerBuilder(); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); + PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( + rawConfiguration, projectProperties, gradleHelpfulSuggestionsBuilder.build()); + ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = gradleHelpfulSuggestionsBuilder .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) @@ -141,9 +116,9 @@ public void buildDocker() BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, - eventDispatcher, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 6c7977167c..4f08f6a47c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,33 +16,23 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; -import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Path; -import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -108,57 +98,28 @@ public void buildImage() "gradle jib --image ")); } - ImageReference targetImageReference = ImageReference.parse(jibExtension.getTo().getImage()); - - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); - Optional optionalToCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.TO_AUTH_USERNAME, - PropertyNames.TO_AUTH_PASSWORD, - jibExtension.getTo().getAuth()); - optionalToCredential.ifPresent( - toCredential -> - defaultCredentialRetrievers.setKnownCredential(toCredential, "jib.to.auth")); - defaultCredentialRetrievers.setCredentialHelper(jibExtension.getTo().getCredHelper()); - - RegistryImage targetImage = RegistryImage.named(targetImageReference); - defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( + PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( rawConfiguration, projectProperties); - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor - .getJibContainerBuilder() - // Only uses possibly non-Docker formats for build to registry. - .setFormat(jibExtension.getContainer().getFormat()); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); - + ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) + .setTargetImageHasConfiguredCredentials( + pluginConfigurationProcessor.isTargetImageCredentialPresent()) .build(); Path buildOutput = getProject().getBuildDir().toPath(); BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, - eventDispatcher, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 150fde2daa..67aebc7a5b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,19 +16,13 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.TarImage; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -120,47 +114,29 @@ public void buildTar() GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - jibExtension.getTo().getImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion().toString().equals("unspecified") - ? "latest" - : getProject().getVersion().toString(), - gradleHelpfulSuggestionsBuilder.build()); - Path tarOutputPath = getTargetPath(); - TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor.getJibContainerBuilder(); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, GradleProjectProperties.TOOL_NAME); + PluginConfigurationProcessor.processCommonConfigurationForTarImage( + rawConfiguration, + projectProperties, + tarOutputPath, + gradleHelpfulSuggestionsBuilder.build()); HelpfulSuggestions helpfulSuggestions = gradleHelpfulSuggestionsBuilder .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) + .setTargetImageReference(pluginConfigurationProcessor.getTargetImageReference()) .build(); Path buildOutput = getProject().getBuildDir().toPath(); BuildStepsRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, - eventDispatcher, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 13675b0965..954c629868 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -123,6 +123,11 @@ public EventHandlers getEventHandlers() { return eventHandlers; } + @Override + public String getToolName() { + return TOOL_NAME; + } + @Override public String getPluginName() { return PLUGIN_NAME; @@ -177,6 +182,16 @@ static FileCollection getInputFiles(File extraDirectory, Project project) { } } + @Override + public String getName() { + return project.getName(); + } + + @Override + public String getVersion() { + return project.getVersion().toString(); + } + /** * Validates and converts a {@code String->String} file-path-to-file-permissions map to an * equivalent {@code AbsoluteUnixPath->FilePermission} map. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 0290a5a258..ca97e5dbea 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import java.util.List; @@ -46,6 +47,21 @@ public Optional getFromCredHelper() { return Optional.ofNullable(jibExtension.getFrom().getCredHelper()); } + @Override + public Optional getToImage() { + return Optional.ofNullable(jibExtension.getTo().getImage()); + } + + @Override + public AuthProperty getToAuth() { + return jibExtension.getTo().getAuth(); + } + + @Override + public Optional getToCredHelper() { + return Optional.ofNullable(jibExtension.getTo().getCredHelper()); + } + @Override public Iterable getToTags() { return jibExtension.getTo().getTags(); @@ -115,4 +131,9 @@ public boolean getUseOnlyProjectCache() { public Optional getInferredAuth(String authTarget) { return Optional.empty(); } + + @Override + public ImageFormat getImageFormat() { + return jibExtension.getContainer().getFormat(); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index dfea131995..d2c642f8ce 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,9 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.DockerDaemonImage; -import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; @@ -29,7 +26,6 @@ import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -88,25 +84,11 @@ public void execute() throws MojoExecutionException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - getTargetImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion(), - mavenHelpfulSuggestionsBuilder.build()); - DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor.getJibContainerBuilder(); - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); + PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( + rawConfiguration, projectProperties, mavenHelpfulSuggestionsBuilder.build()); + ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) @@ -119,8 +101,8 @@ public void execute() throws MojoExecutionException { BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index dcf0c38e28..5f72719c49 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,38 +16,27 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; -import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; -import com.google.cloud.tools.jib.plugins.common.DefaultCredentialRetrievers; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.common.base.Verify; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; -import java.util.Optional; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -112,65 +101,26 @@ public void execute() throws MojoExecutionException, MojoFailureException { RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( + PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( rawConfiguration, projectProperties); - ImageReference targetImageReference = ImageReference.parse(getTargetImage()); - - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(targetImageReference, eventDispatcher)); - Optional optionalToCredential = - ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - PropertyNames.TO_AUTH_USERNAME, - PropertyNames.TO_AUTH_PASSWORD, - getTargetImageAuth()); - if (optionalToCredential.isPresent()) { - defaultCredentialRetrievers.setKnownCredential( - optionalToCredential.get(), "jib-maven-plugin configuration"); - } else { - Optional optionalInferredAuth = - rawConfiguration.getInferredAuth(targetImageReference.getRegistry()); - if (optionalInferredAuth.isPresent()) { - AuthProperty auth = optionalInferredAuth.get(); - String username = Verify.verifyNotNull(auth.getUsername()); - String password = Verify.verifyNotNull(auth.getPassword()); - Credential credential = Credential.basic(username, password); - defaultCredentialRetrievers.setInferredCredential( - credential, auth.getPropertyDescriptor()); - } - } - defaultCredentialRetrievers.setCredentialHelper(getTargetImageCredentialHelperName()); - - RegistryImage targetImage = RegistryImage.named(targetImageReference); - defaultCredentialRetrievers.asList().forEach(targetImage::addCredentialRetriever); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor - .getJibContainerBuilder() - // Only uses possibly non-Docker formats for build to registry. - .setFormat(ImageFormat.valueOf(getFormat())); - - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); - + ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials(optionalToCredential.isPresent()) + .setTargetImageHasConfiguredCredentials( + pluginConfigurationProcessor.isTargetImageCredentialPresent()) .build(); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 5411349977..a67cd25882 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,19 +16,14 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.TarImage; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -82,40 +77,28 @@ public void execute() throws MojoExecutionException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - ImageReference targetImageReference = - ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - getTargetImage(), - eventDispatcher, - getProject().getName(), - getProject().getVersion(), - mavenHelpfulSuggestionsBuilder.build()); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); Path tarOutputPath = buildOutput.resolve("jib-image.tar"); - TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarOutputPath); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); - - JibContainerBuilder jibContainerBuilder = - pluginConfigurationProcessor.getJibContainerBuilder(); - Containerizer containerizer = Containerizer.to(targetImage); - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties, MavenProjectProperties.TOOL_NAME); + PluginConfigurationProcessor.processCommonConfigurationForTarImage( + rawConfiguration, + projectProperties, + tarOutputPath, + mavenHelpfulSuggestionsBuilder.build()); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) .setBaseImageHasConfiguredCredentials( pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) + .setTargetImageReference(pluginConfigurationProcessor.getTargetImageReference()) .build(); BuildStepsRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .build( - jibContainerBuilder, - containerizer, + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a4ad4e1298..12fd439e8a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -111,6 +111,11 @@ public EventHandlers getEventHandlers() { return eventHandlers; } + @Override + public String getToolName() { + return TOOL_NAME; + } + @Override public String getPluginName() { return PLUGIN_NAME; @@ -156,4 +161,14 @@ public String getJarPluginName() { public boolean isWarProject() { return "war".equals(project.getPackaging()); } + + @Override + public String getName() { + return project.getName(); + } + + @Override + public String getVersion() { + return project.getVersion(); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 01925fcd4e..da155a8520 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -61,6 +62,21 @@ public Optional getFromCredHelper() { return Optional.ofNullable(jibPluginConfiguration.getBaseImageCredentialHelperName()); } + @Override + public Optional getToImage() { + return Optional.ofNullable(jibPluginConfiguration.getTargetImage()); + } + + @Override + public AuthProperty getToAuth() { + return jibPluginConfiguration.getTargetImageAuth(); + } + + @Override + public Optional getToCredHelper() { + return Optional.ofNullable(jibPluginConfiguration.getTargetImageCredentialHelperName()); + } + @Override public Iterable getToTags() { return jibPluginConfiguration.getTargetImageAdditionalTags(); @@ -131,4 +147,9 @@ public Optional getInferredAuth(String authTarget) throws InferredAuthRetrievalException { return mavenSettingsServerCredentials.retrieve(authTarget); } + + @Override + public ImageFormat getImageFormat() { + return ImageFormat.valueOf(jibPluginConfiguration.getFormat()); + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 6a2e71d027..300043926d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -17,9 +17,11 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DockerDaemonImage; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.TarImage; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; @@ -32,8 +34,12 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.base.Verify; import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Optional; @@ -45,32 +51,11 @@ */ public class PluginConfigurationProcessor { - /** - * Gets the value of the {@code appRoot} parameter. If the parameter is empty, returns {@link - * JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link - * JavaLayerConfigurations#DEFAULT_APP_ROOT} for other projects. - * - * @param rawConfiguration raw configuration data - * @param projectProperties used for providing additional information - * @return the app root value - * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path - */ - static AbsoluteUnixPath getAppRootChecked( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws AppRootInvalidException { - String appRoot = rawConfiguration.getAppRoot(); - if (appRoot.isEmpty()) { - appRoot = - projectProperties.isWarProject() - ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT - : JavaLayerConfigurations.DEFAULT_APP_ROOT; - } - try { - return AbsoluteUnixPath.get(appRoot); - } catch (IllegalArgumentException ex) { - throw new AppRootInvalidException(appRoot, appRoot, ex); - } - } + @FunctionalInterface + private interface InferredAuthProvider { + + Optional getInferredAuth(String registry) throws InferredAuthRetrievalException; + }; /** * Compute the container entrypoint, in this order: @@ -133,13 +118,82 @@ public static String getBaseImage( : "gcr.io/distroless/java"); } - public static PluginConfigurationProcessor processCommonConfiguration( + public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + HelpfulSuggestions helpfulSuggestions) + throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, + InferredAuthRetrievalException, IOException { + ImageReference targetImageReference = + getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); + DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); + Containerizer containerizer = Containerizer.to(targetImage); + + return processCommonConfiguration( + rawConfiguration, projectProperties, containerizer, targetImageReference, false); + } + + public static PluginConfigurationProcessor processCommonConfigurationForTarImage( + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + Path tarImagePath, + HelpfulSuggestions helpfulSuggestions) + throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, + InferredAuthRetrievalException, IOException { + ImageReference targetImageReference = + getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); + TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); + Containerizer containerizer = Containerizer.to(targetImage); + + return processCommonConfiguration( + rawConfiguration, projectProperties, containerizer, targetImageReference, false); + } + + public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws InvalidImageReferenceException, NumberFormatException, FileNotFoundException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + throws InferredAuthRetrievalException, InvalidImageReferenceException, + MainClassInferenceException, AppRootInvalidException, IOException { + Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); + + ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); + RegistryImage targetImage = RegistryImage.named(targetImageReference); + + EventDispatcher eventDispatcher = + new DefaultEventDispatcher(projectProperties.getEventHandlers()); + boolean isTargetImageCredentialPresent = + configureCredentialRetrievers( + eventDispatcher, + targetImage, + targetImageReference, + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, + rawConfiguration.getToAuth(), + "to.auth/", + rawConfiguration::getInferredAuth, + rawConfiguration.getToCredHelper().orElse(null)); + + PluginConfigurationProcessor processor = + processCommonConfiguration( + rawConfiguration, + projectProperties, + Containerizer.to(targetImage), + targetImageReference, + isTargetImageCredentialPresent); + processor.getJibContainerBuilder().setFormat(rawConfiguration.getImageFormat()); + return processor; + } + + @VisibleForTesting + static PluginConfigurationProcessor processCommonConfiguration( + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + Containerizer containerizer, + ImageReference targetImageReference, + boolean isTargetImageCredentialPresent) + throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, + InferredAuthRetrievalException, IOException { JibSystemProperties.checkHttpTimeoutProperty(); - // TODO: Instead of disabling logging, have authentication credentials be provided ImageReference baseImageReference = ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); @@ -151,42 +205,19 @@ public static PluginConfigurationProcessor processCommonConfiguration( "Authentication over HTTP is enabled. It is strongly recommended that you do not " + "enable this on a public network!")); } - DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(baseImageReference, eventDispatcher)); - Optional optionalFromCredential = - ConfigurationPropertyValidator.getImageCredential( + + RegistryImage baseImage = RegistryImage.named(baseImageReference); + boolean isBaseImageCredentialPresent = + configureCredentialRetrievers( eventDispatcher, + baseImage, + baseImageReference, PropertyNames.FROM_AUTH_USERNAME, PropertyNames.FROM_AUTH_PASSWORD, - rawConfiguration.getFromAuth()); - if (optionalFromCredential.isPresent()) { - // TODO: fix https://github.com/GoogleContainerTools/jib/issues/1177 - // rawConfiguration.getFromAuth().getPropertyDescriptor() may cause NPE, so using - // "from.auth/" as a compromise. - defaultCredentialRetrievers.setKnownCredential( - optionalFromCredential.get(), "from.auth/"); - } else { - // TODO: this is here only for getting values from Maven settings.xml. Consider passing a - // Supplier for an inferred credential as an additional argument, rather than - // having RawConfigurations.getInferredAuth(). - // https://github.com/GoogleContainerTools/jib/pull/1163#discussion_r228389684 - Optional optionalInferredAuth = - rawConfiguration.getInferredAuth(baseImageReference.getRegistry()); - if (optionalInferredAuth.isPresent()) { - AuthProperty auth = optionalInferredAuth.get(); - String username = Verify.verifyNotNull(auth.getUsername()); - String password = Verify.verifyNotNull(auth.getPassword()); - Credential credential = Credential.basic(username, password); - defaultCredentialRetrievers.setInferredCredential(credential, auth.getPropertyDescriptor()); - optionalFromCredential = Optional.of(credential); - } - } - defaultCredentialRetrievers.setCredentialHelper( - rawConfiguration.getFromCredHelper().orElse(null)); - - RegistryImage baseImage = RegistryImage.named(baseImageReference); - defaultCredentialRetrievers.asList().forEach(baseImage::addCredentialRetriever); + rawConfiguration.getFromAuth(), + "from.auth/", + rawConfiguration::getInferredAuth, + rawConfiguration.getFromCredHelper().orElse(null)); JibContainerBuilder jibContainerBuilder = Jib.from(baseImage) @@ -204,8 +235,100 @@ public static PluginConfigurationProcessor processCommonConfiguration( jibContainerBuilder.setCreationTime(Instant.now()); } + PluginConfigurationProcessor.configureContainerizer( + containerizer, rawConfiguration, projectProperties); + return new PluginConfigurationProcessor( - jibContainerBuilder, baseImageReference, optionalFromCredential.isPresent()); + jibContainerBuilder, + containerizer, + baseImageReference, + targetImageReference, + isBaseImageCredentialPresent, + isTargetImageCredentialPresent); + } + + /** + * Gets the value of the {@code appRoot} parameter. If the parameter is empty, returns {@link + * JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link + * JavaLayerConfigurations#DEFAULT_APP_ROOT} for other projects. + * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the app root value + * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path + */ + @VisibleForTesting + static AbsoluteUnixPath getAppRootChecked( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws AppRootInvalidException { + String appRoot = rawConfiguration.getAppRoot(); + if (appRoot.isEmpty()) { + appRoot = + projectProperties.isWarProject() + ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT + : JavaLayerConfigurations.DEFAULT_APP_ROOT; + } + try { + return AbsoluteUnixPath.get(appRoot); + } catch (IllegalArgumentException ex) { + throw new AppRootInvalidException(appRoot, appRoot, ex); + } + } + + // TODO: find a way to reduce the number of arguments. + private static boolean configureCredentialRetrievers( + EventDispatcher eventDispatcher, + RegistryImage registryImage, + ImageReference imageReference, + String usernamePropertyName, + String passwordPropertyName, + AuthProperty knownAuth, + String knownAuthSource, + InferredAuthProvider inferredAuthProvider, + @Nullable String credHelper) + throws FileNotFoundException, InferredAuthRetrievalException { + DefaultCredentialRetrievers defaultCredentialRetrievers = + DefaultCredentialRetrievers.init( + CredentialRetrieverFactory.forImage(imageReference, eventDispatcher)); + Optional optionalToCredential = + ConfigurationPropertyValidator.getImageCredential( + eventDispatcher, usernamePropertyName, passwordPropertyName, knownAuth); + boolean credentialPresent = optionalToCredential.isPresent(); + if (optionalToCredential.isPresent()) { + // TODO: fix https://github.com/GoogleContainerTools/jib/issues/1177 + // knownAuth.getPropertyDescriptor() may NPE. Fix it and remove the knownAuthSource parameter. + defaultCredentialRetrievers.setKnownCredential(optionalToCredential.get(), knownAuthSource); + } else { + Optional optionalInferredAuth = + inferredAuthProvider.getInferredAuth(imageReference.getRegistry()); + credentialPresent = optionalInferredAuth.isPresent(); + if (optionalInferredAuth.isPresent()) { + AuthProperty auth = optionalInferredAuth.get(); + String username = Verify.verifyNotNull(auth.getUsername()); + String password = Verify.verifyNotNull(auth.getPassword()); + Credential credential = Credential.basic(username, password); + defaultCredentialRetrievers.setInferredCredential(credential, auth.getPropertyDescriptor()); + } + } + defaultCredentialRetrievers.setCredentialHelper(credHelper); + defaultCredentialRetrievers.asList().forEach(registryImage::addCredentialRetriever); + + return credentialPresent; + } + + private static ImageReference getGeneratedTargetDockerTag( + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + HelpfulSuggestions helpfulSuggestions) + throws InvalidImageReferenceException { + return ConfigurationPropertyValidator.getGeneratedTargetDockerTag( + rawConfiguration.getToImage().orElse(null), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getName(), + projectProperties.getVersion().equals("unspecified") + ? "latest" + : projectProperties.getVersion(), + helpfulSuggestions); } /** @@ -217,13 +340,12 @@ public static PluginConfigurationProcessor processCommonConfiguration( * @param projectProperties the project properties * @param toolName tool name to set */ - public static void configureContainerizer( + private static void configureContainerizer( Containerizer containerizer, RawConfiguration rawConfiguration, - ProjectProperties projectProperties, - String toolName) { + ProjectProperties projectProperties) { containerizer - .setToolName(toolName) + .setToolName(projectProperties.getToolName()) .setEventHandlers(projectProperties.getEventHandlers()) .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) @@ -238,26 +360,47 @@ public static void configureContainerizer( private final JibContainerBuilder jibContainerBuilder; private final ImageReference baseImageReference; + private final ImageReference targetImageReference; private final boolean isBaseImageCredentialPresent; + private final boolean isTargetImageCredentialPresent; + private final Containerizer containerizer; private PluginConfigurationProcessor( JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, ImageReference baseImageReference, - boolean isBaseImageCredentialPresent) { + ImageReference targetImageReference, + boolean isBaseImageCredentialPresent, + boolean isTargetImageCredentialPresent) { this.jibContainerBuilder = jibContainerBuilder; + this.containerizer = containerizer; this.baseImageReference = baseImageReference; + this.targetImageReference = targetImageReference; this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; + this.isTargetImageCredentialPresent = isTargetImageCredentialPresent; } public JibContainerBuilder getJibContainerBuilder() { return jibContainerBuilder; } + public Containerizer getContainerizer() { + return containerizer; + } + public ImageReference getBaseImageReference() { return baseImageReference; } + public ImageReference getTargetImageReference() { + return targetImageReference; + } + public boolean isBaseImageCredentialPresent() { return isBaseImageCredentialPresent; } + + public boolean isTargetImageCredentialPresent() { + return isTargetImageCredentialPresent; + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index c2d26ba5d3..1c18e3ca9e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -35,6 +35,8 @@ public interface ProjectProperties { EventHandlers getEventHandlers(); + String getToolName(); + String getPluginName(); JavaLayerConfigurations getJavaLayerConfigurations(); @@ -48,4 +50,8 @@ public interface ProjectProperties { String getMainClassFromJar(); boolean isWarProject(); + + String getName(); + + String getVersion(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 3ddf837518..316e64ea06 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.image.ImageFormat; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,10 +29,16 @@ public interface RawConfiguration { Optional getFromImage(); + Optional getToImage(); + AuthProperty getFromAuth(); + AuthProperty getToAuth(); + Optional getFromCredHelper(); + Optional getToCredHelper(); + Iterable getToTags(); Optional> getEntrypoint(); @@ -58,5 +65,10 @@ public interface RawConfiguration { boolean getUseOnlyProjectCache(); + // TODO: This is only for getting values from Maven settings.xml, and in some sense, auth info + // from settings.xml is not necessary raw configuration values. Consider removing it. + // https://github.com/GoogleContainerTools/jib/pull/1163#discussion_r228389684 Optional getInferredAuth(String authTarget) throws InferredAuthRetrievalException; + + ImageFormat getImageFormat(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 6d9510cb39..5ad55ff080 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -29,8 +29,9 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.Optional; @@ -56,6 +57,8 @@ private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibC @Mock private RawConfiguration rawConfiguration; @Mock private ProjectProperties projectProperties; + @Mock private Containerizer containerizer; + @Mock private ImageReference targetImageReference; @Mock private AuthProperty authProperty; @Mock private Consumer logger; @@ -65,11 +68,23 @@ public void setUp() { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); + Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getJavaLayerConfigurations()) .thenReturn(JavaLayerConfigurations.builder().build()); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getEventHandlers()) .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); + Mockito.when(projectProperties.getCacheDirectory()).thenReturn(Paths.get("cache")); + + Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); + Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) + .thenReturn(containerizer); + Mockito.when(containerizer.setAllowInsecureRegistries(Mockito.anyBoolean())) + .thenReturn(containerizer); + Mockito.when(containerizer.setBaseImageLayersCache(Mockito.any(Path.class))) + .thenReturn(containerizer); + Mockito.when(containerizer.setApplicationLayersCache(Mockito.any(Path.class))) + .thenReturn(containerizer); } /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ @@ -79,7 +94,7 @@ public void testPluginConfigurationProcessor_defaults() MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -93,13 +108,13 @@ public void testPluginConfigurationProcessor_defaults() @Test public void testPluginConfigurationProcessor_warProjectBaseImage() - throws InvalidImageReferenceException, FileNotFoundException, MainClassInferenceException, - AppRootInvalidException, InferredAuthRetrievalException { + throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, + InferredAuthRetrievalException, IOException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); Assert.assertEquals( ImageReference.parse("gcr.io/distroless/java/jetty").toString(), @@ -116,7 +131,7 @@ public void testEntrypoint() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -136,7 +151,7 @@ public void testEntrypoint_defaultWarPackaging() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); @@ -155,7 +170,7 @@ public void testEntrypoint_defaulNonWarPackaging() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -175,7 +190,7 @@ public void testUser() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -189,7 +204,7 @@ public void testUser_null() MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -207,7 +222,7 @@ public void testEntrypoint_warningOnJvmFlags() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -229,7 +244,7 @@ public void testEntrypoint_warningOnMainclass() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -249,7 +264,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -272,7 +287,7 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties); + rawConfiguration, projectProperties, containerizer, targetImageReference, false); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); From 5a34806a433519c0a3f1541be20cd3c20cd5c917 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 6 Nov 2018 08:37:13 -0800 Subject: [PATCH 0327/2020] =?UTF-8?q?Avoid=20creating=20directory=20in=20e?= =?UTF-8?q?xploded-war=20build=20output=20directory=20in=20Ja=E2=80=A6=20(?= =?UTF-8?q?#1218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JavaLayerConfigurationsHelperTest.java | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java index 54a3652f99..bd5c61aab5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java @@ -17,8 +17,10 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -31,7 +33,9 @@ import java.util.function.Function; import java.util.stream.Collectors; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Tests for {@link JavaLayerConfigurationsHelper}. */ public class JavaLayerConfigurationsHelperTest { @@ -54,45 +58,51 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test public void testFromExplodedWar() throws URISyntaxException, IOException { - Path explodedWar = Paths.get(Resources.getResource("exploded-war").toURI()); - Files.createDirectories(explodedWar.resolve("WEB-INF/classes/empty_dir")); + // Copy test files to a temporary directory that we can safely operate on + Path resourceExplodedWar = Paths.get(Resources.getResource("exploded-war").toURI()); + FileOperations.copy(ImmutableList.of(resourceExplodedWar), temporaryFolder.getRoot().toPath()); + Path temporaryExplodedWar = temporaryFolder.getRoot().toPath().resolve("exploded-war"); + + Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); JavaLayerConfigurations configuration = JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWar, + temporaryExplodedWar, AbsoluteUnixPath.get("/my/app"), extraFilesDirectory, Collections.emptyMap()); assertSourcePathsUnordered( - Collections.singletonList(explodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), + Collections.singletonList(temporaryExplodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), configuration.getDependencyLayerEntries()); assertSourcePathsUnordered( Collections.singletonList( - explodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + temporaryExplodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), configuration.getSnapshotDependencyLayerEntries()); assertSourcePathsUnordered( Arrays.asList( - explodedWar.resolve("META-INF"), - explodedWar.resolve("META-INF/context.xml"), - explodedWar.resolve("Test.jsp"), - explodedWar.resolve("WEB-INF"), - explodedWar.resolve("WEB-INF/classes"), - explodedWar.resolve("WEB-INF/classes/empty_dir"), - explodedWar.resolve("WEB-INF/classes/package"), - explodedWar.resolve("WEB-INF/classes/package/test.properties"), - explodedWar.resolve("WEB-INF/lib"), - explodedWar.resolve("WEB-INF/web.xml")), + temporaryExplodedWar.resolve("META-INF"), + temporaryExplodedWar.resolve("META-INF/context.xml"), + temporaryExplodedWar.resolve("Test.jsp"), + temporaryExplodedWar.resolve("WEB-INF"), + temporaryExplodedWar.resolve("WEB-INF/classes"), + temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir"), + temporaryExplodedWar.resolve("WEB-INF/classes/package"), + temporaryExplodedWar.resolve("WEB-INF/classes/package/test.properties"), + temporaryExplodedWar.resolve("WEB-INF/lib"), + temporaryExplodedWar.resolve("WEB-INF/web.xml")), configuration.getResourceLayerEntries()); assertSourcePathsUnordered( Arrays.asList( - explodedWar.resolve("WEB-INF/classes/HelloWorld.class"), - explodedWar.resolve("WEB-INF/classes/empty_dir"), - explodedWar.resolve("WEB-INF/classes/package"), - explodedWar.resolve("WEB-INF/classes/package/Other.class")), + temporaryExplodedWar.resolve("WEB-INF/classes/HelloWorld.class"), + temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir"), + temporaryExplodedWar.resolve("WEB-INF/classes/package"), + temporaryExplodedWar.resolve("WEB-INF/classes/package/Other.class")), configuration.getClassLayerEntries()); assertSourcePathsUnordered( Arrays.asList( From 5a3115b7668397f76d4ca74d1c1fdfece795b328 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 7 Nov 2018 13:44:30 -0800 Subject: [PATCH 0328/2020] Disables Apache HTTP logging at registry client level. (#1221) --- .../java/com/google/cloud/tools/jib/http/Connection.java | 7 +++++++ .../com/google/cloud/tools/jib/gradle/TaskCommon.java | 1 - .../com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 1 - .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 1 - .../com/google/cloud/tools/jib/maven/BuildTarMojo.java | 1 - .../google/cloud/tools/jib/maven/DockerContextMojo.java | 1 - .../java/com/google/cloud/tools/jib/maven/MojoCommon.java | 8 -------- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 5602b53622..70802bae60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -78,6 +78,13 @@ public static Function getInsecureConnectionFactory() return url -> new Connection(url, transport); } + static { + // Disables annoying Apache HTTP client logging. + System.setProperty( + "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); + } + private HttpRequestFactory requestFactory; @Nullable private HttpResponse httpResponse; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 98f7347993..44b7fa996e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -57,7 +57,6 @@ static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension, Project pro } /** Disables annoying Apache HTTP client logging. */ - // TODO: Instead of disabling logging, have authentication credentials be provided static void disableHttpLogging() { // Disables Apache HTTP client logging. OutputEventListenerBackedLoggerContext context = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index d2c642f8ce..fb4e1a5e30 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -67,7 +67,6 @@ public void execute() throws MojoExecutionException { HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - MojoCommon.disableHttpLogging(); try { AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); MavenProjectProperties projectProperties = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 5f72719c49..c3154a14a0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -85,7 +85,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { "mvn compile jib:build -Dimage=")); } - MojoCommon.disableHttpLogging(); try { AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a67cd25882..d735f9e943 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -60,7 +60,6 @@ public void execute() throws MojoExecutionException { return; } - MojoCommon.disableHttpLogging(); try { AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); MavenProjectProperties projectProperties = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index 75ff22d920..6d9c783726 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -69,7 +69,6 @@ public void execute() throws MojoExecutionException { try { JibSystemProperties.checkHttpTimeoutProperty(); - MojoCommon.disableHttpLogging(); AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); MavenProjectProperties projectProperties = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 574a8ba87c..7b15a45a59 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -100,13 +100,5 @@ static Map convertPermissionsList( return permissionsMap; } - /** Disables annoying Apache HTTP client logging. */ - // TODO: Instead of disabling logging, have authentication credentials be provided - static void disableHttpLogging() { - System.setProperty( - "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); - } - private MojoCommon() {} } From b4cce76c0cbf93aea80c2752fe3224f016c75593 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 7 Nov 2018 16:53:06 -0500 Subject: [PATCH 0329/2020] Allow configuration docker executable path and additional environment variables (#1214) --- .../builder/BuildStepsIntegrationTest.java | 4 +- .../tools/jib/api/DockerDaemonImage.java | 38 ++++--- .../cloud/tools/jib/docker/DockerClient.java | 99 ++++++++++++++++--- .../tools/jib/api/DockerDaemonImageTest.java | 6 +- .../tools/jib/docker/DockerClientTest.java | 37 +++++-- .../tools/jib/gradle/BuildDockerTask.java | 29 +++++- .../jib/gradle/DockerClientParameters.java | 56 +++++++++++ .../tools/jib/maven/BuildDockerMojo.java | 10 +- .../common/PluginConfigurationProcessor.java | 9 ++ 9 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index aebab6db26..81c98eaa17 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -216,7 +216,7 @@ public void testSteps_forBuildToDockerDaemon() ImageReference.of("gcr.io", "distroless/java", "latest"), ImageReference.of(null, imageReference, null)) .build(); - BuildSteps.forBuildToDockerDaemon(DockerClient.newClient(), buildConfiguration).run(); + BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); assertDockerInspect(imageReference); Assert.assertEquals( @@ -233,7 +233,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() ImageReference.of(null, imageReference, null)) .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) .build(); - BuildSteps.forBuildToDockerDaemon(DockerClient.newClient(), buildConfiguration).run(); + BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); assertDockerInspect(imageReference); Assert.assertEquals( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index 9cf6c3e542..56d62bfe61 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -21,10 +21,13 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.docker.DockerClient.Builder; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableMap; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.Map; +import javax.annotation.Nullable; /** Builds to the Docker daemon. */ // TODO: Add tests once JibContainerBuilder#containerize() is added. @@ -55,7 +58,8 @@ public static DockerDaemonImage named(String imageReference) } private final ImageReference imageReference; - private Path dockerExecutable = Paths.get("docker"); + @Nullable private Path dockerExecutable; + @Nullable private Map dockerEnvironment; /** Instantiate with {@link #named}. */ private DockerDaemonImage(ImageReference imageReference) { @@ -73,6 +77,17 @@ public DockerDaemonImage setDockerExecutable(Path dockerExecutable) { return this; } + /** + * Sets the additional environment variables to use when running {@link #dockerExecutable docker}. + * + * @param dockerEnvironment additional environment variables + * @return this + */ + public DockerDaemonImage setDockerEnvironment(Map dockerEnvironment) { + this.dockerEnvironment = dockerEnvironment; + return this; + } + @Override public ImageConfiguration toImageConfiguration() { return ImageConfiguration.builder(imageReference).build(); @@ -80,16 +95,13 @@ public ImageConfiguration toImageConfiguration() { @Override public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { - return BuildSteps.forBuildToDockerDaemon( - DockerClient.newClient(dockerExecutable), buildConfiguration); - } - - /** - * Gets the path to the {@code docker} CLI. - * - * @return the path to the {@code docker} CLI - */ - Path getDockerExecutable() { - return dockerExecutable; + Builder dockerClientBuilder = DockerClient.builder(); + if (dockerExecutable != null) { + dockerClientBuilder.setDockerExecutable(dockerExecutable); + } + if (dockerEnvironment != null) { + dockerClientBuilder.setDockerEnvironment(ImmutableMap.copyOf(dockerEnvironment)); + } + return BuildSteps.forBuildToDockerDaemon(dockerClientBuilder.build(), buildConfiguration); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 9aea451cb2..693b6dd0ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -19,39 +19,74 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Function; /** Calls out to the {@code docker} CLI. */ public class DockerClient { - private static final String DEFAULT_DOCKER_CLIENT = "docker"; + /** Builds a {@link DockerClient}. */ + public static class Builder { + + private Path dockerExecutable = DEFAULT_DOCKER_CLIENT; + private ImmutableMap dockerEnvironment = ImmutableMap.of(); + + private Builder() {} + + /** + * Sets a path for a {@code docker} executable. + * + * @param dockerExecutable path to {@code docker} + * @return this + */ + public Builder setDockerExecutable(Path dockerExecutable) { + this.dockerExecutable = dockerExecutable; + return this; + } + + /** + * Sets environment variables to use when executing the {@code docker} executable. + * + * @param dockerEnvironment environment variables for {@code docker} + * @return this + */ + public Builder setDockerEnvironment(ImmutableMap dockerEnvironment) { + this.dockerEnvironment = dockerEnvironment; + return this; + } + + public DockerClient build() { + return new DockerClient(dockerExecutable, dockerEnvironment); + } + } /** - * Instantiates with the default {@code docker} executable. + * Gets a new {@link Builder} for {@link DockerClient} with defaults. * - * @return a new {@link DockerClient} + * @return a new {@link Builder} */ - public static DockerClient newClient() { - return new DockerClient(defaultProcessBuilderFactory(DEFAULT_DOCKER_CLIENT)); + public static Builder builder() { + return new Builder(); } /** - * Instantiates with a custom {@code docker} executable. + * Instantiates with the default {@code docker} executable. * - * @param dockerExecutable path to {@code docker} * @return a new {@link DockerClient} */ - public static DockerClient newClient(Path dockerExecutable) { - return new DockerClient(defaultProcessBuilderFactory(dockerExecutable.toString())); + public static DockerClient newDefaultClient() { + return builder().build(); } /** @@ -61,16 +96,24 @@ public static DockerClient newClient(Path dockerExecutable) { * @param dockerExecutable path to {@code docker} * @return the default {@link ProcessBuilder} factory for running a {@code docker} subcommand */ - private static Function, ProcessBuilder> defaultProcessBuilderFactory( - String dockerExecutable) { + @VisibleForTesting + static Function, ProcessBuilder> defaultProcessBuilderFactory( + String dockerExecutable, ImmutableMap dockerEnvironment) { return dockerSubCommand -> { List dockerCommand = new ArrayList<>(1 + dockerSubCommand.size()); dockerCommand.add(dockerExecutable); dockerCommand.addAll(dockerSubCommand); - return new ProcessBuilder(dockerCommand); + + ProcessBuilder processBuilder = new ProcessBuilder(dockerCommand); + Map environment = processBuilder.environment(); + environment.putAll(dockerEnvironment); + + return processBuilder; }; } + private static final Path DEFAULT_DOCKER_CLIENT = Paths.get("docker"); + /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ private final Function, ProcessBuilder> processBuilderFactory; @@ -80,12 +123,36 @@ private static Function, ProcessBuilder> defaultProcessBuilderFacto } /** - * @return {@code true} if Docker is installed on the user's system and accessible as {@code - * docker} + * Instantiates with a {@code docker} executable and environment variables. + * + * @param dockerExecutable path to {@code docker} + * @param dockerEnvironment environment variables for {@code docker} + * @return a new {@link DockerClient} + */ + private DockerClient(Path dockerExecutable, ImmutableMap dockerEnvironment) { + this(defaultProcessBuilderFactory(dockerExecutable.toString(), dockerEnvironment)); + } + + /** + * Checks if Docker is installed on the user's system and accessible by running the default {@code + * docker} command. + * + * @return {@code true} if Docker is installed on the user's system and accessible + */ + public static boolean isDefaultDockerInstalled() { + return isDockerInstalled(DEFAULT_DOCKER_CLIENT); + } + + /** + * Checks if Docker is installed on the user's system and accessible by running the given {@code + * docker} executable. + * + * @param dockerExecutable path to the executable to test running + * @return {@code true} if Docker is installed on the user's system and accessible */ - public boolean isDockerInstalled() { + public static boolean isDockerInstalled(Path dockerExecutable) { try { - docker(); + new ProcessBuilder(dockerExecutable.toString()).start(); return true; } catch (IOException ex) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java index cdf704eef7..9d8fb27723 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; @@ -28,15 +27,12 @@ public class DockerDaemonImageTest { @Test public void testToImageConfiguration() throws InvalidImageReferenceException { - DockerDaemonImage dockerDaemonImage = - DockerDaemonImage.named("docker/daemon/image") - .setDockerExecutable(Paths.get("docker", "executable")); + DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named("docker/daemon/image"); ImageConfiguration imageConfiguration = dockerDaemonImage.toImageConfiguration(); Assert.assertEquals( ImageReference.parse("docker/daemon/image").toString(), imageConfiguration.getImage().toString()); Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); - Assert.assertEquals(Paths.get("docker", "executable"), dockerDaemonImage.getDockerExecutable()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 6e71e0d7b0..b1ccf8c0f7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -29,6 +30,8 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -53,16 +56,9 @@ public void setUp() throws IOException { Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); } - @Test - public void testIsDockerInstalled_pass() { - Assert.assertTrue(new DockerClient(ignored -> mockProcessBuilder).isDockerInstalled()); - } - @Test public void testIsDockerInstalled_fail() { - ProcessBuilder nonexistentProcessBuilder = - new ProcessBuilder(Paths.get("path/to/nonexistent/file").toString()); - Assert.assertFalse(new DockerClient(ignored -> nonexistentProcessBuilder).isDockerInstalled()); + Assert.assertFalse(DockerClient.isDockerInstalled(Paths.get("path/to/nonexistent/file"))); } @Test @@ -181,6 +177,31 @@ public void testTag() throws InterruptedException, IOException, InvalidImageRefe testDockerClient.tag(ImageReference.of(null, "original", null), ImageReference.parse("new")); } + @Test + public void testDefaultProcessorBuilderFactory_customExecutable() { + ProcessBuilder processBuilder = + DockerClient.defaultProcessBuilderFactory("docker-executable", ImmutableMap.of()) + .apply(Arrays.asList("sub", "command")); + + Assert.assertEquals( + Arrays.asList("docker-executable", "sub", "command"), processBuilder.command()); + Assert.assertEquals(System.getenv(), processBuilder.environment()); + } + + @Test + public void testDefaultProcessorBuilderFactory_customEnvironment() { + ImmutableMap environment = ImmutableMap.of("Key1", "Value1"); + + Map expectedEnvironment = new HashMap<>(System.getenv()); + expectedEnvironment.putAll(environment); + + ProcessBuilder processBuilder = + DockerClient.defaultProcessBuilderFactory("docker", environment) + .apply(Collections.emptyList()); + + Assert.assertEquals(expectedEnvironment, processBuilder.environment()); + } + @Test public void testTag_fail() throws InterruptedException, InvalidImageReferenceException { DockerClient testDockerClient = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index fe8cfbb6a4..a7f7cd806f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -34,9 +34,11 @@ import java.io.IOException; import java.nio.file.Path; import javax.annotation.Nullable; +import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; @@ -45,10 +47,10 @@ public class BuildDockerTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; - private static final DockerClient DOCKER_CLIENT = DockerClient.newClient(); - @Nullable private JibExtension jibExtension; + private final DockerClientParameters dockerClientParameters = new DockerClientParameters(); + /** * This will call the property {@code "jib"} so that it is the same name as the extension. This * way, the user would see error messages for missing configuration with the prefix {@code jib.}. @@ -71,12 +73,27 @@ public void setTargetImage(String targetImage) { Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); } + @Nested + @Optional + public DockerClientParameters getDockerClient() { + return dockerClientParameters; + } + + public void dockerClient(Action action) { + action.execute(dockerClientParameters); + } + @TaskAction public void buildDocker() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, CacheDirectoryCreationException, MainClassInferenceException, InferredAuthRetrievalException { - if (!DOCKER_CLIENT.isDockerInstalled()) { + Path dockerExecutable = dockerClientParameters.getExecutable(); + boolean isDockerInstalled = + dockerExecutable == null + ? DockerClient.isDefaultDockerInstalled() + : DockerClient.isDockerInstalled(dockerExecutable); + if (!isDockerInstalled) { throw new GradleException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } @@ -101,7 +118,11 @@ public void buildDocker() PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - rawConfiguration, projectProperties, gradleHelpfulSuggestionsBuilder.build()); + rawConfiguration, + projectProperties, + dockerClientParameters.getExecutable(), + dockerClientParameters.getEnvironment(), + gradleHelpfulSuggestionsBuilder.build()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java new file mode 100644 index 0000000000..4f1409e34e --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import javax.annotation.Nullable; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +/** + * Object in {@link BuildDockerTask} that configures the Docker executable and the additional + * environment variables to use when executing the executable. + */ +public class DockerClientParameters { + + @Nullable private Path executable; + @Nullable private Map environment; + + @Input + @Nullable + @Optional + public Path getExecutable() { + return executable; + } + + public void setExecutable(String executable) { + this.executable = Paths.get(executable); + } + + @Input + @Nullable + @Optional + public Map getEnvironment() { + return environment; + } + + public void setEnvironment(@Nullable Map environment) { + this.environment = environment; + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index fb4e1a5e30..e21e77e5fb 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -49,8 +49,6 @@ public class BuildDockerMojo extends JibPluginConfiguration { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; - private static final DockerClient DOCKER_CLIENT = DockerClient.newClient(); - @Override public void execute() throws MojoExecutionException { if (isSkipped()) { @@ -62,7 +60,7 @@ public void execute() throws MojoExecutionException { return; } - if (!DOCKER_CLIENT.isDockerInstalled()) { + if (!DockerClient.isDefaultDockerInstalled()) { throw new MojoExecutionException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } @@ -85,7 +83,11 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - rawConfiguration, projectProperties, mavenHelpfulSuggestionsBuilder.build()); + rawConfiguration, + projectProperties, + null, + null, + mavenHelpfulSuggestionsBuilder.build()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 300043926d..9294eb701f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -42,6 +42,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; @@ -121,12 +122,20 @@ public static String getBaseImage( public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, ProjectProperties projectProperties, + @Nullable Path dockerExecutable, + @Nullable Map dockerEnvironment, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException, IOException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); + if (dockerExecutable != null) { + targetImage.setDockerExecutable(dockerExecutable); + } + if (dockerEnvironment != null) { + targetImage.setDockerEnvironment(dockerEnvironment); + } Containerizer containerizer = Containerizer.to(targetImage); return processCommonConfiguration( From f585ae7624e234160750f423b2df832cec56582a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 7 Nov 2018 14:27:38 -0800 Subject: [PATCH 0330/2020] Fixes overriding existing env var/label from base image. (#1222) --- .../google/cloud/tools/jib/image/Image.java | 12 ++++++++---- .../jib/builder/steps/BuildImageStepTest.java | 18 +++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index e36f962db0..ed5806575a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -33,8 +34,11 @@ public static class Builder { private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); - private final ImmutableMap.Builder environmentBuilder = ImmutableMap.builder(); - private final ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); + + // Don't use ImmutableMap.Builder because it does not allow for replacing existing keys with new + // values. + private final Map environmentBuilder = new HashMap<>(); + private final Map labelsBuilder = new HashMap<>(); @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @@ -188,11 +192,11 @@ public Image build() { created, imageLayersBuilder.build(), historyBuilder.build(), - environmentBuilder.build(), + ImmutableMap.copyOf(environmentBuilder), entrypoint, programArguments, exposedPorts, - labelsBuilder.build(), + ImmutableMap.copyOf(labelsBuilder), workingDirectory, user); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index d1795fd99c..96e1231021 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -123,8 +123,9 @@ public BlobDescriptor getBlobDescriptor() { Image baseImage = Image.builder() - .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE")) + .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "BASE_ENV_2", "DEFAULT")) .addLabel("base.label", "base.label.value") + .addLabel("base.label.2", "default") .setWorkingDirectory("/base/working/directory") .setEntrypoint(ImmutableList.of("baseImageEntrypoint")) .setProgramArguments(ImmutableList.of("catalina.sh", "run")) @@ -186,9 +187,9 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup public void test_propagateBaseImageConfiguration() throws ExecutionException, InterruptedException { Mockito.when(mockContainerConfiguration.getEnvironmentMap()) - .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE")); + .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE")); Mockito.when(mockContainerConfiguration.getLabels()) - .thenReturn(ImmutableMap.of("my.label", "my.label.value")); + .thenReturn(ImmutableMap.of("my.label", "my.label.value", "base.label.2", "new.value")); BuildImageStep buildImageStep = new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), @@ -201,10 +202,17 @@ public void test_propagateBaseImageConfiguration() mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( - ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE"), + ImmutableMap.of( + "BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE"), image.getEnvironment()); Assert.assertEquals( - ImmutableMap.of("base.label", "base.label.value", "my.label", "my.label.value"), + ImmutableMap.of( + "base.label", + "base.label.value", + "my.label", + "my.label.value", + "base.label.2", + "new.value"), image.getLabels()); Assert.assertEquals("/base/working/directory", image.getWorkingDirectory()); Assert.assertEquals("root", image.getUser()); From 225cd7d9c19fae9f0900b7ab843ccb778edbe51a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 8 Nov 2018 10:44:32 -0500 Subject: [PATCH 0331/2020] CHANGELOG for jibDockerBuild.dockerClient params (#1223) --- jib-gradle-plugin/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e3c2461c1b..1334721f75 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string) - Image digest is now written to `build/jib-image.digest` ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) - Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) +- `jibDockerBuild.dockerClient.executable` and `jibDockerBuild.dockerClient.environment` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary ([#1214](https://github.com/GoogleContainerTools/jib/pull/1214)) ### Changed From c69339ea2e2568205c12513a3b7de322afcbf70e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 8 Nov 2018 11:00:08 -0500 Subject: [PATCH 0332/2020] Gradle release v0.10.0 (#1228) * [Gradle Release Plugin] - pre tag commit: 'v0.10.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.10.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 92180d0637..0340ae781e 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.9.14-SNAPSHOT +version = 0.10.1-SNAPSHOT From 40718f4447339c6286913f4d149c561d2de8e0ed Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 8 Nov 2018 11:23:52 -0500 Subject: [PATCH 0333/2020] Maven release v0.10.0 (#1230) * preparing release 0.10.0 * 0.10.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 02244a2777..56875d0b01 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.14-SNAPSHOT + 0.10.1-SNAPSHOT maven-plugin Jib From c9311830d8469b3fda7f52e6d8f6f271324694bf Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 8 Nov 2018 13:42:27 -0500 Subject: [PATCH 0334/2020] Fix wrong Maven build directory (#1232) --- jib-maven-plugin/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 35891a5133..69b4f013b2 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. - `` object with a `` and `` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `` configures the extra layer directory (still also configurable via `...`) - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) -- Image digest is now written to `build/jib-image.digest` ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) +- Image digest is now written to `target/jib-image.digest` ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) - Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) ### Changed From d22496f261cd33bcad74edaa542dac391c813f01 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 8 Nov 2018 14:03:07 -0500 Subject: [PATCH 0335/2020] Update version strings for 0.10.0 (#1233) --- CONTRIBUTING.md | 4 ++-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 16 files changed, 33 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a79ae0b7d..66de355b56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.9.12-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.10.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.9.11' + // id 'com.google.cloud.tools.jib' version '0.10.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index cf56e74407..cfc48680e8 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.12' + id 'com.google.cloud.tools.jib' version '0.10.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index b6807d99cc..22e8e3c2da 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.9.11 + 0.10.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index a8d3cf5906..5d461f7701 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.9.12' + id 'com.google.cloud.tools.jib' version '0.10.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 0f9d9feb86..f67e324ac3 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.9.11 + 0.10.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 7b7d96a535..96bf7de1b4 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.9.12' + id 'com.google.cloud.tools.jib' version '0.10.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index edc342f6ff..2b5d112295 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.12' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.10.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index cd426ab2ca..5fca3e82dd 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -39,7 +39,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.11 + 0.10.0 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index a35cc171a1..4bf75fbb6f 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.9.12' + id 'com.google.cloud.tools.jib' version '0.10.0' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 2102950542..b263ee1630 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.9.11 + 0.10.0 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index eabe0072c0..67deb04159 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '0.9.12' + id 'com.google.cloud.tools.jib' version '0.10.0' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index e31680e6bf..72982ac188 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 0.9.11 + 0.10.0 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1334721f75..d49787c994 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.0 + +### Added + - Properties for each configuration parameter, allowing any parameter to be set via commandline ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) - `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) @@ -23,8 +31,6 @@ All notable changes to this project will be documented in this file. - File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) - Sets tag to "latest" instead of "unspecified" if `jib.to.image` and project version are both unspecified when running `jibDockerBuild` or `jibBuildTar` ([#1096](https://github.com/GoogleContainerTools/jib/issues/1096)) -### Fixed - ## 0.9.13 ### Fixed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index b99706709c..ae558689f8 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.9.11' + id 'com.google.cloud.tools.jib' version '0.10.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 69b4f013b2..6e1e86bb07 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.0 + +### Added + - Properties for each configuration parameter, allowing any parameter to be set via commandline ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) - `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) - `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) @@ -21,8 +29,6 @@ All notable changes to this project will be documented in this file. - `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) - File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) -### Fixed - ## 0.9.13 ### Fixed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 11c161ef2b..ee05281997 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.11:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.11:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.9.11 + 0.10.0 myimage From 2ff2d6c8272f2aa32f8b022936416e615c81ef07 Mon Sep 17 00:00:00 2001 From: sullis Date: Thu, 8 Nov 2018 11:50:30 -0800 Subject: [PATCH 0336/2020] maven-surefire-plugin and maven-failsafe-plugin to 2.22.1 (#1174) --- jib-maven-plugin/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 56875d0b01..0bba2168d5 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -147,7 +147,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 2.21.0 + 2.22.1 ${project.build.directory}/classes @@ -358,7 +358,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20.1 + 2.22.1 **/*IntegrationTest.java From c8f0570e2f1168fc7612a723937b82e0e4340512 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 9 Nov 2018 10:11:55 -0500 Subject: [PATCH 0337/2020] Update README for 0.10.0 (#1147) --- jib-gradle-plugin/README.md | 77 ++++++++++++++++++++++++++++---- jib-maven-plugin/README.md | 88 +++++++++++++++++++++++++++++++++---- 2 files changed, 149 insertions(+), 16 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index ae558689f8..507976a034 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -152,6 +152,11 @@ You can then build your image with Docker: docker build -t myimage my/docker/context/ ``` +### Additional Build Artifacts + +As part of an image build, Jib also writes out the _image digest_ to +`build/jib-image.digest`. + ## Extended Usage The plugin provides the `jib` extension for configuration with the following options for customizing the image build: @@ -161,6 +166,7 @@ Field | Type | Default | Description `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. +`extraDirectory` | [`extraDirectory`](#extradirectory-closure) / `File` | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. @@ -170,7 +176,7 @@ Property | Type | Default | Description --- | --- | --- | --- `image` | `String` | `gcr.io/distroless/java` | The image reference for the base image. `auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`credHelper` | `String` | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). `to` is a closure with the following properties: @@ -178,7 +184,7 @@ Property | Type | Default | Description --- | --- | --- | --- `image` | `String` | *Required* | The image reference for the target image. This can also be specified via the `--image` command line option. `auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). -`credHelper` | `String` | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`credHelper` | `String` | *None* | Specifies a credential helper that can authenticate pushing the target image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). `tags` | `List` | *None* | Additional tags to push to. `auth` is a closure with the following properties (see [Using Specific Credentials](#using-specific-credentials)): @@ -192,8 +198,8 @@ Property | Type Property | Type | Default | Description --- | --- | --- | --- -`appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. -`args` | `List` | *None* | Default main method arguments to run your application with. +`appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. +`args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). @@ -202,6 +208,30 @@ Property | Type | Default | Description `mainClass` | `String` | *Inferred\** | The main class to launch your application from. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. +`user` | `String` | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. + +`extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): + +Property | Type +--- | --- +`path` | `File` +`permissions` | `Map` + +#### System Properties + +Each of these parameters is configurable via commandline using system properties. Jib's system properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: +```shell +gradle jib \ + -Djib.to.image=myregistry/myimage:latest \ + -Djib.to.auth.username=$USERNAME \ + -Djib.to.auth.password=$PASSWORD + +gradle jibDockerBuild \ + -Djib.container.environment=key1="value1",key2="value2" \ + -Djib.container.args=arg1,arg2,arg3 + +gradle jibBuildTar -Djib.useProjectOnlyCache=true +``` You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -248,14 +278,25 @@ jib { You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). -You can configure a different directory by using the `extraDirectory` parameter in your `build.gradle`: - +You can configure a different directory by using the `jib.extraDirectory` parameter in your `build.gradle`: ```groovy jib { - ... // Copies files from 'src/main/custom-extra-dir' instead of 'src/main/jib' extraDirectory = file('src/main/custom-extra-dir') - ... +} +``` + +Alternatively, the `jib.extraDirectory` parameter can be used as a closure to set a custom extra directory, as well as the extra files' permissions on the container: + +```groovy +jib { + extraDirectory { + path = file('src/main/custom-extra-dir') // Copies files from 'src/main/custom-extra-dir' + permissions = [ + '/path/on/container/to/fileA': '755', // Read/write/execute for owner, read/execute for group/other + '/path/to/another/file': '644' // Read/write for owner, read-only for group/other + ] + } } ``` @@ -330,6 +371,26 @@ jib { } ``` +### WAR Projects + +Jib also containerizes WAR projects. If the Gradle project uses the [WAR Plugin](https://docs.gradle.org/current/userguide/war_plugin.html), Jib will by default use the [distroless Jetty](https://github.com/GoogleContainerTools/distroless/tree/master/java/jetty) as a base image to deploy the project WAR. No extra configuration is necessary other than using the WAR Plugin to make Jib build WAR images. + +Note that Jib will work slightly differently for WAR projects from JAR projects: + - `container.mainClass` and `container.jvmFlags` are ignored. + - The WAR will be exploded into `/jetty/webapps/ROOT`, which is the expected WAR location for the distroless Jetty base image. + +To use a different Servlet engine base image, you can customize `container.appRoot`, `container.entrypoint`, and `container.args`. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may need to configure them. However, you will most likely have to set `container.appRoot` to a proper location depending on the base image. Here is an example of using a Tomcat image: + +```gradle +jib { + from.image = 'tomcat:8.5-jre8-alpine' + + // For demonstration only: this directory in the base image contains a Tomcat default + // app (welcome page), so you may first want to delete this directory in the base image. + container.appRoot = '/usr/local/tomcat/webapps/ROOT' +} +``` + ## How Jib Works See the [Jib project README](/../../#how-jib-works). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index ee05281997..528a18798c 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -193,6 +193,11 @@ You can then build your image with Docker: docker build -t myimage my/docker/context/ ``` +### Additional Build Artifacts + +As part of an image build, Jib also writes out the _image digest_ to +`target/jib-image.digest`. + ## Extended Usage Extended configuration options provide additional options for customizing the image build. @@ -202,6 +207,7 @@ Field | Type | Default | Description `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. +`extraDirectory` | [`extraDirectory`](#extradirectory-object) / string | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. `useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. @@ -212,7 +218,7 @@ Property | Type | Default | Description --- | --- | --- | --- `image` | string | `gcr.io/distroless/java` | The image reference for the base image. `auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`credHelper` | string | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). `to` is an object with the following properties: @@ -220,7 +226,7 @@ Property | Type | Default | Description --- | --- | --- | --- `image` | string | *Required* | The image reference for the target image. This can also be specified via the `-Dimage` command line option. `auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). -`credHelper` | string | *None* | Suffix for the credential helper that can authenticate pulling the base image (following `docker-credential-`). +`credHelper` | string | *None* | Specifies a credential helper that can authenticate pushing the target image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). `tags` | list | *None* | Additional tags to push to. `auth` is an object with the following properties (see [Using Specific Credentials](#using-specific-credentials)): @@ -234,8 +240,8 @@ Property | Type Property | Type | Default | Description --- | --- | --- | --- -`appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. -`args` | list | *None* | Default main method arguments to run your application with. +`appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. +`args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). @@ -244,6 +250,30 @@ Property | Type | Default | Description `mainClass` | string | *Inferred\** | The main class to launch the application from. `ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. +`user` | string | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. + +`extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): + +Property | Type +--- | --- +`path` | string +`permissions` | list + +#### System Properties + +Each of these parameters is configurable via commandline using system properties. Jib's system properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: +```shell +mvn compile jib:build \ + -Djib.to.image=myregistry/myimage:latest \ + -Djib.to.auth.username=$USERNAME \ + -Djib.to.auth.password=$PASSWORD + +mvn compile jib:dockerBuild \ + -Djib.container.environment=key1="value1",key2="value2" \ + -Djib.container.args=arg1,arg2,arg3 + +mvn jib:buildTar -Djib.useProjectOnlyCache=true +``` You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): @@ -306,14 +336,31 @@ In this configuration, the image: You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). -You can configure a different directory by using the `extraDirectory` parameter in your `pom.xml`: - +You can configure a different directory by using the `` parameter in your `pom.xml`: ```xml - ... ${project.basedir}/src/main/custom-extra-dir - ... + +``` + +Alternatively, the `` parameter can be used as an object to set a custom extra directory, as well as the extra files' permissions on the container: + +```xml + + + ${project.basedir}/src/main/custom-extra-dir + + + /path/on/container/to/fileA + 755 + + + /path/to/another/file + 644 + + + ``` @@ -413,6 +460,31 @@ If you're considering putting credentials in Maven, we highly *recommend* using * The `id` field should be the registry server these credentials are for. * We *do not* recommend putting your raw password in `settings.xml`. +### WAR Projects + +Jib also containerizes WAR projects. If the Maven project uses [the `war`-packaging type](https://maven.apache.org/plugins/maven-war-plugin/index.html), Jib will by default use the [distroless Jetty](https://github.com/GoogleContainerTools/distroless/tree/master/java/jetty) as a base image to deploy the project WAR. No extra configuration is necessary other than having the packaging type to `war`. + +Note that Jib will work slightly differently for WAR projects from JAR projects: + - `` and `` are ignored. + - The WAR will be exploded into `/jetty/webapps/ROOT`, which is the expected WAR location for the distroless Jetty base image. + +To use a different Servlet engine base image, you can customize ``, ``, and ``. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may need to configure them. However, you will most likely have to set `` to a proper location depending on the base image. Here is an example of using a Tomcat image: + +```xml + + + tomcat:8.5-jre8-alpine + + + + /usr/local/tomcat/webapps/ROOT + + +``` + ## How Jib Works See the [Jib project README](/../../#how-jib-works). From 0b785cd1b4ba713f3656011d837ae561aa6989bb Mon Sep 17 00:00:00 2001 From: Guillaume Laforge Date: Fri, 9 Nov 2018 16:49:35 +0100 Subject: [PATCH 0338/2020] Update dependencies to use the 1.0.0 GA release of Micronaut (#1235) --- examples/micronaut/build.gradle | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 96bf7de1b4..8bc3b1ce43 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -10,32 +10,31 @@ version '0.1' group 'example.micronaut-jib' repositories { - mavenLocal() mavenCentral() maven { url 'https://jcenter.bintray.com' } } dependencyManagement { imports { - mavenBom 'io.micronaut:bom:1.0.0.M4' + mavenBom 'io.micronaut:micronaut-bom:1.0.0' } } dependencies { - annotationProcessor 'io.micronaut:inject-java' - compile 'io.micronaut:http-client' - compile 'io.micronaut:http-server-netty' - compile 'io.micronaut:runtime-groovy' - compile 'io.micronaut:inject' - compile 'io.micronaut:runtime' - compileOnly 'io.micronaut:inject-groovy' - compileOnly 'io.micronaut:inject-java' + annotationProcessor 'io.micronaut:micronaut-inject-java' + compile 'io.micronaut:micronaut-http-client' + compile 'io.micronaut:micronaut-http-server-netty' + compile 'io.micronaut:micronaut-runtime-groovy' + compile 'io.micronaut:micronaut-inject' + compile 'io.micronaut:micronaut-runtime' + compileOnly 'io.micronaut:micronaut-inject-groovy' + compileOnly 'io.micronaut:micronaut-inject-java' runtime 'ch.qos.logback:logback-classic:1.2.3' - testCompile 'io.micronaut:inject-groovy' + testCompile 'io.micronaut:micronaut-inject-groovy' testCompile('org.spockframework:spock-core:1.1-groovy-2.4') { exclude group: 'org.codehaus.groovy', module: 'groovy-all' } - testCompile 'io.micronaut:inject-java' + testCompile 'io.micronaut:micronaut-inject-java' } compileJava.options.compilerArgs += '-parameters' From 67791ff856683ff3c369c8c49f0f6763ea891d03 Mon Sep 17 00:00:00 2001 From: Gustavo Silva Paiva Date: Fri, 9 Nov 2018 16:37:57 -0200 Subject: [PATCH 0339/2020] Add volume support to jib-core. (#1215) --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JibContainerBuilder.java | 36 ++++++++++++ .../jib/builder/steps/BuildImageStep.java | 41 +++++++------- .../configuration/ContainerConfiguration.java | 34 ++++++++++++ .../google/cloud/tools/jib/image/Image.java | 24 ++++++++ .../json/ContainerConfigurationTemplate.java | 13 +++++ .../jib/image/json/ImageToJsonTranslator.java | 55 ++++++++++++++++--- .../jib/image/json/JsonToImageTranslator.java | 32 +++++++++++ .../ContainerConfigurationTemplateTest.java | 3 + .../image/json/ImageToJsonTranslatorTest.java | 17 ++++++ .../image/json/JsonToImageTranslatorTest.java | 31 +++++++++++ .../test/resources/json/containerconfig.json | 2 +- .../json/translated_ocimanifest.json | 2 +- .../json/translated_v22manifest.json | 2 +- 14 files changed, 265 insertions(+), 29 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 68e44c3231..59c426eb4d 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) + ### Changed ### Fixed \ No newline at end of file diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 209591a2dc..8aa5c3120c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -239,6 +239,42 @@ public JibContainerBuilder addEnvironmentVariable(String name, String value) { return this; } + /** + * Sets the directories that may hold externally mounted volumes. + * + *

This is similar to {@code + * VOLUME} in Dockerfiles. + * + * @param volumes the list of directory paths on the container filesystem to set as volumes + * @return this + */ + public JibContainerBuilder setVolumes(List volumes) { + containerConfigurationBuilder.setVolumes(volumes); + return this; + } + + /** + * Sets the directories that may hold externally mounted volumes. + * + * @param volumes the list of directory paths on the container filesystem to set as volumes + * @return this + * @see #setVolumes(List) for more details + */ + public JibContainerBuilder setVolumes(AbsoluteUnixPath... volumes) { + return setVolumes(new ArrayList<>(Arrays.asList(volumes))); + } + + /** + * Adds a directory that may hold an externally mounted volume. + * + * @param volume a directory path on the container filesystem to represent a volume. + * @return this + * @see #setVolumes(List) for more details + */ + public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { + containerConfigurationBuilder.addVolume(volume); + return this; + } /** * Sets the ports to expose from the container. Ports exposed will allow ingress traffic. This * replaces any previously-set exposed ports. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index be9048bd5b..449316d20d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -118,9 +118,10 @@ private Image afterCachedLayerSteps() nonEmptyLayerCount++; } } - imageBuilder.addEnvironment(baseImage.getEnvironment()); - imageBuilder.addLabels(baseImage.getLabels()); - imageBuilder.setWorkingDirectory(baseImage.getWorkingDirectory()); + imageBuilder + .addEnvironment(baseImage.getEnvironment()) + .addLabels(baseImage.getLabels()) + .setWorkingDirectory(baseImage.getWorkingDirectory()); // Add history elements for non-empty layers that don't have one yet Instant layerCreationTime = @@ -138,24 +139,26 @@ private Image afterCachedLayerSteps() // Add built layers/configuration for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); - imageBuilder.addHistory( - HistoryEntry.builder() - .setCreationTimestamp(layerCreationTime) - .setAuthor("Jib") - .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) - .setComment(buildAndCacheApplicationLayerStep.getLayerType()) - .build()); + imageBuilder + .addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)) + .addHistory( + HistoryEntry.builder() + .setCreationTimestamp(layerCreationTime) + .setAuthor("Jib") + .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) + .setComment(buildAndCacheApplicationLayerStep.getLayerType()) + .build()); } if (containerConfiguration != null) { - imageBuilder.addEnvironment(containerConfiguration.getEnvironmentMap()); - imageBuilder.setCreated(containerConfiguration.getCreationTime()); - imageBuilder.setUser(containerConfiguration.getUser()); - imageBuilder.setEntrypoint(computeEntrypoint(baseImage, containerConfiguration)); - imageBuilder.setProgramArguments( - computeProgramArguments(baseImage, containerConfiguration)); - imageBuilder.setExposedPorts(containerConfiguration.getExposedPorts()); - imageBuilder.addLabels(containerConfiguration.getLabels()); + imageBuilder + .addEnvironment(containerConfiguration.getEnvironmentMap()) + .setCreated(containerConfiguration.getCreationTime()) + .setUser(containerConfiguration.getUser()) + .setEntrypoint(computeEntrypoint(baseImage, containerConfiguration)) + .setProgramArguments(computeProgramArguments(baseImage, containerConfiguration)) + .setExposedPorts(containerConfiguration.getExposedPorts()) + .setVolumes(containerConfiguration.getVolumes()) + .addLabels(containerConfiguration.getLabels()); } // Gets the container configuration content descriptor. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index ce44efca6a..460d697e0b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -43,6 +44,7 @@ public static class Builder { @Nullable private ImmutableList programArguments; @Nullable private Map environmentMap; @Nullable private List exposedPorts; + @Nullable private List volumes; @Nullable private Map labels; @Nullable private String user; @@ -120,6 +122,29 @@ public void addExposedPort(Port port) { exposedPorts.add(port); } + /** + * Sets the container's volumes. + * + * @param volumes the list of volumes + * @return this + */ + public Builder setVolumes(@Nullable List volumes) { + if (volumes == null) { + this.volumes = null; + } else { + Preconditions.checkArgument(!volumes.contains(null)); + this.volumes = new ArrayList<>(volumes); + } + return this; + } + + public void addVolume(AbsoluteUnixPath volume) { + if (volumes == null) { + volumes = new ArrayList<>(); + } + volumes.add(volume); + } + /** * Sets the container's labels. * @@ -185,6 +210,7 @@ public ContainerConfiguration build() { programArguments, environmentMap == null ? null : ImmutableMap.copyOf(environmentMap), exposedPorts == null ? null : ImmutableList.copyOf(exposedPorts), + volumes == null ? null : ImmutableList.copyOf(volumes), labels == null ? null : ImmutableMap.copyOf(labels), user); } @@ -206,6 +232,7 @@ public static Builder builder() { @Nullable private final ImmutableList programArguments; @Nullable private final ImmutableMap environmentMap; @Nullable private final ImmutableList exposedPorts; + @Nullable private final ImmutableList volumes; @Nullable private final ImmutableMap labels; @Nullable private final String user; @@ -215,6 +242,7 @@ private ContainerConfiguration( @Nullable ImmutableList programArguments, @Nullable ImmutableMap environmentMap, @Nullable ImmutableList exposedPorts, + @Nullable ImmutableList volumes, @Nullable ImmutableMap labels, @Nullable String user) { this.creationTime = creationTime; @@ -222,6 +250,7 @@ private ContainerConfiguration( this.programArguments = programArguments; this.environmentMap = environmentMap; this.exposedPorts = exposedPorts; + this.volumes = volumes; this.labels = labels; this.user = user; } @@ -250,6 +279,11 @@ public ImmutableList getExposedPorts() { return exposedPorts; } + @Nullable + public ImmutableList getVolumes() { + return volumes; + } + @Nullable public String getUser() { return user; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index ed5806575a..128a98dcc5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -44,6 +45,7 @@ public static class Builder { @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; @Nullable private ImmutableList exposedPorts; + @Nullable private ImmutableList volumes; @Nullable private String workingDirectory; @Nullable private String user; @@ -128,6 +130,17 @@ public Builder setExposedPorts(@Nullable List exposedPorts) { return this; } + /** + * Sets the items in the "Volumes" field in the container configuration. + * + * @param volumes the list of directories to create a volume. + * @return this + */ + public Builder setVolumes(@Nullable List volumes) { + this.volumes = (volumes == null) ? null : ImmutableList.copyOf(volumes); + return this; + } + /** * Adds items to the "Labels" field in the container configuration. * @@ -196,6 +209,7 @@ public Image build() { entrypoint, programArguments, exposedPorts, + volumes, ImmutableMap.copyOf(labelsBuilder), workingDirectory, user); @@ -227,6 +241,9 @@ public static Builder builder() { /** Ports that the container listens on. */ @Nullable private final ImmutableList exposedPorts; + /** List of directories to mount as volumes. */ + @Nullable private final ImmutableList volumes; + /** Labels on the container configuration */ @Nullable private final ImmutableMap labels; @@ -244,6 +261,7 @@ private Image( @Nullable ImmutableList entrypoint, @Nullable ImmutableList programArguments, @Nullable ImmutableList exposedPorts, + @Nullable ImmutableList volumes, @Nullable ImmutableMap labels, @Nullable String workingDirectory, @Nullable String user) { @@ -254,6 +272,7 @@ private Image( this.entrypoint = entrypoint; this.programArguments = programArguments; this.exposedPorts = exposedPorts; + this.volumes = volumes; this.labels = labels; this.workingDirectory = workingDirectory; this.user = user; @@ -284,6 +303,11 @@ public ImmutableList getExposedPorts() { return exposedPorts; } + @Nullable + public ImmutableList getVolumes() { + return volumes; + } + @Nullable public ImmutableMap getLabels() { return labels; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index bc9cd0fd64..143d7344b6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -40,6 +40,7 @@ * "Entrypoint": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], * "Cmd": ["arg1", "arg2"], * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} }, + * "Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}}, * "Labels": { "com.example.label": "value" }, * "WorkingDir": "/home/user/workspace", * "User": "me" @@ -114,6 +115,9 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { /** User. */ @Nullable private String User; + + /** Volumes */ + @Nullable private Map> Volumes; } /** @@ -164,6 +168,10 @@ public void setContainerUser(@Nullable String user) { config.User = user; } + public void setContainerVolumes(@Nullable Map> volumes) { + config.Volumes = volumes; + } + public void addLayerDiffId(DescriptorDigest diffId) { rootfs.diff_ids.add(diffId); } @@ -220,6 +228,11 @@ String getContainerUser() { return config.User; } + @Nullable + Map> getContainerVolumes() { + return config.Volumes; + } + @VisibleForTesting DescriptorDigest getLayerDiffId(int index) { return rootfs.diff_ids.get(index); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index c90944f2c2..77ee02b017 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -31,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Function; import javax.annotation.Nullable; /** @@ -58,15 +60,51 @@ public class ImageToJsonTranslator { @VisibleForTesting @Nullable static Map> portListToMap(@Nullable List exposedPorts) { - if (exposedPorts == null) { + return listToMap(exposedPorts, port -> port.getPort() + "/" + port.getProtocol()); + } + + /** + * Converts a list of {@link AbsoluteUnixPath}s to the corresponding container config format for + * volumes (e.g. {@code AbsoluteUnixPath().get("/var/log/my-app-logs")} -> {@code + * {"/var/log/my-app-logs":{}}}). + * + * @param volumes the list of {@link AbsoluteUnixPath}s to translate, or {@code null} + * @return a sorted map with the string representation of the ports as keys and empty maps as + * values, or {@code null} if {@code exposedPorts} is {@code null} + */ + @VisibleForTesting + @Nullable + static Map> volumesListToMap(@Nullable List volumes) { + return listToMap(volumes, AbsoluteUnixPath::toString); + } + + /** + * Turns a list into a sorted map where each element of the list is mapped to an entry composed by + * the key generated with {@code Function elementMapper} and an empty map as value. + * + *

This method is needed because the volume object is a direct JSON serialization of the Go + * type map[string]struct{} and is represented in JSON as an object mapping its keys to an empty + * object. + * + *

Further read at the image specs. + * + * @param list the list of elements to be transformed + * @param keyMapper the mapper function to generate keys to the map + * @param the type of the elements from the list + * @return an map + */ + @Nullable + private static Map> listToMap( + @Nullable List list, Function keyMapper) { + if (list == null) { return null; } - ImmutableSortedMap.Builder> result = - new ImmutableSortedMap.Builder<>(String::compareTo); - for (Port port : exposedPorts) { - result.put(port.getPort() + "/" + port.getProtocol(), Collections.emptyMap()); - } - return result.build(); + + return list.stream() + .collect( + ImmutableSortedMap.toImmutableSortedMap( + String::compareTo, keyMapper, ignored -> Collections.emptyMap())); } /** @@ -135,6 +173,9 @@ public Blob getContainerConfigurationBlob() { // Sets the exposed ports. template.setContainerExposedPorts(portListToMap(image.getExposedPorts())); + // Sets the volumes. + template.setContainerVolumes(volumesListToMap(image.getVolumes())); + // Sets the labels. template.setContainerLabels(image.getLabels()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 034965211e..395a41bdec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.DigestOnlyLayer; import com.google.cloud.tools.jib.image.Image; @@ -150,6 +151,11 @@ public static Image toImage( portMapToList(containerConfigurationTemplate.getContainerExposedPorts())); } + if (containerConfigurationTemplate.getContainerVolumes() != null) { + imageBuilder.setVolumes( + volumeMapToList(containerConfigurationTemplate.getContainerVolumes())); + } + if (containerConfigurationTemplate.getContainerEnvironment() != null) { for (String environmentVariable : containerConfigurationTemplate.getContainerEnvironment()) { Matcher matcher = ENVIRONMENT_PATTERN.matcher(environmentVariable); @@ -196,5 +202,31 @@ static ImmutableList portMapToList(@Nullable Map> portMa return ports.build(); } + /** + * Converts a map of volumes strings to a list of {@link AbsoluteUnixPath}s (e.g. {@code {@code + * {"/var/log/my-app-logs":{}}} -> AbsoluteUnixPath().get("/var/log/my-app-logs")}). + * + * @param volumeMap the map to convert + * @return a list of {@link AbsoluteUnixPath}s + */ + @VisibleForTesting + static ImmutableList volumeMapToList(@Nullable Map> volumeMap) + throws BadContainerConfigurationFormatException { + if (volumeMap == null) { + return ImmutableList.of(); + } + + ImmutableList.Builder volumeList = ImmutableList.builder(); + for (String volume : volumeMap.keySet()) { + try { + volumeList.add(AbsoluteUnixPath.get(volume)); + } catch (IllegalArgumentException exception) { + throw new BadContainerConfigurationFormatException("Invalid volume path: " + volume); + } + } + + return volumeList.build(); + } + private JsonToImageTranslator() {} } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 14dac4065f..3ff2dfc877 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -60,6 +60,9 @@ public void testToJson() throws IOException, URISyntaxException, DigestException "3000/udp", ImmutableMap.of())); containerConfigJson.setContainerLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); + containerConfigJson.setContainerVolumes( + ImmutableMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of())); containerConfigJson.setContainerWorkingDir("/some/workspace"); containerConfigJson.setContainerUser("tomcat"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index ea60a9ab50..5cc2a6755f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -61,6 +62,10 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { testImageBuilder.setProgramArguments(Arrays.asList("arg1", "arg2")); testImageBuilder.setExposedPorts( ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); + testImageBuilder.setVolumes( + Arrays.asList( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs"))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); testImageBuilder.setWorkingDirectory("/some/workspace"); testImageBuilder.setUser("tomcat"); @@ -136,6 +141,18 @@ public void testPortListToMap() { Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input)); } + @Test + public void testVolumeListToMap() { + ImmutableList input = + ImmutableList.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")); + ImmutableSortedMap> expected = + ImmutableSortedMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); + Assert.assertEquals(expected, ImageToJsonTranslator.volumesListToMap(input)); + } + @Test public void testEnvironmentMapToList() { ImmutableMap input = ImmutableMap.of("NAME1", "VALUE1", "NAME2", "VALUE2"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 827b919a00..819c2fd5ff 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -106,6 +107,31 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException } } + @Test + public void testvolumeMapToList() throws BadContainerConfigurationFormatException { + ImmutableSortedMap> input = + ImmutableSortedMap.of( + "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); + ImmutableList expected = + ImmutableList.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")); + Assert.assertEquals(expected, JsonToImageTranslator.volumeMapToList(input)); + + ImmutableList>> badInputs = + ImmutableList.of( + ImmutableMap.of("var/job-result-data", ImmutableMap.of()), + ImmutableMap.of("log", ImmutableMap.of()), + ImmutableMap.of("C:/udp", ImmutableMap.of())); + for (Map> badInput : badInputs) { + try { + JsonToImageTranslator.volumeMapToList(badInput); + Assert.fail(); + } catch (BadContainerConfigurationFormatException ignored) { + } + } + } + @Test public void testJsonToImageTranslatorRegex() { assertGoodEnvironmentPattern("NAME=VALUE", "NAME", "VALUE"); @@ -183,6 +209,11 @@ private void testToImage_buildable( Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); Assert.assertEquals( ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); + Assert.assertEquals( + ImmutableList.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs")), + image.getVolumes()); Assert.assertEquals("tomcat", image.getUser()); } } diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index d4fe8ced9e..b43afe5e65 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat"},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/json/translated_ocimanifest.json index 239aa3ab56..8b909b11b2 100644 --- a/jib-core/src/test/resources/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:f200c357a04f3deee6422664f36fa5e1f7ac1ab09b4e1f5f6ab1a2bff56bdab4","size":626},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:39feb4ae7cd3041613e19fcc33c7ec9bd14e6a2bd6fd182c404cd1ba9e20f00d","size":690},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index d9b83ead76..e1da31cdd3 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:f200c357a04f3deee6422664f36fa5e1f7ac1ab09b4e1f5f6ab1a2bff56bdab4","size":626},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:39feb4ae7cd3041613e19fcc33c7ec9bd14e6a2bd6fd182c404cd1ba9e20f00d","size":690},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file From 717fea5aca7ffc1cc303d67c5ea1295feacadcc8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 9 Nov 2018 17:31:09 -0500 Subject: [PATCH 0340/2020] Add DockerHealthCheck class (#1236) --- .../jib/configuration/DockerHealthCheck.java | 214 ++++++++++++++++++ .../configuration/DockerHealthCheckTest.java | 84 +++++++ 2 files changed, 298 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java new file mode 100644 index 0000000000..baa3696b2c --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java @@ -0,0 +1,214 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Configuration information for performing healthchecks on a Docker container. */ +public class DockerHealthCheck { + + /** Builds the immutable {@link DockerHealthCheck}. */ + public static class Builder { + + @Nullable private final ImmutableList command; + @Nullable private Duration interval; + @Nullable private Duration timeout; + @Nullable private Duration startPeriod; + @Nullable private Integer retries; + + private Builder(@Nullable ImmutableList command) { + this.command = command; + } + + /** + * Sets the time between healthchecks. + * + * @param interval the duration to wait between healthchecks. + * @return this + */ + public Builder setInterval(Duration interval) { + this.interval = interval; + return this; + } + + /** + * Sets the time until a healthcheck is considered hung. + * + * @param timeout the duration to wait until considering the healthcheck to be hung. + * @return this + */ + public Builder setTimeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Sets the initialization time to wait before using healthchecks. + * + * @param startPeriod the duration to wait before using healthchecks + * @return this + */ + public Builder setStartPeriod(Duration startPeriod) { + this.startPeriod = startPeriod; + return this; + } + + /** + * Sets the number of times to retry the healthcheck before the container is considered to be + * unhealthy. + * + * @param retries the number of retries before the container is considered to be unhealthy + * @return this + */ + public Builder setRetries(int retries) { + this.retries = retries; + return this; + } + + public DockerHealthCheck build() { + return new DockerHealthCheck(command, interval, timeout, startPeriod, retries); + } + } + + /** + * Creates a disabled {@link DockerHealthCheck} (corresponds to "NONE" in container config). + * + * @return the new {@link DockerHealthCheck} + */ + public static DockerHealthCheck disabled() { + return new DockerHealthCheck(ImmutableList.of("NONE"), null, null, null, null); + } + + /** + * Creates a new {@link DockerHealthCheck.Builder} with the command set to be inherited from the + * base image (corresponds to empty list in container config). + * + * @return the new {@link DockerHealthCheck.Builder} + */ + public static Builder builderWithInheritedCommand() { + return new Builder(null); + } + + /** + * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be + * directly executed (corresponds to "CMD" in container config). + * + * @param command the healthcheck command to execute + * @return the new {@link DockerHealthCheck.Builder} + */ + public static Builder builderWithExecCommand(List command) { + return new Builder(ImmutableList.builder().add("CMD").addAll(command).build()); + } + + /** + * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be + * directly executed (corresponds to "CMD" in container config). + * + * @param command the healthcheck command to execute + * @return the new {@link DockerHealthCheck.Builder} + */ + public static Builder builderWithExecCommand(String... command) { + return new Builder( + ImmutableList.builder().add("CMD").addAll(Arrays.asList(command)).build()); + } + + /** + * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be + * run by the container's default shell (corresponds to "CMD-SHELL" in container config). This + * command cannot be run on containers with no default shell. + * + * @param command the shell command to run + * @return the new {@link DockerHealthCheck.Builder} + */ + public static Builder builderWithShellCommand(String command) { + return new Builder(ImmutableList.of("CMD-SHELL", command)); + } + + @Nullable private final ImmutableList command; + @Nullable private final Duration interval; + @Nullable private final Duration timeout; + @Nullable private final Duration startPeriod; + @Nullable private final Integer retries; + + private DockerHealthCheck( + @Nullable ImmutableList command, + @Nullable Duration interval, + @Nullable Duration timeout, + @Nullable Duration startPeriod, + @Nullable Integer retries) { + this.command = command; + this.interval = interval; + this.timeout = timeout; + this.startPeriod = startPeriod; + this.retries = retries; + } + + /** + * Gets the optional healthcheck command. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck command + */ + public Optional> getCommand() { + return Optional.ofNullable(command); + } + + /** + * Gets the optional healthcheck interval. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck interval + */ + public Optional getInterval() { + return Optional.ofNullable(interval); + } + + /** + * Gets the optional healthcheck timeout. A missing command means that it will be inherited from + * the base image. + * + * @return the healthcheck timeout + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + /** + * Gets the optional healthcheck start period. A missing command means that it will be inherited + * from the base image. + * + * @return the healthcheck start period + */ + public Optional getStartPeriod() { + return Optional.ofNullable(startPeriod); + } + + /** + * Gets the optional healthcheck retry count. A missing command means that it will be inherited + * from the base image. + * + * @return the healthcheck retry count + */ + public Optional getRetries() { + return Optional.ofNullable(retries); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java new file mode 100644 index 0000000000..91afd759d5 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.configuration; + +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link DockerHealthCheck}. */ +public class DockerHealthCheckTest { + + @Test + public void testBuild_parameters() { + DockerHealthCheck healthCheck = + DockerHealthCheck.builderWithInheritedCommand() + .setInterval(Duration.ofNanos(123)) + .setTimeout(Duration.ofNanos(456)) + .setStartPeriod(Duration.ofNanos(789)) + .setRetries(10) + .build(); + + Assert.assertTrue(healthCheck.getInterval().isPresent()); + Assert.assertEquals(Duration.ofNanos(123), healthCheck.getInterval().get()); + Assert.assertTrue(healthCheck.getTimeout().isPresent()); + Assert.assertEquals(Duration.ofNanos(456), healthCheck.getTimeout().get()); + Assert.assertTrue(healthCheck.getStartPeriod().isPresent()); + Assert.assertEquals(Duration.ofNanos(789), healthCheck.getStartPeriod().get()); + Assert.assertTrue(healthCheck.getRetries().isPresent()); + Assert.assertEquals(10, (int) healthCheck.getRetries().get()); + } + + @Test + public void testBuild_propagated() { + DockerHealthCheck healthCheck = DockerHealthCheck.builderWithInheritedCommand().build(); + Assert.assertFalse(healthCheck.getCommand().isPresent()); + } + + @Test + public void testBuild_execArray() { + DockerHealthCheck healthCheck = + DockerHealthCheck.builderWithExecCommand("test", "command").build(); + Assert.assertTrue(healthCheck.getCommand().isPresent()); + Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand().get()); + } + + @Test + public void testBuild_execList() { + DockerHealthCheck healthCheck = + DockerHealthCheck.builderWithExecCommand(ImmutableList.of("test", "command")).build(); + Assert.assertTrue(healthCheck.getCommand().isPresent()); + Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand().get()); + } + + @Test + public void testBuild_shell() { + DockerHealthCheck healthCheck = + DockerHealthCheck.builderWithShellCommand("shell command").build(); + Assert.assertTrue(healthCheck.getCommand().isPresent()); + Assert.assertEquals( + ImmutableList.of("CMD-SHELL", "shell command"), healthCheck.getCommand().get()); + } + + @Test + public void testDisabled() { + DockerHealthCheck healthCheck = DockerHealthCheck.disabled(); + Assert.assertTrue(healthCheck.getCommand().isPresent()); + Assert.assertEquals(ImmutableList.of("NONE"), healthCheck.getCommand().get()); + } +} From d260fdfc5876e0b0dbda1d722d984d8964a5d553 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 12 Nov 2018 09:23:20 -0500 Subject: [PATCH 0341/2020] Configure surefire and failsafe to not trim stack traces (#1237) --- jib-maven-plugin/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0bba2168d5..d58f50fe6f 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -156,6 +156,7 @@ <_JIB_DISABLE_USER_AGENT>true + false @@ -363,6 +364,7 @@ **/*IntegrationTest.java + false From 11e161c6a8d3c49d9b912f8f71a809bf8d0589d3 Mon Sep 17 00:00:00 2001 From: Guillaume Laforge Date: Mon, 12 Nov 2018 17:49:04 +0100 Subject: [PATCH 0342/2020] Define run.main for the main class to launch in Micronaut example (#1243) --- examples/micronaut/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 8bc3b1ce43..59975a7ef0 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -39,6 +39,7 @@ dependencies { compileJava.options.compilerArgs += '-parameters' compileTestJava.options.compilerArgs += '-parameters' +run.main = 'example.micronaut.Application' /** CHANGE THIS TO THE IMAGE YOU WANT TO PUSH TO */ // jib.to.image = 'gcr.io/PROJECT_ID/micronaut-jib' From 8dc209195c2dfcf765c029e0cecc36203d901d01 Mon Sep 17 00:00:00 2001 From: Julien Ponge Date: Mon, 12 Nov 2018 21:05:52 +0100 Subject: [PATCH 0343/2020] Add Eclipse Vert.x example (#1240) --- examples/README.md | 4 + examples/vertx/README.md | 17 ++ examples/vertx/build.gradle | 25 +++ .../vertx/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/vertx/gradlew | 172 ++++++++++++++++++ examples/vertx/gradlew.bat | 84 +++++++++ examples/vertx/settings.gradle | 1 + .../main/java/example/vertx/MainVerticle.java | 56 ++++++ examples/vertx/src/main/resources/logback.xml | 17 ++ 10 files changed, 382 insertions(+) create mode 100644 examples/vertx/README.md create mode 100644 examples/vertx/build.gradle create mode 100644 examples/vertx/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/vertx/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/vertx/gradlew create mode 100644 examples/vertx/gradlew.bat create mode 100644 examples/vertx/settings.gradle create mode 100644 examples/vertx/src/main/java/example/vertx/MainVerticle.java create mode 100644 examples/vertx/src/main/resources/logback.xml diff --git a/examples/README.md b/examples/README.md index ef0c3116b8..4604897693 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,10 @@ See [helloworld](helloworld) for containerizing a simple `Hello World` applicati See [multi-module](multi-module) for containerizing projects with multiple modules. +### Vert.x + +See [vertx](vertx) for containerizing [Eclipse Vert.x](https://vertx.io/) applications. + diff --git a/examples/vertx/README.md b/examples/vertx/README.md new file mode 100644 index 0000000000..10faf3d5ce --- /dev/null +++ b/examples/vertx/README.md @@ -0,0 +1,17 @@ +# Containerize a [Eclipse Vert.x](https://vertx.io/) application with Jib + +This is an example of how to easily build a Docker image for a [Eclipse Vert.x application](https://vertx.io/) with Jib. + +```shell +./gradlew jibDockerBuild + +docker run -d --rm -p 8080:8080 vertx-jib-example +``` + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [more about Eclipse Vert.x](https://vertx.io). + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/vertx)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle new file mode 100644 index 0000000000..10bcdf5e7a --- /dev/null +++ b/examples/vertx/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'io.vertx.vertx-plugin' version '0.1.0' + id 'com.google.cloud.tools.jib' version '0.10.0' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'io.vertx:vertx-web' + implementation 'ch.qos.logback:logback-classic:1.2.3' +} + +vertx { + vertxVersion = '3.6.0.CR1' + mainVerticle = 'example.vertx.MainVerticle' + jvmArgs = ['-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory'] +} + +jib { + container { + ports = ['8080'] + } +} \ No newline at end of file diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.jar b/examples/vertx/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..29953ea141f55e3b8fc691d31b5ca8816d89fa87 GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/vertx/gradlew.bat b/examples/vertx/gradlew.bat new file mode 100644 index 0000000000..f9553162f1 --- /dev/null +++ b/examples/vertx/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/vertx/settings.gradle b/examples/vertx/settings.gradle new file mode 100644 index 0000000000..268b013937 --- /dev/null +++ b/examples/vertx/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'vertx-jib-example' diff --git a/examples/vertx/src/main/java/example/vertx/MainVerticle.java b/examples/vertx/src/main/java/example/vertx/MainVerticle.java new file mode 100644 index 0000000000..c74eb02863 --- /dev/null +++ b/examples/vertx/src/main/java/example/vertx/MainVerticle.java @@ -0,0 +1,56 @@ +package example.vertx; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MainVerticle extends AbstractVerticle { + + public static void main(String[] args) { + Vertx.vertx().deployVerticle(new MainVerticle()); + } + + private final Logger logger = LoggerFactory.getLogger(MainVerticle.class); + + @Override + public void start(Future startFuture) { + Router router = Router.router(vertx); + + router.get("/").handler(this::hello); + router.get("/time").handler(this::now); + + vertx.createHttpServer() + .requestHandler(router) + .listen(8080, asyncStart -> { + if (asyncStart.succeeded()) { + startFuture.complete(); + logger.info("HTTP server running on port 8080"); + } else { + logger.error("Woops", asyncStart.cause()); + startFuture.fail(asyncStart.cause()); + } + }); + } + + private void hello(RoutingContext context) { + logger.info("Hello request from {}", context.request().remoteAddress()); + context.response() + .putHeader("Content-Type", "text/plain") + .end("Hello from Vert.x!"); + } + + private void now(RoutingContext context) { + logger.info("Time request from {}", context.request().remoteAddress()); + JsonObject data = new JsonObject() + .put("powered-by", "vertx") + .put("current-time", System.currentTimeMillis()); + context.response() + .putHeader("Content-Type", "application/json") + .end(data.encode()); + } +} diff --git a/examples/vertx/src/main/resources/logback.xml b/examples/vertx/src/main/resources/logback.xml new file mode 100644 index 0000000000..e60adb276e --- /dev/null +++ b/examples/vertx/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + \ No newline at end of file From f88fd29c9279762d33c202f85dfbea720ea65ed6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 12 Nov 2018 15:54:52 -0500 Subject: [PATCH 0344/2020] Fix NullPointerException with incomplete auth config (#1244) --- .../tools/jib/gradle/AuthParameters.java | 32 ---------------- .../tools/jib/gradle/BaseImageParameters.java | 4 +- .../jib/gradle/GradleRawConfiguration.java | 20 ++++++++++ .../cloud/tools/jib/gradle/JibExtension.java | 4 +- .../jib/gradle/TargetImageParameters.java | 4 +- .../gradle/GradleRawConfigurationTest.java | 9 ++--- .../jib/maven/JibPluginConfiguration.java | 26 ------------- .../jib/maven/MavenRawConfiguration.java | 20 ++++++++++ .../maven/MavenSettingsServerCredentials.java | 15 -------- .../jib/maven/JibPluginConfigurationTest.java | 14 +------ .../jib/maven/MavenRawConfigurationTest.java | 14 +++---- .../MavenSettingsServerCredentialsTest.java | 2 - .../jib/plugins/common/AuthProperty.java | 21 ----------- .../ConfigurationPropertyValidator.java | 10 +++-- .../common/PluginConfigurationProcessor.java | 32 +++++++++++----- .../jib/plugins/common/RawConfiguration.java | 8 ++++ .../ConfigurationPropertyValidatorTest.java | 37 +++++++++++++++---- 17 files changed, 122 insertions(+), 150 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index 245a951426..b2ebf76a1a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -17,11 +17,8 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import com.google.common.base.Preconditions; import javax.annotation.Nullable; -import javax.inject.Inject; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; /** @@ -32,35 +29,6 @@ public class AuthParameters implements AuthProperty { @Nullable private String username; @Nullable private String password; - private String descriptor; - - /** - * Constructs a new {@link AuthParameters}. - * - * @param descriptor the name of the auth configuration property - */ - @Inject - public AuthParameters(String descriptor) { - this.descriptor = descriptor; - } - - @Internal - @Override - public String getPropertyDescriptor() { - return Preconditions.checkNotNull(descriptor); - } - - @Internal - @Override - public String getUsernamePropertyDescriptor() { - return Preconditions.checkNotNull(descriptor) + ".username"; - } - - @Internal - @Override - public String getPasswordPropertyDescriptor() { - return Preconditions.checkNotNull(descriptor) + ".password"; - } @Input @Optional diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java index 8b8caa62ad..79570ff43e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -34,8 +34,8 @@ public class BaseImageParameters { @Nullable private String credHelper; @Inject - public BaseImageParameters(ObjectFactory objectFactory, String imageDescriptor) { - auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); + public BaseImageParameters(ObjectFactory objectFactory) { + auth = objectFactory.newInstance(AuthParameters.class); } @Input diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index ca97e5dbea..4431b3a8b6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -57,6 +57,21 @@ public AuthProperty getToAuth() { return jibExtension.getTo().getAuth(); } + @Override + public String getAuthDescriptor(String source) { + return source + ".auth"; + } + + @Override + public String getUsernameAuthDescriptor(String source) { + return getAuthDescriptor(source) + ".username"; + } + + @Override + public String getPasswordAuthDescriptor(String source) { + return getAuthDescriptor(source) + ".password"; + } + @Override public Optional getToCredHelper() { return Optional.ofNullable(jibExtension.getTo().getCredHelper()); @@ -132,6 +147,11 @@ public Optional getInferredAuth(String authTarget) { return Optional.empty(); } + @Override + public String getInferredAuthDescriptor() { + return ""; + } + @Override public ImageFormat getImageFormat() { return jibExtension.getContainer().getFormat(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 2a68dea65f..b7c4cfc6ac 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -78,8 +78,8 @@ public class JibExtension { public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); - from = objectFactory.newInstance(BaseImageParameters.class, "jib.from"); - to = objectFactory.newInstance(TargetImageParameters.class, "jib.to"); + from = objectFactory.newInstance(BaseImageParameters.class); + to = objectFactory.newInstance(TargetImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); extraDirectory = objectFactory.newInstance(ExtraDirectoryParameters.class, project.getProjectDir().toPath()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java index f4bea234e6..17d6ce04c4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -39,8 +39,8 @@ public class TargetImageParameters { @Nullable private String credHelper; @Inject - public TargetImageParameters(ObjectFactory objectFactory, String imageDescriptor) { - auth = objectFactory.newInstance(AuthParameters.class, imageDescriptor + ".auth"); + public TargetImageParameters(ObjectFactory objectFactory) { + auth = objectFactory.newInstance(AuthParameters.class); } @Input diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index e57e472a37..25b56b740f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -39,9 +39,6 @@ public void testGetters() { Mockito.when(authParameters.getUsername()).thenReturn("user"); Mockito.when(authParameters.getPassword()).thenReturn("password"); - Mockito.when(authParameters.getPropertyDescriptor()).thenReturn("auth source"); - Mockito.when(authParameters.getUsernamePropertyDescriptor()).thenReturn("from.auth.username"); - Mockito.when(authParameters.getPasswordPropertyDescriptor()).thenReturn(""); Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); @@ -74,9 +71,9 @@ public void testGetters() { AuthProperty fromAuth = rawConfiguration.getFromAuth(); Assert.assertEquals("user", fromAuth.getUsername()); Assert.assertEquals("password", fromAuth.getPassword()); - Assert.assertEquals("auth source", fromAuth.getPropertyDescriptor()); - Assert.assertEquals("from.auth.username", fromAuth.getUsernamePropertyDescriptor()); - Assert.assertEquals("", fromAuth.getPasswordPropertyDescriptor()); + Assert.assertEquals("test.auth", rawConfiguration.getAuthDescriptor("test")); + Assert.assertEquals("test2.auth.username", rawConfiguration.getUsernameAuthDescriptor("test2")); + Assert.assertEquals("test3.auth.password", rawConfiguration.getPasswordAuthDescriptor("test3")); Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); Assert.assertEquals("/app/root", rawConfiguration.getAppRoot()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 28891aeb51..4cfe21f399 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -49,22 +49,6 @@ public static class AuthConfiguration implements AuthProperty { @Nullable @Parameter private String username; @Nullable @Parameter private String password; - @Nullable private String descriptor; - - @Override - public String getPropertyDescriptor() { - return Preconditions.checkNotNull(descriptor); - } - - @Override - public String getUsernamePropertyDescriptor() { - return Preconditions.checkNotNull(descriptor) + ""; - } - - @Override - public String getPasswordPropertyDescriptor() { - return Preconditions.checkNotNull(descriptor) + ""; - } @Override @Nullable @@ -87,10 +71,6 @@ void setUsername(String username) { void setPassword(String password) { this.password = password; } - - private void setPropertyDescriptor(String descriptor) { - this.descriptor = descriptor; - } } /** Used to configure {@code extraDirectory.permissions} parameter. */ @@ -230,12 +210,6 @@ public File getPath() { @Nullable @Component protected SettingsDecrypter settingsDecrypter; - /** Default constructor handles setting up auth property descriptors. */ - JibPluginConfiguration() { - to.auth.setPropertyDescriptor(""); - from.auth.setPropertyDescriptor(""); - } - MavenSession getSession() { return Preconditions.checkNotNull(session); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index da155a8520..af93c95cbf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -72,6 +72,21 @@ public AuthProperty getToAuth() { return jibPluginConfiguration.getTargetImageAuth(); } + @Override + public String getAuthDescriptor(String source) { + return "<" + source + ">"; + } + + @Override + public String getUsernameAuthDescriptor(String source) { + return getAuthDescriptor(source) + ""; + } + + @Override + public String getPasswordAuthDescriptor(String source) { + return getAuthDescriptor(source) + ""; + } + @Override public Optional getToCredHelper() { return Optional.ofNullable(jibPluginConfiguration.getTargetImageCredentialHelperName()); @@ -148,6 +163,11 @@ public Optional getInferredAuth(String authTarget) return mavenSettingsServerCredentials.retrieve(authTarget); } + @Override + public String getInferredAuthDescriptor() { + return MavenSettingsServerCredentials.CREDENTIAL_SOURCE; + } + @Override public ImageFormat getImageFormat() { return ImageFormat.valueOf(jibPluginConfiguration.getFormat()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 87cf7cb26e..2ada7bc69e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -136,21 +136,6 @@ public String getUsername() { public String getPassword() { return password; } - - @Override - public String getPropertyDescriptor() { - return CREDENTIAL_SOURCE; - } - - @Override - public String getUsernamePropertyDescriptor() { - return CREDENTIAL_SOURCE; - } - - @Override - public String getPasswordPropertyDescriptor() { - return CREDENTIAL_SOURCE; - } }); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 8cb69fb8d4..1f5dbc6101 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -70,19 +70,7 @@ public void teardown() { } @Test - public void testAuthDefaults() { - Assert.assertEquals( - "", - testPluginConfiguration.getBaseImageAuth().getUsernamePropertyDescriptor()); - Assert.assertEquals( - "", - testPluginConfiguration.getBaseImageAuth().getPasswordPropertyDescriptor()); - Assert.assertEquals( - "", - testPluginConfiguration.getTargetImageAuth().getUsernamePropertyDescriptor()); - Assert.assertEquals( - "", - testPluginConfiguration.getTargetImageAuth().getPasswordPropertyDescriptor()); + public void testDefaults() { Assert.assertEquals("", testPluginConfiguration.getAppRoot()); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index 8dffae9eaf..4ed198f469 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -52,9 +52,6 @@ public void testGetters() throws InferredAuthRetrievalException { AuthConfiguration auth = Mockito.mock(AuthConfiguration.class); Mockito.when(auth.getUsername()).thenReturn("user"); Mockito.when(auth.getPassword()).thenReturn("password"); - Mockito.when(auth.getPropertyDescriptor()).thenReturn("auth source"); - Mockito.when(auth.getUsernamePropertyDescriptor()).thenReturn("from.auth.username"); - Mockito.when(auth.getPasswordPropertyDescriptor()).thenReturn(""); Mockito.when(jibPluginConfiguration.getSession()).thenReturn(mavenSession); Mockito.when(jibPluginConfiguration.getBaseImageAuth()).thenReturn(auth); @@ -84,16 +81,15 @@ public void testGetters() throws InferredAuthRetrievalException { AuthProperty fromAuth = rawConfiguration.getFromAuth(); Assert.assertEquals("user", fromAuth.getUsername()); Assert.assertEquals("password", fromAuth.getPassword()); - Assert.assertEquals("auth source", fromAuth.getPropertyDescriptor()); - Assert.assertEquals("from.auth.username", fromAuth.getUsernamePropertyDescriptor()); - Assert.assertEquals("", fromAuth.getPasswordPropertyDescriptor()); + Assert.assertEquals("", rawConfiguration.getAuthDescriptor("test")); + Assert.assertEquals( + "", rawConfiguration.getUsernameAuthDescriptor("test")); + Assert.assertEquals( + "", rawConfiguration.getPasswordAuthDescriptor("test")); AuthProperty mavenSettingsAuth = rawConfiguration.getInferredAuth("base registry").get(); Assert.assertEquals("maven settings user", mavenSettingsAuth.getUsername()); Assert.assertEquals("maven settings password", mavenSettingsAuth.getPassword()); - Assert.assertEquals("Maven settings", mavenSettingsAuth.getPropertyDescriptor()); - Assert.assertEquals("Maven settings", mavenSettingsAuth.getUsernamePropertyDescriptor()); - Assert.assertEquals("Maven settings", mavenSettingsAuth.getPasswordPropertyDescriptor()); Assert.assertFalse(rawConfiguration.getInferredAuth("unknown registry").isPresent()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 23fa29c09c..583fd326e9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -62,7 +62,6 @@ public void testRetrieve_found() throws InferredAuthRetrievalException { Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); - Assert.assertEquals("Maven settings", auth.get().getPropertyDescriptor()); Mockito.verifyZeroInteractions(mockEventDispatcher); } @@ -114,7 +113,6 @@ public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalExc Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); - Assert.assertEquals("Maven settings", auth.get().getPropertyDescriptor()); Mockito.verify(mockDecrypter).decrypt(Mockito.any()); Mockito.verify(mockResult).getProblems(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java index d81b6a6452..b62d61ad27 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java @@ -26,25 +26,4 @@ public interface AuthProperty { @Nullable String getPassword(); - - /** - * Returns the full descriptor used to configure the {@link AuthProperty}. - * - * @return the descriptor used to configure the property (e.g. 'jib.to.auth') - */ - String getPropertyDescriptor(); - - /** - * Returns the full descriptor used to configure the {@link AuthProperty}'s username. - * - * @return the descriptor used to configure the username property (e.g. 'jib.to.auth.username') - */ - String getUsernamePropertyDescriptor(); - - /** - * Returns the full descriptor used to configure the {@link AuthProperty}'s password. - * - * @return the descriptor used to configure the password property (e.g. 'jib.to.auth.password') - */ - String getPasswordPropertyDescriptor(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 708a2dfce1..11455887eb 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -48,6 +48,8 @@ public class ConfigurationPropertyValidator { * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials + * @param authUsernameDescriptor the name of the auth username parameter + * @param authPasswordDescriptor the name of the auth password parameter * @return a new {@link Authorization} from the system properties or build configuration, or * {@link Optional#empty} if neither is configured. */ @@ -55,7 +57,9 @@ public static Optional getImageCredential( EventDispatcher eventDispatcher, String usernameProperty, String passwordProperty, - AuthProperty auth) { + AuthProperty auth, + String authUsernameDescriptor, + String authPasswordDescriptor) { // System property takes priority over build configuration String commandlineUsername = System.getProperty(usernameProperty); String commandlinePassword = System.getProperty(passwordProperty); @@ -89,14 +93,14 @@ public static Optional getImageCredential( if (Strings.isNullOrEmpty(auth.getUsername())) { eventDispatcher.dispatch( LogEvent.warn( - auth.getUsernamePropertyDescriptor() + authUsernameDescriptor + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { eventDispatcher.dispatch( LogEvent.warn( - auth.getPasswordPropertyDescriptor() + authPasswordDescriptor + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 9294eb701f..e7fa89f869 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -56,7 +56,7 @@ public class PluginConfigurationProcessor { private interface InferredAuthProvider { Optional getInferredAuth(String registry) throws InferredAuthRetrievalException; - }; + } /** * Compute the container entrypoint, in this order: @@ -177,8 +177,11 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry PropertyNames.TO_AUTH_USERNAME, PropertyNames.TO_AUTH_PASSWORD, rawConfiguration.getToAuth(), - "to.auth/", + rawConfiguration.getAuthDescriptor("to"), + rawConfiguration.getUsernameAuthDescriptor("to"), + rawConfiguration.getPasswordAuthDescriptor("to"), rawConfiguration::getInferredAuth, + rawConfiguration.getInferredAuthDescriptor(), rawConfiguration.getToCredHelper().orElse(null)); PluginConfigurationProcessor processor = @@ -224,8 +227,11 @@ static PluginConfigurationProcessor processCommonConfiguration( PropertyNames.FROM_AUTH_USERNAME, PropertyNames.FROM_AUTH_PASSWORD, rawConfiguration.getFromAuth(), - "from.auth/", + rawConfiguration.getAuthDescriptor("from"), + rawConfiguration.getUsernameAuthDescriptor("from"), + rawConfiguration.getPasswordAuthDescriptor("from"), rawConfiguration::getInferredAuth, + rawConfiguration.getInferredAuthDescriptor(), rawConfiguration.getFromCredHelper().orElse(null)); JibContainerBuilder jibContainerBuilder = @@ -292,8 +298,11 @@ private static boolean configureCredentialRetrievers( String usernamePropertyName, String passwordPropertyName, AuthProperty knownAuth, - String knownAuthSource, + String knownAuthDescriptor, + String knownAuthUsernameDescriptor, + String knownAuthPasswordDescriptor, InferredAuthProvider inferredAuthProvider, + String inferredAuthDescriptor, @Nullable String credHelper) throws FileNotFoundException, InferredAuthRetrievalException { DefaultCredentialRetrievers defaultCredentialRetrievers = @@ -301,12 +310,16 @@ private static boolean configureCredentialRetrievers( CredentialRetrieverFactory.forImage(imageReference, eventDispatcher)); Optional optionalToCredential = ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, usernamePropertyName, passwordPropertyName, knownAuth); + eventDispatcher, + usernamePropertyName, + passwordPropertyName, + knownAuth, + knownAuthUsernameDescriptor, + knownAuthPasswordDescriptor); boolean credentialPresent = optionalToCredential.isPresent(); if (optionalToCredential.isPresent()) { - // TODO: fix https://github.com/GoogleContainerTools/jib/issues/1177 - // knownAuth.getPropertyDescriptor() may NPE. Fix it and remove the knownAuthSource parameter. - defaultCredentialRetrievers.setKnownCredential(optionalToCredential.get(), knownAuthSource); + defaultCredentialRetrievers.setKnownCredential( + optionalToCredential.get(), knownAuthDescriptor); } else { Optional optionalInferredAuth = inferredAuthProvider.getInferredAuth(imageReference.getRegistry()); @@ -316,7 +329,7 @@ private static boolean configureCredentialRetrievers( String username = Verify.verifyNotNull(auth.getUsername()); String password = Verify.verifyNotNull(auth.getPassword()); Credential credential = Credential.basic(username, password); - defaultCredentialRetrievers.setInferredCredential(credential, auth.getPropertyDescriptor()); + defaultCredentialRetrievers.setInferredCredential(credential, inferredAuthDescriptor); } } defaultCredentialRetrievers.setCredentialHelper(credHelper); @@ -347,7 +360,6 @@ private static ImageReference getGeneratedTargetDockerTag( * @param containerizer the {@link Containerizer} to configure * @param rawConfiguration the raw build configuration * @param projectProperties the project properties - * @param toolName tool name to set */ private static void configureContainerizer( Containerizer containerizer, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 316e64ea06..6ad730653d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -35,6 +35,12 @@ public interface RawConfiguration { AuthProperty getToAuth(); + String getAuthDescriptor(String source); + + String getUsernameAuthDescriptor(String source); + + String getPasswordAuthDescriptor(String source); + Optional getFromCredHelper(); Optional getToCredHelper(); @@ -70,5 +76,7 @@ public interface RawConfiguration { // https://github.com/GoogleContainerTools/jib/pull/1163#discussion_r228389684 Optional getInferredAuth(String authTarget) throws InferredAuthRetrievalException; + String getInferredAuthDescriptor(); + ImageFormat getImageFormat(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index e539f21e73..e702690316 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -41,8 +41,6 @@ public class ConfigurationPropertyValidatorTest { @Test public void testGetImageAuth() { - Mockito.when(mockAuth.getUsernamePropertyDescriptor()).thenReturn("user"); - Mockito.when(mockAuth.getPasswordPropertyDescriptor()).thenReturn("pass"); Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); @@ -52,7 +50,12 @@ public void testGetImageAuth() { Credential expected = Credential.basic("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, + "jib.test.auth.user", + "jib.test.auth.pass", + mockAuth, + "user", + "pass"); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); @@ -62,7 +65,12 @@ public void testGetImageAuth() { expected = Credential.basic("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, + "jib.test.auth.user", + "jib.test.auth.pass", + mockAuth, + "user", + "pass"); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); Mockito.verify(mockEventDispatcher, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); @@ -72,7 +80,12 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, + "jib.test.auth.user", + "jib.test.auth.pass", + mockAuth, + "user", + "pass"); Assert.assertFalse(actual.isPresent()); // Password missing @@ -80,7 +93,12 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, + "jib.test.auth.user", + "jib.test.auth.pass", + mockAuth, + "user", + "pass"); Assert.assertFalse(actual.isPresent()); Mockito.verify(mockEventDispatcher) .dispatch( @@ -91,7 +109,12 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, "jib.test.auth.user", "jib.test.auth.pass", mockAuth); + mockEventDispatcher, + "jib.test.auth.user", + "jib.test.auth.pass", + mockAuth, + "user", + "pass"); Assert.assertFalse(actual.isPresent()); Mockito.verify(mockEventDispatcher) .dispatch( From 5e70a9a044642c4658827faefdda1278998106a3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 15 Nov 2018 09:38:49 +0800 Subject: [PATCH 0345/2020] Adds initial style guide. (#1208) --- CONTRIBUTING.md | 2 +- STYLE_GUIDE.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 STYLE_GUIDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66de355b56..52b179be87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ To build, use the provided `build.sh` which builds and tests each of the compone If you're a Googler or other corporate contributor, use your corporate email address here, not your personal address. 2. Fork the repository into your own Github account. -3. We follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) +3. We follow our own [Java style guide](STYLE_GUIDE.md) that extends the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). 3. Please include unit tests (and integration tests if applicable) for all new code. 4. Make sure all existing tests pass (but see the note below about integration tests). * In `jib-core`, run `./gradlew clean goJF build integrationTest` diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000000..099f7aa47c --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,48 @@ +# Style guide + +This style guide defines specific coding standards and advice for this Java codebase. The rules here are extensions to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). + +Please see the [contributing guide](CONTRIBUTING.md) for general guidance for contributing to this project. + +### Automatic formatting + +Automatic formatting should be performed with `./gradlew goJF` or `./mvnw fmt:format`. Formatting all projects can be done with `./build.sh format`. + +### Class member order + +*Extends [3.4.2](https://google.github.io/styleguide/javaguide.html#s3.4.2-ordering-class-contents)* + +Class members should be in the following order, in decreasing priority: + +1. Static before non-static +1. Nested classes/interfaces before fields before constructors before methods +1. Public before private +1. Final before non-final + +### Public APIs + +User-facing methods (such as those in Jib Core) should not have types in their signature that are not standard JDK classes. For example, a parameter should take type `List` rather than Guava's `ImmutableList`. + +### Package hierarchy + +Packages should depend on each other without cycles. + +The following is a list of current `jib-core` packages (under `com.google.cloud.tools.jib`) and their immediate dependencies. These can be amended as code changes, but there should not be cyclical dependencies. + +- `api` +- `async` +- `blob` - `filesystem`, `hash`, `image` (cycle - should fix) +- `builder` - `async`, `blob`, `builder`, `cache` `configuration`, `docker`, `event`, `filesystem`, `global`, `http`, `image`, `json`, `registry` +- `cache` - `blob`, `filesystem`, `hash`, `image`, `json` +- `configuration` - `cache`, `filesystem`, `event`, `image`, `registry` +- `docker` - `blob`, `cache`, `image`, `json`, `tar` +- `event` +- `filesystem` +- `frontend` - `configuration`, `event`, `filesystem`, `image`, `registry` +- `global` +- `hash` - `blob`, `image` +- `http` - `blob` +- `image` - `blob`, `configuration` (cycle - should fix - `ImageToJsonTranslator`), `filesystem`, `json`, `tar` +- `json` - `blob` +- `registry` - `blob`, `builder` (cycle - should fix - `RegistryClient`), `configuration` (cycle - should fix - `DockerConfigCredentialRetriever`), `event`, `global`, `http`, `image`, `json` +- `tar` - `blob` From 30dfa106151bd1bdd564347e750f3aeea3a16f03 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 19 Nov 2018 10:26:10 -0500 Subject: [PATCH 0346/2020] Update changelog for auth NPE fix (#1246) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index d49787c994..571cd418c5 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) + ## 0.10.0 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 6e1e86bb07..c51d95b757 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) + ## 0.10.0 ### Added From 57f235dc0048f55497e5cf0f979d9a3699b3c80e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 19 Nov 2018 15:46:54 -0500 Subject: [PATCH 0347/2020] Write out jib-image.id when image is built (#1260) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../cloud/tools/jib/gradle/JibRunHelper.java | 16 +++++++++++----- .../tools/jib/gradle/BuildDockerTask.java | 1 + .../cloud/tools/jib/gradle/BuildImageTask.java | 1 + .../cloud/tools/jib/gradle/BuildTarTask.java | 1 + jib-maven-plugin/CHANGELOG.md | 2 ++ .../cloud/tools/jib/maven/BuildDockerMojo.java | 1 + .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../cloud/tools/jib/maven/BuildTarMojo.java | 1 + .../maven/BuildImageMojoIntegrationTest.java | 16 ++++++++++++++-- .../jib/plugins/common/BuildStepsRunner.java | 18 +++++++++++++++++- 11 files changed, 52 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 571cd418c5..303be7c02b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index f22395ed98..fc7b0bbf03 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -52,7 +52,7 @@ static String buildAndRun( testProject.build( "clean", "jib", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); - assertImageDigest(testProject.getProjectRoot()); + assertImageDigestAndId(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); return pullAndRunBuiltImage(imageReference, extraRunArguments); @@ -68,7 +68,7 @@ static void buildAndRunAdditionalTag( "-D_TARGET_IMAGE=" + imageReference, "-D_ADDITIONAL_TAG=" + additionalTag); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); - assertImageDigest(testProject.getProjectRoot()); + assertImageDigestAndId(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); String additionalImageReference = @@ -92,7 +92,7 @@ static void buildToDockerDaemon( "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); - assertImageDigest(testProject.getProjectRoot()); + assertImageDigestAndId(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); String history = new Command("docker", "history", imageReference).run(); @@ -130,11 +130,17 @@ static void assertCreationTimeEpoch(String imageReference) new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } - static void assertImageDigest(Path projectRoot) throws IOException, DigestException { + static void assertImageDigestAndId(Path projectRoot) throws IOException, DigestException { Path digestPath = projectRoot.resolve("build/jib-image.digest"); Assert.assertTrue(Files.exists(digestPath)); String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); - DescriptorDigest.fromDigest(digest); + DescriptorDigest digest1 = DescriptorDigest.fromDigest(digest); + + Path idPath = projectRoot.resolve("build/jib-image.id"); + Assert.assertTrue(Files.exists(idPath)); + String id = new String(Files.readAllBytes(idPath), StandardCharsets.UTF_8); + DescriptorDigest digest2 = DescriptorDigest.fromDigest(id); + Assert.assertNotEquals(digest1, digest2); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index a7f7cd806f..2bb66186b4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -136,6 +136,7 @@ public void buildDocker() Path buildOutput = getProject().getBuildDir().toPath(); BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 4f08f6a47c..f6c964f555 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -116,6 +116,7 @@ public void buildImage() Path buildOutput = getProject().getBuildDir().toPath(); BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 67aebc7a5b..f65bfefe03 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -133,6 +133,7 @@ public void buildTar() Path buildOutput = getProject().getBuildDir().toPath(); BuildStepsRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index c51d95b757..126a831001 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index e21e77e5fb..926ceb3d5e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -101,6 +101,7 @@ public void execute() throws MojoExecutionException { Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c3154a14a0..d9759c3d24 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -117,6 +117,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index d735f9e943..3548cb648f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -95,6 +95,7 @@ public void execute() throws MojoExecutionException { BuildStepsRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index b884eae60f..1037e558bd 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -84,11 +84,16 @@ static String assertImageDigest(Path projectRoot) throws IOException, DigestExce return DescriptorDigest.fromDigest(digest).toString(); } + static String assertImageId(Path projectRoot) throws IOException, DigestException { + Path idPath = projectRoot.resolve("target/jib-image.id"); + Assert.assertTrue(Files.exists(idPath)); + String id = new String(Files.readAllBytes(idPath), StandardCharsets.UTF_8); + return DescriptorDigest.fromDigest(id).toString(); + } + /** * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. - * - * @throws DigestException */ private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) throws VerificationException, IOException, InterruptedException, DigestException { @@ -116,10 +121,17 @@ private static String buildAndRun(Path projectRoot, String imageReference, boole String output = pullAndRunBuiltImage(imageReference); try { + // Test pulling/running using image digest String digest = assertImageDigest(projectRoot); String imageReferenceWithDigest = ImageReference.parse(imageReference).withTag(digest).toString(); Assert.assertEquals(output, pullAndRunBuiltImage(imageReferenceWithDigest)); + + // Test running using image id + String id = assertImageId(projectRoot); + Assert.assertNotEquals(digest, id); + Assert.assertEquals(output, new Command("docker", "run", "--rm", id).run()); + } catch (InvalidImageReferenceException ex) { throw new AssertionError("error replacing tag with digest"); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 90855ea288..64e1658404 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -160,6 +160,7 @@ private static String capitalizeFirstLetter(String string) { private final String startupMessage; private final String successMessage; @Nullable private Path imageDigestOutputPath; + @Nullable private Path imageIdOutputPath; @VisibleForTesting BuildStepsRunner(String startupMessage, String successMessage) { @@ -213,11 +214,15 @@ public JibContainer build( eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); - // when an image is built, write out the digest + // when an image is built, write out the digest and id if (imageDigestOutputPath != null) { String imageDigest = jibContainer.getDigest().toString(); Files.write(imageDigestOutputPath, imageDigest.getBytes(StandardCharsets.UTF_8)); } + if (imageIdOutputPath != null) { + String imageId = jibContainer.getImageId().toString(); + Files.write(imageIdOutputPath, imageId.getBytes(StandardCharsets.UTF_8)); + } return jibContainer; @@ -288,4 +293,15 @@ public BuildStepsRunner writeImageDigest(@Nullable Path imageDigestOutputPath) { this.imageDigestOutputPath = imageDigestOutputPath; return this; } + + /** + * Set the location where the image id will be saved. If {@code null} then digest is not saved. + * + * @param imageIdOutputPath the location to write the image id or {@code null} to skip + * @return this + */ + public BuildStepsRunner writeImageId(@Nullable Path imageIdOutputPath) { + this.imageIdOutputPath = imageIdOutputPath; + return this; + } } From 8aa0264a20fc54b92c869f35d7603a4c9aa27fdc Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 20 Nov 2018 05:23:07 +0800 Subject: [PATCH 0348/2020] Disables Apache Commons Logging with properties file. (#1224) --- .../java/com/google/cloud/tools/jib/http/Connection.java | 7 ------- jib-core/src/main/resources/commons-logging.properties | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) create mode 100644 jib-core/src/main/resources/commons-logging.properties diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 70802bae60..5602b53622 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -78,13 +78,6 @@ public static Function getInsecureConnectionFactory() return url -> new Connection(url, transport); } - static { - // Disables annoying Apache HTTP client logging. - System.setProperty( - "org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "error"); - } - private HttpRequestFactory requestFactory; @Nullable private HttpResponse httpResponse; diff --git a/jib-core/src/main/resources/commons-logging.properties b/jib-core/src/main/resources/commons-logging.properties new file mode 100644 index 0000000000..463615463a --- /dev/null +++ b/jib-core/src/main/resources/commons-logging.properties @@ -0,0 +1 @@ +org.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog From 882c85b39fcf03abed1235788eb4af73f3d4dd3e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 20 Nov 2018 17:19:06 -0500 Subject: [PATCH 0349/2020] Remove Docker context generator (#1264) --- .../frontend/JavaDockerContextGenerator.java | 357 ------------------ .../JavaDockerContextGeneratorTest.java | 209 ---------- jib-core/src/test/resources/sampleDockerfile | 17 - jib-gradle-plugin/CHANGELOG.md | 2 + .../gradle/MultiProjectIntegrationTest.java | 47 --- .../gradle/SingleProjectIntegrationTest.java | 58 --- .../jib/gradle/WarProjectIntegrationTest.java | 42 --- .../tools/jib/gradle/DockerContextTask.java | 180 --------- .../cloud/tools/jib/gradle/JibPlugin.java | 8 - .../jib/gradle/DockerContextTaskTest.java | 181 --------- .../cloud/tools/jib/gradle/JibPluginTest.java | 32 +- .../cloud/tools/jib/gradle/TestProject.java | 1 - jib-maven-plugin/CHANGELOG.md | 2 + .../tools/jib/maven/DockerContextMojo.java | 129 ------- .../DockerContextMojoIntegrationTest.java | 145 ------- .../jib/maven/DockerContextMojoTest.java | 272 ------------- 16 files changed, 15 insertions(+), 1667 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java delete mode 100644 jib-core/src/test/resources/sampleDockerfile delete mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java deleted file mode 100644 index c8fa25440f..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGenerator.java +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.io.MoreFiles; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.StringJoiner; -import javax.annotation.Nullable; - -/** - * Generates a Docker context for a Java application. - * - *

The image consists of a base image layer and 5 application layers under the directories: - * - *

    - *
  • {@code libs/} (dependency jars) - *
  • {@code snapshot-libs/} (snapshot dependency jars) - *
  • {@code resources/} (resource files) - *
  • {@code classes/} ({@code .class} files) - *
  • {@code root/} (extra files) - *
- * - * Empty application layers are omitted. - */ -public class JavaDockerContextGenerator { - - private static final String DEPENDENCIES_LAYER_DIRECTORY = "libs"; - private static final String SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY = "snapshot-libs"; - private static final String RESOURCES_LAYER_DIRECTORY = "resources"; - private static final String CLASSES_LAYER_DIRECTORY = "classes"; - private static final String EXTRA_FILES_LAYER_DIRECTORY = "root"; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - /** Represents a Dockerfile {@code COPY} directive. */ - private static class CopyDirective { - - /** The layer entries to put into the context. */ - private final ImmutableList layerEntries; - - /** The directory in the context to put the source files for the layer */ - private final String directoryInContext; - - /** The extraction path in the image. */ - private final AbsoluteUnixPath extractionPath; - - private CopyDirective( - ImmutableList layerEntries, - String directoryInContext, - AbsoluteUnixPath extractionPath) { - this.layerEntries = layerEntries; - this.directoryInContext = directoryInContext; - this.extractionPath = extractionPath; - } - } - - /** - * Adds a copy directive for the {@code layerEntries} if it's not empty. - * - * @param listBuilder the {@link ImmutableList.Builder} to add to - * @param layerEntries the layer entries - * @param directoryInContext the directory in the context to put the source files for the layer - */ - private static void addIfNotEmpty( - ImmutableList.Builder listBuilder, - ImmutableList layerEntries, - String directoryInContext) { - if (layerEntries.isEmpty()) { - return; - } - - listBuilder.add(new CopyDirective(layerEntries, directoryInContext, AbsoluteUnixPath.get("/"))); - } - - /** - * Converts a map to a corresponding dockerfile string in the form of: - * - *
{@code
-   * command key1="value1" \
-   *     key2="value2" \
-   *     ...
-   * }
- * - * @param map the map to convert - * @param command the dockerfile command to prefix the map values with - * @return the new dockerfile command as a string - * @throws JsonProcessingException if getting the json string of a map value fails - */ - private static String mapToDockerfileString(Map map, String command) - throws JsonProcessingException { - if (map.isEmpty()) { - return ""; - } - - StringJoiner joiner = new StringJoiner(" \\\n ", "\n" + command + " ", ""); - for (Entry entry : map.entrySet()) { - joiner.add(entry.getKey() + "=" + objectMapper.writeValueAsString(entry.getValue())); - } - return joiner.toString(); - } - - private final ImmutableList copyDirectives; - - @Nullable private String baseImage; - @Nullable private List entrypoint; - @Nullable private List programArguments; - @Nullable private Map environment; - @Nullable private String user; - @Nullable private List exposedPorts; - @Nullable private Map labels; - - /** - * Constructs a Docker context generator for a Java application. - * - * @param javaLayerConfigurations the {@link JavaLayerConfigurations} - */ - public JavaDockerContextGenerator(JavaLayerConfigurations javaLayerConfigurations) { - ImmutableList.Builder copyDirectivesBuilder = ImmutableList.builder(); - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getDependencyLayerEntries(), - DEPENDENCIES_LAYER_DIRECTORY); - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getSnapshotDependencyLayerEntries(), - SNAPSHOT_DEPENDENCIES_LAYER_DIRECTORY); - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getResourceLayerEntries(), - RESOURCES_LAYER_DIRECTORY); - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getClassLayerEntries(), - CLASSES_LAYER_DIRECTORY); - addIfNotEmpty( - copyDirectivesBuilder, - javaLayerConfigurations.getExtraFilesLayerEntries(), - EXTRA_FILES_LAYER_DIRECTORY); - copyDirectives = copyDirectivesBuilder.build(); - } - - /** - * Sets the base image for the {@code FROM} directive. This must be called before {@link - * #generate}. - * - * @param baseImage the base image - * @return this - */ - public JavaDockerContextGenerator setBaseImage(String baseImage) { - this.baseImage = baseImage; - return this; - } - - /** - * Sets the entrypoint to be used as the {@code ENTRYPOINT}. - * - * @param entrypoint the entrypoint - * @return this - */ - public JavaDockerContextGenerator setEntrypoint(@Nullable List entrypoint) { - this.entrypoint = entrypoint; - return this; - } - - /** - * Sets the user for the {@code USER} directive. - * - * @param user the username/UID and optionally the groupname/GID - * @return this - */ - public JavaDockerContextGenerator setUser(@Nullable String user) { - this.user = user; - return this; - } - - /** - * Sets the arguments used in the {@code CMD}. - * - * @param programArguments the list of arguments to append to {@code ENTRYPOINT} - * @return this - */ - public JavaDockerContextGenerator setProgramArguments(@Nullable List programArguments) { - this.programArguments = programArguments; - return this; - } - - /** - * Sets the environment variables. - * - * @param environment map from the environment variable name to value - * @return this - */ - public JavaDockerContextGenerator setEnvironment(@Nullable Map environment) { - this.environment = environment; - return this; - } - - /** - * Sets the exposed ports. - * - * @param exposedPorts the list of port numbers/port ranges to expose - * @return this - */ - public JavaDockerContextGenerator setExposedPorts(@Nullable List exposedPorts) { - this.exposedPorts = exposedPorts; - return this; - } - - /** - * Sets the labels. - * - * @param labels the map of labels - * @return this - */ - public JavaDockerContextGenerator setLabels(@Nullable Map labels) { - this.labels = labels; - return this; - } - - /** - * Creates the Docker context in {@code #targetDirectory}. - * - * @param targetDirectory the directory to generate the Docker context in - * @throws IOException if the export fails - */ - public void generate(Path targetDirectory) throws IOException { - Preconditions.checkNotNull(baseImage); - - // Deletes the targetDir if it exists. - try { - Files.deleteIfExists(targetDirectory); - - } catch (DirectoryNotEmptyException ex) { - MoreFiles.deleteDirectoryContents(targetDirectory); - Files.delete(targetDirectory); - } - - Files.createDirectory(targetDirectory); - - for (CopyDirective copyDirective : copyDirectives) { - // Creates the directories. - Path directoryInContext = targetDirectory.resolve(copyDirective.directoryInContext); - Files.createDirectory(directoryInContext); - - // Copies the source files to the directoryInContext. - for (LayerEntry layerEntry : copyDirective.layerEntries) { - String noLeadingSlash = layerEntry.getExtractionPath().toString().substring(1); - Path destination = directoryInContext.resolve(noLeadingSlash); - - if (Files.isDirectory(layerEntry.getSourceFile())) { - Files.createDirectories(destination); - } else { - Files.createDirectories(destination.getParent()); - Files.copy(layerEntry.getSourceFile(), destination); - } - } - } - - // Creates the Dockerfile. - Files.write( - targetDirectory.resolve("Dockerfile"), makeDockerfile().getBytes(StandardCharsets.UTF_8)); - } - - /** - * Makes the contents of a {@code Dockerfile} using configuration data, in the following format: - * - *
{@code
-   * FROM [base image]
-   *
-   * COPY libs /
-   * COPY snapshot-libs /
-   * COPY resources /
-   * COPY classes /
-   * COPY root /
-   *
-   * EXPOSE [port]
-   * [More EXPOSE instructions, if necessary]
-   * ENV [key1]="[value1]" \
-   *     [key2]="[value2]" \
-   *     [...]
-   * LABEL [key1]="[value1]" \
-   *     [key2]="[value2]" \
-   *     [...]
-   * USER [user name (or UID) and optionally user group (or GID)]
-   * ENTRYPOINT java [jvm flags] -cp [classpaths] [main class]
-   * CMD [main class args]
-   * }
- * - * @return the {@code Dockerfile} contents - */ - @VisibleForTesting - String makeDockerfile() throws JsonProcessingException { - StringBuilder dockerfile = new StringBuilder(); - dockerfile.append("FROM ").append(Preconditions.checkNotNull(baseImage)).append("\n"); - for (CopyDirective copyDirective : copyDirectives) { - boolean hasTrailingSlash = copyDirective.extractionPath.toString().endsWith("/"); - dockerfile - .append("\nCOPY ") - .append(copyDirective.directoryInContext) - .append(" ") - .append(copyDirective.extractionPath) - .append(hasTrailingSlash ? "" : "/"); - } - - dockerfile.append("\n"); - if (exposedPorts != null) { - for (String port : exposedPorts) { - dockerfile.append("\nEXPOSE ").append(port); - } - } - - if (environment != null) { - dockerfile.append(mapToDockerfileString(environment, "ENV")); - } - if (labels != null) { - dockerfile.append(mapToDockerfileString(labels, "LABEL")); - } - if (entrypoint != null) { - dockerfile.append("\nENTRYPOINT ").append(objectMapper.writeValueAsString(entrypoint)); - } - if (programArguments != null) { - dockerfile.append("\nCMD ").append(objectMapper.writeValueAsString(programArguments)); - } - if (user != null) { - dockerfile.append("\nUSER ").append(user); - } - return dockerfile.toString(); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java deleted file mode 100644 index 546e25e978..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaDockerContextGeneratorTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import org.junit.Assert; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link JavaDockerContextGenerator}. */ -@RunWith(MockitoJUnitRunner.class) -public class JavaDockerContextGeneratorTest { - - private static void assertSameFiles(Path directory1, Path directory2) throws IOException { - ImmutableList directory1Files = - new DirectoryWalker(directory1) - .walk() - .stream() - .map(directory1::relativize) - .collect(ImmutableList.toImmutableList()); - ImmutableList directory2Files = - new DirectoryWalker(directory2) - .walk() - .stream() - .map(directory2::relativize) - .collect(ImmutableList.toImmutableList()); - Assert.assertEquals(directory1Files, directory2Files); - } - - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; - - private ImmutableList filesToLayerEntries(Path directory, String extractionRootString) - throws IOException { - AbsoluteUnixPath extractionRoot = AbsoluteUnixPath.get(extractionRootString); - return new DirectoryWalker(directory) - .walk() - .stream() - .map( - sourceFile -> - new LayerEntry( - sourceFile, extractionRoot.resolve(directory.relativize(sourceFile)), null)) - .collect(ImmutableList.toImmutableList()); - } - - @Test - public void testGenerate() throws IOException, URISyntaxException { - Path testDependencies = Paths.get(Resources.getResource("application/dependencies").toURI()); - Path testSnapshotDependencies = - Paths.get(Resources.getResource("application/snapshot-dependencies").toURI()); - Path testResources = Paths.get(Resources.getResource("application/resources").toURI()); - Path testClasses = Paths.get(Resources.getResource("application/classes").toURI()); - Path testExtraFiles = Paths.get(Resources.getResource("layer").toURI()); - - Path targetDirectory = temporaryFolder.newFolder().toPath(); - - /* - * Deletes the directory so that JavaDockerContextGenerator#generate does not throw - * InsecureRecursiveDeleteException. - */ - Files.delete(targetDirectory); - - Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testDependencies, "/app/libs")); - Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn(filesToLayerEntries(testSnapshotDependencies, "/snapshots")); - Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(filesToLayerEntries(testResources, "/more/resources")); - Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn(filesToLayerEntries(testClasses, "/my/classes")); - Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(filesToLayerEntries(testExtraFiles, "/")); - - new JavaDockerContextGenerator(mockJavaLayerConfigurations) - .setBaseImage("somebaseimage") - .generate(targetDirectory); - - Assert.assertTrue(Files.exists(targetDirectory.resolve("Dockerfile"))); - assertSameFiles(targetDirectory.resolve("libs/app/libs"), testDependencies); - assertSameFiles(targetDirectory.resolve("snapshot-libs/snapshots"), testSnapshotDependencies); - assertSameFiles(targetDirectory.resolve("resources/more/resources"), testResources); - assertSameFiles(targetDirectory.resolve("classes/my/classes"), testClasses); - assertSameFiles(targetDirectory.resolve("root"), testExtraFiles); - } - - @Test - public void testMakeDockerfile() throws IOException { - String expectedBaseImage = "somebaseimage"; - List expectedJvmFlags = Arrays.asList("-flag", "another\"Flag"); - String expectedMainClass = "SomeMainClass"; - List expectedProgramArguments = Arrays.asList("arg1", "arg2"); - Map expectedEnv = ImmutableMap.of("key1", "value1", "key2", "value2"); - List exposedPorts = Arrays.asList("1000/tcp", "2000-2010/udp"); - Map expectedLabels = - ImmutableMap.of( - "key1", - "value", - "key2", - "value with\\backslashes\"and\\\\\"\"quotes\"\\", - "key3", - "value3"); - - Path ignored = Paths.get("ignored"); - Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/app/libs"), null))); - Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/snapshots"), null))); - Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/resources"), null))); - Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn( - ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/my/classes/"), null))); - Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(ignored, AbsoluteUnixPath.get("/"), null))); - - String dockerfile = - new JavaDockerContextGenerator(mockJavaLayerConfigurations) - .setBaseImage(expectedBaseImage) - .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get("/app"), expectedJvmFlags, expectedMainClass)) - .setProgramArguments(expectedProgramArguments) - .setEnvironment(expectedEnv) - .setExposedPorts(exposedPorts) - .setLabels(expectedLabels) - .makeDockerfile(); - - // Need to split/rejoin the string here to avoid cross-platform troubles - List sampleDockerfile = - Resources.readLines(Resources.getResource("sampleDockerfile"), StandardCharsets.UTF_8); - Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); - } - - @Test - public void testMakeDockerfileWithWebApp() throws IOException { - String expectedBaseImage = "tomcat:8.5-jre8-alpine"; - AbsoluteUnixPath exepectedAppRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); - - Mockito.when(mockJavaLayerConfigurations.getDependencyLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry( - Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib"), null))); - Mockito.when(mockJavaLayerConfigurations.getSnapshotDependencyLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry( - Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/lib"), null))); - Mockito.when(mockJavaLayerConfigurations.getResourceLayerEntries()) - .thenReturn(ImmutableList.of(new LayerEntry(Paths.get("ignored"), exepectedAppRoot, null))); - Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry( - Paths.get("ignored"), exepectedAppRoot.resolve("WEB-INF/classes"), null))); - Mockito.when(mockJavaLayerConfigurations.getExtraFilesLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/"), null))); - String dockerfile = - new JavaDockerContextGenerator(mockJavaLayerConfigurations) - .setBaseImage(expectedBaseImage) - .setEntrypoint(ImmutableList.of("catalina.sh", "run")) - .makeDockerfile(); - - // Need to split/rejoin the string here to avoid cross-platform troubles - List sampleDockerfile = - Resources.readLines( - Resources.getResource("webAppSampleDockerfile"), StandardCharsets.UTF_8); - Assert.assertEquals(String.join("\n", sampleDockerfile), dockerfile); - } -} diff --git a/jib-core/src/test/resources/sampleDockerfile b/jib-core/src/test/resources/sampleDockerfile deleted file mode 100644 index c2eae1c103..0000000000 --- a/jib-core/src/test/resources/sampleDockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM somebaseimage - -COPY libs / -COPY snapshot-libs / -COPY resources / -COPY classes / -COPY root / - -EXPOSE 1000/tcp -EXPOSE 2000-2010/udp -ENV key1="value1" \ - key2="value2" -LABEL key1="value" \ - key2="value with\\backslashes\"and\\\\\"\"quotes\"\\" \ - key3="value3" -ENTRYPOINT ["java","-flag","another\"Flag","-cp","/app/resources:/app/classes:/app/libs/*","SomeMainClass"] -CMD ["arg1","arg2"] diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 303be7c02b..3b3ff5d715 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed `jibExportDockerContext` task ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) + ### Fixed - NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java deleted file mode 100644 index 297613b9e6..0000000000 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/MultiProjectIntegrationTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.BuildTask; -import org.gradle.testkit.runner.TaskOutcome; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Test; - -/** Integration tests for building multi-project images. */ -public class MultiProjectIntegrationTest { - - @ClassRule - public static final TestProject multiprojectTestProject = new TestProject("multiproject"); - - @Test - public void testMultiProject() { - BuildResult buildResult = - multiprojectTestProject.build("clean", ":a_packaged:jibExportDockerContext", "--info"); - - BuildTask classesTask = buildResult.task(":a_packaged:classes"); - BuildTask jibTask = buildResult.task(":a_packaged:jibExportDockerContext"); - Assert.assertNotNull(classesTask); - Assert.assertEquals(TaskOutcome.SUCCESS, classesTask.getOutcome()); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - Assert.assertThat( - buildResult.getOutput(), CoreMatchers.containsString("Created Docker context at ")); - } -} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 31ee5c0a59..f1b8e191bc 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -20,12 +20,9 @@ import com.google.cloud.tools.jib.IntegrationTestingConfiguration; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; -import java.nio.file.Files; import java.security.DigestException; import java.time.Instant; import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.BuildTask; -import org.gradle.testkit.runner.TaskOutcome; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.hamcrest.CoreMatchers; import org.junit.Assert; @@ -180,61 +177,6 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException, assertDockerInspect(targetImage); } - @Test - public void testDockerContext() throws IOException, InterruptedException { - BuildResult buildResult = simpleTestProject.build("clean", "jibExportDockerContext", "--info"); - - JibRunHelper.assertBuildSuccess( - buildResult, "jibExportDockerContext", "Created Docker context at "); - - String imageName = "jib-gradle-plugin/integration-test" + System.nanoTime(); - new Command( - "docker", - "build", - "-t", - imageName, - simpleTestProject - .getProjectRoot() - .resolve("build") - .resolve("jib-docker-context") - .toString()) - .run(); - - assertDockerInspect(imageName); - String output = new Command("docker", "run", "--rm", imageName).run(); - Assert.assertThat(output, CoreMatchers.startsWith("Hello, world. An argument.\n")); - Assert.assertThat(output, CoreMatchers.endsWith("foo\ncat\n")); - - // Checks that generating the Docker context again is skipped. - BuildTask upToDateJibDockerContextTask = - simpleTestProject.build("jibExportDockerContext").task(":jibExportDockerContext"); - Assert.assertNotNull(upToDateJibDockerContextTask); - Assert.assertEquals(TaskOutcome.UP_TO_DATE, upToDateJibDockerContextTask.getOutcome()); - - // Checks that adding a new file generates the Docker context again. - Files.createFile( - simpleTestProject - .getProjectRoot() - .resolve("src") - .resolve("main") - .resolve("resources") - .resolve("newfile")); - try { - BuildTask reexecutedJibDockerContextTask = - simpleTestProject.build("jibExportDockerContext").task(":jibExportDockerContext"); - Assert.assertNotNull(reexecutedJibDockerContextTask); - Assert.assertEquals(TaskOutcome.SUCCESS, reexecutedJibDockerContextTask.getOutcome()); - - } catch (UnexpectedBuildFailure ex) { - // This might happen on systems without SecureDirectoryStream, so we just ignore it. - // See com.google.common.io.MoreFiles#deleteDirectoryContents. - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "Export Docker context failed because cannot clear directory")); - } - } - @Test public void testBuildTar_simple() throws IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 3aaef472d1..5a093ee96c 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -23,8 +23,6 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.DigestException; import javax.annotation.Nullable; import org.junit.After; @@ -74,28 +72,6 @@ public void testBuild_tomcatServlet25() verifyBuildAndRun(servlet25Project, "war_tomcat_servlet25:gradle", "build-tomcat.gradle"); } - @Test - public void testDockerContext_jettyServlet25() throws IOException, InterruptedException { - String expectedDockerContext = - "FROM gcr.io/distroless/java/jetty\n" - + "\n" - + "COPY libs /\n" - + "COPY resources /\n" - + "COPY classes /"; - verifyDockerContextBuildAndRun(expectedDockerContext, "build.gradle"); - } - - @Test - public void testDockerContext_tomcatServlet25() throws IOException, InterruptedException { - String expectedDockerContext = - "FROM tomcat:8.5-jre8-alpine\n" - + "\n" - + "COPY libs /\n" - + "COPY resources /\n" - + "COPY classes /"; - verifyDockerContextBuildAndRun(expectedDockerContext, "build-tomcat.gradle"); - } - private void verifyBuildAndRun(TestProject project, String label, String gradleBuildFile) throws IOException, InterruptedException, DigestException { String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; @@ -106,22 +82,4 @@ private void verifyBuildAndRun(TestProject project, String label, String gradleB Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); } - - private void verifyDockerContextBuildAndRun(String expectedDockerfile, String gradleBuildFile) - throws IOException, InterruptedException { - servlet25Project.build("clean", "jibExportDockerContext", "-b=" + gradleBuildFile); - - Path dockerContext = - servlet25Project.getProjectRoot().resolve("build").resolve("jib-docker-context"); - Assert.assertTrue(Files.exists(dockerContext)); - String dockerfile = String.join("\n", Files.readAllLines(dockerContext.resolve("Dockerfile"))); - Assert.assertEquals(expectedDockerfile, dockerfile); - - String imageName = "jib/integration-test" + System.nanoTime(); - new Command("docker", "build", "-t", imageName, dockerContext.toString()).run(); - containerName = - new Command("docker", "run", "--rm", "--detach", "-p8080:8080", imageName).run().trim(); - - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java deleted file mode 100644 index fa10f044de..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; -import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; -import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; -import com.google.common.base.Preconditions; -import com.google.common.io.InsecureRecursiveDeleteException; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.List; -import javax.annotation.Nullable; -import org.gradle.api.DefaultTask; -import org.gradle.api.GradleException; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.Nested; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.options.Option; - -public class DockerContextTask extends DefaultTask implements JibTask { - - @Nullable private String targetDir; - @Nullable private JibExtension jibExtension; - - /** - * This will call the property {@code "jib"} so that it is the same name as the extension. This - * way, the user would see error messages for missing configuration with the prefix {@code jib.}. - * - * @return the {@link JibExtension}. - */ - @Nested - @Nullable - public JibExtension getJib() { - return jibExtension; - } - - /** - * @return the input files to this task are all the output files for all the dependencies of the - * {@code classes} task. - */ - @InputFiles - public FileCollection getInputFiles() { - return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPath().toFile(), - getProject()); - } - - /** - * The output directory to check for task up-to-date. - * - * @return the output directory - */ - @OutputDirectory - public String getOutputDirectory() { - return getTargetDir(); - } - - /** - * Returns the output directory for the Docker context. By default, it is {@code - * build/jib-docker-context}. - * - * @return the output directory - */ - @Input - public String getTargetDir() { - if (targetDir == null) { - return getProject().getBuildDir().toPath().resolve("jib-docker-context").toString(); - } - return targetDir; - } - - /** - * The output directory can be overriden with the {@code --jibTargetDir} command line option. - * - * @param targetDir the output directory. - */ - @Option(option = "jibTargetDir", description = "Directory to output the Docker context to") - public void setTargetDir(String targetDir) { - this.targetDir = targetDir; - } - - @TaskAction - public void generateDockerContext() throws MainClassInferenceException { - Preconditions.checkNotNull(jibExtension); - JibSystemProperties.checkHttpTimeoutProperty(); - TaskCommon.disableHttpLogging(); - - if (!jibExtension.getExtraDirectory().getPermissions().isEmpty()) { - getLogger() - .warn( - "'jib.extraDirectory.permissions' configuration is not supported by Jib Docker " - + "context generator - building using Docker may produce unexpected results."); - } - - try { - AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); - - GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); - String targetDir = getTargetDir(); - - List entrypoint = - PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties); - String baseImage = - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); - - // Validate port input, but don't save the output because we don't want the ranges expanded - // here. - ExposedPortsParser.parse(jibExtension.getContainer().getPorts()); - - new JavaDockerContextGenerator(projectProperties.getJavaLayerConfigurations()) - .setBaseImage(baseImage) - .setEntrypoint(entrypoint) - .setProgramArguments(jibExtension.getContainer().getArgs()) - .setExposedPorts(jibExtension.getContainer().getPorts()) - .setEnvironment(jibExtension.getContainer().getEnvironment()) - .setLabels(jibExtension.getContainer().getLabels()) - .setUser(jibExtension.getContainer().getUser()) - .generate(Paths.get(targetDir)); - - getLogger().lifecycle("Created Docker context at " + targetDir); - - } catch (InsecureRecursiveDeleteException ex) { - throw new GradleException( - HelpfulSuggestions.forDockerContextInsecureRecursiveDelete( - "Export Docker context failed because cannot clear directory '" - + getTargetDir() - + "' safely", - getTargetDir()), - ex); - - } catch (IOException ex) { - throw new GradleException( - HelpfulSuggestions.suggest( - "Export Docker context failed", - "check if the command-line option `--jibTargetDir` is set correctly"), - ex); - - } catch (AppRootInvalidException ex) { - throw new GradleException( - "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); - } - } - - @Override - public DockerContextTask setJibExtension(JibExtension jibExtension) { - this.jibExtension = jibExtension; - return this; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index eba9879645..aa9af4f1a6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -39,7 +39,6 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_IMAGE_TASK_NAME = "jib"; @VisibleForTesting static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; - @VisibleForTesting static final String DOCKER_CONTEXT_TASK_NAME = "jibExportDockerContext"; @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; @@ -90,11 +89,6 @@ public void apply(Project project) { .getTasks() .create(BUILD_IMAGE_TASK_NAME, BuildImageTask.class) .setJibExtension(jibExtension); - Task dockerContextTask = - project - .getTasks() - .create(DOCKER_CONTEXT_TASK_NAME, DockerContextTask.class) - .setJibExtension(jibExtension); Task buildDockerTask = project .getTasks() @@ -130,7 +124,6 @@ public void apply(Project project) { dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); } buildImageTask.dependsOn(dependsOnTask); - dockerContextTask.dependsOn(dependsOnTask); buildDockerTask.dependsOn(dependsOnTask); buildTarTask.dependsOn(dependsOnTask); @@ -147,7 +140,6 @@ public void apply(Project project) { Task assembleTask = dependencyProject.getTasks().getByPath(BasePlugin.ASSEMBLE_TASK_NAME); buildImageTask.dependsOn(assembleTask); - dockerContextTask.dependsOn(assembleTask); buildDockerTask.dependsOn(assembleTask); buildTarTask.dependsOn(assembleTask); }); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java deleted file mode 100644 index 5751127b2c..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/DockerContextTaskTest.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import javax.annotation.Nullable; -import org.gradle.api.GradleException; -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Assert; -import org.junit.Before; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for {@link DockerContextTask}. */ -@RunWith(MockitoJUnitRunner.class) -public class DockerContextTaskTest { - - @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); - - @Mock private ContainerParameters containerParameters; - @Mock private BaseImageParameters baseImageParameters; - @Mock private ExtraDirectoryParameters extraDirectoryParameters; - - private DockerContextTask task; - private Project project; - - @Before - public void setUp() throws IOException { - projectRoot.newFolder("build", "jib-exploded-war", "WEB-INF", "lib"); - projectRoot.newFolder("build", "jib-exploded-war", "WEB-INF", "classes"); - projectRoot.newFolder("build", "jib-docker-context"); - - JibExtension jibExtension = Mockito.mock(JibExtension.class); - Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); - Mockito.when(containerParameters.getEnvironment()) - .thenReturn(ImmutableMap.of("envKey", "envVal")); - Mockito.when(containerParameters.getMainClass()).thenReturn("MainClass"); - Mockito.when(containerParameters.getAppRoot()).thenReturn("/app"); - Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); - - Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); - Mockito.when(baseImageParameters.getImage()).thenReturn("base image"); - - Mockito.when(jibExtension.getExtraDirectory()).thenReturn(extraDirectoryParameters); - Mockito.when(extraDirectoryParameters.getPath()) - .thenReturn(projectRoot.newFolder("src", "main", "jib").toPath()); - - project = ProjectBuilder.builder().withProjectDir(projectRoot.getRoot()).build(); - project.getPluginManager().apply("java"); - - task = project.getTasks().create("jibExportDockerContext", DockerContextTask.class); - task.setJibExtension(jibExtension); - } - - @Test - public void testEntrypoint() throws IOException, MainClassInferenceException { - task.generateDockerContext(); - - Assert.assertEquals( - "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]", - getDockerfileLine("ENTRYPOINT")); - } - - @Test - public void testEntrypoint_nonDefaultAppRoot() throws IOException, MainClassInferenceException { - Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); - task.generateDockerContext(); - - Assert.assertEquals( - "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", - getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testEntrypoint_inheritedEntrypoint() throws IOException, MainClassInferenceException { - Mockito.when(containerParameters.getAppRoot()).thenReturn("/"); - Mockito.when(containerParameters.getArgs()).thenCallRealMethod(); - project.getPluginManager().apply("war"); - - task.generateDockerContext(); - - Assert.assertNull(getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testUser() throws IOException, MainClassInferenceException { - Mockito.when(containerParameters.getUser()).thenReturn("tomcat"); - task.generateDockerContext(); - - Assert.assertEquals("USER tomcat", getDockerfileLine("USER")); - } - - @Test - public void testUser_null() throws IOException, MainClassInferenceException { - Mockito.when(containerParameters.getUser()).thenReturn(null); - task.generateDockerContext(); - Assert.assertNull(getDockerfileLine("USER")); - } - - @Test - public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() - throws MainClassInferenceException { - Mockito.when(containerParameters.getAppRoot()).thenReturn("relative/path"); - - try { - task.generateDockerContext(); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: relative/path", ex.getMessage()); - } - } - - @Test - public void testGenerateDockerContext_errorOnWindowsAppRoot() throws MainClassInferenceException { - Mockito.when(containerParameters.getAppRoot()).thenReturn("\\windows\\path"); - - try { - task.generateDockerContext(); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: \\windows\\path", ex.getMessage()); - } - } - - @Test - public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() - throws MainClassInferenceException { - Mockito.when(containerParameters.getAppRoot()).thenReturn("C:\\windows\\path"); - - try { - task.generateDockerContext(); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "container.appRoot is not an absolute Unix-style path: C:\\windows\\path", - ex.getMessage()); - } - } - - @Test - public void testGenerateDockerContext_env() throws IOException, MainClassInferenceException { - task.generateDockerContext(); - Assert.assertEquals("ENV envKey=\"envVal\"", getDockerfileLine("ENV")); - } - - @Nullable - private String getDockerfileLine(String command) throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("build/jib-docker-context/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith(command)).findFirst().orElse(null); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index ffdf955094..060862c50f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -46,7 +46,6 @@ public class JibPluginTest { ImmutableList.of( JibPlugin.BUILD_IMAGE_TASK_NAME, JibPlugin.BUILD_DOCKER_TASK_NAME, - JibPlugin.DOCKER_CONTEXT_TASK_NAME, JibPlugin.BUILD_TAR_TASK_NAME); @Rule public final TemporaryFolder testProjectRoot = new TemporaryFolder(); @@ -140,18 +139,17 @@ public void testProjectDependencyAssembleTasksAreRun() { ((ProjectInternal) rootProject).evaluate(); KNOWN_JIB_TASKS.forEach( - taskName -> { - Assert.assertEquals( - ImmutableSet.of(":sub:assemble", ":classes", ":myCustomTask"), - rootProject - .getTasks() - .getByPath(taskName) - .getDependsOn() - .stream() - .map(Task.class::cast) - .map(Task::getPath) - .collect(Collectors.toSet())); - }); + taskName -> + Assert.assertEquals( + ImmutableSet.of(":sub:assemble", ":classes", ":myCustomTask"), + rootProject + .getTasks() + .getByPath(taskName) + .getDependsOn() + .stream() + .map(Task.class::cast) + .map(Task::getPath) + .collect(Collectors.toSet()))); } @Test @@ -193,14 +191,6 @@ public void testWebAppProject() { .getDependsOn() .iterator() .next()); - Assert.assertEquals( - explodedWarTask, - rootProject - .getTasks() - .getByPath(JibPlugin.DOCKER_CONTEXT_TASK_NAME) - .getDependsOn() - .iterator() - .next()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index 536024bb53..fd3b39fc76 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -39,7 +39,6 @@ private static void copyProject(String projectName, Path destination) throws IOException, URISyntaxException { Path projectPathInResources = Paths.get(Resources.getResource(PROJECTS_PATH_IN_RESOURCES + projectName).toURI()); - // TODO: Consolidate with DockerContextMojo#copyFiles. new DirectoryWalker(projectPathInResources) .filterRoot() .walk( diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 126a831001..fa0332dcd4 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed `jib:exportDockerContext` goal ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) + ### Fixed - NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java deleted file mode 100644 index 6d9c783726..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; -import com.google.cloud.tools.jib.frontend.JavaDockerContextGenerator; -import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; -import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.io.InsecureRecursiveDeleteException; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.List; -import javax.annotation.Nullable; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.plugins.annotations.ResolutionScope; - -/** Exports to a Docker context. */ -@Mojo( - name = DockerContextMojo.GOAL_NAME, - requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) -public class DockerContextMojo extends JibPluginConfiguration { - - @VisibleForTesting static final String GOAL_NAME = "exportDockerContext"; - - @Nullable - @Parameter( - property = "jibTargetDir", - defaultValue = "${project.build.directory}/jib-docker-context", - required = true) - @VisibleForTesting - String targetDir; - - @Override - public void execute() throws MojoExecutionException { - Preconditions.checkNotNull(targetDir); - - if (isSkipped()) { - getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); - return; - } - if ("pom".equals(getProject().getPackaging())) { - getLog().info("Skipping containerization because packaging is 'pom'..."); - return; - } - - try { - JibSystemProperties.checkHttpTimeoutProperty(); - AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); - - MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject( - getProject(), - getLog(), - MojoCommon.getExtraDirectoryPath(this), - MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); - DefaultEventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); - RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); - - List entrypoint = - PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties); - String baseImage = - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); - - // Validate port input, but don't save the output because we don't want the ranges expanded - // here. - ExposedPortsParser.parse(getExposedPorts()); - - new JavaDockerContextGenerator(projectProperties.getJavaLayerConfigurations()) - .setBaseImage(baseImage) - .setEntrypoint(entrypoint) - .setProgramArguments(getArgs()) - .setExposedPorts(getExposedPorts()) - .setEnvironment(getEnvironment()) - .setLabels(getLabels()) - .setUser(getUser()) - .generate(Paths.get(targetDir)); - - getLog().info("Created Docker context at " + targetDir); - - } catch (InsecureRecursiveDeleteException ex) { - throw new MojoExecutionException( - HelpfulSuggestions.forDockerContextInsecureRecursiveDelete( - "Export Docker context failed because cannot clear directory '" - + targetDir - + "' safely", - targetDir), - ex); - - } catch (IOException ex) { - throw new MojoExecutionException( - HelpfulSuggestions.suggest( - "Export Docker context failed", "check if `targetDir` is set correctly"), - ex); - - } catch (AppRootInvalidException ex) { - throw new MojoExecutionException( - " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); - - } catch (MainClassInferenceException ex) { - throw new MojoExecutionException(ex.getMessage(), ex); - } - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java deleted file mode 100644 index 2a3849a1f4..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoIntegrationTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.Command; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import javax.annotation.Nullable; -import org.apache.maven.it.VerificationException; -import org.apache.maven.it.Verifier; -import org.hamcrest.CoreMatchers; -import org.junit.After; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -/** Integration tests for {@link DockerContextMojo}. */ -@RunWith(MockitoJUnitRunner.class) -public class DockerContextMojoIntegrationTest { - - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); - - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); - - @ClassRule - public static final TestProject servlet25Project = new TestProject(testPlugin, "war_servlet25"); - - @Nullable private String detachedContainerName; - - @After - public void tearDown() throws IOException, InterruptedException { - if (detachedContainerName != null) { - new Command("docker", "stop", detachedContainerName).run(); - } - } - - @Test - public void testExecute() throws VerificationException, IOException, InterruptedException { - Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - verifier.setAutoclean(false); - verifier.executeGoal("compile"); - verifier.executeGoal("jib:" + DockerContextMojo.GOAL_NAME); - - Path dockerContextDirectory = - simpleTestProject.getProjectRoot().resolve("target").resolve("jib-docker-context"); - Assert.assertTrue(Files.exists(dockerContextDirectory)); - - String imageName = "jib/integration-test" + System.nanoTime(); - new Command("docker", "build", "-t", imageName, dockerContextDirectory.toString()).run(); - String dockerInspect = new Command("docker", "inspect", imageName).run(); - Assert.assertThat( - dockerInspect, - CoreMatchers.containsString( - " \"ExposedPorts\": {\n" - + " \"1000/tcp\": {},\n" - + " \"2000/udp\": {},\n" - + " \"2001/udp\": {},\n" - + " \"2002/udp\": {},\n" - + " \"2003/udp\": {}")); - Assert.assertThat( - dockerInspect, - CoreMatchers.containsString( - " \"Labels\": {\n" - + " \"key1\": \"value1\",\n" - + " \"key2\": \"value2\"\n" - + " }")); - - String output = new Command("docker", "run", "--rm", imageName).run(); - Assert.assertThat(output, CoreMatchers.startsWith("Hello, world. An argument.\n")); - Assert.assertThat(output, CoreMatchers.endsWith("foo\ncat\n")); - } - - @Test - public void testExecute_skipJibGoal() throws VerificationException, IOException { - SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildDockerMojo.GOAL_NAME); - } - - @Test - public void testExecute_jettyServlet25() - throws VerificationException, IOException, InterruptedException { - String expectedDockerfile = - "FROM gcr.io/distroless/java/jetty\n" - + "\n" - + "COPY libs /\n" - + "COPY resources /\n" - + "COPY classes /"; - verifyWarBuildAndRun(expectedDockerfile, "pom.xml"); - } - - @Test - public void testExecute_tomcatServlet25() - throws VerificationException, IOException, InterruptedException { - String expectedDockerfile = - "FROM tomcat:8.5-jre8-alpine\n" - + "\n" - + "COPY libs /\n" - + "COPY resources /\n" - + "COPY classes /"; - verifyWarBuildAndRun(expectedDockerfile, "pom-tomcat.xml"); - } - - private void verifyWarBuildAndRun(String expectedDockerfile, String pomXml) - throws VerificationException, IOException, InterruptedException { - Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); - verifier.setAutoclean(false); - verifier.addCliOption("--file=" + pomXml); - verifier.executeGoals(Arrays.asList("clean", "package", "jib:exportDockerContext")); - - Path dockerContext = - servlet25Project.getProjectRoot().resolve("target").resolve("jib-docker-context"); - Assert.assertTrue(Files.exists(dockerContext)); - String dockerfile = String.join("\n", Files.readAllLines(dockerContext.resolve("Dockerfile"))); - Assert.assertEquals(expectedDockerfile, dockerfile); - - String imageName = "jib/integration-test" + System.nanoTime(); - new Command("docker", "build", "-t", imageName, dockerContext.toString()).run(); - detachedContainerName = - new Command("docker", "run", "--rm", "--detach", "-p8080:8080", imageName).run().trim(); - - HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java deleted file mode 100644 index c7d078e6e1..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DockerContextMojoTest.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; -import org.apache.maven.execution.MavenSession; -import org.apache.maven.model.Build; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.project.MavenProject; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.junit.Assert; -import org.junit.Before; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link DockerContextMojo}. */ -@RunWith(MockitoJUnitRunner.class) -public class DockerContextMojoTest { - - @Rule public final TemporaryFolder projectRoot = new TemporaryFolder(); - - @Mock private MavenProject project; - @Mock private Build build; - - private DockerContextMojo mojo; - private String appRoot = "/app"; - private File outputFolder; - - @Before - public void setUp() throws IOException { - outputFolder = projectRoot.newFolder("target"); - Mockito.when(project.getBuild()).thenReturn(build); - Mockito.when(project.getBasedir()).thenReturn(projectRoot.getRoot()); - Mockito.when(build.getOutputDirectory()).thenReturn(outputFolder.toString()); - - mojo = new BaseDockerContextMojo(); - mojo.targetDir = outputFolder.toString(); - } - - @Test - public void testEntrypoint() throws MojoExecutionException, IOException { - mojo.execute(); - - Assert.assertEquals( - "ENTRYPOINT [\"java\",\"-cp\",\"/app/resources:/app/classes:/app/libs/*\",\"MainClass\"]", - getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testEntrypoint_nonDefaultAppRoot() throws MojoExecutionException, IOException { - appRoot = "/"; - mojo.execute(); - - Assert.assertEquals( - "ENTRYPOINT [\"java\",\"-cp\",\"/resources:/classes:/libs/*\",\"MainClass\"]", - getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testGenerateDockerContext_errorOnNonAbsoluteAppRoot() { - appRoot = "relative/path"; - try { - mojo.execute(); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: relative/path", - ex.getMessage()); - } - } - - @Test - public void testGenerateDockerContext_errorOnWindowsAppRoot() { - appRoot = "\\windows\\path"; - try { - mojo.execute(); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: \\windows\\path", - ex.getMessage()); - } - } - - @Test - public void testGenerateDockerContext_errorOnWindowsAppRootWithDriveLetter() { - appRoot = "C:\\windows\\path"; - try { - mojo.execute(); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals( - " is not an absolute Unix-style path: C:\\windows\\path", - ex.getMessage()); - } - } - - @Test - public void testGeneratedDockerContext_env() throws MojoExecutionException, IOException { - mojo.execute(); - Assert.assertEquals("ENV envKey=\"envVal\"", getDockerfileLine("ENV")); - } - - public void testBaseImage_nonWarPackaging() throws MojoExecutionException, IOException { - mojo.execute(); - - Assert.assertEquals("FROM gcr.io/distroless/java", getDockerfileLine("FROM")); - } - - @Test - public void testBaseImage_warPackaging() throws MojoExecutionException, IOException { - Mockito.doReturn("war").when(project).getPackaging(); - Mockito.doReturn("final-name").when(build).getFinalName(); - projectRoot.newFolder("final-name", "WEB-INF", "lib"); - projectRoot.newFolder("final-name", "WEB-INF", "classes"); - Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); - mojo.execute(); - - Assert.assertEquals("FROM gcr.io/distroless/java/jetty", getDockerfileLine("FROM")); - } - - @Test - public void testBaseImage_nonDefault() throws MojoExecutionException, IOException { - Mockito.doReturn("war").when(project).getPackaging(); - Mockito.doReturn("final-name").when(build).getFinalName(); - mojo = - new BaseDockerContextMojo() { - @Override - String getBaseImage() { - return "tomcat:8.5-jre8-alpine"; - } - }; - mojo.targetDir = outputFolder.toString(); - - projectRoot.newFolder("final-name", "WEB-INF", "lib"); - projectRoot.newFolder("final-name", "WEB-INF", "classes"); - Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); - mojo.execute(); - - Assert.assertEquals("FROM tomcat:8.5-jre8-alpine", getDockerfileLine("FROM")); - } - - @Test - public void testEntrypoint_defaultWarPackaging() throws MojoExecutionException, IOException { - Mockito.doReturn("war").when(project).getPackaging(); - Mockito.doReturn("final-name").when(build).getFinalName(); - projectRoot.newFolder("final-name", "WEB-INF", "lib"); - projectRoot.newFolder("final-name", "WEB-INF", "classes"); - Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); - mojo.execute(); - - Assert.assertNull(getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testEntrypoint_warPackaging() throws MojoExecutionException, IOException { - Mockito.doReturn("war").when(project).getPackaging(); - Mockito.doReturn("final-name").when(build).getFinalName(); - projectRoot.newFolder("final-name", "WEB-INF", "lib"); - projectRoot.newFolder("final-name", "WEB-INF", "classes"); - Mockito.doReturn(projectRoot.getRoot().toString()).when(build).getDirectory(); - mojo = - new BaseDockerContextMojo() { - @Override - List getEntrypoint() { - return ImmutableList.of("catalina.sh", "run"); - } - }; - mojo.targetDir = outputFolder.toString(); - mojo.execute(); - Assert.assertEquals("ENTRYPOINT [\"catalina.sh\",\"run\"]", getDockerfileLine("ENTRYPOINT")); - Assert.assertNull(getDockerfileLine("CMD")); - } - - @Test - public void testUser() throws IOException, MojoExecutionException { - mojo = - new BaseDockerContextMojo() { - @Override - String getUser() { - return "tomcat"; - } - }; - mojo.targetDir = outputFolder.toString(); - mojo.execute(); - Assert.assertEquals("USER tomcat", getDockerfileLine("USER")); - } - - @Test - public void testUser_null() throws IOException, MojoExecutionException { - mojo = new BaseDockerContextMojo(); - mojo.targetDir = outputFolder.toString(); - mojo.execute(); - Assert.assertNull(getDockerfileLine("USER")); - } - - private class BaseDockerContextMojo extends DockerContextMojo { - - @Override - MavenProject getProject() { - return project; - } - - @Override - Optional getExtraDirectoryPath() { - return Optional.of(projectRoot.getRoot().toPath()); - } - - @Override - String getMainClass() { - return "MainClass"; - } - - @Override - String getAppRoot() { - return appRoot; - } - - @Override - Map getEnvironment() { - return ImmutableMap.of("envKey", "envVal"); - } - - @Override - MavenSession getSession() { - return Mockito.mock(MavenSession.class); - } - - @Override - SettingsDecrypter getSettingsDecrypter() { - return Mockito.mock(SettingsDecrypter.class); - } - } - - @Nullable - private String getDockerfileLine(String command) throws IOException { - Path dockerfile = projectRoot.getRoot().toPath().resolve("target/Dockerfile"); - List lines = Files.readAllLines(dockerfile); - return lines.stream().filter(line -> line.startsWith(command)).findFirst().orElse(null); - } -} From 3166426a1dd16b68b5b14d636bbb119fe197ad4c Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 21 Nov 2018 14:27:17 -0500 Subject: [PATCH 0350/2020] Fix FileOperationsTest since tryLock() can return null (#1269) --- .../cloud/tools/jib/filesystem/FileOperationsTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java index 3992971c11..ac751a9ecd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java @@ -26,6 +26,7 @@ import java.net.URISyntaxException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -44,9 +45,10 @@ private static void verifyWriteWithLock(Path file) throws IOException { OutputStream fileOutputStream = FileOperations.newLockingOutputStream(file); // Checks that the file was locked. - try { - FileChannel.open(file, StandardOpenOption.READ).tryLock(0, Long.MAX_VALUE, true); - Assert.fail("Lock attempt should have failed"); + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { + // locking should either fail and return null or throw an OverlappingFileLockException + FileLock lock = channel.tryLock(0, Long.MAX_VALUE, true); + Assert.assertNull("Lock attempt should have failed", lock); } catch (OverlappingFileLockException ex) { // pass From d3418c1504d6ce8fbd98e120f929d2ccc20072dd Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 26 Nov 2018 10:22:21 -0500 Subject: [PATCH 0351/2020] Allow inheriting ENTRYPOINT and CMD with INHERIT keyword (#1263) * Allow inheriting ENTRYPOINT and CMD with INHERIT * Allow String type for container.entrypoint --- jib-gradle-plugin/CHANGELOG.md | 1 + .../tools/jib/gradle/ContainerParameters.java | 4 ++++ .../cloud/tools/jib/gradle/JibExtension.java | 14 ++++++------- jib-maven-plugin/CHANGELOG.md | 1 + .../common/PluginConfigurationProcessor.java | 5 +++++ .../PluginConfigurationProcessorTest.java | 20 +++++++++++++++++++ 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3b3ff5d715..303ff8dd50 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) +- `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. ### Changed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 3dd4968989..1d6e3ccfce 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -73,6 +73,10 @@ public void setEntrypoint(List entrypoint) { this.entrypoint = entrypoint; } + public void setEntrypoint(String entrypoint) { + this.entrypoint = Collections.singletonList(entrypoint); + } + @Input @Optional public List getJvmFlags() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index b7c4cfc6ac..107f22c383 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -34,20 +34,20 @@ *
{@code
  * jib {
  *   from {
- *     image = ‘gcr.io/my-gcp-project/my-base-image’
- *     credHelper = ‘gcr’
+ *     image = 'gcr.io/my-gcp-project/my-base-image'
+ *     credHelper = 'gcr'
  *   }
  *   to {
- *     image = ‘gcr.io/gcp-project/my-app:built-with-jib’
- *     credHelper = ‘ecr-login’
+ *     image = 'gcr.io/gcp-project/my-app:built-with-jib'
+ *     credHelper = 'ecr-login'
  *   }
  *   container {
- *     jvmFlags = [‘-Xms512m’, ‘-Xdebug’]
- *     mainClass = ‘com.mycompany.myproject.Main’
+ *     jvmFlags = ['-Xms512m', '-Xdebug']
+ *     mainClass = 'com.mycompany.myproject.Main'
  *     args = ['arg1', 'arg2']
  *     exposedPorts = ['1000', '2000-2010', '3000']
  *     format = OCI
- *     appRoot = "/app";
+ *     appRoot = '/app'
  *   }
  *   extraDirectory {
  *     path = file('path/to/extra/dir')
diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md
index fa0332dcd4..038c2e631f 100644
--- a/jib-maven-plugin/CHANGELOG.md
+++ b/jib-maven-plugin/CHANGELOG.md
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
 ### Added
 
 - Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204))
+- `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``.
 
 ### Changed
 
diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java
index e7fa89f869..6c02726c4a 100644
--- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java
+++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java
@@ -62,6 +62,7 @@ private interface InferredAuthProvider {
    * Compute the container entrypoint, in this order:
    *
    * 
    + *
  1. null (inheriting from the base image), if the user specified value is {@code INHERIT} *
  2. the user specified one, if set *
  3. for a WAR project, null (it must be inherited from base image) *
  4. for a non-WAR project, by resolving the main class @@ -85,6 +86,10 @@ public static List computeEntrypoint( .dispatch( LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); } + + if (rawEntrypoint.get().size() == 1 && "INHERIT".equals(rawEntrypoint.get().get(0))) { + return null; + } return rawEntrypoint.get(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 5ad55ff080..d1b374bf5e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -142,6 +142,26 @@ public void testEntrypoint() Mockito.verifyZeroInteractions(logger); } + @Test + public void testComputeEntrypoint_inheritKeyword() + throws MainClassInferenceException, AppRootInvalidException { + Mockito.when(rawConfiguration.getEntrypoint()) + .thenReturn(Optional.of(Collections.singletonList("INHERIT"))); + + Assert.assertNull( + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties)); + } + + @Test + public void testComputeEntrypoint_inheritKeywordInNonSingletonList() + throws MainClassInferenceException, AppRootInvalidException { + Mockito.when(rawConfiguration.getEntrypoint()) + .thenReturn(Optional.of(Arrays.asList("INHERIT", ""))); + + Assert.assertNotNull( + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties)); + } + @Test public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, From 7fe4b4bec93fbe48b7a963f9ea1e4607e7f0dfb4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 27 Nov 2018 18:25:12 -0500 Subject: [PATCH 0352/2020] Allow configuring working directory (#1266) * Allow configuring WorkDir * Validate workingDirectory * Add integration tests for WorkingDir --- .../tools/jib/api/JibContainerBuilder.java | 11 +++ .../jib/builder/steps/BuildImageStep.java | 3 + .../configuration/ContainerConfiguration.java | 28 +++++- .../jib/api/JibContainerBuilderTest.java | 6 +- .../jib/builder/steps/BuildImageStepTest.java | 21 +++++ .../ContainerConfigurationTest.java | 8 ++ jib-gradle-plugin/CHANGELOG.md | 1 + .../gradle/SingleProjectIntegrationTest.java | 14 +++ .../resources/projects/simple/build.gradle | 1 + .../tools/jib/gradle/BuildDockerTask.java | 12 ++- .../tools/jib/gradle/BuildImageTask.java | 12 ++- .../cloud/tools/jib/gradle/BuildTarTask.java | 12 ++- .../tools/jib/gradle/ContainerParameters.java | 15 ++++ .../jib/gradle/GradleRawConfiguration.java | 5 ++ .../cloud/tools/jib/gradle/TaskCommon.java | 8 +- jib-maven-plugin/CHANGELOG.md | 3 +- .../tools/jib/maven/BuildDockerMojo.java | 13 ++- .../cloud/tools/jib/maven/BuildImageMojo.java | 13 ++- .../cloud/tools/jib/maven/BuildTarMojo.java | 13 ++- .../jib/maven/JibPluginConfiguration.java | 15 ++++ .../jib/maven/MavenRawConfiguration.java | 5 ++ .../cloud/tools/jib/maven/MojoCommon.java | 8 +- .../maven/BuildImageMojoIntegrationTest.java | 13 +++ .../jib/maven/JibPluginConfigurationTest.java | 4 + .../test/resources/projects/simple/pom.xml | 1 + ...tion.java => InvalidAppRootException.java} | 8 +- .../InvalidWorkingDirectoryException.java | 35 ++++++++ .../common/PluginConfigurationProcessor.java | 42 ++++++--- .../jib/plugins/common/PropertyNames.java | 1 + .../jib/plugins/common/RawConfiguration.java | 2 + .../PluginConfigurationProcessorTest.java | 87 ++++++++++++++----- 31 files changed, 352 insertions(+), 68 deletions(-) rename jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/{AppRootInvalidException.java => InvalidAppRootException.java} (76%) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidWorkingDirectoryException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 8aa5c3120c..e4dbef7b2a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -390,6 +390,17 @@ public JibContainerBuilder setUser(@Nullable String user) { return this; } + /** + * Sets the working directory in the container. + * + * @param workingDirectory the working directory + * @return this + */ + public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workingDirectory) { + containerConfigurationBuilder.setWorkingDirectory(workingDirectory); + return this; + } + /** * Builds the container. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 449316d20d..47b036ee43 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -159,6 +159,9 @@ private Image afterCachedLayerSteps() .setExposedPorts(containerConfiguration.getExposedPorts()) .setVolumes(containerConfiguration.getVolumes()) .addLabels(containerConfiguration.getLabels()); + if (containerConfiguration.getWorkingDirectory() != null) { + imageBuilder.setWorkingDirectory(containerConfiguration.getWorkingDirectory().toString()); + } } // Gets the container configuration content descriptor. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 460d697e0b..4f486235ac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -47,6 +47,7 @@ public static class Builder { @Nullable private List volumes; @Nullable private Map labels; @Nullable private String user; + @Nullable private AbsoluteUnixPath workingDirectory; /** * Sets the image creation time. @@ -198,6 +199,17 @@ public Builder setUser(@Nullable String user) { return this; } + /** + * Sets the working directory in the container. + * + * @param workingDirectory the working directory + * @return this + */ + public Builder setWorkingDirectory(@Nullable AbsoluteUnixPath workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + /** * Builds the {@link ContainerConfiguration}. * @@ -212,7 +224,8 @@ public ContainerConfiguration build() { exposedPorts == null ? null : ImmutableList.copyOf(exposedPorts), volumes == null ? null : ImmutableList.copyOf(volumes), labels == null ? null : ImmutableMap.copyOf(labels), - user); + user, + workingDirectory); } private Builder() {} @@ -235,6 +248,7 @@ public static Builder builder() { @Nullable private final ImmutableList volumes; @Nullable private final ImmutableMap labels; @Nullable private final String user; + @Nullable private final AbsoluteUnixPath workingDirectory; private ContainerConfiguration( Instant creationTime, @@ -244,7 +258,8 @@ private ContainerConfiguration( @Nullable ImmutableList exposedPorts, @Nullable ImmutableList volumes, @Nullable ImmutableMap labels, - @Nullable String user) { + @Nullable String user, + @Nullable AbsoluteUnixPath workingDirectory) { this.creationTime = creationTime; this.entrypoint = entrypoint; this.programArguments = programArguments; @@ -253,6 +268,7 @@ private ContainerConfiguration( this.volumes = volumes; this.labels = labels; this.user = user; + this.workingDirectory = workingDirectory; } public Instant getCreationTime() { @@ -294,6 +310,11 @@ public ImmutableMap getLabels() { return labels; } + @Nullable + public AbsoluteUnixPath getWorkingDirectory() { + return workingDirectory; + } + @Override @VisibleForTesting public boolean equals(Object other) { @@ -310,7 +331,8 @@ public boolean equals(Object other) { && Objects.equals(environmentMap, otherContainerConfiguration.environmentMap) && Objects.equals(exposedPorts, otherContainerConfiguration.exposedPorts) && Objects.equals(labels, otherContainerConfiguration.labels) - && Objects.equals(user, otherContainerConfiguration.user); + && Objects.equals(user, otherContainerConfiguration.user) + && Objects.equals(workingDirectory, otherContainerConfiguration.workingDirectory); } @Override diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 496cb9a7d7..3c9840246f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -69,7 +70,8 @@ public void testToBuildConfiguration_containerConfigurationSet() .setLabels(ImmutableMap.of("key", "value")) .setProgramArguments(Arrays.asList("program", "arguments")) .setCreationTime(Instant.ofEpochMilli(1000)) - .setUser("user"); + .setUser("user") + .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(Containerizer.to(baseImage)); @@ -84,6 +86,8 @@ public void testToBuildConfiguration_containerConfigurationSet() Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); Assert.assertEquals(Instant.ofEpochMilli(1000), containerConfiguration.getCreationTime()); Assert.assertEquals("user", containerConfiguration.getUser()); + Assert.assertEquals( + AbsoluteUnixPath.get("/working/directory"), containerConfiguration.getWorkingDirectory()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 96e1231021..d7608c973c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -224,6 +225,26 @@ public void test_propagateBaseImageConfiguration() Assert.assertEquals(ImmutableList.of(), image.getProgramArguments()); } + @Test + public void testOverrideWorkingDirectory() throws InterruptedException, ExecutionException { + Mockito.when(mockContainerConfiguration.getWorkingDirectory()) + .thenReturn(AbsoluteUnixPath.get("/my/directory")); + + BuildImageStep buildImageStep = + new BuildImageStep( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + mockBuildConfiguration, + mockPullBaseImageStep, + mockPullAndCacheBaseImageLayersStep, + ImmutableList.of( + mockBuildAndCacheApplicationLayerStepDependencies, + mockBuildAndCacheApplicationLayerStepResources, + mockBuildAndCacheApplicationLayerStepClasses)); + Image image = buildImageStep.getFuture().get().getFuture().get(); + + Assert.assertEquals("/my/directory", image.getWorkingDirectory()); + } + @Test public void test_inheritedEntrypoint() throws ExecutionException, InterruptedException { Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index c02368430c..c347d7dc1a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.HashMap; @@ -104,4 +105,11 @@ public void testBuilder_user() { ContainerConfiguration configuration = ContainerConfiguration.builder().setUser("john").build(); Assert.assertEquals("john", configuration.getUser()); } + + @Test + public void testBuilder_workingDirectory() { + ContainerConfiguration configuration = + ContainerConfiguration.builder().setWorkingDirectory(AbsoluteUnixPath.get("/path")).build(); + Assert.assertEquals(AbsoluteUnixPath.get("/path"), configuration.getWorkingDirectory()); + } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 303ff8dd50..431934d723 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. +- `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index f1b8e191bc..b9a5ca56b1 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -61,6 +61,15 @@ private static void assertSimpleCreationTimeIsAfter(Instant before, String image Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); } + private static void assertWorkingDirectory(String expected, String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + expected, + new Command("docker", "inspect", "-f", "{{.Config.WorkingDir}}", imageReference) + .run() + .trim()); + } + /** * Asserts that the test project has the required exposed ports and labels. * @@ -144,6 +153,7 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE JibRunHelper.buildAndRun(simpleTestProject, targetImage)); assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + assertWorkingDirectory("/home", targetImage); } @Test @@ -154,6 +164,7 @@ public void testBuild_complex() throws IOException, InterruptedException { "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + assertWorkingDirectory("", targetImage); } @Test @@ -164,6 +175,7 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + assertWorkingDirectory("", targetImage); } @Test @@ -175,6 +187,7 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException, JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage)); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertDockerInspect(targetImage); + assertWorkingDirectory("/home", targetImage); } @Test @@ -196,5 +209,6 @@ public void testBuildTar_simple() throws IOException, InterruptedException { new Command("docker", "run", "--rm", targetImage).run()); assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + assertWorkingDirectory("/home", targetImage); } } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 2cf938a3c2..ea5d74319e 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -24,6 +24,7 @@ jib { args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] + workingDirectory = '/home' } extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2bb66186b4..fc999030b1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -22,11 +22,12 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -144,9 +145,14 @@ public void buildDocker() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new GradleException( - "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index f6c964f555..6c13528a69 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -21,11 +21,12 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -124,9 +125,14 @@ public void buildImage() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new GradleException( - "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index f65bfefe03..0a7ae154e9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -20,11 +20,12 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -141,9 +142,14 @@ public void buildTar() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new GradleException( - "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new GradleException( + "container.workingDirectory is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 1d6e3ccfce..2bef23d70d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -44,6 +44,7 @@ public class ContainerParameters { private Map labels = Collections.emptyMap(); private String appRoot = ""; @Nullable private String user; + @Nullable private String workingDirectory; @Input @Optional @@ -201,4 +202,18 @@ public String getUser() { public void setUser(String user) { this.user = user; } + + @Input + @Nullable + @Optional + public String getWorkingDirectory() { + if (System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY) != null) { + return System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY); + } + return workingDirectory; + } + + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 4431b3a8b6..f023363508 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -127,6 +127,11 @@ public Optional getUser() { return Optional.ofNullable(jibExtension.getContainer().getUser()); } + @Override + public Optional getWorkingDirectory() { + return Optional.ofNullable(jibExtension.getContainer().getWorkingDirectory()); + } + @Override public boolean getUseCurrentTimestamp() { return jibExtension.getContainer().getUseCurrentTimestamp(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 44b7fa996e..0e58ec1a38 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -19,7 +19,7 @@ import com.google.api.client.http.HttpTransport; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import java.util.logging.Level; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -37,11 +37,11 @@ class TaskCommon { * * @param jibExtension the {@link JibExtension} providing the configuration data * @return the app root value - * @throws AppRootInvalidException if the app root is not an absolute path in Unix-style + * @throws InvalidAppRootException if the app root is not an absolute path in Unix-style */ // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension, Project project) - throws AppRootInvalidException { + throws InvalidAppRootException { String appRoot = jibExtension.getContainer().getAppRoot(); if (appRoot.isEmpty()) { appRoot = @@ -52,7 +52,7 @@ static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension, Project pro try { return AbsoluteUnixPath.get(appRoot); } catch (IllegalArgumentException ex) { - throw new AppRootInvalidException(appRoot, appRoot, ex); + throw new InvalidAppRootException(appRoot, appRoot, ex); } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 038c2e631f..49fca3e6a0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. +- `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) ### Changed @@ -22,7 +23,7 @@ All notable changes to this project will be documented in this file. - Properties for each configuration parameter, allowing any parameter to be set via commandline ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) - `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) -- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- `` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) - Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) - `` object with a `` and `` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `` configures the extra layer directory (still also configurable via `...`) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 926ceb3d5e..4a382e3274 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -23,11 +23,12 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -110,9 +111,15 @@ public void execute() throws MojoExecutionException { helpfulSuggestions); getLog().info(""); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new MojoExecutionException( - " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), + ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } catch (InvalidImageReferenceException | IOException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index d9759c3d24..469fd22845 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -23,11 +23,12 @@ import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -126,9 +127,15 @@ public void execute() throws MojoExecutionException, MojoFailureException { helpfulSuggestions); getLog().info(""); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new MojoExecutionException( - " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), + ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } catch (InvalidImageReferenceException | IOException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 3548cb648f..5a81261776 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -21,11 +21,12 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -104,9 +105,15 @@ public void execute() throws MojoExecutionException { helpfulSuggestions); getLog().info(""); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { throw new MojoExecutionException( - " is not an absolute Unix-style path: " + ex.getInvalidAppRoot()); + " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), + ex); + } catch (InvalidWorkingDirectoryException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + + ex.getInvalidPathValue(), + ex); } catch (InvalidImageReferenceException | IOException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 4cfe21f399..7b0da4e4ad 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -151,6 +151,8 @@ public static class ContainerParameters { @Parameter private String appRoot = ""; @Nullable @Parameter private String user; + + @Nullable @Parameter private String workingDirectory; } /** Configuration for the {@code extraDirectory} parameter. */ @@ -375,6 +377,19 @@ String getUser() { return container.user; } + /** + * Gets the working directory in the container. + * + * @return the working directory + */ + @Nullable + String getWorkingDirectory() { + if (System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY) != null) { + return System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY); + } + return container.workingDirectory; + } + /** * Gets the configured main arguments. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index af93c95cbf..265dda0972 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -142,6 +142,11 @@ public Optional getUser() { return Optional.ofNullable(jibPluginConfiguration.getUser()); } + @Override + public Optional getWorkingDirectory() { + return Optional.ofNullable(jibPluginConfiguration.getWorkingDirectory()); + } + @Override public boolean getUseCurrentTimestamp() { return jibPluginConfiguration.getUseCurrentTimestamp(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 7b15a45a59..4d332c1c86 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; -import com.google.cloud.tools.jib.plugins.common.AppRootInvalidException; +import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.nio.file.Path; @@ -38,11 +38,11 @@ class MojoCommon { * * @param jibPluginConfiguration the Jib plugin configuration * @return the app root value - * @throws AppRootInvalidException if the app root is not an absolute path in Unix-style + * @throws InvalidAppRootException if the app root is not an absolute path in Unix-style */ // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) - throws AppRootInvalidException { + throws InvalidAppRootException { String appRoot = jibPluginConfiguration.getAppRoot(); if (appRoot.isEmpty()) { boolean isWarProject = "war".equals(jibPluginConfiguration.getProject().getPackaging()); @@ -54,7 +54,7 @@ static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfig try { return AbsoluteUnixPath.get(appRoot); } catch (IllegalArgumentException ex) { - throw new AppRootInvalidException(appRoot, appRoot, ex); + throw new InvalidAppRootException(appRoot, appRoot, ex); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 1037e558bd..379c40ac3e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -258,6 +258,15 @@ private static void assertCreationTimeEpoch(String imageReference) new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } + private static void assertWorkingDirectory(String expected, String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + expected, + new Command("docker", "inspect", "-f", "{{.Config.WorkingDir}}", imageReference) + .run() + .trim()); + } + @Nullable private String detachedContainerName; @Before @@ -318,6 +327,7 @@ public void testExecute_simple() Instant.parse( new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + assertWorkingDirectory("/home", targetImage); } @Test @@ -326,6 +336,7 @@ public void testExecute_empty() String targetImage = getGcrImageReference("emptyimage:maven"); Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); assertCreationTimeEpoch(targetImage); + assertWorkingDirectory("", targetImage); } @Test @@ -365,6 +376,7 @@ public void testExecute_complex() Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); + assertWorkingDirectory("", targetImage); } @Test @@ -374,6 +386,7 @@ public void testExecute_complex_sameFromAndToRegistry() Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); + assertWorkingDirectory("", targetImage); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 1f5dbc6101..42e48b6f10 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -50,6 +50,7 @@ private static void clearProperties() { System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); System.clearProperty("jib.container.user"); + System.clearProperty("jib.container.workingDirectory"); System.clearProperty("jib.extraDirectory.path"); System.clearProperty("jib.extraDirectory.permissions"); } @@ -72,6 +73,7 @@ public void teardown() { @Test public void testDefaults() { Assert.assertEquals("", testPluginConfiguration.getAppRoot()); + Assert.assertNull(testPluginConfiguration.getWorkingDirectory()); } @Test @@ -121,6 +123,8 @@ public void testSystemProperties() { Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); + System.setProperty("jib.container.workingDirectory", "working directory"); + Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); System.setProperty("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index e5c4e478c1..66b17c2392 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -52,6 +52,7 @@ value1 value2 + /home ${project.basedir}/src/main/jib-custom diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidAppRootException.java similarity index 76% rename from jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidAppRootException.java index fce57f6bb0..ae16473dae 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AppRootInvalidException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidAppRootException.java @@ -17,19 +17,19 @@ package com.google.cloud.tools.jib.plugins.common; /** - * Indicates that the container.appRoot config value is invalid (i.e., the path is not in the + * Indicates that the {@code container.appRoot} config value is invalid. (The path is not in the * absolute unix-path style. */ -public class AppRootInvalidException extends Exception { +public class InvalidAppRootException extends Exception { private final String invalidAppRoot; - public AppRootInvalidException(String message, String invalidAppRoot, Throwable ex) { + public InvalidAppRootException(String message, String invalidAppRoot, Throwable ex) { super(message, ex); this.invalidAppRoot = invalidAppRoot; } - public String getInvalidAppRoot() { + public String getInvalidPathValue() { return invalidAppRoot; } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidWorkingDirectoryException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidWorkingDirectoryException.java new file mode 100644 index 0000000000..9706c33511 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidWorkingDirectoryException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Indicates that the {@code container.workingDirectory} config value is invalid. (The path is not + * in the absolute unix-path style). + */ +public class InvalidWorkingDirectoryException extends Exception { + + private final String invalidPath; + + public InvalidWorkingDirectoryException(String message, String invalidPath, Throwable ex) { + super(message, ex); + this.invalidPath = invalidPath; + } + + public String getInvalidPathValue() { + return invalidPath; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 6c02726c4a..3152f61868 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -72,12 +72,12 @@ private interface InferredAuthProvider { * @param projectProperties used for providing additional information * @return the entrypoint * @throws MainClassInferenceException if no valid main class is configured or discovered - * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path + * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path */ @Nullable public static List computeEntrypoint( RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws MainClassInferenceException, AppRootInvalidException { + throws MainClassInferenceException, InvalidAppRootException { Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { if (rawConfiguration.getMainClass().isPresent() @@ -130,8 +130,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa @Nullable Path dockerExecutable, @Nullable Map dockerEnvironment, HelpfulSuggestions helpfulSuggestions) - throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, - InferredAuthRetrievalException, IOException { + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -152,8 +152,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage ProjectProperties projectProperties, Path tarImagePath, HelpfulSuggestions helpfulSuggestions) - throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, - InferredAuthRetrievalException, IOException { + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -166,7 +166,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( RawConfiguration rawConfiguration, ProjectProperties projectProperties) throws InferredAuthRetrievalException, InvalidImageReferenceException, - MainClassInferenceException, AppRootInvalidException, IOException { + MainClassInferenceException, InvalidAppRootException, IOException, + InvalidWorkingDirectoryException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -207,8 +208,8 @@ static PluginConfigurationProcessor processCommonConfiguration( Containerizer containerizer, ImageReference targetImageReference, boolean isTargetImageCredentialPresent) - throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, - InferredAuthRetrievalException, IOException { + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { JibSystemProperties.checkHttpTimeoutProperty(); ImageReference baseImageReference = @@ -248,6 +249,8 @@ static PluginConfigurationProcessor processCommonConfiguration( .setExposedPorts(ExposedPortsParser.parse(rawConfiguration.getPorts())) .setLabels(rawConfiguration.getLabels()) .setUser(rawConfiguration.getUser().orElse(null)); + getWorkingDirectoryChecked(rawConfiguration) + .ifPresent(workingDirectory -> jibContainerBuilder.setWorkingDirectory(workingDirectory)); if (rawConfiguration.getUseCurrentTimestamp()) { eventDispatcher.dispatch( LogEvent.warn( @@ -275,12 +278,12 @@ static PluginConfigurationProcessor processCommonConfiguration( * @param rawConfiguration raw configuration data * @param projectProperties used for providing additional information * @return the app root value - * @throws AppRootInvalidException if {@code appRoot} value is not an absolute Unix path + * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path */ @VisibleForTesting static AbsoluteUnixPath getAppRootChecked( RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws AppRootInvalidException { + throws InvalidAppRootException { String appRoot = rawConfiguration.getAppRoot(); if (appRoot.isEmpty()) { appRoot = @@ -291,7 +294,22 @@ static AbsoluteUnixPath getAppRootChecked( try { return AbsoluteUnixPath.get(appRoot); } catch (IllegalArgumentException ex) { - throw new AppRootInvalidException(appRoot, appRoot, ex); + throw new InvalidAppRootException(appRoot, appRoot, ex); + } + } + + @VisibleForTesting + static Optional getWorkingDirectoryChecked(RawConfiguration rawConfiguration) + throws InvalidWorkingDirectoryException { + if (!rawConfiguration.getWorkingDirectory().isPresent()) { + return Optional.empty(); + } + + String path = rawConfiguration.getWorkingDirectory().get(); + try { + return Optional.of(AbsoluteUnixPath.get(path)); + } catch (IllegalArgumentException ex) { + throw new InvalidWorkingDirectoryException(path, path, ex); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 3c8876f1d9..995767f9e9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -37,6 +37,7 @@ public class PropertyNames { public static final String CONTAINER_LABELS = "jib.container.labels"; public static final String CONTAINER_MAIN_CLASS = "jib.container.mainClass"; public static final String CONTAINER_USER = "jib.container.user"; + public static final String CONTAINER_WORKING_DIRECTORY = "jib.container.workingDirectory"; public static final String CONTAINER_PORTS = "jib.container.ports"; public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 6ad730653d..93e207285b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -65,6 +65,8 @@ public interface RawConfiguration { Optional getUser(); + Optional getWorkingDirectory(); + boolean getUseCurrentTimestamp(); boolean getAllowInsecureRegistries(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index d1b374bf5e..65b412e1e9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -87,11 +87,16 @@ public void setUp() { .thenReturn(containerizer); } - /** Test with our default mocks, which try to mimic the bare Gradle configuration. */ + /** + * Test with our default mocks, which try to mimic the bare Gradle configuration. + * + * @throws InvalidWorkingDirectoryException + */ @Test public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, projectProperties, containerizer, targetImageReference, false); @@ -108,8 +113,8 @@ public void testPluginConfigurationProcessor_defaults() @Test public void testPluginConfigurationProcessor_warProjectBaseImage() - throws InvalidImageReferenceException, MainClassInferenceException, AppRootInvalidException, - InferredAuthRetrievalException, IOException { + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = @@ -125,7 +130,8 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() @Test public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -144,7 +150,7 @@ public void testEntrypoint() @Test public void testComputeEntrypoint_inheritKeyword() - throws MainClassInferenceException, AppRootInvalidException { + throws MainClassInferenceException, InvalidAppRootException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Collections.singletonList("INHERIT"))); @@ -154,7 +160,7 @@ public void testComputeEntrypoint_inheritKeyword() @Test public void testComputeEntrypoint_inheritKeywordInNonSingletonList() - throws MainClassInferenceException, AppRootInvalidException { + throws MainClassInferenceException, InvalidAppRootException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("INHERIT", ""))); @@ -165,7 +171,8 @@ public void testComputeEntrypoint_inheritKeywordInNonSingletonList() @Test public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -184,7 +191,8 @@ public void testEntrypoint_defaultWarPackaging() @Test public void testEntrypoint_defaulNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -205,7 +213,8 @@ public void testEntrypoint_defaulNonWarPackaging() @Test public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = @@ -221,7 +230,8 @@ public void testUser() @Test public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, projectProperties, containerizer, targetImageReference, false); @@ -235,7 +245,8 @@ public void testUser_null() @Test public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -257,7 +268,8 @@ public void testEntrypoint_warningOnJvmFlags() @Test public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -279,7 +291,8 @@ public void testEntrypoint_warningOnMainclass() @Test public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = @@ -302,7 +315,8 @@ public void testEntrypointClasspath_nonDefaultAppRoot() @Test public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, AppRootInvalidException, InferredAuthRetrievalException { + MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, + InvalidWorkingDirectoryException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = @@ -316,7 +330,7 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() } @Test - public void testGetAppRootChecked() throws AppRootInvalidException { + public void testGetAppRootChecked() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/some/root"); Assert.assertEquals( @@ -331,7 +345,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() { try { PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { Assert.assertEquals("relative/path", ex.getMessage()); } } @@ -343,7 +357,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() { try { PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { Assert.assertEquals("\\windows\\path", ex.getMessage()); } } @@ -355,13 +369,13 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { try { PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); - } catch (AppRootInvalidException ex) { + } catch (InvalidAppRootException ex) { Assert.assertEquals("C:\\windows\\path", ex.getMessage()); } } @Test - public void testGetAppRootChecked_defaultNonWarProject() throws AppRootInvalidException { + public void testGetAppRootChecked_defaultNonWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -371,7 +385,7 @@ public void testGetAppRootChecked_defaultNonWarProject() throws AppRootInvalidEx } @Test - public void testGetAppRootChecked_defaultWarProject() throws AppRootInvalidException { + public void testGetAppRootChecked_defaultWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -380,6 +394,37 @@ public void testGetAppRootChecked_defaultWarProject() throws AppRootInvalidExcep PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } + @Test + public void testGetWorkingDirectoryChecked() throws InvalidWorkingDirectoryException { + Mockito.when(rawConfiguration.getWorkingDirectory()).thenReturn(Optional.of("/valid/path")); + + Optional checkedPath = + PluginConfigurationProcessor.getWorkingDirectoryChecked(rawConfiguration); + Assert.assertTrue(checkedPath.isPresent()); + Assert.assertEquals(AbsoluteUnixPath.get("/valid/path"), checkedPath.get()); + } + + @Test + public void testGetWorkingDirectoryChecked_undefined() throws InvalidWorkingDirectoryException { + Mockito.when(rawConfiguration.getWorkingDirectory()).thenReturn(Optional.empty()); + Assert.assertEquals( + Optional.empty(), + PluginConfigurationProcessor.getWorkingDirectoryChecked(rawConfiguration)); + } + + @Test + public void testGetWorkingDirectoryChecked_notAbsolute() { + Mockito.when(rawConfiguration.getWorkingDirectory()).thenReturn(Optional.of("relative/path")); + + try { + PluginConfigurationProcessor.getWorkingDirectoryChecked(rawConfiguration); + Assert.fail(); + } catch (InvalidWorkingDirectoryException ex) { + Assert.assertEquals("relative/path", ex.getMessage()); + Assert.assertEquals("relative/path", ex.getInvalidPathValue()); + } + } + @Test public void testGetBaseImage_defaultNonWarPackaging() { Mockito.when(projectProperties.isWarProject()).thenReturn(false); From ebdae608c73d40b0c637ec45f93b09d544260941 Mon Sep 17 00:00:00 2001 From: Gustavo Silva Paiva Date: Wed, 28 Nov 2018 14:14:35 -0200 Subject: [PATCH 0353/2020] Support for volumes on plugins (#1278) * Support for volumes on maven and gradle plugins. * refactoring exception name, adding more unit and integration tests. * change logs * normalizing exception handling. * Propagate the cause of the exception. --- jib-gradle-plugin/CHANGELOG.md | 1 + .../DefaultTargetProjectIntegrationTest.java | 2 +- .../gradle/SingleProjectIntegrationTest.java | 9 +++- .../resources/projects/simple/build.gradle | 1 + .../projects/simple/complex-build.gradle | 1 + .../tools/jib/gradle/BuildDockerTask.java | 4 ++ .../tools/jib/gradle/BuildImageTask.java | 4 ++ .../cloud/tools/jib/gradle/BuildTarTask.java | 4 ++ .../tools/jib/gradle/ContainerParameters.java | 15 +++++++ .../jib/gradle/GradleRawConfiguration.java | 5 +++ jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildImageMojo.java | 4 ++ .../cloud/tools/jib/maven/BuildTarMojo.java | 4 ++ .../jib/maven/JibPluginConfiguration.java | 15 +++++++ .../jib/maven/MavenRawConfiguration.java | 5 +++ .../maven/BuildDockerMojoIntegrationTest.java | 7 +++ .../resources/projects/default-target/pom.xml | 4 ++ .../src/test/resources/projects/empty/pom.xml | 4 ++ .../resources/projects/simple/pom-complex.xml | 4 ++ .../test/resources/projects/simple/pom.xml | 4 ++ .../InvalidContainerVolumeException.java | 35 +++++++++++++++ .../common/PluginConfigurationProcessor.java | 36 +++++++++++++-- .../jib/plugins/common/PropertyNames.java | 1 + .../jib/plugins/common/RawConfiguration.java | 2 + .../PluginConfigurationProcessorTest.java | 45 ++++++++++++++----- 26 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerVolumeException.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 431934d723..90ef8e567f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. - `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java index 60eca3ae02..f64f64ccc0 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -32,7 +32,7 @@ public class DefaultTargetProjectIntegrationTest { public static final TestProject defaultTargetTestProject = new TestProject("default-target"); /** - * Asserts that the test project has the required exposed ports and labels. + * Asserts that the test project has the required exposed ports, labels and volumes. * * @param imageReference the image to test * @throws IOException if the {@code docker inspect} command fails to run diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index b9a5ca56b1..a3dc7097b8 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -71,7 +71,7 @@ private static void assertWorkingDirectory(String expected, String imageReferenc } /** - * Asserts that the test project has the required exposed ports and labels. + * Asserts that the test project has the required exposed ports, labels and volumes. * * @param imageReference the image to test * @throws IOException if the {@code docker inspect} command fails to run @@ -80,6 +80,13 @@ private static void assertWorkingDirectory(String expected, String imageReferenc private static void assertDockerInspect(String imageReference) throws IOException, InterruptedException { String dockerInspect = new Command("docker", "inspect", imageReference).run(); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Volumes\": {\n" + + " \"/var/log\": {},\n" + + " \"/var/log2\": {}\n" + + " },")); Assert.assertThat( dockerInspect, CoreMatchers.containsString( diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index ea5d74319e..858a46be96 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -24,6 +24,7 @@ jib { args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] workingDirectory = '/home' } extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index b12d6940fd..be0653b1af 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -37,6 +37,7 @@ jib { environment = [env1:'envvalue1', env2:'envvalue2'] ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] } extraDirectory { path = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index fc999030b1..b438fd2aa0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -153,6 +154,9 @@ public void buildDocker() "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 6c13528a69..87d407f6e6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -133,6 +134,9 @@ public void buildImage() "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 0a7ae154e9..ce876c16bd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -150,6 +151,9 @@ public void buildTar() "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new GradleException( + "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 2bef23d70d..421bd8e5ce 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -41,6 +41,7 @@ public class ContainerParameters { @Nullable private List args; private ImageFormat format = ImageFormat.Docker; private List ports = Collections.emptyList(); + private List volumes = Collections.emptyList(); private Map labels = Collections.emptyMap(); private String appRoot = ""; @Nullable private String user; @@ -162,6 +163,20 @@ public void setPorts(List ports) { this.ports = ports; } + @Input + @Optional + public List getVolumes() { + if (System.getProperty(PropertyNames.CONTAINER_VOLUMES) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_VOLUMES)); + } + return volumes; + } + + public void setVolumes(List volumes) { + this.volumes = volumes; + } + @Input @Optional public Map getLabels() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index f023363508..525db125b2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -117,6 +117,11 @@ public Map getLabels() { return jibExtension.getContainer().getLabels(); } + @Override + public List getVolumes() { + return jibExtension.getContainer().getVolumes(); + } + @Override public List getPorts() { return jibExtension.getContainer().getPorts(); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 49fca3e6a0..a7050a1568 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. - `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 4a382e3274..6268bac235 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -121,6 +122,9 @@ public void execute() throws MojoExecutionException { + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 469fd22845..f304f2f859 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -137,6 +138,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 5a81261776..84297001df 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; @@ -115,6 +116,9 @@ public void execute() throws MojoExecutionException { + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { + throw new MojoExecutionException( + " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 7b0da4e4ad..1c987869ad 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -146,6 +146,8 @@ public static class ContainerParameters { @Parameter private List ports = Collections.emptyList(); + @Parameter private List volumes = Collections.emptyList(); + @Parameter private Map labels = Collections.emptyMap(); @Parameter private String appRoot = ""; @@ -417,6 +419,19 @@ List getExposedPorts() { return container.ports; } + /** + * Gets the configured volumes. + * + * @return the configured volumes + */ + List getVolumes() { + if (System.getProperty(PropertyNames.CONTAINER_VOLUMES) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_VOLUMES)); + } + return container.volumes; + } + /** * Gets the configured labels. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 265dda0972..f1b8ff75e4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -132,6 +132,11 @@ public Map getLabels() { return jibPluginConfiguration.getLabels(); } + @Override + public List getVolumes() { + return jibPluginConfiguration.getVolumes(); + } + @Override public List getPorts() { return jibPluginConfiguration.getExposedPorts(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index b8c54a67aa..cf0ce44794 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -66,6 +66,13 @@ private static String buildToDockerDaemonAndRun(Path projectRoot, String imageRe buildToDockerDaemon(projectRoot, imageReference, "pom.xml"); String dockerInspect = new Command("docker", "inspect", imageReference).run(); + Assert.assertThat( + dockerInspect, + CoreMatchers.containsString( + " \"Volumes\": {\n" + + " \"/var/log\": {},\n" + + " \"/var/log2\": {}\n" + + " },")); Assert.assertThat( dockerInspect, CoreMatchers.containsString( diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml index c413787d0e..e738671d1c 100644 --- a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml @@ -48,6 +48,10 @@ value1 value2 + + /var/log + /var/log2 + true diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 5f73a8ebe5..3f4c4d9bd6 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -42,6 +42,10 @@ value1 value2 + + /var/log + /var/log2 + true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index 36e375a6b8..ec8c8d9eaf 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -74,6 +74,10 @@ value1 value2 + + /var/log + /var/log2 + Docker diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index 66b17c2392..598d32c4f0 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -52,6 +52,10 @@ value1 value2 + + /var/log + /var/log2 + /home ${project.basedir}/src/main/jib-custom diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerVolumeException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerVolumeException.java new file mode 100644 index 0000000000..caf8aa14fd --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerVolumeException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Indicates that the {@code container.volumes} config value has at least one invalid path. (The + * path is not in the absolute unix-path style). + */ +public class InvalidContainerVolumeException extends Exception { + + private String invalidVolume; + + InvalidContainerVolumeException(String message, String invalidVolume, Throwable cause) { + super(message, cause); + this.invalidVolume = invalidVolume; + } + + public String getInvalidVolume() { + return invalidVolume; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 3152f61868..5dea14d70d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -131,7 +132,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa @Nullable Map dockerEnvironment, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -153,7 +155,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage Path tarImagePath, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -167,7 +170,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry RawConfiguration rawConfiguration, ProjectProperties projectProperties) throws InferredAuthRetrievalException, InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -209,7 +212,8 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageReference targetImageReference, boolean isTargetImageCredentialPresent) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException { JibSystemProperties.checkHttpTimeoutProperty(); ImageReference baseImageReference = @@ -247,6 +251,7 @@ static PluginConfigurationProcessor processCommonConfiguration( .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(rawConfiguration.getPorts())) + .setVolumes(getVolumesList(rawConfiguration)) .setLabels(rawConfiguration.getLabels()) .setUser(rawConfiguration.getUser().orElse(null)); getWorkingDirectoryChecked(rawConfiguration) @@ -270,6 +275,29 @@ static PluginConfigurationProcessor processCommonConfiguration( isTargetImageCredentialPresent); } + /** + * Parses the list of raw volumes directories to a list of {@link AbsoluteUnixPath} + * + * @param rawConfiguration raw configuration data + * @return the list of parsed volumes. + * @throws InvalidContainerVolumeException if {@code volumes} are not valid absolute Unix paths + */ + @VisibleForTesting + static List getVolumesList(RawConfiguration rawConfiguration) + throws InvalidContainerVolumeException { + List volumes = new ArrayList<>(); + for (String path : rawConfiguration.getVolumes()) { + try { + AbsoluteUnixPath absoluteUnixPath = AbsoluteUnixPath.get(path); + volumes.add(absoluteUnixPath); + } catch (IllegalArgumentException exception) { + throw new InvalidContainerVolumeException(path, path, exception); + } + } + + return volumes; + } + /** * Gets the value of the {@code appRoot} parameter. If the parameter is empty, returns {@link * JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 995767f9e9..2a4a42d657 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -38,6 +38,7 @@ public class PropertyNames { public static final String CONTAINER_MAIN_CLASS = "jib.container.mainClass"; public static final String CONTAINER_USER = "jib.container.user"; public static final String CONTAINER_WORKING_DIRECTORY = "jib.container.workingDirectory"; + public static final String CONTAINER_VOLUMES = "jib.container.volumes"; public static final String CONTAINER_PORTS = "jib.container.ports"; public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 93e207285b..3f8e41e1f9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -61,6 +61,8 @@ public interface RawConfiguration { Map getLabels(); + List getVolumes(); + List getPorts(); Optional getUser(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 65b412e1e9..fe60e20364 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -96,7 +96,7 @@ public void setUp() { public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, projectProperties, containerizer, targetImageReference, false); @@ -114,7 +114,8 @@ public void testPluginConfigurationProcessor_defaults() @Test public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException { + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = @@ -131,7 +132,7 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -172,7 +173,7 @@ public void testComputeEntrypoint_inheritKeywordInNonSingletonList() public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -192,7 +193,7 @@ public void testEntrypoint_defaultWarPackaging() public void testEntrypoint_defaulNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -214,7 +215,7 @@ public void testEntrypoint_defaulNonWarPackaging() public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = @@ -231,7 +232,7 @@ public void testUser() public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { PluginConfigurationProcessor processor = PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, projectProperties, containerizer, targetImageReference, false); @@ -246,7 +247,7 @@ public void testUser_null() public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -269,7 +270,7 @@ public void testEntrypoint_warningOnJvmFlags() public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -292,7 +293,7 @@ public void testEntrypoint_warningOnMainclass() public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = @@ -316,7 +317,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = @@ -450,4 +451,26 @@ public void testGetBaseImage_nonDefault() { Assert.assertEquals( "tomcat", PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); } + + @Test + public void testGetValidVolumesList() throws InvalidContainerVolumeException { + Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("/some/root")); + + Assert.assertEquals( + Collections.singletonList(AbsoluteUnixPath.get("/some/root")), + PluginConfigurationProcessor.getVolumesList(rawConfiguration)); + } + + @Test + public void testGetInvalidVolumesList() { + Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("`some/root")); + + try { + PluginConfigurationProcessor.getVolumesList(rawConfiguration); + Assert.fail(); + } catch (InvalidContainerVolumeException ex) { + Assert.assertEquals("`some/root", ex.getMessage()); + Assert.assertEquals("`some/root", ex.getInvalidVolume()); + } + } } From 7df3f7ed1f132e8dcf32e1d8a4f4dcbca1213e23 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 28 Nov 2018 17:25:29 -0500 Subject: [PATCH 0354/2020] Change exposed port/volumes API to use sets instead of lists (#1284) --- .../tools/jib/api/JibContainerBuilder.java | 26 ++++++------ .../configuration/ContainerConfiguration.java | 40 ++++++++++--------- .../jib/frontend/ExposedPortsParser.java | 6 +-- .../google/cloud/tools/jib/image/Image.java | 28 +++++++------ .../jib/image/json/ImageToJsonTranslator.java | 36 ++++++++--------- .../jib/image/json/JsonToImageTranslator.java | 27 ++++++------- .../jib/api/JibContainerBuilderTest.java | 6 +-- .../jib/builder/steps/BuildImageStepTest.java | 3 +- .../configuration/BuildConfigurationTest.java | 3 +- .../ContainerConfigurationTest.java | 5 ++- .../jib/frontend/ExposedPortsParserTest.java | 8 ++-- .../cloud/tools/jib/image/ImageTest.java | 6 +-- .../image/json/ImageToJsonTranslatorTest.java | 15 +++---- .../image/json/JsonToImageTranslatorTest.java | 19 ++++----- .../common/PluginConfigurationProcessor.java | 15 +++---- .../PluginConfigurationProcessorTest.java | 12 ++---- 16 files changed, 130 insertions(+), 125 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index e4dbef7b2a..54f9877ead 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -32,8 +32,10 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; @@ -245,10 +247,10 @@ public JibContainerBuilder addEnvironmentVariable(String name, String value) { *

    This is similar to {@code * VOLUME} in Dockerfiles. * - * @param volumes the list of directory paths on the container filesystem to set as volumes + * @param volumes the directory paths on the container filesystem to set as volumes * @return this */ - public JibContainerBuilder setVolumes(List volumes) { + public JibContainerBuilder setVolumes(Set volumes) { containerConfigurationBuilder.setVolumes(volumes); return this; } @@ -256,20 +258,20 @@ public JibContainerBuilder setVolumes(List volumes) { /** * Sets the directories that may hold externally mounted volumes. * - * @param volumes the list of directory paths on the container filesystem to set as volumes + * @param volumes the directory paths on the container filesystem to set as volumes * @return this - * @see #setVolumes(List) for more details + * @see #setVolumes(Set) for more details */ public JibContainerBuilder setVolumes(AbsoluteUnixPath... volumes) { - return setVolumes(new ArrayList<>(Arrays.asList(volumes))); + return setVolumes(new HashSet<>(Arrays.asList(volumes))); } /** * Adds a directory that may hold an externally mounted volume. * - * @param volume a directory path on the container filesystem to represent a volume. + * @param volume a directory path on the container filesystem to represent a volume * @return this - * @see #setVolumes(List) for more details + * @see #setVolumes(Set) for more details */ public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { containerConfigurationBuilder.addVolume(volume); @@ -287,10 +289,10 @@ public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { * href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#container-v1-core">Kubernetes * Container spec. * - * @param ports the list of ports to expose + * @param ports the ports to expose * @return this */ - public JibContainerBuilder setExposedPorts(List ports) { + public JibContainerBuilder setExposedPorts(Set ports) { containerConfigurationBuilder.setExposedPorts(ports); return this; } @@ -300,10 +302,10 @@ public JibContainerBuilder setExposedPorts(List ports) { * * @param ports the ports to expose * @return this - * @see #setExposedPorts(List) for more details + * @see #setExposedPorts(Set) for more details */ public JibContainerBuilder setExposedPorts(Port... ports) { - return setExposedPorts(Arrays.asList(ports)); + return setExposedPorts(new HashSet<>(Arrays.asList(ports))); } /** @@ -311,7 +313,7 @@ public JibContainerBuilder setExposedPorts(Port... ports) { * * @param port the port to expose * @return this - * @see #setExposedPorts(List) for more details + * @see #setExposedPorts(Set) for more details */ public JibContainerBuilder addExposedPort(Port port) { containerConfigurationBuilder.addExposedPort(port); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 4f486235ac..b539dc1dd8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -21,13 +21,15 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.time.Instant; -import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.annotation.Nullable; /** Immutable configuration options for the container. */ @@ -43,8 +45,8 @@ public static class Builder { @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; @Nullable private Map environmentMap; - @Nullable private List exposedPorts; - @Nullable private List volumes; + @Nullable private Set exposedPorts; + @Nullable private Set volumes; @Nullable private Map labels; @Nullable private String user; @Nullable private AbsoluteUnixPath workingDirectory; @@ -103,22 +105,22 @@ public void addEnvironment(String name, String value) { /** * Sets the container's exposed ports. * - * @param exposedPorts the list of ports + * @param exposedPorts the set of ports * @return this */ - public Builder setExposedPorts(@Nullable List exposedPorts) { + public Builder setExposedPorts(@Nullable Set exposedPorts) { if (exposedPorts == null) { this.exposedPorts = null; } else { Preconditions.checkArgument(!exposedPorts.contains(null)); - this.exposedPorts = new ArrayList<>(exposedPorts); + this.exposedPorts = new HashSet<>(exposedPorts); } return this; } public void addExposedPort(Port port) { if (exposedPorts == null) { - exposedPorts = new ArrayList<>(); + exposedPorts = new HashSet<>(); } exposedPorts.add(port); } @@ -126,22 +128,22 @@ public void addExposedPort(Port port) { /** * Sets the container's volumes. * - * @param volumes the list of volumes + * @param volumes the set of volumes * @return this */ - public Builder setVolumes(@Nullable List volumes) { + public Builder setVolumes(@Nullable Set volumes) { if (volumes == null) { this.volumes = null; } else { Preconditions.checkArgument(!volumes.contains(null)); - this.volumes = new ArrayList<>(volumes); + this.volumes = new HashSet<>(volumes); } return this; } public void addVolume(AbsoluteUnixPath volume) { if (volumes == null) { - volumes = new ArrayList<>(); + volumes = new HashSet<>(); } volumes.add(volume); } @@ -221,8 +223,8 @@ public ContainerConfiguration build() { entrypoint, programArguments, environmentMap == null ? null : ImmutableMap.copyOf(environmentMap), - exposedPorts == null ? null : ImmutableList.copyOf(exposedPorts), - volumes == null ? null : ImmutableList.copyOf(volumes), + exposedPorts == null ? null : ImmutableSet.copyOf(exposedPorts), + volumes == null ? null : ImmutableSet.copyOf(volumes), labels == null ? null : ImmutableMap.copyOf(labels), user, workingDirectory); @@ -244,8 +246,8 @@ public static Builder builder() { @Nullable private final ImmutableList entrypoint; @Nullable private final ImmutableList programArguments; @Nullable private final ImmutableMap environmentMap; - @Nullable private final ImmutableList exposedPorts; - @Nullable private final ImmutableList volumes; + @Nullable private final ImmutableSet exposedPorts; + @Nullable private final ImmutableSet volumes; @Nullable private final ImmutableMap labels; @Nullable private final String user; @Nullable private final AbsoluteUnixPath workingDirectory; @@ -255,8 +257,8 @@ private ContainerConfiguration( @Nullable ImmutableList entrypoint, @Nullable ImmutableList programArguments, @Nullable ImmutableMap environmentMap, - @Nullable ImmutableList exposedPorts, - @Nullable ImmutableList volumes, + @Nullable ImmutableSet exposedPorts, + @Nullable ImmutableSet volumes, @Nullable ImmutableMap labels, @Nullable String user, @Nullable AbsoluteUnixPath workingDirectory) { @@ -291,12 +293,12 @@ public ImmutableMap getEnvironmentMap() { } @Nullable - public ImmutableList getExposedPorts() { + public ImmutableSet getExposedPorts() { return exposedPorts; } @Nullable - public ImmutableList getVolumes() { + public ImmutableSet getVolumes() { return volumes; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java index 7e56c4022b..8193051ec3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.configuration.Port; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,8 +44,8 @@ public class ExposedPortsParser { * @return the ports as a list of {@link Port} * @throws NumberFormatException if any of the ports are in an invalid format or out of range */ - public static ImmutableList parse(List ports) throws NumberFormatException { - ImmutableList.Builder result = new ImmutableList.Builder<>(); + public static ImmutableSet parse(List ports) throws NumberFormatException { + ImmutableSet.Builder result = new ImmutableSet.Builder<>(); for (String port : ports) { Matcher matcher = portPattern.matcher(port); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 128a98dcc5..adc699d7b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -21,10 +21,12 @@ import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; /** Represents an image. */ @@ -44,8 +46,8 @@ public static class Builder { @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; - @Nullable private ImmutableList exposedPorts; - @Nullable private ImmutableList volumes; + @Nullable private ImmutableSet exposedPorts; + @Nullable private ImmutableSet volumes; @Nullable private String workingDirectory; @Nullable private String user; @@ -125,8 +127,8 @@ public Builder setProgramArguments(@Nullable List programArguments) { * @param exposedPorts the list of exposed ports to add * @return this */ - public Builder setExposedPorts(@Nullable List exposedPorts) { - this.exposedPorts = (exposedPorts == null) ? null : ImmutableList.copyOf(exposedPorts); + public Builder setExposedPorts(@Nullable Set exposedPorts) { + this.exposedPorts = (exposedPorts == null) ? null : ImmutableSet.copyOf(exposedPorts); return this; } @@ -136,8 +138,8 @@ public Builder setExposedPorts(@Nullable List exposedPorts) { * @param volumes the list of directories to create a volume. * @return this */ - public Builder setVolumes(@Nullable List volumes) { - this.volumes = (volumes == null) ? null : ImmutableList.copyOf(volumes); + public Builder setVolumes(@Nullable Set volumes) { + this.volumes = (volumes == null) ? null : ImmutableSet.copyOf(volumes); return this; } @@ -239,10 +241,10 @@ public static Builder builder() { @Nullable private final ImmutableList programArguments; /** Ports that the container listens on. */ - @Nullable private final ImmutableList exposedPorts; + @Nullable private final ImmutableSet exposedPorts; - /** List of directories to mount as volumes. */ - @Nullable private final ImmutableList volumes; + /** Directories to mount as volumes. */ + @Nullable private final ImmutableSet volumes; /** Labels on the container configuration */ @Nullable private final ImmutableMap labels; @@ -260,8 +262,8 @@ private Image( @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @Nullable ImmutableList programArguments, - @Nullable ImmutableList exposedPorts, - @Nullable ImmutableList volumes, + @Nullable ImmutableSet exposedPorts, + @Nullable ImmutableSet volumes, @Nullable ImmutableMap labels, @Nullable String workingDirectory, @Nullable String user) { @@ -299,12 +301,12 @@ public ImmutableList getProgramArguments() { } @Nullable - public ImmutableList getExposedPorts() { + public ImmutableSet getExposedPorts() { return exposedPorts; } @Nullable - public ImmutableList getVolumes() { + public ImmutableSet getVolumes() { return volumes; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 77ee02b017..550fca5090 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -30,8 +30,8 @@ import com.google.common.collect.ImmutableSortedMap; import java.lang.reflect.InvocationTargetException; import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import javax.annotation.Nullable; @@ -50,36 +50,36 @@ public class ImageToJsonTranslator { /** - * Converts a list of {@link Port}s to the corresponding container config format for exposed ports + * Converts a set of {@link Port}s to the corresponding container config format for exposed ports * (e.g. {@code Port(1000, Protocol.TCP)} -> {@code {"1000/tcp":{}}}). * - * @param exposedPorts the list of {@link Port}s to translate, or {@code null} + * @param exposedPorts the set of {@link Port}s to translate, or {@code null} * @return a sorted map with the string representation of the ports as keys and empty maps as * values, or {@code null} if {@code exposedPorts} is {@code null} */ @VisibleForTesting @Nullable - static Map> portListToMap(@Nullable List exposedPorts) { - return listToMap(exposedPorts, port -> port.getPort() + "/" + port.getProtocol()); + static Map> portSetToMap(@Nullable Set exposedPorts) { + return setToMap(exposedPorts, port -> port.getPort() + "/" + port.getProtocol()); } /** - * Converts a list of {@link AbsoluteUnixPath}s to the corresponding container config format for + * Converts a set of {@link AbsoluteUnixPath}s to the corresponding container config format for * volumes (e.g. {@code AbsoluteUnixPath().get("/var/log/my-app-logs")} -> {@code * {"/var/log/my-app-logs":{}}}). * - * @param volumes the list of {@link AbsoluteUnixPath}s to translate, or {@code null} + * @param volumes the set of {@link AbsoluteUnixPath}s to translate, or {@code null} * @return a sorted map with the string representation of the ports as keys and empty maps as * values, or {@code null} if {@code exposedPorts} is {@code null} */ @VisibleForTesting @Nullable - static Map> volumesListToMap(@Nullable List volumes) { - return listToMap(volumes, AbsoluteUnixPath::toString); + static Map> volumesSetToMap(@Nullable Set volumes) { + return setToMap(volumes, AbsoluteUnixPath::toString); } /** - * Turns a list into a sorted map where each element of the list is mapped to an entry composed by + * Turns a set into a sorted map where each element of the set is mapped to an entry composed by * the key generated with {@code Function elementMapper} and an empty map as value. * *

    This method is needed because the volume object is a direct JSON serialization of the Go @@ -89,19 +89,19 @@ public class ImageToJsonTranslator { *

    Further read at the image specs. * - * @param list the list of elements to be transformed + * @param set the set of elements to be transformed * @param keyMapper the mapper function to generate keys to the map - * @param the type of the elements from the list + * @param the type of the elements from the set * @return an map */ @Nullable - private static Map> listToMap( - @Nullable List list, Function keyMapper) { - if (list == null) { + private static Map> setToMap( + @Nullable Set set, Function keyMapper) { + if (set == null) { return null; } - return list.stream() + return set.stream() .collect( ImmutableSortedMap.toImmutableSortedMap( String::compareTo, keyMapper, ignored -> Collections.emptyMap())); @@ -171,10 +171,10 @@ public Blob getContainerConfigurationBlob() { template.setContainerCmd(image.getProgramArguments()); // Sets the exposed ports. - template.setContainerExposedPorts(portListToMap(image.getExposedPorts())); + template.setContainerExposedPorts(portSetToMap(image.getExposedPorts())); // Sets the volumes. - template.setContainerVolumes(volumesListToMap(image.getVolumes())); + template.setContainerVolumes(volumesSetToMap(image.getVolumes())); // Sets the labels. template.setContainerLabels(image.getLabels()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 395a41bdec..d6896bc5ee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -28,7 +28,7 @@ import com.google.cloud.tools.jib.image.ReferenceLayer; import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.ArrayList; @@ -148,12 +148,11 @@ public static Image toImage( if (containerConfigurationTemplate.getContainerExposedPorts() != null) { imageBuilder.setExposedPorts( - portMapToList(containerConfigurationTemplate.getContainerExposedPorts())); + portMapToSet(containerConfigurationTemplate.getContainerExposedPorts())); } if (containerConfigurationTemplate.getContainerVolumes() != null) { - imageBuilder.setVolumes( - volumeMapToList(containerConfigurationTemplate.getContainerVolumes())); + imageBuilder.setVolumes(volumeMapToSet(containerConfigurationTemplate.getContainerVolumes())); } if (containerConfigurationTemplate.getContainerEnvironment() != null) { @@ -174,19 +173,19 @@ public static Image toImage( } /** - * Converts a map of exposed ports as strings to a list of {@link Port}s (e.g. {@code + * Converts a map of exposed ports as strings to a set of {@link Port}s (e.g. {@code * {"1000/tcp":{}}} -> {@code Port(1000, Protocol.TCP)}). * * @param portMap the map to convert - * @return a list of {@link Port}s + * @return a set of {@link Port}s */ @VisibleForTesting - static ImmutableList portMapToList(@Nullable Map> portMap) + static ImmutableSet portMapToSet(@Nullable Map> portMap) throws BadContainerConfigurationFormatException { if (portMap == null) { - return ImmutableList.of(); + return ImmutableSet.of(); } - ImmutableList.Builder ports = new ImmutableList.Builder<>(); + ImmutableSet.Builder ports = new ImmutableSet.Builder<>(); for (Map.Entry> entry : portMap.entrySet()) { String port = entry.getKey(); Matcher matcher = PORT_PATTERN.matcher(port); @@ -203,20 +202,20 @@ static ImmutableList portMapToList(@Nullable Map> portMa } /** - * Converts a map of volumes strings to a list of {@link AbsoluteUnixPath}s (e.g. {@code {@code + * Converts a map of volumes strings to a set of {@link AbsoluteUnixPath}s (e.g. {@code {@code * {"/var/log/my-app-logs":{}}} -> AbsoluteUnixPath().get("/var/log/my-app-logs")}). * * @param volumeMap the map to convert - * @return a list of {@link AbsoluteUnixPath}s + * @return a set of {@link AbsoluteUnixPath}s */ @VisibleForTesting - static ImmutableList volumeMapToList(@Nullable Map> volumeMap) + static ImmutableSet volumeMapToSet(@Nullable Map> volumeMap) throws BadContainerConfigurationFormatException { if (volumeMap == null) { - return ImmutableList.of(); + return ImmutableSet.of(); } - ImmutableList.Builder volumeList = ImmutableList.builder(); + ImmutableSet.Builder volumeList = ImmutableSet.builder(); for (String volume : volumeMap.keySet()) { try { volumeList.add(AbsoluteUnixPath.get(volume)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 3c9840246f..2a28cc290d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -66,7 +66,7 @@ public void testToBuildConfiguration_containerConfigurationSet() new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) - .setExposedPorts(Arrays.asList(Port.tcp(1234), Port.udp(5678))) + .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) .setLabels(ImmutableMap.of("key", "value")) .setProgramArguments(Arrays.asList("program", "arguments")) .setCreationTime(Instant.ofEpochMilli(1000)) @@ -80,7 +80,7 @@ public void testToBuildConfiguration_containerConfigurationSet() Assert.assertEquals( ImmutableMap.of("name", "value"), containerConfiguration.getEnvironmentMap()); Assert.assertEquals( - Arrays.asList(Port.tcp(1234), Port.udp(5678)), containerConfiguration.getExposedPorts()); + ImmutableSet.of(Port.tcp(1234), Port.udp(5678)), containerConfiguration.getExposedPorts()); Assert.assertEquals(ImmutableMap.of("key", "value"), containerConfiguration.getLabels()); Assert.assertEquals( Arrays.asList("program", "arguments"), containerConfiguration.getProgramArguments()); @@ -113,7 +113,7 @@ public void testToBuildConfiguration_containerConfigurationAdd() ImmutableMap.of("name", "value", "environment", "variable"), containerConfiguration.getEnvironmentMap()); Assert.assertEquals( - Arrays.asList(Port.tcp(1234), Port.udp(5678), Port.tcp(1337)), + ImmutableSet.of(Port.tcp(1234), Port.udp(5678), Port.tcp(1337)), containerConfiguration.getExposedPorts()); Assert.assertEquals( ImmutableMap.of("key", "value", "added", "label"), containerConfiguration.getLabels()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index d7608c973c..691b68ab16 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import java.security.DigestException; @@ -104,7 +105,7 @@ public BlobDescriptor getBlobDescriptor() { Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); - Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableList.of()); + Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableSet.of()); Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); Mockito.when(mockContainerConfiguration.getUser()).thenReturn("root"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 550468ff63..321124b88c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.IOException; @@ -59,7 +58,7 @@ public void testBuilder() throws Exception { List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedProgramArguments = Arrays.asList("arg1", "arg2"); Map expectedEnvironment = ImmutableMap.of("key", "value"); - ImmutableList expectedExposedPorts = ImmutableList.of(Port.tcp(1000), Port.tcp(2000)); + ImmutableSet expectedExposedPorts = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)); Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); Class expectedTargetFormat = OCIManifestTemplate.class; Path expectedApplicationLayersCacheDirectory = Paths.get("application/layers"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index c347d7dc1a..eb32817bf4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -20,9 +20,10 @@ import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Hashtable; -import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import org.junit.Assert; import org.junit.Test; @@ -49,7 +50,7 @@ public void testBuilder_nullValues() { } // Exposed ports element should not be null. - List badPorts = Arrays.asList(Port.tcp(1000), null); + Set badPorts = new HashSet<>(Arrays.asList(Port.tcp(1000), null)); try { ContainerConfiguration.builder().setExposedPorts(badPorts); Assert.fail("The IllegalArgumentException should be thrown."); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index c3e2b48274..de2e5748bb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.configuration.Port; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -31,8 +31,8 @@ public class ExposedPortsParserTest { public void testParse() { List goodInputs = Arrays.asList("1000", "2000-2003", "3000-3000", "4000/tcp", "5000/udp", "6000-6002/udp"); - ImmutableList expected = - new ImmutableList.Builder() + ImmutableSet expected = + new ImmutableSet.Builder() .add( Port.tcp(1000), Port.tcp(2000), @@ -46,7 +46,7 @@ public void testParse() { Port.udp(6001), Port.udp(6002)) .build(); - ImmutableList result = ExposedPortsParser.parse(goodInputs); + ImmutableSet result = ExposedPortsParser.parse(goodInputs); Assert.assertEquals(expected, result); List badInputs = Arrays.asList("abc", "/udp", "1000/abc", "a100/tcp", "20/udpabc"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 5419be278d..83106841cc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.time.Instant; import java.util.Arrays; import org.junit.Assert; @@ -52,7 +52,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) .setProgramArguments(Arrays.asList("arg1", "arg2")) - .setExposedPorts(ImmutableList.of(Port.tcp(1000), Port.tcp(2000))) + .setExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000))) .setUser("john") .addLayer(mockLayer) .build(); @@ -64,7 +64,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment()); Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getProgramArguments()); - Assert.assertEquals(ImmutableList.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); + Assert.assertEquals(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); Assert.assertEquals("john", image.getUser()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 5cc2a6755f..82c52c68c6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; @@ -61,9 +62,9 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); testImageBuilder.setProgramArguments(Arrays.asList("arg1", "arg2")); testImageBuilder.setExposedPorts( - ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); + ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); testImageBuilder.setVolumes( - Arrays.asList( + ImmutableSet.of( AbsoluteUnixPath.get("/var/job-result-data"), AbsoluteUnixPath.get("/var/log/my-app-logs"))); testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); @@ -135,22 +136,22 @@ public void testGetManifest_oci() throws URISyntaxException, IOException { @Test public void testPortListToMap() { - ImmutableList input = ImmutableList.of(Port.tcp(1000), Port.udp(2000)); + ImmutableSet input = ImmutableSet.of(Port.tcp(1000), Port.udp(2000)); ImmutableSortedMap> expected = ImmutableSortedMap.of("1000/tcp", ImmutableMap.of(), "2000/udp", ImmutableMap.of()); - Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input)); + Assert.assertEquals(expected, ImageToJsonTranslator.portSetToMap(input)); } @Test public void testVolumeListToMap() { - ImmutableList input = - ImmutableList.of( + ImmutableSet input = + ImmutableSet.of( AbsoluteUnixPath.get("/var/job-result-data"), AbsoluteUnixPath.get("/var/log/my-app-logs")); ImmutableSortedMap> expected = ImmutableSortedMap.of( "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); - Assert.assertEquals(expected, ImageToJsonTranslator.volumesListToMap(input)); + Assert.assertEquals(expected, ImageToJsonTranslator.volumesSetToMap(input)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 819c2fd5ff..d3188ce63d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import java.io.IOException; import java.net.URISyntaxException; @@ -89,8 +90,8 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException ImmutableMap.of(), "3000/udp", ImmutableMap.of()); - ImmutableList expected = ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)); - Assert.assertEquals(expected, JsonToImageTranslator.portMapToList(input)); + ImmutableSet expected = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)); + Assert.assertEquals(expected, JsonToImageTranslator.portMapToSet(input)); ImmutableList>> badInputs = ImmutableList.of( @@ -100,7 +101,7 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException ImmutableMap.of("123/xxx", ImmutableMap.of())); for (Map> badInput : badInputs) { try { - JsonToImageTranslator.portMapToList(badInput); + JsonToImageTranslator.portMapToSet(badInput); Assert.fail(); } catch (BadContainerConfigurationFormatException ignored) { } @@ -112,11 +113,11 @@ public void testvolumeMapToList() throws BadContainerConfigurationFormatExceptio ImmutableSortedMap> input = ImmutableSortedMap.of( "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); - ImmutableList expected = - ImmutableList.of( + ImmutableSet expected = + ImmutableSet.of( AbsoluteUnixPath.get("/var/job-result-data"), AbsoluteUnixPath.get("/var/log/my-app-logs")); - Assert.assertEquals(expected, JsonToImageTranslator.volumeMapToList(input)); + Assert.assertEquals(expected, JsonToImageTranslator.volumeMapToSet(input)); ImmutableList>> badInputs = ImmutableList.of( @@ -125,7 +126,7 @@ public void testvolumeMapToList() throws BadContainerConfigurationFormatExceptio ImmutableMap.of("C:/udp", ImmutableMap.of())); for (Map> badInput : badInputs) { try { - JsonToImageTranslator.volumeMapToList(badInput); + JsonToImageTranslator.volumeMapToSet(badInput); Assert.fail(); } catch (BadContainerConfigurationFormatException ignored) { } @@ -208,9 +209,9 @@ private void testToImage_buildable( Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment()); Assert.assertEquals("/some/workspace", image.getWorkingDirectory()); Assert.assertEquals( - ImmutableList.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); + ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)), image.getExposedPorts()); Assert.assertEquals( - ImmutableList.of( + ImmutableSet.of( AbsoluteUnixPath.get("/var/job-result-data"), AbsoluteUnixPath.get("/var/log/my-app-logs")), image.getVolumes()); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 5dea14d70d..e09834eb5d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -41,10 +41,11 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Instant; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; /** @@ -251,11 +252,11 @@ static PluginConfigurationProcessor processCommonConfiguration( .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) .setExposedPorts(ExposedPortsParser.parse(rawConfiguration.getPorts())) - .setVolumes(getVolumesList(rawConfiguration)) + .setVolumes(getVolumesSet(rawConfiguration)) .setLabels(rawConfiguration.getLabels()) .setUser(rawConfiguration.getUser().orElse(null)); getWorkingDirectoryChecked(rawConfiguration) - .ifPresent(workingDirectory -> jibContainerBuilder.setWorkingDirectory(workingDirectory)); + .ifPresent(jibContainerBuilder::setWorkingDirectory); if (rawConfiguration.getUseCurrentTimestamp()) { eventDispatcher.dispatch( LogEvent.warn( @@ -276,16 +277,16 @@ static PluginConfigurationProcessor processCommonConfiguration( } /** - * Parses the list of raw volumes directories to a list of {@link AbsoluteUnixPath} + * Parses the list of raw volumes directories to a set of {@link AbsoluteUnixPath} * * @param rawConfiguration raw configuration data - * @return the list of parsed volumes. + * @return the set of parsed volumes. * @throws InvalidContainerVolumeException if {@code volumes} are not valid absolute Unix paths */ @VisibleForTesting - static List getVolumesList(RawConfiguration rawConfiguration) + static Set getVolumesSet(RawConfiguration rawConfiguration) throws InvalidContainerVolumeException { - List volumes = new ArrayList<>(); + Set volumes = new HashSet<>(); for (String path : rawConfiguration.getVolumes()) { try { AbsoluteUnixPath absoluteUnixPath = AbsoluteUnixPath.get(path); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index fe60e20364..92fbea5175 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -87,11 +88,6 @@ public void setUp() { .thenReturn(containerizer); } - /** - * Test with our default mocks, which try to mimic the bare Gradle configuration. - * - * @throws InvalidWorkingDirectoryException - */ @Test public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, @@ -457,8 +453,8 @@ public void testGetValidVolumesList() throws InvalidContainerVolumeException { Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("/some/root")); Assert.assertEquals( - Collections.singletonList(AbsoluteUnixPath.get("/some/root")), - PluginConfigurationProcessor.getVolumesList(rawConfiguration)); + ImmutableSet.of(AbsoluteUnixPath.get("/some/root")), + PluginConfigurationProcessor.getVolumesSet(rawConfiguration)); } @Test @@ -466,7 +462,7 @@ public void testGetInvalidVolumesList() { Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("`some/root")); try { - PluginConfigurationProcessor.getVolumesList(rawConfiguration); + PluginConfigurationProcessor.getVolumesSet(rawConfiguration); Assert.fail(); } catch (InvalidContainerVolumeException ex) { Assert.assertEquals("`some/root", ex.getMessage()); From a233b27ac7501ef0fa759034612b9551e9bbada0 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 29 Nov 2018 11:15:58 -0500 Subject: [PATCH 0355/2020] Ensure Gradle dependency files exist (#1285) --- .../jib/gradle/GradleLayerConfigurations.java | 16 ++++++++++------ .../gradle/GradleLayerConfigurationsTest.java | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index f20173859e..d84df73947 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -131,12 +131,16 @@ private static JavaLayerConfigurations getForNonWarProject( // Adds dependency files. for (File dependencyFile : dependencyFiles) { - boolean isSnapshot = dependencyFile.getName().contains("SNAPSHOT"); - LayerType layerType = isSnapshot ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; - layerBuilder.addFile( - layerType, - dependencyFile.toPath(), - dependenciesExtractionPath.resolve(dependencyFile.getName())); + if (dependencyFile.exists()) { + boolean isSnapshot = dependencyFile.getName().contains("SNAPSHOT"); + LayerType layerType = isSnapshot ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; + layerBuilder.addFile( + layerType, + dependencyFile.toPath(), + dependenciesExtractionPath.resolve(dependencyFile.getName())); + } else { + logger.info("\t'" + dependencyFile + "' (not found, skipped)"); + } } // Adds all the extra files. diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index aaeffc6802..16c8b4848f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -211,6 +211,22 @@ public void test_noClassesFiles() throws IOException { Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } + @Test + public void test_missingDependencyFiles() throws IOException { + Path nonexistentFile = Paths.get("/nonexistent/dependency"); + FileCollection runtimeFileCollection = + new TestFileCollection(Collections.singleton(nonexistentFile)); + Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); + + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); + GradleLayerConfigurations.getForProject( + mockProject, mockLogger, extraFilesDirectory, Collections.emptyMap(), appRoot); + + Mockito.verify(mockLogger) + .info("Adding corresponding output directories of source sets to image"); + Mockito.verify(mockLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); + } + @Test public void test_extraFiles() throws IOException { JavaLayerConfigurations javaLayerConfigurations = From 386162cbfc1c8a6eb004434d6b1eb4ee1176a5b4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 29 Nov 2018 12:17:16 -0500 Subject: [PATCH 0356/2020] Propagate exposed ports and volumes from base image (#1286) --- .../jib/builder/steps/BuildImageStep.java | 6 ++-- .../google/cloud/tools/jib/image/Image.java | 29 +++++++++++-------- .../jib/image/json/JsonToImageTranslator.java | 4 +-- .../jib/builder/steps/BuildImageStepTest.java | 21 ++++++++++++++ .../cloud/tools/jib/image/ImageTest.java | 9 +++++- .../image/json/ImageToJsonTranslatorTest.java | 4 +-- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 8 files changed, 56 insertions(+), 19 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 47b036ee43..bfc4436409 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -121,6 +121,8 @@ private Image afterCachedLayerSteps() imageBuilder .addEnvironment(baseImage.getEnvironment()) .addLabels(baseImage.getLabels()) + .addExposedPorts(baseImage.getExposedPorts()) + .addVolumes(baseImage.getVolumes()) .setWorkingDirectory(baseImage.getWorkingDirectory()); // Add history elements for non-empty layers that don't have one yet @@ -156,8 +158,8 @@ private Image afterCachedLayerSteps() .setUser(containerConfiguration.getUser()) .setEntrypoint(computeEntrypoint(baseImage, containerConfiguration)) .setProgramArguments(computeProgramArguments(baseImage, containerConfiguration)) - .setExposedPorts(containerConfiguration.getExposedPorts()) - .setVolumes(containerConfiguration.getVolumes()) + .addExposedPorts(containerConfiguration.getExposedPorts()) + .addVolumes(containerConfiguration.getVolumes()) .addLabels(containerConfiguration.getLabels()); if (containerConfiguration.getWorkingDirectory() != null) { imageBuilder.setWorkingDirectory(containerConfiguration.getWorkingDirectory().toString()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index adc699d7b8..51df3263f0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import java.time.Instant; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,12 +43,12 @@ public static class Builder { // values. private final Map environmentBuilder = new HashMap<>(); private final Map labelsBuilder = new HashMap<>(); + private final Set exposedPortsBuilder = new HashSet<>(); + private final Set volumesBuilder = new HashSet<>(); @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; - @Nullable private ImmutableSet exposedPorts; - @Nullable private ImmutableSet volumes; @Nullable private String workingDirectory; @Nullable private String user; @@ -122,24 +123,28 @@ public Builder setProgramArguments(@Nullable List programArguments) { } /** - * Sets the items in the "ExposedPorts" field in the container configuration. + * Adds items to the "ExposedPorts" field in the container configuration. * - * @param exposedPorts the list of exposed ports to add + * @param exposedPorts the exposed ports to add * @return this */ - public Builder setExposedPorts(@Nullable Set exposedPorts) { - this.exposedPorts = (exposedPorts == null) ? null : ImmutableSet.copyOf(exposedPorts); + public Builder addExposedPorts(@Nullable Set exposedPorts) { + if (exposedPorts != null) { + exposedPortsBuilder.addAll(exposedPorts); + } return this; } /** - * Sets the items in the "Volumes" field in the container configuration. + * Adds items to the "Volumes" field in the container configuration. * - * @param volumes the list of directories to create a volume. + * @param volumes the directories to create volumes * @return this */ - public Builder setVolumes(@Nullable Set volumes) { - this.volumes = (volumes == null) ? null : ImmutableSet.copyOf(volumes); + public Builder addVolumes(@Nullable Set volumes) { + if (volumes != null) { + volumesBuilder.addAll(ImmutableSet.copyOf(volumes)); + } return this; } @@ -210,8 +215,8 @@ public Image build() { ImmutableMap.copyOf(environmentBuilder), entrypoint, programArguments, - exposedPorts, - volumes, + ImmutableSet.copyOf(exposedPortsBuilder), + ImmutableSet.copyOf(volumesBuilder), ImmutableMap.copyOf(labelsBuilder), workingDirectory, user); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index d6896bc5ee..452d590aac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -147,12 +147,12 @@ public static Image toImage( } if (containerConfigurationTemplate.getContainerExposedPorts() != null) { - imageBuilder.setExposedPorts( + imageBuilder.addExposedPorts( portMapToSet(containerConfigurationTemplate.getContainerExposedPorts())); } if (containerConfigurationTemplate.getContainerVolumes() != null) { - imageBuilder.setVolumes(volumeMapToSet(containerConfigurationTemplate.getContainerVolumes())); + imageBuilder.addVolumes(volumeMapToSet(containerConfigurationTemplate.getContainerVolumes())); } if (containerConfigurationTemplate.getContainerEnvironment() != null) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 691b68ab16..7ac3b58396 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -131,6 +132,10 @@ public BlobDescriptor getBlobDescriptor() { .setWorkingDirectory("/base/working/directory") .setEntrypoint(ImmutableList.of("baseImageEntrypoint")) .setProgramArguments(ImmutableList.of("catalina.sh", "run")) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.udp(2000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/base/path1"), AbsoluteUnixPath.get("/base/path2"))) .addHistory(nonEmptyLayerHistory) .addHistory(emptyLayerHistory) .addHistory(emptyLayerHistory) @@ -192,6 +197,12 @@ public void test_propagateBaseImageConfiguration() .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE")); Mockito.when(mockContainerConfiguration.getLabels()) .thenReturn(ImmutableMap.of("my.label", "my.label.value", "base.label.2", "new.value")); + Mockito.when(mockContainerConfiguration.getExposedPorts()) + .thenReturn(ImmutableSet.of(Port.tcp(3000), Port.udp(4000))); + Mockito.when(mockContainerConfiguration.getVolumes()) + .thenReturn( + ImmutableSet.of( + AbsoluteUnixPath.get("/new/path1"), AbsoluteUnixPath.get("/new/path2"))); BuildImageStep buildImageStep = new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), @@ -216,6 +227,16 @@ public void test_propagateBaseImageConfiguration() "base.label.2", "new.value"), image.getLabels()); + Assert.assertEquals( + ImmutableSet.of(Port.tcp(1000), Port.udp(2000), Port.tcp(3000), Port.udp(4000)), + image.getExposedPorts()); + Assert.assertEquals( + ImmutableSet.of( + AbsoluteUnixPath.get("/base/path1"), + AbsoluteUnixPath.get("/base/path2"), + AbsoluteUnixPath.get("/new/path1"), + AbsoluteUnixPath.get("/new/path2")), + image.getVolumes()); Assert.assertEquals("/base/working/directory", image.getWorkingDirectory()); Assert.assertEquals("root", image.getUser()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 83106841cc..0fd5d2a0a8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.time.Instant; @@ -52,7 +53,10 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) .setProgramArguments(Arrays.asList("arg1", "arg2")) - .setExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000))) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/a/path"), AbsoluteUnixPath.get("/another/path"))) .setUser("john") .addLayer(mockLayer) .build(); @@ -65,6 +69,9 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getProgramArguments()); Assert.assertEquals(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); + Assert.assertEquals( + ImmutableSet.of(AbsoluteUnixPath.get("/a/path"), AbsoluteUnixPath.get("/another/path")), + image.getVolumes()); Assert.assertEquals("john", image.getUser()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 82c52c68c6..d4b113050f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -61,9 +61,9 @@ public void setUp() throws DigestException, LayerPropertyNotFoundException { testImageBuilder.addEnvironmentVariable("VAR2", "VAL2"); testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); testImageBuilder.setProgramArguments(Arrays.asList("arg1", "arg2")); - testImageBuilder.setExposedPorts( + testImageBuilder.addExposedPorts( ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); - testImageBuilder.setVolumes( + testImageBuilder.addVolumes( ImmutableSet.of( AbsoluteUnixPath.get("/var/job-result-data"), AbsoluteUnixPath.get("/var/log/my-app-logs"))); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 90ef8e567f..4d08d12772 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. - `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a7050a1568..108880677f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. - `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed From 29480e0a68be41e36339d9af99193f8debd0bb4a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 30 Nov 2018 11:46:53 -0500 Subject: [PATCH 0357/2020] Add FAQ entry for containerizing executable JAR (#1287) --- docs/faq.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index be5c8020d8..ec5359535a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,6 +10,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint)\ +[I want to containerize an executable JAR.](#i-want-to-containerize-an-executable-jar)\ [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ @@ -153,6 +154,10 @@ See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.for Normally, the plugin sets a default entrypoint for java applications, or lets you configure a custom entrypoint using the `container.entrypoint` configuration parameter. You can also override the default/configured entrypoint by defining a custom entrypoint when running the container. See [`docker run --entrypoint` reference](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) for running the image with Docker and overriding the entrypoint command, or see [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. +### I want to containerize an executable JAR. + +Although it is possible to configure Jib to add a JAR to the container and run it using a custom `java -jar ...` or `java -cp ...` entrypoint command, the intention of Jib is to add individual class files and dependency JARs into the container instead of putting a runnable JAR into the container. This lets Jib choose an opinionated, optimal layout for the application on the container image, which also allows it to skip the extra JAR-packaging step. + ### Where is the application in the container filesystem? Jib packages your Java application into the following paths on the image: From f59326e914c7f59a2aa2831994deff6c64afabb7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 30 Nov 2018 13:23:42 -0500 Subject: [PATCH 0358/2020] Add Healthcheck to ContainerConfigurationTemplate (#1217) --- .../jib/builder/steps/BuildImageStep.java | 2 +- .../jib/configuration/DockerHealthCheck.java | 25 ++--- .../jib/docker/ImageToTarballTranslator.java | 2 +- .../google/cloud/tools/jib/image/Image.java | 44 ++++++++- .../json/ContainerConfigurationTemplate.java | 92 +++++++++++++++++++ .../jib/image/json/ImageToJsonTranslator.java | 82 +++++++++-------- .../jib/image/json/JsonToImageTranslator.java | 4 +- .../jib/builder/steps/BuildImageStepTest.java | 3 +- .../configuration/DockerHealthCheckTest.java | 19 ++-- .../docker/ImageToTarballTranslatorTest.java | 4 +- .../cloud/tools/jib/image/ImageTest.java | 4 +- .../ContainerConfigurationTemplateTest.java | 18 ++++ .../image/json/ImageToJsonTranslatorTest.java | 56 ++++++----- .../test/resources/json/containerconfig.json | 2 +- .../json/translated_v22manifest.json | 2 +- 15 files changed, 263 insertions(+), 96 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index bfc4436409..6a1c9b2e1b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -98,7 +98,7 @@ private Image afterCachedLayerSteps() try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. - Image.Builder imageBuilder = Image.builder(); + Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java index baa3696b2c..40c28ac642 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.configuration; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.time.Duration; import java.util.Arrays; @@ -29,13 +30,14 @@ public class DockerHealthCheck { /** Builds the immutable {@link DockerHealthCheck}. */ public static class Builder { - @Nullable private final ImmutableList command; + private final ImmutableList command; @Nullable private Duration interval; @Nullable private Duration timeout; @Nullable private Duration startPeriod; @Nullable private Integer retries; - private Builder(@Nullable ImmutableList command) { + private Builder(ImmutableList command) { + Preconditions.checkArgument(command.size() > 1, "command must not be empty"); this.command = command; } @@ -99,13 +101,13 @@ public static DockerHealthCheck disabled() { } /** - * Creates a new {@link DockerHealthCheck.Builder} with the command set to be inherited from the - * base image (corresponds to empty list in container config). + * Creates a new {@link DockerHealthCheck} with the command set to be inherited from the base + * image. * - * @return the new {@link DockerHealthCheck.Builder} + * @return the new {@link DockerHealthCheck} */ - public static Builder builderWithInheritedCommand() { - return new Builder(null); + public static DockerHealthCheck inherited() { + return new DockerHealthCheck(ImmutableList.of(), null, null, null, null); } /** @@ -140,17 +142,18 @@ public static Builder builderWithExecCommand(String... command) { * @return the new {@link DockerHealthCheck.Builder} */ public static Builder builderWithShellCommand(String command) { + Preconditions.checkArgument(!command.trim().isEmpty(), "command must not be empty/whitespace"); return new Builder(ImmutableList.of("CMD-SHELL", command)); } - @Nullable private final ImmutableList command; + private final ImmutableList command; @Nullable private final Duration interval; @Nullable private final Duration timeout; @Nullable private final Duration startPeriod; @Nullable private final Integer retries; private DockerHealthCheck( - @Nullable ImmutableList command, + ImmutableList command, @Nullable Duration interval, @Nullable Duration timeout, @Nullable Duration startPeriod, @@ -168,8 +171,8 @@ private DockerHealthCheck( * * @return the healthcheck command */ - public Optional> getCommand() { - return Optional.ofNullable(command); + public List getCommand() { + return command; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java index edc6083fe7..c87e876a12 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java @@ -45,7 +45,7 @@ public class ImageToTarballTranslator { /** * Instantiate with an {@link Image}. * - * @param image the image to convert into a tarball. + * @param image the image to convert into a tarball */ public ImageToTarballTranslator(Image image) { this.image = image; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 51df3263f0..8cb0bd9cbf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -16,9 +16,11 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -36,6 +38,7 @@ public class Image { /** Builds the immutable {@link Image}. */ public static class Builder { + private final Class imageFormat; private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); @@ -49,9 +52,14 @@ public static class Builder { @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; + private DockerHealthCheck healthCheck = DockerHealthCheck.inherited(); @Nullable private String workingDirectory; @Nullable private String user; + private Builder(Class imageFormat) { + this.imageFormat = imageFormat; + } + /** * Sets the image creation time. * @@ -122,6 +130,17 @@ public Builder setProgramArguments(@Nullable List programArguments) { return this; } + /** + * Sets the container's healthcheck configuration. + * + * @param healthCheck the healthcheck configuration + * @return this + */ + public Builder setHealthCheck(DockerHealthCheck healthCheck) { + this.healthCheck = healthCheck; + return this; + } + /** * Adds items to the "ExposedPorts" field in the container configuration. * @@ -209,12 +228,14 @@ public Builder addHistory(HistoryEntry history) { public Image build() { return new Image<>( + imageFormat, created, imageLayersBuilder.build(), historyBuilder.build(), ImmutableMap.copyOf(environmentBuilder), entrypoint, programArguments, + healthCheck, ImmutableSet.copyOf(exposedPortsBuilder), ImmutableSet.copyOf(volumesBuilder), ImmutableMap.copyOf(labelsBuilder), @@ -223,10 +244,14 @@ public Image build() { } } - public static Builder builder() { - return new Builder<>(); + public static Builder builder( + Class imageFormat) { + return new Builder<>(imageFormat); } + /** The image format. */ + private final Class imageFormat; + /** The image creation time. */ @Nullable private final Instant created; @@ -245,6 +270,9 @@ public static Builder builder() { /** Arguments to append to the image entrypoint when running the image. */ @Nullable private final ImmutableList programArguments; + /** Healthcheck configuration. */ + private final DockerHealthCheck healthCheck; + /** Ports that the container listens on. */ @Nullable private final ImmutableSet exposedPorts; @@ -261,23 +289,27 @@ public static Builder builder() { @Nullable private final String user; private Image( + Class imageFormat, @Nullable Instant created, ImageLayers layers, ImmutableList history, @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @Nullable ImmutableList programArguments, + DockerHealthCheck healthCheck, @Nullable ImmutableSet exposedPorts, @Nullable ImmutableSet volumes, @Nullable ImmutableMap labels, @Nullable String workingDirectory, @Nullable String user) { + this.imageFormat = imageFormat; this.created = created; this.layers = layers; this.history = history; this.environment = environment; this.entrypoint = entrypoint; this.programArguments = programArguments; + this.healthCheck = healthCheck; this.exposedPorts = exposedPorts; this.volumes = volumes; this.labels = labels; @@ -285,6 +317,10 @@ private Image( this.user = user; } + public Class getImageFormat() { + return this.imageFormat; + } + @Nullable public Instant getCreated() { return created; @@ -305,6 +341,10 @@ public ImmutableList getProgramArguments() { return programArguments; } + public DockerHealthCheck getHealthCheck() { + return healthCheck; + } + @Nullable public ImmutableSet getExposedPorts() { return exposedPorts; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 143d7344b6..9a7887ed3e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,6 +40,13 @@ * "Env": ["/usr/bin/java"], * "Entrypoint": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], * "Cmd": ["arg1", "arg2"], + * "Healthcheck": { + * "Test": ["CMD-SHELL", "/usr/bin/check-health localhost"], + * "Interval": 30000000000, + * "Timeout": 10000000000, + * "StartPeriod": 0, + * "Retries": 3 + * } * "ExposedPorts": { "6000/tcp":{}, "8000/tcp":{}, "9000/tcp":{} }, * "Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}}, * "Labels": { "com.example.label": "value" }, @@ -104,6 +112,9 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { /** Arguments to pass into main. */ @Nullable private List Cmd; + /** Healthcheck. */ + @Nullable private HealthCheckObjectTemplate Healthcheck; + /** Network ports the container exposes. */ @Nullable private Map> ExposedPorts; @@ -120,6 +131,27 @@ private static class ConfigurationObjectTemplate implements JsonTemplate { @Nullable private Map> Volumes; } + /** Template for inner JSON object representing the healthcheck configuration. */ + private static class HealthCheckObjectTemplate implements JsonTemplate { + + /** The test to perform to check that the container is healthy. */ + @Nullable private List Test; + + /** Number of nanoseconds to wait between probe attempts. */ + @Nullable private Long Interval; + + /** Number of nanoseconds to wait before considering the check to have hung. */ + @Nullable private Long Timeout; + + /** + * Number of nanoseconds to wait for the container to initialize before starting health-retries. + */ + @Nullable private Long StartPeriod; + + /** The number of consecutive failures needed to consider the container as unhealthy. */ + @Nullable private Integer Retries; + } + /** * Template for inner JSON object representing the filesystem changesets used to build the * container filesystem. @@ -152,6 +184,41 @@ public void setContainerCmd(@Nullable List cmd) { config.Cmd = cmd; } + public void setContainerHealthCheckTest(List test) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Test = test; + } + + public void setContainerHealthCheckInterval(@Nullable Long interval) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Interval = interval; + } + + public void setContainerHealthCheckTimeout(@Nullable Long timeout) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Timeout = timeout; + } + + public void setContainerHealthCheckStartPeriod(@Nullable Long startPeriod) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).StartPeriod = startPeriod; + } + + public void setContainerHealthCheckRetries(@Nullable Integer retries) { + if (config.Healthcheck == null) { + config.Healthcheck = new HealthCheckObjectTemplate(); + } + Preconditions.checkNotNull(config.Healthcheck).Retries = retries; + } + public void setContainerExposedPorts(@Nullable Map> exposedPorts) { config.ExposedPorts = exposedPorts; } @@ -208,6 +275,31 @@ List getContainerCmd() { return config.Cmd; } + @Nullable + List getContainerHealthTest() { + return config.Healthcheck == null ? null : config.Healthcheck.Test; + } + + @Nullable + Long getContainerHealthInterval() { + return config.Healthcheck == null ? null : config.Healthcheck.Interval; + } + + @Nullable + Long getContainerHealthTimeout() { + return config.Healthcheck == null ? null : config.Healthcheck.Timeout; + } + + @Nullable + Long getContainerHealthStartPeriod() { + return config.Healthcheck == null ? null : config.Healthcheck.StartPeriod; + } + + @Nullable + Integer getContainerHealthRetries() { + return config.Healthcheck == null ? null : config.Healthcheck.Retries; + } + @Nullable Map> getContainerExposedPorts() { return config.ExposedPorts; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 550fca5090..3be70e2be2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -78,6 +79,27 @@ public class ImageToJsonTranslator { return setToMap(volumes, AbsoluteUnixPath::toString); } + /** + * Converts the map of environment variables to a list with items in the format "NAME=VALUE". + * + * @return the list + */ + @VisibleForTesting + @Nullable + static ImmutableList environmentMapToList(@Nullable Map environment) { + if (environment == null) { + return null; + } + Preconditions.checkArgument( + environment.keySet().stream().noneMatch(key -> key.contains("=")), + "Illegal environment variable: name cannot contain '='"); + return environment + .entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(ImmutableList.toImmutableList()); + } + /** * Turns a set into a sorted map where each element of the set is mapped to an entry composed by * the key generated with {@code Function elementMapper} and an empty map as value. @@ -107,33 +129,12 @@ public class ImageToJsonTranslator { String::compareTo, keyMapper, ignored -> Collections.emptyMap())); } - /** - * Converts the map of environment variables to a list with items in the format "NAME=VALUE". - * - * @return the list - */ - @VisibleForTesting - @Nullable - static ImmutableList environmentMapToList(@Nullable Map environment) { - if (environment == null) { - return null; - } - Preconditions.checkArgument( - environment.keySet().stream().noneMatch(key -> key.contains("=")), - "Illegal environment variable: name cannot contain '='"); - return environment - .entrySet() - .stream() - .map(entry -> entry.getKey() + "=" + entry.getValue()) - .collect(ImmutableList.toImmutableList()); - } - private final Image image; /** * Instantiate with an {@link Image}. * - * @param image the image to translate. + * @param image the image to translate */ public ImageToJsonTranslator(Image image) { this.image = image; @@ -142,7 +143,7 @@ public ImageToJsonTranslator(Image image) { /** * Gets the container configuration as a {@link Blob}. * - * @return the container configuration {@link Blob}. + * @return the container configuration {@link Blob} */ public Blob getContainerConfigurationBlob() { // Set up the JSON template. @@ -158,33 +159,34 @@ public Blob getContainerConfigurationBlob() { template.addHistoryEntry(historyObject); } - // Sets the creation time. Instant#toString() returns an ISO-8601 formatted string. template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); - - // Adds the environment variables. template.setContainerEnvironment(environmentMapToList(image.getEnvironment())); - - // Sets the entrypoint. template.setContainerEntrypoint(image.getEntrypoint()); - - // Sets the main method arguments. template.setContainerCmd(image.getProgramArguments()); - - // Sets the exposed ports. template.setContainerExposedPorts(portSetToMap(image.getExposedPorts())); - - // Sets the volumes. template.setContainerVolumes(volumesSetToMap(image.getVolumes())); - - // Sets the labels. template.setContainerLabels(image.getLabels()); - - // Sets the working directory. template.setContainerWorkingDir(image.getWorkingDirectory()); - - // Sets the user. template.setContainerUser(image.getUser()); + // Ignore healthcheck if not Docker/command is empty + DockerHealthCheck healthCheck = image.getHealthCheck(); + if (image.getImageFormat() == V22ManifestTemplate.class + && !healthCheck.getCommand().isEmpty()) { + template.setContainerHealthCheckTest(healthCheck.getCommand()); + healthCheck + .getInterval() + .ifPresent(interval -> template.setContainerHealthCheckInterval(interval.toNanos())); + healthCheck + .getTimeout() + .ifPresent(timeout -> template.setContainerHealthCheckTimeout(timeout.toNanos())); + healthCheck + .getStartPeriod() + .ifPresent( + startPeriod -> template.setContainerHealthCheckStartPeriod(startPeriod.toNanos())); + template.setContainerHealthCheckRetries(healthCheck.getRetries().orElse(null)); + } + // Serializes into JSON. return JsonTemplateMapper.toBlob(template); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 452d590aac..6575d6064b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -68,7 +68,7 @@ public class JsonToImageTranslator { */ public static Image toImage(V21ManifestTemplate manifestTemplate) throws LayerPropertyNotFoundException { - Image.Builder imageBuilder = Image.builder(); + Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); for (DescriptorDigest digest : manifestTemplate.getLayerDigests()) { imageBuilder.addLayer(new DigestOnlyLayer(digest)); @@ -117,7 +117,7 @@ public static Image toImage( "Mismatch between image manifest and container configuration"); } - Image.Builder imageBuilder = Image.builder(); + Image.Builder imageBuilder = Image.builder(manifestTemplate.getClass()); for (int layerIndex = 0; layerIndex < layers.size(); layerIndex++) { ReferenceNoDiffIdLayer noDiffIdLayer = layers.get(layerIndex); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 7ac3b58396..65659f3cd6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -125,7 +126,7 @@ public BlobDescriptor getBlobDescriptor() { .build(); Image baseImage = - Image.builder() + Image.builder(V22ManifestTemplate.class) .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "BASE_ENV_2", "DEFAULT")) .addLabel("base.label", "base.label.value") .addLabel("base.label.2", "default") diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java index 91afd759d5..c03fe5d8a5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java @@ -27,7 +27,7 @@ public class DockerHealthCheckTest { @Test public void testBuild_parameters() { DockerHealthCheck healthCheck = - DockerHealthCheck.builderWithInheritedCommand() + DockerHealthCheck.builderWithShellCommand("echo hi") .setInterval(Duration.ofNanos(123)) .setTimeout(Duration.ofNanos(456)) .setStartPeriod(Duration.ofNanos(789)) @@ -46,39 +46,34 @@ public void testBuild_parameters() { @Test public void testBuild_propagated() { - DockerHealthCheck healthCheck = DockerHealthCheck.builderWithInheritedCommand().build(); - Assert.assertFalse(healthCheck.getCommand().isPresent()); + DockerHealthCheck healthCheck = DockerHealthCheck.inherited(); + Assert.assertTrue(healthCheck.getCommand().isEmpty()); } @Test public void testBuild_execArray() { DockerHealthCheck healthCheck = DockerHealthCheck.builderWithExecCommand("test", "command").build(); - Assert.assertTrue(healthCheck.getCommand().isPresent()); - Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand().get()); + Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand()); } @Test public void testBuild_execList() { DockerHealthCheck healthCheck = DockerHealthCheck.builderWithExecCommand(ImmutableList.of("test", "command")).build(); - Assert.assertTrue(healthCheck.getCommand().isPresent()); - Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand().get()); + Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand()); } @Test public void testBuild_shell() { DockerHealthCheck healthCheck = DockerHealthCheck.builderWithShellCommand("shell command").build(); - Assert.assertTrue(healthCheck.getCommand().isPresent()); - Assert.assertEquals( - ImmutableList.of("CMD-SHELL", "shell command"), healthCheck.getCommand().get()); + Assert.assertEquals(ImmutableList.of("CMD-SHELL", "shell command"), healthCheck.getCommand()); } @Test public void testDisabled() { DockerHealthCheck healthCheck = DockerHealthCheck.disabled(); - Assert.assertTrue(healthCheck.getCommand().isPresent()); - Assert.assertEquals(ImmutableList.of("NONE"), healthCheck.getCommand().get()); + Assert.assertEquals(ImmutableList.of("NONE"), healthCheck.getCommand()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java index a15a7f33ff..564d77736d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.CharStreams; import com.google.common.io.Resources; @@ -79,7 +80,8 @@ public void testToTarballBlob() Mockito.when(mockLayer2.getBlobDescriptor()) .thenReturn(new BlobDescriptor(fileBSize, fakeDigestB)); Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); - Image testImage = Image.builder().addLayer(mockLayer1).addLayer(mockLayer2).build(); + Image testImage = + Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); Blob tarballBlob = new ImageToTarballTranslator(testImage).toTarballBlob(ImageReference.parse("my/image:tag")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 0fd5d2a0a8..13fb259257 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.time.Instant; @@ -47,7 +48,7 @@ public void setUp() throws LayerPropertyNotFoundException { @Test public void test_smokeTest() throws LayerPropertyNotFoundException { Image image = - Image.builder() + Image.builder(V22ManifestTemplate.class) .setCreated(Instant.ofEpochSecond(10000)) .addEnvironmentVariable("crepecake", "is great") .addEnvironmentVariable("VARIABLE", "VALUE") @@ -61,6 +62,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { .addLayer(mockLayer) .build(); + Assert.assertEquals(V22ManifestTemplate.class, image.getImageFormat()); Assert.assertEquals( mockDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest()); Assert.assertEquals(Instant.ofEpochSecond(10000), image.getCreated()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 3ff2dfc877..e688d2ede4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -51,6 +51,11 @@ public void testToJson() throws IOException, URISyntaxException, DigestException containerConfigJson.setContainerEnvironment(Arrays.asList("VAR1=VAL1", "VAR2=VAL2")); containerConfigJson.setContainerEntrypoint(Arrays.asList("some", "entrypoint", "command")); containerConfigJson.setContainerCmd(Arrays.asList("arg1", "arg2")); + containerConfigJson.setContainerHealthCheckTest(Arrays.asList("CMD-SHELL", "/checkhealth")); + containerConfigJson.setContainerHealthCheckInterval(3000000000L); + containerConfigJson.setContainerHealthCheckTimeout(1000000000L); + containerConfigJson.setContainerHealthCheckStartPeriod(2000000000L); + containerConfigJson.setContainerHealthCheckRetries(3); containerConfigJson.setContainerExposedPorts( ImmutableSortedMap.of( "1000/tcp", @@ -106,6 +111,19 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti Arrays.asList("some", "entrypoint", "command"), containerConfigJson.getContainerEntrypoint()); Assert.assertEquals(Arrays.asList("arg1", "arg2"), containerConfigJson.getContainerCmd()); + + Assert.assertEquals( + Arrays.asList("CMD-SHELL", "/checkhealth"), containerConfigJson.getContainerHealthTest()); + Assert.assertNotNull(containerConfigJson.getContainerHealthInterval()); + Assert.assertEquals(3000000000L, containerConfigJson.getContainerHealthInterval().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthTimeout()); + Assert.assertEquals(1000000000L, containerConfigJson.getContainerHealthTimeout().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthStartPeriod()); + Assert.assertEquals( + 2000000000L, containerConfigJson.getContainerHealthStartPeriod().longValue()); + Assert.assertNotNull(containerConfigJson.getContainerHealthRetries()); + Assert.assertEquals(3, containerConfigJson.getContainerHealthRetries().intValue()); + Assert.assertEquals( ImmutableMap.of("key1", "value1", "key2", "value2"), containerConfigJson.getContainerLabels()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index d4b113050f..bccc92d371 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -40,11 +41,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Map; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; /** Tests for {@link ImageToJsonTranslator}. */ @@ -52,24 +53,30 @@ public class ImageToJsonTranslatorTest { private ImageToJsonTranslator imageToJsonTranslator; - @Before - public void setUp() throws DigestException, LayerPropertyNotFoundException { - Image.Builder testImageBuilder = Image.builder(); - - testImageBuilder.setCreated(Instant.ofEpochSecond(20)); - testImageBuilder.addEnvironmentVariable("VAR1", "VAL1"); - testImageBuilder.addEnvironmentVariable("VAR2", "VAL2"); - testImageBuilder.setEntrypoint(Arrays.asList("some", "entrypoint", "command")); - testImageBuilder.setProgramArguments(Arrays.asList("arg1", "arg2")); - testImageBuilder.addExposedPorts( - ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))); - testImageBuilder.addVolumes( - ImmutableSet.of( - AbsoluteUnixPath.get("/var/job-result-data"), - AbsoluteUnixPath.get("/var/log/my-app-logs"))); - testImageBuilder.addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")); - testImageBuilder.setWorkingDirectory("/some/workspace"); - testImageBuilder.setUser("tomcat"); + private void setUp(Class imageFormat) + throws DigestException, LayerPropertyNotFoundException { + Image.Builder testImageBuilder = + Image.builder(imageFormat) + .setCreated(Instant.ofEpochSecond(20)) + .addEnvironmentVariable("VAR1", "VAL1") + .addEnvironmentVariable("VAR2", "VAL2") + .setEntrypoint(Arrays.asList("some", "entrypoint", "command")) + .setProgramArguments(Arrays.asList("arg1", "arg2")) + .setHealthCheck( + DockerHealthCheck.builderWithShellCommand("/checkhealth") + .setInterval(Duration.ofSeconds(3)) + .setTimeout(Duration.ofSeconds(1)) + .setStartPeriod(Duration.ofSeconds(2)) + .setRetries(3) + .build()) + .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000))) + .addVolumes( + ImmutableSet.of( + AbsoluteUnixPath.get("/var/job-result-data"), + AbsoluteUnixPath.get("/var/log/my-app-logs"))) + .addLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) + .setWorkingDirectory("/some/workspace") + .setUser("tomcat"); DescriptorDigest fakeDigest = DescriptorDigest.fromDigest( @@ -109,7 +116,10 @@ public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { } @Test - public void testGetContainerConfiguration() throws IOException, URISyntaxException { + public void testGetContainerConfiguration() + throws IOException, URISyntaxException, DigestException { + setUp(V22ManifestTemplate.class); + // Loads the expected JSON string. Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); @@ -125,12 +135,14 @@ public void testGetContainerConfiguration() throws IOException, URISyntaxExcepti } @Test - public void testGetManifest_v22() throws URISyntaxException, IOException { + public void testGetManifest_v22() throws URISyntaxException, IOException, DigestException { + setUp(V22ManifestTemplate.class); testGetManifest(V22ManifestTemplate.class, "json/translated_v22manifest.json"); } @Test - public void testGetManifest_oci() throws URISyntaxException, IOException { + public void testGetManifest_oci() throws URISyntaxException, IOException, DigestException { + setUp(OCIManifestTemplate.class); testGetManifest(OCIManifestTemplate.class, "json/translated_ocimanifest.json"); } diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/json/containerconfig.json index b43afe5e65..9c6bd20a55 100644 --- a/jib-core/src/test/resources/json/containerconfig.json +++ b/jib-core/src/test/resources/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"Healthcheck":{"Test":["CMD-SHELL","/checkhealth"],"Interval":3000000000,"Timeout":1000000000,"StartPeriod":2000000000,"Retries":3},"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/json/translated_v22manifest.json index e1da31cdd3..4a2f6dbaf3 100644 --- a/jib-core/src/test/resources/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:39feb4ae7cd3041613e19fcc33c7ec9bd14e6a2bd6fd182c404cd1ba9e20f00d","size":690},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:7bf20598dd1d70fd26a4ff5c60106e021f48aabef7abc201d6d0a5d38f55b067","size":822},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file From 36c152a232eb5b1af1f96c0a86d0062995ff8301 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 3 Dec 2018 10:27:55 -0500 Subject: [PATCH 0359/2020] Inherit Healthcheck from base image (#1288) --- .../jib/builder/steps/BuildImageStep.java | 1 + .../jib/configuration/DockerHealthCheck.java | 59 +++---------------- .../google/cloud/tools/jib/image/Image.java | 9 +-- .../jib/image/json/ImageToJsonTranslator.java | 3 +- .../jib/image/json/JsonToImageTranslator.java | 23 ++++++++ .../jib/builder/steps/BuildImageStepTest.java | 20 +++++++ .../configuration/DockerHealthCheckTest.java | 47 +++++---------- .../image/json/ImageToJsonTranslatorTest.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 10 files changed, 76 insertions(+), 90 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 6a1c9b2e1b..08747e45cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -121,6 +121,7 @@ private Image afterCachedLayerSteps() imageBuilder .addEnvironment(baseImage.getEnvironment()) .addLabels(baseImage.getLabels()) + .setHealthCheck(baseImage.getHealthCheck()) .addExposedPorts(baseImage.getExposedPorts()) .addVolumes(baseImage.getVolumes()) .setWorkingDirectory(baseImage.getWorkingDirectory()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java index 40c28ac642..9cb130ba51 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java @@ -19,7 +19,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.time.Duration; -import java.util.Arrays; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; @@ -37,7 +36,6 @@ public static class Builder { @Nullable private Integer retries; private Builder(ImmutableList command) { - Preconditions.checkArgument(command.size() > 1, "command must not be empty"); this.command = command; } @@ -92,58 +90,15 @@ public DockerHealthCheck build() { } /** - * Creates a disabled {@link DockerHealthCheck} (corresponds to "NONE" in container config). + * Creates a new {@link DockerHealthCheck.Builder} with the specified command. * - * @return the new {@link DockerHealthCheck} + * @param command the command + * @return a new {@link DockerHealthCheck.Builder} */ - public static DockerHealthCheck disabled() { - return new DockerHealthCheck(ImmutableList.of("NONE"), null, null, null, null); - } - - /** - * Creates a new {@link DockerHealthCheck} with the command set to be inherited from the base - * image. - * - * @return the new {@link DockerHealthCheck} - */ - public static DockerHealthCheck inherited() { - return new DockerHealthCheck(ImmutableList.of(), null, null, null, null); - } - - /** - * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be - * directly executed (corresponds to "CMD" in container config). - * - * @param command the healthcheck command to execute - * @return the new {@link DockerHealthCheck.Builder} - */ - public static Builder builderWithExecCommand(List command) { - return new Builder(ImmutableList.builder().add("CMD").addAll(command).build()); - } - - /** - * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be - * directly executed (corresponds to "CMD" in container config). - * - * @param command the healthcheck command to execute - * @return the new {@link DockerHealthCheck.Builder} - */ - public static Builder builderWithExecCommand(String... command) { - return new Builder( - ImmutableList.builder().add("CMD").addAll(Arrays.asList(command)).build()); - } - - /** - * Creates a new {@link DockerHealthCheck.Builder} with the specified healthcheck command to be - * run by the container's default shell (corresponds to "CMD-SHELL" in container config). This - * command cannot be run on containers with no default shell. - * - * @param command the shell command to run - * @return the new {@link DockerHealthCheck.Builder} - */ - public static Builder builderWithShellCommand(String command) { - Preconditions.checkArgument(!command.trim().isEmpty(), "command must not be empty/whitespace"); - return new Builder(ImmutableList.of("CMD-SHELL", command)); + public static DockerHealthCheck.Builder fromCommand(List command) { + Preconditions.checkArgument(command.size() > 0, "command must not be empty"); + Preconditions.checkArgument(!command.contains(null), "command must not contain null elements"); + return new Builder(ImmutableList.copyOf(command)); } private final ImmutableList command; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 8cb0bd9cbf..2e5389d906 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -52,7 +52,7 @@ public static class Builder { @Nullable private Instant created; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; - private DockerHealthCheck healthCheck = DockerHealthCheck.inherited(); + @Nullable private DockerHealthCheck healthCheck; @Nullable private String workingDirectory; @Nullable private String user; @@ -136,7 +136,7 @@ public Builder setProgramArguments(@Nullable List programArguments) { * @param healthCheck the healthcheck configuration * @return this */ - public Builder setHealthCheck(DockerHealthCheck healthCheck) { + public Builder setHealthCheck(@Nullable DockerHealthCheck healthCheck) { this.healthCheck = healthCheck; return this; } @@ -271,7 +271,7 @@ public static Builder builder( @Nullable private final ImmutableList programArguments; /** Healthcheck configuration. */ - private final DockerHealthCheck healthCheck; + @Nullable private final DockerHealthCheck healthCheck; /** Ports that the container listens on. */ @Nullable private final ImmutableSet exposedPorts; @@ -296,7 +296,7 @@ private Image( @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @Nullable ImmutableList programArguments, - DockerHealthCheck healthCheck, + @Nullable DockerHealthCheck healthCheck, @Nullable ImmutableSet exposedPorts, @Nullable ImmutableSet volumes, @Nullable ImmutableMap labels, @@ -341,6 +341,7 @@ public ImmutableList getProgramArguments() { return programArguments; } + @Nullable public DockerHealthCheck getHealthCheck() { return healthCheck; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 3be70e2be2..e64fbd86c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -171,8 +171,7 @@ public Blob getContainerConfigurationBlob() { // Ignore healthcheck if not Docker/command is empty DockerHealthCheck healthCheck = image.getHealthCheck(); - if (image.getImageFormat() == V22ManifestTemplate.class - && !healthCheck.getCommand().isEmpty()) { + if (image.getImageFormat() == V22ManifestTemplate.class && healthCheck != null) { template.setContainerHealthCheckTest(healthCheck.getCommand()); healthCheck .getInterval() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 6575d6064b..5f1a1d2a0c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.image.json; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -29,6 +30,7 @@ import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.ArrayList; @@ -146,6 +148,27 @@ public static Image toImage( imageBuilder.setProgramArguments(containerConfigurationTemplate.getContainerCmd()); } + List baseHealthCheckCommand = containerConfigurationTemplate.getContainerHealthTest(); + if (baseHealthCheckCommand != null) { + DockerHealthCheck.Builder builder = DockerHealthCheck.fromCommand(baseHealthCheckCommand); + if (containerConfigurationTemplate.getContainerHealthInterval() != null) { + builder.setInterval( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthInterval())); + } + if (containerConfigurationTemplate.getContainerHealthTimeout() != null) { + builder.setTimeout( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthTimeout())); + } + if (containerConfigurationTemplate.getContainerHealthStartPeriod() != null) { + builder.setStartPeriod( + Duration.ofNanos(containerConfigurationTemplate.getContainerHealthStartPeriod())); + } + if (containerConfigurationTemplate.getContainerHealthRetries() != null) { + builder.setRetries(containerConfigurationTemplate.getContainerHealthRetries()); + } + imageBuilder.setHealthCheck(builder.build()); + } + if (containerConfigurationTemplate.getContainerExposedPorts() != null) { imageBuilder.addExposedPorts( portMapToSet(containerConfigurationTemplate.getContainerExposedPorts())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 65659f3cd6..34f8a2ba66 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; @@ -36,6 +37,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import java.security.DigestException; +import java.time.Duration; import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -133,6 +135,13 @@ public BlobDescriptor getBlobDescriptor() { .setWorkingDirectory("/base/working/directory") .setEntrypoint(ImmutableList.of("baseImageEntrypoint")) .setProgramArguments(ImmutableList.of("catalina.sh", "run")) + .setHealthCheck( + DockerHealthCheck.fromCommand(ImmutableList.of("CMD-SHELL", "echo hi")) + .setInterval(Duration.ofSeconds(3)) + .setTimeout(Duration.ofSeconds(2)) + .setStartPeriod(Duration.ofSeconds(1)) + .setRetries(20) + .build()) .addExposedPorts(ImmutableSet.of(Port.tcp(1000), Port.udp(2000))) .addVolumes( ImmutableSet.of( @@ -228,6 +237,17 @@ public void test_propagateBaseImageConfiguration() "base.label.2", "new.value"), image.getLabels()); + Assert.assertNotNull(image.getHealthCheck()); + Assert.assertEquals( + ImmutableList.of("CMD-SHELL", "echo hi"), image.getHealthCheck().getCommand()); + Assert.assertTrue(image.getHealthCheck().getInterval().isPresent()); + Assert.assertEquals(Duration.ofSeconds(3), image.getHealthCheck().getInterval().get()); + Assert.assertTrue(image.getHealthCheck().getTimeout().isPresent()); + Assert.assertEquals(Duration.ofSeconds(2), image.getHealthCheck().getTimeout().get()); + Assert.assertTrue(image.getHealthCheck().getStartPeriod().isPresent()); + Assert.assertEquals(Duration.ofSeconds(1), image.getHealthCheck().getStartPeriod().get()); + Assert.assertTrue(image.getHealthCheck().getRetries().isPresent()); + Assert.assertEquals(20, (int) image.getHealthCheck().getRetries().get()); Assert.assertEquals( ImmutableSet.of(Port.tcp(1000), Port.udp(2000), Port.tcp(3000), Port.udp(4000)), image.getExposedPorts()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java index c03fe5d8a5..9d0a90d592 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableList; import java.time.Duration; +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -25,9 +26,9 @@ public class DockerHealthCheckTest { @Test - public void testBuild_parameters() { + public void testBuild() { DockerHealthCheck healthCheck = - DockerHealthCheck.builderWithShellCommand("echo hi") + DockerHealthCheck.fromCommand(ImmutableList.of("echo", "hi")) .setInterval(Duration.ofNanos(123)) .setTimeout(Duration.ofNanos(456)) .setStartPeriod(Duration.ofNanos(789)) @@ -45,35 +46,19 @@ public void testBuild_parameters() { } @Test - public void testBuild_propagated() { - DockerHealthCheck healthCheck = DockerHealthCheck.inherited(); - Assert.assertTrue(healthCheck.getCommand().isEmpty()); - } - - @Test - public void testBuild_execArray() { - DockerHealthCheck healthCheck = - DockerHealthCheck.builderWithExecCommand("test", "command").build(); - Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand()); - } - - @Test - public void testBuild_execList() { - DockerHealthCheck healthCheck = - DockerHealthCheck.builderWithExecCommand(ImmutableList.of("test", "command")).build(); - Assert.assertEquals(ImmutableList.of("CMD", "test", "command"), healthCheck.getCommand()); - } + public void testBuild_invalidCommand() { + try { + DockerHealthCheck.fromCommand(ImmutableList.of()); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("command must not be empty", ex.getMessage()); + } - @Test - public void testBuild_shell() { - DockerHealthCheck healthCheck = - DockerHealthCheck.builderWithShellCommand("shell command").build(); - Assert.assertEquals(ImmutableList.of("CMD-SHELL", "shell command"), healthCheck.getCommand()); - } - - @Test - public void testDisabled() { - DockerHealthCheck healthCheck = DockerHealthCheck.disabled(); - Assert.assertEquals(ImmutableList.of("NONE"), healthCheck.getCommand()); + try { + DockerHealthCheck.fromCommand(Arrays.asList("CMD", null)); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("command must not contain null elements", ex.getMessage()); + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index bccc92d371..9a7fe5f093 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -63,7 +63,7 @@ private void setUp(Class imageFormat) .setEntrypoint(Arrays.asList("some", "entrypoint", "command")) .setProgramArguments(Arrays.asList("arg1", "arg2")) .setHealthCheck( - DockerHealthCheck.builderWithShellCommand("/checkhealth") + DockerHealthCheck.fromCommand(ImmutableList.of("CMD-SHELL", "/checkhealth")) .setInterval(Duration.ofSeconds(3)) .setTimeout(Duration.ofSeconds(1)) .setStartPeriod(Duration.ofSeconds(2)) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4d08d12772..0ea7785d03 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) - Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 108880677f..04b59f1328 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) - Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed From 681fab1f51122247fb9a1b48dd5ac6d24bea5d57 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 3 Dec 2018 16:20:22 -0500 Subject: [PATCH 0360/2020] Gradle release v0.10.1 (#1293) * [Gradle Release Plugin] - pre tag commit: 'v0.10.1-gradle'. * [Gradle Release Plugin] - new version commit: 'v0.10.2-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 0340ae781e..3b0f75f558 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.10.1-SNAPSHOT +version = 0.10.2-SNAPSHOT From 2c3a43aabbf16c0e2fc568bb157a05bdc9cac2ce Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 3 Dec 2018 16:21:07 -0500 Subject: [PATCH 0361/2020] Maven release v0.10.1 (#1292) * preparing release 0.10.1 * 0.10.2-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index d58f50fe6f..de2192fa34 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.1-SNAPSHOT + 0.10.2-SNAPSHOT maven-plugin Jib From 6ce73c5694ab0c194a18a0354ec45f354cc27a25 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 3 Dec 2018 18:17:23 -0500 Subject: [PATCH 0362/2020] Update version strings for 0.10.1 (#1294) --- CONTRIBUTING.md | 4 ++-- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 17 files changed, 34 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52b179be87..1566bb99a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.10.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:0.10.2-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.10.0' + // id 'com.google.cloud.tools.jib' version '0.10.1' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index cfc48680e8..398b1079b2 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 22e8e3c2da..3a9d8bc418 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.10.0 + 0.10.1 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 5d461f7701..9b98c01b7b 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index f67e324ac3..be4638a35c 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -7,7 +7,7 @@ UTF-8 - 0.10.0 + 0.10.1 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 59975a7ef0..1ecd6b3516 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 2b5d112295..499891f07b 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.10.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.10.1' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 5fca3e82dd..7b8fe174ea 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -39,7 +39,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.0 + 0.10.1 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 4bf75fbb6f..1dd75184d1 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index b263ee1630..73d446b274 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.10.0 + 0.10.1 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index 67deb04159..3a4c3a3ca2 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index 72982ac188..1a7e21822d 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.0 + 0.10.1 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 10bcdf5e7a..c9bc842cd8 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 0ea7785d03..294336f0fd 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.1 + +### Added + - Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. - `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 507976a034..a7fffeae31 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -23,7 +23,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.10.0' + id 'com.google.cloud.tools.jib' version '0.10.1' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 04b59f1328..a1a91c58e8 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.1 + +### Added + - Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. - `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 528a18798c..45490efec4 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -18,7 +18,7 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.1:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -26,7 +26,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -44,7 +44,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.10.0 + 0.10.1 myimage From 420d334dc800e2c30dd9c2cebba4bc4d7a65c322 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 4 Dec 2018 12:27:51 -0500 Subject: [PATCH 0363/2020] Update documentation for 0.10.1 (#1265) --- docs/faq.md | 32 +++++++++++++++++++++ jib-gradle-plugin/README.md | 55 +++++++++++++++++++++---------------- jib-maven-plugin/README.md | 55 +++++++++++++++++++++---------------- 3 files changed, 94 insertions(+), 48 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index ec5359535a..da20a2c569 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -16,6 +16,8 @@ If a question you have is not answered below, please [submit an issue](/../../is [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ [I need to add files generated during the build process to a custom directory on the image.](#i-need-to-add-files-generated-during-the-build-process-to-a-custom-directory-on-the-image)\ [Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ +[What would a Dockerfile for a Jib-built image look like?](#what-would-a-dockerfile-for-a-jib-built-image-look-like)\ +[How can I inspect the image Jib built?](#how-can-i-inspect-the-image-jib-built)\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ @@ -276,6 +278,36 @@ There are several ways of doing this: - [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. - Alternatively, instead of using a Docker daemon, you can run a local container registry, such as [Docker registry](https://docs.docker.com/registry/deploying/) or other repository managers, and point Jib to push to the local registry. +### What would a Dockerfile for a Jib-built image look like? + +A Dockerfile that performs a Jib-like build is shown below: + +```Dockerfile +# Jib uses distroless java as the default base image +FROM gcr.io/distroless/java:latest + +# Multiple copy statements are used to break the app into layers, allowing for faster rebuilds after small changes +COPY dependencyJars /app/libs +COPY snapshotDependencyJars /app/libs +COPY resources /app/resources +COPY classFiles /app/classes + +# Jib's extra directory ("src/main/jib" by default) is used to add extra, non-classpath files +COPY src/main/jib / + +# Jib's default entrypoint when container.entrypoint is not set +ENTRYPOINT ["java", jib.container.jvmFlags, "-cp", "/app/resources:/app/classes:/app/libs/*", jib.container.mainClass] +CMD [jib.container.args] +``` + +When unset, Jib will infer the value for `jib.container.mainClass`. + +Some plugins, such as the [Docker Prepare Gradle Plugin](https://github.com/gclayburg/dockerPreparePlugin), will even automatically generate a Docker context for your project, including a Dockerfile. + +### How can I inspect the image Jib built? + +To inspect the image that is produced from the build using Docker, you can use commands such as `docker inspect your/image:tag` to view the image configuration, or you can also download the image using `docker save` to manually inspect the container image. Other tools, such as [dive](https://github.com/wagoodman/dive), provide nicer UI to inspect the image. + ### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index a7fffeae31..cad1bdcdf6 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -9,6 +9,29 @@ Jib is a [Gradle](https://gradle.org/) plugin for building Docker and [OCI](http For information about the project, see the [Jib project README](../README.md). For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). +## Table of Contents + +* [Upcoming Features](#upcoming-features) +* [Quickstart](#quickstart) + * [Setup](#setup) + * [Configuration](#configuration) + * [Build your image](#build-your-image) + * [Build to Docker Daemon](#build-to-docker-daemon) + * [Build an image tarball](#build-an-image-tarball) + * [Run `jib` with each build](#run-jib-with-each-build) + * [Additional Build Artifacts](#additional-build-artifacts) +* [Extended Usage](#extended-usage) + * [System Properties](#system-properties) + * [Example](#example) + * [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) + * [Authentication Methods](#authentication-methods) + * [Using Docker Credential Helpers](#using-docker-credential-helpers) + * [Using Specific Credentials](#using-specific-credentials) + * [WAR Projects](#war-projects) +* [How Jib Works](#how-jib-works) +* [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) +* [Community](#community) + ## Upcoming Features See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. @@ -132,30 +155,10 @@ tasks.build.dependsOn tasks.jib Then, ```gradle build``` will build and containerize your application. -### Export to a Docker context - -Jib can also export a Docker context so that you can build with Docker, if needed: - -```shell -gradle jibExportDockerContext -``` - -The Docker context will be created at `build/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `--jibTargetDir` parameter: - -```shell -gradle jibExportDockerContext --jibTargetDir=my/docker/context/ -``` - -You can then build your image with Docker: - -```shell -docker build -t myimage my/docker/context/ -``` - ### Additional Build Artifacts As part of an image build, Jib also writes out the _image digest_ to -`build/jib-image.digest`. +`build/jib-image.digest`, as well as the _image ID_ to `build/jib-image.id` ## Extended Usage @@ -200,15 +203,17 @@ Property | Type | Default | Description --- | --- | --- | --- `appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. `args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. -`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. +`entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. `labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). -`mainClass` | `String` | *Inferred\** | The main class to launch your application from. +`mainClass` | `String` | *Inferred*\*\* | The main class to launch your application from. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. `user` | `String` | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. +`volumes` | `List` | *None* | Specifies a list of mount points on the container. +`workingDirectory` | `String` | *None* | The working directory in the container. `extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): @@ -239,7 +244,9 @@ You can also configure HTTP connection/read timeouts for registry interactions u gradle jib -Djib.httpTimeout=3000 ``` -*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* +*\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* + +*\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* ### Example diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 45490efec4..7feb42900d 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -9,6 +9,29 @@ Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and [OCI] For information about the project, see the [Jib project README](../README.md). For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). +## Table of Contents + +* [Upcoming Features](#upcoming-features) +* [Quickstart](#quickstart) + * [Setup](#setup) + * [Configuration](#configuration) + * [Build your image](#build-your-image) + * [Build to Docker Daemon](#build-to-docker-daemon) + * [Build an image tarball](#build-an-image-tarball) + * [Bind to a lifecycle](#bind-to-a-lifecycle) + * [Additional Build Artifacts](#additional-build-artifacts) +* [Extended Usage](#extended-usage) + * [System Properties](#system-properties) + * [Example](#example) + * [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image) + * [Authentication Methods](#authentication-methods) + * [Using Docker Credential Helpers](#using-docker-credential-helpers) + * [Using Specific Credentials](#using-specific-credentials) + * [WAR Projects](#war-projects) +* [How Jib Works](#how-jib-works) +* [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) +* [Community](#community) + ## Upcoming Features See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. @@ -173,30 +196,10 @@ Then, you can build your container image by running: mvn package ``` -### Export to a Docker context - -Jib can also export a Docker context so that you can build with Docker, if needed: - -```shell -mvn compile jib:exportDockerContext -``` - -The Docker context will be created at `target/jib-docker-context` by default. You can change this directory with the `targetDir` configuration option or the `jibTargetDir` parameter: - -```shell -mvn compile jib:exportDockerContext -DjibTargetDir=my/docker/context/ -``` - -You can then build your image with Docker: - -```shell -docker build -t myimage my/docker/context/ -``` - ### Additional Build Artifacts As part of an image build, Jib also writes out the _image digest_ to -`target/jib-image.digest`. +`target/jib-image.digest`, as well as the _image ID_ to `target/jib-image.id`. ## Extended Usage @@ -242,15 +245,17 @@ Property | Type | Default | Description --- | --- | --- | --- `appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. `args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. -`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. +`entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `INHERIT` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). -`mainClass` | string | *Inferred\** | The main class to launch the application from. +`mainClass` | string | *Inferred*\*\* | The main class to launch the application from. `ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). `useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. `user` | string | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. +`volumes` | list | *None* | Specifies a list of mount points on the container. +`workingDirectory` | string | *None* | The working directory in the container. `extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): @@ -281,7 +286,9 @@ You can also configure HTTP connection/read timeouts for registry interactions u mvn compile jib:build -Djib.httpTimeout=3000 ``` -*\* Uses `mainClass` from `maven-jar-plugin` or tries to find a valid main class.* +*\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* + +*\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* ### Example From 706f67c4aa83fb12e9ef5ba7180e71ff5a6b6fd6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 4 Dec 2018 17:22:59 -0500 Subject: [PATCH 0364/2020] Archive inactive proposals (#1299) --- proposals/{ => archives}/events.md | 0 proposals/{ => archives}/jib_core_library.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename proposals/{ => archives}/events.md (100%) rename proposals/{ => archives}/jib_core_library.md (100%) diff --git a/proposals/events.md b/proposals/archives/events.md similarity index 100% rename from proposals/events.md rename to proposals/archives/events.md diff --git a/proposals/jib_core_library.md b/proposals/archives/jib_core_library.md similarity index 100% rename from proposals/jib_core_library.md rename to proposals/archives/jib_core_library.md From af93240d95098a436176715132629fb29eda16cf Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 5 Dec 2018 11:44:16 -0500 Subject: [PATCH 0365/2020] Adds progress Allocation to represent Decentralized Allocation Tree node. (#1298) --- .../tools/jib/event/progress/Allocation.java | 100 ++++++++++++++++++ .../jib/event/progress/AllocationTest.java | 93 ++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java new file mode 100644 index 0000000000..77101c5ad8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * Represents a Decentralized Allocation Tree (DAT) node. + * + *

    A DAT node is immutable and pointers only go in the direction from child to parent. Each node + * has a set number of allocated units, the total of which represents a single allocation unit of + * its parent. Each node is therefore a sub-allocation of its parent node. This allows the DAT to + * sub-allocate progress in a decentralized, asynchronous manner. + * + *

    For example, thread 1 creates node A as the root node with 2 allocation units. A subtask is + * launched on thread 1 and creates node B with 3 allocation units as a child of node A. Thread 1 + * then also launches a subtask on thread 2 that creates node C with 5 allocation units. Once the + * first subtask finishes and reports its progress, that completion would entail completion of 3 + * allocation units of node B and 1 allocation unit of node A. The second subtask finishes and + * reports its progress as well, indicating completion of 5 units of node C and thus 1 unit of node + * A. Allocation A is then deemed complete as well in terms of overall progress. + * + *

    Note that it is up to the user of the class to ensure that the number of sub-allocations does + * not exceed the number of allocation units. + */ +class Allocation { + + /** + * Creates a new root {@link Allocation}. + * + * @param description thuser-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return a new {@link Allocation} + */ + static Allocation newRoot(String description, long allocationUnits) { + return new Allocation(description, allocationUnits, null); + } + + /** The parent {@link Allocation}, or {@code null} to indicate a root node. */ + @Nullable private final Allocation parent; + + /** User-facing description of what the allocation represents. */ + private final String description; + + /** The number of allocation units this node holds. */ + private final long allocationUnits; + + /** How much of the root allocation (1.0) this allocation accounts for. */ + private final double fractionOfRoot; + + private Allocation(String description, long allocationUnits, @Nullable Allocation parent) { + this.description = description; + this.allocationUnits = allocationUnits; + this.parent = parent; + + this.fractionOfRoot = parent == null ? 1.0 : parent.fractionOfRoot / parent.allocationUnits; + } + + /** + * Creates a new child {@link Allocation} (sub-allocation). + * + * @param description user-facing description of what the sub-allocation represents + * @param allocationUnits number of allocation units the child holds + * @return a new {@link Allocation} + */ + Allocation newChild(String description, long allocationUnits) { + return new Allocation(description, allocationUnits, this); + } + + Optional getParent() { + return Optional.ofNullable(parent); + } + + String getDescription() { + return description; + } + + long getAllocationUnits() { + return allocationUnits; + } + + double getFractionOfRoot() { + return fractionOfRoot; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java new file mode 100644 index 0000000000..c251016807 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Allocation}. */ +public class AllocationTest { + + /** Error margin for checking equality of two doubles. */ + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + @Test + public void testSmoke_linear() { + Allocation root = Allocation.newRoot("root", 1); + Allocation node1 = root.newChild("node1", 2); + Allocation node2 = node1.newChild("node2", 3); + + Assert.assertEquals("node2", node2.getDescription()); + Assert.assertEquals(3, node2.getAllocationUnits()); + Assert.assertEquals(1.0 / 2, node2.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertTrue(node2.getParent().isPresent()); + Assert.assertEquals(node1, node2.getParent().get()); + + Assert.assertEquals("node1", node1.getDescription()); + Assert.assertEquals(2, node1.getAllocationUnits()); + Assert.assertTrue(node1.getParent().isPresent()); + Assert.assertEquals(root, node1.getParent().get()); + Assert.assertEquals(1.0, node1.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + + Assert.assertEquals("root", root.getDescription()); + Assert.assertEquals(1, root.getAllocationUnits()); + Assert.assertFalse(root.getParent().isPresent()); + Assert.assertEquals(1.0, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + } + + @Test + public void testFractionOfRoot_tree_partial() { + Allocation root = Allocation.newRoot("ignored", 10); + Allocation left = root.newChild("ignored", 2); + Allocation right = root.newChild("ignored", 4); + Allocation leftDown = left.newChild("ignored", 20); + Allocation rightLeft = right.newChild("ignored", 20); + Allocation rightRight = right.newChild("ignored", 100); + Allocation rightRightDown = rightRight.newChild("ignored", 200); + + Assert.assertEquals(1.0, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 2, leftDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4, rightLeft.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4, rightRight.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals( + 1.0 / 10 / 4 / 100, rightRightDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + } + + @Test + public void testFractionOfRoot_tree_complete() { + Allocation root = Allocation.newRoot("ignored", 2); + + Allocation left = root.newChild("ignored", 3); + Allocation leftLeft = left.newChild("ignored", 1); + Allocation leftLeftDown = leftLeft.newChild("ignored", 100); + Allocation leftMiddle = left.newChild("ignored", 100); + Allocation leftRight = left.newChild("ignored", 100); + + Allocation right = root.newChild("ignored", 1); + Allocation rightDown = right.newChild("ignored", 100); + + // Checks that the leaf allocations add up to a full 1.0. + double total = + leftLeftDown.getFractionOfRoot() + + leftMiddle.getFractionOfRoot() + + leftRight.getFractionOfRoot() + + rightDown.getFractionOfRoot(); + Assert.assertEquals(1.0, total, DOUBLE_ERROR_MARGIN); + } +} From 2f6ff2bb223cbe3364a59a16fe8b1f19f9c75d1e Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 5 Dec 2018 13:52:33 -0500 Subject: [PATCH 0366/2020] Adds logo with no text and small square icons. (#1295) * Adds logo with no text and small square icons. * Adds anti-aliased logos. --- logo/jib-docker-java-128x128.png | Bin 0 -> 5290 bytes logo/jib-docker-java-64x64.png | Bin 0 -> 4445 bytes logo/jib-dockerize-java-no-text.png | Bin 0 -> 42246 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 logo/jib-docker-java-128x128.png create mode 100644 logo/jib-docker-java-64x64.png create mode 100644 logo/jib-dockerize-java-no-text.png diff --git a/logo/jib-docker-java-128x128.png b/logo/jib-docker-java-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..c1148284937adde1431864317fabae95c436b108 GIT binary patch literal 5290 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 literal 0 HcmV?d00001 diff --git a/logo/jib-dockerize-java-no-text.png b/logo/jib-dockerize-java-no-text.png new file mode 100644 index 0000000000000000000000000000000000000000..92abdd7b2f5a1201cee3cb21c3d2777e5b6db3d2 GIT binary patch literal 42246 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

    PDzXtSO_%wNyP=0AauM zjtgV!7(*<)y2&h`vI{4Pj~uV1kf%KehWwR>D-8z$H$%e3x3ZIW*t)m8ib9lP?!CRL z@-LRu9>N~Vp0V4HZ5H!O5*9jK-8|=|-uVg7TDqYBGzxJ@$SVV0^DRs+<^t1zHfZD+ zNo2v4V%!QVfrO9m7Gtml1!P{t*62|(R?Em(t)%EUAMnJo9DfZHuX}-XwC-_>G?Q%K z&yq;sFi5+;rPhDqO?&LR0@;uRC7+hSQccHdt{?Bo$tK)u7BKh&ZX8oqYMeSE9f5H- z^W_sJp!*!tDt4A44sLLqRn7!88#{Ko32f75`I;Qgmet^#{vi&ujD*QO7>zXkF~7Ue z!dYl@-Jnl^$OA|lG6o5!n{c!Dtgb+J@z5IrA_xaqBiiph4^u)2|RfI1n`S<{C$|C?GH(;qlS-l;6mTTZde)AW_M6e_=iU`RrnKB zrfdLA{ai0r-|dQx7M(e6H~~rHtCWPQ8=()uyJbwUB`%9xGK11syT z;bRpT^=4mt8@(zolR6=%YEeWmCanF9<>xEY@3o&z(;H(lV6-0X(MLc2k!T!Q5fZ(z zNih3R)Zy|TK&_Sb>lcDxPq~;$QjB7g7aSOBKzb;wX13o;LYtX%hB-XR*F2|upMQ0m z%Ox<(Nt~G9n~;-IUAzhc@0SrJ!2wA^0gxovGV}-3F9JAnk2p;-&j3(zEM5zE z-Ow|ZO7O>=j#_S=dr5+o0nyX1mujB4>;|Vc=fA)|wu9VSD}~!00YN|E&cK_Mb)Ao3 z>UHLSL7mXI?=-rmU1mWyRpPvAkMnu{#Yc5Jubpy`FF5~7s6g_}8EfH|DJ9fpI>s=C zI{d!zuV_jsA`uyQIq7eauTID>Z*&z7mzI zLu>%L;bVcRqN6ysD4d2+5y_^tua=pTe5kjTDNf{Ol6wb#_8vd7`Sz6nz(PB4sb7D4 zk5{@KM?e&BbKW@vIje1r{@bzABpj+6Vkv+e$~;~exj_qFLm8x~35?C9nV%hHZ`f~0 zfIoxg*n}}0x7Q@CX@9Vb)v~^DFI+T)^8!2MsYT;uU#=pJ9;kL8LU%f zxjFkdG-lw_3p`n?%ecTiRo#Xwq<4F4d!fS}FgXV)o8ITbv4z}FKSqERryx(!7wiO- z+L){0%O#3?q!UMu?XQ!nt{E3(t3N_Bdt*f1MP6jv^Ut{_jYnB{<4iMNV^Bqi$y>pB%V3e(AP%K9T@l=ioay+sh&!nU`*MGWhnlS`t{ z;+?~B8Ym}>7vlsCT)P7ggON{~=Z4x~P61afnGqQN-3NH>&YHm4|1iwxXG$Kyk)faX zyMEzokjNn1_YyWGKp-p6?;9d}!lw*8nh$=kyc;N3X7F>lQDjRk4c=@(i1Pl4?g}lX ze4k13PB8rT**-9Bz=P_$BfZBBh!#pdb~Rm{t0o!wRI#Cn0@V;bg-%}ghA6zJpDgqv*%bz$G>g96cC1IP$PtI zCp1LETVl@eKgZ}74Rup2Pp9JIJI_3aeTX?E`pp*V)60boYA60;#V$eeLSgrX4}IA8 z)%&X+uxvLVggX+73YT@~?s>?0TlxPsdi3YMK=Pr;;=6#S!&(1mF8wE9$l$AX2Ga*+J@Q`)NcH$}1ec>T%T zsNG>U``I~D7|Pq#A&`1I7`wz0LtOU{E?;5FCO`m9GQTSc8-E6rRKoE|Fl$ zLB*%?$wy;}T^&6RW(T-#TC#kcE16g4WVO!&YnmnfOBiOY-=&+ypUZs*N^cBIZdOB% zX`y6zq&QVc)aCxGp*&hJGKQl)Fh#f#ERl!YxzjKEj;h!k)$ z|9}G^1hthj{#b5|LHq7Oj6M-JB&4TtX-0YfwAfpS3i)6i3diUTS}OHt z6s(NJiJppMq`hp0H>7bT(O=IcYAO4EX{!hpgy6UWNxxOxO#BNm5pib8FXWa}&bz0+ zecCAT?|k{82oqxgbl)835uy4A7D{&a zX!w`_V3BVcKRwP2Aju3E*hmvzJNh2z{`hGSz?^@*?8~W|e%|ayQr~)F05|cLF&`UH zo$VZI*1)Y9KFDDE`(QSMVJkjRX-@CY`GHD*_i?3rC5l|Q4 zvuRJjTsTXRs}JY2j;7YU=sr@Wv?l?KpE&vdLdSS+;If>vCcvs>mjySni-?QW*OwEJ zpw=_?PY+HjKU<4AF>h!d5*S{rQFQ|kUu3s#aX}u8Mxw9tF{#6i& zBLky5r)-!>@_zlZ`l}FDYcUx{H0#Bw3e3;ojX`(h?BoK>?>b1zXR(obW{F?UVGx>) zba(8ajka<#D*hAA)cCS%+XVZu+*uAXt5Oo2>_ZP-;PqWus)XO_aV5HphASPYP@wha zu3E{Luhv+q-%cz4`}}NgpXGP=<`@c*2;54xo!6sD z*H(zyUQY(7f>Q1l+(7wVhL5WfvBt+7Os)W$TTTr482cE`esuK-KeVNZ`_X%qgDd;7 zP$ZIzkrIdz!qiBKO;q*==^x-``~?gN2jKHU@>uKNg~0>hsi%+&I(lX4nx^El3&xn? zkuokrTXi$|4j#92pS1O(RRB+}eAw!&nODVfV2_e_WV@FxWb9|HLa-04eURtp#B~@& z*a-L4!Bkr#Pm`C<7ePBY^_7c zF)~wjGAhC`U)dp)l`Taw&&j)@s2nSVl93gXvK_BvMUpM!6lG**&)@Ub`}6sJe*Yfs z`@Yumx}M|lcwE5GE1~{u`ZznGeh&h%m$$Id(smsp)uOKXx1achJy~`qyvpc*^QbYn zIj;Pj+^w4U1-VA#OD8pF=%!NgNWMe^B4D}dncu?qYt*sKj8~xa#I_e zJX?`|qDn_g_rb}NW5bd*i{uo@jgAwSj~C1bLuvXOiN$sdrbJo#oc{dBa_Uu^El{R=tnNVM;(4nVQ1OE1oljYN)C_Me5VMPEjj_u`LWQw}}b zopa(`A%MBEMpZLTY09AG`~Tq<4*fQ> zqw98nO4Qkjyqs)h7WiPiLMZzl;>wnc75@OjWLNjsY0I#bLlqp67CI&9r#kk>-kMx3 zWiE}n)>vKlfj?V6Fuc)NFDbSRUEq)u-4+cAjjZUicQ`-zG>t7whFQnuKhpnvS2kvd ztN3Y;V91#cQnb|AAU*XTTGi9F<6Q@kkoY4Iq3Gh!ebKo7@X;A-l#m7g*^ek5RC zo}suthHchx8o|0WLwQVR@p4(jj|{Q=t!#hxt!xGe6E8F|>Ftb;Z5-_LV3Xn?9%&e8W@rUIQ@N`WAKuNp>P3#0g;<{s;L|zZOFA zgqe~<#_gO8qY4mm$1g5ixf=UC@6eBlaV0`)$kBG!w5oTQ;&7fz_c`#Ecm18+`TT1m zw3p|_rcC&n}gi>S}|{Quf_{xzks}Co*AI)H)#)&tV|03H6AQI*zWx;aRUg zrue=ln93jBS9yOSo?XjOcFlgFI!P|M@qG4SX_A_>-5Bf7F&8%k>Z+Xmh9$cm`$xpH zd+BUio8xkSn&cdg77BeVm;FCk=4>`5p*V_4(!jxjH>T`CqL0%2!fh;5=u!xGtb?E; z$JQRRZ)_gR5aZp2=< zX>A&|e!+7i9(|Y%KejY+uR`gYaWTRS=A6AZ5lFkaFMg)PLQAHbo1SqJDXOMqK6>uQ z8-Ivq0Kkok$@QxjDc(zZ9^D;4&{glVtwJhxQ-e3>CNI!l@&q58(@e{!dNy5daS)zk zpF8&RldhZV-G2=@B-n@Uvvj8C*g-0zs|G>zd{{hP7d?A5D9aJ^m=zEB^67I88fnI( zdua-URg>`>j@Y7@&{jt+ z2=2FNS_GQ0{z$m@zf=3}W?^q}>=)O{A%K&T9SndT0d0l<2E=g`>kCR-N&ttbe7d>m zZ!5m`DgC`a!zyD@&iB*Qt1MCJ3^(QGJ|oy{wRhHe!gKx}xZv;KdwlaS+jq?ej^&?W z^GejVUy1!RPwi+b(d|ECd&n#7zIqN1@~v%voa=cUJHe09xGS#bk?4=%Xu3?FtBhd6 z4Kx4~9&hT}-BUQaa%q~6()pYj_Znyv{^ygcK8K*rWK~-;1=f8i@a<=;O!eCn&t$51 ztp7ls*vK}spsA7-1<$KtpIK9@Mlitv#nCVboFPD{>LUaRRtM2Pl-b9})|Fz?@TIAH z{JD|ZBiV*yE8p?9pFsGKx}4i=XRrVB!(~;4E`u@zBcW*w?Y03rc07q{M{T8^$exd? zQ!9-Z-hT4hI5g~euadW!#f;|S?@iXVOIIk03E&JFZ^)}V0R#qkn1TD~3*-dk4}OZ? zU&98;1@5SKJK|9uA-cPmrE_%kL*cbzL>C|WD01xb$&ZK2UtJK}A6UJ~wwit4hI`R6 zO}VxsVICXS70G*Iq}rGMkirUiM^t4Ei7q1IvM^? z{ruvBgx}#VZp*)n*qCIF(0Lv7b$^Qt+6H!^TMl}}WCRo171vIcP#5aDaA5XK$;q-W zT!=g5L%Vuij!ih}tWvD_s3g@gdViy9qfL`0g>!JbF7)18hdP)1C3}XoKC{E@`~9@e zOx~tNHp%{D!9q~`&C~pCAPCd`;SUn)Q(RM(;n*H4#=)2B1D2d2-bSKfNMV$mS<{Js z-|%a6Y(>Cu{9&Kg;>%Ks@Z}hRgWcHDgX2lJSq8N|B=am*FkbeKQyw%1n6q{bpmn|+ zjsK%ZH^VRWbtA?(UY{vjjR6%fE$8QJqkzq*E%hp9V zn5ds_Mg~zfN_2`Pp-^(}9Gn1Omjh!PpXpVwD3pG-9y1zcn6O4?KC!$b z7^|kUaLb2b?b7he@b0~0pGs>!gp21nB+Q3~>8=#P&h-a&=k!VGqor}=t~H4 zMlK7aC@}-xQHPQ`tC!x)BK&3$OASG9pl#6G2$61D@!+aGlE@ZY>?#jvV)+{MNZ2P{ z^ru-<$){rE-7O6l90qz+)P2`Ofr|^aee1SD`sa28*v6ufi2&P-56(7F1;A%L@SwoE zgKx=F91Au~w>Qm{e^xY?7i)XK8%iyo^fwr_Ot18s=MGP+~P$!6ru0 z9ZFPY>fFn~CU(%VAY2Z8BfMq7yHD5tpGOj2?#qHd_-vM-6xYVtB@SV6w(F9R=`{<~ z08yz(@<3%jN|!(7+T-*^n#u1rRi=nEzqv^V+L=0zmX4Etg2aqTjXdKxW@~rB=8}!> zKG)0VJ?17&Xjs5qk6j|>ajxdJ_!*?-|EugUVCMQ^^?w6?!@J0EzCLxN^gnxqFB>eH z3;SM?i}j-@@EqwT@2?1eqVfp&1t54Btj?x4*_;h70D99#;5~B*iD~PFq-QPM1Zj)0se&cZg8u=G%cRvy2r)Xy z$#Amp6+UW=n{0eggaF>Bw>FJoeC1c`tSG*n=09GXd*9iMrJm~S2_2EG3yr!N*B7K- z|4GgO>+?dI%!{s)8hIGb*hq$n3;a=QQ-j&?=jiIkeTKY3A%Y(DX%6_`f&h93_qQPM z4sgs|AEnEW`6~=))4k@Io6|D9G=(0}h4GrDvQ8FEerNLz*zZoz#xYrE=WadQb99h- zDw$K2&JXQ(K;f1!vTw7e0qztKis?Wn=r6R)s%{C zE+|#0z~=vUdviEC=m0q6$yx=&kIhZK&|(E*H94Y|iacHN?h148{^L}z6uiJ?XLJ6x zSx~fZog2;XD&O6=Rbgk1PKRo5uq9mHt?O5WM+6WZu0lbe*%N&KLV(?CNkTo~Wb!o` z#i*|x{qayudB5^!KZ!+1-qeOJWd&%1my-8aN=VDaKFU92+1ky9cW`Dw$NTSl)vTl^ zb8Dvt>i6oi@4QbKc06t@PrPJnQ*(^RQCk!Xag;I7UljuX_4{NM`gg8;aXu-yqfe}? zmQGx^`9le@3rRZ_@FLZdA(%y%U^o!6$8GT>(Xfwuf1sH3yC>uKO+0xBD_5FKXGElhDrU4 z^PZD$up516>0UFmFLP`yQ6qucIRe&|^XhU~${A%NJJ)dQdrQ8_Td1{jWu8xLG6hYd z9j`ik(F`|9?Fh*|&Yn?FkQJCw4bgpFnzFLtNPfbclfWz+MUe^x+R3N2>TQp+qW7o4 zZWXJZxGqGnbU}PovR~r+sK)NyYAJ`vq#EfDVqVGL71P|5rY|mhnsOOj9~G(*k$tzP zwAbjY?2qHr8uqQT9n8lM@%rUOY;Y!R235x3RTHjp7Zu!Bn>>OHC6vJ=>h{V17Z%Yj za1#}kIaC1I3+?oGv*(mrzahl)u&moS)Udc$5tOnbny3{JkcMq5E;*hk+dBFl?Zn37 zlx}e<^x90yWiQ)fY&TPk>VvrV=LxO9$mC9#a!S9iCYlTxuKwy7^(WUHz1yz~6&e9) z3n2WMqv!JWm}~DPN*8&54!jyZ#__GUtu6XtAqUxpJxB|vL?f)U$(CM@I<-gs(y5Kd z^ef)9$Zr@rT}rV^-8v;bggbKKc2WfkKWCXFLQb~tsTush7h4a9*}-tk<13orkju_3joul*Obvo^%H+)|l4aB7S9aJh5^L7V zeS4-fJSJ!y)G=iy;}6|vPx{xwUY<(m(!;%0nL$f9W&xgmGMCW7^j5U*Px=uD`6n0n z|LH+C8XkS>{f24r=M2(XJZ)j#9tg`%#95tWI`L)@P>Uae!rpvzQc|aMI#e?qO~wb1 zz6KoazH^%TrL#vueS(&o!jNr zQSaJJ_6Ul|prc8D$VQDp{kp$?1X(X^u^sTt8H&S2Kg@`t2D?vFk0~0jD<&JuKYD0&w$p_4+h)4QB1Kj^8N?rsa|`d&Y3WCP-;@$9){l_Z;@|d z3jDOv2Nc&)(_m$g%3V-TzXQHI)RV4wGlhL!W~^2r23=%@;7NmS@v*(_yl%(pqfrPtYQ&mPT#` z%9Wj4X9edtJMrdm)8~4i>V*BBT|+ylZhA@4uZt2Whp#lOvN*qZYU>6fDx`pTBbzTn!ne)0fF2)j zelJMaOXTP8_s58$sSl%*zP-Iv_n>I$*Qo^^T!?y1iO!N6)0|_Pv0Q$M*~HD}o`J2i zgp=FX90Rr9q#7w7HB5OL$BMjBARDXyOY zB(c~d`u6Lt!e-JmJQfi3_ zlv&$8PR;@YFkVU|r6RcVcWhOz>s{Y}U)J6ZO-UjR2Z+8pw{?0g$vhE19Dj}JR6)eb zC8DL}WMo+?RJ;bCq<#ig&8whR?Zqa=@E>CO$}F2KQKs8yf({501o+~DnWtjb#E+O~^?Wv9!wWgrV77aV&aG!j zP!C)^7wVQUygFK5U{&?QV&jW46!PQ8jc&cUuNDaf({u}I&eFS$>E(NRiyM9ShYNnj zu{s22EQ>i$KQ6hk@CPYj^3k>+qw-1J^8nzL%K2^^cy^taaU05IwsF1T-BESJ#qD6x zzNGZPc#dhlH|~6o_%VY&CvPaS9y$NEO6&LjP4w#X`{yGe%t*KR!mWUPN=!FMCC)_8 zn<9`PKEu3*Cl#C50}yzA#5jC3YX3IC^HGoJJp`mGp;|HhfjW^;jFh|6C%YbN z^{60e(DN}X%DU-RERY73V>KvUCZ14va7lCVeffjUgU-muw0`gr*K0q%th>XjJUO?d z5*_$C^RUtychqHq^9BQJ#ydOd5zDZT!S#?$=m;}C)VG~mgBikA_RoYM=>+dxvQ>`8 zf9(Obuada6B|0HP|GCN2S54x(x6Pq&^eH?ArxtcF0b&lrpb4e{4S=F@7w%ueL&E%x zHm;zX6@QJ=zazg`KsAsaIm1?)n;qlHmpWhfq5Hq#eXUIPjL)ro0w&=?SMSEdNg$lU zK6pN6RhCU`FV7cpkTRxHJ#X===@(Z;FTuHxQTeGDfv(2@WlXo@yZFHRab|QX_kzgP z?;OPG_j^|*36_6cM}@+4kKNJ#K)(E`umH5GridO?7sjahB_Z3Q`|3VHAFd(rUcxV2Y5e4XN?5bsws{7s-E>FRCEpLW` z0L_`U3z23Q>CK0A$xzTOq`E_x|4)wjoXf%aQ8&mRAcM%SfGRy9{Vt|)F5j$;!c}RZ zxtJv5Cpi7|TR3$^8ba-G+`B)Y-lm?b%X!u3+7(PqB{lKv>p~O^6~A5kL0s{s5eQ7> zPwrC12^aj&FTAjNa6J58{9tq{Z)bh*XQ6espcFqT31Uu&GPnv%zbduqy#0G7T&v?Z z4<3{x@CXt6ud}d8i2eeC`!VgkPBG4^6&Ov9zO=rT5?<`#k$rb_)F46P1Dr=ZwPh5$ zrJGjk&>d%=q#350eJW-scNU#y>}5l+%SFTm!UUff2*t6~pnJ!!P}1*ti>%c?+h~&f zlzmu|k~*z)u(aOjoM`S?_{Y1uIAT+A$hyhgn*HqB`lA>h1A)EvvPf4KR9j87Rnx@3 zDwlt3%U)CnW{_ts{p6GsI?SNSmfiO;4^@o|Ox+U)@PZ!Jw3g$S|J`Q1Na=X5 zZ}Ufd!OVEmljd!J%lEcjR# zoqn;-DDUbolo_f=7mc1w?Of>9Qr`<>>y4`teZc=3-K*t&?jNnd-ygo0SZTymKN%~p$^&9L8`@`Aq zqMKUJ)xa5j$J`_%d{5v(SIo{4Hmz^CRT(&J} zzGPuGrN=*N1;Q4p{)d4AJh=j8GUD6tdvUb;`Mv-xmrmxxD9>HQ%Z0NpmtSy==#bY^ zm<_v*v=cW@cW0YcLH0cRNl@6&64gD6AgGrCYMX4_m2Bo0=+c<62%B<@G8k?qjvxg4t}IfaY003QD@LQ`>eE- z@`p00a$KY`>J$4UL)qhpy9UEj_;HAjHpfNqa`-?b8fsqF+*XTgAdUQ9l~tvS_!JRz zW^EJJS|(JzJ6TpGX^b@C1sw)6jDf%~*Dw$umDx)B;~Pswe>s5e+^z6~%!0(pSH&c0 zh^Z7;Pn(L#p#q*HDOVj?rDTNy5fhdyry0lmf$}9n4c8a3r7e&!B#g_v!tMmS(tbak z9ylp3c*j&#d-*u=>m!8&rJ~aBjwrnNY_qae_rNYcG2A+iOIc8WxCF_>&QfNyqeJ~W zfx}cpM+Zp5s7g#k<}|fO8DwNUqaV6Kg{$+k`f%M4xcPP1L>`~b^21R!imR7t)&_e4 zQUVhto0&}NAm^*AK8Yuv0=me3a!>5)tULV-KgLEA;1@z^FJ@w?p*##k!-UJ$v>W4B zXmyIgtz6;M%j?AfB6Fv~xUX91_*hO(nhbiGMc+5=YJsNtEu%dIQaXKc%mzWT{OSzd zS4LO<6}4wN$$O+aIVtpTxMO(_rY&HP0P&!g;ykO+v^6bVF=X&YqXf4b`m>N!8<-Lo zLt%QoH|Jc?LQ-XBBq3#B`PiMycg3+O^cwaj5{rLv^qIkE%#pF3! zZ>QgHsR4e{l?%&VjGN>=R)f=>Iuud2$im4Z%w|!3@4$FYUXXP?767eo5Ncub~1L4KNCjdT~Vr8*w?z;ur3^|3Pu~g zkh`^~-&SEfl%MX^ZHWjvVzwA7&?N#b3rb=Pd_2(g6eWjZLD@wY1Ym=rAAaYMo@L{B zSNu4OluPDCNuK@*+8#HhyskLrvE4D#X`EHQ%|EBPJCjl*!LFDwkzN>1$|u8x`l!3==gw#rVmk)Tljjbx&}H&D4E z{u;{@U!L{~LrQx2_FTNNst|Sst7%^^&sv*XG#bxwNNS%S`5Hq~w|b1j)QB;twS37U zhqOktKKH6=@PEUopd#%n=2emPAA#&4FttfFPw>2V-k@_b)SYX4*>_s8VL~%3j`g=7 zL=Pl4OL7CkraPSaF`)<-zOdrot*i88GM01W%-lt!BmA(kyQ;Y*@NPP)G%Z^T$4Zg1 z+oh4~)@fD%@m@#2fAX!t=itdw^lMkAC#hGqibj{f=U#!Jr^H?ArwBZDE6vSIv0X-P zS~dE*&OSk6Y&YE|Yp(Tf$v52ReD@>5kI zsKVSJM=Oa_()TGI_S1B96I~-G`!BM1OQ-apc;{oD2*|R-q)%ttH_?`lGF9$VO2PA@ z2ABe=A3&j*WJmUNs7IKklY=*DPg{DAlg5-K08m$liZiwpGFqe5PUFunawdQ{5i!Qs z3Vrj>KR_U~KTI2I8q>{JZMvpdi0*7mmP!KsXI>)DWVwjp-P-gtSZsqGD<$UwXwf`|M8m!Q>_G)z zf8O9C36*cx>N)xC>s4M_@=v>2_|Zs>()kLet1Bx}hvD1TRAKzHC_LzbajfnwV}dN2 z=r7ArdC64V(J!ciphG7WrI%0JCS=_e$c{of^~pY`28KeJGJIXMP~uP33Y*?A&i|<( z(cO>B%LB^$CtstjH$)e>y4z6Izn%GrE;qs0C;xF34E-jJ_S@qpA{~tJ;*D&tfvC4U zh|J5DaVOstr773=+XgcgweHV9ckX!~(#COUfLtHq5u?o(j_{38ky*Ql{~!uGjxiO6I8Le1zNH+Z2+)J*!|RKlf}!qx2NfQThrogjJXD%o@Au{eQFJDf z>Lg{i!XY>jNn^7;JNdg6GE;H}Ohn2Zw0QQPE?1baAy;q(F-ah}dn@hq>`C~ei`M@T zw7wh>V?Lkv+RxB&%9V;ip7Zt;74%7I>bwO{K2w)@6~reUOvlz45SkSxAR8k@)9^NX zHOQc(31uGt66PZ@_<%j7oiHNlRxpyE+qGaMZzvQceg$5{HmFIQPB}s8JgKtxSt34( z`Dn+nf*n5Z&|8+k0gy=HqzL>(7Kt@KvJ}szU77|mm_AjOTGAAvLz(QkM$1hox=)^G zT3F{5*!kWiA4UPWl^CG-<>WQV_%_*E`lFAtw+<#r4{w~Db0Ua=sfVm%c3p!M<56Vk7LVWxjNHQ7#RVB4^u*Z3h!>cjif7bwQ^pBaV{@JS32E>C(?Q#bFM zn_-HTWI=cq^h}Mdql@edUoz9Jvnrqa3d~-nz()gyU=`D+kCqbP=syBO5yD?+`HM4f z@7!%<;QJw$Aj+b{@X3xP&ECs`ypmIZQN{WCCW1e#D(3kqP`{c|S+H{L$?2z9M!g^i zADOEsC!kZ`pn+#TQ)(9`_4CfkMs7OP4JWirux0@h>+?Z`vK44s!*F}+VPnWsAEuLC zNS~%5Q!@Ru$y~}@WF-3GR#(FO2<0^I2xJUe{f@V9h`KWrak}&OGA`1UXVj-)R7Q}& z+W;3wv7S=<`8s1!=PIRTb%;7F{A@{xm~-Mom>OY>)aXMi>MIAM7IjYhQ#lFWp>chF!*+r35Q_kaRYo}=GExnIPssa95 zl+Iw7sIR&VGauk{-S^F{w|IVJ`}BukYjD1|U_d6V_C>Tr4C0Osz$m&d?3{sC>Zw!! z5pypL#x#7-r_x4`&M~^iqC=!thvluBG=h)xv-UM4fB+ zVmjq?7;--yNt>U(kud+g3G5#xxbr6iS{|d9UF1YoOtnh~X%T_U}Hre+CTV1pOksD5-W>|pJ z`#Ag#>6;i=sW`^&w7t~O zQTdPH%dAES>(c2(->H~o_qwK34*=SefA ziH*L7I7RgFW3*>$gdV+y%$K7|Mzp2vN9MPI1w4HWYwmIrF^{1|L2ZUC$ND5v8@7hQ z_d1M$+aqjs6yEaeh{K7RJ&VMkszVA59k5^*PLeV-9GYymYToM|yYmD#vw$;2oZZ_n zvD1Z@!x?*cvWw!wg3SlHuJL3>4|~qn0MF?H*y6BWO6N6kyxUPbaznV#zs{O$3AaX) zp;E-xgTXn}^I#cdp>;UO?{7%Y@gwV@xd9c&*=i5Z$Y^3L$sdt@%!b5*EQs%IowqJUEWp*89>U6__2(#goq7i2@U;%h=$j_yBf=%-Ua9e8|7U zE>UB5pLqn&B*CuoYWZ8}9zT7_xgkFhFOI_E4gum#oCn`~dk^Wtr~zy9NZV6I6!%=c z-Lr~=ND+l0pS&v=no%ZMHu29uOeB9?@NnYVPz7ZkOgEk3)+w*6Ptz1ohw&;tT6gkSC)nwWPfA+a8vAKzs z-k_!pNB&Fur;_jisMULLo)Kf6P&y?ZU?=Z-wA0Gmels;F(0B-bJ_qL2Z+nEcwHK+g zdV*Lx>zq8TDn*0B*8qx4EHV;`tiySfWa9**I1no-_JE43IYvAu>G|xjp>tjO;rmzrVx5;{^w>A&t)f z215~a*1HHg3_3ShykrWU95?sUj0u+cUB?!%@{zAtE-L!5e4(rjWs!z4LCHcypPBQt z(U#uDQR$4RJcHK~U&T63&{h8uArKA6U`<*`!#erqD$zdXLRiDA)d;TJ>IsGr7s+Rf zI~smd%91Vwf7p;09KoH|qfB;&Q_ZC9Sla!93XZ)gV92N*(uiU9SeAjYzTx>L-NLrX z)|m%u&7k2>{~iruv&@w-5kU_Jwz+Y(l6g~l1KzYX;OnXj(FC%z43j5t1LyO;_PfZ= zlU}yJ=H?ah*T@XwDvpJS3A`@j{o;7>Rz_RZw0NWb;c&wzI!zJ}12|W2J6aa^dUI>X zJQ5f6ioS)LI|c9JUc0hz`fCooxrPV!k47Qi?Noz!W)*!z6b7okWc^`6lzU z?|%JmSMOHU99rx9)X3z*O^$RfUM9Thm}!l;1v0e3s70?tfv(cWsM$s!0} zMmF-0t>IGE0}bjxXkI?jJZbfWZ>5;wip23t3vOJhHG5Wr99tY}|11$qUnFYB*kvZu z4=^zzZ+rDJe4+HMCixVT!r_PetegVl{rrRgCQU+%n}8YVNXPG4;7-KbuvFq!g$pyd zcRo8L6lk(my^Zn2%%R$y#NEZ`>$Zz>*|V7s>}ImJ47*7T^aY%BnMb`41+fp+@MA*#0wfvyR1u z8d$MsGimqG6&yNwSx`bnZ?crNh6rYJRwZrbjZMfnPSAc}dboy`IOB3B!-Y?Ezw3dyG^63n z8FjgVh*1XNX;sSn)-xfZNhYp+#igfZ%u^>cc=@vc#^2egWB&emcwq!|H5ch>q&lnp z(K04qV!bA2CC1t2J=!3%dI;}Vr}pK9UHC$@Ny7Zg@UznjFc}Be7}_;Yx${IOELGAe zoRaoM;&L$3oKUp54?WE5AI5g!%74zzo}qG)od9W+KPve$SiFcc_T8*7VZT_#oe>mG zQk~zHIO*a^uM&q(BHA(Zzh}=~$~3bK1j}>Fs0?J{Ba{}jCCYOvOhmWdo-#2gfH~@Od z8JG3!{Y&~EHl`Rk3km#rry{*w03xU_^rO=C7cbZbI=Y!ITpGNV7|u9%)3eEh+JG2Y zjtUr=GvWpuco=qXy6@+w_o!6V=VS7(jER!y9;bRJ_Nm}!;C>xGd(_MQL9&7#y`>`b5R6prtNEt~_Q_+3~g?1=F*q>-l- z2-GOKNb?_o{)e|b+Ayl0BBp2fzP;dTRNHs7%>yc^dZTe~vEsAj!yl_AWd{U7VLJB4|2JeZk*0Z-(9{yM}BHtvzr9@{YFucsNfTO6$$$Zp&`j#ZZm_nn3_9B~mZw=4}0m!Ir zRf-sDUXn5|2ZD5P-`Bem)P^v(B(t2=l^XFsOV?;b7S~Ht+Ijb6OQ& zbWUg|$NJ3UvxAZ2^b5T2wwP9{vmQX9uoe}k_%E_P`28AZ1ynP1D0B6}1 ziuNfuS9OX~_Gm@me47JjJ=Nv9^EmyL`^57`!n68CQ#r^E^uLx0M?Gh4o)L)d<4gl_ zYvR@hF@h`CzMM`-2(-=!dbGJ6`rnxlIb0XV825_^GHl19y3f|>YNX^o%t8{= z)FoT%ZcG+B75%P@_S=O4G$6RbnFhtKNZOivzl`lxE}Jh;sc+4EV%&R%Tr6$3LfJW% z5v8P0w`1upupTNKP!988$!O~wu)BIeETQ!Aan2iAtvC(zj@d}Te3rW7rCb%N8qfNJ z@z?=BeF?8(AGx{1v?XxMZ9@o!$?uLTWRl11nb&G7B(MAe4wft!>I1O(o09BeO!}xq zNJ`*O-ZQVR>~^QCj>ez4vsphPn!1;&v|yHfRFm%MWA6v)_x@r_Ilrmr1%AsqH)hYo z)~S<(3;fn`0R%uGrE(XRarv6MXRe*pND}D=d4=tg>(UQ@T1ID206Y1B$!FoTqK%G( z`4{TpU!3)$It#1!MSPBE`pnnafZxHiOjyLl$Z6jX5FrAm_-NCH&g-|BH?aJ!i2QY)LuB%}X zpSuYb;R_}v2CL*f&(jNZ*~lt6kyf_$Ofh)N3{wCn4z9RRQBC#g-6FWkMTmBlCdc{S zUdu%WOXg}~vMQis_Mn}J$`0->+ov$^*QXNZ5B(6C&880j4rETj z1yk_9XS3Ty$~pQDc910!X1AQAsvma>e^^Ab3PFeFmaPdLTXZfhcI*U}m@ikT|)rnACxEHQH%Ft;LmPUfvhj zjx6H!tEk*lZUy#UtZ|Hv`8sz{9AO8CzitCBvBf8GdxFRNQ%_=;$-XYxdQ=-LdE#v- zW#nd#5!{mZH=+FFL)KqZ8uijBZXULPNNJME4q~&}ajc)7q&~rW3MKxkPS`XVri*u| zR!P}JJy2}(N_(-r+2UsdI-kdr3E7TQn9XP7u01Xig=f}SE^#&y7O;Lj?3bKjhx&Yk znqw2k=-Z;37QVHeFdu^QL~q~UV=pr7N!B#7xrIydvM4tWfwUC}xN*(t(T*o@T_tu+ za1w-*mxYT+YA^erRfc3F^{Fxi*6()M9+j(tZ0`9~8Jc<;-nU;VEvth!Ia5xSLySE$ zlFsyu?ByIBTV9HGUUTfSO4{a)=N$qj`71&@I!PMAab7PI63XJ7(V46gC`kHnOOZ@M zcKd6MI)okBA2^F^dM0rzvH#~G+Y6JwRYv0rvt4yjJQ(Zdh}0*QP7z)Y(sQJdv~>XH z$RO>{!Vx6?(i|-agDl8o7&>nqBKZa-^_X%8Y%mv@h1KAOyrSOpJ3OTBd=JQV zLv_DtS!K0V^vX1X+)@gpvMw9iafMOJteKP~Eu(ja0p7{J8j}?PHN94pX%IM0b~ETl zbjvLflE+Q2Zxk7I^2tB3lq{~pv8?cj?;72@F^gGd`ax_8 z!-%2Pmys_{!-it#$X*o;Y*{n3C#L#*-qm#O_xo0_IJ((-W78(R+1n4S-)&iCQ~xs0 z1lzH5IUD{$KsrKHqvKK*^Y-`NiuUzbxXlm90}?u~9|?DKN)pVyH|sDrI?C4*#SX^~ zzxT`UJ3&QLxAb*P6z}Xg0oNH0bHMEb4I&i9kB~`EK{Dwuvj^71B)28!#0>E8w)^ll zWM=aO-ssdHULEpjTJ6v`x}d}~0NdxtTjP8(bz432YCq(ln}$E>>qi@(+Q9@0N&Bi^ zocuO!O*oWzt%Osd%^4=52uy+rF$t`CL4;dVEd#A4ERUiGZv$n>H!b{y=YnN_U$7S) zQVT8{yC0_e5mHsgz5!_YsEWeI{p!D5UM4XhNhgK5N+W)vq1`Ex$rduNngh{Rb2AC} z5JK6xoou5{kwfU3wg`70i<}{K+Y?1;_P(1cZbA~VMn($>Z*RG{am^hCk_=}%M5-bE zykWLL7-`p+%4|1*}DFc4AVg4zE5 zw(~!ryWfC=3=yh^K_&jE0g?L#rU>KF|KY&Qx87@xPf~-HhSg0$?8D_OG^`CWYg6NQ zXM(9Jz~kpIcqb|N{&2Kn4-yVXr}n*^e2cp%rI1e@YcM0HMt`%qWVkVvvbmLhSks(| zac(_g3=zf%K?jdI!?2Bj;VZ-Yo<^$swwb4)efr&K=ClHRqxcH!)716yGlc`r+4#Zm zh$qg}xR#Y6BHfew3VMb_57fQPY7cZAyVxDMawpQDAt3*sMY5zp8?QkdLt*f0sF#@| z#OA7Uqq%0Bk~4F@oX^vGD!0$CJv&=uB)onzS9+v@t!k_u4zh~CBMQpWa*>5m+*{J#YMcmudp8E&9@IqLM_l`9lb64w2lXlz*-zm7(4CM!qb zhq+d{?qcUD);9#aZacc2Vj%JHxEzIm5p6Zn2^Z#K%`Z1lY9Bc>WU^YmIaYqi87hhEjg5PDqnEk_0s5p$wYnW#En~bQ0`Fv_tm^DH2 zjGoDC*C@k@X;#}}BctL}K8|RvHZ!Q!ytm)9%_3rGy0kptuF=%9?sbi-mR7Koe$9>& zAWkH)_%Or11d>duYq`&qfgI2rD=ztp+l)g{I)7Q)!`Qf@9%X=TgPWu{`Z9NZ9muZb zMp?zT2zgwk$$C4p&CEMOXrB9^LQ{Ym?`iS3XE`%j%5iN7%>-+EHE zFZcPys^+-)8T}XzYubVAb4w@>dH}#xX)F1i8c{SosVpMxd#dP1wgB;~DxR8C36Xj) zTN^8$`|g{s)!0flzi#K8OYzaLm-DVM)tUq_unLAsON|Ut?6&RF+ABbETd-zxD>%@L zzVJ}5y!0~boL-g&MZf{xbrk953Aku1IA(d@oIWAi?qP>-i&C`>$>{E}99mxL7gz=zDG_>0l%zBxqxgAe%%FL580eD^?Me%?sYk#NzXXp(279kEx=hi zl6m}ww6JtF`|4rZzN}ZT2Mre0wJm?Y?M$v#Z=!X!_;JkLru9%y=toc1?bb+ct6$2e zU0DbqUDmfR07=(_4k*_KJ@5%1^`Zv$NAQQ3ym5vY?q*gtU!XDsmIQvrmH^`rr zk&UQmbRXJQw7uJ5_+B5wgWFEQt3L2MLYs8UG0|(1t$osIeq-Tw&+r7AU>P*rvKff- z>q5-nn;k0G4veaFgEUAEl^&7A?(7X@@12y9edZ>aH$p!AEUw(ge&JTNg7gsY>i#*t zEG^k_JE|oBl)OH+nnJy=aHVRr9PZF^u7DXuGNiuh(s1QSqOC7|=3Vu^-W^3frr&1j zzpm!;jN;^tZ5Y@Sjue9=54uNC{@oz-H*AFDX(KE^{k|~j#xZ4)yD-qt^hpM zMHN}Qvh8nu2y3~KTSt#zm43;{zICJ}B@{*VeyL&`GHYP1orY~!1+$qjE+Sx3d^!A} zEFARK+Mh3n7G4qZf_n8K+kLN3f@+>NxadJhXYbZU_w0gpy&vAXjn1tzAdMN?te|;r zT5$fWPAK*Pv;tF8!0dPZsGMFO>bZoXhDMvq?=MOS?VyKQNEOM*8rg?6|Ay2@&bV-% zJlcCkdK(gQeYcVc8EzOP$fTH;XfMsKj#hn7Xjw(8Cw#9tb0OzEuw$;iNeyZ^BYSqS z+?62DF^#7g>BH?$qOs__A4t=d)XS)TX{mbs!vKiAq#wNxv>>XSOOCUhHMLVvxLp&Z zfZU#XW)8pb=}E?GZ(%PJsQRR3z5JK)$2rsfK64(pXGoEXGQ9#5F%Yee?+u3_j`o`> zK%3Q%eqg@eDhbbZPs~V;qD)z#P0*_poS^q$fC6ymM{o@JSI|BFKVIlbX5qr_2lcs< zQ_&$ek*Ux;LT&!rS-GfF(M&V>^!nHnn0Kf3`XA+z%#3VUYQ^(3I=GK$#$0s>jdv0n z2~8IAdGRTneGD{PNEKJFJZc@4dl$l^FpMlZZU@wFS)_fYwxaES?0Q;Oeqmpdc5Wng z>bG$)4R)-bkzjIGDkPaV$+!!!bt=`07#>#OUiEUEvMylr*2OUcur8fC{016Ufv4_6 z-UJs1g>p2`>iv;!{3|f*x$z}RFYKn2ftrox)R$uH4C@@ObmpM?%C%r7- z6=C$p;8UaL)g7I*$I0-n`*`XbdoRtF_(KZ`6BVuM5&2AuvA_x0Ww)Dwl%CwtgQCgh)gyk58X@) z#F=kpHl4cbMwNvFPE?BERU6@T&ls(q;wv7HcVre~jzr-%xV>_b{iNAP`ips{ZlW%o zIfR&lQZ)Ls0uzhh|VjK98{MIdoMNwZ6Tg!b+STzRfk%GwL!sR5iJe~V^22K z8^uX5>z89D(hFOwLr+ZXW7&x#By`!05L8=ELht!_o$`U*!Dk0 z(RO&`GMOdnC38i%7C;n`SI`Ls&Q)JWH@0>boq~kD&z|5nlnOf$V~9nVk^+0zQSUK; zh?B$yk$LpnRPz~-Y#RtFfm@cY4FDD*h0ZLR_vs}%#_KNL;tb0G$7Vx7$h)uDy@pk` z{I5C;`4n^#ZWJ;KPS^wYNeh_7Absp?WX8O1jgY9#oWA5regW8H>U(67cH*HP4>!Sd zg^mZ5xVpGuztAoE`g@C`mbHVb@P4X=yT?Q_|27g;yM>n)rII_$Xqjy_yZ5bV`8?*171^Vbj=V$_s5xx*B z%snuL^zrlGK2SWU61&9#>Zj4=f7z9^f;IZzhd)OKFl%=dIGaG*dQ4`&!a#OY5qZx- zMsepM><;3+roZ03EmQV^JSG*!j$TMX_Vx^JSud!H2?nYi8K}#DyFvFq$Rw0na-!0o zCQ0tf#71M7=1IuS!I!FBF&+Z9WUJh%+Rtk`qV4z#b$)SP2e>NYQTYfW^PN2u#17ZV zgZfo>c9S&jNZfiky~2L;+1+;RcBQMh9VvAF0Y6GF<-(|ZX${ZQ$=yFgLJYaZrJ9<5 z_Hkz~XPVm=*XLGA(YLi%_5^U2enA@AMSUm_XH3Cs+_FK?AVvwz=G-3{j_SjL02}P->5MN}+Ecc6aE?qiGacT!G%aHL9)cK@5$VwX{rR`G?M=t2VygG< zkAT5M{@3}S*EPsg&MEKy$obzN_ZDg*w@I;}T6dczyW6bEHaFUTT6*J2$9J8~hku(D zFH1!Ot&j9+cO-KE`=chQC#!l$9V+8DklD=NUHitWGdO)X3z*DDk(sdAT}as~dDwgQ z^*osBncW|uPz}pdMTw44H>irAVg<5#6Y%2NJRz$gGMOqdDE-=y}z5}7vFPz&-2{({oL>K=0`4g zqZ=38l%iQ%{k)lOAfbDAEo){V`GA-4$7Sa>-1gbRT8dO} zvDN#9-ofXs9jv`xn|<|2r}z6U&rzE^KhJc|+g367puef-SRc#fL*1PYEeK_*dq+%4`($}~$!R2$ zQmGcGue^y~jA~Wu7e#n;v=!>}u-J@J+j6+VVZ9U^`Y1u=22>em>4>@?OV~j-4@rvf zO%lo?p`78xvGHsh<)pku;Dl1gY~vBScJe6RcT9Or-kJGB=sN{hIA;{6;jrorDBc<} z4=Y{hHp%NpHdMQ_+c@vBY^nh=K*nk$Yq1JUK>ppXG5_|8=-!! zIQ^;B=+f>7$+WN;XoT+r8lusQhDy!!6*`~_I{1+eb(3=EFqN*x5chiv#Z-8m;k%i(yEz&}J(`xozZp}{Clz}XOb2@?GzuQ2bHOe%4W!&Q4r zM3EVs(7MnuPP*bM1APx6E>Wx@>n!e#PzK?|3@0SpN=P^t@P%h)dlaN~5pyt#488 zI}N813RQrBEk3T1!cn6*0~ZiX4AJ4?(h2M}(R)+&P(xLKG4%HcrN7Q7sFdBoGm*k$ z*pn0$k)m_?`-y{c@dbj%-y9mj>1&#$#8_7Qmw=}uhTsX!bK{6+gXz`-%w_UsuK7)g zAMZ2*`y17N<}Y$0ln0j zMt=*`ovgiNXrh+w_?Pj@Ex#7YSA}`HTbtPOuI16EIsyB$jVaXlT2HlfCC_9H{l^mO z3`Wg#Cw0|af^;gF9H(P;>Wb^WnJPhuZJzJUN+7dZGTBcTr#m9qCj{o>_M7geuZ-ei zTmh1|h=}AN^QZ9F$CP1i?L7c8DhebU_Vsczt>{hX=mEdYz;_!sO+5Y{b^AkpSnOOU zXj5(;V8N=K0J%yx``f`h$Pp8(uhBQWCB?psirNO2WN#%_8{%<=|1gS=<13_XA!(a5 z|2h`@n!g(-@<7CgZUkMmJ1#sk+v&#aqQ!)olu)bt=K$q?fnPzHDoA58lvyS@T4*$i z^RNf0=R@knE~=>%r(gjwDE!x26u*m%>UM`a8C3@D8UP6IkRua1Iz`{`QH5MI2mxOs z8}RAt;Yr^!-$wS^5UAi`R$(Lz*BnwMT{0At7lX-*y<=SA8`!@@yN^CVI6oqsLm!V( zDK5c+Sa4VnY3)K<+qiOxyWhe=aDH3z3C2%7Y5Sc;=N2Tml;BpGV?lCfABiIU=_9?C zl#gL;GPfXWhY-5>na1s1W9VfmGirJfkV9p${w(tZFgf4?eV0exr zX>`mkU309k6!gl6^WXjPC@j`xd%hRN83%Sr&^PrsFH(5RD2~BHBr%*MYUK0{UzM)X zkO_PjKr31JwS7!E_uRaGIlzshhSXY;s)054H=~%3`vIMi?EO;12<2YJ5A0g9L9#aq zvi;`08W#8&Z(!kG975Z{L$dHc?PU(uYo%4ID;@~_3Z~orvLuvwQm^f=TAet8&>eGV z_&4cmJ@eW0z(Fuy4k#>>Dnpqw^;#31>j=nv$!c_tID`^%a1;r#O${=a?1n@(JtYVOGGzp@%Q6|?jfDos0O8Ty6O zHaIz6b?XO-tcUewvA#)(51#qkV*M(8#=kDC8{mJ(013WT+(xpt4tuWOe!((1+el_(M{{{m6;k5t& literal 0 HcmV?d00001 From 043ec6460308bf5aeb8f08c04c372d4024ffa862 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 5 Dec 2018 14:37:32 -0500 Subject: [PATCH 0367/2020] Proposal: Provide feedback on push progress. (#1262) --- proposals/progress_output.md | 184 +++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 proposals/progress_output.md diff --git a/proposals/progress_output.md b/proposals/progress_output.md new file mode 100644 index 0000000000..58aa823d3d --- /dev/null +++ b/proposals/progress_output.md @@ -0,0 +1,184 @@ +# Proposal: Provide feedback on push progress + +Relevant issues: [#806](https://github.com/GoogleContainerTools/jib/issues/806), [#1251](https://github.com/GoogleContainerTools/jib/issues/1251) + +## Motivation + +Currently, Jib lacks any progress feedback for layer pushes. In cases where layers take a long time to push, this lack of progress indication: + +- makes it hard to estimate how long the build will take +- makes it unclear as to whether the build is stuck or will complete in some reasonable amount of time +- may deter first-time users from continuing to use Jib + +This is especially prevalent for first-time users whose first builds would need to push all the layers, including the large base image layers. + +## Current output + +An example of the current output (as of version `0.10.0`) looks like: + +``` +Containerizing application to ... +warning: Base image 'gcr.io/distroless/java' does not use a specific image digest - build may not be reproducible +Retrieving registry credentials for gcr.io... +Getting base image gcr.io/distroless/java... +Building dependencies layer... +Building resources layer... +Building classes layer... + +Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*,

    ] +Finalizing... + +Built and pushed image as +``` + +Note that the build would pause on `Finalizing...` until all layers are finished pushing. + +## Goal + +To display fine-grained progress feedback during the build, but not clutter the log messages. + +## Design considerations + +Since Jib builds/pulls and pushes each layer independently, synchronized progress feedback (completion over all layers, such as a progress bar) could potentially limit the concurrency of the build process - but this might be negligible. + +The available information for each layer include: + +- type (classes, resources, dependencies, base image layer, etc.) +- digest +- size + +The non-available information for each layer include: + +- sizes of other layers (eg. other layers may not have finished building) +- number of other layers (eg. base image layers have not been resolved before starting to push an application layer) + +Therefore, the actions we can perform for progress feedback include: + +- monitor the number of bytes sent (and percentage completion) + +And the actions we cannot perform include: + +- know ahead of time how many layers will be pulled/pushed + +## Proposal + +Display an overall progress bar along with the tasks currently being executed. + +### Example + +``` +[===================== ] 60% complete +> Pushing classes layer +> Pulling base image layer 50501d3b88f7 +> Pushing dependencies layer +> Pushing base image layer 8b106a18283f +``` + +The currently executing tasks are displayed below the overall progress bar. + +*Note that this would replace the logs messages that are currently outputted as those may corrupt the progress bar display.* + +### Summary + +The implementation consists of two parts: + +1. Emit progress events from the builder steps. +1. Monitor progress events and display to console. + +### Emit progress events + +There are a few issues to address for the progress event design: + +- the total amount of work is not known beforehand, so there is no static max progress known at any time +- the builder steps are asynchronous and therefore progress events should not share a single progress state +- rather, the progress event receiver should be the only potentially stateful entity + +These issues can be resolved with a *decentralized allocation tree*. + +#### Decentralized allocation tree (DAT) + +Each node in the DAT is immutable and initialized with a count of allocation units. These allocation units are to be claimed by progress made on that node or child nodes. Each child node claims 1 allocation unit of the parent node, meaning that completion of all allocation units on that child node means completion of 1 allocation unit on the parent node. + +##### Example + +Consider a root node (`Node A`) with 10 allocation units. A child node (`Node B`) is added with 4 allocation units. Each of these allocation units represents `10% x 25% = 2.5%` of the entire DAT progress. Completion of all the 4 child node allocation units means that the root node completed 1 allocation unit, or `10%`. + +New allocations can be added on-the-fly in a decentralized manner. Let's say a new download started for `123000` bytes and that download should be represented by one of the allocation units of `Node B`. We would create a new node (`Node C`) with `123000` allocation units and have its parent be `Node B`. This establishes the `123000` bytes that would represent `2.5%` of the overall progress without needing to modify other nodes or synchronize with other tasks that may add their own sub-allocations. This also limits the creation of suballocations to only children tasks of the task that created that allocation, making it fully-compatible with a DAG task pipeline. + +#### Benefits of DAT approach + +The DAT approach solves all of the issues present: + +- The progress % would never decrease even in an unknown total work scenario +- The dynamic suballocation supports the asynchronous builder steps +- The allocations are immutable, and state (total progress amount) can be kept at the receiver (DAT reader) side + +The downside is that the overall progress is not linear in scale among the work of each allocation. For example, an allocation representing a download may have each byte represent `1%` of the total progress while another may have each byte represent `2%`. + +### Monitor progress events + +Progress events will be emitted with +1. an allocation node, and +1. a number of progress units completed on that allocation node + +Therefore, the progress monitor receiving the progress events will only need to keep a single total progress amount as its state. Upon receiving a progress event, the progress monitor updates its total progress amount by: + +``` +totalProgressAmount += progressUnits / progressTotal * allocationFraction; +``` + +`progressUnits` - the progress units from the progress event \ +`progressTotal` - the number of allocation units for the allocation node associated with the progress event \ +`allocationFraction` - the fraction of the total progress represented by the allocation node, calculated as the *inverted product of all the parent allocation units* + +### Display to console + +The progress monitor can display a progress bar to the console based on the `totalProgressAmount` upon each update. Using `\r` could work, but results in the console cursor overlapping with the progress bar line. The proposal is to, for each update, move the cursor up a line (`\033[1A`) and print the progress bar line with a newline at the end. + +The progress monitor can also keep track of which allocation nodes have completed to display the currently executing tasks. + +For Gradle, in order to keep the log messages in sync with Gradle's own log messages (such as the Gradle progress bar), the progress bar display should effectively become a "footer" to the normal log messages Jib outputs and be displayed via the same Gradle Logger interface as the rest of the log messages. + +## Alternative rejected proposal + +*This alternative proposal was rejected because it cluttered the log messages too much.* + +Display percentage completion of layer pushes at 10s intervals with a 20s cliff. + +### Example + +``` +... +Getting base image gcr.io/distroless/java… +Building dependencies layer… +Building resources layer... +Building classes layer... +Pushing base image layer d4c593cddccd: 30% +Pushing dependencies layer: 30% +Pushing base image layer 8b106a18283f: 10% +Pushing resources layer: 100% +Pushing classes layer: 100% +Pushing base image layer 8b106a18283f: 45% +Pushing base image layer 50501d3b88f7: 60% +Pushing base image layer d4c593cddccd: 100% +Pushing dependencies layer: 65% +Pushing base image layer 50501d3b88f7: 100% +Pushing dependencies layer: 90% +Pushing base image layer 8b106a18283f: 70% +Pushing dependencies layer: 100% +Pushing base image layer 8b106a18283f: 90% +Pushing base image layer 8b106a18283f: 100% + +Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*,
    ] +Finalizing... + +Built and pushed image as +``` + +Note that `100%` will always be displayed to indicate completion of each layer. + +### Alternative considerations + +- display byte-completion (eg. `10MB/100MB`) +- display with time (eg. `25% (10s)`) +- display with progress bar (eg. `[===== ] 10MB/100MB (15s)`) From ac22ab8fe495f06e154b85d390ace297a8b48f66 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 5 Dec 2018 14:37:48 -0500 Subject: [PATCH 0368/2020] Adds ProgressEvent. (#1305) --- .../cloud/tools/jib/event/JibEventType.java | 5 + .../tools/jib/event/events/ProgressEvent.java | 62 ++++++++ .../tools/jib/event/progress/Allocation.java | 38 ++++- .../jib/event/events/ProgressEventTest.java | 140 ++++++++++++++++++ 4 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java index cae441e903..6f9c47dae7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.event; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.common.annotations.VisibleForTesting; @@ -35,6 +36,10 @@ public class JibEventType { /** Timer event for timing various part of Jib's execution. */ public static final JibEventType TIMING = new JibEventType<>(TimerEvent.class); + /** Event indicating progress for tasks in Jib's execution. */ + public static final JibEventType PROGRESS = + new JibEventType<>(ProgressEvent.class); + // TODO: Add entries for all JibEvent types. private final Class eventClass; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java new file mode 100644 index 0000000000..ff8fd9db2d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; + +/** + * Event representing progress. The progress accounts for allocation units in an {@link Allocation}, + * which makes up a Decentralized Allocation Tree. + * + * @see Allocation for more details + */ +public class ProgressEvent implements JibEvent { + + /** + * The allocation this progress is for. Each progress unit accounts for a single allocation unit + * on the {@link Allocation}. + */ + private final Allocation allocation; + + /** Units of progress. */ + private final long progressUnits; + + ProgressEvent(Allocation allocation, long progressUnits) { + this.allocation = allocation; + this.progressUnits = progressUnits; + } + + /** + * Gets the {@link Allocation} this progress event accounts for. + * + * @return the {@link Allocation} + */ + Allocation getAllocation() { + return allocation; + } + + /** + * Gets the units of progress this progress event accounts for in the associated {@link + * Allocation}. + * + * @return units of allocation + */ + long getUnits() { + return progressUnits; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java index 77101c5ad8..ba0a3982a7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -38,7 +38,7 @@ *

    Note that it is up to the user of the class to ensure that the number of sub-allocations does * not exceed the number of allocation units. */ -class Allocation { +public class Allocation { /** * Creates a new root {@link Allocation}. @@ -47,7 +47,7 @@ class Allocation { * @param allocationUnits number of allocation units * @return a new {@link Allocation} */ - static Allocation newRoot(String description, long allocationUnits) { + public static Allocation newRoot(String description, long allocationUnits) { return new Allocation(description, allocationUnits, null); } @@ -78,23 +78,47 @@ private Allocation(String description, long allocationUnits, @Nullable Allocatio * @param allocationUnits number of allocation units the child holds * @return a new {@link Allocation} */ - Allocation newChild(String description, long allocationUnits) { + public Allocation newChild(String description, long allocationUnits) { return new Allocation(description, allocationUnits, this); } - Optional getParent() { + /** + * Gets the parent allocation, or {@link Optional#empty} if this is a root allocation. This + * allocation represents 1 allocation unit of the parent allocation. + * + * @return the parent {@link Allocation} + */ + public Optional getParent() { return Optional.ofNullable(parent); } - String getDescription() { + /** + * Gets a user-facing description of what this allocation represents. For example, this can a + * description of the task this allocation represents. + * + * @return the description + */ + public String getDescription() { return description; } - long getAllocationUnits() { + /** + * Gets the allocation units this allocation holds. If this allocation is not the root, the full + * number of units represents 1 allocation unit of the parent. + * + * @return the allocation units + */ + public long getAllocationUnits() { return allocationUnits; } - double getFractionOfRoot() { + /** + * Gets how much of the root allocation this allocation accounts for. The entire root allocation + * is {@code 1.0}. + * + * @return the fraction of the root allocation this allocation accounts for + */ + public double getFractionOfRoot() { return fractionOfRoot; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java new file mode 100644 index 0000000000..ed99bd9aa2 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.progress.Allocation; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ProgressEvent}. */ +public class ProgressEventTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("ignored", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("ignored", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("ignored", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("ignored", 200); + + private AllocationTree() {} + } + + private static EventDispatcher makeEventDispatcher( + Consumer progressEventConsumer) { + return new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.PROGRESS, progressEventConsumer)); + } + + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + private final Map allocationCompletionMap = new HashMap<>(); + + private double progress = 0.0; + + @Test + public void testAccumulateProgress() { + Consumer progressEventConsumer = + progressEvent -> { + Allocation allocation = progressEvent.getAllocation(); + long units = progressEvent.getUnits(); + + progress += units * allocation.getFractionOfRoot() / allocation.getAllocationUnits(); + }; + + EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + Assert.assertEquals(1.0 / 2 / 100 * 50, progress, DOUBLE_ERROR_MARGIN); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + Assert.assertEquals(1.0 / 2, progress, DOUBLE_ERROR_MARGIN); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 10)); + Assert.assertEquals(1.0 / 2 + 1.0 / 2 / 200 * 10, progress, DOUBLE_ERROR_MARGIN); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 190)); + Assert.assertEquals(1.0, progress, DOUBLE_ERROR_MARGIN); + } + + @Test + public void testSmoke() { + Consumer progressEventConsumer = + progressEvent -> { + Allocation allocation = progressEvent.getAllocation(); + long units = progressEvent.getUnits(); + + updateCompletionMap(allocation, units); + }; + + EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + + Assert.assertEquals(1, allocationCompletionMap.size()); + Assert.assertEquals(50, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + + Assert.assertEquals(3, allocationCompletionMap.size()); + Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.root).longValue()); + + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 200)); + + Assert.assertEquals(4, allocationCompletionMap.size()); + Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); + Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); + Assert.assertEquals(200, allocationCompletionMap.get(AllocationTree.child2).longValue()); + Assert.assertEquals(2, allocationCompletionMap.get(AllocationTree.root).longValue()); + } + + /** + * Updates the {@link #allocationCompletionMap} with {@code units} more progress for {@code + * allocation}. This also updates {@link Allocation} parents if {@code allocation} is complete in + * terms of progress. + * + * @param allocation the allocation the progress is made on + * @param units the progress units + */ + private void updateCompletionMap(Allocation allocation, long units) { + if (allocationCompletionMap.containsKey(allocation)) { + units += allocationCompletionMap.get(allocation); + } + allocationCompletionMap.put(allocation, units); + + if (allocation.getAllocationUnits() == units) { + allocation + .getParent() + .ifPresent(parentAllocation -> updateCompletionMap(parentAllocation, 1)); + } + } +} From 2689a0b6fe09f2ef477c196c3aff66095aba7ea1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 5 Dec 2018 17:18:42 -0500 Subject: [PATCH 0369/2020] Consolidate digest generation in LoadDockerStep and WriteTarFileStep (#1307) --- .../tools/jib/builder/steps/BuildResult.java | 35 +++++++++++++++++++ .../jib/builder/steps/LoadDockerStep.java | 24 +------------ .../jib/builder/steps/WriteTarFileStep.java | 24 +------------ .../jib/builder/steps/BuildResultTest.java | 17 +++++++++ 4 files changed, 54 insertions(+), 46 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index e5b37b1398..383b055760 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -16,12 +16,47 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; +import java.io.IOException; import java.util.Objects; /** Used to record the results of a build. */ public class BuildResult { + /** + * Gets a {@link BuildResult} from an {@link Image}. + * + * @param image the image + * @param targetFormat the target format of the image + * @return a new {@link BuildResult} with the image's digest and id + * @throws IOException if writing the digest or container configuration fails + */ + static BuildResult fromImage( + Image image, Class targetFormat) + throws IOException { + ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); + BlobDescriptor containerConfigurationBlobDescriptor = + imageToJsonTranslator + .getContainerConfigurationBlob() + .writeTo(ByteStreams.nullOutputStream()); + BuildableManifestTemplate manifestTemplate = + imageToJsonTranslator.getManifestTemplate( + targetFormat, containerConfigurationBlobDescriptor); + DescriptorDigest imageDigest = + JsonTemplateMapper.toBlob(manifestTemplate) + .writeTo(ByteStreams.nullOutputStream()) + .getDigest(); + DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + return new BuildResult(imageDigest, imageId); + } + private final DescriptorDigest imageDigest; private final DescriptorDigest imageId; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index c124db5810..a162ef1703 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -18,20 +18,14 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; -import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -114,22 +108,6 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } - // TODO: Consolide image digest generation with PushImageStep and WriteTarFileStep. - // Gets the image manifest to generate the image digest. - ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); - BlobDescriptor containerConfigurationBlobDescriptor = - imageToJsonTranslator - .getContainerConfigurationBlob() - .writeTo(ByteStreams.nullOutputStream()); - BuildableManifestTemplate manifestTemplate = - imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); - DescriptorDigest imageDigest = - JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); - DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); - - return new BuildResult(imageDigest, imageId); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index a0b43db8e1..5f69c3d9cc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -18,19 +18,13 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; -import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -110,22 +104,6 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { .writeTo(outputStream); } - // TODO: Consolidate image digest generation with PushImageStep and LoadDockerStep. - // Gets the image manifest to generate the image digest. - ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); - BlobDescriptor containerConfigurationBlobDescriptor = - imageToJsonTranslator - .getContainerConfigurationBlob() - .writeTo(ByteStreams.nullOutputStream()); - BuildableManifestTemplate manifestTemplate = - imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); - DescriptorDigest imageDigest = - JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); - DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); - - return new BuildResult(imageDigest, imageId); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java index 8786a50d44..2357b9d62a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -17,6 +17,10 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import java.io.IOException; import java.security.DigestException; import org.junit.Assert; import org.junit.Before; @@ -59,4 +63,17 @@ public void testEquality() { Assert.assertEquals(container1.hashCode(), container2.hashCode()); Assert.assertNotEquals(container1, container3); } + + @Test + public void testFromImage() throws IOException { + Image image1 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image2 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image3 = Image.builder(V22ManifestTemplate.class).setUser("anotherUser").build(); + Assert.assertEquals( + BuildResult.fromImage(image1, V22ManifestTemplate.class), + BuildResult.fromImage(image2, V22ManifestTemplate.class)); + Assert.assertNotEquals( + BuildResult.fromImage(image1, V22ManifestTemplate.class), + BuildResult.fromImage(image3, V22ManifestTemplate.class)); + } } From 844d9b4a86209d80077cb5167644377900b58bc2 Mon Sep 17 00:00:00 2001 From: auke- Date: Thu, 6 Dec 2018 20:23:50 +0100 Subject: [PATCH 0370/2020] Handle gwt-app packaging the same as war packaging (#1302) (#1303) Project which result in a war file are added in their exploded form to the docker image layers. This is only the case if the packaging is 'war', which poses a problem for GWT projects created by the gwt-maven-plugin. This change adds support for the gwt-app packaging in order to use the jib maven plugin together with the gwt maven plugin. --- .../jib/maven/MavenLayerConfigurations.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../cloud/tools/jib/maven/MojoCommon.java | 17 +++++++++-- .../maven/MavenLayerConfigurationsTest.java | 28 +++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index c3a3f7abc8..c88a24e1f3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -52,7 +52,7 @@ static JavaLayerConfigurations getForProject( Map extraDirectoryPermissions, AbsoluteUnixPath appRoot) throws IOException { - if ("war".equals(project.getPackaging())) { + if (MojoCommon.isWarProject(project)) { return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); } else { return getForNonWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 12fd439e8a..d600c89ed8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -159,7 +159,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - return "war".equals(project.getPackaging()); + return MojoCommon.isWarProject(project); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 4d332c1c86..7961e09bb2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -27,10 +27,23 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.maven.project.MavenProject; -/** Collection of common methods to share between Gradle tasks. */ +/** Collection of common methods to share between Maven goals. */ class MojoCommon { + /** + * Gets whether or not the given project is a war project. This is the case for projects with + * packaging {@code war} and {@code gwt-app}. + * + * @param project the Maven project + * @return {@code true} if the project is a war project, {@code false} if not + */ + static boolean isWarProject(MavenProject project) { + String packaging = project.getPackaging(); + return "war".equals(packaging) || "gwt-app".equals(packaging); + } + /** * Gets the value of the {@code } parameter. If the parameter is empty, * returns {@link JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for project with WAR packaging or @@ -45,7 +58,7 @@ static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfig throws InvalidAppRootException { String appRoot = jibPluginConfiguration.getAppRoot(); if (appRoot.isEmpty()) { - boolean isWarProject = "war".equals(jibPluginConfiguration.getProject().getPackaging()); + boolean isWarProject = isWarProject(jibPluginConfiguration.getProject()); appRoot = isWarProject ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 127e8cf90e..14c555faa2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -347,4 +347,32 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOE MavenLayerConfigurations.getForProject( mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass } + + @Test + public void testIsWarProject_WarPackagingIsWar() throws IOException { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + + Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_GwtAppPackagingIsWar() throws IOException { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-app"); + + Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_JarPackagingIsNotWar() throws IOException { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); + + Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_GwtLibPackagingIsNotWar() throws IOException { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-lib"); + + Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + } } From add4f95477e9d5804154b671ab69e2d9a4274da0 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 6 Dec 2018 16:46:01 -0500 Subject: [PATCH 0371/2020] Adds ListenableCountingOutputStream. (#1300) --- .../http/ListenableCountingOutputStream.java | 127 ++++++++++++++++++ .../ListenableCountingOutputStreamTest.java | 87 ++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java new file mode 100644 index 0000000000..5de0164620 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Counts the number of bytes written and reports the count to a callback. The count is reported + * with certain time delays to avoid calling the callback too often. + */ +class ListenableCountingOutputStream extends OutputStream { + + /** The underlying {@link OutputStream} to wrap and forward bytes to. */ + private final OutputStream underlyingOutputStream; + + /** Receives a count of bytes written since the last call. */ + private final Consumer byteCountConsumer; + + /** Delay between each call to {@link #byteCountConsumer}. */ + private final Duration delayBetweenCallbacks; + + /** Supplies the current {@link Instant}. */ + private final Supplier instantSupplier; + + /** Number of bytes to provide to {@link #byteCountConsumer}. */ + private long byteCount = 0; + + /** Last time {@link #byteCountConsumer} was called. */ + private Instant previousCallback; + + /** + * Wraps the {@code underlyingOutputStream} to count the bytes written. + * + * @param underlyingOutputStream the wrapped {@link OutputStream} + * @param byteCountConsumer the byte count {@link Consumer} + * @param delayBetweenCallbacks the minimum delay between each call to {@link #byteCountConsumer} + */ + ListenableCountingOutputStream( + OutputStream underlyingOutputStream, + Consumer byteCountConsumer, + Duration delayBetweenCallbacks) { + this(underlyingOutputStream, byteCountConsumer, delayBetweenCallbacks, Instant::now); + } + + @VisibleForTesting + ListenableCountingOutputStream( + OutputStream underlyingOutputStream, + Consumer byteCountConsumer, + Duration delayBetweenCallbacks, + Supplier instantSupplier) { + this.underlyingOutputStream = underlyingOutputStream; + this.byteCountConsumer = byteCountConsumer; + this.delayBetweenCallbacks = delayBetweenCallbacks; + this.instantSupplier = instantSupplier; + + previousCallback = instantSupplier.get(); + } + + @Override + public void write(int singleByte) throws IOException { + underlyingOutputStream.write(singleByte); + countBytesWritten(1); + } + + @Override + public void write(byte[] byteArray) throws IOException { + underlyingOutputStream.write(byteArray); + countBytesWritten(byteArray.length); + } + + @Override + public void write(byte byteArray[], int offset, int length) throws IOException { + underlyingOutputStream.write(byteArray, offset, length); + countBytesWritten(length); + } + + @Override + public void flush() throws IOException { + underlyingOutputStream.flush(); + callByteCountConsumer(instantSupplier.get()); + } + + @Override + public void close() throws IOException { + underlyingOutputStream.close(); + callByteCountConsumer(instantSupplier.get()); + } + + private void countBytesWritten(int byteCount) { + this.byteCount += byteCount; + + Instant now = instantSupplier.get(); + if (previousCallback.plus(delayBetweenCallbacks).isBefore(now)) { + callByteCountConsumer(now); + } + } + + private void callByteCountConsumer(Instant now) { + if (byteCount == 0) { + return; + } + + byteCountConsumer.accept(byteCount); + byteCount = 0; + previousCallback = now; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java new file mode 100644 index 0000000000..6f3f126897 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ListenableCountingOutputStream}. */ +public class ListenableCountingOutputStreamTest { + + @Test + public void testCallback_correctSequence() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + List byteCounts = new ArrayList<>(); + + try (ListenableCountingOutputStream listenableCountingOutputStream = + new ListenableCountingOutputStream( + byteArrayOutputStream, byteCounts::add, Duration.ofSeconds(-1))) { + listenableCountingOutputStream.write(0); + listenableCountingOutputStream.write(new byte[] {1, 2, 3}); + listenableCountingOutputStream.write(new byte[] {1, 2, 3, 4, 5}, 3, 2); + } + + Assert.assertEquals(Arrays.asList(1L, 3L, 2L), byteCounts); + Assert.assertArrayEquals(new byte[] {0, 1, 2, 3, 4, 5}, byteArrayOutputStream.toByteArray()); + } + + @Test + public void testDelay() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + List byteCounts = new ArrayList<>(); + + Queue instantQueue = new ArrayDeque<>(); + instantQueue.add(Instant.EPOCH); + + try (ListenableCountingOutputStream listenableCountingOutputStream = + new ListenableCountingOutputStream( + byteArrayOutputStream, byteCounts::add, Duration.ofSeconds(3), instantQueue::remove)) { + instantQueue.add(Instant.EPOCH); + listenableCountingOutputStream.write(100); + instantQueue.add(Instant.EPOCH); + listenableCountingOutputStream.write(new byte[] {101, 102, 103}); + instantQueue.add(Instant.EPOCH.plusSeconds(4)); + listenableCountingOutputStream.write(new byte[] {104, 105, 106}); + + instantQueue.add(Instant.EPOCH.plusSeconds(10)); + listenableCountingOutputStream.write(new byte[] {107, 108}); + + instantQueue.add(Instant.EPOCH.plusSeconds(10)); + listenableCountingOutputStream.write(new byte[] {109}); + instantQueue.add(Instant.EPOCH.plusSeconds(13)); + listenableCountingOutputStream.write(new byte[] {0, 110}, 1, 1); + + instantQueue.add(Instant.EPOCH.plusSeconds(14)); + } + + Assert.assertEquals(Arrays.asList(7L, 2L, 2L), byteCounts); + Assert.assertArrayEquals( + new byte[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110}, + byteArrayOutputStream.toByteArray()); + } +} From db551ba47cfc6c2b2b127cf4374c65c768904f36 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 6 Dec 2018 18:32:41 -0500 Subject: [PATCH 0372/2020] CHANGELOG: WAR image for gwt-app packaging type (#1313) --- jib-maven-plugin/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a1a91c58e8..99f891bd4b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added ### Changed +- `gwt-app` packaging type now builds a WAR container. ### Fixed From 887da310635076f3f3e3a4e1bf921e946349d77f Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 6 Dec 2018 18:40:55 -0500 Subject: [PATCH 0373/2020] Change StepsRunner to be more of a builder to prepare for assigning Allocations. (#1312) --- .../cloud/tools/jib/builder/BuildSteps.java | 84 ++--- .../tools/jib/builder/steps/StepsRunner.java | 310 ++++++++++-------- 2 files changed, 209 insertions(+), 185 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 4039891578..19355f0aff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -30,20 +30,6 @@ public class BuildSteps { private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; - /** Runs appropriate steps to build an image. */ - @FunctionalInterface - private interface ImageBuildRunnable { - - /** - * Builds an image. - * - * @return the built image - * @throws ExecutionException if an exception occurs during execution - * @throws InterruptedException if the execution is interrupted - */ - BuildResult build() throws ExecutionException, InterruptedException; - } - /** * All the steps to build an image to a Docker registry. * @@ -54,20 +40,18 @@ public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfig return new BuildSteps( DESCRIPTION_FOR_DOCKER_REGISTRY, buildConfiguration, - () -> - new StepsRunner(buildConfiguration) - .runRetrieveTargetRegistryCredentialsStep() - .runAuthenticatePushStep() - .runPullBaseImageStep() - .runPullAndCacheBaseImageLayersStep() - .runPushBaseImageLayersStep() - .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep() - .runPushContainerConfigurationStep() - .runPushApplicationLayersStep() - .runFinalizingPushStep() - .runPushImageStep() - .waitOnPushImageStep()); + StepsRunner.begin(buildConfiguration) + .retrieveTargetRegistryCredentials() + .authenticatePush() + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .pushBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .pushContainerConfiguration() + .pushApplicationLayers() + .finalizingPush() + .pushImage()); } /** @@ -82,15 +66,13 @@ public static BuildSteps forBuildToDockerDaemon( return new BuildSteps( DESCRIPTION_FOR_DOCKER_DAEMON, buildConfiguration, - () -> - new StepsRunner(buildConfiguration) - .runPullBaseImageStep() - .runPullAndCacheBaseImageLayersStep() - .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep() - .runFinalizingBuildStep() - .runLoadDockerStep(dockerClient) - .waitOnLoadDockerStep()); + StepsRunner.begin(buildConfiguration) + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .finalizingBuild() + .loadDocker(dockerClient)); } /** @@ -104,33 +86,29 @@ public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration build return new BuildSteps( DESCRIPTION_FOR_TARBALL, buildConfiguration, - () -> - new StepsRunner(buildConfiguration) - .runPullBaseImageStep() - .runPullAndCacheBaseImageLayersStep() - .runBuildAndCacheApplicationLayerSteps() - .runBuildImageStep() - .runFinalizingBuildStep() - .runWriteTarFileStep(outputPath) - .waitOnWriteTarFileStep()); + StepsRunner.begin(buildConfiguration) + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .finalizingBuild() + .writeTarFile(outputPath)); } private final String description; private final BuildConfiguration buildConfiguration; - private final ImageBuildRunnable imageBuildRunnable; + private final StepsRunner stepsRunner; /** * @param description a description of what the steps do * @param buildConfiguration the configuration parameters for the build - * @param imageBuildRunnable runs the necessary steps to build an image + * @param stepsRunner runs the necessary steps to build an image */ private BuildSteps( - String description, - BuildConfiguration buildConfiguration, - ImageBuildRunnable imageBuildRunnable) { + String description, BuildConfiguration buildConfiguration, StepsRunner stepsRunner) { this.description = description; this.buildConfiguration = buildConfiguration; - this.imageBuildRunnable = imageBuildRunnable; + this.stepsRunner = stepsRunner; } public BuildConfiguration getBuildConfiguration() { @@ -147,7 +125,7 @@ public BuildConfiguration getBuildConfiguration() { public BuildResult run() throws InterruptedException, ExecutionException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { - return imageBuildRunnable.build(); + return stepsRunner.run(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 934c43365f..44051b449b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -34,183 +35,228 @@ /** * Runs steps for building an image. * - *

    Use by constructing the runner and calling {@code run...} each step. Make sure that steps are - * run before other steps that depend on them. Wait on the last step. + *

    Use by first calling {@link #begin} and then calling the individual step running methods. Note + * that order matters, so make sure that steps are run before other steps that depend on them. Wait + * on the last step by calling the respective {@code wait...} methods. */ public class StepsRunner { - private final ListeningExecutorService listeningExecutorService; - private final BuildConfiguration buildConfiguration; + /** Holds the individual steps. */ + private static class Steps { - @Nullable private RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; - @Nullable private AuthenticatePushStep authenticatePushStep; - @Nullable private PullBaseImageStep pullBaseImageStep; - @Nullable private PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; + @Nullable private RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; + @Nullable private AuthenticatePushStep authenticatePushStep; + @Nullable private PullBaseImageStep pullBaseImageStep; + @Nullable private PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - @Nullable - private ImmutableList buildAndCacheApplicationLayerSteps; + @Nullable + private ImmutableList buildAndCacheApplicationLayerSteps; - @Nullable private PushLayersStep pushBaseImageLayersStep; - @Nullable private PushLayersStep pushApplicationLayersStep; - @Nullable private BuildImageStep buildImageStep; - @Nullable private PushContainerConfigurationStep pushContainerConfigurationStep; - @Nullable private PushImageStep pushImageStep; - @Nullable private LoadDockerStep loadDockerStep; - @Nullable private WriteTarFileStep writeTarFileStep; + @Nullable private PushLayersStep pushBaseImageLayersStep; + @Nullable private PushLayersStep pushApplicationLayersStep; + @Nullable private BuildImageStep buildImageStep; + @Nullable private PushContainerConfigurationStep pushContainerConfigurationStep; - public StepsRunner(BuildConfiguration buildConfiguration) { - this.buildConfiguration = buildConfiguration; + @Nullable private AsyncStep finalStep; + } + /** + * Starts building the steps to run. + * + * @param buildConfiguration the {@link BuildConfiguration} + * @return a new {@link StepsRunner} + */ + public static StepsRunner begin(BuildConfiguration buildConfiguration) { ExecutorService executorService = JibSystemProperties.isSerializedExecutionEnabled() ? MoreExecutors.newDirectExecutorService() : buildConfiguration.getExecutorService(); - listeningExecutorService = MoreExecutors.listeningDecorator(executorService); + + return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildConfiguration); } - public StepsRunner runRetrieveTargetRegistryCredentialsStep() { - retrieveTargetRegistryCredentialsStep = - RetrieveRegistryCredentialsStep.forTargetImage( - listeningExecutorService, buildConfiguration); - return this; + private final Steps steps = new Steps(); + + private final ListeningExecutorService listeningExecutorService; + private final BuildConfiguration buildConfiguration; + + /** Runnable to run all the steps. */ + private Runnable stepsRunnable = () -> {}; + + /** The total number of steps added. */ + private int stepsCount = 0; + + private StepsRunner( + ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { + this.listeningExecutorService = listeningExecutorService; + this.buildConfiguration = buildConfiguration; } - public StepsRunner runAuthenticatePushStep() { - authenticatePushStep = - new AuthenticatePushStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(retrieveTargetRegistryCredentialsStep)); - return this; + public StepsRunner retrieveTargetRegistryCredentials() { + return enqueueStep( + () -> + steps.retrieveTargetRegistryCredentialsStep = + RetrieveRegistryCredentialsStep.forTargetImage( + listeningExecutorService, buildConfiguration)); } - public StepsRunner runPullBaseImageStep() { - pullBaseImageStep = new PullBaseImageStep(listeningExecutorService, buildConfiguration); - return this; + public StepsRunner authenticatePush() { + return enqueueStep( + () -> + steps.authenticatePushStep = + new AuthenticatePushStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.retrieveTargetRegistryCredentialsStep))); } - public StepsRunner runPullAndCacheBaseImageLayersStep() { - pullAndCacheBaseImageLayersStep = - new PullAndCacheBaseImageLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(pullBaseImageStep)); - return this; + public StepsRunner pullBaseImage() { + return enqueueStep( + () -> + steps.pullBaseImageStep = + new PullBaseImageStep(listeningExecutorService, buildConfiguration)); } - public StepsRunner runPushBaseImageLayersStep() { - pushBaseImageLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep)); - return this; + public StepsRunner pullAndCacheBaseImageLayers() { + return enqueueStep( + () -> + steps.pullAndCacheBaseImageLayersStep = + new PullAndCacheBaseImageLayersStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.pullBaseImageStep))); } - public StepsRunner runBuildAndCacheApplicationLayerSteps() { - buildAndCacheApplicationLayerSteps = - BuildAndCacheApplicationLayerStep.makeList(listeningExecutorService, buildConfiguration); - return this; + public StepsRunner pushBaseImageLayers() { + return enqueueStep( + () -> + steps.pushBaseImageLayersStep = + new PushLayersStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.authenticatePushStep), + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); } - public StepsRunner runBuildImageStep() { - buildImageStep = - new BuildImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(pullBaseImageStep), - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps)); - return this; + public StepsRunner buildAndCacheApplicationLayers() { + return enqueueStep( + () -> + steps.buildAndCacheApplicationLayerSteps = + BuildAndCacheApplicationLayerStep.makeList( + listeningExecutorService, buildConfiguration)); } - public StepsRunner runPushContainerConfigurationStep() { - pushContainerConfigurationStep = - new PushContainerConfigurationStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(buildImageStep)); - return this; + public StepsRunner buildImage() { + return enqueueStep( + () -> + steps.buildImageStep = + new BuildImageStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.pullBaseImageStep), + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); } - public StepsRunner runPushApplicationLayersStep() { - pushApplicationLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - AsyncSteps.immediate(Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps))); - return this; + public StepsRunner pushContainerConfiguration() { + return enqueueStep( + () -> + steps.pushContainerConfigurationStep = + new PushContainerConfigurationStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.authenticatePushStep), + Preconditions.checkNotNull(steps.buildImageStep))); } - public StepsRunner runFinalizingPushStep() { - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Arrays.asList( - Preconditions.checkNotNull(pushBaseImageLayersStep), - Preconditions.checkNotNull(pushApplicationLayersStep)), - Collections.emptyList()); - return this; + public StepsRunner pushApplicationLayers() { + return enqueueStep( + () -> + steps.pushApplicationLayersStep = + new PushLayersStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.authenticatePushStep), + AsyncSteps.immediate( + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); } - public StepsRunner runFinalizingBuildStep() { - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Collections.singletonList(Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep)), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps)); - return this; + public StepsRunner finalizingPush() { + return enqueueStep( + () -> + new FinalizingStep( + listeningExecutorService, + buildConfiguration, + Arrays.asList( + Preconditions.checkNotNull(steps.pushBaseImageLayersStep), + Preconditions.checkNotNull(steps.pushApplicationLayersStep)), + Collections.emptyList())); } - public StepsRunner runPushImageStep() { - pushImageStep = - new PushImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(authenticatePushStep), - Preconditions.checkNotNull(pushBaseImageLayersStep), - Preconditions.checkNotNull(pushApplicationLayersStep), - Preconditions.checkNotNull(pushContainerConfigurationStep), - Preconditions.checkNotNull(buildImageStep)); - return this; + public StepsRunner finalizingBuild() { + return enqueueStep( + () -> + new FinalizingStep( + listeningExecutorService, + buildConfiguration, + Collections.singletonList( + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep)), + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); } - public StepsRunner runLoadDockerStep(DockerClient dockerClient) { - loadDockerStep = - new LoadDockerStep( - listeningExecutorService, - dockerClient, - buildConfiguration, - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), - Preconditions.checkNotNull(buildImageStep)); - return this; + public StepsRunner pushImage() { + return enqueueStep( + () -> + steps.finalStep = + new PushImageStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(steps.authenticatePushStep), + Preconditions.checkNotNull(steps.pushBaseImageLayersStep), + Preconditions.checkNotNull(steps.pushApplicationLayersStep), + Preconditions.checkNotNull(steps.pushContainerConfigurationStep), + Preconditions.checkNotNull(steps.buildImageStep))); } - public StepsRunner runWriteTarFileStep(Path outputPath) { - writeTarFileStep = - new WriteTarFileStep( - listeningExecutorService, - outputPath, - buildConfiguration, - Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps), - Preconditions.checkNotNull(buildImageStep)); - return this; + public StepsRunner loadDocker(DockerClient dockerClient) { + return enqueueStep( + () -> + steps.finalStep = + new LoadDockerStep( + listeningExecutorService, + dockerClient, + buildConfiguration, + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), + Preconditions.checkNotNull(steps.buildImageStep))); } - public BuildResult waitOnPushImageStep() throws ExecutionException, InterruptedException { - return Preconditions.checkNotNull(pushImageStep).getFuture().get(); + public StepsRunner writeTarFile(Path outputPath) { + return enqueueStep( + () -> + steps.finalStep = + new WriteTarFileStep( + listeningExecutorService, + outputPath, + buildConfiguration, + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), + Preconditions.checkNotNull(steps.buildImageStep))); } - public BuildResult waitOnLoadDockerStep() throws ExecutionException, InterruptedException { - return Preconditions.checkNotNull(loadDockerStep).getFuture().get(); + public BuildResult run() throws ExecutionException, InterruptedException { + stepsRunnable.run(); + return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); } - public BuildResult waitOnWriteTarFileStep() throws ExecutionException, InterruptedException { - return Preconditions.checkNotNull(writeTarFileStep).getFuture().get(); + private StepsRunner enqueueStep(Runnable stepRunnable) { + Runnable previousStepsRunnable = stepsRunnable; + stepsRunnable = + () -> { + previousStepsRunnable.run(); + stepRunnable.run(); + }; + stepsCount++; + return this; } } From 36c77ffdacd1539f597a43547832db9ebb1736df Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 7 Dec 2018 10:50:11 -0500 Subject: [PATCH 0374/2020] Adds ProgressEventHandler. (#1309) --- .../tools/jib/event/events/ProgressEvent.java | 6 +- .../tools/jib/event/progress/Allocation.java | 10 +- .../progress/AllocationCompletionTracker.java | 167 ++++++++++++++++++ .../event/progress/ProgressEventHandler.java | 85 +++++++++ .../jib/event/events/ProgressEventTest.java | 4 +- .../AllocationCompletionTrackerTest.java | 166 +++++++++++++++++ .../jib/event/progress/AllocationTest.java | 26 +-- .../event/progress/MultithreadedExecutor.java | 73 ++++++++ .../progress/ProgressEventHandlerTest.java | 124 +++++++++++++ 9 files changed, 638 insertions(+), 23 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java index ff8fd9db2d..feb116f624 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -36,7 +36,7 @@ public class ProgressEvent implements JibEvent { /** Units of progress. */ private final long progressUnits; - ProgressEvent(Allocation allocation, long progressUnits) { + public ProgressEvent(Allocation allocation, long progressUnits) { this.allocation = allocation; this.progressUnits = progressUnits; } @@ -46,7 +46,7 @@ public class ProgressEvent implements JibEvent { * * @return the {@link Allocation} */ - Allocation getAllocation() { + public Allocation getAllocation() { return allocation; } @@ -56,7 +56,7 @@ Allocation getAllocation() { * * @return units of allocation */ - long getUnits() { + public long getUnits() { return progressUnits; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java index ba0a3982a7..9bcd1b8275 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -60,7 +60,7 @@ public static Allocation newRoot(String description, long allocationUnits) { /** The number of allocation units this node holds. */ private final long allocationUnits; - /** How much of the root allocation (1.0) this allocation accounts for. */ + /** How much of the root allocation (1.0) each allocation unit accounts for. */ private final double fractionOfRoot; private Allocation(String description, long allocationUnits, @Nullable Allocation parent) { @@ -68,7 +68,7 @@ private Allocation(String description, long allocationUnits, @Nullable Allocatio this.allocationUnits = allocationUnits; this.parent = parent; - this.fractionOfRoot = parent == null ? 1.0 : parent.fractionOfRoot / parent.allocationUnits; + this.fractionOfRoot = (parent == null ? 1.0 : parent.fractionOfRoot) / allocationUnits; } /** @@ -113,10 +113,10 @@ public long getAllocationUnits() { } /** - * Gets how much of the root allocation this allocation accounts for. The entire root allocation - * is {@code 1.0}. + * Gets how much of the root allocation each of the allocation units of this allocation accounts + * for. The entire root allocation is {@code 1.0}. * - * @return the fraction of the root allocation this allocation accounts for + * @return the fraction of the root allocation this allocation's allocation units accounts for */ public double getFractionOfRoot() { return fractionOfRoot; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java new file mode 100644 index 0000000000..1ff1c10523 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Keeps track of the progress for {@link Allocation}s as well as their order in which they appear. + * + *

    This implementation is thread-safe. + */ +class AllocationCompletionTracker { + + /** + * Holds the progress units completed along with a creation order. This is used as the value of + * the {@link #completionMap}. + */ + private static class InsertionOrderUnits implements Comparable { + + /** Monotonically-increasing source for {@link #index}. */ + private static final AtomicInteger currentIndex = new AtomicInteger(); + + /** The creation order that monotonically increases. */ + private final int index = currentIndex.getAndIncrement(); + + /** + * Progress units completed. This can be shared across multiple threads and should be updated + * atomically. + */ + private final AtomicLong units = new AtomicLong(); + + /** The {@link Allocation} this is for. */ + private final Allocation allocation; + + private InsertionOrderUnits(Allocation allocation) { + this.allocation = allocation; + } + + @Override + public int compareTo(InsertionOrderUnits otherInsertionOrderUnits) { + return index - otherInsertionOrderUnits.index; + } + } + + /** + * Maps from {@link Allocation} to the number of progress units completed in that {@link + * Allocation}, while keeping track of the insertion order of the {@link Allocation}s with {@link + * InsertionOrderUnits}. + */ + private final ConcurrentHashMap completionMap = + new ConcurrentHashMap<>(); + + /** + * Updates the progress for {@link Allocation} atomically relative to the {@code allocation}. + * + *

    For any {@link Allocation}, this method must have been called on all of its parents + * beforehand. + * + * @param allocation the {@link Allocation} to update progress for + * @param units the units of progress + * @return {@code true} if the map was updated; {@code false} if {@code allocation} was already + * present. Note that this may return {@code true} even if the map was not updated if called + * concurrently, but never {@code false} if the map was updated. + */ + boolean updateProgress(Allocation allocation, long units) { + if (units == 0L) { + // Puts the allocation in the map if not present, with progress initialized to 0. + boolean alreadyPresent = completionMap.containsKey(allocation); + completionMap.computeIfAbsent(allocation, InsertionOrderUnits::new); + return !alreadyPresent; + } + + completionMap.compute( + allocation, + (ignored, insertionOrderUnits) -> { + if (insertionOrderUnits == null) { + insertionOrderUnits = new InsertionOrderUnits(allocation); + } + + updateInsertionOrderUnits(insertionOrderUnits, units); + + return insertionOrderUnits; + }); + return true; + } + + /** + * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link + * Allocation}s were encountered. This can be used to display, for example, currently executing + * tasks. The order helps to keep the displayed tasks in a deterministic order (new subtasks + * appear below older ones) and not jumbled together in some random order. + * + * @return a list of unfinished {@link Allocation}s + */ + List getUnfinishedAllocations() { + Queue unfinishedInsertionOrderUnits = new PriorityQueue<>(); + + for (InsertionOrderUnits insertionOrderUnits : completionMap.values()) { + if (insertionOrderUnits.units.get() < insertionOrderUnits.allocation.getAllocationUnits()) { + unfinishedInsertionOrderUnits.add(insertionOrderUnits); + } + } + + List unfinishedAllocations = new ArrayList<>(); + while (!unfinishedInsertionOrderUnits.isEmpty()) { + unfinishedAllocations.add(unfinishedInsertionOrderUnits.remove().allocation); + } + return unfinishedAllocations; + } + + /** + * Helper method for {@link #updateProgress(Allocation, long)}. Adds units to {@code + * insertionOrderUnits}. Updates {@link InsertionOrderUnits} for parent {@link Allocation}s if + * {@code insertionOrderUnits.units} reached completion (equals {@code + * allocation.getAllocationUnits()}. This method is not thread-safe for the {@code + * insertionOrderUnits} and should be called atomically relative to the {@code + * insertionOrderUnits}. + * + * @param insertionOrderUnits the {@link InsertionOrderUnits} to update progress for + * @param units the units of progress + */ + private void updateInsertionOrderUnits(InsertionOrderUnits insertionOrderUnits, long units) { + Allocation allocation = insertionOrderUnits.allocation; + + long newUnits = insertionOrderUnits.units.addAndGet(units); + if (newUnits > allocation.getAllocationUnits()) { + throw new IllegalStateException( + "Progress exceeds max for '" + + allocation.getDescription() + + "': " + + newUnits + + " > " + + allocation.getAllocationUnits()); + } + + // Updates the parent allocations if this allocation completed. + if (newUnits == allocation.getAllocationUnits()) { + allocation + .getParent() + .ifPresent( + parentAllocation -> + updateInsertionOrderUnits( + Preconditions.checkNotNull(completionMap.get(parentAllocation)), 1L)); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java new file mode 100644 index 0000000000..06ac50a771 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import java.util.List; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.function.Consumer; + +/** + * Handles {@link ProgressEvent}s by accumulating an overall progress and keeping track of which + * {@link Allocation}s are complete. + * + *

    This implementation is thread-safe. + */ +class ProgressEventHandler implements Consumer { + + /** Keeps track of the progress for each {@link Allocation} encountered. */ + private final AllocationCompletionTracker completionTracker = new AllocationCompletionTracker(); + + /** Accumulates an overall progress, with {@code 1.0} indicating full completion. */ + private final DoubleAdder progress = new DoubleAdder(); + + /** + * A callback to notify that {@link #progress} or {@link #completionTracker} could have changed. + * Note that every change will be reported (though multiple could be reported together), and there + * could be false positives. + */ + private final Runnable updateNotifier; + + ProgressEventHandler(Runnable updateNotifier) { + this.updateNotifier = updateNotifier; + } + + @Override + public void accept(ProgressEvent progressEvent) { + Allocation allocation = progressEvent.getAllocation(); + long progressUnits = progressEvent.getUnits(); + double allocationFraction = allocation.getFractionOfRoot(); + + if (progressUnits != 0) { + progress.add(progressUnits * allocationFraction); + } + + if (completionTracker.updateProgress(allocation, progressUnits)) { + // Note: Could produce false positives. + updateNotifier.run(); + } + } + + /** + * Gets the overall progress, with {@code 1.0} meaning fully complete. + * + * @return the overall progress + */ + double getProgress() { + return progress.sum(); + } + + /** + * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link + * Allocation}s were encountered. + * + * @return a list of unfinished {@link Allocation}s + */ + // TODO: Change this to do every time update notifier is called, so this is not called many times + // per update. + List getUnfinishedAllocations() { + return completionTracker.getUnfinishedAllocations(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java index ed99bd9aa2..4c5452f9c5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -63,10 +63,10 @@ private static EventDispatcher makeEventDispatcher( public void testAccumulateProgress() { Consumer progressEventConsumer = progressEvent -> { - Allocation allocation = progressEvent.getAllocation(); + double fractionOfRoot = progressEvent.getAllocation().getFractionOfRoot(); long units = progressEvent.getUnits(); - progress += units * allocation.getFractionOfRoot() / allocation.getAllocationUnits(); + progress += units * fractionOfRoot; }; EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java new file mode 100644 index 0000000000..a9b03c1260 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link AllocationCompletionTracker}. */ +public class AllocationCompletionTrackerTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("root", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("child1", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("child1Child", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("child2", 200); + + private AllocationTree() {} + } + + @Test + public void testGetUnfinishedAllocations_singleThread() { + AllocationCompletionTracker allocationCompletionTracker = new AllocationCompletionTracker(); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.root, 0L)); + Assert.assertEquals( + Collections.singletonList(AllocationTree.root), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 0L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 50L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 50L)); + Assert.assertEquals( + Collections.singletonList(AllocationTree.root), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child2, 100L)); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertTrue(allocationCompletionTracker.updateProgress(AllocationTree.child2, 100L)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + + Assert.assertFalse(allocationCompletionTracker.updateProgress(AllocationTree.child2, 0L)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + + try { + allocationCompletionTracker.updateProgress(AllocationTree.child1, 1L); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals("Progress exceeds max for 'child1': 2 > 1", ex.getMessage()); + } + } + + @Test + public void testGetUnfinishedAllocations_multipleThreads() + throws InterruptedException, ExecutionException, IOException, TimeoutException { + try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { + AllocationCompletionTracker allocationCompletionTracker = new AllocationCompletionTracker(); + + // Adds root, child1, and child1Child. + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.root, 0L))); + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L))); + Assert.assertEquals( + true, + multithreadedExecutor.invoke( + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 0L))); + Assert.assertEquals( + Arrays.asList(AllocationTree.root, AllocationTree.child1, AllocationTree.child1Child), + allocationCompletionTracker.getUnfinishedAllocations()); + + // Adds 50 to child1Child and 100 to child2. + List> callables = new ArrayList<>(150); + callables.addAll( + Collections.nCopies( + 50, + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1Child, 1L))); + callables.addAll( + Collections.nCopies( + 100, () -> allocationCompletionTracker.updateProgress(AllocationTree.child2, 1L))); + + Assert.assertEquals( + Collections.nCopies(150, true), multithreadedExecutor.invokeAll(callables)); + Assert.assertEquals( + Arrays.asList( + AllocationTree.root, + AllocationTree.child1, + AllocationTree.child1Child, + AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + // 0 progress doesn't do anything. + Assert.assertEquals( + Collections.nCopies(100, false), + multithreadedExecutor.invokeAll( + Collections.nCopies( + 100, + () -> allocationCompletionTracker.updateProgress(AllocationTree.child1, 0L)))); + Assert.assertEquals( + Arrays.asList( + AllocationTree.root, + AllocationTree.child1, + AllocationTree.child1Child, + AllocationTree.child2), + allocationCompletionTracker.getUnfinishedAllocations()); + + // Adds 50 to child1Child and 100 to child2 to finish it up. + Assert.assertEquals( + Collections.nCopies(150, true), multithreadedExecutor.invokeAll(callables)); + Assert.assertEquals( + Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java index c251016807..28509c573f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java @@ -33,7 +33,7 @@ public void testSmoke_linear() { Assert.assertEquals("node2", node2.getDescription()); Assert.assertEquals(3, node2.getAllocationUnits()); - Assert.assertEquals(1.0 / 2, node2.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 2 / 3, node2.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); Assert.assertTrue(node2.getParent().isPresent()); Assert.assertEquals(node1, node2.getParent().get()); @@ -41,7 +41,7 @@ public void testSmoke_linear() { Assert.assertEquals(2, node1.getAllocationUnits()); Assert.assertTrue(node1.getParent().isPresent()); Assert.assertEquals(root, node1.getParent().get()); - Assert.assertEquals(1.0, node1.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 2, node1.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); Assert.assertEquals("root", root.getDescription()); Assert.assertEquals(1, root.getAllocationUnits()); @@ -59,14 +59,14 @@ public void testFractionOfRoot_tree_partial() { Allocation rightRight = right.newChild("ignored", 100); Allocation rightRightDown = rightRight.newChild("ignored", 200); - Assert.assertEquals(1.0, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); - Assert.assertEquals(1.0 / 10, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); - Assert.assertEquals(1.0 / 10, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); - Assert.assertEquals(1.0 / 10 / 2, leftDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); - Assert.assertEquals(1.0 / 10 / 4, rightLeft.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); - Assert.assertEquals(1.0 / 10 / 4, rightRight.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 2, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 2 / 20, leftDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4 / 20, rightLeft.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0 / 10 / 4 / 100, rightRight.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); Assert.assertEquals( - 1.0 / 10 / 4 / 100, rightRightDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + 1.0 / 10 / 4 / 100 / 200, rightRightDown.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); } @Test @@ -84,10 +84,10 @@ public void testFractionOfRoot_tree_complete() { // Checks that the leaf allocations add up to a full 1.0. double total = - leftLeftDown.getFractionOfRoot() - + leftMiddle.getFractionOfRoot() - + leftRight.getFractionOfRoot() - + rightDown.getFractionOfRoot(); + leftLeftDown.getFractionOfRoot() * leftLeftDown.getAllocationUnits() + + leftMiddle.getFractionOfRoot() * leftMiddle.getAllocationUnits() + + leftRight.getFractionOfRoot() * leftRight.getAllocationUnits() + + rightDown.getFractionOfRoot() * rightDown.getAllocationUnits(); Assert.assertEquals(1.0, total, DOUBLE_ERROR_MARGIN); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java new file mode 100644 index 0000000000..27d25af9be --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Assert; + +/** Testing infrastructure for running code across multiple threads. */ +class MultithreadedExecutor implements Closeable { + + private static final Duration MULTITHREADED_TEST_TIMEOUT = Duration.ofSeconds(1); + private static final int THREAD_COUNT = 20; + + private final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT); + + E invoke(Callable callable) + throws ExecutionException, InterruptedException, TimeoutException { + List returnValue = invokeAll(Collections.singletonList(callable)); + return returnValue.get(0); + } + + List invokeAll(List> callables) + throws InterruptedException, ExecutionException, TimeoutException { + List> futures = + executorService.invokeAll( + callables, MULTITHREADED_TEST_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + List returnValues = new ArrayList<>(); + for (Future future : futures) { + Assert.assertTrue(future.isDone()); + returnValues.add(future.get()); + } + + return returnValues; + } + + @Override + public void close() throws IOException { + executorService.shutdown(); + try { + executorService.awaitTermination(MULTITHREADED_TEST_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + } catch (InterruptedException ex) { + throw new IOException(ex); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java new file mode 100644 index 0000000000..97a1dc9abc --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ProgressEventHandler}. */ +public class ProgressEventHandlerTest { + + /** {@link Allocation} tree for testing. */ + private static class AllocationTree { + + /** The root node. */ + private static final Allocation root = Allocation.newRoot("root", 2); + + /** First child of the root node. */ + private static final Allocation child1 = root.newChild("child1", 1); + /** Child of the first child of the root node. */ + private static final Allocation child1Child = child1.newChild("child1Child", 100); + + /** Second child of the root node. */ + private static final Allocation child2 = root.newChild("child2", 200); + + private AllocationTree() {} + } + + private static final double DOUBLE_ERROR_MARGIN = 1e-10; + + @Test + public void testAccept() + throws ExecutionException, InterruptedException, IOException, TimeoutException { + try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { + ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + EventDispatcher eventDispatcher = + new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler)); + + // Adds root, child1, and child1Child. + multithreadedExecutor.invoke( + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.root, 0L)); + return null; + }); + multithreadedExecutor.invoke( + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + return null; + }); + multithreadedExecutor.invoke( + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L)); + return null; + }); + Assert.assertEquals(0.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + + // Adds 50 to child1Child and 100 to child2. + List> callables = new ArrayList<>(150); + callables.addAll( + Collections.nCopies( + 50, + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L)); + return null; + })); + callables.addAll( + Collections.nCopies( + 100, + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 1L)); + return null; + })); + + multithreadedExecutor.invokeAll(callables); + Assert.assertEquals( + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, + progressEventHandler.getProgress(), + DOUBLE_ERROR_MARGIN); + + // 0 progress doesn't do anything. + multithreadedExecutor.invokeAll( + Collections.nCopies( + 100, + () -> { + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + return null; + })); + Assert.assertEquals( + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, + progressEventHandler.getProgress(), + DOUBLE_ERROR_MARGIN); + + // Adds 50 to child1Child and 100 to child2 to finish it up. + multithreadedExecutor.invokeAll(callables); + Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + } + } +} From a31676c0f4aeac1f8e268491e76258077017fae0 Mon Sep 17 00:00:00 2001 From: Jordan Moore Date: Fri, 7 Dec 2018 09:54:30 -0600 Subject: [PATCH 0375/2020] Example: Dropwizard. Initial Commit (#1290) --- examples/README.md | 4 + examples/dropwizard/.gitignore | 5 + .../.mvn/wrapper/MavenWrapperDownloader.java | 110 +++++++ .../.mvn/wrapper/maven-wrapper.properties | 1 + examples/dropwizard/README.md | 40 +++ examples/dropwizard/mvnw | 286 ++++++++++++++++++ examples/dropwizard/mvnw.cmd | 161 ++++++++++ examples/dropwizard/pom.xml | 131 ++++++++ .../java/example/JibExampleApplication.java | 56 ++++ .../java/example/JibExampleConfiguration.java | 36 +++ .../src/main/java/example/api/Saying.java | 44 +++ .../config/HelloWorldConfiguration.java | 41 +++ .../example/health/TemplateHealthCheck.java | 36 +++ .../example/resources/HelloWorldResource.java | 60 ++++ .../dropwizard/src/main/resources/banner.txt | 6 + .../src/main/resources/dropwizard.yml | 18 ++ 16 files changed, 1035 insertions(+) create mode 100644 examples/dropwizard/.gitignore create mode 100755 examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java create mode 100755 examples/dropwizard/.mvn/wrapper/maven-wrapper.properties create mode 100644 examples/dropwizard/README.md create mode 100755 examples/dropwizard/mvnw create mode 100755 examples/dropwizard/mvnw.cmd create mode 100644 examples/dropwizard/pom.xml create mode 100644 examples/dropwizard/src/main/java/example/JibExampleApplication.java create mode 100644 examples/dropwizard/src/main/java/example/JibExampleConfiguration.java create mode 100644 examples/dropwizard/src/main/java/example/api/Saying.java create mode 100644 examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java create mode 100644 examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java create mode 100644 examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java create mode 100644 examples/dropwizard/src/main/resources/banner.txt create mode 100644 examples/dropwizard/src/main/resources/dropwizard.yml diff --git a/examples/README.md b/examples/README.md index 4604897693..74b3d24bd5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,10 @@ You can find usage by Gradle or Maven with SpringBoot in [spring-boot](spring-bo See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). +### Dropwizard example + +See [dropwizard](dropwizard) for containerizing [Dropwizard](https://dropwizard.io) applications. + ### Micronaut example See [micronaut](micronaut) for containerizing a [Micronaut framework](https://micronaut.io/) Groovy/Java application. diff --git a/examples/dropwizard/.gitignore b/examples/dropwizard/.gitignore new file mode 100644 index 0000000000..63648064d7 --- /dev/null +++ b/examples/dropwizard/.gitignore @@ -0,0 +1,5 @@ +target/ + +.mvn/wrapper/maven-wrapper.jar + +*.iml \ No newline at end of file diff --git a/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java b/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 0000000000..fa4f7b499f --- /dev/null +++ b/examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties b/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 0000000000..b6e6781222 --- /dev/null +++ b/examples/dropwizard/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip \ No newline at end of file diff --git a/examples/dropwizard/README.md b/examples/dropwizard/README.md new file mode 100644 index 0000000000..1fb539899b --- /dev/null +++ b/examples/dropwizard/README.md @@ -0,0 +1,40 @@ +# Containerize a [Dropwizard](https://dropwizard.io) application with Jib + +## How to start the Dropwizard application + +1. Run `./mvnw clean package -Djib.to.image=dropwizard-jib-example` to build your container +1. Start the application + - **With Docker**: `docker run --rm -p 8080:8080 dropwizard-jib-example:1` + - **Without Docker**: `./mvnw exec:java` +1. Check that your application is running at http://localhost:8080/hello-world + +## Health Check + +See your application's health at http://localhost:8080/admin/healthcheck + +## Extras + +FreeMaker templating is setup for [`dropwizard.yml`](src/main/resources/dropwizard.yml) through [`tkrille/dropwizard-template-config`](https://github.com/tkrille/dropwizard-template-config); this allows one to heavily customize the properties file via the container environment with FTL conditional checks and for loops, for example. + +## How this example was generated + +Starter Maven template generated with [`dropwizard-archetypes`](https://github.com/dropwizard/dropwizard/tree/master/dropwizard-archetypes) + +```sh +mvn archetype:generate \ + -DarchetypeGroupId=io.dropwizard.archetypes \ + -DarchetypeArtifactId=example \ + -DarchetypeVersion=[REPLACE ME WITH A VALID DROPWIZARD VERSION] +``` + +Ref. [Dropwizard - Getting Started, Setting up With Maven](https://www.dropwizard.io/1.3.5/docs/getting-started.html#setting-up-using-maven) + +The remainder of the archetype code was filled-in following the above guide. + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [more about Dropwizard](https://dropwizard.io). + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/dropwizard)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/dropwizard/mvnw b/examples/dropwizard/mvnw new file mode 100755 index 0000000000..5551fde8e7 --- /dev/null +++ b/examples/dropwizard/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/dropwizard/mvnw.cmd b/examples/dropwizard/mvnw.cmd new file mode 100755 index 0000000000..e5cfb0ae9e --- /dev/null +++ b/examples/dropwizard/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml new file mode 100644 index 0000000000..e0f865ff2f --- /dev/null +++ b/examples/dropwizard/pom.xml @@ -0,0 +1,131 @@ + + + + 4.0.0 + + example + dropwizard + 1 + jar + + Dropwizard Jib Example + + + UTF-8 + UTF-8 + + 1.8 + 1.8 + + example.JibExampleApplication + + 1.3.7 + dropwizard.yml + 1.5.0 + + gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib + /app + 0.10.1 + + + + + + io.dropwizard + dropwizard-bom + ${dropwizard.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + + de.thomaskrille + dropwizard-template-config + ${dropwizard-template-config.version} + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${jib.to.image} + + ${project.version} + + + + ${mainClass} + + server + ${jib.container.appRoot}/resources/${dropwizard.server.config} + + + 8080 + + + + + -server + -Djava.awt.headless=true + -XX:+UnlockExperimentalVMOptions + -XX:+UseCGroupMemoryLimitForHeap + -XX:InitialRAMFraction=2 + -XX:MinRAMFraction=2 + -XX:MaxRAMFraction=2 + -XX:+UseG1GC + -XX:MaxGCPauseMillis=100 + -XX:+UseStringDeduplication + + + + + + dockerBuild + + dockerBuild + + package + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + java + + java + + + + + java + ${mainClass} + + server + ${project.build.sourceDirectory}/../resources/${dropwizard.server.config} + + + + + + diff --git a/examples/dropwizard/src/main/java/example/JibExampleApplication.java b/examples/dropwizard/src/main/java/example/JibExampleApplication.java new file mode 100644 index 0000000000..edc75a979f --- /dev/null +++ b/examples/dropwizard/src/main/java/example/JibExampleApplication.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import de.thomaskrille.dropwizard_template_config.TemplateConfigBundle; +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import example.health.TemplateHealthCheck; +import example.resources.HelloWorldResource; + +/** + * + * Refer Dropwizard User Manual + */ +public class JibExampleApplication extends Application { + + public static void main(final String[] args) throws Exception { + new JibExampleApplication().run(args); + } + + @Override + public String getName() { + return "Dropwizard Jib Example"; + } + + @Override + public void initialize(final Bootstrap bootstrap) { + // Enable FreeMarker config templates + bootstrap.addBundle(new TemplateConfigBundle()); + } + + @Override + public void run(final JibExampleConfiguration configuration, final Environment environment) { + final TemplateHealthCheck healthCheck = + new TemplateHealthCheck(configuration.getHelloConfiguration().getTemplate()); + environment.healthChecks().register("template", healthCheck); + + environment.jersey().register(HelloWorldResource.from(configuration)); + } +} diff --git a/examples/dropwizard/src/main/java/example/JibExampleConfiguration.java b/examples/dropwizard/src/main/java/example/JibExampleConfiguration.java new file mode 100644 index 0000000000..65ecc6b295 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/JibExampleConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.Configuration; + +import example.config.HelloWorldConfiguration; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@SuppressWarnings("unused") +public class JibExampleConfiguration extends Configuration { + @Valid @NotNull @JsonProperty + private HelloWorldConfiguration hello; + + @JsonProperty("hello") + public HelloWorldConfiguration getHelloConfiguration() { + return hello; + } +} diff --git a/examples/dropwizard/src/main/java/example/api/Saying.java b/examples/dropwizard/src/main/java/example/api/Saying.java new file mode 100644 index 0000000000..86aae674d1 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/api/Saying.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.api; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Saying { + private long id; + + private String content; + + public Saying() { + // Jackson deserialization + } + + public Saying(long id, String content) { + this.id = id; + this.content = content; + } + + @JsonProperty + public long getId() { + return id; + } + + @JsonProperty + public String getContent() { + return content; + } +} diff --git a/examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java b/examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java new file mode 100644 index 0000000000..7692b796b4 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.config; + +import io.dropwizard.Configuration; + +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("unused") +public class HelloWorldConfiguration extends Configuration { + + @NotEmpty private String template; + + @NotEmpty private String defaultName = "Stranger"; + + @JsonProperty + public String getTemplate() { + return template; + } + + @JsonProperty + public String getDefaultName() { + return defaultName; + } +} diff --git a/examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java b/examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java new file mode 100644 index 0000000000..15220d9bf1 --- /dev/null +++ b/examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.health; + +import com.codahale.metrics.health.HealthCheck; + +public class TemplateHealthCheck extends HealthCheck { + private final String template; + + public TemplateHealthCheck(String template) { + this.template = template; + } + + @Override + protected Result check() throws Exception { + final String saying = String.format(template, "TEST"); + if (!saying.contains("TEST")) { + return Result.unhealthy("template doesn't include a name"); + } + return Result.healthy(); + } +} diff --git a/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java new file mode 100644 index 0000000000..a754cb9caf --- /dev/null +++ b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.resources; + +import com.codahale.metrics.annotation.Timed; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import example.JibExampleConfiguration; +import example.api.Saying; +import example.config.HelloWorldConfiguration; + +@Path("/hello-world") +@Produces(MediaType.APPLICATION_JSON) +public class HelloWorldResource { + + private final String template; + private final String defaultName; + private final AtomicLong counter; + + public HelloWorldResource(String template, String defaultName) { + this.template = template; + this.defaultName = defaultName; + this.counter = new AtomicLong(); + } + + public static HelloWorldResource from(JibExampleConfiguration conf) { + final HelloWorldConfiguration helloConfiguration = conf.getHelloConfiguration(); + return new HelloWorldResource( + helloConfiguration.getTemplate(), helloConfiguration.getDefaultName()); + } + + @GET + @Timed + public Saying sayHello(@QueryParam("name") Optional name) { + final String value = String.format(template, name.orElse(defaultName)); + return new Saying(counter.incrementAndGet(), value); + } +} diff --git a/examples/dropwizard/src/main/resources/banner.txt b/examples/dropwizard/src/main/resources/banner.txt new file mode 100644 index 0000000000..57c331f963 --- /dev/null +++ b/examples/dropwizard/src/main/resources/banner.txt @@ -0,0 +1,6 @@ +================================================================================ + + Dropwizard Jib Example + +================================================================================ + diff --git a/examples/dropwizard/src/main/resources/dropwizard.yml b/examples/dropwizard/src/main/resources/dropwizard.yml new file mode 100644 index 0000000000..db13ff16a4 --- /dev/null +++ b/examples/dropwizard/src/main/resources/dropwizard.yml @@ -0,0 +1,18 @@ +<#-- FreeMarker Enabled - https://github.com/tkrille/dropwizard-template-config --> +hello: + template: ${DW_TEMPLATE!'Hello, %s!'} + defaultName: ${DW_DEFAULT_NAME!'Stranger'} + +server: + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 + +logging: + level: ${LOG_LEVEL!'INFO'} + loggers: + <#-- Log level for a specific package --> + example: DEBUG From dde74321174c27bdc645734910d9e7ddf6ee2b58 Mon Sep 17 00:00:00 2001 From: Jordan Moore Date: Fri, 7 Dec 2018 10:52:11 -0600 Subject: [PATCH 0376/2020] [example/ktor] Initial Commit (#1291) --- examples/README.md | 4 + examples/ktor/.gitignore | 6 + examples/ktor/README.md | 32 ++++ examples/ktor/build.gradle.kts | 65 +++++++ examples/ktor/gradle.properties | 1 + .../ktor/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55741 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/ktor/gradlew | 172 ++++++++++++++++++ examples/ktor/gradlew.bat | 84 +++++++++ examples/ktor/settings.gradle.kts | 1 + .../ktor/src/main/kotlin/example/ktor/App.kt | 161 ++++++++++++++++ .../ktor/src/main/resources/application.conf | 13 ++ examples/ktor/src/main/resources/logback.xml | 15 ++ 13 files changed, 559 insertions(+) create mode 100644 examples/ktor/.gitignore create mode 100644 examples/ktor/README.md create mode 100644 examples/ktor/build.gradle.kts create mode 100644 examples/ktor/gradle.properties create mode 100644 examples/ktor/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/ktor/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/ktor/gradlew create mode 100644 examples/ktor/gradlew.bat create mode 100644 examples/ktor/settings.gradle.kts create mode 100644 examples/ktor/src/main/kotlin/example/ktor/App.kt create mode 100644 examples/ktor/src/main/resources/application.conf create mode 100644 examples/ktor/src/main/resources/logback.xml diff --git a/examples/README.md b/examples/README.md index 74b3d24bd5..6cce6ac8c1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,10 @@ You can find usage by Gradle or Maven with SpringBoot in [spring-boot](spring-bo See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). +## Ktor example + +See [ktor](ktor) for containerizing a [Ktor](https://ktor.io) Kotlin Application using the Kotlin Gradle DSL. + ### Dropwizard example See [dropwizard](dropwizard) for containerizing [Dropwizard](https://dropwizard.io) applications. diff --git a/examples/ktor/.gitignore b/examples/ktor/.gitignore new file mode 100644 index 0000000000..64756acc81 --- /dev/null +++ b/examples/ktor/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +out \ No newline at end of file diff --git a/examples/ktor/README.md b/examples/ktor/README.md new file mode 100644 index 0000000000..04470cc141 --- /dev/null +++ b/examples/ktor/README.md @@ -0,0 +1,32 @@ +# Containerize a [Ktor] application with Jib + +This is an example of how to easily build a Docker image for a [Ktor] application with Jib. + +```shell +./gradlew jibDockerBuild + +docker run --rm -p 8080:8080 ktor-jib-example:1 +``` + +The application can also be ran outside of Jib's build process via `./gradlew run` + +## Defined environment variables + +A few variables have been added in the code-base to show case some of the unique features of Ktor. + +- `KTOR_APP_ID` - The name of the application in the logs + - Type: String +- `KTOR_METRICS_ENABLED` - Exposes JMX Metrics through the application port (`8080`) + - Type: Boolean (default: `false`) +- `KTOR_ROUTE_TRACING` - Enables verbose logging of route matches for debugging complex/nested routing tables + - Type: Boolean + +## More information + +Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +Learn [Ktor]. + + [Ktor]: https://ktor.io + +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/ktor)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts new file mode 100644 index 0000000000..f780f94389 --- /dev/null +++ b/examples/ktor/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + application + kotlin("jvm") version "1.3.10" + id("com.google.cloud.tools.jib") version "0.10.1" +} + +group = "example" +version = "1" + +val ktor_version by extra("1.0.0") +val logback_version by extra("1.2.3") + +val main_class by extra("io.ktor.server.netty.EngineMain") + +application { + mainClassName = main_class + + applicationDefaultJvmArgs = listOf( + "-server", + "-Djava.awt.headless=true", + "-Xms128m", + "-Xmx256m", + "-XX:+UseG1GC", + "-XX:MaxGCPauseMillis=100" + ) +} + +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + implementation(kotlin("stdlib")) + + implementation("ch.qos.logback:logback-classic:$logback_version") + + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("io.ktor:ktor-metrics:$ktor_version") + implementation("io.ktor:ktor-html-builder:$ktor_version") +} + +jib { + container { + ports = listOf("8080") + mainClass = main_class + + // good defauls intended for containers + jvmFlags = listOf( + "-server", + "-Djava.awt.headless=true", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+UseCGroupMemoryLimitForHeap", + "-XX:InitialRAMFraction=2", + "-XX:MinRAMFraction=2", + "-XX:MaxRAMFraction=2", + "-XX:+UseG1GC", + "-XX:MaxGCPauseMillis=100", + "-XX:+UseStringDeduplication" + ) + } +} + +repositories { + jcenter() + mavenCentral() +} diff --git a/examples/ktor/gradle.properties b/examples/ktor/gradle.properties new file mode 100644 index 0000000000..29e08e8ca8 --- /dev/null +++ b/examples/ktor/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.jar b/examples/ktor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..457aad0d98108420a977756b7145c93c8910b076 GIT binary patch literal 55741 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 O(FVj@M23F*_WuFL)$*MH literal 0 HcmV?d00001 diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.properties b/examples/ktor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..75b8c7c8c6 --- /dev/null +++ b/examples/ktor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/ktor/gradlew b/examples/ktor/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/examples/ktor/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/ktor/gradlew.bat b/examples/ktor/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/examples/ktor/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/ktor/settings.gradle.kts b/examples/ktor/settings.gradle.kts new file mode 100644 index 0000000000..c89c4b6286 --- /dev/null +++ b/examples/ktor/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ktor-jib-example" diff --git a/examples/ktor/src/main/kotlin/example/ktor/App.kt b/examples/ktor/src/main/kotlin/example/ktor/App.kt new file mode 100644 index 0000000000..2cf7b70e6b --- /dev/null +++ b/examples/ktor/src/main/kotlin/example/ktor/App.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example.ktor + +import com.codahale.metrics.JmxReporter +import io.ktor.application.* +import io.ktor.features.CallLogging +import io.ktor.features.DefaultHeaders +import io.ktor.features.StatusPages +import io.ktor.html.respondHtml +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.metrics.Metrics +import io.ktor.request.receiveOrNull +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.route +import io.ktor.routing.routing +import kotlinx.html.* +import java.util.concurrent.TimeUnit + +/** + * Ktor Example Application + * + * See documentation at https://ktor.io/servers/index.html + */ +fun Application.main() { + init() + router() +} + +// Variable for maintaining health-check status +var healthy = true + +/** + * For enabling Metrics - https://ktor.io/servers/features/metrics.html + */ +val metricsEnabled = (true == System.getenv("KTOR_METRICS_ENABLED")?.toBoolean()) + +/** + * For enabling route tracing - https://ktor.io/servers/features/routing.html#tracing + */ +val routeTracing = (true == System.getenv("KTOR_ROUTE_TRACING")?.toBoolean()) + +/** + * A method for defining the routes - https://ktor.io/servers/structure.html#extracting-routes + */ +private fun Application.router() { + log.info("Route tracing enabled? $routeTracing") + routing { + // If enabled, will print out a tree of the routes that were matched while trying to process the URI + if (routeTracing) { + trace { application.log.trace(it.buildText()) } + } + + get("/") { + call.respondText("Hello World!") + } + + get("/info") { + // The `respondHtml` extension method is available at the `ktor-html-builder` artifact. + // It provides a DSL for building HTML to a Writer, potentially in a chunked way. + // More information about this DSL: https://ktor.io/servers/features/templates/html-dsl.html + call.respondHtml { + head { + title { +"Ktor: Jib" } + } + body { + val runtime = Runtime.getRuntime() + h1 { +"Hello from Ktor sample application built with Jib" } + val runtimeString = "Runtime.getRuntime()" + p { +"$runtimeString.availableProcessors(): ${runtime.availableProcessors()}" } + p { +"$runtimeString.freeMemory(): ${runtime.freeMemory()}" } + p { +"$runtimeString.totalMemory(): ${runtime.totalMemory()}" } + p { +"$runtimeString.maxMemory(): ${runtime.maxMemory()}" } + p { +"System.getProperty(\"user.name\"): ${System.getProperty("user.name")}" } + } + } + } + + route("/healthz") { + // Some simple toy health check example + post("/") { + when(call.receiveOrNull()) { + "infect" -> { + healthy = false + call.respond(HttpStatusCode.Accepted) + } + "heal" -> { + healthy = true + call.respond(HttpStatusCode.Accepted) + } + else -> { + // Showcase the StatusPage installation feature + throw IllegalAccessException("POST /healthz only accepts 'infect' or 'heal'") + } + } + } + get("/") { + var txt = "ok" + var status = HttpStatusCode.OK + if (!healthy) { + txt = "failure" + status = HttpStatusCode.ServiceUnavailable + } + call.respondText(txt.toUpperCase(), status = status) + } + } + } +} + +/** + * A method for installing features - https://ktor.io/servers/features.html#installing + */ +private fun Application.init() { + // This adds automatically Date and Server headers to each response, and would allow you to configure + // additional headers served to each response. + install(DefaultHeaders) + + // This allows handling exceptions and status codes in a specific way. + install(StatusPages) { + // For all thrown Exceptions, log, and return the message of it as text + exception { e -> + val message = e.localizedMessage + application.log.error(message) + call.respondText(message, + ContentType.Text.Plain, HttpStatusCode.InternalServerError) + } + } + + // This uses use the logger to log every call (request/response) + install(CallLogging) + + // Conditionally enable the Metrics API + log.info("Metrics Registry enabled? $metricsEnabled") + if (metricsEnabled) { + install(Metrics) { + JmxReporter.forRegistry(registry) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build() + .start() + } + } +} \ No newline at end of file diff --git a/examples/ktor/src/main/resources/application.conf b/examples/ktor/src/main/resources/application.conf new file mode 100644 index 0000000000..42b1a9a79d --- /dev/null +++ b/examples/ktor/src/main/resources/application.conf @@ -0,0 +1,13 @@ +# Ktor configuration - https://ktor.io/servers/configuration.html +ktor { + deployment { + port = 8080 + } + + application { + # Defining values twice is way to define a default with substitutable value + id = "Ktor Jib Example" + id = ${?KTOR_APP_ID} + modules = [ example.ktor.AppKt.main ] + } +} \ No newline at end of file diff --git a/examples/ktor/src/main/resources/logback.xml b/examples/ktor/src/main/resources/logback.xml new file mode 100644 index 0000000000..9e78155525 --- /dev/null +++ b/examples/ktor/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file From a7377b9227c06e1e209b5c1efc1c9e9839c53ba9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 7 Dec 2018 11:52:18 -0500 Subject: [PATCH 0377/2020] Add system properties for setting cache directories (#1306) * Add system properties for setting cache directories * Feedback * Shorten system property names * Naming * Update changelog --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../jib/gradle/GradleProjectProperties.java | 2 +- jib-maven-plugin/CHANGELOG.md | 2 ++ .../jib/maven/MavenProjectProperties.java | 2 +- .../common/PluginConfigurationProcessor.java | 29 +++++++++++++++++-- .../jib/plugins/common/ProjectProperties.java | 2 +- .../jib/plugins/common/PropertyNames.java | 2 ++ .../PluginConfigurationProcessorTest.java | 25 ++++++++++++++-- 8 files changed, 58 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 294336f0fd..fdbd15da8e 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 954c629868..7a9377338e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -144,7 +144,7 @@ public String getMainClassFromJar() { } @Override - public Path getCacheDirectory() { + public Path getDefaultCacheDirectory() { return project.getBuildDir().toPath().resolve(CACHE_DIRECTORY_NAME); } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 99f891bd4b..ff57f95806 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) + ### Changed - `gwt-app` packaging type now builds a WAR container. diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index d600c89ed8..e6f0122794 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -148,7 +148,7 @@ public String getMainClassFromJar() { } @Override - public Path getCacheDirectory() { + public Path getDefaultCacheDirectory() { return Paths.get(project.getBuild().getDirectory(), CACHE_DIRECTORY_NAME); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index e09834eb5d..bcb33fe677 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -40,6 +40,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Instant; import java.util.HashSet; import java.util.List; @@ -421,16 +422,38 @@ private static void configureContainerizer( .setToolName(projectProperties.getToolName()) .setEventHandlers(projectProperties.getEventHandlers()) .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) - .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY) - .setApplicationLayersCache(projectProperties.getCacheDirectory()); + .setBaseImageLayersCache( + getCheckedCacheDirectory( + PropertyNames.BASE_IMAGE_CACHE, Containerizer.DEFAULT_BASE_CACHE_DIRECTORY)) + .setApplicationLayersCache( + getCheckedCacheDirectory( + PropertyNames.APPLICATION_CACHE, projectProperties.getDefaultCacheDirectory())); rawConfiguration.getToTags().forEach(containerizer::withAdditionalTag); if (rawConfiguration.getUseOnlyProjectCache()) { - containerizer.setBaseImageLayersCache(projectProperties.getCacheDirectory()); + containerizer.setBaseImageLayersCache( + getCheckedCacheDirectory( + PropertyNames.BASE_IMAGE_CACHE, projectProperties.getDefaultCacheDirectory())); } } + /** + * Returns the value of a cache directory system property if it is set, otherwise returns {@code + * defaultPath}. + * + * @param property the name of the system property to check + * @param defaultPath the path to return if the system property isn't set + * @return the value of a cache directory system property if it is set, otherwise returns {@code + * defaultPath} + */ + private static Path getCheckedCacheDirectory(String property, Path defaultPath) { + if (System.getProperty(property) != null) { + return Paths.get(System.getProperty(property)); + } + return defaultPath; + } + private final JibContainerBuilder jibContainerBuilder; private final ImageReference baseImageReference; private final ImageReference targetImageReference; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 1c18e3ca9e..59570c6326 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -41,7 +41,7 @@ public interface ProjectProperties { JavaLayerConfigurations getJavaLayerConfigurations(); - Path getCacheDirectory(); + Path getDefaultCacheDirectory(); String getJarPluginName(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 2a4a42d657..8f4c8bda24 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -42,6 +42,8 @@ public class PropertyNames { public static final String CONTAINER_PORTS = "jib.container.ports"; public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; + public static final String BASE_IMAGE_CACHE = "jib.baseImageCache"; + public static final String APPLICATION_CACHE = "jib.applicationCache"; public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 92fbea5175..e5b8d50bc7 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -75,7 +75,7 @@ public void setUp() { Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getEventHandlers()) .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); - Mockito.when(projectProperties.getCacheDirectory()).thenReturn(Paths.get("cache")); + Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) @@ -103,10 +103,32 @@ public void testPluginConfigurationProcessor_defaults() Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verify(containerizer) + .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); + Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("cache")); + ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); } + @Test + public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() + throws InferredAuthRetrievalException, InvalidContainerVolumeException, + MainClassInferenceException, InvalidAppRootException, IOException, + InvalidWorkingDirectoryException, InvalidImageReferenceException { + System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); + System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); + + PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, projectProperties, containerizer, targetImageReference, false); + + Mockito.verify(containerizer).setBaseImageLayersCache(Paths.get("new/base/cache")); + Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("/new/application/cache")); + + System.clearProperty(PropertyNames.BASE_IMAGE_CACHE); + System.clearProperty(PropertyNames.APPLICATION_CACHE); + } + @Test public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, @@ -179,7 +201,6 @@ public void testEntrypoint_defaultWarPackaging() JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(logger); From 62b9d162f6590d962d7dc4f892b742391b70d609 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 7 Dec 2018 11:52:34 -0500 Subject: [PATCH 0378/2020] Minor updates to Dropwizard example (#1319) --- examples/dropwizard/README.md | 2 +- examples/dropwizard/pom.xml | 22 ++-------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/examples/dropwizard/README.md b/examples/dropwizard/README.md index 1fb539899b..c423cdc41d 100644 --- a/examples/dropwizard/README.md +++ b/examples/dropwizard/README.md @@ -2,7 +2,7 @@ ## How to start the Dropwizard application -1. Run `./mvnw clean package -Djib.to.image=dropwizard-jib-example` to build your container +1. Run `./mvnw clean package` to build your container 1. Start the application - **With Docker**: `docker run --rm -p 8080:8080 dropwizard-jib-example:1` - **Without Docker**: `./mvnw exec:java` diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index e0f865ff2f..cb2435cbdd 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -7,12 +7,10 @@ 4.0.0 example - dropwizard + dropwizard-jib-example 1 jar - Dropwizard Jib Example - UTF-8 UTF-8 @@ -20,13 +18,13 @@ 1.8 1.8 + example.JibExampleApplication 1.3.7 dropwizard.yml 1.5.0 - gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib /app 0.10.1 @@ -63,14 +61,7 @@ jib-maven-plugin ${jib-maven-plugin.version} - - ${jib.to.image} - - ${project.version} - - - ${mainClass} server ${jib.container.appRoot}/resources/${dropwizard.server.config} @@ -109,16 +100,7 @@ org.codehaus.mojo exec-maven-plugin 1.6.0 - - - java - - java - - - - java ${mainClass} server From 932085dea01e2a3d6f6ee60a43141f1a95d17c32 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 7 Dec 2018 14:36:01 -0500 Subject: [PATCH 0379/2020] Remove getInferredAuth from RawConfiguration / embed auth source in AuthProperties (#1314) --- .../tools/jib/gradle/AuthParameters.java | 26 ++++++++ .../tools/jib/gradle/BaseImageParameters.java | 2 +- .../tools/jib/gradle/BuildDockerTask.java | 6 +- .../tools/jib/gradle/BuildImageTask.java | 7 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 6 +- .../jib/gradle/GradleRawConfiguration.java | 25 -------- .../jib/gradle/TargetImageParameters.java | 2 +- .../gradle/GradleRawConfigurationTest.java | 9 ++- .../tools/jib/maven/BuildDockerMojo.java | 6 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 +-- .../cloud/tools/jib/maven/BuildTarMojo.java | 6 +- .../jib/maven/JibPluginConfiguration.java | 46 ++++++++++--- .../jib/maven/MavenRawConfiguration.java | 38 +---------- .../maven/MavenSettingsServerCredentials.java | 25 ++++++-- .../jib/maven/MavenRawConfigurationTest.java | 27 +++----- .../MavenSettingsServerCredentialsTest.java | 12 ++-- .../jib/plugins/common/AuthProperty.java | 6 ++ .../plugins/common/InferredAuthProvider.java | 25 ++++++++ .../InferredAuthRetrievalException.java | 6 +- .../common/PluginConfigurationProcessor.java | 60 +++++++++-------- .../jib/plugins/common/RawConfiguration.java | 13 ---- .../PluginConfigurationProcessorTest.java | 64 ++++++++----------- 22 files changed, 215 insertions(+), 210 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java index b2ebf76a1a..c110e15e6d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java @@ -18,7 +18,9 @@ import com.google.cloud.tools.jib.plugins.common.AuthProperty; import javax.annotation.Nullable; +import javax.inject.Inject; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; /** @@ -29,6 +31,12 @@ public class AuthParameters implements AuthProperty { @Nullable private String username; @Nullable private String password; + private final String source; + + @Inject + public AuthParameters(String source) { + this.source = source; + } @Input @Optional @@ -53,4 +61,22 @@ public String getPassword() { public void setPassword(String password) { this.password = password; } + + @Internal + @Override + public String getAuthDescriptor() { + return source; + } + + @Internal + @Override + public String getUsernameDescriptor() { + return getAuthDescriptor() + ".username"; + } + + @Internal + @Override + public String getPasswordDescriptor() { + return getAuthDescriptor() + ".password"; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java index 79570ff43e..c1afc93ec7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java @@ -35,7 +35,7 @@ public class BaseImageParameters { @Inject public BaseImageParameters(ObjectFactory objectFactory) { - auth = objectFactory.newInstance(AuthParameters.class); + auth = objectFactory.newInstance(AuthParameters.class, "from.auth"); } @Input diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index b438fd2aa0..a635ec90be 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -31,7 +31,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -106,6 +105,7 @@ public void buildDocker() try { AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + GradleProjectProperties projectProperties = GradleProjectProperties.getForProject( getProject(), @@ -113,14 +113,14 @@ public void buildDocker() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - rawConfiguration, + new GradleRawConfiguration(jibExtension), + ignored -> java.util.Optional.empty(), projectProperties, dockerClientParameters.getExecutable(), dockerClientParameters.getEnvironment(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 87d407f6e6..db7624c193 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -30,11 +30,11 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -89,7 +89,6 @@ public void buildImage() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( @@ -102,7 +101,9 @@ public void buildImage() PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - rawConfiguration, projectProperties); + new GradleRawConfiguration(jibExtension), + ignored -> Optional.empty(), + projectProperties); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index ce876c16bd..e8a0b8eb20 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -29,10 +29,10 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -111,7 +111,6 @@ public void buildTar() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - RawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); @@ -119,7 +118,8 @@ public void buildTar() Path tarOutputPath = getTargetPath(); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( - rawConfiguration, + new GradleRawConfiguration(jibExtension), + ignored -> Optional.empty(), projectProperties, tarOutputPath, gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 525db125b2..ecd1d42702 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -57,21 +57,6 @@ public AuthProperty getToAuth() { return jibExtension.getTo().getAuth(); } - @Override - public String getAuthDescriptor(String source) { - return source + ".auth"; - } - - @Override - public String getUsernameAuthDescriptor(String source) { - return getAuthDescriptor(source) + ".username"; - } - - @Override - public String getPasswordAuthDescriptor(String source) { - return getAuthDescriptor(source) + ".password"; - } - @Override public Optional getToCredHelper() { return Optional.ofNullable(jibExtension.getTo().getCredHelper()); @@ -152,16 +137,6 @@ public boolean getUseOnlyProjectCache() { return jibExtension.getUseOnlyProjectCache(); } - @Override - public Optional getInferredAuth(String authTarget) { - return Optional.empty(); - } - - @Override - public String getInferredAuthDescriptor() { - return ""; - } - @Override public ImageFormat getImageFormat() { return jibExtension.getContainer().getFormat(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java index 17d6ce04c4..c03ab4d2cc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java @@ -40,7 +40,7 @@ public class TargetImageParameters { @Inject public TargetImageParameters(ObjectFactory objectFactory) { - auth = objectFactory.newInstance(AuthParameters.class); + auth = objectFactory.newInstance(AuthParameters.class, "to.auth"); } @Input diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index 25b56b740f..dced5dbf72 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -39,6 +39,9 @@ public void testGetters() { Mockito.when(authParameters.getUsername()).thenReturn("user"); Mockito.when(authParameters.getPassword()).thenReturn("password"); + Mockito.when(authParameters.getAuthDescriptor()).thenReturn("from.auth"); + Mockito.when(authParameters.getUsernameDescriptor()).thenReturn("from.auth.username"); + Mockito.when(authParameters.getPasswordDescriptor()).thenReturn("from.auth.password"); Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); @@ -71,9 +74,9 @@ public void testGetters() { AuthProperty fromAuth = rawConfiguration.getFromAuth(); Assert.assertEquals("user", fromAuth.getUsername()); Assert.assertEquals("password", fromAuth.getPassword()); - Assert.assertEquals("test.auth", rawConfiguration.getAuthDescriptor("test")); - Assert.assertEquals("test2.auth.username", rawConfiguration.getUsernameAuthDescriptor("test2")); - Assert.assertEquals("test3.auth.password", rawConfiguration.getPasswordAuthDescriptor("test3")); + Assert.assertEquals("from.auth", fromAuth.getAuthDescriptor()); + Assert.assertEquals("from.auth.username", fromAuth.getUsernameDescriptor()); + Assert.assertEquals("from.auth.password", fromAuth.getPasswordDescriptor()); Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); Assert.assertEquals("/app/root", rawConfiguration.getAppRoot()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 6268bac235..f02ba36913 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -32,7 +32,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -78,14 +77,15 @@ public void execute() throws MojoExecutionException { appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); - RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - rawConfiguration, + new MavenRawConfiguration(this), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, null, null, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index f304f2f859..51711c6c7f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -32,7 +32,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.io.IOException; @@ -89,7 +88,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); - MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), @@ -99,11 +97,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); - RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - rawConfiguration, projectProperties); + new MavenRawConfiguration(this), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), + projectProperties); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 84297001df..a88e60d926 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -30,7 +30,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -73,7 +72,6 @@ public void execute() throws MojoExecutionException { appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); - RawConfiguration rawConfiguration = new MavenRawConfiguration(this, eventDispatcher); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); @@ -82,7 +80,9 @@ public void execute() throws MojoExecutionException { Path tarOutputPath = buildOutput.resolve("jib-image.tar"); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( - rawConfiguration, + new MavenRawConfiguration(this), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, tarOutputPath, mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 1c987869ad..6dede10731 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -44,11 +44,16 @@ /** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */ public abstract class JibPluginConfiguration extends AbstractMojo { - /** Used to configure {@code from.auth} and {@code to.auth} parameters. */ - public static class AuthConfiguration implements AuthProperty { + /** Base for {@link FromAuthConfiguration} and {@link ToAuthConfiguration}. */ + private static class AuthConfiguration implements AuthProperty { @Nullable @Parameter private String username; @Nullable @Parameter private String password; + private final String source; + + private AuthConfiguration(String source) { + this.source = source; + } @Override @Nullable @@ -62,14 +67,35 @@ public String getPassword() { return password; } - @VisibleForTesting - void setUsername(String username) { - this.username = username; + @Override + public String getAuthDescriptor() { + return "<" + source + ">"; } - @VisibleForTesting - void setPassword(String password) { - this.password = password; + @Override + public String getUsernameDescriptor() { + return getAuthDescriptor() + ""; + } + + @Override + public String getPasswordDescriptor() { + return getAuthDescriptor() + ""; + } + } + + /** Used to configure {@code from.auth} parameters. */ + public static class FromAuthConfiguration extends AuthConfiguration { + + public FromAuthConfiguration() { + super("from"); + } + } + + /** Used to configure {@code to.auth} parameters. */ + public static class ToAuthConfiguration extends AuthConfiguration { + + public ToAuthConfiguration() { + super("to"); } } @@ -106,7 +132,7 @@ public static class FromConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(); + @Parameter private FromAuthConfiguration auth = new FromAuthConfiguration(); } /** Configuration for {@code to} parameter, where image is required. */ @@ -118,7 +144,7 @@ public static class ToConfiguration { @Nullable @Parameter private String credHelper; - @Parameter private AuthConfiguration auth = new AuthConfiguration(); + @Parameter private ToAuthConfiguration auth = new ToAuthConfiguration(); public void set(String image) { this.image = image; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index f1b8ff75e4..d47a0dad06 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -16,10 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import java.util.List; import java.util.Map; @@ -29,22 +27,14 @@ class MavenRawConfiguration implements RawConfiguration { private final JibPluginConfiguration jibPluginConfiguration; - private final MavenSettingsServerCredentials mavenSettingsServerCredentials; /** * Creates a raw configuration instances. * * @param jibPluginConfiguration the Jib plugin configuration - * @param eventDispatcher Jib event dispatcher */ - MavenRawConfiguration( - JibPluginConfiguration jibPluginConfiguration, EventDispatcher eventDispatcher) { + MavenRawConfiguration(JibPluginConfiguration jibPluginConfiguration) { this.jibPluginConfiguration = jibPluginConfiguration; - mavenSettingsServerCredentials = - new MavenSettingsServerCredentials( - jibPluginConfiguration.getSession().getSettings(), - jibPluginConfiguration.getSettingsDecrypter(), - eventDispatcher); } @Override @@ -72,21 +62,6 @@ public AuthProperty getToAuth() { return jibPluginConfiguration.getTargetImageAuth(); } - @Override - public String getAuthDescriptor(String source) { - return "<" + source + ">"; - } - - @Override - public String getUsernameAuthDescriptor(String source) { - return getAuthDescriptor(source) + ""; - } - - @Override - public String getPasswordAuthDescriptor(String source) { - return getAuthDescriptor(source) + ""; - } - @Override public Optional getToCredHelper() { return Optional.ofNullable(jibPluginConfiguration.getTargetImageCredentialHelperName()); @@ -167,17 +142,6 @@ public boolean getUseOnlyProjectCache() { return jibPluginConfiguration.getUseOnlyProjectCache(); } - @Override - public Optional getInferredAuth(String authTarget) - throws InferredAuthRetrievalException { - return mavenSettingsServerCredentials.retrieve(authTarget); - } - - @Override - public String getInferredAuthDescriptor() { - return MavenSettingsServerCredentials.CREDENTIAL_SOURCE; - } - @Override public ImageFormat getImageFormat() { return ImageFormat.valueOf(jibPluginConfiguration.getFormat()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 2ada7bc69e..6a816c82a0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthProvider; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.common.annotations.VisibleForTesting; import java.util.Optional; @@ -37,7 +38,7 @@ * Retrieves credentials for servers defined in Maven settings. */ -class MavenSettingsServerCredentials { +class MavenSettingsServerCredentials implements InferredAuthProvider { static final String CREDENTIAL_SOURCE = "Maven settings"; @@ -84,11 +85,8 @@ static boolean isEncrypted(String password) { * @return the auth info for the registry, or {@link Optional#empty} if none could be retrieved * @throws InferredAuthRetrievalException if the credentials could not be retrieved */ - Optional retrieve(@Nullable String registry) throws InferredAuthRetrievalException { - if (registry == null) { - return Optional.empty(); - } - + @Override + public Optional getAuth(String registry) throws InferredAuthRetrievalException { Server registryServer = settings.getServer(registry); if (registryServer == null) { return Optional.empty(); @@ -136,6 +134,21 @@ public String getUsername() { public String getPassword() { return password; } + + @Override + public String getAuthDescriptor() { + return CREDENTIAL_SOURCE; + } + + @Override + public String getUsernameDescriptor() { + return CREDENTIAL_SOURCE; + } + + @Override + public String getPasswordDescriptor() { + return CREDENTIAL_SOURCE; + } }); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index 4ed198f469..b5fe9ff2e3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -17,9 +17,8 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.AuthConfiguration; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.FromAuthConfiguration; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.HashMap; @@ -35,7 +34,7 @@ public class MavenRawConfigurationTest { @Test - public void testGetters() throws InferredAuthRetrievalException { + public void testGetters() { JibPluginConfiguration jibPluginConfiguration = Mockito.mock(JibPluginConfiguration.class); EventDispatcher eventDispatcher = Mockito.mock(EventDispatcher.class); @@ -49,9 +48,12 @@ public void testGetters() throws InferredAuthRetrievalException { MavenSession mavenSession = Mockito.mock(MavenSession.class); Mockito.when(mavenSession.getSettings()).thenReturn(mavenSettings); - AuthConfiguration auth = Mockito.mock(AuthConfiguration.class); + FromAuthConfiguration auth = Mockito.mock(FromAuthConfiguration.class); Mockito.when(auth.getUsername()).thenReturn("user"); Mockito.when(auth.getPassword()).thenReturn("password"); + Mockito.when(auth.getAuthDescriptor()).thenReturn(""); + Mockito.when(auth.getUsernameDescriptor()).thenReturn(""); + Mockito.when(auth.getPasswordDescriptor()).thenReturn(""); Mockito.when(jibPluginConfiguration.getSession()).thenReturn(mavenSession); Mockito.when(jibPluginConfiguration.getBaseImageAuth()).thenReturn(auth); @@ -75,23 +77,14 @@ public void testGetters() throws InferredAuthRetrievalException { Mockito.when(jibPluginConfiguration.getUseOnlyProjectCache()).thenReturn(true); Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); - MavenRawConfiguration rawConfiguration = - new MavenRawConfiguration(jibPluginConfiguration, eventDispatcher); + MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); AuthProperty fromAuth = rawConfiguration.getFromAuth(); Assert.assertEquals("user", fromAuth.getUsername()); Assert.assertEquals("password", fromAuth.getPassword()); - Assert.assertEquals("", rawConfiguration.getAuthDescriptor("test")); - Assert.assertEquals( - "", rawConfiguration.getUsernameAuthDescriptor("test")); - Assert.assertEquals( - "", rawConfiguration.getPasswordAuthDescriptor("test")); - - AuthProperty mavenSettingsAuth = rawConfiguration.getInferredAuth("base registry").get(); - Assert.assertEquals("maven settings user", mavenSettingsAuth.getUsername()); - Assert.assertEquals("maven settings password", mavenSettingsAuth.getPassword()); - - Assert.assertFalse(rawConfiguration.getInferredAuth("unknown registry").isPresent()); + Assert.assertEquals("", fromAuth.getAuthDescriptor()); + Assert.assertEquals("", fromAuth.getUsernameDescriptor()); + Assert.assertEquals("", fromAuth.getPasswordDescriptor()); Assert.assertTrue(rawConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Arrays.asList("java", "Main"), rawConfiguration.getEntrypoint().get()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 583fd326e9..48b81eeaf3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -58,7 +58,7 @@ public void testRetrieve_found() throws InferredAuthRetrievalException { Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); @@ -68,12 +68,12 @@ public void testRetrieve_found() throws InferredAuthRetrievalException { @Test public void testRetrieve_notFound() throws InferredAuthRetrievalException { - Assert.assertFalse(testMavenSettingsServerCredentials.retrieve("serverUnknown").isPresent()); + Assert.assertFalse(testMavenSettingsServerCredentials.getAuth("serverUnknown").isPresent()); } @Test public void testRetrieve_withNullServer() throws InferredAuthRetrievalException { - Assert.assertFalse(testMavenSettingsServerCredentials.retrieve(null).isPresent()); + Assert.assertFalse(testMavenSettingsServerCredentials.getAuth(null).isPresent()); } @Test @@ -82,7 +82,7 @@ public void testRetrieve_withNullDecrypter_encrypted() throws InferredAuthRetrie Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); - Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("{COQLCE6DU6GtcS5P=}", auth.get().getPassword()); @@ -109,7 +109,7 @@ public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalExc Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - Optional auth = testMavenSettingsServerCredentials.retrieve("server1"); + Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); @@ -140,7 +140,7 @@ public void testRetrieve_withDecrypter_failure() { Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); try { - testMavenSettingsServerCredentials.retrieve("server1"); + testMavenSettingsServerCredentials.getAuth("server1"); Assert.fail("decryption should have failed"); } catch (InferredAuthRetrievalException ex) { Assert.assertEquals( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java index b62d61ad27..40a24fb80c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AuthProperty.java @@ -26,4 +26,10 @@ public interface AuthProperty { @Nullable String getPassword(); + + String getAuthDescriptor(); + + String getUsernameDescriptor(); + + String getPasswordDescriptor(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java new file mode 100644 index 0000000000..01838fdf0a --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.util.Optional; + +/** Provides inferred auth information. */ +public interface InferredAuthProvider { + + Optional getAuth(String registry) throws InferredAuthRetrievalException; +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java index 5198597dbc..5f729d5e3d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java @@ -16,11 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -/** - * Exception when trying to retrieve an inferred auth info. - * - * @see RawConfiguration#getInferredAuth(String) - */ +/** Exception when trying to retrieve an inferred auth info. */ public class InferredAuthRetrievalException extends Exception { public InferredAuthRetrievalException(String message) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index bcb33fe677..aba6c81dac 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -55,12 +55,6 @@ */ public class PluginConfigurationProcessor { - @FunctionalInterface - private interface InferredAuthProvider { - - Optional getInferredAuth(String registry) throws InferredAuthRetrievalException; - } - /** * Compute the container entrypoint, in this order: * @@ -129,6 +123,7 @@ public static String getBaseImage( public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, @Nullable Path dockerExecutable, @Nullable Map dockerEnvironment, @@ -148,11 +143,17 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa Containerizer containerizer = Containerizer.to(targetImage); return processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + rawConfiguration, + inferredAuthProvider, + projectProperties, + containerizer, + targetImageReference, + false); } public static PluginConfigurationProcessor processCommonConfigurationForTarImage( RawConfiguration rawConfiguration, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, Path tarImagePath, HelpfulSuggestions helpfulSuggestions) @@ -165,11 +166,18 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage Containerizer containerizer = Containerizer.to(targetImage); return processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + rawConfiguration, + inferredAuthProvider, + projectProperties, + containerizer, + targetImageReference, + false); } public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) + RawConfiguration rawConfiguration, + InferredAuthProvider inferredAuthProvider, + ProjectProperties projectProperties) throws InferredAuthRetrievalException, InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException { @@ -188,16 +196,13 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry PropertyNames.TO_AUTH_USERNAME, PropertyNames.TO_AUTH_PASSWORD, rawConfiguration.getToAuth(), - rawConfiguration.getAuthDescriptor("to"), - rawConfiguration.getUsernameAuthDescriptor("to"), - rawConfiguration.getPasswordAuthDescriptor("to"), - rawConfiguration::getInferredAuth, - rawConfiguration.getInferredAuthDescriptor(), + inferredAuthProvider, rawConfiguration.getToCredHelper().orElse(null)); PluginConfigurationProcessor processor = processCommonConfiguration( rawConfiguration, + inferredAuthProvider, projectProperties, Containerizer.to(targetImage), targetImageReference, @@ -209,6 +214,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry @VisibleForTesting static PluginConfigurationProcessor processCommonConfiguration( RawConfiguration rawConfiguration, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, Containerizer containerizer, ImageReference targetImageReference, @@ -239,11 +245,7 @@ static PluginConfigurationProcessor processCommonConfiguration( PropertyNames.FROM_AUTH_USERNAME, PropertyNames.FROM_AUTH_PASSWORD, rawConfiguration.getFromAuth(), - rawConfiguration.getAuthDescriptor("from"), - rawConfiguration.getUsernameAuthDescriptor("from"), - rawConfiguration.getPasswordAuthDescriptor("from"), - rawConfiguration::getInferredAuth, - rawConfiguration.getInferredAuthDescriptor(), + inferredAuthProvider, rawConfiguration.getFromCredHelper().orElse(null)); JibContainerBuilder jibContainerBuilder = @@ -351,38 +353,34 @@ private static boolean configureCredentialRetrievers( String usernamePropertyName, String passwordPropertyName, AuthProperty knownAuth, - String knownAuthDescriptor, - String knownAuthUsernameDescriptor, - String knownAuthPasswordDescriptor, InferredAuthProvider inferredAuthProvider, - String inferredAuthDescriptor, @Nullable String credHelper) throws FileNotFoundException, InferredAuthRetrievalException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage(imageReference, eventDispatcher)); - Optional optionalToCredential = + Optional optionalCredential = ConfigurationPropertyValidator.getImageCredential( eventDispatcher, usernamePropertyName, passwordPropertyName, knownAuth, - knownAuthUsernameDescriptor, - knownAuthPasswordDescriptor); - boolean credentialPresent = optionalToCredential.isPresent(); - if (optionalToCredential.isPresent()) { + knownAuth.getUsernameDescriptor(), + knownAuth.getPasswordDescriptor()); + boolean credentialPresent = optionalCredential.isPresent(); + if (optionalCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( - optionalToCredential.get(), knownAuthDescriptor); + optionalCredential.get(), knownAuth.getAuthDescriptor()); } else { Optional optionalInferredAuth = - inferredAuthProvider.getInferredAuth(imageReference.getRegistry()); + inferredAuthProvider.getAuth(imageReference.getRegistry()); credentialPresent = optionalInferredAuth.isPresent(); if (optionalInferredAuth.isPresent()) { AuthProperty auth = optionalInferredAuth.get(); String username = Verify.verifyNotNull(auth.getUsername()); String password = Verify.verifyNotNull(auth.getPassword()); Credential credential = Credential.basic(username, password); - defaultCredentialRetrievers.setInferredCredential(credential, inferredAuthDescriptor); + defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); } } defaultCredentialRetrievers.setCredentialHelper(credHelper); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 3f8e41e1f9..e78a9c4dd3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -35,12 +35,6 @@ public interface RawConfiguration { AuthProperty getToAuth(); - String getAuthDescriptor(String source); - - String getUsernameAuthDescriptor(String source); - - String getPasswordAuthDescriptor(String source); - Optional getFromCredHelper(); Optional getToCredHelper(); @@ -75,12 +69,5 @@ public interface RawConfiguration { boolean getUseOnlyProjectCache(); - // TODO: This is only for getting values from Maven settings.xml, and in some sense, auth info - // from settings.xml is not necessary raw configuration values. Consider removing it. - // https://github.com/GoogleContainerTools/jib/pull/1163#discussion_r228389684 - Optional getInferredAuth(String authTarget) throws InferredAuthRetrievalException; - - String getInferredAuthDescriptor(); - ImageFormat getImageFormat(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index e5b8d50bc7..819689a56f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -93,11 +93,10 @@ public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, InvalidWorkingDirectoryException, InvalidContainerVolumeException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), @@ -119,8 +118,7 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + createPluginConfigurationProcessor(); Mockito.verify(containerizer).setBaseImageLayersCache(Paths.get("new/base/cache")); Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("/new/application/cache")); @@ -136,9 +134,7 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() InvalidContainerVolumeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); Assert.assertEquals( ImageReference.parse("gcr.io/distroless/java/jetty").toString(), @@ -154,9 +150,7 @@ public void testEntrypoint() Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -195,12 +189,10 @@ public void testEntrypoint_defaultWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); - + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verifyZeroInteractions(logger); @@ -214,11 +206,10 @@ public void testEntrypoint_defaulNonWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), @@ -235,9 +226,7 @@ public void testUser() InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -250,9 +239,7 @@ public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, InvalidWorkingDirectoryException, InvalidContainerVolumeException { - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -269,9 +256,7 @@ public void testEntrypoint_warningOnJvmFlags() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -292,9 +277,7 @@ public void testEntrypoint_warningOnMainclass() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -313,9 +296,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -337,9 +318,7 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() InvalidWorkingDirectoryException, InvalidContainerVolumeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = - PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, projectProperties, containerizer, targetImageReference, false); + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -490,4 +469,17 @@ public void testGetInvalidVolumesList() { Assert.assertEquals("`some/root", ex.getInvalidVolume()); } } + + private PluginConfigurationProcessor createPluginConfigurationProcessor() + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException { + return PluginConfigurationProcessor.processCommonConfiguration( + rawConfiguration, + ignored -> Optional.empty(), + projectProperties, + containerizer, + targetImageReference, + false); + } } From cd17858736cfd42be82e1b20ca6dd7a134809924 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 7 Dec 2018 14:37:19 -0500 Subject: [PATCH 0380/2020] Use artifact ID as image ref for dockerBuild (#1322) --- jib-maven-plugin/CHANGELOG.md | 1 + .../jib/maven/MavenProjectProperties.java | 2 +- .../maven/BuildDockerMojoIntegrationTest.java | 9 ++++ .../projects/simple/pom-no-to-image.xml | 50 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ff57f95806..acd7cabdb1 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changed - `gwt-app` packaging type now builds a WAR container. +- When building to Docker and no `` is defined, artifact ID is used as an image reference instead of project name. ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index e6f0122794..df12dbe483 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -164,7 +164,7 @@ public boolean isWarProject() { @Override public String getName() { - return project.getName(); + return project.getArtifactId(); } @Override diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index cf0ce44794..b7db8ff602 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -154,4 +154,13 @@ public void testExecute_userNames() "myuser:mygroup", new Command("docker", "inspect", "-f", "{{.Config.User}}", targetImage).run().trim()); } + + @Test + public void testExecute_noToImageAndInvalidProjectName() + throws DigestException, VerificationException, IOException, InterruptedException { + buildToDockerDaemon( + simpleTestProject.getProjectRoot(), "image reference ignored", "pom-no-to-image.xml"); + Assert.assertEquals( + "Hello, world. \n", new Command("docker", "run", "--rm", "my-artifact-id:1").run()); + } } diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml new file mode 100644 index 0000000000..74e84e303e --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + com.test + my-artifact-id + 1 + + Name not Usable as Image Reference + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + + true + + + + + From 0a5be526eb6bfa4d825fca89c1c3c5c6bc38b680 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 7 Dec 2018 15:12:19 -0500 Subject: [PATCH 0381/2020] Assigns Allocation to each builder step. (#1321) --- .../builder/steps/AuthenticatePushStep.java | 26 ++++++++--- .../BuildAndCacheApplicationLayerStep.java | 42 ++++++++++++----- .../jib/builder/steps/BuildImageStep.java | 16 ++++++- .../jib/builder/steps/LoadDockerStep.java | 26 ++++++++--- .../steps/PullAndCacheBaseImageLayerStep.java | 17 ++++++- .../PullAndCacheBaseImageLayersStep.java | 19 ++++++-- .../jib/builder/steps/PullBaseImageStep.java | 42 ++++++++++++++--- .../tools/jib/builder/steps/PushBlobStep.java | 13 ++++++ .../steps/PushContainerConfigurationStep.java | 13 +++++- .../jib/builder/steps/PushImageStep.java | 22 +++++++-- .../jib/builder/steps/PushLayersStep.java | 14 +++++- .../RetrieveRegistryCredentialsStep.java | 45 ++++++++++++++----- .../tools/jib/builder/steps/StepsRunner.java | 42 ++++++++++++++--- .../jib/builder/steps/WriteTarFileStep.java | 23 +++++++--- .../configuration/credentials/Credential.java | 5 +++ ...BuildAndCacheApplicationLayerStepTest.java | 5 ++- .../jib/builder/steps/BuildImageStepTest.java | 8 ++++ .../RetrieveRegistryCredentialsStepTest.java | 15 ++++--- 18 files changed, 323 insertions(+), 70 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 7c14849436..fa1c4b3547 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; @@ -45,6 +47,8 @@ class AuthenticatePushStep implements AsyncStep, Callable listenableFuture; @@ -52,8 +56,10 @@ class AuthenticatePushStep implements AsyncStep, Callable getFuture() { public Authorization call() throws ExecutionException, RegistryAuthenticationFailedException, IOException, RegistryException { + String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + + Allocation progressAllocation = + parentProgressAllocation.newChild("authenticate push to " + registry, 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), - String.format( - DESCRIPTION, - buildConfiguration.getTargetImageConfiguration().getImageRegistry()))) { + buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); Authorization registryAuthorization = registryCredential == null @@ -92,9 +101,16 @@ public Authorization call() .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); if (registryAuthenticator == null) { + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + return registryAuthorization; } - return registryAuthenticator.setAuthorization(registryAuthorization).authenticatePush(); + Authorization authorization = + registryAuthenticator.setAuthorization(registryAuthorization).authenticatePush(); + + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + + return authorization; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index f4853e24d0..146aeeea2b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -25,6 +25,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; @@ -43,41 +45,56 @@ class BuildAndCacheApplicationLayerStep implements AsyncStep, Calla * classes layers. Optionally adds an extra layer if configured to do so. */ static ImmutableList makeList( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { + ListeningExecutorService listeningExecutorService, + BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation) { + int layerCount = buildConfiguration.getLayerConfigurations().size(); + Allocation progressAllocation = + parentProgressAllocation.newChild("Build application layers", layerCount); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = - ImmutableList.builderWithExpectedSize(buildConfiguration.getLayerConfigurations().size()); + ImmutableList.builderWithExpectedSize(layerCount); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { // Skips the layer if empty. if (layerConfiguration.getLayerEntries().isEmpty()) { + buildConfiguration + .getEventDispatcher() + .dispatch(new ProgressEvent(progressAllocation, 1)); continue; } buildAndCacheApplicationLayerSteps.add( new BuildAndCacheApplicationLayerStep( - layerConfiguration.getName(), listeningExecutorService, buildConfiguration, + progressAllocation, + layerConfiguration.getName(), layerConfiguration)); } return buildAndCacheApplicationLayerSteps.build(); } } - private final String layerType; private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; + + private final String layerType; private final LayerConfiguration layerConfiguration; private final ListenableFuture listenableFuture; private BuildAndCacheApplicationLayerStep( - String layerType, ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, + String layerType, LayerConfiguration layerConfiguration) { - this.layerType = layerType; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; + this.layerType = layerType; this.layerConfiguration = layerConfiguration; listenableFuture = listeningExecutorService.submit(this); @@ -88,15 +105,14 @@ public ListenableFuture getFuture() { return listenableFuture; } - public String getLayerType() { - return layerType; - } - @Override public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); + Allocation progressAllocation = + parentProgressAllocation.newChild("Build " + layerType + " layer", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { @@ -106,6 +122,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { Optional optionalCachedLayer = cache.retrieve(layerConfiguration.getLayerEntries()); if (optionalCachedLayer.isPresent()) { + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCachedLayer.get(); } @@ -116,8 +133,13 @@ public CachedLayer call() throws IOException, CacheCorruptedException { buildConfiguration .getEventDispatcher() .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return cachedLayer; } } + + String getLayerType() { + return layerType; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 08747e45cf..901833f987 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -23,6 +23,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -44,22 +46,25 @@ class BuildImageStep private static final String DESCRIPTION = "Building container configuration"; + private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; private final PullBaseImageStep pullBaseImageStep; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture>> listenableFuture; BuildImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, PullBaseImageStep pullBaseImageStep, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.pullBaseImageStep = pullBaseImageStep; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -95,6 +100,9 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { + Allocation progressAllocation = parentProgressAllocation.newChild("build image format", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. @@ -168,7 +176,11 @@ private Image afterCachedLayerSteps() } // Gets the container configuration content descriptor. - return imageBuilder.build(); + Image image = imageBuilder.build(); + + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + + return image; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index a162ef1703..7749e96106 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; @@ -36,25 +38,30 @@ /** Adds image layers to a tarball and loads into Docker daemon. */ class LoadDockerStep implements AsyncStep, Callable { - private final DockerClient dockerClient; + private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; + + private final DockerClient dockerClient; + private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; private final BuildImageStep buildImageStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture listenableFuture; LoadDockerStep( ListeningExecutorService listeningExecutorService, - DockerClient dockerClient, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, + DockerClient dockerClient, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; - this.dockerClient = dockerClient; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; + this.dockerClient = dockerClient; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; this.buildImageStep = buildImageStep; @@ -89,14 +96,17 @@ public BuildResult call() throws ExecutionException, InterruptedException { private BuildResult afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); + Allocation progressAllocation = parentProgressAllocation.newChild("Load to Docker daemon", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. - buildConfiguration - .getEventDispatcher() - .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. @@ -108,6 +118,8 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 0235fc0cd2..4e3c9f7381 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -38,6 +40,8 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable private static final String DESCRIPTION = "Pulling base image layer %s"; private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; + private final DescriptorDigest layerDigest; private final @Nullable Authorization pullAuthorization; @@ -46,9 +50,11 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable PullAndCacheBaseImageLayerStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, DescriptorDigest layerDigest, @Nullable Authorization pullAuthorization) { this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.layerDigest = layerDigest; this.pullAuthorization = pullAuthorization; @@ -62,6 +68,10 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { + Allocation progressAllocation = + parentProgressAllocation.newChild("pull base image layer " + layerDigest, 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { @@ -70,6 +80,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { // Checks if the layer already exists in the cache. Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCachedLayer.get(); } @@ -78,7 +89,11 @@ public CachedLayer call() throws IOException, CacheCorruptedException { .newBaseImageRegistryClientFactory() .setAuthorization(pullAuthorization) .newRegistryClient(); - return cache.writeCompressedLayer(registryClient.pullBlob(layerDigest)); + CachedLayer cachedLayer = cache.writeCompressedLayer(registryClient.pullBlob(layerDigest)); + + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + + return cachedLayer; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 6b07112f13..9d6421e143 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -38,17 +40,21 @@ class PullAndCacheBaseImageLayersStep private static final String DESCRIPTION = "Setting up base image caching"; private final BuildConfiguration buildConfiguration; + private final ListeningExecutorService listeningExecutorService; + private final Allocation parentProgressAllocation; + private final PullBaseImageStep pullBaseImageStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture> listenableFuture; PullAndCacheBaseImageLayersStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, PullBaseImageStep pullBaseImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.pullBaseImageStep = pullBaseImageStep; listenableFuture = @@ -63,11 +69,15 @@ public ListenableFuture> getFuture @Override public ImmutableList call() throws ExecutionException, LayerPropertyNotFoundException { + BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); + ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); + + Allocation progressAllocation = + parentProgressAllocation.newChild("pull base imgae layers", baseImageLayers.size()); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { - BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); - ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); - ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = ImmutableList.builderWithExpectedSize(baseImageLayers.size()); for (Layer layer : baseImageLayers) { @@ -75,6 +85,7 @@ public ImmutableList call() new PullAndCacheBaseImageLayerStep( listeningExecutorService, buildConfiguration, + progressAllocation, layer.getBlobDescriptor().getDigest(), pullBaseImageStepResult.getBaseImageAuthorization())); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 344a2f2e15..37599aba26 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -24,6 +24,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.Image; @@ -82,12 +84,16 @@ Authorization getBaseImageAuthorization() { } private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; private final ListenableFuture listenableFuture; PullBaseImageStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { + ListeningExecutorService listeningExecutorService, + BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation) { this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; listenableFuture = listeningExecutorService.submit(this); } @@ -109,12 +115,19 @@ public BaseImageWithAuthorization call() "Getting base image " + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); + Allocation progressAllocation = parentProgressAllocation.newChild("pull base image", 2); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. try { - return new BaseImageWithAuthorization(pullBaseImage(null), null); + BaseImageWithAuthorization baseImageWithAuthorization = + new BaseImageWithAuthorization(pullBaseImage(null), null); + + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 2)); + + return baseImageWithAuthorization; } catch (RegistryUnauthorizedException ex) { buildConfiguration @@ -130,7 +143,8 @@ public BaseImageWithAuthorization call() // registry.credentials.RegistryCredentialsRetriever to avoid this direct executor hack. ListeningExecutorService directExecutorService = MoreExecutors.newDirectExecutorService(); RetrieveRegistryCredentialsStep retrieveBaseRegistryCredentialsStep = - RetrieveRegistryCredentialsStep.forBaseImage(directExecutorService, buildConfiguration); + RetrieveRegistryCredentialsStep.forBaseImage( + directExecutorService, buildConfiguration, progressAllocation); Credential registryCredential = NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); Authorization registryAuthorization = @@ -140,8 +154,15 @@ public BaseImageWithAuthorization call() registryCredential.getUsername(), registryCredential.getPassword()); try { - return new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); + BaseImageWithAuthorization baseImageWithAuthorization = + new BaseImageWithAuthorization( + pullBaseImage(registryAuthorization), registryAuthorization); + + buildConfiguration + .getEventDispatcher() + .dispatch(new ProgressEvent(progressAllocation, 1)); + + return baseImageWithAuthorization; } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. @@ -164,8 +185,15 @@ public BaseImageWithAuthorization call() registryAuthorization = registryAuthenticator.setAuthorization(registryAuthorization).authenticatePull(); - return new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); + BaseImageWithAuthorization baseImageWithAuthorization = + new BaseImageWithAuthorization( + pullBaseImage(registryAuthorization), registryAuthorization); + + buildConfiguration + .getEventDispatcher() + .dispatch(new ProgressEvent(progressAllocation, 1)); + + return baseImageWithAuthorization; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 58664395bb..a1cef8154f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -23,6 +23,8 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.Futures; @@ -38,6 +40,8 @@ class PushBlobStep implements AsyncStep, Callable, Callable getFuture() { @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { + Allocation progressAllocation = + parentProgressAllocation.newChild("push blob " + blobDescriptor.getDigest(), 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { @@ -81,12 +91,15 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc buildConfiguration .getEventDispatcher() .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return blobDescriptor; } // todo: leverage cross-repository mounts registryClient.pushBlob(blobDescriptor.getDigest(), blob, null); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + return blobDescriptor; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 04de287a0e..5e69c50e2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -40,19 +42,23 @@ class PushContainerConfigurationStep private static final String DESCRIPTION = "Pushing container configuration"; private final BuildConfiguration buildConfiguration; + private final ListeningExecutorService listeningExecutorService; + private final Allocation parentProgressAllocation; + private final AuthenticatePushStep authenticatePushStep; private final BuildImageStep buildImageStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture> listenableFuture; PushContainerConfigurationStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, AuthenticatePushStep authenticatePushStep, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.authenticatePushStep = authenticatePushStep; this.buildImageStep = buildImageStep; @@ -76,6 +82,10 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { + Allocation progressAllocation = + parentProgressAllocation.newChild("push container configuration", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); @@ -87,6 +97,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() return new PushBlobStep( listeningExecutorService, buildConfiguration, + progressAllocation, authenticatePushStep, blobDescriptor, containerConfigurationBlob); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 553cd46b17..f684c3abf7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -44,6 +46,9 @@ class PushImageStep implements AsyncStep, Callable { private static final String DESCRIPTION = "Pushing new image"; private final BuildConfiguration buildConfiguration; + private final ListeningExecutorService listeningExecutorService; + private final Allocation parentProgressAllocation; + private final AuthenticatePushStep authenticatePushStep; private final PushLayersStep pushBaseImageLayersStep; @@ -51,12 +56,12 @@ class PushImageStep implements AsyncStep, Callable { private final PushContainerConfigurationStep pushContainerConfigurationStep; private final BuildImageStep buildImageStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture listenableFuture; PushImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, AuthenticatePushStep authenticatePushStep, PushLayersStep pushBaseImageLayersStep, PushLayersStep pushApplicationLayersStep, @@ -64,8 +69,8 @@ class PushImageStep implements AsyncStep, Callable { BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.authenticatePushStep = authenticatePushStep; - this.pushBaseImageLayersStep = pushBaseImageLayersStep; this.pushApplicationLayersStep = pushApplicationLayersStep; this.pushContainerConfigurationStep = pushContainerConfigurationStep; @@ -121,6 +126,9 @@ private ListenableFuture> afterPushSteps() } private ListenableFuture afterAllPushed() throws ExecutionException, IOException { + Allocation progressAllocation = parentProgressAllocation.newChild("Push to registry", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { RegistryClient registryClient = @@ -163,7 +171,15 @@ private ListenableFuture afterAllPushed() throws ExecutionException BuildResult result = new BuildResult(imageDigest, imageId); return Futures.whenAllSucceed(pushAllTagsFutures) - .call(() -> result, listeningExecutorService); + .call( + () -> { + buildConfiguration + .getEventDispatcher() + .dispatch(new ProgressEvent(progressAllocation, 1)); + + return result; + }, + listeningExecutorService); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 5ee6bf46ca..bdac0023e3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -36,21 +38,25 @@ class PushLayersStep private static final String DESCRIPTION = "Setting up to push layers"; private final BuildConfiguration buildConfiguration; + private final ListeningExecutorService listeningExecutorService; + private final Allocation parentProgressAllocation; + private final AuthenticatePushStep authenticatePushStep; private final AsyncStep>> cachedLayerStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture>> listenableFuture; PushLayersStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, AuthenticatePushStep authenticatePushStep, AsyncStep>> cachedLayerStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.authenticatePushStep = authenticatePushStep; this.cachedLayerStep = cachedLayerStep; @@ -86,9 +92,15 @@ public ImmutableList> call() throws ExecutionException { private PushBlobStep makePushBlobStep(AsyncStep cachedLayerStep) throws ExecutionException { CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep); + + Allocation progressAllocation = + parentProgressAllocation.newChild("push layer " + cachedLayer.getDigest(), 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + return new PushBlobStep( listeningExecutorService, buildConfiguration, + progressAllocation, authenticatePushStep, new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), cachedLayer.getBlob()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 2b3de9ea80..4b86453f5d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -21,8 +21,9 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -41,25 +42,33 @@ private static String makeDescription(String registry) { /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { + ListeningExecutorService listeningExecutorService, + BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getEventDispatcher(), + buildConfiguration, + parentProgressAllocation, buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } /** Retrieves credentials for the target image. */ static RetrieveRegistryCredentialsStep forTargetImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { + ListeningExecutorService listeningExecutorService, + BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, - buildConfiguration.getEventDispatcher(), + buildConfiguration, + parentProgressAllocation, buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } - private final EventDispatcher eventDispatcher; + private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; + private final String registry; private final ImmutableList credentialRetrievers; @@ -68,10 +77,12 @@ static RetrieveRegistryCredentialsStep forTargetImage( @VisibleForTesting RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, - EventDispatcher eventDispatcher, + BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, String registry, ImmutableList credentialRetrievers) { - this.eventDispatcher = eventDispatcher; + this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.registry = registry; this.credentialRetrievers = credentialRetrievers; @@ -87,20 +98,30 @@ public ListenableFuture getFuture() { @Nullable public Credential call() throws CredentialRetrievalException { String description = makeDescription(registry); - eventDispatcher.dispatch(LogEvent.lifecycle(description + "...")); - try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventDispatcher, description)) { + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); + Allocation progressAllocation = + parentProgressAllocation.newChild("Retrieve registry credentials for " + registry, 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { + buildConfiguration + .getEventDispatcher() + .dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCredential.get(); } } // If no credentials found, give an info (not warning because in most cases, the base image is // public and does not need extra credentials) and return null. - eventDispatcher.dispatch( - LogEvent.info("No credentials could be retrieved for registry " + registry)); + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.info("No credentials could be retrieved for registry " + registry)); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return null; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 44051b449b..9cc6fef644 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -20,6 +20,8 @@ import com.google.cloud.tools.jib.async.AsyncSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -86,6 +88,8 @@ public static StepsRunner begin(BuildConfiguration buildConfiguration) { /** The total number of steps added. */ private int stepsCount = 0; + @Nullable private Allocation rootProgressAllocation; + private StepsRunner( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { this.listeningExecutorService = listeningExecutorService; @@ -97,7 +101,9 @@ public StepsRunner retrieveTargetRegistryCredentials() { () -> steps.retrieveTargetRegistryCredentialsStep = RetrieveRegistryCredentialsStep.forTargetImage( - listeningExecutorService, buildConfiguration)); + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation))); } public StepsRunner authenticatePush() { @@ -107,6 +113,7 @@ public StepsRunner authenticatePush() { new AuthenticatePushStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.retrieveTargetRegistryCredentialsStep))); } @@ -114,7 +121,10 @@ public StepsRunner pullBaseImage() { return enqueueStep( () -> steps.pullBaseImageStep = - new PullBaseImageStep(listeningExecutorService, buildConfiguration)); + new PullBaseImageStep( + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation))); } public StepsRunner pullAndCacheBaseImageLayers() { @@ -124,6 +134,7 @@ public StepsRunner pullAndCacheBaseImageLayers() { new PullAndCacheBaseImageLayersStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.pullBaseImageStep))); } @@ -134,6 +145,7 @@ public StepsRunner pushBaseImageLayers() { new PushLayersStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); } @@ -143,7 +155,9 @@ public StepsRunner buildAndCacheApplicationLayers() { () -> steps.buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - listeningExecutorService, buildConfiguration)); + listeningExecutorService, + buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation))); } public StepsRunner buildImage() { @@ -153,6 +167,7 @@ public StepsRunner buildImage() { new BuildImageStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.pullBaseImageStep), Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); @@ -165,6 +180,7 @@ public StepsRunner pushContainerConfiguration() { new PushContainerConfigurationStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.buildImageStep))); } @@ -176,6 +192,7 @@ public StepsRunner pushApplicationLayers() { new PushLayersStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.authenticatePushStep), AsyncSteps.immediate( Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); @@ -205,12 +222,15 @@ public StepsRunner finalizingBuild() { } public StepsRunner pushImage() { + createRootProgressAllocation("Build to registry"); + return enqueueStep( () -> steps.finalStep = new PushImageStep( listeningExecutorService, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.pushBaseImageLayersStep), Preconditions.checkNotNull(steps.pushApplicationLayersStep), @@ -219,32 +239,39 @@ public StepsRunner pushImage() { } public StepsRunner loadDocker(DockerClient dockerClient) { + createRootProgressAllocation("Build to Docker daemon"); + return enqueueStep( () -> steps.finalStep = new LoadDockerStep( listeningExecutorService, - dockerClient, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), + dockerClient, Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), Preconditions.checkNotNull(steps.buildImageStep))); } public StepsRunner writeTarFile(Path outputPath) { + createRootProgressAllocation("Build to tar file"); + return enqueueStep( () -> steps.finalStep = new WriteTarFileStep( listeningExecutorService, - outputPath, buildConfiguration, + Preconditions.checkNotNull(rootProgressAllocation), + outputPath, Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), Preconditions.checkNotNull(steps.buildImageStep))); } public BuildResult run() throws ExecutionException, InterruptedException { + Preconditions.checkNotNull(rootProgressAllocation); stepsRunnable.run(); return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); } @@ -259,4 +286,9 @@ private StepsRunner enqueueStep(Runnable stepRunnable) { stepsCount++; return this; } + + private void createRootProgressAllocation(String description) { + rootProgressAllocation = Allocation.newRoot(description, stepsCount); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(rootProgressAllocation, 0L)); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 5f69c3d9cc..dbcbab188e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -38,24 +40,28 @@ public class WriteTarFileStep implements AsyncStep, Callable { - private final Path outputPath; + private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; + private final Allocation parentProgressAllocation; + + private final Path outputPath; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; private final BuildImageStep buildImageStep; - private final ListeningExecutorService listeningExecutorService; private final ListenableFuture listenableFuture; WriteTarFileStep( ListeningExecutorService listeningExecutorService, - Path outputPath, BuildConfiguration buildConfiguration, + Allocation parentProgressAllocation, + Path outputPath, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; + this.parentProgressAllocation = parentProgressAllocation; this.outputPath = outputPath; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -90,12 +96,15 @@ public BuildResult call() throws ExecutionException, InterruptedException { } private BuildResult writeTarFile() throws ExecutionException, IOException { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - - // Builds the image to a tarball. buildConfiguration .getEventDispatcher() .dispatch(LogEvent.lifecycle("Building image to tar file...")); + Allocation progressAllocation = parentProgressAllocation.newChild("Write to tar file", 1); + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + + // Builds the image to a tarball. Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { @@ -104,6 +113,8 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { .writeTo(outputStream); } + buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index b77a0978e6..3da9613f0b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -75,4 +75,9 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(username, password); } + + @Override + public String toString() { + return username + ":" + password; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 213dd510b7..6df39ba887 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.Layer; @@ -129,7 +130,9 @@ private ImageLayers buildFakeLayersToCache() throws ExecutionException { ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - MoreExecutors.newDirectExecutorService(), mockBuildConfiguration); + MoreExecutors.newDirectExecutorService(), + mockBuildConfiguration, + Allocation.newRoot("ignored", 1)); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 34f8a2ba66..c74ff80712 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; @@ -189,6 +190,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -217,6 +219,7 @@ public void test_propagateBaseImageConfiguration() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -277,6 +280,7 @@ public void testOverrideWorkingDirectory() throws InterruptedException, Executio new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -298,6 +302,7 @@ public void test_inheritedEntrypoint() throws ExecutionException, InterruptedExc new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -320,6 +325,7 @@ public void test_inheritedEntrypointAndProgramArguments() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -342,6 +348,7 @@ public void test_notInheritedProgramArguments() throws ExecutionException, Inter new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -360,6 +367,7 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, + Allocation.newRoot("ignored", 1), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 0e73892f4d..51fd5037fd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -22,6 +22,8 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; @@ -59,12 +61,12 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio Assert.assertEquals( Credential.basic("baseusername", "basepassword"), RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration) + mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) .call()); Assert.assertEquals( Credential.basic("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration) + mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) .call()); } @@ -75,15 +77,17 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertNull( RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration) + mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) .call()); + Mockito.verify(mockEventDispatcher, Mockito.atLeastOnce()) + .dispatch(Mockito.any(ProgressEvent.class)); Mockito.verify(mockEventDispatcher) .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration) + mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) .call()); Mockito.verify(mockEventDispatcher) @@ -102,7 +106,8 @@ public void testCall_exception() throws IOException { }), Collections.emptyList()); try { - RetrieveRegistryCredentialsStep.forBaseImage(mockListeningExecutorService, buildConfiguration) + RetrieveRegistryCredentialsStep.forBaseImage( + mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) .call(); Assert.fail("Should have thrown exception"); From 3f29f93f5be075b0772384fada2b6e06e34573f9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 10 Dec 2018 11:33:11 -0500 Subject: [PATCH 0382/2020] Remove useOnlyProjectCache from plugin config section of docs (#1329) --- jib-gradle-plugin/README.md | 12 +++++------- jib-maven-plugin/README.md | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index cad1bdcdf6..3cf903fb8a 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -171,7 +171,6 @@ Field | Type | Default | Description `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. `extraDirectory` | [`extraDirectory`](#extradirectory-closure) / `File` | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. -`useProjectOnlyCache` | `boolean` | `false` | If set to `true`, Jib does not share a cache between different Maven projects. `from` is a closure with the following properties: @@ -234,15 +233,14 @@ gradle jib \ gradle jibDockerBuild \ -Djib.container.environment=key1="value1",key2="value2" \ -Djib.container.args=arg1,arg2,arg3 - -gradle jibBuildTar -Djib.useProjectOnlyCache=true ``` -You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): +The following table contains additional system properties that are not available as build configuration parameters: -```shell -gradle jib -Djib.httpTimeout=3000 -``` +Property | Type | Default | Description +--- | --- | --- | --- +`jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. +`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 7feb42900d..64d23dccb4 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -213,7 +213,6 @@ Field | Type | Default | Description `extraDirectory` | [`extraDirectory`](#extradirectory-object) / string | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. -`useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. `from` is an object with the following properties: @@ -276,15 +275,14 @@ mvn compile jib:build \ mvn compile jib:dockerBuild \ -Djib.container.environment=key1="value1",key2="value2" \ -Djib.container.args=arg1,arg2,arg3 - -mvn jib:buildTar -Djib.useProjectOnlyCache=true ``` -You can also configure HTTP connection/read timeouts for registry interactions using the `jib.httpTimeout` system property, configured in milliseconds via commandline (the default is `20000`; you can also set it to `0` for infinite timeout): +The following table contains additional system properties that are not available as build configuration parameters: -```shell -mvn compile jib:build -Djib.httpTimeout=3000 -``` +Property | Type | Default | Description +--- | --- | --- | --- +`jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. +`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* From d328923ac293914e1e755a1760aa9b9c393ff471 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 10 Dec 2018 11:47:11 -0500 Subject: [PATCH 0383/2020] Remove useOnlyProjectCache in favor of system property (#1324) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../DefaultTargetProjectIntegrationTest.java | 3 ++- .../cloud/tools/jib/gradle/JibRunHelper.java | 8 +++++++- .../gradle/SingleProjectIntegrationTest.java | 14 ++++++++++++-- .../projects/default-target/build.gradle | 3 --- .../projects/empty/build-broken-user.gradle | 2 -- .../resources/projects/empty/build.gradle | 2 -- .../multiproject/a_packaged/build.gradle | 2 -- .../resources/projects/simple/build.gradle | 3 --- .../projects/simple/complex-build.gradle | 3 --- .../projects/war_servlet25/build-tomcat.gradle | 3 --- .../projects/war_servlet25/build.gradle | 3 --- .../cloud/tools/jib/gradle/BuildTarTask.java | 1 + .../jib/gradle/GradleRawConfiguration.java | 5 ----- .../cloud/tools/jib/gradle/JibExtension.java | 18 ------------------ .../jib/gradle/GradleRawConfigurationTest.java | 2 -- .../tools/jib/gradle/JibExtensionTest.java | 8 -------- jib-maven-plugin/CHANGELOG.md | 2 ++ .../jib/maven/JibPluginConfiguration.java | 10 ---------- .../tools/jib/maven/MavenRawConfiguration.java | 5 ----- .../maven/BuildDockerMojoIntegrationTest.java | 1 + .../maven/BuildImageMojoIntegrationTest.java | 4 ++++ .../jib/maven/BuildTarMojoIntegrationTest.java | 1 + .../jib/maven/MavenRawConfigurationTest.java | 2 -- .../resources/projects/default-target/pom.xml | 2 -- .../projects/empty/pom-broken-user.xml | 2 -- .../src/test/resources/projects/empty/pom.xml | 2 -- .../resources/projects/simple/pom-complex.xml | 1 - .../projects/simple/pom-no-to-image.xml | 5 ----- .../src/test/resources/projects/simple/pom.xml | 2 -- .../projects/war_servlet25/pom-tomcat.xml | 2 -- .../resources/projects/war_servlet25/pom.xml | 2 -- .../jib/plugins/common/HelpfulSuggestions.java | 4 ++-- .../common/PluginConfigurationProcessor.java | 11 ++++------- .../jib/plugins/common/RawConfiguration.java | 2 -- .../plugins/common/HelpfulSuggestionsTest.java | 2 +- 36 files changed, 39 insertions(+), 105 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index fdbd15da8e..70cac3d346 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed `jib.useOnlyProjectCache` parameter in favor of the `jib.useOnlyProjectCache` system property ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) + ### Fixed ## 0.10.1 diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java index f64f64ccc0..7965c8ac63 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -63,7 +63,8 @@ private static void assertDockerInspect(String imageReference) public void testBuild_defaultTarget() { // Test error when 'to' is missing try { - defaultTargetTestProject.build("clean", "jib", "-x=classes"); + defaultTargetTestProject.build( + "clean", "jib", "-Djib.useOnlyProjectCache=true", "-x=classes"); Assert.fail(); } catch (UnexpectedBuildFailure ex) { Assert.assertThat( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index fc7b0bbf03..57fd144dfa 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -50,7 +50,11 @@ static String buildAndRun( throws IOException, InterruptedException, DigestException { BuildResult buildResult = testProject.build( - "clean", "jib", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-D_TARGET_IMAGE=" + imageReference, + "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); assertImageDigestAndId(testProject.getProjectRoot()); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(imageReference)); @@ -65,6 +69,7 @@ static void buildAndRunAdditionalTag( testProject.build( "clean", "jib", + "-Djib.useOnlyProjectCache=true", "-D_TARGET_IMAGE=" + imageReference, "-D_ADDITIONAL_TAG=" + additionalTag); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); @@ -89,6 +94,7 @@ static void buildToDockerDaemon( testProject.build( "clean", "jibDockerBuild", + "-Djib.useOnlyProjectCache=true", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index a3dc7097b8..26f10755ee 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -112,6 +112,7 @@ private static String buildAndRunComplex( simpleTestProject.build( "clean", "jib", + "-Djib.useOnlyProjectCache=true", "-D_TARGET_IMAGE=" + imageReference, "-D_TARGET_USERNAME=" + username, "-D_TARGET_PASSWORD=" + password, @@ -144,7 +145,12 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE // Test empty output error try { - simpleTestProject.build("clean", "jib", "-x=classes", "-D_TARGET_IMAGE=" + targetImage); + simpleTestProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-x=classes", + "-D_TARGET_IMAGE=" + targetImage); Assert.fail(); } catch (UnexpectedBuildFailure ex) { @@ -205,7 +211,11 @@ public void testBuildTar_simple() throws IOException, InterruptedException { simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); Instant beforeBuild = Instant.now(); BuildResult buildResult = - simpleTestProject.build("clean", "jibBuildTar", "-D_TARGET_IMAGE=" + targetImage); + simpleTestProject.build( + "clean", + "jibBuildTar", + "-Djib.useOnlyProjectCache=true", + "-D_TARGET_IMAGE=" + targetImage); JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(outputPath)); diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle index d8fb81ef62..61b220e3a4 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle @@ -20,7 +20,4 @@ jib { ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle index 054212b5fb..fe514bee06 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle @@ -20,6 +20,4 @@ jib { container { user = 'myuser:mygroup' } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index f7b6cec689..8214cf0aa9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -22,8 +22,6 @@ jib { ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } def additionalTag = System.getProperty("_ADDITIONAL_TAG") diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle index a96169ea32..4f11ab3491 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle @@ -16,6 +16,4 @@ jib { ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] } - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 858a46be96..6b996ee339 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -28,7 +28,4 @@ jib { workingDirectory = '/home' } extraDirectory = file('src/main/custom-extra-dir') - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle index be0653b1af..9a9b9d1fbe 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle @@ -44,7 +44,4 @@ jib { permissions = ['/foo':'755', '/bar/cat':'777'] } allowInsecureRegistries = true - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle index ad6dcb00cf..97065475e5 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle @@ -33,7 +33,4 @@ jib { credHelper = 'gcr' } container.appRoot = '/usr/local/tomcat/webapps/ROOT' - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle index dfded0f525..5960808868 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle @@ -31,7 +31,4 @@ jib { image = System.getProperty("_TARGET_IMAGE") credHelper = 'gcr' } - - // Does not have tests use user-level cache for base image layers. - useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index e8a0b8eb20..9cd0e052d5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -101,6 +101,7 @@ public void buildTar() // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); TaskCommon.disableHttpLogging(); + try { AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index ecd1d42702..6511be1bf8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -132,11 +132,6 @@ public boolean getAllowInsecureRegistries() { return jibExtension.getAllowInsecureRegistries(); } - @Override - public boolean getUseOnlyProjectCache() { - return jibExtension.getUseOnlyProjectCache(); - } - @Override public ImageFormat getImageFormat() { return jibExtension.getContainer().getFormat(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 107f22c383..a2cdf2a5e8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -57,14 +57,12 @@ * ] * } * allowInsecureRegistries = false - * useOnlyProjectCache = false * } * }

*/ public class JibExtension { // Defines default configuration values. - private static final boolean DEFAULT_USE_ONLY_PROJECT_CACHE = false; private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; private final BaseImageParameters from; @@ -72,7 +70,6 @@ public class JibExtension { private final ContainerParameters container; private final ExtraDirectoryParameters extraDirectory; - private final Property useOnlyProjectCache; private final Property allowInsecureRegistries; public JibExtension(Project project) { @@ -84,11 +81,9 @@ public JibExtension(Project project) { extraDirectory = objectFactory.newInstance(ExtraDirectoryParameters.class, project.getProjectDir().toPath()); - useOnlyProjectCache = objectFactory.property(Boolean.class); allowInsecureRegistries = objectFactory.property(Boolean.class); // Sets defaults. - useOnlyProjectCache.set(DEFAULT_USE_ONLY_PROJECT_CACHE); allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); } @@ -116,10 +111,6 @@ public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } - void setUseOnlyProjectCache(boolean useOnlyProjectCache) { - this.useOnlyProjectCache.set(useOnlyProjectCache); - } - @Nested @Optional public BaseImageParameters getFrom() { @@ -144,15 +135,6 @@ public ExtraDirectoryParameters getExtraDirectory() { return extraDirectory; } - @Input - @Optional - boolean getUseOnlyProjectCache() { - if (System.getProperty(PropertyNames.USE_ONLY_PROJECT_CACHE) != null) { - return Boolean.getBoolean(PropertyNames.USE_ONLY_PROJECT_CACHE); - } - return useOnlyProjectCache.get(); - } - @Input @Optional boolean getAllowInsecureRegistries() { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index dced5dbf72..cd77320f0e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -47,7 +47,6 @@ public void testGetters() { Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); Mockito.when(jibExtension.getAllowInsecureRegistries()).thenReturn(true); - Mockito.when(jibExtension.getUseOnlyProjectCache()).thenReturn(true); Mockito.when(baseImageParameters.getCredHelper()).thenReturn("gcr"); Mockito.when(baseImageParameters.getImage()).thenReturn("openjdk:15"); @@ -94,7 +93,6 @@ public void testGetters() { Assert.assertEquals( new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); - Assert.assertTrue(rawConfiguration.getUseOnlyProjectCache()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index f95f080387..734e7ef8f6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -212,12 +212,4 @@ public void testProperties() { System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); } - - @Test - public void testUseOnlyProjectCache() { - Assert.assertFalse(testJibExtension.getUseOnlyProjectCache()); - - testJibExtension.setUseOnlyProjectCache(true); - Assert.assertTrue(testJibExtension.getUseOnlyProjectCache()); - } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index acd7cabdb1..266f804c74 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,8 +8,10 @@ All notable changes to this project will be documented in this file. - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) ### Changed + - `gwt-app` packaging type now builds a WAR container. - When building to Docker and no `` is defined, artifact ID is used as an image reference instead of project name. +- Removed `` parameter in favor of the `jib.useOnlyProjectCache` system property ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 6dede10731..ad39352050 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -223,12 +223,6 @@ public File getPath() { // this parameter is cloned in FilesMojo @Parameter private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); - @Parameter( - defaultValue = "false", - required = true, - property = PropertyNames.USE_ONLY_PROJECT_CACHE) - private boolean useOnlyProjectCache; - @Parameter( defaultValue = "false", required = true, @@ -527,10 +521,6 @@ List getExtraDirectoryPermissions() { return extraDirectory.permissions; } - boolean getUseOnlyProjectCache() { - return useOnlyProjectCache; - } - boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index d47a0dad06..e84dca3b0c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -137,11 +137,6 @@ public boolean getAllowInsecureRegistries() { return jibPluginConfiguration.getAllowInsecureRegistries(); } - @Override - public boolean getUseOnlyProjectCache() { - return jibPluginConfiguration.getUseOnlyProjectCache(); - } - @Override public ImageFormat getImageFormat() { return ImageFormat.valueOf(jibPluginConfiguration.getFormat()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index b7db8ff602..08585e4210 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -46,6 +46,7 @@ public class BuildDockerMojoIntegrationTest { private static void buildToDockerDaemon(Path projectRoot, String imageReference, String pomXml) throws VerificationException, DigestException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); verifier.addCliOption("--file=" + pomXml); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 379c40ac3e..5d1589cd6b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -98,6 +98,7 @@ static String assertImageId(Path projectRoot) throws IOException, DigestExceptio private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) throws VerificationException, IOException, InterruptedException, DigestException { Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); verifier.addCliOption("-X"); @@ -144,6 +145,7 @@ private static String buildAndRunAdditionalTag( throws VerificationException, InvalidImageReferenceException, IOException, InterruptedException, DigestException { Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); verifier.setAutoclean(false); @@ -174,6 +176,7 @@ private static String buildAndRunComplex( throws VerificationException, IOException, InterruptedException, DigestException { Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setSystemProperty("_TARGET_USERNAME", username); verifier.setSystemProperty("_TARGET_PASSWORD", password); @@ -413,6 +416,7 @@ private void buildAndRunWar(String label, String pomXml) String targetImage = getGcrImageReference(label); Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); verifier.addCliOption("-X"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index febe2d8830..4f1d9f454b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -49,6 +49,7 @@ public void testExecute_simple() Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); verifier.setAutoclean(false); verifier.executeGoal("package"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index b5fe9ff2e3..a66f71a4a2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -74,7 +74,6 @@ public void testGetters() { Mockito.when(jibPluginConfiguration.getTargetImageAdditionalTags()) .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); Mockito.when(jibPluginConfiguration.getUseCurrentTimestamp()).thenReturn(true); - Mockito.when(jibPluginConfiguration.getUseOnlyProjectCache()).thenReturn(true); Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); @@ -102,7 +101,6 @@ public void testGetters() { Assert.assertEquals( new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); - Assert.assertTrue(rawConfiguration.getUseOnlyProjectCache()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Mockito.verifyNoMoreInteractions(eventDispatcher); diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml index e738671d1c..05be41b219 100644 --- a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml @@ -53,8 +53,6 @@ /var/log2
- - true
diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml index 5ae90666e2..02872d9742 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml @@ -34,8 +34,6 @@ myuser:mygroup - - true
diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 3f4c4d9bd6..3c29682028 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -47,8 +47,6 @@ /var/log2
- - true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index ec8c8d9eaf..1e304a1ebe 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -94,7 +94,6 @@ true - true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml index 74e84e303e..eef6fd30ae 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml @@ -39,11 +39,6 @@ com.google.cloud.tools jib-maven-plugin ${jib-maven-plugin.version} - - - - true - diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index 598d32c4f0..af682afed8 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -59,8 +59,6 @@ /home
${project.basedir}/src/main/jib-custom - - true diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml index 576fad6506..f8fd908873 100644 --- a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml @@ -52,8 +52,6 @@ /usr/local/tomcat/webapps/ROOT - - true diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml index 06e0ed6e08..6b38a8f882 100644 --- a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml @@ -46,8 +46,6 @@ ${_TARGET_IMAGE} - - true diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index d610bf1979..ff2260a61a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -151,8 +151,8 @@ public String forCacheDirectoryNotOwned(Path cacheDirectory) { return suggest( "check that '" + cacheDirectory - + "' is not used by another application or set the `useOnlyProjectCache` " - + "configuration"); + + "' is not used by another application or set the `jib.useOnlyProjectCache` system " + + "property"); } public String forHttpStatusCodeForbidden(String imageReference) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index aba6c81dac..ce6c604ca3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -422,18 +422,15 @@ private static void configureContainerizer( .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) .setBaseImageLayersCache( getCheckedCacheDirectory( - PropertyNames.BASE_IMAGE_CACHE, Containerizer.DEFAULT_BASE_CACHE_DIRECTORY)) + PropertyNames.BASE_IMAGE_CACHE, + Boolean.getBoolean(PropertyNames.USE_ONLY_PROJECT_CACHE) + ? projectProperties.getDefaultCacheDirectory() + : Containerizer.DEFAULT_BASE_CACHE_DIRECTORY)) .setApplicationLayersCache( getCheckedCacheDirectory( PropertyNames.APPLICATION_CACHE, projectProperties.getDefaultCacheDirectory())); rawConfiguration.getToTags().forEach(containerizer::withAdditionalTag); - - if (rawConfiguration.getUseOnlyProjectCache()) { - containerizer.setBaseImageLayersCache( - getCheckedCacheDirectory( - PropertyNames.BASE_IMAGE_CACHE, projectProperties.getDefaultCacheDirectory())); - } } /** diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index e78a9c4dd3..a20772cb7e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -67,7 +67,5 @@ public interface RawConfiguration { boolean getAllowInsecureRegistries(); - boolean getUseOnlyProjectCache(); - ImageFormat getImageFormat(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 596e72519e..96726ec98a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -52,7 +52,7 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should run 'clearCacheCommand' to clear your build cache", TEST_HELPFUL_SUGGESTIONS.forCacheNeedsClean()); Assert.assertEquals( - "messagePrefix, perhaps you should check that 'cacheDirectory' is not used by another application or set the `useOnlyProjectCache` configuration", + "messagePrefix, perhaps you should check that 'cacheDirectory' is not used by another application or set the `jib.useOnlyProjectCache` system property", TEST_HELPFUL_SUGGESTIONS.forCacheDirectoryNotOwned(Paths.get("cacheDirectory"))); Assert.assertEquals( "messagePrefix, perhaps you should make sure you have permissions for imageReference", From be3b39f563d279d9a9eab57399d6971e3c76a503 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Dec 2018 12:42:07 -0500 Subject: [PATCH 0384/2020] Adds AsyncDependencies to reduce boilerplate code. (#1320) --- .../tools/jib/async/AsyncDependencies.java | 83 +++++++++++++++++++ .../builder/steps/AuthenticatePushStep.java | 7 +- .../jib/builder/steps/BuildImageStep.java | 26 ++---- .../jib/builder/steps/FinalizingStep.java | 36 ++++---- .../jib/builder/steps/LoadDockerStep.java | 26 +++--- .../PullAndCacheBaseImageLayersStep.java | 6 +- .../tools/jib/builder/steps/PushBlobStep.java | 7 +- .../steps/PushContainerConfigurationStep.java | 13 +-- .../jib/builder/steps/PushImageStep.java | 50 +++++------ .../jib/builder/steps/PushLayersStep.java | 5 +- .../jib/builder/steps/WriteTarFileStep.java | 26 +++--- 11 files changed, 172 insertions(+), 113 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java new file mode 100644 index 0000000000..b562282bc1 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.async; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Builds a list of dependency {@link ListenableFuture}s to wait on before calling a {@link + * Callable}. + */ +public class AsyncDependencies { + + /** + * Initialize with a {@link ListeningExecutorService}. + * + * @param listeningExecutorService the {@link ListeningExecutorService} + * @return a new {@link AsyncDependencies} + */ + public static AsyncDependencies using(ListeningExecutorService listeningExecutorService) { + return new AsyncDependencies(listeningExecutorService); + } + + private final ListeningExecutorService listeningExecutorService; + + /** Stores the list of {@link ListenableFuture}s to wait on. */ + private final List> futures = new ArrayList<>(); + + private AsyncDependencies(ListeningExecutorService listeningExecutorService) { + this.listeningExecutorService = listeningExecutorService; + } + + /** + * Adds the future of an {@link AsyncStep}. + * + * @param asyncStep the {@link AsyncStep} + * @return this + */ + public AsyncDependencies addStep(AsyncStep asyncStep) { + futures.add(asyncStep.getFuture()); + return this; + } + + /** + * Adds the futures of a list of {@link AsyncStep}s. + * + * @param asyncSteps the {@link AsyncStep}s + * @return this + */ + public AsyncDependencies addSteps(List> asyncSteps) { + asyncSteps.forEach(this::addStep); + return this; + } + + /** + * Calls {@code combiner} when all the added futures succeed. + * + * @param combiner the {@link Callable} + * @param the return type of {@code combiner} + * @return a {@link ListenableFuture} to handle completion of the call to {@code combiner} + */ + public ListenableFuture whenAllSucceed(Callable combiner) { + return Futures.whenAllSucceed(futures).call(combiner, listeningExecutorService); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index fa1c4b3547..85fd167737 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -28,7 +29,6 @@ import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; @@ -63,8 +63,9 @@ class AuthenticatePushStep implements AsyncStep, Callable>> getFuture() { @Override public AsyncStep> call() throws ExecutionException { - List> dependencies = new ArrayList<>(); - - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - dependencies.add(pullAndCacheBaseImageLayerStep.getFuture()); - } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - dependencies.add(buildAndCacheApplicationLayerStep.getFuture()); - } ListenableFuture> future = - Futures.whenAllSucceed(dependencies) - .call(this::afterCachedLayerSteps, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) + .addSteps(buildAndCacheApplicationLayerSteps) + .whenAllSucceed(this::afterCachedLayerSteps); return () -> future; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index 5656f46e54..5dce1eea73 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -16,15 +16,14 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -53,16 +52,11 @@ class FinalizingStep implements AsyncStep, Callable { this.buildConfiguration = buildConfiguration; this.futureDependencyLists = wrappedDependencyLists; - List> dependenciesFutures = - new ArrayList<>(wrappedDependencyLists.size() + dependencyList.size()); - for (AsyncStep dependency : wrappedDependencyLists) { - dependenciesFutures.add(dependency.getFuture()); - } - for (AsyncStep dependency : dependencyList) { - dependenciesFutures.add(dependency.getFuture()); - } listenableFuture = - Futures.whenAllSucceed(dependenciesFutures).call(this, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addSteps(wrappedDependencyLists) + .addSteps(dependencyList) + .whenAllSucceed(this); } @Override @@ -72,22 +66,20 @@ public ListenableFuture getFuture() { @Override public Void call() throws ExecutionException { + AsyncDependencies asyncDependencies = AsyncDependencies.using(listeningExecutorService); + // Unwrap the wrapped dependencies. - List> unwrappedDependencies = new ArrayList<>(); for (AsyncStep>> wrappedDependency : futureDependencyLists) { - for (AsyncStep unwrappedDependency : NonBlockingSteps.get(wrappedDependency)) { - unwrappedDependencies.add(unwrappedDependency.getFuture()); - } + asyncDependencies.addSteps(NonBlockingSteps.get(wrappedDependency)); } - Futures.whenAllSucceed(unwrappedDependencies) - .call( - () -> { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); - return null; - }, - listeningExecutorService); + // TODO: Don't let future error be suppressed + asyncDependencies.whenAllSucceed( + () -> { + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); + return null; + }); return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 7749e96106..de87d5ea7d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -28,7 +29,6 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; @@ -67,9 +67,10 @@ class LoadDockerStep implements AsyncStep, Callable { this.buildImageStep = buildImageStep; listenableFuture = - Futures.whenAllSucceed( - pullAndCacheBaseImageLayersStep.getFuture(), buildImageStep.getFuture()) - .call(this, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addStep(pullAndCacheBaseImageLayersStep) + .addStep(buildImageStep) + .whenAllSucceed(this); } @Override @@ -79,18 +80,11 @@ public ListenableFuture getFuture() { @Override public BuildResult call() throws ExecutionException, InterruptedException { - ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - dependenciesBuilder.add(pullAndCacheBaseImageLayerStep.getFuture()); - } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - dependenciesBuilder.add(buildAndCacheApplicationLayerStep.getFuture()); - } - dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); - return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::afterPushBaseImageLayerFuturesFuture, listeningExecutorService) + return AsyncDependencies.using(listeningExecutorService) + .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) + .addSteps(buildAndCacheApplicationLayerSteps) + .addStep(NonBlockingSteps.get(buildImageStep)) + .whenAllSucceed(this::afterPushBaseImageLayerFuturesFuture) .get(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 9d6421e143..cc6ebb3a94 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -26,7 +27,6 @@ import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.concurrent.Callable; @@ -58,7 +58,9 @@ class PullAndCacheBaseImageLayersStep this.pullBaseImageStep = pullBaseImageStep; listenableFuture = - Futures.whenAllSucceed(pullBaseImageStep.getFuture()).call(this, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addStep(pullBaseImageStep) + .whenAllSucceed(this); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index a1cef8154f..0a89da9817 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; @@ -27,7 +28,6 @@ import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; @@ -62,8 +62,9 @@ class PushBlobStep implements AsyncStep, Callable> getFuture() { @Override public AsyncStep call() throws ExecutionException { ListenableFuture pushBlobStepFuture = - Futures.whenAllSucceed( - authenticatePushStep.getFuture(), NonBlockingSteps.get(buildImageStep).getFuture()) - .call(this::afterBuildConfigurationFutureFuture, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addStep(authenticatePushStep) + .addStep(NonBlockingSteps.get(buildImageStep)) + .whenAllSucceed(this::afterBuildConfigurationFutureFuture); return () -> pushBlobStepFuture; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index f684c3abf7..ed8e9efb3c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; @@ -29,7 +30,6 @@ import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -77,11 +77,11 @@ class PushImageStep implements AsyncStep, Callable { this.buildImageStep = buildImageStep; listenableFuture = - Futures.whenAllSucceed( - pushBaseImageLayersStep.getFuture(), - pushApplicationLayersStep.getFuture(), - pushContainerConfigurationStep.getFuture()) - .call(this, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addStep(pushBaseImageLayersStep) + .addStep(pushApplicationLayersStep) + .addStep(pushContainerConfigurationStep) + .whenAllSucceed(this); } @Override @@ -91,19 +91,13 @@ public ListenableFuture getFuture() { @Override public BuildResult call() throws ExecutionException, InterruptedException { - ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); - dependenciesBuilder.add(authenticatePushStep.getFuture()); - for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { - dependenciesBuilder.add(pushBlobStepStep.getFuture()); - } - for (AsyncStep pushBlobStepStep : - NonBlockingSteps.get(pushApplicationLayersStep)) { - dependenciesBuilder.add(pushBlobStepStep.getFuture()); - } - dependenciesBuilder.add(NonBlockingSteps.get(pushContainerConfigurationStep).getFuture()); - dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); - return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::afterPushSteps, listeningExecutorService) + return AsyncDependencies.using(listeningExecutorService) + .addStep(authenticatePushStep) + .addSteps(NonBlockingSteps.get(pushBaseImageLayersStep)) + .addSteps(NonBlockingSteps.get(pushApplicationLayersStep)) + .addStep(NonBlockingSteps.get(pushContainerConfigurationStep)) + .addStep(NonBlockingSteps.get(buildImageStep)) + .whenAllSucceed(this::afterPushSteps) .get() .get() .get(); @@ -111,18 +105,18 @@ public BuildResult call() throws ExecutionException, InterruptedException { private ListenableFuture> afterPushSteps() throws ExecutionException { - List> dependencies = new ArrayList<>(); - for (AsyncStep pushBlobStepStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { - dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); + AsyncDependencies dependencies = AsyncDependencies.using(listeningExecutorService); + for (AsyncStep pushBaseImageLayerStep : + NonBlockingSteps.get(pushBaseImageLayersStep)) { + dependencies.addStep(NonBlockingSteps.get(pushBaseImageLayerStep)); } - for (AsyncStep pushBlobStepStep : + for (AsyncStep pushApplicationLayerStep : NonBlockingSteps.get(pushApplicationLayersStep)) { - dependencies.add(NonBlockingSteps.get(pushBlobStepStep).getFuture()); + dependencies.addStep(NonBlockingSteps.get(pushApplicationLayerStep)); } - dependencies.add( - NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep)).getFuture()); - return Futures.whenAllSucceed(dependencies) - .call(this::afterAllPushed, listeningExecutorService); + return dependencies + .addStep(NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep))) + .whenAllSucceed(this::afterAllPushed); } private ListenableFuture afterAllPushed() throws ExecutionException, IOException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index bdac0023e3..7d013fe1f1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; @@ -61,7 +62,9 @@ class PushLayersStep this.cachedLayerStep = cachedLayerStep; listenableFuture = - Futures.whenAllSucceed(cachedLayerStep.getFuture()).call(this, listeningExecutorService); + AsyncDependencies.using(listeningExecutorService) + .addStep(cachedLayerStep) + .whenAllSucceed(this); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index dbcbab188e..c5578823fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -27,7 +28,6 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.BufferedOutputStream; @@ -68,9 +68,10 @@ public class WriteTarFileStep implements AsyncStep, Callable getFuture() { @Override public BuildResult call() throws ExecutionException, InterruptedException { - ImmutableList.Builder> dependenciesBuilder = ImmutableList.builder(); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) { - dependenciesBuilder.add(pullAndCacheBaseImageLayerStep.getFuture()); - } - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { - dependenciesBuilder.add(buildAndCacheApplicationLayerStep.getFuture()); - } - dependenciesBuilder.add(NonBlockingSteps.get(buildImageStep).getFuture()); - return Futures.whenAllSucceed(dependenciesBuilder.build()) - .call(this::writeTarFile, listeningExecutorService) + return AsyncDependencies.using(listeningExecutorService) + .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) + .addSteps(buildAndCacheApplicationLayerSteps) + .addStep(NonBlockingSteps.get(buildImageStep)) + .whenAllSucceed(this::writeTarFile) .get(); } From e4dcca62c49a6c7afb17ca630bdd788160f70e04 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Dec 2018 12:42:35 -0500 Subject: [PATCH 0385/2020] Change RegistryClient push/pull to take callback to listen on progress. (#1323) --- .../registry/BlobPullerIntegrationTest.java | 19 +++++++++-- .../registry/BlobPusherIntegrationTest.java | 2 +- .../ManifestPusherIntegrationTest.java | 9 +++-- .../steps/PullAndCacheBaseImageLayerStep.java | 8 ++++- .../jib/builder/steps/PullBaseImageStep.java | 5 ++- .../tools/jib/builder/steps/PushBlobStep.java | 8 ++++- .../cloud/tools/jib/http/BlobHttpContent.java | 15 ++++++-- .../http/ListenableCountingOutputStream.java | 4 +-- .../cloud/tools/jib/registry/BlobPuller.java | 18 ++++++++-- .../cloud/tools/jib/registry/BlobPusher.java | 12 ++++--- .../tools/jib/registry/ManifestPusher.java | 6 +++- .../tools/jib/registry/RegistryClient.java | 30 ++++++++++++---- .../cloud/tools/jib/http/ConnectionTest.java | 11 ++++-- .../tools/jib/registry/BlobPullerTest.java | 18 ++++++++-- .../tools/jib/registry/BlobPusherTest.java | 34 ++++++++++++------- 15 files changed, 157 insertions(+), 42 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 3529608f1b..b5672616c4 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -23,6 +23,7 @@ import com.google.common.io.ByteStreams; import java.io.IOException; import java.security.DigestException; +import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.ClassRule; @@ -54,7 +55,19 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls a layer BLOB of the busybox image. CountingDigestOutputStream layerOutputStream = new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - registryClient.pullBlob(realDigest).writeTo(layerOutputStream); + LongAdder byteCount = new LongAdder(); + LongAdder expectedSize = new LongAdder(); + registryClient + .pullBlob( + realDigest, + size -> { + Assert.assertEquals(0, expectedSize.sum()); + expectedSize.add(size); + }, + byteCount::add) + .writeTo(layerOutputStream); + Assert.assertTrue(expectedSize.sum() > 0); + Assert.assertEquals(expectedSize.sum(), byteCount.sum()); Assert.assertEquals(realDigest, layerOutputStream.toBlobDescriptor().getDigest()); } @@ -72,7 +85,9 @@ public void testPull_unknownBlob() throws IOException, DigestException, Interrup .newRegistryClient(); try { - registryClient.pullBlob(nonexistentDigest).writeTo(ByteStreams.nullOutputStream()); + registryClient + .pullBlob(nonexistentDigest, ignored -> {}, ignored -> {}) + .writeTo(ByteStreams.nullOutputStream()); Assert.fail("Trying to pull nonexistent blob should have errored"); } catch (IOException ex) { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index f356e57ac0..fb737ae18a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -46,6 +46,6 @@ public void testPush() RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null)); + Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 432b5ea7d9..cdb083290f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -86,10 +86,14 @@ public void testPush() RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null)); + Assert.assertFalse( + registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); Assert.assertFalse( registryClient.pushBlob( - testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null)); + testContainerConfigurationBlobDigest, + testContainerConfigurationBlob, + null, + ignored -> {})); // Pushes the manifest. DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); @@ -99,6 +103,7 @@ public void testPush() registryClient.pullManifest("latest", V22ManifestTemplate.class); Assert.assertEquals(1, manifestTemplate.getLayers().size()); Assert.assertEquals(testLayerBlobDigest, manifestTemplate.getLayers().get(0).getDigest()); + Assert.assertNotNull(manifestTemplate.getContainerConfiguration()); Assert.assertEquals( testContainerConfigurationBlobDigest, manifestTemplate.getContainerConfiguration().getDigest()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 4e3c9f7381..97bef35800 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -89,7 +89,13 @@ public CachedLayer call() throws IOException, CacheCorruptedException { .newBaseImageRegistryClientFactory() .setAuthorization(pullAuthorization) .newRegistryClient(); - CachedLayer cachedLayer = cache.writeCompressedLayer(registryClient.pullBlob(layerDigest)); + CachedLayer cachedLayer = + cache.writeCompressedLayer( + registryClient.pullBlob( + layerDigest, + // TODO: Replace with progress-reporting. + alsoIgnored -> {}, + alsoIgnored -> {})); buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 37599aba26..39b37aabff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -242,7 +242,10 @@ private Image pullBaseImage(@Nullable Authorization registryAuthorization String containerConfigurationString = Blobs.writeToString( registryClient.pullBlob( - v22ManifestTemplate.getContainerConfiguration().getDigest())); + v22ManifestTemplate.getContainerConfiguration().getDigest(), + // TODO: Replace with progress updates. + ignored -> {}, + ignored -> {})); ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJson( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 0a89da9817..08274d7400 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -97,7 +97,13 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc } // todo: leverage cross-repository mounts - registryClient.pushBlob(blobDescriptor.getDigest(), blob, null); + registryClient.pushBlob( + blobDescriptor.getDigest(), + blob, + null, + alsoIgnored -> { + // TODO: Replace with progress-reporting. + }); buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java index b61dd9a96a..de860c63e0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java @@ -20,16 +20,22 @@ import com.google.cloud.tools.jib.blob.Blob; import java.io.IOException; import java.io.OutputStream; +import java.time.Duration; +import java.util.function.Consumer; /** {@link Blob}-backed {@link HttpContent}. */ public class BlobHttpContent implements HttpContent { private final Blob blob; private final String contentType; + // TODO: Refactor into BlobPushMonitor or something. + private final Consumer sentByteCountConsumer; + private final Duration delayBetweenCallbacks = Duration.ofMillis(100); - public BlobHttpContent(Blob blob, String contentType) { + public BlobHttpContent(Blob blob, String contentType, Consumer sentByteCountConsumer) { this.blob = blob; this.contentType = contentType; + this.sentByteCountConsumer = sentByteCountConsumer; } @Override @@ -50,7 +56,10 @@ public boolean retrySupported() { @Override public void writeTo(OutputStream outputStream) throws IOException { - blob.writeTo(outputStream); - outputStream.flush(); + ListenableCountingOutputStream listenableCountingOutputStream = + new ListenableCountingOutputStream( + outputStream, sentByteCountConsumer, delayBetweenCallbacks); + blob.writeTo(listenableCountingOutputStream); + listenableCountingOutputStream.flush(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java index 5de0164620..fcc77470a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java @@ -28,7 +28,7 @@ * Counts the number of bytes written and reports the count to a callback. The count is reported * with certain time delays to avoid calling the callback too often. */ -class ListenableCountingOutputStream extends OutputStream { +public class ListenableCountingOutputStream extends OutputStream { /** The underlying {@link OutputStream} to wrap and forward bytes to. */ private final OutputStream underlyingOutputStream; @@ -55,7 +55,7 @@ class ListenableCountingOutputStream extends OutputStream { * @param byteCountConsumer the byte count {@link Consumer} * @param delayBetweenCallbacks the minimum delay between each call to {@link #byteCountConsumer} */ - ListenableCountingOutputStream( + public ListenableCountingOutputStream( OutputStream underlyingOutputStream, Consumer byteCountConsumer, Duration delayBetweenCallbacks) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index 1dccf926b2..d2019320dc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -19,14 +19,17 @@ import com.google.api.client.http.HttpMethods; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.ListenableCountingOutputStream; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; +import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; /** Pulls an image's BLOB (layer or container configuration). */ @@ -42,17 +45,28 @@ class BlobPuller implements RegistryEndpointProvider { */ private final OutputStream destinationOutputStream; + // TODO: Refactor into BlobPullMonitor. + private final Consumer sizeConsumer; + private final Duration delayBetweenCallbacks = Duration.ofMillis(100); + BlobPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, - OutputStream destinationOutputStream) { + OutputStream destinationOutputStream, + Consumer sizeConsumer, + Consumer receivedByteCountConsumer) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; - this.destinationOutputStream = destinationOutputStream; + this.destinationOutputStream = + new ListenableCountingOutputStream( + destinationOutputStream, receivedByteCountConsumer, delayBetweenCallbacks); + this.sizeConsumer = sizeConsumer; } @Override public Void handleResponse(Response response) throws IOException, UnexpectedBlobDigestException { + sizeConsumer.accept(response.getContentLength()); + try (OutputStream outputStream = destinationOutputStream) { BlobDescriptor receivedBlobDescriptor = response.getBody().writeTo(outputStream); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 5316ff0268..2084ac140b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -28,6 +28,7 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; /** @@ -111,11 +112,12 @@ public String getActionDescription() { private class Writer implements RegistryEndpointProvider { private final URL location; + private final Consumer sentByteCountConsumer; @Nullable @Override public BlobHttpContent getContent() { - return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString()); + return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), sentByteCountConsumer); } @Override @@ -145,8 +147,9 @@ public String getActionDescription() { return BlobPusher.this.getActionDescription(); } - private Writer(URL location) { + private Writer(URL location, Consumer sentByteCountConsumer) { this.location = location; + this.sentByteCountConsumer = sentByteCountConsumer; } } @@ -213,10 +216,11 @@ RegistryEndpointProvider initializer() { /** * @param location the upload URL + * @param sentByteCountConsumer callback to receive counts of bytes sent * @return a {@link RegistryEndpointProvider} for writing the BLOB to an upload location */ - RegistryEndpointProvider writer(URL location) { - return new Writer(location); + RegistryEndpointProvider writer(URL location, Consumer sentByteCountConsumer) { + return new Writer(location, sentByteCountConsumer); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 540b5e99d5..726ff41624 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -82,7 +82,11 @@ private static String makeUnexpectedImageDigestWarning( @Override public BlobHttpContent getContent() { return new BlobHttpContent( - JsonTemplateMapper.toBlob(manifestTemplate), manifestTemplate.getManifestMediaType()); + JsonTemplateMapper.toBlob(manifestTemplate), + manifestTemplate.getManifestMediaType(), + ignored -> { + // TODO: Consider giving progress on manifest push as well? + }); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 9cdd705447..7e759e8295 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -34,6 +34,7 @@ import com.google.common.base.Verify; import java.io.IOException; import java.net.URL; +import java.util.function.Consumer; import javax.annotation.Nullable; /** Interfaces with a registry. */ @@ -244,15 +245,25 @@ public BlobDescriptor checkBlob(DescriptorDigest blobDigest) * written out. * * @param blobDigest the digest of the BLOB to download - * @return a {@link Blob} backed by the file at {@code destPath}. The file at {@code destPath} - * must exist for {@link Blob} to be valid. + * @param blobSizeConsumer callback to receive the total size of the BLOB to pull + * @param receivedByteCountConsumer callback to receive counts of bytes received + * @return a {@link Blob} */ - public Blob pullBlob(DescriptorDigest blobDigest) { + // TODO: Refactor callbacks into BlobPullMonitor. + public Blob pullBlob( + DescriptorDigest blobDigest, + Consumer blobSizeConsumer, + Consumer receivedByteCountConsumer) { return Blobs.from( outputStream -> { try { callRegistryEndpoint( - new BlobPuller(registryEndpointRequestProperties, blobDigest, outputStream)); + new BlobPuller( + registryEndpointRequestProperties, + blobDigest, + outputStream, + blobSizeConsumer, + receivedByteCountConsumer)); } catch (RegistryException ex) { throw new IOException(ex); @@ -268,12 +279,18 @@ public Blob pullBlob(DescriptorDigest blobDigest) { * @param blob the BLOB to push * @param sourceRepository if pushing to the same registry then the source image, or {@code null} * otherwise; used to optimize the BLOB push + * @param sentByteCountConsumer callback to receive counts of bytes sent * @return {@code true} if the BLOB already exists on the registry and pushing was skipped; false * if the BLOB was pushed * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String sourceRepository) + // TODO: Refactor callbacks into BlobPushMonitor. + public boolean pushBlob( + DescriptorDigest blobDigest, + Blob blob, + @Nullable String sourceRepository, + Consumer sentByteCountConsumer) throws IOException, RegistryException { BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); @@ -294,7 +311,8 @@ public boolean pushBlob(DescriptorDigest blobDigest, Blob blob, @Nullable String timerEventDispatcher2.lap("pushBlob PATCH " + blobDigest); // PATCH with BLOB - URL putLocation = callRegistryEndpoint(blobPusher.writer(patchLocation)); + URL putLocation = + callRegistryEndpoint(blobPusher.writer(patchLocation, sentByteCountConsumer)); Preconditions.checkNotNull(putLocation); timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index 99e6e9946b..8b08508524 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -41,7 +41,7 @@ public class ConnectionTest { @FunctionalInterface - private static interface SendFunction { + private interface SendFunction { Response send(Connection connection, Request request) throws IOException; } @@ -58,6 +58,8 @@ private static interface SendFunction { private Request fakeRequest; private HttpResponse mockHttpResponse; + private long byteCount = 0; + @InjectMocks private final Connection testConnection = Connection.getConnectionFactory().apply(fakeUrl.toURL()); @@ -107,7 +109,11 @@ private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { Request.builder() .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) .setUserAgent("fake user agent") - .setBody(new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type")) + .setBody( + new BlobHttpContent( + Blobs.from("crepecake"), + "fake.content.type", + sentByteCount -> byteCount += sentByteCount)) .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) .setHttpTimeout(httpTimeout) .build(); @@ -153,5 +159,6 @@ private void testSend(String httpMethod, SendFunction sendFunction) throws IOExc Assert.assertEquals( "crepecake", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals("crepecake".length(), byteCount); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index b3ed09c768..cab84ce540 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -28,6 +28,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.DigestException; +import java.util.concurrent.atomic.LongAdder; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -56,7 +57,12 @@ public void setUpFakes() throws DigestException { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); testBlobPuller = - new BlobPuller(fakeRegistryEndpointRequestProperties, fakeDigest, layerOutputStream); + new BlobPuller( + fakeRegistryEndpointRequestProperties, + fakeDigest, + layerOutputStream, + ignored -> {}, + ignored -> {}); } @Test @@ -65,15 +71,23 @@ public void testHandleResponse() throws IOException, UnexpectedBlobDigestExcepti DescriptorDigest testBlobDigest = testBlob.writeTo(ByteStreams.nullOutputStream()).getDigest(); Response mockResponse = Mockito.mock(Response.class); + Mockito.when(mockResponse.getContentLength()).thenReturn((long) "some BLOB content".length()); Mockito.when(mockResponse.getBody()).thenReturn(testBlob); + LongAdder byteCount = new LongAdder(); BlobPuller blobPuller = - new BlobPuller(fakeRegistryEndpointRequestProperties, testBlobDigest, layerOutputStream); + new BlobPuller( + fakeRegistryEndpointRequestProperties, + testBlobDigest, + layerOutputStream, + size -> Assert.assertEquals("some BLOB content".length(), size.longValue()), + byteCount::add); blobPuller.handleResponse(mockResponse); Assert.assertEquals( "some BLOB content", new String(layerContentOutputStream.toByteArray(), StandardCharsets.UTF_8)); Assert.assertEquals(testBlobDigest, layerOutputStream.toBlobDescriptor().getDigest()); + Assert.assertEquals("some BLOB content".length(), byteCount.sum()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index e24444aba1..2c44c32d63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -18,16 +18,19 @@ import com.google.api.client.http.GenericUrl; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.DigestException; import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -41,7 +44,9 @@ @RunWith(MockitoJUnitRunner.class) public class BlobPusherTest { - @Mock private Blob mockBlob; + private static final String TEST_BLOB_CONTENT = "some BLOB content"; + private static final Blob TEST_BLOB = Blobs.from(TEST_BLOB_CONTENT); + @Mock private URL mockURL; @Mock private Response mockResponse; @@ -57,7 +62,7 @@ public void setUpFakes() throws DigestException { new BlobPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeDescriptorDigest, - mockBlob, + TEST_BLOB, null); } @@ -132,7 +137,7 @@ public void testInitializer_getApiRoute_sameSource() throws MalformedURLExceptio new BlobPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeDescriptorDigest, - mockBlob, + TEST_BLOB, "sourceImageName"); Assert.assertEquals( @@ -157,18 +162,23 @@ public void testInitializer_getActionDescription() { @Test public void testWriter_getContent() throws IOException { - BlobHttpContent body = testBlobPusher.writer(mockURL).getContent(); + LongAdder byteCount = new LongAdder(); + BlobHttpContent body = testBlobPusher.writer(mockURL, byteCount::add).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); - body.writeTo(ByteStreams.nullOutputStream()); - Mockito.verify(mockBlob).writeTo(ByteStreams.nullOutputStream()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + body.writeTo(byteArrayOutputStream); + + Assert.assertEquals( + TEST_BLOB_CONTENT, new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(TEST_BLOB_CONTENT.length(), byteCount.sum()); } @Test public void testWriter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.writer(mockURL).getAccept().size()); + Assert.assertEquals(0, testBlobPusher.writer(mockURL, ignored -> {}).getAccept().size()); } @Test @@ -179,25 +189,25 @@ public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), - testBlobPusher.writer(mockURL).handleResponse(mockResponse)); + testBlobPusher.writer(mockURL, ignored -> {}).handleResponse(mockResponse)); } @Test public void testWriter_getApiRoute() throws MalformedURLException { URL fakeUrl = new URL("http://someurl"); - Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl).getApiRoute("")); + Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl, ignored -> {}).getApiRoute("")); } @Test public void testWriter_getHttpMethod() { - Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL).getHttpMethod()); + Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL, ignored -> {}).getHttpMethod()); } @Test public void testWriter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.writer(mockURL).getActionDescription()); + testBlobPusher.writer(mockURL, ignored -> {}).getActionDescription()); } @Test From 3ed22cff3f7c190ebe1d1641cbef9e6f329463bd Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 10 Dec 2018 18:23:25 -0500 Subject: [PATCH 0386/2020] Adds ProgressDisplayGenerator. (#1333) --- .../common/ProgressDisplayGenerator.java | 130 ++++++++++++++++++ .../common/ProgressDisplayGeneratorTest.java | 71 ++++++++++ 2 files changed, 201 insertions(+) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java new file mode 100644 index 0000000000..be1c9b5ef8 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java @@ -0,0 +1,130 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.event.progress.Allocation; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Generates a display of progress and unfinished tasks. + * + *

Example: + * + *

Executing tasks...
+ * [================= ] 72.5% complete
+ * > task 1 running
+ * > task 3 running + */ +class ProgressDisplayGenerator { + + /** Line above progress bar. */ + private static final String HEADER = "Executing tasks:"; + + /** Maximum number of bars in the progress display. */ + private static final int PROGRESS_BAR_COUNT = 50; + + /** + * Generates a progress display. + * + * @param progress the overall progress, with {@code 1.0} meaning fully complete + * @param unfinishedAllocations the unfinished {@link Allocation}s + * @return the progress display as a list of lines + */ + static List generateProgressDisplay( + double progress, List unfinishedAllocations) { + List lines = new ArrayList<>(); + + lines.add(HEADER); + lines.add(generateProgressBar(progress)); + lines.addAll(generateUnfinishedTasks(unfinishedAllocations)); + + return lines; + } + + /** + * Generates the progress bar line. + * + * @param progress the overall progress, with {@code 1.0} meaning fully complete + * @return the progress bar line + */ + private static String generateProgressBar(double progress) { + StringBuilder progressBar = new StringBuilder(); + progressBar.append('['); + + int barsToDisplay = (int) Math.round(PROGRESS_BAR_COUNT * progress); + for (int barIndex = 0; barIndex < PROGRESS_BAR_COUNT; barIndex++) { + progressBar.append(barIndex < barsToDisplay ? '=' : ' '); + } + + return progressBar + .append(']') + .append(String.format(" %.1f", progress * 100)) + .append("% complete") + .toString(); + } + + /** + * Generates the display of the unfinished tasks from a list of unfinished {@link Allocation}s + * + * @param unfinishedAllocations the list of unfinished {@link Allocation}s + * @return the display of the unfinished {@link Allocation}s + */ + private static List generateUnfinishedTasks(List unfinishedAllocations) { + List lines = new ArrayList<>(); + for (Allocation unfinishedAllocation : getLeafAllocations(unfinishedAllocations)) { + lines.add("> " + unfinishedAllocation.getDescription()); + } + return lines; + } + + /** + * Gets a list of just the leaf {@link Allocation}s in {@code unfinishedAllocations} in the same + * order as they appear in {@code unfinishedAllocations}. + * + * @param unfinishedAllocations the list of unfinished {@link Allocation}s + * @return the list of unfinished {@link Allocation}s + */ + // TODO: Optimization: Change AllocationCompletionTracker#getUnfinishedAllocations to only return + // leaf Allocations so that this computation is unnecessary. + private static List getLeafAllocations(List unfinishedAllocations) { + // Prunes the set of all unfinished Allocations to leave just the leaves. + Set leafAllocationSet = new HashSet<>(unfinishedAllocations); + for (Allocation allocation : unfinishedAllocations) { + Optional parent = allocation.getParent(); + + while (parent.isPresent()) { + leafAllocationSet.remove(parent.get()); + parent = parent.get().getParent(); + } + } + + // Makes a list of leaf allocations in the same order as the unfinishedAllocations. + List leafAllocations = new ArrayList<>(); + for (Allocation allocation : unfinishedAllocations) { + if (leafAllocationSet.contains(allocation)) { + leafAllocations.add(allocation); + } + } + return leafAllocations; + } + + private ProgressDisplayGenerator() {} +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java new file mode 100644 index 0000000000..2977a8d46d --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.event.progress.Allocation; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ProgressDisplayGenerator}. */ +public class ProgressDisplayGeneratorTest { + + @Test + public void testGenerateProgressDisplay_progressBar_0() { + Assert.assertEquals( + Arrays.asList( + "Executing tasks:", + "[ ] 0.0% complete"), + ProgressDisplayGenerator.generateProgressDisplay(0, Collections.emptyList())); + } + + @Test + public void testGenerateProgressDisplay_progressBar_50() { + Assert.assertEquals( + Arrays.asList( + "Executing tasks:", + "[========================= ] 50.0% complete"), + ProgressDisplayGenerator.generateProgressDisplay(0.5, Collections.emptyList())); + } + + @Test + public void testGenerateProgressDisplay_progressBar_100() { + Assert.assertEquals( + Arrays.asList( + "Executing tasks:", + "[==================================================] 100.0% complete"), + ProgressDisplayGenerator.generateProgressDisplay(1, Collections.emptyList())); + } + + @Test + public void testGenerateProgressDisplay_unfinishedTasks() { + Allocation root = Allocation.newRoot("does not display", 2); + Allocation childLeft = root.newChild("does not display", 2); + Allocation childLeftDown = childLeft.newChild("childLeftDown", 2); + Allocation childRight = root.newChild("childRight", 2); + + Assert.assertEquals( + Arrays.asList( + "Executing tasks:", + "[========================= ] 50.0% complete", + "> childLeftDown", + "> childRight"), + ProgressDisplayGenerator.generateProgressDisplay( + 0.5, Arrays.asList(root, childLeft, childLeftDown, childRight))); + } +} From d21f7d8c786071f1a2018a784af9842118d72c1d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 11 Dec 2018 11:01:46 -0500 Subject: [PATCH 0387/2020] Ensure internally-created executor service is shutdown on completion (#1331) --- .../tools/jib/api/JibIntegrationTest.java | 24 ++++ .../builder/BuildStepsIntegrationTest.java | 12 +- .../tools/jib/api/JibContainerBuilder.java | 40 +++++-- .../jib/configuration/BuildConfiguration.java | 9 +- .../jib/api/JibContainerBuilderTest.java | 109 +++++++++++++++++- .../RetrieveRegistryCredentialsStepTest.java | 2 + .../configuration/BuildConfigurationTest.java | 12 +- .../api/JibContainerBuilderTestHelper.java | 5 +- 8 files changed, 192 insertions(+), 21 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index abe0ccead0..44498cc1b0 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -25,6 +25,8 @@ import java.io.IOException; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -83,4 +85,26 @@ public void testBasic_helloWorld() pullAndRunBuiltImage( targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); } + + /** Ensure that a provided executor is not disposed. */ + @Test + public void testProvidedExecutorNotDisposed() + throws InvalidImageReferenceException, InterruptedException, ExecutionException, + CacheDirectoryCreationException, IOException { + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld"); + Containerizer containerizer = + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.basic("username", "password")))) + .setAllowInsecureRegistries(true); + + ExecutorService executorService = Executors.newCachedThreadPool(); + containerizer.setExecutorService(executorService); + Jib.from("busybox").setEntrypoint("echo", "Hello World").containerize(containerizer); + Assert.assertFalse(executorService.isShutdown()); + + executorService.shutdown(); + } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 81c98eaa17..9d35ec4c76 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -41,8 +41,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Stream; import org.hamcrest.CoreMatchers; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; @@ -56,6 +59,12 @@ public class BuildStepsIntegrationTest { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + private static final ExecutorService executorService = Executors.newCachedThreadPool(); + + @AfterClass + public static void cleanUp() { + executorService.shutdown(); + } /** * Lists the files in the {@code resourcePath} resources directory and builds a {@link @@ -288,6 +297,7 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( .setApplicationLayersCacheDirectory(cacheDirectory) .setAllowInsecureRegistries(true) .setLayerConfigurations(fakeLayerConfigurations) - .setToolName("jib-integration-test"); + .setToolName("jib-integration-test") + .setExecutorService(executorService); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 54f9877ead..2d6eecc387 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -37,6 +37,9 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; import javax.annotation.Nullable; /** @@ -417,21 +420,45 @@ public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workin public JibContainer containerize(Containerizer containerizer) throws InterruptedException, ExecutionException, IOException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer); - BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); - return new JibContainer(result.getImageDigest(), result.getImageId()); + + return containerize(containerizer, Executors::newCachedThreadPool); + } + + @VisibleForTesting + JibContainer containerize( + Containerizer containerizer, Supplier defaultExecutorServiceFactory) + throws InterruptedException, ExecutionException, IOException, + CacheDirectoryCreationException { + + boolean shutdownExecutorService = !containerizer.getExecutorService().isPresent(); + ExecutorService executorService = + containerizer.getExecutorService().orElseGet(defaultExecutorServiceFactory); + + BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer, executorService); + + try { + BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); + return new JibContainer(result.getImageDigest(), result.getImageId()); + } finally { + if (shutdownExecutorService) { + executorService.shutdown(); + } + } } /** * Builds a {@link BuildConfiguration} using this and a {@link Containerizer}. * * @param containerizer the {@link Containerizer} + * @param executorService the {@link ExecutorService} to use, overriding the executor in the + * {@link Containerizer} * @return the {@link BuildConfiguration} * @throws CacheDirectoryCreationException if a cache directory could not be created * @throws IOException if an I/O exception occurs */ @VisibleForTesting - BuildConfiguration toBuildConfiguration(Containerizer containerizer) + BuildConfiguration toBuildConfiguration( + Containerizer containerizer, ExecutorService executorService) throws CacheDirectoryCreationException, IOException { buildConfigurationBuilder .setTargetImageConfiguration(containerizer.getTargetImage().toImageConfiguration()) @@ -441,9 +468,8 @@ BuildConfiguration toBuildConfiguration(Containerizer containerizer) .setContainerConfiguration(containerConfigurationBuilder.build()) .setLayerConfigurations(layerConfigurations) .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) - .setToolName(containerizer.getToolName()); - - containerizer.getExecutorService().ifPresent(buildConfigurationBuilder::setExecutorService); + .setToolName(containerizer.getToolName()) + .setExecutorService(executorService); containerizer .getEventHandlers() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index dc8d2b4e7c..0917a8ec8b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -223,6 +223,9 @@ public BuildConfiguration build() throws IOException { if (applicationLayersCacheDirectory == null) { missingFields.add("application layers cache directory"); } + if (executorService == null) { + missingFields.add("executor service"); + } switch (missingFields.size()) { case 0: // No errors @@ -234,10 +237,6 @@ public BuildConfiguration build() throws IOException { + "' does not use a specific image digest - build may not be reproducible")); } - if (executorService == null) { - executorService = Executors.newCachedThreadPool(); - } - return new BuildConfiguration( baseImageConfiguration, Preconditions.checkNotNull(targetImageConfiguration), @@ -250,7 +249,7 @@ public BuildConfiguration build() throws IOException { layerConfigurations, toolName, eventDispatcher, - executorService); + Preconditions.checkNotNull(executorService)); case 1: throw new IllegalStateException(missingFields.get(0) + " is required but not set"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 2a28cc290d..ced2a867d4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.builder.BuildSteps; +import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -26,16 +29,23 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Paths; +import java.security.DigestException; import java.time.Instant; import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import org.junit.Assert; @@ -74,7 +84,8 @@ public void testToBuildConfiguration_containerConfigurationSet() .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration(Containerizer.to(baseImage)); + jibContainerBuilder.toBuildConfiguration( + Containerizer.to(baseImage), MoreExecutors.newDirectExecutorService()); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -106,7 +117,8 @@ public void testToBuildConfiguration_containerConfigurationAdd() .setProgramArguments("program", "arguments"); BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration(Containerizer.to(baseImage)); + jibContainerBuilder.toBuildConfiguration( + Containerizer.to(baseImage), MoreExecutors.newDirectExecutorService()); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -141,7 +153,9 @@ public void testToBuildConfiguration() JibContainerBuilder jibContainerBuilder = new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); - BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(containerizer); + BuildConfiguration buildConfiguration = + jibContainerBuilder.toBuildConfiguration( + containerizer, containerizer.getExecutorService().get()); Assert.assertEquals( spyBuildConfigurationBuilder.build().getContainerConfiguration(), @@ -201,11 +215,98 @@ public void testToBuildConfiguration() containerizer .withAdditionalTag("tag1") .withAdditionalTag("tag2") - .setToolName("toolName")); + .setToolName("toolName"), + MoreExecutors.newDirectExecutorService()); Assert.assertSame( ImageFormat.OCI.getManifestTemplateClass(), buildConfiguration.getTargetFormat()); Assert.assertEquals( ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals("toolName", buildConfiguration.getToolName()); } + + /** Verify that an internally-created ExecutorService is shutdown. */ + @Test + public void testContainerize_executorCreated() throws Exception { + + RegistryImage baseImage = RegistryImage.named("test-image"); + JibContainerBuilder jibContainerBuilder = + new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + .setEntrypoint(Arrays.asList("entry", "point")) + .setEnvironment(ImmutableMap.of("name", "value")) + .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) + .setLabels(ImmutableMap.of("key", "value")) + .setProgramArguments(Arrays.asList("program", "arguments")) + .setCreationTime(Instant.ofEpochMilli(1000)) + .setUser("user") + .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); + + Containerizer mockContainerizer = createMockContainerizer(); + + jibContainerBuilder.containerize(mockContainerizer, Suppliers.ofInstance(mockExecutorService)); + + Mockito.verify(mockExecutorService).shutdown(); + } + + /** Verify that a provided ExecutorService is not shutdown. */ + @Test + public void testContainerize_configuredExecutor() throws Exception { + + RegistryImage baseImage = RegistryImage.named("test-image"); + JibContainerBuilder jibContainerBuilder = + new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + .setEntrypoint(Arrays.asList("entry", "point")) + .setEnvironment(ImmutableMap.of("name", "value")) + .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) + .setLabels(ImmutableMap.of("key", "value")) + .setProgramArguments(Arrays.asList("program", "arguments")) + .setCreationTime(Instant.ofEpochMilli(1000)) + .setUser("user") + .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); + Containerizer mockContainerizer = createMockContainerizer(); + Mockito.when(mockContainerizer.getExecutorService()) + .thenReturn(Optional.of(mockExecutorService)); + + jibContainerBuilder.containerize( + mockContainerizer, + () -> { + throw new AssertionError(); + }); + + Mockito.verify(mockExecutorService, Mockito.never()).shutdown(); + } + + private Containerizer createMockContainerizer() + throws CacheDirectoryCreationException, InvalidImageReferenceException, InterruptedException, + ExecutionException, DigestException { + + ImageReference targetImage = ImageReference.parse("target-image"); + TargetImage mockTargetImage = Mockito.mock(TargetImage.class); + Containerizer mockContainerizer = Mockito.mock(Containerizer.class); + BuildSteps mockBuildSteps = Mockito.mock(BuildSteps.class); + BuildResult mockBuildResult = Mockito.mock(BuildResult.class); + + Mockito.when(mockTargetImage.toImageConfiguration()) + .thenReturn(ImageConfiguration.builder(targetImage).build()); + Mockito.when(mockTargetImage.toBuildSteps(Mockito.any(BuildConfiguration.class))) + .thenReturn(mockBuildSteps); + Mockito.when(mockBuildSteps.run()).thenReturn(mockBuildResult); + Mockito.when(mockBuildResult.getImageDigest()) + .thenReturn( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + Mockito.when(mockBuildResult.getImageId()) + .thenReturn( + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + + Mockito.when(mockContainerizer.getTargetImage()).thenReturn(mockTargetImage); + Mockito.when(mockContainerizer.getAdditionalTags()).thenReturn(Collections.emptySet()); + Mockito.when(mockContainerizer.getBaseImageLayersCacheDirectory()).thenReturn(Paths.get("/")); + Mockito.when(mockContainerizer.getApplicationLayersCacheDirectory()).thenReturn(Paths.get("/")); + Mockito.when(mockContainerizer.getAllowInsecureRegistries()).thenReturn(false); + Mockito.when(mockContainerizer.getToolName()).thenReturn("mocktool"); + Mockito.when(mockContainerizer.getExecutorService()).thenReturn(Optional.empty()); + Mockito.when(mockContainerizer.getEventHandlers()).thenReturn(Optional.empty()); + return mockContainerizer; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 51fd5037fd..10786ab29e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Paths; import java.util.Arrays; @@ -134,6 +135,7 @@ private BuildConfiguration makeFakeBuildConfiguration( .build()) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()) .build(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 321124b88c..4f179c37c2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -101,7 +102,8 @@ public void testBuilder() throws Exception { .setTargetFormat(OCIManifestTemplate.class) .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) - .setToolName(expectedCreatedBy); + .setToolName(expectedCreatedBy) + .setExecutorService(MoreExecutors.newDirectExecutorService()); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -151,6 +153,7 @@ public void testBuilder() throws Exception { Assert.assertEquals( expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); Assert.assertEquals(expectedCreatedBy, buildConfiguration.getToolName()); + Assert.assertNotNull(buildConfiguration.getExecutorService()); } @Test @@ -178,7 +181,8 @@ public void testBuilder_default() throws IOException { .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) - .setApplicationLayersCacheDirectory(Paths.get("ignored")); + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); @@ -204,6 +208,7 @@ public void testBuilder_missingValues() throws IOException { ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()) .build(); Assert.fail("Build configuration should not be built with missing values"); @@ -216,6 +221,7 @@ public void testBuilder_missingValues() throws IOException { BuildConfiguration.builder() .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()) .build(); Assert.fail("Build configuration should not be built with missing values"); @@ -233,7 +239,7 @@ public void testBuilder_missingValues() throws IOException { } catch (IllegalStateException ex) { Assert.assertEquals( "base image configuration, target image configuration, base image layers cache directory, " - + "and application layers cache directory are required but not set", + + "application layers cache directory, and executor service are required but not set", ex.getMessage()); } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java index b6116220d8..cac18e3820 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; /** Test helper to expose package-private members of {@link JibContainerBuilder}. */ @@ -26,7 +27,9 @@ public class JibContainerBuilderTestHelper { public static BuildConfiguration toBuildConfiguration( JibContainerBuilder jibContainerBuilder, Containerizer containerizer) throws IOException, CacheDirectoryCreationException { - return jibContainerBuilder.toBuildConfiguration(containerizer); + return jibContainerBuilder.toBuildConfiguration( + containerizer, + containerizer.getExecutorService().orElseGet(MoreExecutors::newDirectExecutorService)); } private JibContainerBuilderTestHelper() {} From 19909a1ba97d3a7e6e368175201be96258f09b70 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 11 Dec 2018 18:48:49 -0500 Subject: [PATCH 0388/2020] Adds ProgressEventDispatcher to replace manual Allocation management. (#1330) --- .../jib/builder/ProgressEventDispatcher.java | 142 ++++++++++++++++++ .../builder/steps/AuthenticatePushStep.java | 30 ++-- .../BuildAndCacheApplicationLayerStep.java | 36 ++--- .../jib/builder/steps/BuildImageStep.java | 24 ++- .../jib/builder/steps/LoadDockerStep.java | 40 +++-- .../steps/PullAndCacheBaseImageLayerStep.java | 38 ++--- .../PullAndCacheBaseImageLayersStep.java | 22 ++- .../jib/builder/steps/PullBaseImageStep.java | 50 ++---- .../tools/jib/builder/steps/PushBlobStep.java | 24 ++- .../steps/PushContainerConfigurationStep.java | 21 ++- .../jib/builder/steps/PushImageStep.java | 18 +-- .../jib/builder/steps/PushLayersStep.java | 47 +++--- .../RetrieveRegistryCredentialsStep.java | 34 ++--- .../tools/jib/builder/steps/StepsRunner.java | 55 +++---- .../jib/builder/steps/WriteTarFileStep.java | 36 +++-- .../tools/jib/event/progress/Allocation.java | 2 +- .../builder/ProgressEventDispatcherTest.java | 78 ++++++++++ ...BuildAndCacheApplicationLayerStepTest.java | 4 +- .../jib/builder/steps/BuildImageStepTest.java | 16 +- .../RetrieveRegistryCredentialsStepTest.java | 26 +++- 20 files changed, 456 insertions(+), 287 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java new file mode 100644 index 0000000000..d34d237faf --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -0,0 +1,142 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.progress.Allocation; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import java.io.Closeable; + +/** + * Dispatches {@link ProgressEvent}s associated with a managed {@link Allocation}. Keeps track of + * the allocation units that are remaining so that it can emit the remaining progress units upon + * {@link #close}. + * + *

This class is not thread-safe. Only use a single instance per thread and create child + * instances with {@link #newChildProducer}. + */ +public class ProgressEventDispatcher implements Closeable { + + /** + * Creates a new {@link ProgressEventDispatcher} based off an existing {@link + * ProgressEventDispatcher}. {@link #create} should only be called once. + */ + @FunctionalInterface + public interface Factory { + + /** + * Creates the {@link ProgressEventDispatcher} with an associated {@link Allocation}. + * + * @param description user-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return the new {@link ProgressEventDispatcher} + */ + ProgressEventDispatcher create(String description, long allocationUnits); + } + + /** + * Creates a new {@link ProgressEventDispatcher} with a root {@link Allocation}. + * + * @param eventDispatcher the {@link EventDispatcher} + * @param description user-facing description of what the allocation represents + * @param allocationUnits number of allocation units + * @return a new {@link ProgressEventDispatcher} + */ + public static ProgressEventDispatcher newRoot( + EventDispatcher eventDispatcher, String description, long allocationUnits) { + return newProgressEventDispatcher( + eventDispatcher, Allocation.newRoot(description, allocationUnits)); + } + + /** + * Creates a new {@link ProgressEventDispatcher} and dispatches a new {@link ProgressEvent} with + * progress 0 for {@code allocation}. + * + * @param eventDispatcher the {@link EventDispatcher} + * @param allocation the {@link Allocation} to manage + * @return a new {@link ProgressEventDispatcher} + */ + private static ProgressEventDispatcher newProgressEventDispatcher( + EventDispatcher eventDispatcher, Allocation allocation) { + return new ProgressEventDispatcher(eventDispatcher, allocation).dispatchProgress(0); + } + + private final EventDispatcher eventDispatcher; + private final Allocation allocation; + + private long remainingAllocationUnits; + private boolean closed = false; + + private ProgressEventDispatcher(EventDispatcher eventDispatcher, Allocation allocation) { + this.eventDispatcher = eventDispatcher; + this.allocation = allocation; + + remainingAllocationUnits = allocation.getAllocationUnits(); + } + + /** + * Creates a new {@link Factory} for a {@link ProgressEventDispatcher} that manages a child {@link + * Allocation}. Since each child {@link Allocation} accounts for 1 allocation unit of its parent, + * this method decrements the {@link #remainingAllocationUnits} by {@code 1}. + * + * @return a new {@link Factory} + */ + public Factory newChildProducer() { + decrementRemainingAllocationUnits(1); + + return new Factory() { + + private boolean used = false; + + @Override + public ProgressEventDispatcher create(String description, long allocationUnits) { + Preconditions.checkState(!used); + used = true; + return newProgressEventDispatcher( + eventDispatcher, allocation.newChild(description, allocationUnits)); + } + }; + } + + /** Emits the remaining allocation units as progress units in a {@link ProgressEvent}. */ + @Override + public void close() { + if (remainingAllocationUnits > 0) { + dispatchProgress(remainingAllocationUnits); + } + closed = true; + } + + private ProgressEventDispatcher dispatchProgress(long progressUnits) { + decrementRemainingAllocationUnits(progressUnits); + eventDispatcher.dispatch(new ProgressEvent(allocation, progressUnits)); + return this; + } + + private void decrementRemainingAllocationUnits(long units) { + Preconditions.checkState(!closed); + + remainingAllocationUnits -= units; + Verify.verify( + remainingAllocationUnits >= 0, + "Remaining allocation units less than 0 for '%s': %s", + allocation.getDescription(), + remainingAllocationUnits); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 85fd167737..d387f9f382 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -19,11 +19,10 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; @@ -47,7 +46,7 @@ class AuthenticatePushStep implements AsyncStep, Callable, Callable, Calla static ImmutableList makeList( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { int layerCount = buildConfiguration.getLayerConfigurations().size(); - Allocation progressAllocation = - parentProgressAllocation.newChild("Build application layers", layerCount); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("Build application layers", layerCount); + TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = ImmutableList.builderWithExpectedSize(layerCount); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { // Skips the layer if empty. if (layerConfiguration.getLayerEntries().isEmpty()) { - buildConfiguration - .getEventDispatcher() - .dispatch(new ProgressEvent(progressAllocation, 1)); continue; } @@ -70,7 +65,7 @@ static ImmutableList makeList( new BuildAndCacheApplicationLayerStep( listeningExecutorService, buildConfiguration, - progressAllocation, + progressEventDispatcher.newChildProducer(), layerConfiguration.getName(), layerConfiguration)); } @@ -79,7 +74,7 @@ static ImmutableList makeList( } private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String layerType; private final LayerConfiguration layerConfiguration; @@ -89,11 +84,11 @@ static ImmutableList makeList( private BuildAndCacheApplicationLayerStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, String layerType, LayerConfiguration layerConfiguration) { this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layerType = layerType; this.layerConfiguration = layerConfiguration; @@ -110,19 +105,17 @@ public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); - Allocation progressAllocation = - parentProgressAllocation.newChild("Build " + layerType + " layer", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("Build " + layerType + " layer", 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); // Don't build the layer if it exists already. Optional optionalCachedLayer = cache.retrieve(layerConfiguration.getLayerEntries()); if (optionalCachedLayer.isPresent()) { - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCachedLayer.get(); } @@ -133,7 +126,6 @@ public CachedLayer call() throws IOException, CacheCorruptedException { buildConfiguration .getEventDispatcher() .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return cachedLayer; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 31d681a606..dcb0090905 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -20,12 +20,11 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -47,7 +46,7 @@ class BuildImageStep private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final PullBaseImageStep pullBaseImageStep; private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; @@ -57,13 +56,13 @@ class BuildImageStep BuildImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, PullBaseImageStep pullBaseImageStep, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pullBaseImageStep = pullBaseImageStep; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -92,11 +91,10 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { - Allocation progressAllocation = parentProgressAllocation.newChild("build image format", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("build image format", 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); @@ -168,11 +166,7 @@ private Image afterCachedLayerSteps() } // Gets the container configuration content descriptor. - Image image = imageBuilder.build(); - - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); - - return image; + return imageBuilder.build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index de87d5ea7d..df32d2fd8e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -19,12 +19,11 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; @@ -40,7 +39,7 @@ class LoadDockerStep implements AsyncStep, Callable { private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final DockerClient dockerClient; @@ -53,14 +52,14 @@ class LoadDockerStep implements AsyncStep, Callable { LoadDockerStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient, PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, ImmutableList buildAndCacheApplicationLayerSteps, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.dockerClient = dockerClient; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -93,27 +92,26 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() buildConfiguration .getEventDispatcher() .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); - Allocation progressAllocation = parentProgressAllocation.newChild("Load to Docker daemon", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - ImageReference targetImageReference = - buildConfiguration.getTargetImageConfiguration().getImage(); + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("Load to Docker daemon", 1)) { + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + ImageReference targetImageReference = + buildConfiguration.getTargetImageConfiguration().getImage(); - // Load the image to docker daemon. - dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); + // Load the image to docker daemon. + dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); - // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. - for (String tag : buildConfiguration.getAllTargetImageTags()) { - if (tag.equals(targetImageReference.getTag())) { - continue; + // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. + for (String tag : buildConfiguration.getAllTargetImageTags()) { + if (tag.equals(targetImageReference.getTag())) { + continue; + } + + dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } - dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } - - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); - - return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 97bef35800..18c945e4c9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -17,13 +17,12 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -40,7 +39,7 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable private static final String DESCRIPTION = "Pulling base image layer %s"; private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final DescriptorDigest layerDigest; private final @Nullable Authorization pullAuthorization; @@ -50,11 +49,11 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable PullAndCacheBaseImageLayerStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, DescriptorDigest layerDigest, @Nullable Authorization pullAuthorization) { this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layerDigest = layerDigest; this.pullAuthorization = pullAuthorization; @@ -68,19 +67,16 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { - Allocation progressAllocation = - parentProgressAllocation.newChild("pull base image layer " + layerDigest, 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("pull base image layer " + layerDigest, 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher( + buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { Cache cache = buildConfiguration.getBaseImageLayersCache(); // Checks if the layer already exists in the cache. Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCachedLayer.get(); } @@ -89,17 +85,13 @@ public CachedLayer call() throws IOException, CacheCorruptedException { .newBaseImageRegistryClientFactory() .setAuthorization(pullAuthorization) .newRegistryClient(); - CachedLayer cachedLayer = - cache.writeCompressedLayer( - registryClient.pullBlob( - layerDigest, - // TODO: Replace with progress-reporting. - alsoIgnored -> {}, - alsoIgnored -> {})); - - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); - return cachedLayer; + return cache.writeCompressedLayer( + registryClient.pullBlob( + layerDigest, + // TODO: Replace with progress-reporting. + alsoIgnored -> {}, + alsoIgnored -> {})); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index cc6ebb3a94..022bcbccab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -19,11 +19,10 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -41,7 +40,7 @@ class PullAndCacheBaseImageLayersStep private final BuildConfiguration buildConfiguration; private final ListeningExecutorService listeningExecutorService; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final PullBaseImageStep pullBaseImageStep; @@ -50,11 +49,11 @@ class PullAndCacheBaseImageLayersStep PullAndCacheBaseImageLayersStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, PullBaseImageStep pullBaseImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pullBaseImageStep = pullBaseImageStep; listenableFuture = @@ -74,12 +73,11 @@ public ImmutableList call() BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); - Allocation progressAllocation = - parentProgressAllocation.newChild("pull base imgae layers", baseImageLayers.size()); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "pull base image layers", baseImageLayers.size()); + TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = ImmutableList.builderWithExpectedSize(baseImageLayers.size()); for (Layer layer : baseImageLayers) { @@ -87,7 +85,7 @@ public ImmutableList call() new PullAndCacheBaseImageLayerStep( listeningExecutorService, buildConfiguration, - progressAllocation, + progressEventDispatcher.newChildProducer(), layer.getBlobDescriptor().getDigest(), pullBaseImageStepResult.getBaseImageAuthorization())); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 39b37aabff..cd384ebe21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -19,13 +19,12 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.Image; @@ -84,16 +83,16 @@ Authorization getBaseImageAuthorization() { } private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final ListenableFuture listenableFuture; PullBaseImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; listenableFuture = listeningExecutorService.submit(this); } @@ -115,19 +114,14 @@ public BaseImageWithAuthorization call() "Getting base image " + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); - Allocation progressAllocation = parentProgressAllocation.newChild("pull base image", 2); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("pull base image", 2); + TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. try { - BaseImageWithAuthorization baseImageWithAuthorization = - new BaseImageWithAuthorization(pullBaseImage(null), null); - - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 2)); - - return baseImageWithAuthorization; + return new BaseImageWithAuthorization(pullBaseImage(null), null); } catch (RegistryUnauthorizedException ex) { buildConfiguration @@ -144,7 +138,9 @@ public BaseImageWithAuthorization call() ListeningExecutorService directExecutorService = MoreExecutors.newDirectExecutorService(); RetrieveRegistryCredentialsStep retrieveBaseRegistryCredentialsStep = RetrieveRegistryCredentialsStep.forBaseImage( - directExecutorService, buildConfiguration, progressAllocation); + directExecutorService, + buildConfiguration, + progressEventDispatcher.newChildProducer()); Credential registryCredential = NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); Authorization registryAuthorization = @@ -154,15 +150,8 @@ public BaseImageWithAuthorization call() registryCredential.getUsername(), registryCredential.getPassword()); try { - BaseImageWithAuthorization baseImageWithAuthorization = - new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); - - buildConfiguration - .getEventDispatcher() - .dispatch(new ProgressEvent(progressAllocation, 1)); - - return baseImageWithAuthorization; + return new BaseImageWithAuthorization( + pullBaseImage(registryAuthorization), registryAuthorization); } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. @@ -185,15 +174,8 @@ public BaseImageWithAuthorization call() registryAuthorization = registryAuthenticator.setAuthorization(registryAuthorization).authenticatePull(); - BaseImageWithAuthorization baseImageWithAuthorization = - new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); - - buildConfiguration - .getEventDispatcher() - .dispatch(new ProgressEvent(progressAllocation, 1)); - - return baseImageWithAuthorization; + return new BaseImageWithAuthorization( + pullBaseImage(registryAuthorization), registryAuthorization); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 08274d7400..075c35f0aa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -21,11 +21,10 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.ListenableFuture; @@ -40,7 +39,7 @@ class PushBlobStep implements AsyncStep, Callable, Callable getFuture() { @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { - Allocation progressAllocation = - parentProgressAllocation.newChild("push blob " + blobDescriptor.getDigest(), 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { + try (ProgressEventDispatcher ignored = + progressEventDipatcherFactory.create("push blob " + blobDescriptor.getDigest(), 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher( + buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() @@ -92,7 +89,6 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc buildConfiguration .getEventDispatcher() .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return blobDescriptor; } @@ -105,8 +101,6 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc // TODO: Replace with progress-reporting. }); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); - return blobDescriptor; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index cbcb8de5f8..6b6e3d5f84 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -21,10 +21,9 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -43,7 +42,7 @@ class PushContainerConfigurationStep private final BuildConfiguration buildConfiguration; private final ListeningExecutorService listeningExecutorService; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final AuthenticatePushStep authenticatePushStep; private final BuildImageStep buildImageStep; @@ -53,12 +52,12 @@ class PushContainerConfigurationStep PushContainerConfigurationStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, AuthenticatePushStep authenticatePushStep, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authenticatePushStep = authenticatePushStep; this.buildImageStep = buildImageStep; @@ -85,12 +84,10 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { - Allocation progressAllocation = - parentProgressAllocation.newChild("push container configuration", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("push container configuration", 1); + TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); Blob containerConfigurationBlob = new ImageToJsonTranslator(image).getContainerConfigurationBlob(); @@ -100,7 +97,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() return new PushBlobStep( listeningExecutorService, buildConfiguration, - progressAllocation, + progressEventDispatcher.newChildProducer(), authenticatePushStep, blobDescriptor, containerConfigurationBlob); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index ed8e9efb3c..d24481956c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -20,11 +20,10 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -47,7 +46,7 @@ class PushImageStep implements AsyncStep, Callable { private final BuildConfiguration buildConfiguration; private final ListeningExecutorService listeningExecutorService; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final AuthenticatePushStep authenticatePushStep; @@ -61,7 +60,7 @@ class PushImageStep implements AsyncStep, Callable { PushImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, AuthenticatePushStep authenticatePushStep, PushLayersStep pushBaseImageLayersStep, PushLayersStep pushApplicationLayersStep, @@ -69,7 +68,7 @@ class PushImageStep implements AsyncStep, Callable { BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authenticatePushStep = authenticatePushStep; this.pushBaseImageLayersStep = pushBaseImageLayersStep; this.pushApplicationLayersStep = pushApplicationLayersStep; @@ -120,8 +119,8 @@ private ListenableFuture> afterPushSteps() } private ListenableFuture afterAllPushed() throws ExecutionException, IOException { - Allocation progressAllocation = parentProgressAllocation.newChild("Push to registry", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("Push to registry", 1); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { @@ -167,10 +166,7 @@ private ListenableFuture afterAllPushed() throws ExecutionException return Futures.whenAllSucceed(pushAllTagsFutures) .call( () -> { - buildConfiguration - .getEventDispatcher() - .dispatch(new ProgressEvent(progressAllocation, 1)); - + progressEventDispatcher.close(); return result; }, listeningExecutorService); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 7d013fe1f1..e086217ce8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -20,11 +20,10 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -40,7 +39,7 @@ class PushLayersStep private final BuildConfiguration buildConfiguration; private final ListeningExecutorService listeningExecutorService; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final AuthenticatePushStep authenticatePushStep; private final AsyncStep>> @@ -51,13 +50,13 @@ class PushLayersStep PushLayersStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, AuthenticatePushStep authenticatePushStep, AsyncStep>> cachedLayerStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authenticatePushStep = authenticatePushStep; this.cachedLayerStep = cachedLayerStep; @@ -76,34 +75,40 @@ public ListenableFuture>> getFuture() { public ImmutableList> call() throws ExecutionException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { - ImmutableList> cachedLayer = + ImmutableList> cachedLayers = NonBlockingSteps.get(cachedLayerStep); - // Constructs a PushBlobStep for each layer. - ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); - for (AsyncStep cachedLayerStep : cachedLayer) { - ListenableFuture pushBlobStepFuture = - Futures.whenAllSucceed(cachedLayerStep.getFuture()) - .call(() -> makePushBlobStep(cachedLayerStep), listeningExecutorService); - pushBlobStepsBuilder.add(() -> pushBlobStepFuture); + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("push layers", cachedLayers.size())) { + // Constructs a PushBlobStep for each layer. + ImmutableList.Builder> pushBlobStepsBuilder = + ImmutableList.builder(); + for (AsyncStep cachedLayerStep : cachedLayers) { + ProgressEventDispatcher.Factory childProgressEventDispatcherFactory = + progressEventDispatcher.newChildProducer(); + ListenableFuture pushBlobStepFuture = + Futures.whenAllSucceed(cachedLayerStep.getFuture()) + .call( + () -> makePushBlobStep(cachedLayerStep, childProgressEventDispatcherFactory), + listeningExecutorService); + pushBlobStepsBuilder.add(() -> pushBlobStepFuture); + } + + return pushBlobStepsBuilder.build(); } - - return pushBlobStepsBuilder.build(); } } - private PushBlobStep makePushBlobStep(AsyncStep cachedLayerStep) + private PushBlobStep makePushBlobStep( + AsyncStep cachedLayerStep, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) throws ExecutionException { CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep); - Allocation progressAllocation = - parentProgressAllocation.newChild("push layer " + cachedLayer.getDigest(), 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - return new PushBlobStep( listeningExecutorService, buildConfiguration, - progressAllocation, + progressEventDispatcherFactory, authenticatePushStep, new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), cachedLayer.getBlob()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 4b86453f5d..171026ece1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -17,13 +17,12 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -44,11 +43,11 @@ private static String makeDescription(String registry) { static RetrieveRegistryCredentialsStep forBaseImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, buildConfiguration, - parentProgressAllocation, + progressEventDispatcherFactory, buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } @@ -57,17 +56,17 @@ static RetrieveRegistryCredentialsStep forBaseImage( static RetrieveRegistryCredentialsStep forTargetImage( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( listeningExecutorService, buildConfiguration, - parentProgressAllocation, + progressEventDispatcherFactory, buildConfiguration.getTargetImageConfiguration().getImageRegistry(), buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } private final BuildConfiguration buildConfiguration; - private final Allocation parentProgressAllocation; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String registry; private final ImmutableList credentialRetrievers; @@ -78,11 +77,11 @@ static RetrieveRegistryCredentialsStep forTargetImage( RetrieveRegistryCredentialsStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, - Allocation parentProgressAllocation, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, String registry, ImmutableList credentialRetrievers) { this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registry = registry; this.credentialRetrievers = credentialRetrievers; @@ -100,28 +99,25 @@ public Credential call() throws CredentialRetrievalException { String description = makeDescription(registry); buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); - Allocation progressAllocation = - parentProgressAllocation.newChild("Retrieve registry credentials for " + registry, 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create( + "Retrieve registry credentials for " + registry, 1); + TimerEventDispatcher ignored2 = + new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { - buildConfiguration - .getEventDispatcher() - .dispatch(new ProgressEvent(progressAllocation, 1)); return optionalCredential.get(); } } - // If no credentials found, give an info (not warning because in most cases, the base image is + // If no credentials found, give an info (not warning because in most cases, the base image + // is // public and does not need extra credentials) and return null. buildConfiguration .getEventDispatcher() .dispatch(LogEvent.info("No credentials could be retrieved for registry " + registry)); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); return null; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 9cc6fef644..df12dac2a0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,10 +18,9 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.AsyncSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -88,7 +87,8 @@ public static StepsRunner begin(BuildConfiguration buildConfiguration) { /** The total number of steps added. */ private int stepsCount = 0; - @Nullable private Allocation rootProgressAllocation; + @Nullable private String rootProgressAllocationDescription; + @Nullable private ProgressEventDispatcher rootProgressEventDispatcher; private StepsRunner( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { @@ -103,7 +103,7 @@ public StepsRunner retrieveTargetRegistryCredentials() { RetrieveRegistryCredentialsStep.forTargetImage( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation))); + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); } public StepsRunner authenticatePush() { @@ -113,7 +113,7 @@ public StepsRunner authenticatePush() { new AuthenticatePushStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.retrieveTargetRegistryCredentialsStep))); } @@ -124,7 +124,7 @@ public StepsRunner pullBaseImage() { new PullBaseImageStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation))); + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); } public StepsRunner pullAndCacheBaseImageLayers() { @@ -134,7 +134,7 @@ public StepsRunner pullAndCacheBaseImageLayers() { new PullAndCacheBaseImageLayersStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.pullBaseImageStep))); } @@ -145,7 +145,7 @@ public StepsRunner pushBaseImageLayers() { new PushLayersStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); } @@ -157,7 +157,7 @@ public StepsRunner buildAndCacheApplicationLayers() { BuildAndCacheApplicationLayerStep.makeList( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation))); + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); } public StepsRunner buildImage() { @@ -167,7 +167,7 @@ public StepsRunner buildImage() { new BuildImageStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.pullBaseImageStep), Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); @@ -180,7 +180,7 @@ public StepsRunner pushContainerConfiguration() { new PushContainerConfigurationStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.buildImageStep))); } @@ -192,7 +192,7 @@ public StepsRunner pushApplicationLayers() { new PushLayersStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), AsyncSteps.immediate( Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); @@ -222,7 +222,7 @@ public StepsRunner finalizingBuild() { } public StepsRunner pushImage() { - createRootProgressAllocation("Build to registry"); + rootProgressAllocationDescription = "Build to registry"; return enqueueStep( () -> @@ -230,7 +230,7 @@ public StepsRunner pushImage() { new PushImageStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), Preconditions.checkNotNull(steps.pushBaseImageLayersStep), Preconditions.checkNotNull(steps.pushApplicationLayersStep), @@ -239,7 +239,7 @@ public StepsRunner pushImage() { } public StepsRunner loadDocker(DockerClient dockerClient) { - createRootProgressAllocation("Build to Docker daemon"); + rootProgressAllocationDescription = "Build to Docker daemon"; return enqueueStep( () -> @@ -247,7 +247,7 @@ public StepsRunner loadDocker(DockerClient dockerClient) { new LoadDockerStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), dockerClient, Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), @@ -255,7 +255,7 @@ public StepsRunner loadDocker(DockerClient dockerClient) { } public StepsRunner writeTarFile(Path outputPath) { - createRootProgressAllocation("Build to tar file"); + rootProgressAllocationDescription = "Build to tar file"; return enqueueStep( () -> @@ -263,7 +263,7 @@ public StepsRunner writeTarFile(Path outputPath) { new WriteTarFileStep( listeningExecutorService, buildConfiguration, - Preconditions.checkNotNull(rootProgressAllocation), + Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), outputPath, Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), @@ -271,9 +271,17 @@ public StepsRunner writeTarFile(Path outputPath) { } public BuildResult run() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(rootProgressAllocation); - stepsRunnable.run(); - return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); + Preconditions.checkNotNull(rootProgressAllocationDescription); + + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot( + buildConfiguration.getEventDispatcher(), + rootProgressAllocationDescription, + stepsCount)) { + rootProgressEventDispatcher = progressEventDispatcher; + stepsRunnable.run(); + return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); + } } private StepsRunner enqueueStep(Runnable stepRunnable) { @@ -286,9 +294,4 @@ private StepsRunner enqueueStep(Runnable stepRunnable) { stepsCount++; return this; } - - private void createRootProgressAllocation(String description) { - rootProgressAllocation = Allocation.newRoot(description, stepsCount); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(rootProgressAllocation, 0L)); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index c5578823fb..7003b28b53 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -19,11 +19,10 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; @@ -42,7 +41,7 @@ public class WriteTarFileStep implements AsyncStep, Callable, Callable buildAndCacheApplicationLayerSteps, BuildImageStep buildImageStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; - this.parentProgressAllocation = parentProgressAllocation; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.outputPath = outputPath; this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; @@ -93,22 +92,21 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { buildConfiguration .getEventDispatcher() .dispatch(LogEvent.lifecycle("Building image to tar file...")); - Allocation progressAllocation = parentProgressAllocation.newChild("Write to tar file", 1); - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 0)); - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + try (ProgressEventDispatcher ignored = + progressEventDispatcherFactory.create("Write to tar file", 1)) { + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - // Builds the image to a tarball. - Files.createDirectories(outputPath.getParent()); - try (OutputStream outputStream = - new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { - new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage()) - .writeTo(outputStream); - } - - buildConfiguration.getEventDispatcher().dispatch(new ProgressEvent(progressAllocation, 1)); + // Builds the image to a tarball. + Files.createDirectories(outputPath.getParent()); + try (OutputStream outputStream = + new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { + new ImageToTarballTranslator(image) + .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage()) + .writeTo(outputStream); + } - return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); + return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java index 9bcd1b8275..e9ae352b4b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -43,7 +43,7 @@ public class Allocation { /** * Creates a new root {@link Allocation}. * - * @param description thuser-facing description of what the allocation represents + * @param description user-facing description of what the allocation represents * @param allocationUnits number of allocation units * @return a new {@link Allocation} */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java new file mode 100644 index 0000000000..2192219ccc --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.common.base.VerifyException; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link ProgressEventDispatcher}. */ +@RunWith(MockitoJUnitRunner.class) +public class ProgressEventDispatcherTest { + + @Mock private EventDispatcher mockEventDispatcher; + + @Test + public void testDispatch() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 10); + ProgressEventDispatcher ignored = + progressEventDispatcher.newChildProducer().create("ignored", 20)) { + // empty + } + + ArgumentCaptor progressEventArgumentCaptor = + ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventDispatcher, Mockito.times(4)) + .dispatch(progressEventArgumentCaptor.capture()); + List progressEvents = progressEventArgumentCaptor.getAllValues(); + + Assert.assertSame(progressEvents.get(0).getAllocation(), progressEvents.get(3).getAllocation()); + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(2).getAllocation()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); + Assert.assertEquals(0, progressEvents.get(1).getUnits()); + Assert.assertEquals(20, progressEvents.get(2).getUnits()); + Assert.assertEquals(9, progressEvents.get(3).getUnits()); + } + + @Test + public void testDispatch_tooManyChildren() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 1)) { + progressEventDispatcher.newChildProducer(); + + try { + progressEventDispatcher.newChildProducer(); + Assert.fail(); + + } catch (VerifyException ex) { + Assert.assertEquals( + "Remaining allocation units less than 0 for 'allocation description': -1", + ex.getMessage()); + } + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 6df39ba887..912582b02f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -19,13 +19,13 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.Layer; @@ -132,7 +132,7 @@ private ImageLayers buildFakeLayersToCache() throws ExecutionException { BuildAndCacheApplicationLayerStep.makeList( MoreExecutors.newDirectExecutorService(), mockBuildConfiguration, - Allocation.newRoot("ignored", 1)); + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer()); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index c74ff80712..d5141b1999 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -19,13 +19,13 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; @@ -190,7 +190,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -219,7 +219,7 @@ public void test_propagateBaseImageConfiguration() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -280,7 +280,7 @@ public void testOverrideWorkingDirectory() throws InterruptedException, Executio new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -302,7 +302,7 @@ public void test_inheritedEntrypoint() throws ExecutionException, InterruptedExc new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -325,7 +325,7 @@ public void test_inheritedEntrypointAndProgramArguments() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -348,7 +348,7 @@ public void test_notInheritedProgramArguments() throws ExecutionException, Inter new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -367,7 +367,7 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - Allocation.newRoot("ignored", 1), + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 10786ab29e..6f55fbd83a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -23,7 +24,6 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; @@ -62,12 +62,18 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio Assert.assertEquals( Credential.basic("baseusername", "basepassword"), RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) + mockListeningExecutorService, + buildConfiguration, + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) + .newChildProducer()) .call()); Assert.assertEquals( Credential.basic("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) + mockListeningExecutorService, + buildConfiguration, + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) + .newChildProducer()) .call()); } @@ -78,7 +84,10 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertNull( RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) + mockListeningExecutorService, + buildConfiguration, + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) + .newChildProducer()) .call()); Mockito.verify(mockEventDispatcher, Mockito.atLeastOnce()) @@ -88,7 +97,10 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) + mockListeningExecutorService, + buildConfiguration, + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) + .newChildProducer()) .call()); Mockito.verify(mockEventDispatcher) @@ -108,7 +120,9 @@ public void testCall_exception() throws IOException { Collections.emptyList()); try { RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, Allocation.newRoot("ignored", 1)) + mockListeningExecutorService, + buildConfiguration, + ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer()) .call(); Assert.fail("Should have thrown exception"); From 81e281e62e5cff55c5d555a9e9b4c65982b110a6 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 11 Dec 2018 18:49:05 -0500 Subject: [PATCH 0389/2020] Fixes html entity > sign. (#1335) --- .../tools/jib/plugins/common/ProgressDisplayGenerator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java index be1c9b5ef8..a92e975230 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java @@ -30,8 +30,8 @@ * *

Executing tasks...
* [================= ] 72.5% complete
- * > task 1 running
- * > task 3 running + * > task 1 running
+ * > task 3 running */ class ProgressDisplayGenerator { From 483c84a3b18d2df2b86744a4fe9b0199b34a2e78 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Dec 2018 11:42:38 -0500 Subject: [PATCH 0390/2020] Adds SingleThreadedLogger to be able to keep a footer for where progress indication will go. (#1326) --- .../plugins/common/AnsiLoggerWithFooter.java | 141 ++++++++++++++++++ .../common/AnsiLoggerWithFooterTest.java | 102 +++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java new file mode 100644 index 0000000000..35c448843d --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.common.util.concurrent.Futures; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Keeps all log messages in a sequential, deterministic order along with an additional footer that + * always appears below log messages. This is intended to log both the messages and the footer to + * the same console. + * + *

Make sure to call {@link #shutDown} when finished. + */ +class AnsiLoggerWithFooter { + + /** ANSI escape sequence for moving the cursor up one line. */ + private static final String CURSOR_UP_SEQUENCE = "\033[1A"; + + /** ANSI escape sequence for erasing to end of display. */ + private static final String ERASE_DISPLAY_BELOW = "\033[0J"; + + /** ANSI escape sequence for setting all further characters to bold. */ + private static final String BOLD = "\033[1m"; + + /** ANSI escape sequence for setting all further characters to not bold. */ + private static final String UNBOLD = "\033[0m"; + + private static final Duration EXECUTOR_SHUTDOWN_WAIT = Duration.ofSeconds(1); + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + private final Consumer plainLogger; + + private List footerLines = Collections.emptyList(); + + AnsiLoggerWithFooter(Consumer plainLogger) { + this.plainLogger = plainLogger; + } + + /** Shuts down the {@link #executorService}. */ + public void shutDown() { + executorService.shutdown(); + } + + /** Waits for the {@link #executorService} to terminate. */ + public void awaitTermination() { + try { + if (!executorService.awaitTermination( + EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + /** + * Runs {@code messageLogger} asynchronously. + * + * @param messageLogger the {@link Runnable} intended to synchronously log a message to the + * console + * @return a {@link Future} to track completion + */ + public Future log(Runnable messageLogger) { + return log(messageLogger, footerLines); + } + + /** + * Sets the footer asynchronously. This will replace the previously-printed footer with the new + * {@code footerLines}. + * + *

The footer is printed in bold. + * + * @param footerLines the footer, with each line as an element (no newline at end) + * @return a {@link Future} to track completion + */ + public Future setFooter(List footerLines) { + if (footerLines.equals(this.footerLines)) { + return Futures.immediateFuture(null); + } + + return log(() -> {}, footerLines); + } + + private Future log(Runnable messageLogger, List newFooterLines) { + return executorService.submit( + () -> { + StringBuilder plainLogBuilder = new StringBuilder(); + + // Moves the cursor up to the start of the footer. + // TODO: Optimize to single init. + for (int i = 0; i < this.footerLines.size(); i++) { + // Moves cursor up. + plainLogBuilder.append(CURSOR_UP_SEQUENCE); + } + + // Erases everything below cursor. + plainLogBuilder.append(ERASE_DISPLAY_BELOW); + + // Writes out logMessage and footer. + plainLogger.accept(plainLogBuilder.toString()); + messageLogger.run(); + + if (newFooterLines.size() > 0) { + StringBuilder footerBuilder = new StringBuilder(); + for (String newFooterLine : newFooterLines) { + footerBuilder.append(BOLD).append(newFooterLine).append(UNBOLD).append('\n'); + } + // Removes last newline. + footerBuilder.setLength(footerBuilder.length() - 1); + plainLogger.accept(footerBuilder.toString()); + } + + this.footerLines = newFooterLines; + + return null; + }); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java new file mode 100644 index 0000000000..924f947611 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link AnsiLoggerWithFooter}. */ +public class AnsiLoggerWithFooterTest { + + private static final Duration FUTURE_TIMEOUT = Duration.ofSeconds(1); + + private final StringBuilder logBuilder = new StringBuilder(); + private final AnsiLoggerWithFooter testAnsiLoggerWithFooter = + new AnsiLoggerWithFooter(logBuilder::append); + + @Test + public void testLog_noFooter() throws InterruptedException, ExecutionException, TimeoutException { + testAnsiLoggerWithFooter + .log(() -> logBuilder.append("message\n")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals("\033[0Jmessage\n", logBuilder.toString()); + } + + @Test + public void testLog_sameFooter() + throws InterruptedException, ExecutionException, TimeoutException { + testAnsiLoggerWithFooter + .setFooter(Collections.singletonList("footer")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter + .log(() -> logBuilder.append("message\n")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter + .log(() -> logBuilder.append("another message\n")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals( + "\033[0J" + + "\033[1mfooter\033[0m" + + "\033[1A\033[0J" + + "message\n" + + "\033[1mfooter\033[0m" + + "\033[1A\033[0J" + + "another message\n" + + "\033[1mfooter\033[0m", + logBuilder.toString()); + } + + @Test + public void testLog_changingFooter() + throws InterruptedException, ExecutionException, TimeoutException { + testAnsiLoggerWithFooter + .setFooter(Collections.singletonList("footer")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter + .log(() -> logBuilder.append("message\n")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter + .setFooter(Arrays.asList("two line", "footer")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter + .log(() -> logBuilder.append("another message\n")) + .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals( + "\033[0J" + + "\033[1mfooter\033[0m" + + "\033[1A\033[0J" + + "message\n" + + "\033[1mfooter\033[0m" + + "\033[1A\033[0J" + + "\033[1mtwo line\033[0m\n" + + "\033[1mfooter\033[0m" + + "\033[1A\033[1A\033[0J" + + "another message\n" + + "\033[1mtwo line\033[0m\n" + + "\033[1mfooter\033[0m", + logBuilder.toString()); + } +} From 5374e884cbfb3ebfd7216248da732a55f92a3131 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Dec 2018 12:30:13 -0500 Subject: [PATCH 0391/2020] Emits progress of push/pull as ProgressEvents. (#1334) --- jib-core/build.gradle | 13 ++-- .../registry/BlobPullerIntegrationTest.java | 9 +-- .../registry/BlobPusherIntegrationTest.java | 5 +- .../ManifestPusherIntegrationTest.java | 6 +- .../jib/builder/ProgressEventDispatcher.java | 14 +++- .../ProgressEventDispatcherContainer.java | 67 +++++++++++++++++++ .../steps/PullAndCacheBaseImageLayerStep.java | 20 +++--- .../jib/builder/steps/PullBaseImageStep.java | 45 ++++++++----- .../tools/jib/builder/steps/PushBlobStep.java | 32 +++++++-- .../cloud/tools/jib/http/BlobHttpContent.java | 26 +++---- .../tools/jib/http/BlobProgressListener.java | 37 ++++++++++ .../cloud/tools/jib/registry/BlobPuller.java | 26 +++---- .../cloud/tools/jib/registry/BlobPusher.java | 16 ++--- .../tools/jib/registry/ManifestPusher.java | 5 +- .../tools/jib/registry/RegistryClient.java | 17 +++-- .../builder/ProgressEventDispatcherTest.java | 17 +++++ .../cloud/tools/jib/http/ConnectionTest.java | 6 +- .../jib/http/TestBlobProgressListener.java | 48 +++++++++++++ .../tools/jib/registry/BlobPullerTest.java | 5 +- .../tools/jib/registry/BlobPusherTest.java | 31 +++++++-- 20 files changed, 345 insertions(+), 100 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 20db8e0a78..e3931cb60a 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -23,11 +23,7 @@ repositories { sourceSets { integrationTest { - java { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/java') - } + java.srcDir file('src/integration-test/java') resources.srcDir file('src/integration-test/resources') } } @@ -52,6 +48,13 @@ dependencies { // NullAway errorprone plugin apt 'com.uber.nullaway:nullaway:0.4.2' errorprone 'com.google.errorprone:error_prone_core:2.2.0' + + integrationTestCompile sourceSets.main.output + integrationTestCompile sourceSets.test.output + integrationTestCompile configurations.compile + integrationTestCompile configurations.testCompile + integrationTestCompile configurations.runtime + integrationTestCompile configurations.testRuntime } jar { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index b5672616c4..1368525e41 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.io.ByteStreams; @@ -55,7 +56,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls a layer BLOB of the busybox image. CountingDigestOutputStream layerOutputStream = new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - LongAdder byteCount = new LongAdder(); + LongAdder totalByteCount = new LongAdder(); LongAdder expectedSize = new LongAdder(); registryClient .pullBlob( @@ -64,10 +65,10 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti Assert.assertEquals(0, expectedSize.sum()); expectedSize.add(size); }, - byteCount::add) + new TestBlobProgressListener(totalByteCount::add)) .writeTo(layerOutputStream); Assert.assertTrue(expectedSize.sum() > 0); - Assert.assertEquals(expectedSize.sum(), byteCount.sum()); + Assert.assertEquals(expectedSize.sum(), totalByteCount.sum()); Assert.assertEquals(realDigest, layerOutputStream.toBlobDescriptor().getDigest()); } @@ -86,7 +87,7 @@ public void testPull_unknownBlob() throws IOException, DigestException, Interrup try { registryClient - .pullBlob(nonexistentDigest, ignored -> {}, ignored -> {}) + .pullBlob(nonexistentDigest, ignored -> {}, new TestBlobProgressListener(ignored -> {})) .writeTo(ByteStreams.nullOutputStream()); Assert.fail("Trying to pull nonexistent blob should have errored"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index fb737ae18a..3d0ef643e0 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.security.DigestException; @@ -46,6 +47,8 @@ public void testPush() RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); + Assert.assertFalse( + registryClient.pushBlob( + testBlobDigest, testBlob, null, new TestBlobProgressListener(ignored -> {}))); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index cdb083290f..67a444f4c7 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -87,13 +88,14 @@ public void testPush() .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse( - registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); + registryClient.pushBlob( + testLayerBlobDigest, testLayerBlob, null, new TestBlobProgressListener(ignored -> {}))); Assert.assertFalse( registryClient.pushBlob( testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null, - ignored -> {})); + new TestBlobProgressListener(ignored -> {}))); // Pushes the manifest. DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index d34d237faf..5b01cc3f06 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -74,7 +74,10 @@ public static ProgressEventDispatcher newRoot( */ private static ProgressEventDispatcher newProgressEventDispatcher( EventDispatcher eventDispatcher, Allocation allocation) { - return new ProgressEventDispatcher(eventDispatcher, allocation).dispatchProgress(0); + ProgressEventDispatcher progressEventDispatcher = + new ProgressEventDispatcher(eventDispatcher, allocation); + progressEventDispatcher.dispatchProgress(0); + return progressEventDispatcher; } private final EventDispatcher eventDispatcher; @@ -123,10 +126,15 @@ public void close() { closed = true; } - private ProgressEventDispatcher dispatchProgress(long progressUnits) { + /** + * Dispatches a {@link ProgressEvent} representing {@code progressUnits} of progress on the + * managed {@link #allocation}. + * + * @param progressUnits units of progress + */ + public void dispatchProgress(long progressUnits) { decrementRemainingAllocationUnits(progressUnits); eventDispatcher.dispatch(new ProgressEvent(allocation, progressUnits)); - return this; } private void decrementRemainingAllocationUnits(long units) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java new file mode 100644 index 0000000000..dec33f0f5e --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.http.BlobProgressListener; +import com.google.common.base.Preconditions; +import java.io.Closeable; +import java.time.Duration; +import javax.annotation.Nullable; + +/** + * Contains a {@link ProgressEventDispatcher}. This class is mutable and should only be used within + * a local context. + * + *

This class is necessary because the total BLOb size (allocation units) is not known until the + * response headers are received, only after which can the {@link ProgressEventDispatcher} be + * created. + */ +class ProgressEventDispatcherContainer implements BlobProgressListener, Closeable { + + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final String description; + @Nullable private ProgressEventDispatcher progressEventDispatcher; + + ProgressEventDispatcherContainer( + ProgressEventDispatcher.Factory progressEventDispatcherFactory, String description) { + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.description = description; + } + + @Override + public void handleByteCount(long byteCount) { + Preconditions.checkNotNull(progressEventDispatcher); + progressEventDispatcher.dispatchProgress(byteCount); + } + + @Override + public Duration getDelayBetweenCallbacks() { + return Duration.ofMillis(100); + } + + @Override + public void close() { + Preconditions.checkNotNull(progressEventDispatcher); + progressEventDispatcher.close(); + } + + void initializeWithBlobSize(long blobSize) { + Preconditions.checkState(progressEventDispatcher == null); + progressEventDispatcher = progressEventDispatcherFactory.create(description, blobSize); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 18c945e4c9..8ed4a60a3f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -67,9 +67,9 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { - try (ProgressEventDispatcher ignored = + try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pull base image layer " + layerDigest, 1); - TimerEventDispatcher ignored2 = + TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { Cache cache = buildConfiguration.getBaseImageLayersCache(); @@ -86,12 +86,16 @@ public CachedLayer call() throws IOException, CacheCorruptedException { .setAuthorization(pullAuthorization) .newRegistryClient(); - return cache.writeCompressedLayer( - registryClient.pullBlob( - layerDigest, - // TODO: Replace with progress-reporting. - alsoIgnored -> {}, - alsoIgnored -> {})); + try (ProgressEventDispatcherContainer progressEventDispatcherContainer = + new ProgressEventDispatcherContainer( + progressEventDispatcher.newChildProducer(), + "pull base image layer blob " + layerDigest)) { + return cache.writeCompressedLayer( + registryClient.pullBlob( + layerDigest, + progressEventDispatcherContainer::initializeWithBlobSize, + progressEventDispatcherContainer)); + } } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index cd384ebe21..d564e4f3a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -25,8 +25,10 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -121,7 +123,7 @@ public BaseImageWithAuthorization call() new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. try { - return new BaseImageWithAuthorization(pullBaseImage(null), null); + return new BaseImageWithAuthorization(pullBaseImage(null, progressEventDispatcher), null); } catch (RegistryUnauthorizedException ex) { buildConfiguration @@ -151,7 +153,7 @@ public BaseImageWithAuthorization call() try { return new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); + pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. @@ -175,7 +177,7 @@ public BaseImageWithAuthorization call() registryAuthenticator.setAuthorization(registryAuthorization).authenticatePull(); return new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization), registryAuthorization); + pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); } } } @@ -185,6 +187,8 @@ public BaseImageWithAuthorization call() * Pulls the base image. * * @param registryAuthorization authentication credentials to possibly use + * @param progressEventDispatcher the {@link ProgressEventDispatcher} for emitting {@link + * ProgressEvent}s * @return the pulled image * @throws IOException when an I/O exception occurs during the pulling * @throws RegistryException if communicating with the registry caused a known error @@ -194,7 +198,9 @@ public BaseImageWithAuthorization call() * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - private Image pullBaseImage(@Nullable Authorization registryAuthorization) + private Image pullBaseImage( + @Nullable Authorization registryAuthorization, + ProgressEventDispatcher progressEventDispatcher) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { RegistryClient registryClient = @@ -221,18 +227,25 @@ private Image pullBaseImage(@Nullable Authorization registryAuthorization + Blobs.writeToString(JsonTemplateMapper.toBlob(v22ManifestTemplate))); } - String containerConfigurationString = - Blobs.writeToString( - registryClient.pullBlob( - v22ManifestTemplate.getContainerConfiguration().getDigest(), - // TODO: Replace with progress updates. - ignored -> {}, - ignored -> {})); - - ContainerConfigurationTemplate containerConfigurationTemplate = - JsonTemplateMapper.readJson( - containerConfigurationString, ContainerConfigurationTemplate.class); - return JsonToImageTranslator.toImage(v22ManifestTemplate, containerConfigurationTemplate); + DescriptorDigest containerConfigurationDigest = + v22ManifestTemplate.getContainerConfiguration().getDigest(); + + try (ProgressEventDispatcherContainer progressEventDispatcherContainer = + new ProgressEventDispatcherContainer( + progressEventDispatcher.newChildProducer(), + "pull container configuration " + containerConfigurationDigest)) { + String containerConfigurationString = + Blobs.writeToString( + registryClient.pullBlob( + containerConfigurationDigest, + progressEventDispatcherContainer::initializeWithBlobSize, + progressEventDispatcherContainer)); + + ContainerConfigurationTemplate containerConfigurationTemplate = + JsonTemplateMapper.readJson( + containerConfigurationString, ContainerConfigurationTemplate.class); + return JsonToImageTranslator.toImage(v22ManifestTemplate, containerConfigurationTemplate); + } } throw new IllegalStateException("Unknown manifest schema version"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 075c35f0aa..46a41572ed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -25,17 +25,38 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; +import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; /** Pushes a BLOB to the target registry. */ class PushBlobStep implements AsyncStep, Callable { + private static class ForwardingProgressListener implements BlobProgressListener { + + private final ProgressEventDispatcher progressEventDispatcher; + + private ForwardingProgressListener(ProgressEventDispatcher progressEventDispatcher) { + this.progressEventDispatcher = progressEventDispatcher; + } + + @Override + public void handleByteCount(long byteCount) { + progressEventDispatcher.dispatchProgress(byteCount); + } + + @Override + public Duration getDelayBetweenCallbacks() { + return Duration.ofMillis(100); + } + } + private static final String DESCRIPTION = "Pushing BLOB "; private final BuildConfiguration buildConfiguration; @@ -73,9 +94,10 @@ public ListenableFuture getFuture() { @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { - try (ProgressEventDispatcher ignored = - progressEventDipatcherFactory.create("push blob " + blobDescriptor.getDigest(), 1); - TimerEventDispatcher ignored2 = + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDipatcherFactory.create( + "push blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); + TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { RegistryClient registryClient = @@ -97,9 +119,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc blobDescriptor.getDigest(), blob, null, - alsoIgnored -> { - // TODO: Replace with progress-reporting. - }); + new ForwardingProgressListener(progressEventDispatcher)); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java index de860c63e0..ad1fe85e73 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java @@ -20,22 +20,20 @@ import com.google.cloud.tools.jib.blob.Blob; import java.io.IOException; import java.io.OutputStream; -import java.time.Duration; -import java.util.function.Consumer; +import javax.annotation.Nullable; /** {@link Blob}-backed {@link HttpContent}. */ public class BlobHttpContent implements HttpContent { private final Blob blob; private final String contentType; - // TODO: Refactor into BlobPushMonitor or something. - private final Consumer sentByteCountConsumer; - private final Duration delayBetweenCallbacks = Duration.ofMillis(100); + @Nullable private final BlobProgressListener blobProgressListener; - public BlobHttpContent(Blob blob, String contentType, Consumer sentByteCountConsumer) { + public BlobHttpContent( + Blob blob, String contentType, @Nullable BlobProgressListener blobProgressListener) { this.blob = blob; this.contentType = contentType; - this.sentByteCountConsumer = sentByteCountConsumer; + this.blobProgressListener = blobProgressListener; } @Override @@ -56,10 +54,14 @@ public boolean retrySupported() { @Override public void writeTo(OutputStream outputStream) throws IOException { - ListenableCountingOutputStream listenableCountingOutputStream = - new ListenableCountingOutputStream( - outputStream, sentByteCountConsumer, delayBetweenCallbacks); - blob.writeTo(listenableCountingOutputStream); - listenableCountingOutputStream.flush(); + outputStream = + blobProgressListener == null + ? outputStream + : new ListenableCountingOutputStream( + outputStream, + blobProgressListener::handleByteCount, + blobProgressListener.getDelayBetweenCallbacks()); + blob.writeTo(outputStream); + outputStream.flush(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java new file mode 100644 index 0000000000..499e36a920 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.time.Duration; + +/** Listens to BLOb send/receive progress. */ +public interface BlobProgressListener { + + /** + * Callback to call with count of newly-processed bytes. + * + * @param byteCount sent/received byte count + */ + void handleByteCount(long byteCount); + + /** + * Gets the minimum delay between calls to {@link #handleByteCount}. + * + * @return the delay between callbacks + */ + Duration getDelayBetweenCallbacks(); +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index d2019320dc..b0534dab42 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -19,6 +19,7 @@ import com.google.api.client.http.HttpMethods; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.http.ListenableCountingOutputStream; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; @@ -26,7 +27,6 @@ import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; -import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -45,29 +45,31 @@ class BlobPuller implements RegistryEndpointProvider { */ private final OutputStream destinationOutputStream; - // TODO: Refactor into BlobPullMonitor. - private final Consumer sizeConsumer; - private final Duration delayBetweenCallbacks = Duration.ofMillis(100); + private final Consumer blobSizeConsumer; + private final BlobProgressListener blobProgressListener; BlobPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, OutputStream destinationOutputStream, - Consumer sizeConsumer, - Consumer receivedByteCountConsumer) { + Consumer blobSizeConsumer, + BlobProgressListener blobProgressListener) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; - this.destinationOutputStream = - new ListenableCountingOutputStream( - destinationOutputStream, receivedByteCountConsumer, delayBetweenCallbacks); - this.sizeConsumer = sizeConsumer; + this.destinationOutputStream = destinationOutputStream; + this.blobSizeConsumer = blobSizeConsumer; + this.blobProgressListener = blobProgressListener; } @Override public Void handleResponse(Response response) throws IOException, UnexpectedBlobDigestException { - sizeConsumer.accept(response.getContentLength()); + blobSizeConsumer.accept(response.getContentLength()); - try (OutputStream outputStream = destinationOutputStream) { + try (OutputStream outputStream = + new ListenableCountingOutputStream( + destinationOutputStream, + blobProgressListener::handleByteCount, + blobProgressListener.getDelayBetweenCallbacks())) { BlobDescriptor receivedBlobDescriptor = response.getBody().writeTo(outputStream); if (!blobDigest.equals(receivedBlobDescriptor.getDigest())) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 2084ac140b..8cb6fb1e23 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -20,6 +20,7 @@ import com.google.api.client.http.HttpMethods; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.net.MediaType; @@ -28,7 +29,6 @@ import java.net.URL; import java.util.Collections; import java.util.List; -import java.util.function.Consumer; import javax.annotation.Nullable; /** @@ -112,12 +112,12 @@ public String getActionDescription() { private class Writer implements RegistryEndpointProvider { private final URL location; - private final Consumer sentByteCountConsumer; + private final BlobProgressListener blobProgressListener; @Nullable @Override public BlobHttpContent getContent() { - return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), sentByteCountConsumer); + return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), blobProgressListener); } @Override @@ -147,9 +147,9 @@ public String getActionDescription() { return BlobPusher.this.getActionDescription(); } - private Writer(URL location, Consumer sentByteCountConsumer) { + private Writer(URL location, BlobProgressListener blobProgressListener) { this.location = location; - this.sentByteCountConsumer = sentByteCountConsumer; + this.blobProgressListener = blobProgressListener; } } @@ -216,11 +216,11 @@ RegistryEndpointProvider initializer() { /** * @param location the upload URL - * @param sentByteCountConsumer callback to receive counts of bytes sent + * @param blobProgressListener the listener for {@link Blob} push progress * @return a {@link RegistryEndpointProvider} for writing the BLOB to an upload location */ - RegistryEndpointProvider writer(URL location, Consumer sentByteCountConsumer) { - return new Writer(location, sentByteCountConsumer); + RegistryEndpointProvider writer(URL location, BlobProgressListener blobProgressListener) { + return new Writer(location, blobProgressListener); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 726ff41624..87834575ba 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -84,9 +84,8 @@ public BlobHttpContent getContent() { return new BlobHttpContent( JsonTemplateMapper.toBlob(manifestTemplate), manifestTemplate.getManifestMediaType(), - ignored -> { - // TODO: Consider giving progress on manifest push as well? - }); + // TODO: Consider giving progress on manifest push as well? + null); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 7e759e8295..1f8d23c684 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; @@ -245,15 +246,14 @@ public BlobDescriptor checkBlob(DescriptorDigest blobDigest) * written out. * * @param blobDigest the digest of the BLOB to download - * @param blobSizeConsumer callback to receive the total size of the BLOB to pull - * @param receivedByteCountConsumer callback to receive counts of bytes received + * @param blobSizeConsumer callback to receive the total size of the BLOb to pull + * @param blobProgressListener listener for progress of the pull * @return a {@link Blob} */ - // TODO: Refactor callbacks into BlobPullMonitor. public Blob pullBlob( DescriptorDigest blobDigest, Consumer blobSizeConsumer, - Consumer receivedByteCountConsumer) { + BlobProgressListener blobProgressListener) { return Blobs.from( outputStream -> { try { @@ -263,7 +263,7 @@ public Blob pullBlob( blobDigest, outputStream, blobSizeConsumer, - receivedByteCountConsumer)); + blobProgressListener)); } catch (RegistryException ex) { throw new IOException(ex); @@ -279,18 +279,17 @@ public Blob pullBlob( * @param blob the BLOB to push * @param sourceRepository if pushing to the same registry then the source image, or {@code null} * otherwise; used to optimize the BLOB push - * @param sentByteCountConsumer callback to receive counts of bytes sent + * @param blobProgressListener listener for BLOb push progress * @return {@code true} if the BLOB already exists on the registry and pushing was skipped; false * if the BLOB was pushed * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - // TODO: Refactor callbacks into BlobPushMonitor. public boolean pushBlob( DescriptorDigest blobDigest, Blob blob, @Nullable String sourceRepository, - Consumer sentByteCountConsumer) + BlobProgressListener blobProgressListener) throws IOException, RegistryException { BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); @@ -312,7 +311,7 @@ public boolean pushBlob( // PATCH with BLOB URL putLocation = - callRegistryEndpoint(blobPusher.writer(patchLocation, sentByteCountConsumer)); + callRegistryEndpoint(blobPusher.writer(patchLocation, blobProgressListener)); Preconditions.checkNotNull(putLocation); timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java index 2192219ccc..2c92cd8929 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -58,6 +58,23 @@ public void testDispatch() { Assert.assertEquals(9, progressEvents.get(3).getUnits()); } + @Test + public void testDispatch_tooMuchProgress() { + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 10)) { + progressEventDispatcher.dispatchProgress(10); + try { + progressEventDispatcher.dispatchProgress(10); + Assert.fail(); + + } catch (VerifyException ex) { + Assert.assertEquals( + "Remaining allocation units less than 0 for 'allocation description': -10", + ex.getMessage()); + } + } + } + @Test public void testDispatch_tooManyChildren() { try (ProgressEventDispatcher progressEventDispatcher = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index 8b08508524..3732146904 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -58,7 +58,7 @@ private interface SendFunction { private Request fakeRequest; private HttpResponse mockHttpResponse; - private long byteCount = 0; + private long totalByteCount = 0; @InjectMocks private final Connection testConnection = @@ -113,7 +113,7 @@ private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { new BlobHttpContent( Blobs.from("crepecake"), "fake.content.type", - sentByteCount -> byteCount += sentByteCount)) + new TestBlobProgressListener(byteCount -> totalByteCount += byteCount))) .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) .setHttpTimeout(httpTimeout) .build(); @@ -159,6 +159,6 @@ private void testSend(String httpMethod, SendFunction sendFunction) throws IOExc Assert.assertEquals( "crepecake", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals("crepecake".length(), byteCount); + Assert.assertEquals("crepecake".length(), totalByteCount); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java new file mode 100644 index 0000000000..e0e24d0874 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.time.Duration; +import java.util.function.Consumer; + +/** + * Test implementation of {@link BlobProgressListener} that always forwards to a {@link Consumer + * }. + */ +public class TestBlobProgressListener implements BlobProgressListener, Consumer { + + private final Consumer byteCountConsumer; + + public TestBlobProgressListener(Consumer byteCountConsumer) { + this.byteCountConsumer = byteCountConsumer; + } + + @Override + public void handleByteCount(long byteCount) { + byteCountConsumer.accept(byteCount); + } + + @Override + public Duration getDelayBetweenCallbacks() { + return Duration.ofSeconds(-1); + } + + @Override + public void accept(Long byteCount) { + handleByteCount(byteCount); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index cab84ce540..e0eb9311cd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; @@ -62,7 +63,7 @@ public void setUpFakes() throws DigestException { fakeDigest, layerOutputStream, ignored -> {}, - ignored -> {}); + new TestBlobProgressListener(ignored -> {})); } @Test @@ -81,7 +82,7 @@ public void testHandleResponse() throws IOException, UnexpectedBlobDigestExcepti testBlobDigest, layerOutputStream, size -> Assert.assertEquals("some BLOB content".length(), size.longValue()), - byteCount::add); + new TestBlobProgressListener(byteCount::add)); blobPuller.handleResponse(mockResponse); Assert.assertEquals( "some BLOB content", diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index 2c44c32d63..9066005762 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -163,7 +164,8 @@ public void testInitializer_getActionDescription() { @Test public void testWriter_getContent() throws IOException { LongAdder byteCount = new LongAdder(); - BlobHttpContent body = testBlobPusher.writer(mockURL, byteCount::add).getContent(); + BlobHttpContent body = + testBlobPusher.writer(mockURL, new TestBlobProgressListener(byteCount::add)).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); @@ -178,7 +180,12 @@ public void testWriter_getContent() throws IOException { @Test public void testWriter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.writer(mockURL, ignored -> {}).getAccept().size()); + Assert.assertEquals( + 0, + testBlobPusher + .writer(mockURL, new TestBlobProgressListener(ignored -> {})) + .getAccept() + .size()); } @Test @@ -189,25 +196,37 @@ public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), - testBlobPusher.writer(mockURL, ignored -> {}).handleResponse(mockResponse)); + testBlobPusher + .writer(mockURL, new TestBlobProgressListener(ignored -> {})) + .handleResponse(mockResponse)); } @Test public void testWriter_getApiRoute() throws MalformedURLException { URL fakeUrl = new URL("http://someurl"); - Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl, ignored -> {}).getApiRoute("")); + Assert.assertEquals( + fakeUrl, + testBlobPusher + .writer(fakeUrl, new TestBlobProgressListener(ignored -> {})) + .getApiRoute("")); } @Test public void testWriter_getHttpMethod() { - Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL, ignored -> {}).getHttpMethod()); + Assert.assertEquals( + "PATCH", + testBlobPusher + .writer(mockURL, new TestBlobProgressListener(ignored -> {})) + .getHttpMethod()); } @Test public void testWriter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.writer(mockURL, ignored -> {}).getActionDescription()); + testBlobPusher + .writer(mockURL, new TestBlobProgressListener(ignored -> {})) + .getActionDescription()); } @Test From 22f9492742bc2f85284fb3c8a610673403cf0bc3 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Dec 2018 15:18:52 -0500 Subject: [PATCH 0392/2020] Suppresses warning about ignored Future in FinalizingStep. (#1342) --- .../tools/jib/builder/steps/FinalizingStep.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index 5dce1eea73..d1810cd134 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** Logs the message before finalizing an image build. */ class FinalizingStep implements AsyncStep, Callable { @@ -74,12 +75,13 @@ public Void call() throws ExecutionException { asyncDependencies.addSteps(NonBlockingSteps.get(wrappedDependency)); } - // TODO: Don't let future error be suppressed - asyncDependencies.whenAllSucceed( - () -> { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); - return null; - }); + // This suppresses any exceptions of this future. + Future ignored = + asyncDependencies.whenAllSucceed( + () -> { + buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); + return null; + }); return null; } From 06574404f8b552926a802992b614ddcd35e401f1 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Dec 2018 15:34:17 -0500 Subject: [PATCH 0393/2020] Checks progress is 1.0 in build integration tests. (#1339) --- .../builder/BuildStepsIntegrationTest.java | 62 ++++++++++++++----- .../event/progress/ProgressEventHandler.java | 8 +-- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 9d35ec4c76..cf20e3f601 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -23,6 +23,10 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; @@ -60,6 +64,9 @@ public class BuildStepsIntegrationTest { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); private static final ExecutorService executorService = Executors.newCachedThreadPool(); + private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); + + private static final double DOUBLE_ERROR_MARGIN = 1e-10; @AfterClass public static void cleanUp() { @@ -112,8 +119,6 @@ private static void assertDockerInspect(String imageReference) Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); } - private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private ImmutableList fakeLayerConfigurations; @@ -131,18 +136,32 @@ public void setUp() throws IOException, URISyntaxException { @Test public void testSteps_forBuildToDockerRegistry() throws IOException, InterruptedException, ExecutionException { - BuildSteps buildImageSteps = - BuildSteps.forBuildToDockerRegistry( - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of("localhost:5000", "testimage", "testtag")) - .build()); + ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); long lastTime = System.nanoTime(); - BuildResult image1 = buildImageSteps.run(); + BuildResult image1 = + BuildSteps.forBuildToDockerRegistry( + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("localhost:5000", "testimage", "testtag")) + .setEventDispatcher( + new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) + .build()) + .run(); + Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + lastTime = System.nanoTime(); - BuildResult image2 = buildImageSteps.run(); + BuildResult image2 = + BuildSteps.forBuildToDockerRegistry( + getBuildConfigurationBuilder( + ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("localhost:5000", "testimage", "testtag")) + .build()) + .run(); logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); Assert.assertEquals(image1, image2); @@ -219,17 +238,24 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() @Test public void testSteps_forBuildToDockerDaemon() throws IOException, InterruptedException, ExecutionException { - String imageReference = "testdocker"; + ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( ImageReference.of("gcr.io", "distroless/java", "latest"), - ImageReference.of(null, imageReference, null)) + ImageReference.of(null, "testdocker", null)) + .setEventDispatcher( + new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) .build(); BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); - assertDockerInspect(imageReference); + Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + + assertDockerInspect("testdocker"); Assert.assertEquals( - "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); + "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testdocker").run()); } @Test @@ -260,14 +286,22 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() @Test public void testSteps_forBuildToTarball() throws IOException, InterruptedException, ExecutionException { + ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( ImageReference.of("gcr.io", "distroless/java", "latest"), ImageReference.of(null, "testtar", null)) + .setEventDispatcher( + new DefaultEventDispatcher( + new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) .build(); Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); BuildSteps.forBuildToTar(outputPath, buildConfiguration).run(); + Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + new Command("docker", "load", "--input", outputPath.toString()).run(); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java index 06ac50a771..d4b22b6885 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java @@ -27,7 +27,7 @@ * *

This implementation is thread-safe. */ -class ProgressEventHandler implements Consumer { +public class ProgressEventHandler implements Consumer { /** Keeps track of the progress for each {@link Allocation} encountered. */ private final AllocationCompletionTracker completionTracker = new AllocationCompletionTracker(); @@ -42,7 +42,7 @@ class ProgressEventHandler implements Consumer { */ private final Runnable updateNotifier; - ProgressEventHandler(Runnable updateNotifier) { + public ProgressEventHandler(Runnable updateNotifier) { this.updateNotifier = updateNotifier; } @@ -67,7 +67,7 @@ public void accept(ProgressEvent progressEvent) { * * @return the overall progress */ - double getProgress() { + public double getProgress() { return progress.sum(); } @@ -79,7 +79,7 @@ public void accept(ProgressEvent progressEvent) { */ // TODO: Change this to do every time update notifier is called, so this is not called many times // per update. - List getUnfinishedAllocations() { + public List getUnfinishedAllocations() { return completionTracker.getUnfinishedAllocations(); } } From 9137f2fa10b5b36b6617c91a0982e00b426c9aca Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 12 Dec 2018 15:55:56 -0500 Subject: [PATCH 0394/2020] Improves the logic of AnsiLoggerWithFooter. (#1340) --- .../plugins/common/AnsiLoggerWithFooter.java | 105 +++++++++++------- .../common/AnsiLoggerWithFooterTest.java | 77 ++++++++----- 2 files changed, 117 insertions(+), 65 deletions(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java index 35c448843d..83dc651127 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java @@ -51,17 +51,24 @@ class AnsiLoggerWithFooter { private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - private final Consumer plainLogger; + private final Consumer plainPrinter; private List footerLines = Collections.emptyList(); - AnsiLoggerWithFooter(Consumer plainLogger) { - this.plainLogger = plainLogger; + /** + * Creates a new {@link AnsiLoggerWithFooter} + * + * @param plainPrinter the {@link Consumer} intended to synchronously print the footer and other + * plain console output. {@code plainPrinter} should print a new line at the end. + */ + AnsiLoggerWithFooter(Consumer plainPrinter) { + this.plainPrinter = plainPrinter; } /** Shuts down the {@link #executorService}. */ - public void shutDown() { + public AnsiLoggerWithFooter shutDown() { executorService.shutdown(); + return this; } /** Waits for the {@link #executorService} to terminate. */ @@ -79,12 +86,26 @@ public void awaitTermination() { /** * Runs {@code messageLogger} asynchronously. * - * @param messageLogger the {@link Runnable} intended to synchronously log a message to the - * console + * @param messageLogger the {@link Consumer} intended to synchronously log a message to the + * console. {@code messageLogger} should print a new line at the end. + * @param message the message to log with {@code messageLogger} * @return a {@link Future} to track completion */ - public Future log(Runnable messageLogger) { - return log(messageLogger, footerLines); + public Future log(Consumer messageLogger, String message) { + return executorService.submit( + () -> { + boolean didErase = eraseFooter(); + + // If a previous footer was erased, the message needs to go up a line. + String messagePrefix = didErase ? CURSOR_UP_SEQUENCE : ""; + messageLogger.accept(messagePrefix + message); + + for (String footerLine : footerLines) { + plainPrinter.accept(BOLD + footerLine + UNBOLD); + } + + return null; + }); } /** @@ -93,49 +114,57 @@ public Future log(Runnable messageLogger) { * *

The footer is printed in bold. * - * @param footerLines the footer, with each line as an element (no newline at end) + * @param newFooterLines the footer, with each line as an element (no newline at end) * @return a {@link Future} to track completion */ - public Future setFooter(List footerLines) { - if (footerLines.equals(this.footerLines)) { + public Future setFooter(List newFooterLines) { + if (newFooterLines.equals(footerLines)) { return Futures.immediateFuture(null); } - return log(() -> {}, footerLines); - } - - private Future log(Runnable messageLogger, List newFooterLines) { return executorService.submit( () -> { - StringBuilder plainLogBuilder = new StringBuilder(); + boolean didErase = eraseFooter(); - // Moves the cursor up to the start of the footer. - // TODO: Optimize to single init. - for (int i = 0; i < this.footerLines.size(); i++) { - // Moves cursor up. - plainLogBuilder.append(CURSOR_UP_SEQUENCE); - } + // If a previous footer was erased, the first new footer line needs to go up a line. + String newFooterPrefix = didErase ? CURSOR_UP_SEQUENCE : ""; - // Erases everything below cursor. - plainLogBuilder.append(ERASE_DISPLAY_BELOW); - - // Writes out logMessage and footer. - plainLogger.accept(plainLogBuilder.toString()); - messageLogger.run(); - - if (newFooterLines.size() > 0) { - StringBuilder footerBuilder = new StringBuilder(); - for (String newFooterLine : newFooterLines) { - footerBuilder.append(BOLD).append(newFooterLine).append(UNBOLD).append('\n'); - } - // Removes last newline. - footerBuilder.setLength(footerBuilder.length() - 1); - plainLogger.accept(footerBuilder.toString()); + for (String newFooterLine : newFooterLines) { + plainPrinter.accept(newFooterPrefix + BOLD + newFooterLine + UNBOLD); + newFooterPrefix = ""; } - this.footerLines = newFooterLines; + footerLines = newFooterLines; return null; }); } + + /** + * Erases the footer. Do not call outside of a task submitted to {@link + * #executorService}. + * + * @return {@code true} if anything was erased; {@code false} otherwise + */ + private boolean eraseFooter() { + if (footerLines.isEmpty()) { + return false; + } + + StringBuilder footerEraserBuilder = new StringBuilder(); + + // Moves the cursor up to the start of the footer. + // TODO: Optimize to single init. + for (int i = 0; i < footerLines.size(); i++) { + // Moves cursor up. + footerEraserBuilder.append(CURSOR_UP_SEQUENCE); + } + + // Erases everything below cursor. + footerEraserBuilder.append(ERASE_DISPLAY_BELOW); + + plainPrinter.accept(footerEraserBuilder.toString()); + + return true; + } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java index 924f947611..109ffb829a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java @@ -32,15 +32,15 @@ public class AnsiLoggerWithFooterTest { private final StringBuilder logBuilder = new StringBuilder(); private final AnsiLoggerWithFooter testAnsiLoggerWithFooter = - new AnsiLoggerWithFooter(logBuilder::append); + new AnsiLoggerWithFooter(this::logBuilderPrinter); @Test public void testLog_noFooter() throws InterruptedException, ExecutionException, TimeoutException { testAnsiLoggerWithFooter - .log(() -> logBuilder.append("message\n")) + .log(this::logBuilderPrinter, "message") .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); - Assert.assertEquals("\033[0Jmessage\n", logBuilder.toString()); + Assert.assertEquals("message\n", logBuilder.toString()); } @Test @@ -49,22 +49,32 @@ public void testLog_sameFooter() testAnsiLoggerWithFooter .setFooter(Collections.singletonList("footer")) .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals("\033[1mfooter\033[0m\n", logBuilder.toString()); + testAnsiLoggerWithFooter - .log(() -> logBuilder.append("message\n")) + .log(this::logBuilderPrinter, "message") .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals( + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1Amessage\n" + + "\033[1mfooter\033[0m\n", + logBuilder.toString()); + testAnsiLoggerWithFooter - .log(() -> logBuilder.append("another message\n")) + .log(this::logBuilderPrinter, "another message") .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); Assert.assertEquals( - "\033[0J" - + "\033[1mfooter\033[0m" - + "\033[1A\033[0J" - + "message\n" - + "\033[1mfooter\033[0m" - + "\033[1A\033[0J" - + "another message\n" - + "\033[1mfooter\033[0m", + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1Amessage\n" + + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1Aanother message\n" + + "\033[1mfooter\033[0m\n", logBuilder.toString()); } @@ -75,28 +85,41 @@ public void testLog_changingFooter() .setFooter(Collections.singletonList("footer")) .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); testAnsiLoggerWithFooter - .log(() -> logBuilder.append("message\n")) + .log(this::logBuilderPrinter, "message") .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter .setFooter(Arrays.asList("two line", "footer")) .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + + Assert.assertEquals( + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1Amessage\n" + + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1A\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n", + logBuilder.toString()); + testAnsiLoggerWithFooter - .log(() -> logBuilder.append("another message\n")) + .log(this::logBuilderPrinter, "another message") .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); Assert.assertEquals( - "\033[0J" - + "\033[1mfooter\033[0m" - + "\033[1A\033[0J" - + "message\n" - + "\033[1mfooter\033[0m" - + "\033[1A\033[0J" - + "\033[1mtwo line\033[0m\n" - + "\033[1mfooter\033[0m" - + "\033[1A\033[1A\033[0J" - + "another message\n" - + "\033[1mtwo line\033[0m\n" - + "\033[1mfooter\033[0m", + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1Amessage\n" + + "\033[1mfooter\033[0m\n" + + "\033[1A\033[0J\n" + + "\033[1A\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n" + + "\033[1A\033[1A\033[0J\n" + + "\033[1Aanother message\n" + + "\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n", logBuilder.toString()); } + + // This mimics a real log printer that always adds a new line at the end. + private void logBuilderPrinter(String message) { + logBuilder.append(message).append('\n'); + } } From 8b823990f8235129d6cf332bcc07468a21efeb1b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Dec 2018 12:02:51 -0500 Subject: [PATCH 0395/2020] Adds AnsiLoggerWithFooter to Maven/Gradle plugins. (#1341) --- .../tools/jib/gradle/BuildDockerTask.java | 3 ++ .../tools/jib/gradle/BuildImageTask.java | 3 ++ .../cloud/tools/jib/gradle/BuildTarTask.java | 3 ++ .../jib/gradle/GradleProjectProperties.java | 29 +++++++---- .../tools/jib/gradle/LogEventHandler.java | 32 +++++++----- .../gradle/GradleProjectPropertiesTest.java | 5 +- .../tools/jib/maven/BuildDockerMojo.java | 3 ++ .../cloud/tools/jib/maven/BuildImageMojo.java | 3 ++ .../cloud/tools/jib/maven/BuildTarMojo.java | 3 ++ .../tools/jib/maven/LogEventHandler.java | 25 ++++++--- .../jib/maven/MavenProjectProperties.java | 29 ++++++++--- .../jib/maven/MavenProjectPropertiesTest.java | 6 +-- .../plugins/common/AnsiLoggerWithFooter.java | 48 ++++++++--------- .../jib/plugins/common/ProjectProperties.java | 4 ++ .../common/AnsiLoggerWithFooterTest.java | 51 +++++-------------- 15 files changed, 144 insertions(+), 103 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index a635ec90be..ac5b4caa8b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -146,6 +146,9 @@ public void buildDocker() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index db7624c193..f669686e5a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -127,6 +127,9 @@ public void buildImage() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 9cd0e052d5..1c98bd09db 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -144,6 +144,9 @@ public void buildTar() projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 7a9377338e..329243d8b0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; @@ -67,7 +68,7 @@ static GradleProjectProperties getForProject( try { return new GradleProjectProperties( project, - makeEventHandlers(logger), + logger, GradleLayerConfigurations.getForProject( project, logger, extraDirectory, convertPermissionsMap(permissions), appRoot)); @@ -76,13 +77,15 @@ static GradleProjectProperties getForProject( } } - private static EventHandlers makeEventHandlers(Logger logger) { - LogEventHandler logEventHandler = new LogEventHandler(logger); + private static EventHandlers makeEventHandlers( + Logger logger, AnsiLoggerWithFooter ansiLoggerWithFooter) { + LogEventHandler logEventHandler = new LogEventHandler(logger, ansiLoggerWithFooter); + TimerEventHandler timerEventHandler = + new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); + return new EventHandlers() .add(JibEventType.LOGGING, logEventHandler) - .add( - JibEventType.TIMING, - new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message)))); + .add(JibEventType.TIMING, timerEventHandler); } @Nullable @@ -100,17 +103,18 @@ static Path getExplodedWarDirectory(Project project) { } private final Project project; + private final AnsiLoggerWithFooter ansiLoggerWithFooter; private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting GradleProjectProperties( - Project project, - EventHandlers eventHandlers, - JavaLayerConfigurations javaLayerConfigurations) { + Project project, Logger logger, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventHandlers = eventHandlers; this.javaLayerConfigurations = javaLayerConfigurations; + + ansiLoggerWithFooter = new AnsiLoggerWithFooter(logger::lifecycle); + eventHandlers = makeEventHandlers(logger, ansiLoggerWithFooter); } @Override @@ -118,6 +122,11 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } + @Override + public void waitForLoggingThread() { + ansiLoggerWithFooter.shutDownAndAwaitTermination(); + } + @Override public EventHandlers getEventHandlers() { return eventHandlers; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java index 6084b6ae2d..5ee4c4f4c1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java @@ -17,46 +17,52 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.event.events.LogEvent; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import java.util.function.Consumer; import org.gradle.api.logging.Logger; /** Handles {@link LogEvent}s by passing to the Gradle {@link Logger}. */ -// We don't care about the return values of the logging futures. -@SuppressWarnings("FutureReturnValueIgnored") class LogEventHandler implements Consumer { - /** This executor keeps all log messages in order. */ - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - private final Logger logger; + private final AnsiLoggerWithFooter ansiLoggerWithFooter; - LogEventHandler(Logger logger) { + LogEventHandler(Logger logger, AnsiLoggerWithFooter ansiLoggerWithFooter) { this.logger = logger; + this.ansiLoggerWithFooter = ansiLoggerWithFooter; } @Override public void accept(LogEvent logEvent) { switch (logEvent.getLevel()) { case LIFECYCLE: - executorService.submit(() -> logger.lifecycle(logEvent.getMessage())); + if (logger.isLifecycleEnabled()) { + ansiLoggerWithFooter.log(logger::lifecycle, logEvent.getMessage()); + } break; case DEBUG: - executorService.submit(() -> logger.debug(logEvent.getMessage())); + if (logger.isDebugEnabled()) { + ansiLoggerWithFooter.log(logger::debug, logEvent.getMessage()); + } break; case ERROR: - executorService.submit(() -> logger.error(logEvent.getMessage())); + if (logger.isErrorEnabled()) { + ansiLoggerWithFooter.log(logger::error, logEvent.getMessage()); + } break; case INFO: - executorService.submit(() -> logger.info(logEvent.getMessage())); + if (logger.isInfoEnabled()) { + ansiLoggerWithFooter.log(logger::info, logEvent.getMessage()); + } break; case WARN: - executorService.submit(() -> logger.warn("warning: " + logEvent.getMessage())); + if (logger.isWarnEnabled()) { + ansiLoggerWithFooter.log(logger::warn, "warning: " + logEvent.getMessage()); + } break; default: diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 97000fe5d3..73f5fb7572 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.common.collect.ImmutableMap; @@ -27,6 +26,7 @@ import org.gradle.api.internal.file.FileResolver; import org.gradle.api.java.archives.Manifest; import org.gradle.api.java.archives.internal.DefaultManifest; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.TaskContainer; @@ -52,6 +52,7 @@ public class GradleProjectPropertiesTest { @Mock private WarPluginConvention mockWarPluginConvection; @Mock private TaskContainer mockTaskContainer; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; + @Mock private Logger mockLogger; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -68,7 +69,7 @@ public void setup() { Mockito.when(mockJar.getManifest()).thenReturn(manifest); gradleProjectProperties = - new GradleProjectProperties(mockProject, new EventHandlers(), mockJavaLayerConfigurations); + new GradleProjectProperties(mockProject, mockLogger, mockJavaLayerConfigurations); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index f02ba36913..106176372c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -110,6 +110,9 @@ public void execute() throws MojoExecutionException { eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); getLog().info(""); } catch (InvalidAppRootException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 51711c6c7f..6c9b0cbe2b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -126,6 +126,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); getLog().info(""); } catch (InvalidAppRootException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a88e60d926..506c49707e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -104,6 +104,9 @@ public void execute() throws MojoExecutionException { eventDispatcher, projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); + + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); getLog().info(""); } catch (InvalidAppRootException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java index 2c63939e23..8ebfbad89a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import java.util.function.Consumer; import org.apache.maven.plugin.logging.Log; @@ -24,33 +25,45 @@ class LogEventHandler implements Consumer { private final Log log; + private final AnsiLoggerWithFooter ansiLoggerWithFooter; - LogEventHandler(Log log) { + LogEventHandler(Log log, AnsiLoggerWithFooter ansiLoggerWithFooter) { this.log = log; + this.ansiLoggerWithFooter = ansiLoggerWithFooter; } @Override public void accept(LogEvent logEvent) { switch (logEvent.getLevel()) { case LIFECYCLE: - log.info(logEvent.getMessage()); + if (log.isInfoEnabled()) { + ansiLoggerWithFooter.log(log::info, logEvent.getMessage()); + } break; case DEBUG: - log.debug(logEvent.getMessage()); + if (log.isDebugEnabled()) { + ansiLoggerWithFooter.log(log::debug, logEvent.getMessage()); + } break; case ERROR: - log.error(logEvent.getMessage()); + if (log.isErrorEnabled()) { + ansiLoggerWithFooter.log(log::error, logEvent.getMessage()); + } break; case INFO: // Use lifecycle for progress-indicating messages. - log.debug(logEvent.getMessage()); + if (log.isDebugEnabled()) { + ansiLoggerWithFooter.log(log::debug, logEvent.getMessage()); + } break; case WARN: - log.warn(logEvent.getMessage()); + if (log.isWarnEnabled()) { + ansiLoggerWithFooter.log(log::warn, logEvent.getMessage()); + } break; default: diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index df12dbe483..a0d99f8555 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -19,8 +19,10 @@ import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; import com.google.common.annotations.VisibleForTesting; @@ -69,7 +71,7 @@ static MavenProjectProperties getForProject( try { return new MavenProjectProperties( project, - makeEventHandlers(log), + log, MavenLayerConfigurations.getForProject(project, extraDirectory, permissions, appRoot)); } catch (IOException ex) { @@ -81,24 +83,30 @@ static MavenProjectProperties getForProject( } } - private static EventHandlers makeEventHandlers(Log log) { + private static EventHandlers makeEventHandlers( + Log log, AnsiLoggerWithFooter ansiLoggerWithFooter) { + LogEventHandler logEventHandler = new LogEventHandler(log, ansiLoggerWithFooter); + TimerEventHandler timerEventHandler = + new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); + return new EventHandlers() - .add(JibEventType.LOGGING, new LogEventHandler(log)) - .add(JibEventType.TIMING, new TimerEventHandler(log::debug)); + .add(JibEventType.LOGGING, logEventHandler) + .add(JibEventType.TIMING, timerEventHandler); } private final MavenProject project; + private final AnsiLoggerWithFooter ansiLoggerWithFooter; private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @VisibleForTesting MavenProjectProperties( - MavenProject project, - EventHandlers eventHandlers, - JavaLayerConfigurations javaLayerConfigurations) { + MavenProject project, Log log, JavaLayerConfigurations javaLayerConfigurations) { this.project = project; - this.eventHandlers = eventHandlers; this.javaLayerConfigurations = javaLayerConfigurations; + + ansiLoggerWithFooter = new AnsiLoggerWithFooter(log::info); + eventHandlers = makeEventHandlers(log, ansiLoggerWithFooter); } @Override @@ -106,6 +114,11 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { return javaLayerConfigurations; } + @Override + public void waitForLoggingThread() { + ansiLoggerWithFooter.shutDownAndAwaitTermination(); + } + @Override public EventHandlers getEventHandlers() { return eventHandlers; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 30559b5fc3..4a2ccf54e6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.junit.Assert; @@ -36,6 +36,7 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; + @Mock private Log mockLog; private Xpp3Dom jarPluginConfiguration; private Xpp3Dom archive; @@ -47,8 +48,7 @@ public class MavenProjectPropertiesTest { @Before public void setup() { mavenProjectProperties = - new MavenProjectProperties( - mockMavenProject, new EventHandlers(), mockJavaLayerConfigurations); + new MavenProjectProperties(mockMavenProject, mockLog, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java index 83dc651127..f94095da49 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java @@ -16,13 +16,12 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.common.util.concurrent.Futures; +import com.google.common.annotations.VisibleForTesting; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -31,9 +30,9 @@ * always appears below log messages. This is intended to log both the messages and the footer to * the same console. * - *

Make sure to call {@link #shutDown} when finished. + *

Make sure to call {@link #shutDownAndAwaitTermination} when finished. */ -class AnsiLoggerWithFooter { +public class AnsiLoggerWithFooter { /** ANSI escape sequence for moving the cursor up one line. */ private static final String CURSOR_UP_SEQUENCE = "\033[1A"; @@ -49,8 +48,7 @@ class AnsiLoggerWithFooter { private static final Duration EXECUTOR_SHUTDOWN_WAIT = Duration.ofSeconds(1); - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - + private final ExecutorService executorService; private final Consumer plainPrinter; private List footerLines = Collections.emptyList(); @@ -61,24 +59,32 @@ class AnsiLoggerWithFooter { * @param plainPrinter the {@link Consumer} intended to synchronously print the footer and other * plain console output. {@code plainPrinter} should print a new line at the end. */ - AnsiLoggerWithFooter(Consumer plainPrinter) { + public AnsiLoggerWithFooter(Consumer plainPrinter) { + this(plainPrinter, Executors.newSingleThreadExecutor()); + } + + @VisibleForTesting + AnsiLoggerWithFooter(Consumer plainPrinter, ExecutorService executorService) { this.plainPrinter = plainPrinter; + this.executorService = executorService; } - /** Shuts down the {@link #executorService}. */ - public AnsiLoggerWithFooter shutDown() { + /** Shuts down the {@link #executorService} and waits for it to terminate. */ + public void shutDownAndAwaitTermination() { executorService.shutdown(); - return this; - } - /** Waits for the {@link #executorService} to terminate. */ - public void awaitTermination() { try { if (!executorService.awaitTermination( EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { executorService.shutdownNow(); + if (!executorService.awaitTermination( + EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + throw new RuntimeException("Could not shut down AnsiLoggerWithFooter executor"); + } } + } catch (InterruptedException ex) { + executorService.shutdownNow(); Thread.currentThread().interrupt(); } } @@ -89,10 +95,9 @@ public void awaitTermination() { * @param messageLogger the {@link Consumer} intended to synchronously log a message to the * console. {@code messageLogger} should print a new line at the end. * @param message the message to log with {@code messageLogger} - * @return a {@link Future} to track completion */ - public Future log(Consumer messageLogger, String message) { - return executorService.submit( + public void log(Consumer messageLogger, String message) { + executorService.execute( () -> { boolean didErase = eraseFooter(); @@ -103,8 +108,6 @@ public Future log(Consumer messageLogger, String message) { for (String footerLine : footerLines) { plainPrinter.accept(BOLD + footerLine + UNBOLD); } - - return null; }); } @@ -115,14 +118,13 @@ public Future log(Consumer messageLogger, String message) { *

The footer is printed in bold. * * @param newFooterLines the footer, with each line as an element (no newline at end) - * @return a {@link Future} to track completion */ - public Future setFooter(List newFooterLines) { + public void setFooter(List newFooterLines) { if (newFooterLines.equals(footerLines)) { - return Futures.immediateFuture(null); + return; } - return executorService.submit( + executorService.execute( () -> { boolean didErase = eraseFooter(); @@ -135,8 +137,6 @@ public Future setFooter(List newFooterLines) { } footerLines = newFooterLines; - - return null; }); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 59570c6326..a21c9dc28a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -33,6 +33,10 @@ public interface ProjectProperties { */ String EXPLODED_WAR_DIRECTORY_NAME = "jib-exploded-war"; + // TODO: Move out of ProjectProperties. + void waitForLoggingThread(); + + // TODO: Move out of ProjectProperties. EventHandlers getEventHandlers(); String getToolName(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java index 109ffb829a..2f07cabd51 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java @@ -16,45 +16,33 @@ package com.google.cloud.tools.jib.plugins.common; -import java.time.Duration; +import com.google.common.util.concurrent.MoreExecutors; import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.junit.Assert; import org.junit.Test; /** Tests for {@link AnsiLoggerWithFooter}. */ public class AnsiLoggerWithFooterTest { - private static final Duration FUTURE_TIMEOUT = Duration.ofSeconds(1); - private final StringBuilder logBuilder = new StringBuilder(); private final AnsiLoggerWithFooter testAnsiLoggerWithFooter = - new AnsiLoggerWithFooter(this::logBuilderPrinter); + new AnsiLoggerWithFooter(this::logBuilderPrinter, MoreExecutors.newDirectExecutorService()); @Test - public void testLog_noFooter() throws InterruptedException, ExecutionException, TimeoutException { - testAnsiLoggerWithFooter - .log(this::logBuilderPrinter, "message") - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + public void testLog_noFooter() { + testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); Assert.assertEquals("message\n", logBuilder.toString()); } @Test - public void testLog_sameFooter() - throws InterruptedException, ExecutionException, TimeoutException { - testAnsiLoggerWithFooter - .setFooter(Collections.singletonList("footer")) - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + public void testLog_sameFooter() { + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); Assert.assertEquals("\033[1mfooter\033[0m\n", logBuilder.toString()); - testAnsiLoggerWithFooter - .log(this::logBuilderPrinter, "message") - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); Assert.assertEquals( "\033[1mfooter\033[0m\n" @@ -63,9 +51,7 @@ public void testLog_sameFooter() + "\033[1mfooter\033[0m\n", logBuilder.toString()); - testAnsiLoggerWithFooter - .log(this::logBuilderPrinter, "another message") - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "another message"); Assert.assertEquals( "\033[1mfooter\033[0m\n" @@ -79,18 +65,11 @@ public void testLog_sameFooter() } @Test - public void testLog_changingFooter() - throws InterruptedException, ExecutionException, TimeoutException { - testAnsiLoggerWithFooter - .setFooter(Collections.singletonList("footer")) - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); - testAnsiLoggerWithFooter - .log(this::logBuilderPrinter, "message") - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); - - testAnsiLoggerWithFooter - .setFooter(Arrays.asList("two line", "footer")) - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + public void testLog_changingFooter() { + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); + testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); + + testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); Assert.assertEquals( "\033[1mfooter\033[0m\n" @@ -101,9 +80,7 @@ public void testLog_changingFooter() + "\033[1A\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n", logBuilder.toString()); - testAnsiLoggerWithFooter - .log(this::logBuilderPrinter, "another message") - .get(FUTURE_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "another message"); Assert.assertEquals( "\033[1mfooter\033[0m\n" From 3a47d661ec0e1629d34e44ff37b8544dec972644 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 13 Dec 2018 16:56:00 -0500 Subject: [PATCH 0396/2020] Changes ProgressEventHandler to provide the callback with updates. (#1343) --- .../builder/BuildStepsIntegrationTest.java | 45 ++++++++++---- .../progress/AllocationCompletionTracker.java | 10 +-- .../event/progress/ProgressEventHandler.java | 61 +++++++++++-------- .../progress/ProgressEventHandlerTest.java | 18 +++--- 4 files changed, 83 insertions(+), 51 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index cf20e3f601..0fdbe739dc 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -62,6 +62,27 @@ /** Integration tests for {@link BuildSteps}. */ public class BuildStepsIntegrationTest { + /** + * Helper class to hold a {@link ProgressEventHandler} and verify that it handles a full progress. + */ + private static class ProgressChecker { + + private final ProgressEventHandler progressEventHandler = + new ProgressEventHandler( + update -> { + lastProgress = update.getProgress(); + areTasksFinished = update.getUnfinishedAllocations().isEmpty(); + }); + + private volatile double lastProgress = 0.0; + private volatile boolean areTasksFinished = false; + + private void checkCompletion() { + Assert.assertEquals(1.0, lastProgress, DOUBLE_ERROR_MARGIN); + Assert.assertTrue(areTasksFinished); + } + } + @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); private static final ExecutorService executorService = Executors.newCachedThreadPool(); private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); @@ -136,7 +157,7 @@ public void setUp() throws IOException, URISyntaxException { @Test public void testSteps_forBuildToDockerRegistry() throws IOException, InterruptedException, ExecutionException { - ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + ProgressChecker progressChecker = new ProgressChecker(); long lastTime = System.nanoTime(); BuildResult image1 = @@ -146,11 +167,11 @@ public void testSteps_forBuildToDockerRegistry() ImageReference.of("localhost:5000", "testimage", "testtag")) .setEventDispatcher( new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) + new EventHandlers() + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) .build()) .run(); - Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); - Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + progressChecker.checkCompletion(); logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); @@ -238,7 +259,7 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() @Test public void testSteps_forBuildToDockerDaemon() throws IOException, InterruptedException, ExecutionException { - ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + ProgressChecker progressChecker = new ProgressChecker(); BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( @@ -246,12 +267,12 @@ public void testSteps_forBuildToDockerDaemon() ImageReference.of(null, "testdocker", null)) .setEventDispatcher( new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) + new EventHandlers() + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) .build(); BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); - Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); - Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + progressChecker.checkCompletion(); assertDockerInspect("testdocker"); Assert.assertEquals( @@ -286,7 +307,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() @Test public void testSteps_forBuildToTarball() throws IOException, InterruptedException, ExecutionException { - ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + ProgressChecker progressChecker = new ProgressChecker(); BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( @@ -294,13 +315,13 @@ public void testSteps_forBuildToTarball() ImageReference.of(null, "testtar", null)) .setEventDispatcher( new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler))) + new EventHandlers() + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) .build(); Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); BuildSteps.forBuildToTar(outputPath, buildConfiguration).run(); - Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); - Assert.assertTrue(progressEventHandler.getUnfinishedAllocations().isEmpty()); + progressChecker.checkCompletion(); new Command("docker", "load", "--input", outputPath.toString()).run(); Assert.assertEquals( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java index 1ff1c10523..9b79138c47 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java @@ -17,8 +17,7 @@ package com.google.cloud.tools.jib.event.progress; import com.google.common.base.Preconditions; -import java.util.ArrayList; -import java.util.List; +import com.google.common.collect.ImmutableList; import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; @@ -113,7 +112,7 @@ boolean updateProgress(Allocation allocation, long units) { * * @return a list of unfinished {@link Allocation}s */ - List getUnfinishedAllocations() { + ImmutableList getUnfinishedAllocations() { Queue unfinishedInsertionOrderUnits = new PriorityQueue<>(); for (InsertionOrderUnits insertionOrderUnits : completionMap.values()) { @@ -122,11 +121,12 @@ List getUnfinishedAllocations() { } } - List unfinishedAllocations = new ArrayList<>(); + ImmutableList.Builder unfinishedAllocations = + ImmutableList.builderWithExpectedSize(unfinishedInsertionOrderUnits.size()); while (!unfinishedInsertionOrderUnits.isEmpty()) { unfinishedAllocations.add(unfinishedInsertionOrderUnits.remove().allocation); } - return unfinishedAllocations; + return unfinishedAllocations.build(); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java index d4b22b6885..2ccba88de5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.event.progress; import com.google.cloud.tools.jib.event.events.ProgressEvent; -import java.util.List; +import com.google.common.collect.ImmutableList; import java.util.concurrent.atomic.DoubleAdder; import java.util.function.Consumer; @@ -29,6 +29,37 @@ */ public class ProgressEventHandler implements Consumer { + /** Contains the accumulated progress and which {@link Allocation}s are not yet complete. */ + public static class Update { + + private final double progress; + private final ImmutableList unfinishedAllocations; + + private Update(double progress, ImmutableList unfinishedAllocations) { + this.progress = progress; + this.unfinishedAllocations = unfinishedAllocations; + } + + /** + * Gets the overall progress, with {@code 1.0} meaning fully complete. + * + * @return the overall progress + */ + public double getProgress() { + return progress; + } + + /** + * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link + * Allocation}s were encountered. + * + * @return a list of unfinished {@link Allocation}s + */ + public ImmutableList getUnfinishedAllocations() { + return unfinishedAllocations; + } + } + /** Keeps track of the progress for each {@link Allocation} encountered. */ private final AllocationCompletionTracker completionTracker = new AllocationCompletionTracker(); @@ -40,9 +71,9 @@ public class ProgressEventHandler implements Consumer { * Note that every change will be reported (though multiple could be reported together), and there * could be false positives. */ - private final Runnable updateNotifier; + private final Consumer updateNotifier; - public ProgressEventHandler(Runnable updateNotifier) { + public ProgressEventHandler(Consumer updateNotifier) { this.updateNotifier = updateNotifier; } @@ -58,28 +89,8 @@ public void accept(ProgressEvent progressEvent) { if (completionTracker.updateProgress(allocation, progressUnits)) { // Note: Could produce false positives. - updateNotifier.run(); + updateNotifier.accept( + new Update(progress.sum(), completionTracker.getUnfinishedAllocations())); } } - - /** - * Gets the overall progress, with {@code 1.0} meaning fully complete. - * - * @return the overall progress - */ - public double getProgress() { - return progress.sum(); - } - - /** - * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link - * Allocation}s were encountered. - * - * @return a list of unfinished {@link Allocation}s - */ - // TODO: Change this to do every time update notifier is called, so this is not called many times - // per update. - public List getUnfinishedAllocations() { - return completionTracker.getUnfinishedAllocations(); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 97a1dc9abc..8ecf716705 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -28,6 +28,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.DoubleAccumulator; import org.junit.Assert; import org.junit.Test; @@ -57,7 +58,10 @@ private AllocationTree() {} public void testAccept() throws ExecutionException, InterruptedException, IOException, TimeoutException { try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { - ProgressEventHandler progressEventHandler = new ProgressEventHandler(() -> {}); + DoubleAccumulator maxProgress = new DoubleAccumulator(Double::max, 0); + + ProgressEventHandler progressEventHandler = + new ProgressEventHandler(update -> maxProgress.accumulate(update.getProgress())); EventDispatcher eventDispatcher = new DefaultEventDispatcher( new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler)); @@ -78,7 +82,7 @@ public void testAccept() eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L)); return null; }); - Assert.assertEquals(0.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(0.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); // Adds 50 to child1Child and 100 to child2. List> callables = new ArrayList<>(150); @@ -99,9 +103,7 @@ public void testAccept() multithreadedExecutor.invokeAll(callables); Assert.assertEquals( - 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, - progressEventHandler.getProgress(), - DOUBLE_ERROR_MARGIN); + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, maxProgress.get(), DOUBLE_ERROR_MARGIN); // 0 progress doesn't do anything. multithreadedExecutor.invokeAll( @@ -112,13 +114,11 @@ public void testAccept() return null; })); Assert.assertEquals( - 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, - progressEventHandler.getProgress(), - DOUBLE_ERROR_MARGIN); + 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, maxProgress.get(), DOUBLE_ERROR_MARGIN); // Adds 50 to child1Child and 100 to child2 to finish it up. multithreadedExecutor.invokeAll(callables); - Assert.assertEquals(1.0, progressEventHandler.getProgress(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); } } } From f0e2d84b469de2de6894fbc579a6fefbcd9919cd Mon Sep 17 00:00:00 2001 From: sullis Date: Thu, 13 Dec 2018 14:30:18 -0800 Subject: [PATCH 0397/2020] maven-compiler-plugin 3.8.0 (#1345) --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index de2192fa34..617b97ebb8 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -309,7 +309,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.8.0 compile-with-nullaway From fd72f6fc9f7b2099c21e886eff30eac7a94631df Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 14 Dec 2018 12:00:44 -0500 Subject: [PATCH 0398/2020] Add file size to dependency filenames to avoid filename collisions (#1338) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../jib/gradle/GradleLayerConfigurations.java | 22 +++++++++- .../gradle/GradleLayerConfigurationsTest.java | 40 +++++++++++++----- .../another/one/dependency-1.0.0.jar | Bin 0 -> 480 bytes .../application/dependencies/library.jarC.jar | 0 .../dependencies/more/dependency-1.0.0.jar | 2 + jib-maven-plugin/CHANGELOG.md | 2 + .../jib/maven/MavenLayerConfigurations.java | 24 ++++++++++- .../maven/MavenLayerConfigurationsTest.java | 38 +++++++++++------ .../another/one/dependency-1.0.0.jar | Bin 0 -> 480 bytes .../application/dependencies/library.jarC.jar | 0 .../dependencies/more/dependency-1.0.0.jar | 2 + 12 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 jib-gradle-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar create mode 100644 jib-gradle-plugin/src/test/resources/application/dependencies/library.jarC.jar create mode 100644 jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar create mode 100644 jib-maven-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar create mode 100644 jib-maven-plugin/src/test/resources/application/dependencies/library.jarC.jar create mode 100644 jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 70cac3d346..3af8d8678f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Builds failing due to dependency JARs with the same name ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) + ## 0.10.1 ### Added diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index d84df73947..ceae0c388f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -27,7 +27,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; @@ -130,6 +133,17 @@ private static JavaLayerConfigurations getForNonWarProject( } // Adds dependency files. + List duplicates = + dependencyFiles + .getFiles() + .stream() + .map(File::getName) + .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Entry::getKey) + .collect(Collectors.toList()); for (File dependencyFile : dependencyFiles) { if (dependencyFile.exists()) { boolean isSnapshot = dependencyFile.getName().contains("SNAPSHOT"); @@ -137,7 +151,13 @@ private static JavaLayerConfigurations getForNonWarProject( layerBuilder.addFile( layerType, dependencyFile.toPath(), - dependenciesExtractionPath.resolve(dependencyFile.getName())); + dependenciesExtractionPath.resolve( + duplicates.contains(dependencyFile.getName()) + ? dependencyFile + .getName() + .replaceFirst("\\.jar$", "-" + Files.size(dependencyFile.toPath())) + + ".jar" + : dependencyFile.getName())); } else { logger.info("\t'" + dependencyFile + "' (not found, skipped)"); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 16c8b4848f..8a0c3a719b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -124,10 +124,19 @@ public void setUp() throws URISyntaxException, IOException { Set allFiles = new HashSet<>(classesFiles); allFiles.add(resourcesOutputDir); + allFiles.add( + Paths.get(Resources.getResource("application/dependencies/library.jarC.jar").toURI())); allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI())); allFiles.add( Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI())); + allFiles.add( + Paths.get( + Resources.getResource("application/dependencies/more/dependency-1.0.0.jar").toURI())); + allFiles.add( + Paths.get( + Resources.getResource("application/dependencies/another/one/dependency-1.0.0.jar") + .toURI())); allFiles.add( Paths.get( Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") @@ -158,8 +167,11 @@ public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedDependenciesFiles = ImmutableList.of( applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/more/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/another/one/dependency-1.0.0.jar"), applicationDirectory.resolve("dependencies/libraryA.jar"), - applicationDirectory.resolve("dependencies/libraryB.jar")); + applicationDirectory.resolve("dependencies/libraryB.jar"), + applicationDirectory.resolve("dependencies/library.jarC.jar")); ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); @@ -262,12 +274,15 @@ public void testGetForProject_nonDefaultAppRoot() throws IOException { assertExtractionPathsUnordered( Arrays.asList( - "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/dependency-1.0.0-770.jar", + "/my/app/libs/dependency-1.0.0-200.jar", + "/my/app/libs/dependency-1.0.0-480.jar", "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar"), + "/my/app/libs/libraryB.jar", + "/my/app/libs/library.jarC.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( @@ -295,10 +310,15 @@ public void testGetForProject_defaultAppRoot() throws IOException { assertExtractionPathsUnordered( Arrays.asList( - "/app/libs/dependency-1.0.0.jar", "/app/libs/libraryA.jar", "/app/libs/libraryB.jar"), + "/app/libs/dependency-1.0.0-770.jar", + "/app/libs/dependency-1.0.0-200.jar", + "/app/libs/dependency-1.0.0-480.jar", + "/app/libs/libraryA.jar", + "/app/libs/libraryB.jar", + "/app/libs/library.jarC.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( @@ -366,10 +386,10 @@ public void testWebApp() throws URISyntaxException, IOException { assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( @@ -409,10 +429,10 @@ public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOExceptio AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); assertExtractionPathsUnordered( - Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), + Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..ca2ad440bb9a6926e15dd4f4a363975b9f9aaded GIT binary patch literal 480 zcmV<60U!PaHD7x?KqvJC&P5?X`ZI%O{O$<2=7?-QbxJLlHfdSoC_548>gW`~0TQgp zDq@pyknKACj~ma7@XE_1tNzIaNKsRR{qs_4+`+aBZD{gc4j%hN-ABcmKFSOE zHr{B#^U-Cf^_AbU{9+fER(dmH4_2CvKcwT~mC@sV($>3Q1oRhh@)^sNSE{t?s{Nu8fX`N-^-*f;_C<0iK!NMA9Fd1KkLb=^J){x+0!<2!bW_xJ2{Rn$)x zT|o5VKsaMTfU4E?1_Z@o9up7aog=6thM`H$WKcrKx^wDiM|mN%KjlV)W4RV+i3ZQ#wVCxCGmAuw8L8uf;B(TKXsL z$1ui|Js7-tuuCZQO$2yUCN2HKhRvdan0TD2nwTiU)pbKq0GB*G81?7{@Wu8#yA48< Wrak8enxWnK85miLmv*vPWXE7fOZhbb literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/library.jarC.jar b/jib-gradle-plugin/src/test/resources/application/dependencies/library.jarC.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar new file mode 100644 index 0000000000..4d14054965 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar @@ -0,0 +1,2 @@ +]e$Ềx®²–,² +.3øÐIÝ…°Ü×ó3ö8® V©KAŒ§ÓMÓÞ)=5ü~Ú'qÑŽ—$[¬Ó- :çÇ&%¿‘ ÙæâÈEÌošÙÍ÷7N±†˜s`Þi€Z0MT.œÖÉ9J[¾}?\E î¶æ½}Uõv‘ú›JÆdÉúo(©ÏiŸ"MŒìÔ›¼½_+/ñ›c£õÛI<ŠÝZje·£44%žÛd2?¶l>.ž¨-OŠ=HÃiÜ \ No newline at end of file diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 266f804c74..327af1075f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Builds failing due to dependency JARs with the same name ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) + ## 0.10.1 ### Added diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index c88a24e1f3..f48f4c3d30 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -23,12 +23,16 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.apache.maven.project.MavenProject; @@ -87,12 +91,30 @@ private static JavaLayerConfigurations getForNonWarProject( Builder layerBuilder = JavaLayerConfigurations.builder(); // Gets all the dependencies. + List duplicates = + project + .getArtifacts() + .stream() + .map(Artifact::getFile) + .map(File::getName) + .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Entry::getKey) + .collect(Collectors.toList()); for (Artifact artifact : project.getArtifacts()) { Path artifactPath = artifact.getFile().toPath(); LayerType layerType = artifact.isSnapshot() ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; + String filename = artifactPath.getFileName().toString(); layerBuilder.addFile( - layerType, artifactPath, dependenciesExtractionPath.resolve(artifactPath.getFileName())); + layerType, + artifactPath, + dependenciesExtractionPath.resolve( + duplicates.contains(filename) + ? filename.replaceFirst("\\.jar$", "-" + Files.size(artifactPath)) + ".jar" + : filename)); } Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index 14c555faa2..b242e482e3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -71,12 +71,15 @@ private static void assertExtractionPathsUnordered( private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuration) { assertExtractionPathsUnordered( Arrays.asList( - "/my/app/libs/dependency-1.0.0.jar", + "/my/app/libs/dependency-1.0.0-770.jar", + "/my/app/libs/dependency-1.0.0-200.jar", + "/my/app/libs/dependency-1.0.0-480.jar", "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar"), + "/my/app/libs/libraryB.jar", + "/my/app/libs/library.jarC.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( @@ -111,14 +114,19 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio @Before public void setUp() throws URISyntaxException, IOException { Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); + Path dependenciesPath = Paths.get(Resources.getResource("application/dependencies").toURI()); Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); Set artifacts = ImmutableSet.of( - makeArtifact(Paths.get("application", "dependencies", "libraryB.jar")), - makeArtifact(Paths.get("application", "dependencies", "libraryA.jar")), + makeArtifact(dependenciesPath.resolve("library.jarC.jar")), + makeArtifact(dependenciesPath.resolve("libraryB.jar")), + makeArtifact(dependenciesPath.resolve("libraryA.jar")), + makeArtifact(dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar")), + makeArtifact( + dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar")), // Maven reads and populates "Artifacts" with its own processing, so read some from a // repository testRepository.findArtifact("com.test", "dependency", "1.0.0"), @@ -135,11 +143,15 @@ public void setUp() throws URISyntaxException, IOException { @Test public void test_correctFiles() throws URISyntaxException, IOException { + Path dependenciesPath = Paths.get(Resources.getResource("application/dependencies").toURI()); ImmutableList expectedDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), - Paths.get("application", "dependencies", "libraryA.jar"), - Paths.get("application", "dependencies", "libraryB.jar")); + dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar"), + dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar"), + dependenciesPath.resolve("libraryA.jar"), + dependenciesPath.resolve("libraryB.jar"), + dependenciesPath.resolve("library.jarC.jar")); ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); @@ -266,10 +278,10 @@ public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), configuration.getDependencyLayerEntries()); assertExtractionPathsUnordered( - Arrays.asList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), configuration.getSnapshotDependencyLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( @@ -349,28 +361,28 @@ public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOE } @Test - public void testIsWarProject_WarPackagingIsWar() throws IOException { + public void testIsWarProject_WarPackagingIsWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); } @Test - public void testIsWarProject_GwtAppPackagingIsWar() throws IOException { + public void testIsWarProject_GwtAppPackagingIsWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-app"); Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); } @Test - public void testIsWarProject_JarPackagingIsNotWar() throws IOException { + public void testIsWarProject_JarPackagingIsNotWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); } @Test - public void testIsWarProject_GwtLibPackagingIsNotWar() throws IOException { + public void testIsWarProject_GwtLibPackagingIsNotWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-lib"); Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..ca2ad440bb9a6926e15dd4f4a363975b9f9aaded GIT binary patch literal 480 zcmV<60U!PaHD7x?KqvJC&P5?X`ZI%O{O$<2=7?-QbxJLlHfdSoC_548>gW`~0TQgp zDq@pyknKACj~ma7@XE_1tNzIaNKsRR{qs_4+`+aBZD{gc4j%hN-ABcmKFSOE zHr{B#^U-Cf^_AbU{9+fER(dmH4_2CvKcwT~mC@sV($>3Q1oRhh@)^sNSE{t?s{Nu8fX`N-^-*f;_C<0iK!NMA9Fd1KkLb=^J){x+0!<2!bW_xJ2{Rn$)x zT|o5VKsaMTfU4E?1_Z@o9up7aog=6thM`H$WKcrKx^wDiM|mN%KjlV)W4RV+i3ZQ#wVCxCGmAuw8L8uf;B(TKXsL z$1ui|Js7-tuuCZQO$2yUCN2HKhRvdan0TD2nwTiU)pbKq0GB*G81?7{@Wu8#yA48< Wrak8enxWnK85miLmv*vPWXE7fOZhbb literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/library.jarC.jar b/jib-maven-plugin/src/test/resources/application/dependencies/library.jarC.jar new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar new file mode 100644 index 0000000000..4d14054965 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar @@ -0,0 +1,2 @@ +]e$Ềx®²–,² +.3øÐIÝ…°Ü×ó3ö8® V©KAŒ§ÓMÓÞ)=5ü~Ú'qÑŽ—$[¬Ó- :çÇ&%¿‘ ÙæâÈEÌošÙÍ÷7N±†˜s`Þi€Z0MT.œÖÉ9J[¾}?\E î¶æ½}Uõv‘ú›JÆdÉúo(©ÏiŸ"MŒìÔ›¼½_+/ñ›c£õÛI<ŠÝZje·£44%žÛd2?¶l>.ž¨-OŠ=HÃiÜ \ No newline at end of file From 6c742464ff5f4387a98423896fd0cb3d07fc562b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 14 Dec 2018 16:06:18 -0500 Subject: [PATCH 0399/2020] Fix gradle validateTaskProperties warning about dockerClient.executable (#1348) --- .../google/cloud/tools/jib/gradle/BuildDockerTask.java | 4 ++-- .../cloud/tools/jib/gradle/DockerClientParameters.java | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index ac5b4caa8b..ad601bfa11 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -89,7 +89,7 @@ public void buildDocker() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, CacheDirectoryCreationException, MainClassInferenceException, InferredAuthRetrievalException { - Path dockerExecutable = dockerClientParameters.getExecutable(); + Path dockerExecutable = dockerClientParameters.getExecutablePath(); boolean isDockerInstalled = dockerExecutable == null ? DockerClient.isDefaultDockerInstalled() @@ -122,7 +122,7 @@ public void buildDocker() new GradleRawConfiguration(jibExtension), ignored -> java.util.Optional.empty(), projectProperties, - dockerClientParameters.getExecutable(), + dockerClientParameters.getExecutablePath(), dockerClientParameters.getEnvironment(), gradleHelpfulSuggestionsBuilder.build()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java index 4f1409e34e..bf057483fe 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -21,6 +21,7 @@ import java.util.Map; import javax.annotation.Nullable; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; /** @@ -35,7 +36,13 @@ public class DockerClientParameters { @Input @Nullable @Optional - public Path getExecutable() { + public String getExecutable() { + return executable == null ? null : executable.toString(); + } + + @Internal + @Nullable + Path getExecutablePath() { return executable; } From 081f23d62d9d64db6c754e2b815f60631cf15f9e Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 14 Dec 2018 17:34:27 -0500 Subject: [PATCH 0400/2020] Disables progress footer by default or if console is null. (#1344) --- jib-core/build.gradle | 4 +- .../tools/jib/api/DockerDaemonImage.java | 3 +- .../BuildAndCacheApplicationLayerStep.java | 2 +- .../jib/builder/steps/FinalizingStep.java | 2 +- .../jib/builder/steps/LoadDockerStep.java | 2 +- .../jib/builder/steps/PullBaseImageStep.java | 2 +- .../RetrieveRegistryCredentialsStep.java | 2 +- .../jib/builder/steps/WriteTarFileStep.java | 2 +- .../jib/configuration/FilePermissions.java | 2 +- .../tools/jib/event/events/LogEvent.java | 7 + .../progress => }/MultithreadedExecutor.java | 12 +- .../tools/jib/event/events/LogEventTest.java | 2 + .../AllocationCompletionTrackerTest.java | 1 + .../progress/ProgressEventHandlerTest.java | 1 + jib-gradle-plugin/build.gradle | 4 +- .../jib/gradle/GradleProjectProperties.java | 72 +++++-- .../tools/jib/gradle/LogEventHandler.java | 72 ------- jib-maven-plugin/pom.xml | 10 +- .../tools/jib/maven/LogEventHandler.java | 73 -------- .../jib/maven/MavenProjectProperties.java | 49 ++++- jib-plugins-common/build.gradle | 4 +- .../common/JavaLayerConfigurationsHelper.java | 5 +- .../jib/plugins/common/PropertyNames.java | 4 + .../{ => logging}/AnsiLoggerWithFooter.java | 96 ++++------ .../plugins/common/logging/ConsoleLogger.java | 32 ++++ .../logging/LogEventHandlerBuilder.java | 146 +++++++++++++++ .../common/logging/PlainConsoleLogger.java | 52 +++++ .../ProgressDisplayGenerator.java | 2 +- .../logging/SingleThreadedExecutor.java | 64 +++++++ .../common/AnsiLoggerWithFooterTest.java | 102 ---------- .../logging/AnsiLoggerWithFooterTest.java | 177 ++++++++++++++++++ .../logging/LogEventHandlerBuilderTest.java | 92 +++++++++ .../logging/PlainConsoleLoggerTest.java | 92 +++++++++ .../ProgressDisplayGeneratorTest.java | 4 +- .../logging/SingleThreadedExecutorTest.java | 54 ++++++ 35 files changed, 887 insertions(+), 363 deletions(-) rename jib-core/src/test/java/com/google/cloud/tools/jib/{event/progress => }/MultithreadedExecutor.java (84%) delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java rename jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/{ => logging}/AnsiLoggerWithFooter.java (52%) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java rename jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/{ => logging}/ProgressDisplayGenerator.java (98%) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java delete mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java rename jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/{ => logging}/ProgressDisplayGeneratorTest.java (93%) create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java diff --git a/jib-core/build.gradle b/jib-core/build.gradle index e3931cb60a..705e6f0d00 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java-library' id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.7.1' + id 'com.github.sherter.google-java-format' version '0.8' id 'net.ltgt.apt' version '0.13' id 'net.ltgt.errorprone' version '0.0.13' id 'biz.aQute.bnd.builder' version '4.0.0' @@ -112,7 +112,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = "7.6.1" + toolVersion = '8.15' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index 56d62bfe61..c2c8062f35 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.docker.DockerClient.Builder; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; @@ -95,7 +94,7 @@ public ImageConfiguration toImageConfiguration() { @Override public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { - Builder dockerClientBuilder = DockerClient.builder(); + DockerClient.Builder dockerClientBuilder = DockerClient.builder(); if (dockerExecutable != null) { dockerClientBuilder.setDockerExecutable(dockerExecutable); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index d65f4e3216..553fd7373c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -104,7 +104,7 @@ public ListenableFuture getFuture() { public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("Build " + layerType + " layer", 1); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java index d1810cd134..a460519566 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java @@ -79,7 +79,7 @@ public Void call() throws ExecutionException { Future ignored = asyncDependencies.whenAllSucceed( () -> { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("Finalizing...")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress("Finalizing...")); return null; }); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index df32d2fd8e..5568af21cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -91,7 +91,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { buildConfiguration .getEventDispatcher() - .dispatch(LogEvent.lifecycle("Loading to Docker daemon...")); + .dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("Load to Docker daemon", 1)) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index d564e4f3a4..5538aecc3c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -112,7 +112,7 @@ public BaseImageWithAuthorization call() buildConfiguration .getEventDispatcher() .dispatch( - LogEvent.lifecycle( + LogEvent.progress( "Getting base image " + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 171026ece1..0ca20d4f14 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -98,7 +98,7 @@ public ListenableFuture getFuture() { public Credential call() throws CredentialRetrievalException { String description = makeDescription(registry); - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(description + "...")); + buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 7003b28b53..3034d73b1a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -91,7 +91,7 @@ public BuildResult call() throws ExecutionException, InterruptedException { private BuildResult writeTarFile() throws ExecutionException, IOException { buildConfiguration .getEventDispatcher() - .dispatch(LogEvent.lifecycle("Building image to tar file...")); + .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("Write to tar file", 1)) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java index c8407099de..89b01eddab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java @@ -73,7 +73,7 @@ public static FilePermissions fromPosixFilePermissions( Set posixFilePermissions) { int permissionBits = 0; for (PosixFilePermission permission : posixFilePermissions) { - permissionBits |= PERMISSION_MAP.get(permission); + permissionBits |= Preconditions.checkNotNull(PERMISSION_MAP.get(permission)); } return new FilePermissions(permissionBits); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java index 48733f3c8e..de7ccb692a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java @@ -35,6 +35,9 @@ public enum Level { /** Default. */ LIFECYCLE, + /** Same as {@link #LIFECYCLE}, except represents progress updates. */ + PROGRESS, + /** * Details that can be ignored. * @@ -54,6 +57,10 @@ public static LogEvent lifecycle(String message) { return new LogEvent(Level.LIFECYCLE, message); } + public static LogEvent progress(String message) { + return new LogEvent(Level.PROGRESS, message); + } + public static LogEvent warn(String message) { return new LogEvent(Level.WARN, message); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java similarity index 84% rename from jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java index 27d25af9be..c2b053856f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/MultithreadedExecutor.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.event.progress; +package com.google.cloud.tools.jib; import java.io.Closeable; import java.io.IOException; @@ -28,25 +28,23 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.junit.Assert; /** Testing infrastructure for running code across multiple threads. */ -class MultithreadedExecutor implements Closeable { +public class MultithreadedExecutor implements Closeable { private static final Duration MULTITHREADED_TEST_TIMEOUT = Duration.ofSeconds(1); private static final int THREAD_COUNT = 20; private final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT); - E invoke(Callable callable) - throws ExecutionException, InterruptedException, TimeoutException { + public E invoke(Callable callable) throws ExecutionException, InterruptedException { List returnValue = invokeAll(Collections.singletonList(callable)); return returnValue.get(0); } - List invokeAll(List> callables) - throws InterruptedException, ExecutionException, TimeoutException { + public List invokeAll(List> callables) + throws InterruptedException, ExecutionException { List> futures = executorService.invokeAll( callables, MULTITHREADED_TEST_TIMEOUT.getSeconds(), TimeUnit.SECONDS); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java index 47ec77efa6..796689acb6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -41,12 +41,14 @@ public class LogEventTest { public void testFactories() { eventDispatcher.dispatch(LogEvent.error("error")); eventDispatcher.dispatch(LogEvent.lifecycle("lifecycle")); + eventDispatcher.dispatch(LogEvent.progress("progress")); eventDispatcher.dispatch(LogEvent.warn("warn")); eventDispatcher.dispatch(LogEvent.info("info")); eventDispatcher.dispatch(LogEvent.debug("debug")); verifyNextLogEvent(Level.ERROR, "error"); verifyNextLogEvent(Level.LIFECYCLE, "lifecycle"); + verifyNextLogEvent(Level.PROGRESS, "progress"); verifyNextLogEvent(Level.WARN, "warn"); verifyNextLogEvent(Level.INFO, "info"); verifyNextLogEvent(Level.DEBUG, "debug"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java index a9b03c1260..25d2f429b9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event.progress; +import com.google.cloud.tools.jib.MultithreadedExecutor; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 8ecf716705..33740d5b13 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event.progress; +import com.google.cloud.tools.jib.MultithreadedExecutor; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 41c0cf0e99..6b4726325b 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java-gradle-plugin' id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.7.1' + id 'com.github.sherter.google-java-format' version '0.8' // For local install id 'maven' @@ -118,7 +118,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = "7.6.1" + toolVersion = '8.15' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 329243d8b0..b6ad6e3d94 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -22,9 +22,11 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; @@ -36,7 +38,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.function.Consumer; import javax.annotation.Nullable; +import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; @@ -77,17 +81,6 @@ static GradleProjectProperties getForProject( } } - private static EventHandlers makeEventHandlers( - Logger logger, AnsiLoggerWithFooter ansiLoggerWithFooter) { - LogEventHandler logEventHandler = new LogEventHandler(logger, ansiLoggerWithFooter); - TimerEventHandler timerEventHandler = - new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); - - return new EventHandlers() - .add(JibEventType.LOGGING, logEventHandler) - .add(JibEventType.TIMING, timerEventHandler); - } - @Nullable static War getWarTask(Project project) { WarPluginConvention warPluginConvention = @@ -102,8 +95,56 @@ static Path getExplodedWarDirectory(Project project) { return project.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME); } + private static EventHandlers makeEventHandlers( + Project project, Logger logger, SingleThreadedExecutor singleThreadedExecutor) { + LogEventHandlerBuilder logEventHandlerBuilder = + (isProgressFooterEnabled(project) + ? LogEventHandlerBuilder.rich(singleThreadedExecutor) + : LogEventHandlerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) + .lifecycle(logger::lifecycle); + if (logger.isDebugEnabled()) { + logEventHandlerBuilder.debug(logger::debug); + } + if (logger.isInfoEnabled()) { + logEventHandlerBuilder.info(logger::info); + } + if (logger.isWarnEnabled()) { + logEventHandlerBuilder.warn(logger::warn); + } + if (logger.isErrorEnabled()) { + logEventHandlerBuilder.error(logger::error); + } + Consumer logEventHandler = logEventHandlerBuilder.build(); + + TimerEventHandler timerEventHandler = + new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); + + return new EventHandlers() + .add(JibEventType.LOGGING, logEventHandler) + .add(JibEventType.TIMING, timerEventHandler); + } + + private static boolean isProgressFooterEnabled(Project project) { + // TODO: Make SHOW_PROGRESS be true by default. + if (!Boolean.getBoolean(PropertyNames.SHOW_PROGRESS)) { + return false; + } + + switch (project.getGradle().getStartParameter().getConsoleOutput()) { + case Plain: + return false; + + case Auto: + // Enables progress footer when ANSI is supported (Windows or TERM not 'dumb'). + return Os.isFamily(Os.FAMILY_WINDOWS) || !"dumb".equals(System.getenv("TERM")); + + default: + return true; + } + } + private final Project project; - private final AnsiLoggerWithFooter ansiLoggerWithFooter; + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @@ -113,8 +154,7 @@ static Path getExplodedWarDirectory(Project project) { this.project = project; this.javaLayerConfigurations = javaLayerConfigurations; - ansiLoggerWithFooter = new AnsiLoggerWithFooter(logger::lifecycle); - eventHandlers = makeEventHandlers(logger, ansiLoggerWithFooter); + eventHandlers = makeEventHandlers(project, logger, singleThreadedExecutor); } @Override @@ -124,7 +164,7 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { @Override public void waitForLoggingThread() { - ansiLoggerWithFooter.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java deleted file mode 100644 index 5ee4c4f4c1..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/LogEventHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; -import java.util.function.Consumer; -import org.gradle.api.logging.Logger; - -/** Handles {@link LogEvent}s by passing to the Gradle {@link Logger}. */ -class LogEventHandler implements Consumer { - - private final Logger logger; - private final AnsiLoggerWithFooter ansiLoggerWithFooter; - - LogEventHandler(Logger logger, AnsiLoggerWithFooter ansiLoggerWithFooter) { - this.logger = logger; - this.ansiLoggerWithFooter = ansiLoggerWithFooter; - } - - @Override - public void accept(LogEvent logEvent) { - switch (logEvent.getLevel()) { - case LIFECYCLE: - if (logger.isLifecycleEnabled()) { - ansiLoggerWithFooter.log(logger::lifecycle, logEvent.getMessage()); - } - break; - - case DEBUG: - if (logger.isDebugEnabled()) { - ansiLoggerWithFooter.log(logger::debug, logEvent.getMessage()); - } - break; - - case ERROR: - if (logger.isErrorEnabled()) { - ansiLoggerWithFooter.log(logger::error, logEvent.getMessage()); - } - break; - - case INFO: - if (logger.isInfoEnabled()) { - ansiLoggerWithFooter.log(logger::info, logEvent.getMessage()); - } - break; - - case WARN: - if (logger.isWarnEnabled()) { - ansiLoggerWithFooter.log(logger::warn, "warning: " + logEvent.getMessage()); - } - break; - - default: - throw new IllegalStateException("Unknown LogEvent.Level: " + logEvent.getLevel()); - } - } -} diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 617b97ebb8..0fdbb50902 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -378,7 +378,7 @@ com.coveo fmt-maven-plugin - 2.5.0 + 2.6.0 com.google.googlejavaformat @@ -399,19 +399,21 @@ maven-checkstyle-plugin - 2.17 + 3.0.0 com.puppycrawl.tools checkstyle - 8.7 + 8.15 true true warning - ${project.build.sourceDirectory} + + {project.build.sourceDirectory} + diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java deleted file mode 100644 index 8ebfbad89a..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/LogEventHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; -import java.util.function.Consumer; -import org.apache.maven.plugin.logging.Log; - -/** Handles {@link LogEvent}s by passing to the Maven {@link Log}. */ -class LogEventHandler implements Consumer { - - private final Log log; - private final AnsiLoggerWithFooter ansiLoggerWithFooter; - - LogEventHandler(Log log, AnsiLoggerWithFooter ansiLoggerWithFooter) { - this.log = log; - this.ansiLoggerWithFooter = ansiLoggerWithFooter; - } - - @Override - public void accept(LogEvent logEvent) { - switch (logEvent.getLevel()) { - case LIFECYCLE: - if (log.isInfoEnabled()) { - ansiLoggerWithFooter.log(log::info, logEvent.getMessage()); - } - break; - - case DEBUG: - if (log.isDebugEnabled()) { - ansiLoggerWithFooter.log(log::debug, logEvent.getMessage()); - } - break; - - case ERROR: - if (log.isErrorEnabled()) { - ansiLoggerWithFooter.log(log::error, logEvent.getMessage()); - } - break; - - case INFO: - // Use lifecycle for progress-indicating messages. - if (log.isDebugEnabled()) { - ansiLoggerWithFooter.log(log::debug, logEvent.getMessage()); - } - break; - - case WARN: - if (log.isWarnEnabled()) { - ansiLoggerWithFooter.log(log::warn, logEvent.getMessage()); - } - break; - - default: - throw new IllegalStateException("Unknown LogEvent.Level: " + logEvent.getLevel()); - } - } -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a0d99f8555..f36235874b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -22,19 +22,23 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.AnsiLoggerWithFooter; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; +import java.util.function.Consumer; import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.utils.Os; import org.codehaus.plexus.util.xml.Xpp3Dom; /** Obtains information about a {@link MavenProject}. */ @@ -84,8 +88,26 @@ static MavenProjectProperties getForProject( } private static EventHandlers makeEventHandlers( - Log log, AnsiLoggerWithFooter ansiLoggerWithFooter) { - LogEventHandler logEventHandler = new LogEventHandler(log, ansiLoggerWithFooter); + Log log, SingleThreadedExecutor singleThreadedExecutor) { + LogEventHandlerBuilder logEventHandlerBuilder = + (isProgressFooterEnabled() + ? LogEventHandlerBuilder.rich(singleThreadedExecutor) + : LogEventHandlerBuilder.plain(singleThreadedExecutor).progress(log::info)) + .lifecycle(log::info); + if (log.isDebugEnabled()) { + logEventHandlerBuilder + .debug(log::debug) + // INFO messages also go to Log#debug since Log#info is used for LIFECYCLE. + .info(log::debug); + } + if (log.isWarnEnabled()) { + logEventHandlerBuilder.warn(log::warn); + } + if (log.isErrorEnabled()) { + logEventHandlerBuilder.error(log::error); + } + Consumer logEventHandler = logEventHandlerBuilder.build(); + TimerEventHandler timerEventHandler = new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); @@ -94,8 +116,22 @@ private static EventHandlers makeEventHandlers( .add(JibEventType.TIMING, timerEventHandler); } + private static boolean isProgressFooterEnabled() { + // TODO: Make SHOW_PROGRESS be true by default. + if (!Boolean.getBoolean(PropertyNames.SHOW_PROGRESS)) { + return false; + } + + // Enables progress footer when ANSI is supported (Windows or System.console() not null and TERM + // not 'dumb'). + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return true; + } + return System.console() != null && !"dumb".equals(System.getenv("TERM")); + } + private final MavenProject project; - private final AnsiLoggerWithFooter ansiLoggerWithFooter; + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; private final JavaLayerConfigurations javaLayerConfigurations; @@ -105,8 +141,7 @@ private static EventHandlers makeEventHandlers( this.project = project; this.javaLayerConfigurations = javaLayerConfigurations; - ansiLoggerWithFooter = new AnsiLoggerWithFooter(log::info); - eventHandlers = makeEventHandlers(log, ansiLoggerWithFooter); + eventHandlers = makeEventHandlers(log, singleThreadedExecutor); } @Override @@ -116,7 +151,7 @@ public JavaLayerConfigurations getJavaLayerConfigurations() { @Override public void waitForLoggingThread() { - ansiLoggerWithFooter.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(); } @Override diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 90235b5d40..57e8c4253e 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.7.1' + id 'com.github.sherter.google-java-format' version '0.8' id 'net.ltgt.apt' version '0.13' id 'net.ltgt.errorprone' version '0.0.13' } @@ -77,7 +77,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = "7.6.1" + toolVersion = '8.15' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java index 3abbcf08dc..aec8abeef6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import java.io.IOException; import java.nio.file.Files; @@ -57,9 +56,9 @@ public static JavaLayerConfigurations fromExplodedWar( Predicate isClassFile = path -> path.startsWith(webInfClasses) && path.getFileName().toString().endsWith(".class"); Predicate isResource = - (isSnapshotDependency.or(isNonSnapshotDependency).or(isClassFile)).negate(); + isSnapshotDependency.or(isNonSnapshotDependency).or(isClassFile).negate(); - Builder layerBuilder = JavaLayerConfigurations.builder(); + JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); // Gets all the dependencies. if (Files.exists(webInfLib)) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 8f4c8bda24..b270b18f5d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -18,6 +18,7 @@ /** Names of system properties used to set configuration via commandline. */ public class PropertyNames { + public static final String FROM_IMAGE = "jib.from.image"; public static final String FROM_CRED_HELPER = "jib.from.credHelper"; public static final String FROM_AUTH_USERNAME = "jib.from.auth.username"; @@ -48,4 +49,7 @@ public class PropertyNames { public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; public static final String SKIP = "jib.skip"; + public static final String SHOW_PROGRESS = "jib.showProgress"; + + private PropertyNames() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java similarity index 52% rename from jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index f94095da49..a586f99a6d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -14,25 +14,20 @@ * the License. */ -package com.google.cloud.tools.jib.plugins.common; +package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.common.annotations.VisibleForTesting; -import java.time.Duration; +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** - * Keeps all log messages in a sequential, deterministic order along with an additional footer that - * always appears below log messages. This is intended to log both the messages and the footer to - * the same console. - * - *

Make sure to call {@link #shutDownAndAwaitTermination} when finished. + * Logs to a console supporting ANSI escape sequences and keeps an additional footer that always + * appears below log messages. */ -public class AnsiLoggerWithFooter { +class AnsiLoggerWithFooter implements ConsoleLogger { /** ANSI escape sequence for moving the cursor up one line. */ private static final String CURSOR_UP_SEQUENCE = "\033[1A"; @@ -46,67 +41,46 @@ public class AnsiLoggerWithFooter { /** ANSI escape sequence for setting all further characters to not bold. */ private static final String UNBOLD = "\033[0m"; - private static final Duration EXECUTOR_SHUTDOWN_WAIT = Duration.ofSeconds(1); - - private final ExecutorService executorService; - private final Consumer plainPrinter; + private final ImmutableMap> messageConsumers; + private final Consumer lifecycleConsumer; + private final SingleThreadedExecutor singleThreadedExecutor; private List footerLines = Collections.emptyList(); /** - * Creates a new {@link AnsiLoggerWithFooter} + * Creates a new {@link AnsiLoggerWithFooter}. * - * @param plainPrinter the {@link Consumer} intended to synchronously print the footer and other - * plain console output. {@code plainPrinter} should print a new line at the end. + * @param messageConsumers map from each {@link Level} to a log message {@link Consumer + * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are logged in a sequential, deterministic order */ - public AnsiLoggerWithFooter(Consumer plainPrinter) { - this(plainPrinter, Executors.newSingleThreadExecutor()); + AnsiLoggerWithFooter( + ImmutableMap> messageConsumers, + SingleThreadedExecutor singleThreadedExecutor) { + Preconditions.checkArgument( + messageConsumers.containsKey(Level.LIFECYCLE), + "Cannot construct AnsiLoggerFooter without LIFECYCLE message consumer"); + this.messageConsumers = messageConsumers; + this.lifecycleConsumer = Preconditions.checkNotNull(messageConsumers.get(Level.LIFECYCLE)); + this.singleThreadedExecutor = singleThreadedExecutor; } - @VisibleForTesting - AnsiLoggerWithFooter(Consumer plainPrinter, ExecutorService executorService) { - this.plainPrinter = plainPrinter; - this.executorService = executorService; - } - - /** Shuts down the {@link #executorService} and waits for it to terminate. */ - public void shutDownAndAwaitTermination() { - executorService.shutdown(); - - try { - if (!executorService.awaitTermination( - EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { - executorService.shutdownNow(); - if (!executorService.awaitTermination( - EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { - throw new RuntimeException("Could not shut down AnsiLoggerWithFooter executor"); - } - } - - } catch (InterruptedException ex) { - executorService.shutdownNow(); - Thread.currentThread().interrupt(); + @Override + public void log(Level logLevel, String message) { + if (!messageConsumers.containsKey(logLevel)) { + return; } - } + Consumer messageConsumer = messageConsumers.get(logLevel); - /** - * Runs {@code messageLogger} asynchronously. - * - * @param messageLogger the {@link Consumer} intended to synchronously log a message to the - * console. {@code messageLogger} should print a new line at the end. - * @param message the message to log with {@code messageLogger} - */ - public void log(Consumer messageLogger, String message) { - executorService.execute( + singleThreadedExecutor.execute( () -> { boolean didErase = eraseFooter(); // If a previous footer was erased, the message needs to go up a line. String messagePrefix = didErase ? CURSOR_UP_SEQUENCE : ""; - messageLogger.accept(messagePrefix + message); + messageConsumer.accept(messagePrefix + message); for (String footerLine : footerLines) { - plainPrinter.accept(BOLD + footerLine + UNBOLD); + lifecycleConsumer.accept(BOLD + footerLine + UNBOLD); } }); } @@ -119,12 +93,12 @@ public void log(Consumer messageLogger, String message) { * * @param newFooterLines the footer, with each line as an element (no newline at end) */ - public void setFooter(List newFooterLines) { + void setFooter(List newFooterLines) { if (newFooterLines.equals(footerLines)) { return; } - executorService.execute( + singleThreadedExecutor.execute( () -> { boolean didErase = eraseFooter(); @@ -132,7 +106,7 @@ public void setFooter(List newFooterLines) { String newFooterPrefix = didErase ? CURSOR_UP_SEQUENCE : ""; for (String newFooterLine : newFooterLines) { - plainPrinter.accept(newFooterPrefix + BOLD + newFooterLine + UNBOLD); + lifecycleConsumer.accept(newFooterPrefix + BOLD + newFooterLine + UNBOLD); newFooterPrefix = ""; } @@ -142,7 +116,7 @@ public void setFooter(List newFooterLines) { /** * Erases the footer. Do not call outside of a task submitted to {@link - * #executorService}. + * #singleThreadedExecutor}. * * @return {@code true} if anything was erased; {@code false} otherwise */ @@ -163,7 +137,7 @@ private boolean eraseFooter() { // Erases everything below cursor. footerEraserBuilder.append(ERASE_DISPLAY_BELOW); - plainPrinter.accept(footerEraserBuilder.toString()); + lifecycleConsumer.accept(footerEraserBuilder.toString()); return true; } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java new file mode 100644 index 0000000000..4cb2dd3547 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.LogEvent.Level; + +/** Logs messages to the console. Implementations must be thread-safe. */ +interface ConsoleLogger { + + /** + * Logs {@code message} to the console at {@link Level#LIFECYCLE}. + * + * @param logLevel the log level for the {@code message} + * @param message the message + */ + void log(LogEvent.Level logLevel, String message); +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java new file mode 100644 index 0000000000..cd845140e7 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Builds a handler for {@link LogEvent}. */ +public class LogEventHandlerBuilder { + + /** + * Alias for function that takes a map from {@link Level} to a log message {@link Consumer} and + * creates a {@link ConsoleLogger}. + */ + @VisibleForTesting + @FunctionalInterface + interface ConsoleLoggerFactory + extends Function>, ConsoleLogger> {} + + /** + * Starts a {@link LogEventHandlerBuilder} for rich logging (ANSI support with footer). + * + * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are + * logged in a sequential, deterministic order + * @return a new {@link LogEventHandlerBuilder} + */ + public static LogEventHandlerBuilder rich(SingleThreadedExecutor singleThreadedExecutor) { + return new LogEventHandlerBuilder( + messageConsumers -> new AnsiLoggerWithFooter(messageConsumers, singleThreadedExecutor)); + } + + /** + * Starts a {@link LogEventHandlerBuilder} for plain-text logging (no ANSI support). + * + * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are + * logged in a sequential, deterministic order + * @return a new {@link LogEventHandlerBuilder} + */ + public static LogEventHandlerBuilder plain(SingleThreadedExecutor singleThreadedExecutor) { + return new LogEventHandlerBuilder( + messageConsumers -> new PlainConsoleLogger(messageConsumers, singleThreadedExecutor)); + } + + private final ImmutableMap.Builder> messageConsumers = + ImmutableMap.builder(); + private final ConsoleLoggerFactory consoleLoggerFactory; + + @VisibleForTesting + LogEventHandlerBuilder(ConsoleLoggerFactory consoleLoggerFactory) { + this.consoleLoggerFactory = consoleLoggerFactory; + } + + /** + * Sets the {@link Consumer} to log a {@link Level#LIFECYCLE} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder lifecycle(Consumer messageConsumer) { + messageConsumers.put(Level.LIFECYCLE, messageConsumer); + return this; + } + + /** + * Sets the {@link Consumer} to log a {@link Level#PROGRESS} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder progress(Consumer messageConsumer) { + messageConsumers.put(Level.PROGRESS, messageConsumer); + return this; + } + + /** + * Sets the {@link Consumer} to log a {@link Level#DEBUG} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder debug(Consumer messageConsumer) { + messageConsumers.put(Level.DEBUG, messageConsumer); + return this; + } + + /** + * Sets the {@link Consumer} to log an {@link Level#ERROR} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder error(Consumer messageConsumer) { + messageConsumers.put(Level.ERROR, messageConsumer); + return this; + } + + /** + * Sets the {@link Consumer} to log an {@link Level#INFO} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder info(Consumer messageConsumer) { + messageConsumers.put(Level.INFO, messageConsumer); + return this; + } + + /** + * Sets the {@link Consumer} to log a {@link Level#WARN} message. + * + * @param messageConsumer the message {@link Consumer} + * @return this + */ + public LogEventHandlerBuilder warn(Consumer messageConsumer) { + messageConsumers.put(Level.WARN, messageConsumer); + return this; + } + + /** + * Builds the {@link LogEvent} handler. + * + * @return the {@link Consumer} + */ + public Consumer build() { + ConsoleLogger consoleLogger = consoleLoggerFactory.apply(messageConsumers.build()); + return logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java new file mode 100644 index 0000000000..51f145a2ab --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.collect.ImmutableMap; +import java.util.function.Consumer; + +/** Logs messages plainly. */ +class PlainConsoleLogger implements ConsoleLogger { + + private final ImmutableMap> messageConsumers; + private final SingleThreadedExecutor singleThreadedExecutor; + + /** + * Creates a {@link PlainConsoleLogger}. + * + * @param messageConsumers map from each {@link Level} to a log message {@link Consumer} + * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are + * logged in a sequential, deterministic order + */ + PlainConsoleLogger( + ImmutableMap> messageConsumers, + SingleThreadedExecutor singleThreadedExecutor) { + this.messageConsumers = messageConsumers; + this.singleThreadedExecutor = singleThreadedExecutor; + } + + @Override + public void log(Level logLevel, String message) { + if (!messageConsumers.containsKey(logLevel)) { + return; + } + Consumer messageConsumer = messageConsumers.get(logLevel); + + singleThreadedExecutor.execute(() -> messageConsumer.accept(message)); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java similarity index 98% rename from jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java index a92e975230..a6d47ffeea 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGenerator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.plugins.common; +package com.google.cloud.tools.jib.plugins.common.logging; import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.ArrayList; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java new file mode 100644 index 0000000000..3468896489 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Executes methods on a single managed thread. Make sure to call {@link + * #shutDownAndAwaitTermination} when finished. + * + *

This implementation is thread-safe. + */ +public class SingleThreadedExecutor { + + private static final Duration EXECUTOR_SHUTDOWN_WAIT = Duration.ofSeconds(1); + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + /** Shuts down the {@link #executorService} and waits for it to terminate. */ + public void shutDownAndAwaitTermination() { + executorService.shutdown(); + + try { + if (!executorService.awaitTermination( + EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + executorService.shutdownNow(); + if (!executorService.awaitTermination( + EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + System.err.println("Could not shut down SingleThreadedExecutor"); + } + } + + } catch (InterruptedException ex) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + /** + * Executes {@code runnable} on the managed thread. + * + * @param runnable the {@link Runnable} + */ + public void execute(Runnable runnable) { + executorService.execute(runnable); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java deleted file mode 100644 index 2f07cabd51..0000000000 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/AnsiLoggerWithFooterTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import com.google.common.util.concurrent.MoreExecutors; -import java.util.Arrays; -import java.util.Collections; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link AnsiLoggerWithFooter}. */ -public class AnsiLoggerWithFooterTest { - - private final StringBuilder logBuilder = new StringBuilder(); - private final AnsiLoggerWithFooter testAnsiLoggerWithFooter = - new AnsiLoggerWithFooter(this::logBuilderPrinter, MoreExecutors.newDirectExecutorService()); - - @Test - public void testLog_noFooter() { - testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); - - Assert.assertEquals("message\n", logBuilder.toString()); - } - - @Test - public void testLog_sameFooter() { - testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); - - Assert.assertEquals("\033[1mfooter\033[0m\n", logBuilder.toString()); - - testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); - - Assert.assertEquals( - "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1Amessage\n" - + "\033[1mfooter\033[0m\n", - logBuilder.toString()); - - testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "another message"); - - Assert.assertEquals( - "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1Amessage\n" - + "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1Aanother message\n" - + "\033[1mfooter\033[0m\n", - logBuilder.toString()); - } - - @Test - public void testLog_changingFooter() { - testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); - testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "message"); - - testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); - - Assert.assertEquals( - "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1Amessage\n" - + "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1A\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n", - logBuilder.toString()); - - testAnsiLoggerWithFooter.log(this::logBuilderPrinter, "another message"); - - Assert.assertEquals( - "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1Amessage\n" - + "\033[1mfooter\033[0m\n" - + "\033[1A\033[0J\n" - + "\033[1A\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n" - + "\033[1A\033[1A\033[0J\n" - + "\033[1Aanother message\n" - + "\033[1mtwo line\033[0m\n\033[1mfooter\033[0m\n", - logBuilder.toString()); - } - - // This mimics a real log printer that always adds a new line at the end. - private void logBuilderPrinter(String message) { - logBuilder.append(message).append('\n'); - } -} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java new file mode 100644 index 0000000000..79ea315722 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link com.google.cloud.tools.jib.plugins.common.logging.AnsiLoggerWithFooter}. */ +public class AnsiLoggerWithFooterTest { + + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); + + private final List messages = new ArrayList<>(); + private final List levels = new ArrayList<>(); + private final Function> messageConsumerFactory = + level -> + message -> { + levels.add(level); + messages.add(message); + }; + + private AnsiLoggerWithFooter testAnsiLoggerWithFooter; + + @Before + public void setUp() { + ImmutableMap.Builder> messageConsumers = ImmutableMap.builder(); + for (Level level : Level.values()) { + messageConsumers.put(level, messageConsumerFactory.apply(level)); + } + + testAnsiLoggerWithFooter = + new AnsiLoggerWithFooter(messageConsumers.build(), singleThreadedExecutor); + } + + @Test + public void testNoLifecycle() { + try { + new AnsiLoggerWithFooter(ImmutableMap.of(), singleThreadedExecutor); + Assert.fail(); + + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "Cannot construct AnsiLoggerFooter without LIFECYCLE message consumer", ex.getMessage()); + } + } + + @Test + public void testLog_noFooter() { + testAnsiLoggerWithFooter.log(Level.LIFECYCLE, "lifecycle"); + testAnsiLoggerWithFooter.log(Level.PROGRESS, "progress"); + testAnsiLoggerWithFooter.log(Level.INFO, "info"); + testAnsiLoggerWithFooter.log(Level.DEBUG, "debug"); + testAnsiLoggerWithFooter.log(Level.WARN, "warn"); + testAnsiLoggerWithFooter.log(Level.ERROR, "error"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals( + Arrays.asList("lifecycle", "progress", "info", "debug", "warn", "error"), messages); + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, Level.PROGRESS, Level.INFO, Level.DEBUG, Level.WARN, Level.ERROR), + levels); + } + + @Test + public void testLog_ignoreIfNoMessageConsumer() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = + new AnsiLoggerWithFooter( + ImmutableMap.of(Level.LIFECYCLE, messageConsumerFactory.apply(Level.LIFECYCLE)), + singleThreadedExecutor); + + testAnsiLoggerWithFooter.log(Level.LIFECYCLE, "lifecycle"); + testAnsiLoggerWithFooter.log(Level.PROGRESS, "progress"); + testAnsiLoggerWithFooter.log(Level.INFO, "info"); + testAnsiLoggerWithFooter.log(Level.DEBUG, "debug"); + testAnsiLoggerWithFooter.log(Level.WARN, "warn"); + testAnsiLoggerWithFooter.log(Level.ERROR, "error"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals(Collections.singletonList("lifecycle"), messages); + Assert.assertEquals(Collections.singletonList(Level.LIFECYCLE), levels); + } + + @Test + public void testLog_sameFooter() { + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); + testAnsiLoggerWithFooter.log(Level.INFO, "message"); + testAnsiLoggerWithFooter.log(Level.INFO, "another message"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals( + Arrays.asList( + "\033[1mfooter\033[0m", + "\033[1A\033[0J", + "\033[1Amessage", + "\033[1mfooter\033[0m", + "\033[1A\033[0J", + "\033[1Aanother message", + "\033[1mfooter\033[0m"), + messages); + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.INFO, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.INFO, + Level.LIFECYCLE), + levels); + } + + @Test + public void testLog_changingFooter() { + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); + testAnsiLoggerWithFooter.log(Level.WARN, "message"); + testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); + testAnsiLoggerWithFooter.log(Level.WARN, "another message"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals( + Arrays.asList( + "\033[1mfooter\033[0m", + "\033[1A\033[0J", + "\033[1Amessage", + "\033[1mfooter\033[0m", + "\033[1A\033[0J", + "\033[1A\033[1mtwo line\033[0m", + "\033[1mfooter\033[0m", + "\033[1A\033[1A\033[0J", + "\033[1Aanother message", + "\033[1mtwo line\033[0m", + "\033[1mfooter\033[0m"), + messages); + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.WARN, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.WARN, + Level.LIFECYCLE, + Level.LIFECYCLE), + levels); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java new file mode 100644 index 0000000000..120ba9f483 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder.ConsoleLoggerFactory; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link LogEventHandlerBuilder}. */ +@RunWith(MockitoJUnitRunner.class) +public class LogEventHandlerBuilderTest { + + @Mock private Consumer mockLifecycleConsumer; + @Mock private Consumer mockProgressConsumer; + @Mock private Consumer mockInfoConsumer; + @Mock private Consumer mockDebugConsumer; + @Mock private Consumer mockWarnConsumer; + @Mock private Consumer mockErrorConsumer; + + @Test + public void testBuild() { + List messages = new ArrayList<>(); + List levels = new ArrayList<>(); + + ConsoleLoggerFactory consoleLoggerFactory = + messageConsumers -> { + Assert.assertEquals( + ImmutableMap.builder() + .put(Level.LIFECYCLE, mockLifecycleConsumer) + .put(Level.PROGRESS, mockProgressConsumer) + .put(Level.INFO, mockInfoConsumer) + .put(Level.DEBUG, mockDebugConsumer) + .put(Level.WARN, mockWarnConsumer) + .put(Level.ERROR, mockErrorConsumer) + .build(), + messageConsumers); + return (ConsoleLogger) + (logLevel, message) -> { + messages.add(message); + levels.add(logLevel); + }; + }; + + Consumer logEventHandler = + new LogEventHandlerBuilder(consoleLoggerFactory) + .lifecycle(mockLifecycleConsumer) + .progress(mockProgressConsumer) + .info(mockInfoConsumer) + .debug(mockDebugConsumer) + .warn(mockWarnConsumer) + .error(mockErrorConsumer) + .build(); + + logEventHandler.accept(LogEvent.lifecycle("lifecycle")); + logEventHandler.accept(LogEvent.progress("progress")); + logEventHandler.accept(LogEvent.info("info")); + logEventHandler.accept(LogEvent.debug("debug")); + logEventHandler.accept(LogEvent.warn("warn")); + logEventHandler.accept(LogEvent.error("error")); + + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, Level.PROGRESS, Level.INFO, Level.DEBUG, Level.WARN, Level.ERROR), + levels); + Assert.assertEquals( + Arrays.asList("lifecycle", "progress", "info", "debug", "warn", "error"), messages); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java new file mode 100644 index 0000000000..7836d39b8e --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link PlainConsoleLogger}. */ +public class PlainConsoleLoggerTest { + + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); + + private final List levels = new ArrayList<>(); + private final List messages = new ArrayList<>(); + private final Function> messageConsumerFactory = + level -> + message -> { + levels.add(level); + messages.add(message); + }; + + private PlainConsoleLogger testPlainConsoleLogger; + + @Test + public void testLog() { + ImmutableMap.Builder> messageConsumers = ImmutableMap.builder(); + for (Level level : Level.values()) { + messageConsumers.put(level, messageConsumerFactory.apply(level)); + } + + testPlainConsoleLogger = + new PlainConsoleLogger(messageConsumers.build(), singleThreadedExecutor); + + testPlainConsoleLogger.log(Level.LIFECYCLE, "lifecycle"); + testPlainConsoleLogger.log(Level.PROGRESS, "progress"); + testPlainConsoleLogger.log(Level.INFO, "info"); + testPlainConsoleLogger.log(Level.DEBUG, "debug"); + testPlainConsoleLogger.log(Level.WARN, "warn"); + testPlainConsoleLogger.log(Level.ERROR, "error"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, Level.PROGRESS, Level.INFO, Level.DEBUG, Level.WARN, Level.ERROR), + levels); + Assert.assertEquals( + Arrays.asList("lifecycle", "progress", "info", "debug", "warn", "error"), messages); + } + + @Test + public void testLog_ignoreIfNoMessageConsumer() { + testPlainConsoleLogger = + new PlainConsoleLogger( + ImmutableMap.of(Level.WARN, messageConsumerFactory.apply(Level.WARN)), + singleThreadedExecutor); + + testPlainConsoleLogger.log(Level.LIFECYCLE, "lifecycle"); + testPlainConsoleLogger.log(Level.PROGRESS, "progress"); + testPlainConsoleLogger.log(Level.INFO, "info"); + testPlainConsoleLogger.log(Level.DEBUG, "debug"); + testPlainConsoleLogger.log(Level.WARN, "warn"); + testPlainConsoleLogger.log(Level.ERROR, "error"); + + singleThreadedExecutor.shutDownAndAwaitTermination(); + + Assert.assertEquals(Collections.singletonList(Level.WARN), levels); + Assert.assertEquals(Collections.singletonList("warn"), messages); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java similarity index 93% rename from jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java index 2977a8d46d..157b388a84 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ProgressDisplayGeneratorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.plugins.common; +package com.google.cloud.tools.jib.plugins.common.logging; import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.Arrays; @@ -22,7 +22,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link ProgressDisplayGenerator}. */ +/** Tests for {@link com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator}. */ public class ProgressDisplayGeneratorTest { @Test diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java new file mode 100644 index 0000000000..4fe5572310 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common.logging; + +import com.google.cloud.tools.jib.MultithreadedExecutor; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link SingleThreadedExecutor}. */ +public class SingleThreadedExecutorTest { + + @Test + public void testExecute_mutualExclusion() + throws IOException, ExecutionException, InterruptedException { + SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); + Lock lock = new ReentrantLock(); + + try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { + multithreadedExecutor.invokeAll( + Collections.nCopies( + 100, + () -> { + singleThreadedExecutor.execute( + () -> { + Assert.assertTrue(lock.tryLock()); + Thread.yield(); + lock.unlock(); + }); + return null; + })); + } + + singleThreadedExecutor.shutDownAndAwaitTermination(); + } +} From c81f8e90b58a379ef999c98311233310164310bd Mon Sep 17 00:00:00 2001 From: Q Chen Date: Fri, 14 Dec 2018 19:57:56 -0500 Subject: [PATCH 0401/2020] Rewords task progress descriptions. (#1351) --- .../builder/steps/AuthenticatePushStep.java | 2 +- .../BuildAndCacheApplicationLayerStep.java | 5 +++-- .../jib/builder/steps/BuildImageStep.java | 2 +- .../jib/builder/steps/LoadDockerStep.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 4 ++-- .../PullAndCacheBaseImageLayersStep.java | 2 +- .../jib/builder/steps/PullBaseImageStep.java | 2 +- .../steps/PushContainerConfigurationStep.java | 2 +- .../jib/builder/steps/PushImageStep.java | 19 +++++++++++++------ .../jib/builder/steps/PushLayersStep.java | 2 +- .../RetrieveRegistryCredentialsStep.java | 3 +-- .../tools/jib/builder/steps/StepsRunner.java | 6 +++--- .../jib/builder/steps/WriteTarFileStep.java | 2 +- 13 files changed, 30 insertions(+), 23 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index d387f9f382..25180b4b11 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -80,7 +80,7 @@ public Authorization call() String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("authenticate push to " + registry, 1); + progressEventDispatcherFactory.create("authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 553fd7373c..c36ceb6e98 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -50,7 +50,8 @@ static ImmutableList makeList( int layerCount = buildConfiguration.getLayerConfigurations().size(); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("Build application layers", layerCount); + progressEventDispatcherFactory.create( + "setting up to build application layers", layerCount); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = @@ -107,7 +108,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("Build " + layerType + " layer", 1); + progressEventDispatcherFactory.create("building " + layerType + " layer", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index dcb0090905..7fce8d5afd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -92,7 +92,7 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("build image format", 1); + progressEventDispatcherFactory.create("building image format", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 5568af21cf..2b9571b218 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -94,7 +94,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() .dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("Load to Docker daemon", 1)) { + progressEventDispatcherFactory.create("loading to Docker daemon", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 8ed4a60a3f..53e897d959 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -68,7 +68,7 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pull base image layer " + layerDigest, 1); + progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { @@ -89,7 +89,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcherContainer progressEventDispatcherContainer = new ProgressEventDispatcherContainer( progressEventDispatcher.newChildProducer(), - "pull base image layer blob " + layerDigest)) { + "pulling base image layer " + layerDigest)) { return cache.writeCompressedLayer( registryClient.pullBlob( layerDigest, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 022bcbccab..efb038e28b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -75,7 +75,7 @@ public ImmutableList call() try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "pull base image layers", baseImageLayers.size()); + "checking base image layers", baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 5538aecc3c..dadbe8cc9a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -118,7 +118,7 @@ public BaseImageWithAuthorization call() + "...")); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pull base image", 2); + progressEventDispatcherFactory.create("pulling base image manifest", 2); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 6b6e3d5f84..03305ff20d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -85,7 +85,7 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("push container configuration", 1); + progressEventDispatcherFactory.create("pushing container configuration", 1); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index d24481956c..97cb96d96a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -119,8 +120,9 @@ private ListenableFuture> afterPushSteps() } private ListenableFuture afterAllPushed() throws ExecutionException, IOException { + ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("Push to registry", 1); + progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { @@ -144,14 +146,19 @@ private ListenableFuture afterAllPushed() throws ExecutionException // Pushes to all target image tags. List> pushAllTagsFutures = new ArrayList<>(); - for (String tag : buildConfiguration.getAllTargetImageTags()) { + for (String tag : targetImageTags) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory = + progressEventDispatcher.newChildProducer(); pushAllTagsFutures.add( listeningExecutorService.submit( () -> { - buildConfiguration - .getEventDispatcher() - .dispatch(LogEvent.info("Tagging with " + tag + "...")); - registryClient.pushManifest(manifestTemplate, tag); + try (ProgressEventDispatcher ignored2 = + progressEventDispatcherFactory.create("tagging with " + tag, 1)) { + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.info("Tagging with " + tag + "...")); + registryClient.pushManifest(manifestTemplate, tag); + } return null; })); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index e086217ce8..52339a0c52 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -79,7 +79,7 @@ public ImmutableList> call() throws ExecutionException { NonBlockingSteps.get(cachedLayerStep); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("push layers", cachedLayers.size())) { + progressEventDispatcherFactory.create("setting up to push layers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 0ca20d4f14..090d565918 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -101,8 +101,7 @@ public Credential call() throws CredentialRetrievalException { buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - "Retrieve registry credentials for " + registry, 1); + progressEventDispatcherFactory.create("retrieving credentials for " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index df12dac2a0..6581dbcf2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -222,7 +222,7 @@ public StepsRunner finalizingBuild() { } public StepsRunner pushImage() { - rootProgressAllocationDescription = "Build to registry"; + rootProgressAllocationDescription = "building image to registry"; return enqueueStep( () -> @@ -239,7 +239,7 @@ public StepsRunner pushImage() { } public StepsRunner loadDocker(DockerClient dockerClient) { - rootProgressAllocationDescription = "Build to Docker daemon"; + rootProgressAllocationDescription = "building image to Docker daemon"; return enqueueStep( () -> @@ -255,7 +255,7 @@ public StepsRunner loadDocker(DockerClient dockerClient) { } public StepsRunner writeTarFile(Path outputPath) { - rootProgressAllocationDescription = "Build to tar file"; + rootProgressAllocationDescription = "building image to tar file"; return enqueueStep( () -> diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 3034d73b1a..661c05da09 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -94,7 +94,7 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("Write to tar file", 1)) { + progressEventDispatcherFactory.create("writing to tar file", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Builds the image to a tarball. From 8beb4e44af1998fa7d1968e152068c6237539926 Mon Sep 17 00:00:00 2001 From: sullis Date: Mon, 17 Dec 2018 08:54:36 -0800 Subject: [PATCH 0402/2020] mockito 2.23.4 (#1352) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 705e6f0d00..b4c0587b41 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation 'org.javassist:javassist:3.22.0-GA' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.12.0' + testImplementation 'org.mockito:mockito-core:2.23.4' testImplementation 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 6b4726325b..e2cb93296c 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -68,7 +68,7 @@ dependencies { compile 'org.javassist:javassist:3.22.0-GA' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.12.0' + testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' compile gradleApi() diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0fdbb50902..217382d02c 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -113,7 +113,7 @@ org.mockito mockito-core - 2.12.0 + 2.23.4 test diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 57e8c4253e..81d83d98b8 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -36,7 +36,7 @@ dependencies { compile 'org.javassist:javassist:3.22.0-GA' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.12.0' + testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin From 33008da17adb58f1878438e5f1c0407af40952da Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 17 Dec 2018 15:38:41 -0500 Subject: [PATCH 0403/2020] Attachs ProgressEventHandler as an event handler via the plugins. (#1350) --- .../jib/gradle/GradleProjectProperties.java | 45 ++++++++++++------- .../jib/maven/MavenProjectProperties.java | 34 +++++++++----- .../common/logging/AnsiLoggerWithFooter.java | 3 +- .../plugins/common/logging/ConsoleLogger.java | 10 ++++- ...Builder.java => ConsoleLoggerBuilder.java} | 44 +++++++++--------- .../common/logging/PlainConsoleLogger.java | 6 +++ .../logging/ProgressDisplayGenerator.java | 4 +- ...est.java => ConsoleLoggerBuilderTest.java} | 42 ++++++++++------- 8 files changed, 116 insertions(+), 72 deletions(-) rename jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/{LogEventHandlerBuilder.java => ConsoleLoggerBuilder.java} (69%) rename jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/{LogEventHandlerBuilderTest.java => ConsoleLoggerBuilderTest.java} (74%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index b6ad6e3d94..12fd726ab8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -20,12 +20,15 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; -import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; import java.io.File; @@ -38,7 +41,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.function.Consumer; import javax.annotation.Nullable; import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.GradleException; @@ -54,7 +56,7 @@ class GradleProjectProperties implements ProjectProperties { /** Used to generate the User-Agent header and history metadata. */ - static final String TOOL_NAME = "jib-gradle-plugin"; + private static final String TOOL_NAME = "jib-gradle-plugin"; /** Used for logging during main class inference. */ private static final String PLUGIN_NAME = "jib"; @@ -97,31 +99,42 @@ static Path getExplodedWarDirectory(Project project) { private static EventHandlers makeEventHandlers( Project project, Logger logger, SingleThreadedExecutor singleThreadedExecutor) { - LogEventHandlerBuilder logEventHandlerBuilder = + ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(project) - ? LogEventHandlerBuilder.rich(singleThreadedExecutor) - : LogEventHandlerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) .lifecycle(logger::lifecycle); if (logger.isDebugEnabled()) { - logEventHandlerBuilder.debug(logger::debug); + consoleLoggerBuilder.debug(logger::debug); } if (logger.isInfoEnabled()) { - logEventHandlerBuilder.info(logger::info); + consoleLoggerBuilder.info(logger::info); } if (logger.isWarnEnabled()) { - logEventHandlerBuilder.warn(logger::warn); + consoleLoggerBuilder.warn(logger::warn); } if (logger.isErrorEnabled()) { - logEventHandlerBuilder.error(logger::error); + consoleLoggerBuilder.error(logger::error); } - Consumer logEventHandler = logEventHandlerBuilder.build(); - - TimerEventHandler timerEventHandler = - new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); + ConsoleLogger consoleLogger = consoleLoggerBuilder.build(); return new EventHandlers() - .add(JibEventType.LOGGING, logEventHandler) - .add(JibEventType.TIMING, timerEventHandler); + .add( + JibEventType.LOGGING, + logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) + .add( + JibEventType.TIMING, + new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + .add( + JibEventType.PROGRESS, + new ProgressEventHandler( + update -> { + List footer = + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedAllocations()); + footer.add(""); + consoleLogger.setFooter(footer); + })); } private static boolean isProgressFooterEnabled(Project project) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index f36235874b..d97cf1c4dc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -20,19 +20,21 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; -import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; +import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; -import java.util.function.Consumer; import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; @@ -51,7 +53,7 @@ public class MavenProjectProperties implements ProjectProperties { public static final String PLUGIN_KEY = "com.google.cloud.tools:" + PLUGIN_NAME; /** Used to generate the User-Agent header and history metadata. */ - static final String TOOL_NAME = "jib-maven-plugin"; + private static final String TOOL_NAME = "jib-maven-plugin"; /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'maven-jar-plugin'"; @@ -89,10 +91,10 @@ static MavenProjectProperties getForProject( private static EventHandlers makeEventHandlers( Log log, SingleThreadedExecutor singleThreadedExecutor) { - LogEventHandlerBuilder logEventHandlerBuilder = + ConsoleLoggerBuilder logEventHandlerBuilder = (isProgressFooterEnabled() - ? LogEventHandlerBuilder.rich(singleThreadedExecutor) - : LogEventHandlerBuilder.plain(singleThreadedExecutor).progress(log::info)) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) .lifecycle(log::info); if (log.isDebugEnabled()) { logEventHandlerBuilder @@ -106,14 +108,22 @@ private static EventHandlers makeEventHandlers( if (log.isErrorEnabled()) { logEventHandlerBuilder.error(log::error); } - Consumer logEventHandler = logEventHandlerBuilder.build(); - - TimerEventHandler timerEventHandler = - new TimerEventHandler(message -> logEventHandler.accept(LogEvent.debug(message))); + ConsoleLogger consoleLogger = logEventHandlerBuilder.build(); return new EventHandlers() - .add(JibEventType.LOGGING, logEventHandler) - .add(JibEventType.TIMING, timerEventHandler); + .add( + JibEventType.LOGGING, + logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) + .add( + JibEventType.TIMING, + new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + .add( + JibEventType.PROGRESS, + new ProgressEventHandler( + update -> + consoleLogger.setFooter( + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedAllocations())))); } private static boolean isProgressFooterEnabled() { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index a586f99a6d..1dc31bca10 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -93,7 +93,8 @@ public void log(Level logLevel, String message) { * * @param newFooterLines the footer, with each line as an element (no newline at end) */ - void setFooter(List newFooterLines) { + @Override + public void setFooter(List newFooterLines) { if (newFooterLines.equals(footerLines)) { return; } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java index 4cb2dd3547..a21e61e6de 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java @@ -18,9 +18,10 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import java.util.List; /** Logs messages to the console. Implementations must be thread-safe. */ -interface ConsoleLogger { +public interface ConsoleLogger { /** * Logs {@code message} to the console at {@link Level#LIFECYCLE}. @@ -29,4 +30,11 @@ interface ConsoleLogger { * @param message the message */ void log(LogEvent.Level logLevel, String message); + + /** + * Sets the footer. + * + * @param footerLines the footer, with each line as an element (no newline at end) + */ + void setFooter(List footerLines); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java similarity index 69% rename from jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java index cd845140e7..49852f5cca 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilder.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java @@ -16,15 +16,14 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.LogEvent.Level; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import java.util.function.Consumer; import java.util.function.Function; -/** Builds a handler for {@link LogEvent}. */ -public class LogEventHandlerBuilder { +/** Builds a handler for {@link ConsoleLogger}. */ +public class ConsoleLoggerBuilder { /** * Alias for function that takes a map from {@link Level} to a log message {@link Consumer} and @@ -36,26 +35,26 @@ interface ConsoleLoggerFactory extends Function>, ConsoleLogger> {} /** - * Starts a {@link LogEventHandlerBuilder} for rich logging (ANSI support with footer). + * Starts a {@link ConsoleLoggerBuilder} for rich logging (ANSI support with footer). * * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are * logged in a sequential, deterministic order - * @return a new {@link LogEventHandlerBuilder} + * @return a new {@link ConsoleLoggerBuilder} */ - public static LogEventHandlerBuilder rich(SingleThreadedExecutor singleThreadedExecutor) { - return new LogEventHandlerBuilder( + public static ConsoleLoggerBuilder rich(SingleThreadedExecutor singleThreadedExecutor) { + return new ConsoleLoggerBuilder( messageConsumers -> new AnsiLoggerWithFooter(messageConsumers, singleThreadedExecutor)); } /** - * Starts a {@link LogEventHandlerBuilder} for plain-text logging (no ANSI support). + * Starts a {@link ConsoleLoggerBuilder} for plain-text logging (no ANSI support). * * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are * logged in a sequential, deterministic order - * @return a new {@link LogEventHandlerBuilder} + * @return a new {@link ConsoleLoggerBuilder} */ - public static LogEventHandlerBuilder plain(SingleThreadedExecutor singleThreadedExecutor) { - return new LogEventHandlerBuilder( + public static ConsoleLoggerBuilder plain(SingleThreadedExecutor singleThreadedExecutor) { + return new ConsoleLoggerBuilder( messageConsumers -> new PlainConsoleLogger(messageConsumers, singleThreadedExecutor)); } @@ -64,7 +63,7 @@ public static LogEventHandlerBuilder plain(SingleThreadedExecutor singleThreaded private final ConsoleLoggerFactory consoleLoggerFactory; @VisibleForTesting - LogEventHandlerBuilder(ConsoleLoggerFactory consoleLoggerFactory) { + ConsoleLoggerBuilder(ConsoleLoggerFactory consoleLoggerFactory) { this.consoleLoggerFactory = consoleLoggerFactory; } @@ -74,7 +73,7 @@ public static LogEventHandlerBuilder plain(SingleThreadedExecutor singleThreaded * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder lifecycle(Consumer messageConsumer) { + public ConsoleLoggerBuilder lifecycle(Consumer messageConsumer) { messageConsumers.put(Level.LIFECYCLE, messageConsumer); return this; } @@ -85,7 +84,7 @@ public LogEventHandlerBuilder lifecycle(Consumer messageConsumer) { * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder progress(Consumer messageConsumer) { + public ConsoleLoggerBuilder progress(Consumer messageConsumer) { messageConsumers.put(Level.PROGRESS, messageConsumer); return this; } @@ -96,7 +95,7 @@ public LogEventHandlerBuilder progress(Consumer messageConsumer) { * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder debug(Consumer messageConsumer) { + public ConsoleLoggerBuilder debug(Consumer messageConsumer) { messageConsumers.put(Level.DEBUG, messageConsumer); return this; } @@ -107,7 +106,7 @@ public LogEventHandlerBuilder debug(Consumer messageConsumer) { * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder error(Consumer messageConsumer) { + public ConsoleLoggerBuilder error(Consumer messageConsumer) { messageConsumers.put(Level.ERROR, messageConsumer); return this; } @@ -118,7 +117,7 @@ public LogEventHandlerBuilder error(Consumer messageConsumer) { * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder info(Consumer messageConsumer) { + public ConsoleLoggerBuilder info(Consumer messageConsumer) { messageConsumers.put(Level.INFO, messageConsumer); return this; } @@ -129,18 +128,17 @@ public LogEventHandlerBuilder info(Consumer messageConsumer) { * @param messageConsumer the message {@link Consumer} * @return this */ - public LogEventHandlerBuilder warn(Consumer messageConsumer) { + public ConsoleLoggerBuilder warn(Consumer messageConsumer) { messageConsumers.put(Level.WARN, messageConsumer); return this; } /** - * Builds the {@link LogEvent} handler. + * Builds the {@link ConsoleLogger}. * - * @return the {@link Consumer} + * @return the {@link ConsoleLogger} */ - public Consumer build() { - ConsoleLogger consoleLogger = consoleLoggerFactory.apply(messageConsumers.build()); - return logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); + public ConsoleLogger build() { + return consoleLoggerFactory.apply(messageConsumers.build()); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java index 51f145a2ab..d244513b93 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.event.events.LogEvent.Level; import com.google.common.collect.ImmutableMap; +import java.util.List; import java.util.function.Consumer; /** Logs messages plainly. */ @@ -49,4 +50,9 @@ public void log(Level logLevel, String message) { singleThreadedExecutor.execute(() -> messageConsumer.accept(message)); } + + @Override + public void setFooter(List footerLines) { + // No op. + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java index a6d47ffeea..a32c1c2e8a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java @@ -33,7 +33,7 @@ * > task 1 running
* > task 3 running */ -class ProgressDisplayGenerator { +public class ProgressDisplayGenerator { /** Line above progress bar. */ private static final String HEADER = "Executing tasks:"; @@ -48,7 +48,7 @@ class ProgressDisplayGenerator { * @param unfinishedAllocations the unfinished {@link Allocation}s * @return the progress display as a list of lines */ - static List generateProgressDisplay( + public static List generateProgressDisplay( double progress, List unfinishedAllocations) { List lines = new ArrayList<>(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java similarity index 74% rename from jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java index 120ba9f483..577f9615e9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/LogEventHandlerBuilderTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java @@ -16,9 +16,8 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.LogEvent.Level; -import com.google.cloud.tools.jib.plugins.common.logging.LogEventHandlerBuilder.ConsoleLoggerFactory; +import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder.ConsoleLoggerFactory; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.Arrays; @@ -30,9 +29,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link LogEventHandlerBuilder}. */ +/** Tests for {@link ConsoleLoggerBuilder}. */ @RunWith(MockitoJUnitRunner.class) -public class LogEventHandlerBuilderTest { +public class ConsoleLoggerBuilderTest { @Mock private Consumer mockLifecycleConsumer; @Mock private Consumer mockProgressConsumer; @@ -58,15 +57,24 @@ public void testBuild() { .put(Level.ERROR, mockErrorConsumer) .build(), messageConsumers); - return (ConsoleLogger) - (logLevel, message) -> { - messages.add(message); - levels.add(logLevel); - }; + + return new ConsoleLogger() { + + @Override + public void log(Level logLevel, String message) { + messages.add(message); + levels.add(logLevel); + } + + @Override + public void setFooter(List footerLines) { + // No op. + } + }; }; - Consumer logEventHandler = - new LogEventHandlerBuilder(consoleLoggerFactory) + ConsoleLogger consoleLogger = + new ConsoleLoggerBuilder(consoleLoggerFactory) .lifecycle(mockLifecycleConsumer) .progress(mockProgressConsumer) .info(mockInfoConsumer) @@ -75,12 +83,12 @@ public void testBuild() { .error(mockErrorConsumer) .build(); - logEventHandler.accept(LogEvent.lifecycle("lifecycle")); - logEventHandler.accept(LogEvent.progress("progress")); - logEventHandler.accept(LogEvent.info("info")); - logEventHandler.accept(LogEvent.debug("debug")); - logEventHandler.accept(LogEvent.warn("warn")); - logEventHandler.accept(LogEvent.error("error")); + consoleLogger.log(Level.LIFECYCLE, "lifecycle"); + consoleLogger.log(Level.PROGRESS, "progress"); + consoleLogger.log(Level.INFO, "info"); + consoleLogger.log(Level.DEBUG, "debug"); + consoleLogger.log(Level.WARN, "warn"); + consoleLogger.log(Level.ERROR, "error"); Assert.assertEquals( Arrays.asList( From de666046f7a8e3d42e78fe7cd674863e33fd9448 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 17 Dec 2018 17:14:54 -0500 Subject: [PATCH 0404/2020] Adds 'jib.console' system property to disable progress bar if set to 'plain'. (#1354) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/gradle/GradleProjectProperties.java | 4 ++-- .../tools/jib/gradle/GradleProjectPropertiesTest.java | 9 +++++++++ jib-maven-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/maven/MavenProjectProperties.java | 4 ++-- .../cloud/tools/jib/plugins/common/PropertyNames.java | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3af8d8678f..4bc09a1062 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) ### Changed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 12fd726ab8..4e134b3876 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -138,8 +138,8 @@ private static EventHandlers makeEventHandlers( } private static boolean isProgressFooterEnabled(Project project) { - // TODO: Make SHOW_PROGRESS be true by default. - if (!Boolean.getBoolean(PropertyNames.SHOW_PROGRESS)) { + // TODO: Consolidate with MavenProjectProperties? + if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 73f5fb7572..57b7e16cff 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -22,11 +22,14 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.Collections; +import org.gradle.StartParameter; import org.gradle.api.Project; import org.gradle.api.internal.file.FileResolver; +import org.gradle.api.invocation.Gradle; import org.gradle.api.java.archives.Manifest; import org.gradle.api.java.archives.internal.DefaultManifest; import org.gradle.api.logging.Logger; +import org.gradle.api.logging.configuration.ConsoleOutput; import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.TaskContainer; @@ -53,6 +56,8 @@ public class GradleProjectPropertiesTest { @Mock private TaskContainer mockTaskContainer; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Logger mockLogger; + @Mock private Gradle mockGradle; + @Mock private StartParameter mockStartParameter; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -68,6 +73,10 @@ public void setup() { Mockito.when(mockTaskContainer.findByName("war")).thenReturn(Mockito.mock(War.class)); Mockito.when(mockJar.getManifest()).thenReturn(manifest); + Mockito.when(mockProject.getGradle()).thenReturn(mockGradle); + Mockito.when(mockGradle.getStartParameter()).thenReturn(mockStartParameter); + Mockito.when(mockStartParameter.getConsoleOutput()).thenReturn(ConsoleOutput.Auto); + gradleProjectProperties = new GradleProjectProperties(mockProject, mockLogger, mockJavaLayerConfigurations); } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 327af1075f..44c674a94e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index d97cf1c4dc..1cb4e4ba41 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -127,8 +127,8 @@ private static EventHandlers makeEventHandlers( } private static boolean isProgressFooterEnabled() { - // TODO: Make SHOW_PROGRESS be true by default. - if (!Boolean.getBoolean(PropertyNames.SHOW_PROGRESS)) { + // TODO: Consolidate with GradleProjectProperties? + if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index b270b18f5d..b66531e25d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -49,7 +49,7 @@ public class PropertyNames { public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; public static final String SKIP = "jib.skip"; - public static final String SHOW_PROGRESS = "jib.showProgress"; + public static final String CONSOLE = "jib.console"; private PropertyNames() {} } From a0159410031eae52378f0e56209c8ad813942b73 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 18 Dec 2018 11:34:26 -0500 Subject: [PATCH 0405/2020] Make DefaultCacheStorageWriter.moveIfDoesNotExist() more robust to concurrency (#1355) --- .../jib/cache/DefaultCacheStorageWriter.java | 35 +++++-------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java index 78372c3962..c826ce0465 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java @@ -29,13 +29,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -67,35 +64,19 @@ private WrittenLayer( * @throws IOException if an I/O exception occurs */ private static void moveIfDoesNotExist(Path source, Path destination) throws IOException { + // If the file already exists, we skip renaming and use the existing file. This happens if a + // new layer happens to have the same content as a previously-cached layer. if (Files.exists(destination)) { return; } try { - Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE); + Files.move(source, destination); - } catch (FileAlreadyExistsException ignored) { - // If the file already exists, we skip renaming and use the existing file. This happens if a - // new layer happens to have the same content as a previously-cached layer. - // - // Do not attempt to remove the try-catch block with the idea of checking file existence - // before moving; there can be concurrent file moves. - - } catch (AtomicMoveNotSupportedException ignored) { - try { - Files.move(source, destination); - - } catch (FileAlreadyExistsException alsoIgnored) { - // Same reasoning - - } catch (DirectoryNotEmptyException ex) { - // The file system cannot rename directories, so we must resort to copying the directory. - Files.createDirectory(destination); - try (Stream sourceFiles = Files.list(source)) { - for (Path sourceFile : sourceFiles.collect(Collectors.toList())) { - Files.copy(sourceFile, destination.resolve(sourceFile.getFileName())); - } - } + } catch (FileSystemException ex) { + if (!Files.exists(destination)) { + // TODO to log that the destination exists + throw ex; } } } From fa26de624a46fd3b0e6c41259f994a250adf4328 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 18 Dec 2018 15:29:24 -0500 Subject: [PATCH 0406/2020] Waits for logging thread to finish before throwing errors. (#1363) --- .../tools/jib/gradle/BuildDockerTask.java | 31 ++++++++++------- .../tools/jib/gradle/BuildImageTask.java | 30 ++++++++++------- .../cloud/tools/jib/gradle/BuildTarTask.java | 30 ++++++++++------- .../tools/jib/maven/BuildDockerMojo.java | 33 +++++++++++-------- .../cloud/tools/jib/maven/BuildImageMojo.java | 32 ++++++++++-------- .../cloud/tools/jib/maven/BuildTarMojo.java | 31 +++++++++-------- 6 files changed, 112 insertions(+), 75 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index ad601bfa11..190212611f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -136,27 +136,34 @@ public void buildDocker() .build(); Path buildOutput = getProject().getBuildDir().toPath(); - BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); + + try { + BuildStepsRunner.forBuildToDockerDaemon( + targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index f669686e5a..ddec930f07 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -117,27 +117,33 @@ public void buildImage() .build(); Path buildOutput = getProject().getBuildDir().toPath(); - BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); + + try { + BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 1c98bd09db..3923fb199f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -134,27 +134,33 @@ public void buildTar() .build(); Path buildOutput = getProject().getBuildDir().toPath(); - BuildStepsRunner.forBuildTar(tarOutputPath) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); + + try { + BuildStepsRunner.forBuildTar(tarOutputPath) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + } } catch (InvalidAppRootException ex) { throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 106176372c..d65105c3e1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -101,24 +101,30 @@ public void execute() throws MojoExecutionException { .build(); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - BuildStepsRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); + + try { + BuildStepsRunner.forBuildToDockerDaemon( + targetImageReference, getTargetImageAdditionalTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + getLog().info(""); + } } catch (InvalidAppRootException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " @@ -128,6 +134,7 @@ public void execute() throws MojoExecutionException { } catch (InvalidContainerVolumeException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 6c9b0cbe2b..9f090557a5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -117,24 +117,29 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build(); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); + + try { + BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + getLog().info(""); + } } catch (InvalidAppRootException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " @@ -144,6 +149,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (InvalidContainerVolumeException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 506c49707e..625e34afaf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -95,24 +95,28 @@ public void execute() throws MojoExecutionException { .setTargetImageReference(pluginConfigurationProcessor.getTargetImageReference()) .build(); - BuildStepsRunner.forBuildTar(tarOutputPath) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), - helpfulSuggestions); - - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); + try { + BuildStepsRunner.forBuildTar(tarOutputPath) + .writeImageDigest(buildOutput.resolve("jib-image.digest")) + .writeImageId(buildOutput.resolve("jib-image.id")) + .build( + pluginConfigurationProcessor.getJibContainerBuilder(), + pluginConfigurationProcessor.getContainerizer(), + eventDispatcher, + projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), + helpfulSuggestions); + + } finally { + // TODO: This should not be called on projectProperties. + projectProperties.waitForLoggingThread(); + getLog().info(""); + } } catch (InvalidAppRootException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " @@ -122,6 +126,7 @@ public void execute() throws MojoExecutionException { } catch (InvalidContainerVolumeException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException From c52c399b96801bfa140937ae284af4d67d940bd2 Mon Sep 17 00:00:00 2001 From: Mark Heeren Date: Tue, 18 Dec 2018 22:06:11 +0100 Subject: [PATCH 0407/2020] Add proxy authentication (#1337) --- .../cloud/tools/jib/http/Connection.java | 46 ++++++++++ .../tools/jib/maven/BuildDockerMojo.java | 2 + .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../cloud/tools/jib/maven/BuildTarMojo.java | 1 + .../cloud/tools/jib/maven/ProxyProvider.java | 84 +++++++++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 5602b53622..922b5ce693 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -31,6 +31,9 @@ import java.security.GeneralSecurityException; import java.util.function.Function; import javax.annotation.Nullable; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.DefaultHttpClient; /** * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than @@ -62,6 +65,7 @@ public static Function getConnectionFactory() { * href="https://github.com/google/google-http-java-client/issues/39">https://github.com/google/google-http-java-client/issues/39 */ HttpTransport transport = new ApacheHttpTransport(); + addProxyCredentials(transport); return url -> new Connection(url, transport); } @@ -75,9 +79,51 @@ public static Function getInsecureConnectionFactory() throws GeneralSecurityException { // Do not use {@link NetHttpTransport}. See {@link getConnectionFactory} for details. HttpTransport transport = new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); + addProxyCredentials(transport); return url -> new Connection(url, transport); } + /** + * Registers proxy credentials onto transport client, in order to deal with proxies that require + * basic authentication. + * + * @param transport + */ + private static void addProxyCredentials(HttpTransport transport) { + DefaultHttpClient httpClient = + (DefaultHttpClient) ((ApacheHttpTransport) transport).getHttpClient(); + + boolean httpProxy = System.getProperty("http.proxyHost") != null; + boolean httpCreds = + System.getProperty("http.proxyUser") != null + && System.getProperty("http.proxyPassword") != null; + if (httpProxy && httpCreds) { + httpClient + .getCredentialsProvider() + .setCredentials( + new AuthScope( + System.getProperty("http.proxyHost"), + Integer.parseInt(System.getProperty("http.proxyPort", "8080"))), + new UsernamePasswordCredentials( + System.getProperty("http.proxyUser"), System.getProperty("http.proxyPassword"))); + } + boolean httpsProxy = System.getProperty("https.proxyHost") != null; + boolean httpsCreds = + System.getProperty("https.proxyUser") != null + && System.getProperty("https.proxyPassword") != null; + if (httpsProxy && httpsCreds) { + httpClient + .getCredentialsProvider() + .setCredentials( + new AuthScope( + System.getProperty("https.proxyHost"), + Integer.parseInt(System.getProperty("https.proxyPort", "443"))), + new UsernamePasswordCredentials( + System.getProperty("https.proxyUser"), + System.getProperty("https.proxyPassword"))); + } + } + private HttpRequestFactory requestFactory; @Nullable private HttpResponse httpResponse; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index d65105c3e1..8d97831527 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -75,6 +75,7 @@ public void execute() throws MojoExecutionException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); @@ -90,6 +91,7 @@ public void execute() throws MojoExecutionException { null, null, mavenHelpfulSuggestionsBuilder.build()); + ProxyProvider.init(getSession().getSettings()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 9f090557a5..eeb569fd9c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -104,6 +104,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties); + ProxyProvider.init(getSession().getSettings()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 625e34afaf..beb93ac698 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -86,6 +86,7 @@ public void execute() throws MojoExecutionException { projectProperties, tarOutputPath, mavenHelpfulSuggestionsBuilder.build()); + ProxyProvider.init(getSession().getSettings()); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java new file mode 100644 index 0000000000..94955345ff --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Settings; + +/** Propagates proxy configuration from Maven settings to system properties * */ +public class ProxyProvider { + + private static final List PROXY_PROPERTIES = + Arrays.asList("proxyHost,proxyPort,proxyUser,proxyPassword".split(",")); + + /** + * Initializes proxy settings based on Maven settings. + * + * @param settings - Maven settings from mojo + */ + public static void init(Settings settings) { + settings + .getProxies() + .stream() + .filter(proxy -> proxy.isActive()) + .collect(Collectors.toList()) + .forEach(proxy -> setProxyProperties(proxy)); + } + + /** + * Set proxy system properties based on Maven proxy configuration. These system properties will be + * picked up by {@link java.net.ProxySelector} used in {@link + * com.google.cloud.tools.jib.http.Connection}, while connecting to container image registries. + * + * @param proxy Maven proxy + */ + private static void setProxyProperties(Proxy proxy) { + String protocol = proxy.getProtocol(); + if (protocol != null && !proxyPropertiesSet(protocol)) { + setProxyProperty(protocol + ".proxyHost", proxy.getHost()); + setProxyProperty(protocol + ".proxyPort", String.valueOf(proxy.getPort())); + setProxyProperty(protocol + ".proxyUser", proxy.getUsername()); + setProxyProperty(protocol + ".proxyPassword", proxy.getPassword()); + setProxyProperty("http.nonProxyHosts", proxy.getNonProxyHosts()); + } + } + + /** + * Set proxy system property if it has a proper value + * + * @param property property name + * @param value property value + */ + private static void setProxyProperty(String property, String value) { + if (property != null && value != null) { + System.setProperty(property, value); + } + } + + /** + * Check if any proxy system properties are already set for a given protocol. Note, + * nonProxyHosts is excluded as it can only be set with http. + * + * @param protocol protocol + */ + private static boolean proxyPropertiesSet(String protocol) { + return PROXY_PROPERTIES.stream().anyMatch(p -> System.getProperty(protocol + "." + p) != null); + } +} From 249004f229a9efab41e7713b05fa4981ac5f0ac7 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 18 Dec 2018 16:43:45 -0500 Subject: [PATCH 0408/2020] Adds support for rc release to prepare_release.sh. (#1364) --- jib-gradle-plugin/scripts/prepare_release.sh | 18 +++++++++--------- jib-maven-plugin/scripts/prepare_release.sh | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh index c24179711f..024587e193 100755 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ b/jib-gradle-plugin/scripts/prepare_release.sh @@ -1,12 +1,8 @@ #!/bin/bash - -# Usage: ./scripts/prepare_release.sh +# Usage: ./scripts/prepare_release.sh [] set -e -Colorize() { - echo "$(tput setff $2)$1$(tput sgr0)" -} - EchoRed() { echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" } @@ -20,20 +16,24 @@ Die() { } DieUsage() { - Die "Usage: ./scripts/prepare_release.sh " + Die "Usage: ./scripts/prepare_release.sh []" } # Usage: CheckVersion CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || Die "Version not in ###.###.### format." + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." } -[ $# -ne 2 ] || DieUsage +[ $# -ne 1 ] && [ $# -ne 2 ] && DieUsage EchoGreen '===== RELEASE SETUP SCRIPT =====' VERSION=$1 CheckVersion ${VERSION} +if [ -n "$2" ]; then + POST_RELEASE_VERSION=$2 + CheckVersion ${POST_RELEASE_VERSION} +fi if [[ $(git status -uno --porcelain) ]]; then Die 'There are uncommitted changes.' @@ -47,7 +47,7 @@ BRANCH=gradle_release_v${VERSION} git checkout -b ${BRANCH} # Changes the version for release and creates the commits/tags. -echo | ./gradlew release -PreleaseVersion=${VERSION} +echo | ./gradlew release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} # Pushes the release branch and tag to Github. git push origin ${BRANCH} diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index 64b78ac0ff..188c91b488 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -1,5 +1,5 @@ #!/bin/bash - -# Usage: ./scripts/prepare_release.sh +# Usage: ./scripts/prepare_release.sh [] set -e @@ -39,8 +39,13 @@ EchoGreen '===== RELEASE SETUP SCRIPT =====' VERSION=$1 CheckVersion ${VERSION} -NEXT_VERSION=$(IncrementVersion $VERSION) -CheckVersion ${NEXT_VERSION} +if [ -n "$2" ]; then + NEXT_VERSION=$2 + CheckVersion ${NEXT_VERSION} +else + NEXT_VERSION=$(IncrementVersion $VERSION) + CheckVersion ${NEXT_VERSION} +fi if [[ $(git status -uno --porcelain) ]]; then Die 'There are uncommitted changes.' @@ -63,7 +68,10 @@ git tag ${TAG} # Updates the pom.xml with the next snapshot version. # For example, when releasing 1.5.7, the next snapshot version would be 1.5.8-SNAPSHOT. -NEXT_SNAPSHOT=${NEXT_VERSION}-SNAPSHOT +NEXT_SNAPSHOT=${NEXT_VERSION} +if [[ "${NEXT_SNAPSHOT}" != *-SNAPSHOT ]]; then + NEXT_SNAPSHOT=${NEXT_SNAPSHOT}-SNAPSHOT +fi mvn versions:set versions:commit -DnewVersion=${NEXT_SNAPSHOT} # Commits this next snapshot version. From e240b55e9e67ea651517351d18e57c155c3d8a4a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 19 Dec 2018 12:08:58 -0500 Subject: [PATCH 0409/2020] Maven release v1.0.0-rc1 (#1367) * preparing release 1.0.0-rc1 * 1.0.0-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 217382d02c..46dd07a1f6 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -3,7 +3,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.2-SNAPSHOT + 1.0.0-SNAPSHOT maven-plugin Jib From 42135def37953f4dee8179e5a0ae9870abcd5126 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Wed, 19 Dec 2018 13:42:15 -0500 Subject: [PATCH 0410/2020] Gradle release v1.0.0-rc1 (#1368) * [Gradle Release Plugin] - pre tag commit: 'v1.0.0-rc1-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.0.0-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 3b0f75f558..617dd212c6 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 0.10.2-SNAPSHOT +version = 1.0.0-SNAPSHOT From 67208a5978319fbb593f47168de931febcaad88b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 19 Dec 2018 18:27:18 -0500 Subject: [PATCH 0411/2020] Fix issues around adding proxy credentials to Apache HttpClient (#1366) * Fix issues * Add CHANGELOG entries * Check http(s).proxyPort property values * Add tests --- .../tools/jib/global/JibSystemProperties.java | 29 +- .../cloud/tools/jib/http/Connection.java | 63 ++-- .../jib/global/JibSystemPropertiesTest.java | 113 +++++++- .../ConnectionWithProxyCredentialsTest.java | 167 +++++++++++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 2 + .../cloud/tools/jib/maven/ProxyProvider.java | 70 ++--- .../tools/jib/maven/ProxyProviderTest.java | 269 ++++++++++++++++++ .../common/PluginConfigurationProcessor.java | 1 + 9 files changed, 635 insertions(+), 80 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index 26f4f8534f..9f8829558e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.collect.Range; /** Names of system properties defined/used by Jib. */ public class JibSystemProperties { @@ -83,18 +84,38 @@ public static boolean isUserAgentEnabled() { * @throws NumberFormatException if invalid values */ public static void checkHttpTimeoutProperty() throws NumberFormatException { - String value = System.getProperty(HTTP_TIMEOUT); + checkNumericSystemProperty(HTTP_TIMEOUT, Range.atLeast(0)); + } + + /** + * Checks if {@code http.proxyPort} and {@code https.proxyPort} system properties are in the + * [0..65535] range when set. + * + * @throws NumberFormatException if invalid values + */ + public static void checkProxyPortProperty() throws NumberFormatException { + checkNumericSystemProperty("http.proxyPort", Range.closed(0, 65535)); + checkNumericSystemProperty("https.proxyPort", Range.closed(0, 65535)); + } + + private static void checkNumericSystemProperty(String property, Range validRange) { + String value = System.getProperty(property); if (value == null) { return; } + int parsed; try { parsed = Integer.parseInt(value); } catch (NumberFormatException ex) { - throw new NumberFormatException(HTTP_TIMEOUT + " must be an integer: " + value); + throw new NumberFormatException(property + " must be an integer: " + value); } - if (parsed < 0) { - throw new NumberFormatException(HTTP_TIMEOUT + " cannot be negative: " + value); + if (validRange.hasLowerBound() && validRange.lowerEndpoint() > parsed) { + throw new NumberFormatException( + property + " cannot be less than " + validRange.lowerEndpoint() + ": " + value); + } else if (validRange.hasUpperBound() && validRange.upperEndpoint() < parsed) { + throw new NumberFormatException( + property + " cannot be greater than " + validRange.upperEndpoint() + ": " + value); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 922b5ce693..750d6e27cc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -64,7 +64,7 @@ public static Function getConnectionFactory() { * @see https://github.com/google/google-http-java-client/issues/39 */ - HttpTransport transport = new ApacheHttpTransport(); + ApacheHttpTransport transport = new ApacheHttpTransport(); addProxyCredentials(transport); return url -> new Connection(url, transport); } @@ -78,7 +78,8 @@ public static Function getConnectionFactory() { public static Function getInsecureConnectionFactory() throws GeneralSecurityException { // Do not use {@link NetHttpTransport}. See {@link getConnectionFactory} for details. - HttpTransport transport = new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); + ApacheHttpTransport transport = + new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); addProxyCredentials(transport); return url -> new Connection(url, transport); } @@ -87,41 +88,33 @@ public static Function getInsecureConnectionFactory() * Registers proxy credentials onto transport client, in order to deal with proxies that require * basic authentication. * - * @param transport + * @param transport Apache HTTP transport */ - private static void addProxyCredentials(HttpTransport transport) { - DefaultHttpClient httpClient = - (DefaultHttpClient) ((ApacheHttpTransport) transport).getHttpClient(); - - boolean httpProxy = System.getProperty("http.proxyHost") != null; - boolean httpCreds = - System.getProperty("http.proxyUser") != null - && System.getProperty("http.proxyPassword") != null; - if (httpProxy && httpCreds) { - httpClient - .getCredentialsProvider() - .setCredentials( - new AuthScope( - System.getProperty("http.proxyHost"), - Integer.parseInt(System.getProperty("http.proxyPort", "8080"))), - new UsernamePasswordCredentials( - System.getProperty("http.proxyUser"), System.getProperty("http.proxyPassword"))); - } - boolean httpsProxy = System.getProperty("https.proxyHost") != null; - boolean httpsCreds = - System.getProperty("https.proxyUser") != null - && System.getProperty("https.proxyPassword") != null; - if (httpsProxy && httpsCreds) { - httpClient - .getCredentialsProvider() - .setCredentials( - new AuthScope( - System.getProperty("https.proxyHost"), - Integer.parseInt(System.getProperty("https.proxyPort", "443"))), - new UsernamePasswordCredentials( - System.getProperty("https.proxyUser"), - System.getProperty("https.proxyPassword"))); + @VisibleForTesting + static void addProxyCredentials(ApacheHttpTransport transport) { + addProxyCredentials(transport, "https"); + addProxyCredentials(transport, "http"); + } + + private static void addProxyCredentials(ApacheHttpTransport transport, String protocol) { + Preconditions.checkArgument(protocol.equals("http") || protocol.equals("https")); + + String proxyHost = System.getProperty(protocol + ".proxyHost"); + String proxyUser = System.getProperty(protocol + ".proxyUser"); + String proxyPassword = System.getProperty(protocol + ".proxyPassword"); + if (proxyHost == null || proxyUser == null || proxyPassword == null) { + return; } + + String defaultProxyPort = protocol.equals("http") ? "80" : "443"; + int proxyPort = Integer.parseInt(System.getProperty(protocol + ".proxyPort", defaultProxyPort)); + + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + httpClient + .getCredentialsProvider() + .setCredentials( + new AuthScope(proxyHost, proxyPort), + new UsernamePasswordCredentials(proxyUser, proxyPassword)); } private HttpRequestFactory requestFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index 1657156973..f055a3c789 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -16,43 +16,140 @@ package com.google.cloud.tools.jib.global; +import javax.annotation.Nullable; import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; /** Tests for {@link JibSystemProperties}. */ public class JibSystemPropertiesTest { + @Nullable private String httpProxyPortSaved; + @Nullable private String httpsProxyPortSaved; + + @Before + public void setUp() { + httpProxyPortSaved = System.getProperty("http.proxyPort"); + httpsProxyPortSaved = System.getProperty("https.proxyPort"); + } + @After public void tearDown() { System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyPort"); + if (httpProxyPortSaved != null) { + System.setProperty("http.proxyPort", httpProxyPortSaved); + } + if (httpsProxyPortSaved != null) { + System.setProperty("https.proxyPort", httpsProxyPortSaved); + } } @Test - public void testCheckHttpTimeoutSystemProperty_ok() throws NumberFormatException { + public void testCheckHttpTimeoutProperty_ok() throws NumberFormatException { Assert.assertNull(System.getProperty(JibSystemProperties.HTTP_TIMEOUT)); JibSystemProperties.checkHttpTimeoutProperty(); } @Test - public void testCheckHttpTimeoutSystemProperty_stringValue() { + public void testCheckHttpTimeoutProperty_stringValue() { System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); try { JibSystemProperties.checkHttpTimeoutProperty(); - Assert.fail("Should error with a non-integer timeout"); + Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals("jib.httpTimeout must be an integer: random string", ex.getMessage()); } } @Test - public void testCheckHttpTimeoutSystemProperty_negativeValue() { - System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-80"); + public void testCheckHttpProxyPortProperty_undefined() throws NumberFormatException { + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyPort"); + JibSystemProperties.checkProxyPortProperty(); + } + + @Test + public void testCheckHttpProxyPortProperty() throws NumberFormatException { + System.setProperty("http.proxyPort", "0"); + System.setProperty("https.proxyPort", "0"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "1"); + System.setProperty("https.proxyPort", "1"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "65535"); + System.setProperty("https.proxyPort", "65535"); + JibSystemProperties.checkProxyPortProperty(); + + System.setProperty("http.proxyPort", "65534"); + System.setProperty("https.proxyPort", "65534"); + JibSystemProperties.checkProxyPortProperty(); + } + + @Test + public void testCheckHttpProxyPortProperty_negativeValue() { + System.setProperty("http.proxyPort", "-1"); + System.clearProperty("https.proxyPort"); try { - JibSystemProperties.checkHttpTimeoutProperty(); - Assert.fail("Should error with a negative timeout"); + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort cannot be less than 0: -1", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "-1"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("https.proxyPort cannot be less than 0: -1", ex.getMessage()); + } + } + + @Test + public void testCheckHttpProxyPortProperty_over65535() { + System.setProperty("http.proxyPort", "65536"); + System.clearProperty("https.proxyPort"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort cannot be greater than 65535: 65536", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "65536"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("https.proxyPort cannot be greater than 65535: 65536", ex.getMessage()); + } + } + + @Test + public void testCheckHttpProxyPortProperty_stringValue() { + System.setProperty("http.proxyPort", "some string"); + System.clearProperty("https.proxyPort"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); + } catch (NumberFormatException ex) { + Assert.assertEquals("http.proxyPort must be an integer: some string", ex.getMessage()); + } + + System.clearProperty("http.proxyPort"); + System.setProperty("https.proxyPort", "some string"); + try { + JibSystemProperties.checkProxyPortProperty(); + Assert.fail(); } catch (NumberFormatException ex) { - Assert.assertEquals("jib.httpTimeout cannot be negative: -80", ex.getMessage()); + Assert.assertEquals("https.proxyPort must be an integer: some string", ex.getMessage()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java new file mode 100644 index 0000000000..0b86e54a59 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link Connection} with setting proxy credentials. */ +public class ConnectionWithProxyCredentialsTest { + + private static final ImmutableList proxyProperties = + ImmutableList.of( + "http.proxyHost", + "http.proxyPort", + "http.proxyUser", + "http.proxyPassword", + "https.proxyHost", + "https.proxyPort", + "https.proxyUser", + "https.proxyPassword"); + + // HashMap to allow saving null values. + private final HashMap savedProperties = new HashMap<>(); + + private final ApacheHttpTransport transport = new ApacheHttpTransport(); + + @Before + public void setUp() { + proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); + proxyProperties.stream().forEach(key -> System.clearProperty(key)); + } + + @After + public void tearDown() { + Consumer> restoreProperty = + entry -> { + if (entry.getValue() == null) { + System.clearProperty(entry.getKey()); + } else { + System.setProperty(entry.getKey(), entry.getValue()); + } + }; + savedProperties.entrySet().stream().forEach(restoreProperty); + } + + @Test + public void testAddProxyCredentials_undefined() { + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyPort", "1080"); + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyPort", "1443"); + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials httpCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); + Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); + Assert.assertEquals("pass", httpCredentials.getPassword()); + + Credentials httpsCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 1443)); + Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); + Assert.assertEquals("s-pass", httpsCredentials.getPassword()); + } + + @Test + public void testAddProxyCredentials_defaultPorts() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials httpCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); + Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); + Assert.assertEquals("pass", httpCredentials.getPassword()); + + Credentials httpsCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 443)); + Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); + Assert.assertEquals("s-pass", httpsCredentials.getPassword()); + } + + @Test + public void testAddProxyCredentials_hostUndefined() { + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials_userUndefined() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials_passwordUndefined() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyUser", "user"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyUser", "s-user"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } +} diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4bc09a1062..139b6e9663 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) - Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) +- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 44c674a94e..d1281fe00b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) - Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) +- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. +- Maven proxy settings are now supported. ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java index 94955345ff..d32632fc2f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java @@ -16,58 +16,59 @@ package com.google.cloud.tools.jib.maven; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import javax.annotation.Nullable; import org.apache.maven.settings.Proxy; import org.apache.maven.settings.Settings; -/** Propagates proxy configuration from Maven settings to system properties * */ -public class ProxyProvider { +/** Propagates proxy configuration from Maven settings to system properties. */ +class ProxyProvider { - private static final List PROXY_PROPERTIES = - Arrays.asList("proxyHost,proxyPort,proxyUser,proxyPassword".split(",")); + private static final ImmutableList PROXY_PROPERTIES = + ImmutableList.of("proxyHost", "proxyPort", "proxyUser", "proxyPassword"); /** * Initializes proxy settings based on Maven settings. * - * @param settings - Maven settings from mojo + * @param settings Maven settings */ - public static void init(Settings settings) { + static void init(Settings settings) { + configureProxy(settings, "https"); + configureProxy(settings, "http"); + } + + private static void configureProxy(Settings settings, String protocol) { settings .getProxies() .stream() - .filter(proxy -> proxy.isActive()) - .collect(Collectors.toList()) - .forEach(proxy -> setProxyProperties(proxy)); + .filter(Proxy::isActive) + .filter(proxy -> protocol.equals(proxy.getProtocol())) + .findFirst() + .ifPresent(ProxyProvider::setProxyProperties); } /** - * Set proxy system properties based on Maven proxy configuration. These system properties will be - * picked up by {@link java.net.ProxySelector} used in {@link - * com.google.cloud.tools.jib.http.Connection}, while connecting to container image registries. + * Set proxy system properties based on Maven proxy configuration. * - * @param proxy Maven proxy + * @param proxy Maven proxy settings */ - private static void setProxyProperties(Proxy proxy) { + @VisibleForTesting + static void setProxyProperties(Proxy proxy) { String protocol = proxy.getProtocol(); - if (protocol != null && !proxyPropertiesSet(protocol)) { - setProxyProperty(protocol + ".proxyHost", proxy.getHost()); - setProxyProperty(protocol + ".proxyPort", String.valueOf(proxy.getPort())); - setProxyProperty(protocol + ".proxyUser", proxy.getUsername()); - setProxyProperty(protocol + ".proxyPassword", proxy.getPassword()); - setProxyProperty("http.nonProxyHosts", proxy.getNonProxyHosts()); + if (areProxyPropertiesSet(protocol)) { + return; } + + setPropertySafe(protocol + ".proxyHost", proxy.getHost()); + setPropertySafe(protocol + ".proxyPort", String.valueOf(proxy.getPort())); + setPropertySafe(protocol + ".proxyUser", proxy.getUsername()); + setPropertySafe(protocol + ".proxyPassword", proxy.getPassword()); + setPropertySafe("http.nonProxyHosts", proxy.getNonProxyHosts()); } - /** - * Set proxy system property if it has a proper value - * - * @param property property name - * @param value property value - */ - private static void setProxyProperty(String property, String value) { - if (property != null && value != null) { + private static void setPropertySafe(String property, @Nullable String value) { + if (value != null) { System.setProperty(property, value); } } @@ -78,7 +79,10 @@ private static void setProxyProperty(String property, String value) { * * @param protocol protocol */ - private static boolean proxyPropertiesSet(String protocol) { - return PROXY_PROPERTIES.stream().anyMatch(p -> System.getProperty(protocol + "." + p) != null); + @VisibleForTesting + static boolean areProxyPropertiesSet(String protocol) { + return PROXY_PROPERTIES + .stream() + .anyMatch(property -> System.getProperty(protocol + "." + property) != null); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java new file mode 100644 index 0000000000..26c57f00f3 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java @@ -0,0 +1,269 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Settings; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +/** Test for {@link ProxyProvider}. */ +public class ProxyProviderTest { + + private static final ImmutableList proxyProperties = + ImmutableList.of( + "http.proxyHost", + "http.proxyPort", + "http.proxyUser", + "http.proxyPassword", + "https.proxyHost", + "https.proxyPort", + "https.proxyUser", + "https.proxyPassword", + "http.nonProxyHosts"); + + // HashMap to allow saving null values. + private final HashMap savedProperties = new HashMap<>(); + + @Before + public void setUp() { + proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); + proxyProperties.stream().forEach(key -> System.clearProperty(key)); + } + + @After + public void tearDown() { + Consumer> restoreProperty = + entry -> { + if (entry.getValue() == null) { + System.clearProperty(entry.getKey()); + } else { + System.setProperty(entry.getKey(), entry.getValue()); + } + }; + savedProperties.entrySet().stream().forEach(restoreProperty); + } + + @Test + public void testAreProxyPropertiesSet() { + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpHostSet() { + System.setProperty("http.proxyHost", "host"); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsHostSet() { + System.setProperty("https.proxyHost", "host"); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpPortSet() { + System.setProperty("http.proxyPort", "port"); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsPortSet() { + System.setProperty("https.proxyPort", "port"); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpUserSet() { + System.setProperty("http.proxyUser", "user"); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsUserSet() { + System.setProperty("https.proxyUser", "user"); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpPasswordSet() { + System.setProperty("http.proxyPassword", "password"); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsPasswordSet() { + System.setProperty("https.proxyPassword", "password"); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_ignoresHttpNonProxyHosts() { + System.setProperty("http.nonProxyHosts", "non proxy hosts"); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testSetProxyProperties() { + Proxy httpProxy = new Proxy(); + httpProxy.setProtocol("http"); + httpProxy.setHost("host"); + httpProxy.setPort(1080); + httpProxy.setUsername("user"); + httpProxy.setPassword("pass"); + httpProxy.setNonProxyHosts("non proxy hosts"); + + ProxyProvider.setProxyProperties(httpProxy); + Assert.assertEquals("host", System.getProperty("http.proxyHost")); + Assert.assertEquals("1080", System.getProperty("http.proxyPort")); + Assert.assertEquals("user", System.getProperty("http.proxyUser")); + Assert.assertEquals("pass", System.getProperty("http.proxyPassword")); + Assert.assertEquals("non proxy hosts", System.getProperty("http.nonProxyHosts")); + + Proxy httpsProxy = new Proxy(); + httpsProxy.setProtocol("https"); + httpsProxy.setHost("https host"); + httpsProxy.setPort(1443); + httpsProxy.setUsername("https user"); + httpsProxy.setPassword("https pass"); + ProxyProvider.setProxyProperties(httpsProxy); + Assert.assertEquals("https host", System.getProperty("https.proxyHost")); + Assert.assertEquals("1443", System.getProperty("https.proxyPort")); + Assert.assertEquals("https user", System.getProperty("https.proxyUser")); + Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); + } + + @Test + public void testSetProxyProperties_someValuesUndefined() { + Proxy httpProxy = new Proxy(); + httpProxy.setProtocol("http"); + httpProxy.setHost("http://host"); + + ProxyProvider.setProxyProperties(httpProxy); + Assert.assertEquals("http://host", System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("http.proxyUser")); + Assert.assertNull(System.getProperty("http.proxyPassword")); + Assert.assertNull(System.getProperty("http.nonProxyHosts")); + + Proxy httpsProxy = new Proxy(); + httpsProxy.setProtocol("https"); + httpsProxy.setUsername("https user"); + httpsProxy.setPassword("https pass"); + ProxyProvider.setProxyProperties(httpsProxy); + Assert.assertNull(System.getProperty("https.proxyHost")); + Assert.assertEquals("https user", System.getProperty("https.proxyUser")); + Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); + } + + @Test + public void testInit_noActiveProxy() { + Proxy httpProxy = new Proxy(); + httpProxy.setProtocol("http"); + httpProxy.setHost("proxy1 host"); + httpProxy.setActive(false); + + Proxy httpsProxy = new Proxy(); + httpsProxy.setProtocol("https"); + httpsProxy.setHost("proxy2 host"); + httpsProxy.setActive(false); + + Settings settings = Mockito.mock(Settings.class); + Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(httpProxy, httpsProxy)); + ProxyProvider.init(settings); + + Assert.assertNull(System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("https.proxyHost")); + } + + @Test + public void testInit_firstActiveHttpProxy() { + Proxy proxy1 = new Proxy(); + proxy1.setProtocol("http"); + proxy1.setHost("proxy1 host"); + proxy1.setActive(false); + + Proxy proxy2 = new Proxy(); + proxy2.setProtocol("http"); + proxy2.setHost("proxy2 host"); + proxy2.setActive(true); + + Proxy proxy3 = new Proxy(); + proxy3.setProtocol("http"); + proxy3.setHost("proxy3 host"); + proxy3.setActive(false); + + Proxy proxy4 = new Proxy(); + proxy4.setProtocol("http"); + proxy4.setHost("proxy4 host"); + proxy4.setActive(true); + + Settings settings = Mockito.mock(Settings.class); + Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); + ProxyProvider.init(settings); + + Assert.assertEquals("proxy2 host", System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("https.proxyHost")); + } + + @Test + public void testInit_firstActiveHttpsProxy() { + Proxy proxy1 = new Proxy(); + proxy1.setProtocol("https"); + proxy1.setHost("proxy1 host"); + proxy1.setActive(false); + + Proxy proxy2 = new Proxy(); + proxy2.setProtocol("https"); + proxy2.setHost("proxy2 host"); + proxy2.setActive(true); + + Proxy proxy3 = new Proxy(); + proxy3.setProtocol("https"); + proxy3.setHost("proxy3 host"); + proxy3.setActive(false); + + Proxy proxy4 = new Proxy(); + proxy4.setProtocol("https"); + proxy4.setHost("proxy4 host"); + proxy4.setActive(true); + + Settings settings = Mockito.mock(Settings.class); + Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); + ProxyProvider.init(settings); + + Assert.assertNull(System.getProperty("http.proxyHost")); + Assert.assertEquals("proxy2 host", System.getProperty("https.proxyHost")); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index ce6c604ca3..8ed15329d5 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -223,6 +223,7 @@ static PluginConfigurationProcessor processCommonConfiguration( InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException { JibSystemProperties.checkHttpTimeoutProperty(); + JibSystemProperties.checkProxyPortProperty(); ImageReference baseImageReference = ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); From b2a546644e753316c3ae1be34574b4b9e0d50383 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 20 Dec 2018 12:14:07 -0500 Subject: [PATCH 0412/2020] Update readmes for 1.0.0 (#1332) --- jib-gradle-plugin/README.md | 7 +++++-- jib-maven-plugin/README.md | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 3cf903fb8a..ad9659d17d 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -239,8 +239,11 @@ The following table contains additional system properties that are not available Property | Type | Default | Description --- | --- | --- | --- -`jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. -`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. +`jib.httpTimeout` | `int` | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. +`jib.useOnlyProjectCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Maven projects. +`jib.baseImageCache` | `File` | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.applicationCache` | `File` | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. If not set, a temporary directory will be used as the application layers cache. +`jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 64d23dccb4..26687e8241 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -282,7 +282,10 @@ The following table contains additional system properties that are not available Property | Type | Default | Description --- | --- | --- | --- `jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. -`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects. +`jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects (i.e. `jib.baseImageCache` defaults to `[project dir]/target/jib-cache` instead of `[user cache home]/google-cloud-tools-java/jib`). +`jib.baseImageCache` | string | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. If not set, a temporary directory will be used as the application layers cache. +`jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* From 6c0790490d4001719e9bca868507beefaac7527d Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 20 Dec 2018 13:26:05 -0500 Subject: [PATCH 0413/2020] Updates CHANGELOG for v1.0.0-rc1. (#1371) --- jib-gradle-plugin/CHANGELOG.md | 11 ++++++++++- jib-maven-plugin/CHANGELOG.md | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 139b6e9663..3c338e4bfe 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,9 +5,18 @@ All notable changes to this project will be documented in this file. ### Added +- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. + +### Changed + +### Fixed + +## 1.0.0-rc1 + +### Added + - `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) - Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) -- Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index d1281fe00b..00c6031f13 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,13 +5,22 @@ All notable changes to this project will be documented in this file. ### Added -- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) -- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. - Maven proxy settings are now supported. ### Changed +### Fixed + +## 1.0.0-rc1 + +### Added + +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) + +### Changed + - `gwt-app` packaging type now builds a WAR container. - When building to Docker and no `` is defined, artifact ID is used as an image reference instead of project name. - Removed `` parameter in favor of the `jib.useOnlyProjectCache` system property ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) From c8c639679a51389c46c7196e2c55e1c41bffe7de Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 20 Dec 2018 14:45:34 -0500 Subject: [PATCH 0414/2020] Add JavaContainerBuilder to jib core (#1347) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JavaContainerBuilder.java | 357 ++++++++++++++++++ .../jib/frontend/JavaLayerConfigurations.java | 1 - .../jib/api/JavaContainerBuilderTest.java | 222 +++++++++++ .../dependencies/more/dependency-1.0.0.jar | 2 + 5 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java create mode 100644 jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 59c426eb4d..5f5d6a03f6 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) ### Changed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java new file mode 100644 index 0000000000..106eeaaef0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -0,0 +1,357 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.frontend.MainClassFinder; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** Creates a {@link JibContainerBuilder} for containerizing Java applications. */ +public class JavaContainerBuilder { + + /** The default root directory of the application on the container. */ + private static final AbsoluteUnixPath APP_ROOT = AbsoluteUnixPath.get("/app"); + + /** Absolute path of directory containing application resources on container. */ + private static final AbsoluteUnixPath RESOURCES_PATH = + APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); + + /** Absolute path of directory containing classes on container. */ + private static final AbsoluteUnixPath CLASSES_PATH = + APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + + /** Absolute path of directory containing dependencies on container. */ + private static final AbsoluteUnixPath DEPENDENCIES_PATH = + APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); + + /** The entrypoint classpath element corresponding to dependencies. */ + private static final AbsoluteUnixPath DEPENDENCIES_CLASSPATH = DEPENDENCIES_PATH.resolve("*"); + + /** Absolute path of directory containing additional classpath files on container. */ + private static final AbsoluteUnixPath OTHERS_PATH = APP_ROOT.resolve("classpath"); + + /** + * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For + * more information on {@code gcr.io/distroless/java}, see the distroless repository. + * + * @return a new {@link JavaContainerBuilder} + * @throws InvalidImageReferenceException if creating the base image reference fails + * @see The distroless repository + */ + public static JavaContainerBuilder fromDistroless() throws InvalidImageReferenceException { + return from(RegistryImage.named("gcr.io/distroless/java")); + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image reference. + * + * @param baseImageReference the base image reference + * @return a new {@link JavaContainerBuilder} + * @throws InvalidImageReferenceException if {@code baseImageReference} is invalid + */ + public static JavaContainerBuilder from(String baseImageReference) + throws InvalidImageReferenceException { + return from(RegistryImage.named(baseImageReference)); + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image reference. + * + * @param baseImageReference the base image reference + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(ImageReference baseImageReference) { + return from(RegistryImage.named(baseImageReference)); + } + + /** + * Creates a new {@link JavaContainerBuilder} with the specified base image. + * + * @param registryImage the {@link RegistryImage} that defines base container registry and + * credentials + * @return a new {@link JavaContainerBuilder} + */ + public static JavaContainerBuilder from(RegistryImage registryImage) { + return new JavaContainerBuilder(Jib.from(registryImage)); + } + + private final JibContainerBuilder jibContainerBuilder; + private final JavaLayerConfigurations.Builder layerConfigurationsBuilder = + JavaLayerConfigurations.builder(); + private final List jvmFlags = new ArrayList<>(); + private final LinkedHashSet classpath = new LinkedHashSet<>(4); + + @Nullable private String mainClass; + + private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { + this.jibContainerBuilder = jibContainerBuilder; + } + + /** + * Adds dependency JARs to the image. Duplicate JAR filenames are renamed with the filesize in + * order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addDependencies(List dependencyFiles) throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + + // Detect duplicate filenames and rename with filesize to avoid collisions + List duplicates = + dependencyFiles + .stream() + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Entry::getKey) + .collect(Collectors.toList()); + for (Path file : dependencyFiles) { + layerConfigurationsBuilder.addFile( + file.getFileName().toString().contains("SNAPSHOT") + ? LayerType.SNAPSHOT_DEPENDENCIES + : LayerType.DEPENDENCIES, + file, + DEPENDENCIES_PATH.resolve( + duplicates.contains(file.getFileName().toString()) + ? file.getFileName().toString().replaceFirst("\\.jar$", "-" + Files.size(file)) + + ".jar" + : file.getFileName().toString())); + } + classpath.add(DEPENDENCIES_CLASSPATH.toString()); + return this; + } + + /** + * Adds dependency JARs to the image. Duplicate JAR filenames are renamed with the filesize in + * order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addDependencies(Path... dependencyFiles) throws IOException { + return addDependencies(Arrays.asList(dependencyFiles)); + } + + /** + * Adds the contents of a resources directory to the image. + * + * @param resourceFilesDirectory the directory containing the project's resources + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addResources(Path resourceFilesDirectory) throws IOException { + return addResources(resourceFilesDirectory, path -> true); + } + + /** + * Adds the contents of a resources directory to the image. + * + * @param resourceFilesDirectory the directory containing the project's resources + * @param pathFilter filter that determines which files (not directories) should be added + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addResources(Path resourceFilesDirectory, Predicate pathFilter) + throws IOException { + return addDirectory(resourceFilesDirectory, RESOURCES_PATH, LayerType.RESOURCES, pathFilter); + } + + /** + * Adds the contents of a classes directory to the image. + * + * @param classFilesDirectory the directory containing the class files + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addClasses(Path classFilesDirectory) throws IOException { + return addClasses(classFilesDirectory, path -> true); + } + + /** + * Adds the contents of a classes directory to the image. + * + * @param classFilesDirectory the directory containing the class files + * @param pathFilter filter that determines which files (not directories) should be added + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addClasses(Path classFilesDirectory, Predicate pathFilter) + throws IOException { + return addDirectory(classFilesDirectory, CLASSES_PATH, LayerType.CLASSES, pathFilter); + } + + /** + * Adds additional files to the classpath. If {@code otherFiles} contains a directory, the files + * within are added recursively, maintaining the directory structure. For files in {@code + * otherFiles}, files with duplicate filenames will be overwritten (e.g. if {@code otherFiles} + * contains '/loser/messages.txt' and '/winner/messages.txt', only the second 'messages.txt' is + * added. + * + * @param otherFiles the list of files to add + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addToClasspath(List otherFiles) throws IOException { + // Make sure all files exist before adding any + for (Path file : otherFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + + for (Path file : otherFiles) { + if (Files.isDirectory(file)) { + layerConfigurationsBuilder.addDirectoryContents( + LayerType.EXTRA_FILES, file, path -> true, OTHERS_PATH); + } else { + layerConfigurationsBuilder.addFile( + LayerType.EXTRA_FILES, file, OTHERS_PATH.resolve(file.getFileName())); + } + } + classpath.add(OTHERS_PATH.toString()); + return this; + } + + /** + * Adds additional files to the classpath. If {@code otherFiles} contains a directory, the files + * within are added recursively, maintaining the directory structure. For files in {@code + * otherFiles}, files with duplicate filenames will be overwritten (e.g. if {@code otherFiles} + * contains '/loser/messages.txt' and '/winner/messages.txt', only the second 'messages.txt' is + * added. + * + * @param otherFiles the list of files to add + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addToClasspath(Path... otherFiles) throws IOException { + return addToClasspath(Arrays.asList(otherFiles)); + } + + /** + * Adds a JVM flag to use when starting the application. + * + * @param jvmFlag the JVM flag to add + * @return this + */ + public JavaContainerBuilder addJvmFlag(String jvmFlag) { + jvmFlags.add(jvmFlag); + return this; + } + + /** + * Adds JVM flags to use when starting the application. + * + * @param jvmFlags the list of JVM flags to add + * @return this + */ + public JavaContainerBuilder addJvmFlags(List jvmFlags) { + this.jvmFlags.addAll(jvmFlags); + return this; + } + + /** + * Adds JVM flags to use when starting the application. + * + * @param jvmFlags the list of JVM flags to add + * @return this + */ + public JavaContainerBuilder addJvmFlags(String... jvmFlags) { + this.jvmFlags.addAll(Arrays.asList(jvmFlags)); + return this; + } + + /** + * Sets the main class used to start the application on the image. To find the main class from + * {@code .class} files, use {@link MainClassFinder}. + * + * @param mainClass the main class used to start the application + * @return this + * @see MainClassFinder + */ + public JavaContainerBuilder setMainClass(String mainClass) { + this.mainClass = mainClass; + return this; + } + + /** + * Returns a new {@link JibContainerBuilder} using the parameters specified on the {@link + * JavaContainerBuilder}. + * + * @return a new {@link JibContainerBuilder} using the parameters specified on the {@link + * JavaContainerBuilder} + */ + public JibContainerBuilder toContainerBuilder() { + if (mainClass == null) { + throw new IllegalStateException( + "mainClass is null on JavaContainerBuilder; specify the main class using " + + "JavaContainerBuilder#setMainClass(String), or consider using a " + + "jib.frontend.MainClassFinder to infer the main class"); + } + if (classpath.isEmpty()) { + throw new IllegalStateException( + "Failed to construct entrypoint because no files were added to the JavaContainerBuilder"); + } + + jibContainerBuilder.setEntrypoint( + JavaEntrypointConstructor.makeEntrypoint(new ArrayList<>(classpath), jvmFlags, mainClass)); + jibContainerBuilder.setLayers(layerConfigurationsBuilder.build().getLayerConfigurations()); + return jibContainerBuilder; + } + + private JavaContainerBuilder addDirectory( + Path directory, AbsoluteUnixPath destination, LayerType layerType, Predicate pathFilter) + throws IOException { + if (!Files.exists(directory)) { + throw new NoSuchFileException(directory.toString()); + } + if (!Files.isDirectory(directory)) { + throw new NotDirectoryException(directory.toString()); + } + layerConfigurationsBuilder.addDirectoryContents(layerType, directory, pathFilter, destination); + classpath.add(destination.toString()); + return this; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index de7a206ff3..8a9dd1f593 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -160,7 +160,6 @@ public Builder addDirectoryContents( * @throws IOException error while listing directories * @throws NotDirectoryException if {@code sourceRoot} is not a directory */ - // TODO: Use in plugins public Builder addDirectoryContents( LayerType layerType, Path sourceRoot, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java new file mode 100644 index 0000000000..46493641f3 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -0,0 +1,222 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link JavaContainerBuilder}. */ +public class JavaContainerBuilderTest { + + /** Gets a resource file as a {@link Path}. */ + private static Path getResource(String directory) throws URISyntaxException { + return Paths.get(Resources.getResource(directory).toURI()); + } + + /** Gets the extraction paths in the specified layer of a give {@link BuildConfiguration}. */ + private static List getExtractionPaths( + BuildConfiguration buildConfiguration, String layerName) { + return buildConfiguration + .getLayerConfigurations() + .stream() + .filter(layerConfiguration -> layerConfiguration.getName().equals(layerName)) + .findFirst() + .map( + layerConfiguration -> + layerConfiguration + .getLayerEntries() + .stream() + .map(LayerEntry::getExtractionPath) + .collect(Collectors.toList())) + .orElse(ImmutableList.of()); + } + + @Test + public void testToJibContainerBuilder_all() + throws InvalidImageReferenceException, URISyntaxException, IOException, + CacheDirectoryCreationException { + BuildConfiguration buildConfiguration = + JavaContainerBuilder.fromDistroless() + .addResources(getResource("application/resources")) + .addClasses(getResource("application/classes")) + .addDependencies( + getResource("application/dependencies/dependency-1.0.0.jar"), + getResource("application/dependencies/more/dependency-1.0.0.jar"), + getResource("application/dependencies/libraryA.jar"), + getResource("application/dependencies/libraryB.jar"), + getResource("application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addToClasspath(getResource("fileA"), getResource("fileB")) + .addJvmFlags("-xflag1", "-xflag2") + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("hello")), + MoreExecutors.newDirectExecutorService()); + + // Check entrypoint + ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + Assert.assertNotNull(containerConfiguration); + Assert.assertEquals( + ImmutableList.of( + "java", + "-xflag1", + "-xflag2", + "-cp", + "/app/resources:/app/classes:/app/libs/*:/app/classpath", + "HelloWorld"), + containerConfiguration.getEntrypoint()); + + // Check dependencies + List expectedDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-770.jar"), + AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-200.jar"), + AbsoluteUnixPath.get("/app/libs/libraryA.jar"), + AbsoluteUnixPath.get("/app/libs/libraryB.jar")); + Assert.assertEquals( + expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + + // Check snapshots + List expectedSnapshotDependencies = + ImmutableList.of(AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-SNAPSHOT.jar")); + Assert.assertEquals( + expectedSnapshotDependencies, + getExtractionPaths(buildConfiguration, "snapshot dependencies")); + + // Check resources + List expectedResources = + ImmutableList.of( + AbsoluteUnixPath.get("/app/resources/resourceA"), + AbsoluteUnixPath.get("/app/resources/resourceB"), + AbsoluteUnixPath.get("/app/resources/world")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/app/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/some.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + + // Check additional classpath files + List expectedOthers = + ImmutableList.of( + AbsoluteUnixPath.get("/app/classpath/fileA"), + AbsoluteUnixPath.get("/app/classpath/fileB")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); + } + + @Test + public void testToJibContainerBuilder_missingAndMultipleAdds() + throws InvalidImageReferenceException, URISyntaxException, IOException, + CacheDirectoryCreationException { + BuildConfiguration buildConfiguration = + JavaContainerBuilder.fromDistroless() + .addDependencies(getResource("application/dependencies/libraryA.jar")) + .addDependencies(getResource("application/dependencies/libraryB.jar")) + .addDependencies( + getResource("application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addClasses(getResource("application/classes/")) + .addClasses(getResource("class-finder-tests/extension")) + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("hello")), + MoreExecutors.newDirectExecutorService()); + + // Check entrypoint + ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + Assert.assertNotNull(containerConfiguration); + Assert.assertEquals( + ImmutableList.of("java", "-cp", "/app/libs/*:/app/classes", "HelloWorld"), + containerConfiguration.getEntrypoint()); + + // Check dependencies + List expectedDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/app/libs/libraryA.jar"), + AbsoluteUnixPath.get("/app/libs/libraryB.jar")); + Assert.assertEquals( + expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + + // Check snapshots + List expectedSnapshotDependencies = + ImmutableList.of(AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-SNAPSHOT.jar")); + Assert.assertEquals( + expectedSnapshotDependencies, + getExtractionPaths(buildConfiguration, "snapshot dependencies")); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/app/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/some.class"), + AbsoluteUnixPath.get("/app/classes/main/"), + AbsoluteUnixPath.get("/app/classes/main/MainClass.class"), + AbsoluteUnixPath.get("/app/classes/pack/"), + AbsoluteUnixPath.get("/app/classes/pack/Apple.class"), + AbsoluteUnixPath.get("/app/classes/pack/Orange.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + + // Check empty layers + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildConfiguration, "resources")); + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildConfiguration, "extra files")); + } + + @Test + public void testToJibContainerBuilder_mainClassNull() throws InvalidImageReferenceException { + try { + JavaContainerBuilder.fromDistroless().toContainerBuilder(); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "mainClass is null on JavaContainerBuilder; specify the main class using " + + "JavaContainerBuilder#setMainClass(String), or consider using a " + + "jib.frontend.MainClassFinder to infer the main class", + ex.getMessage()); + } + } + + @Test + public void testToJibContainerBuilder_classpathEmpty() throws InvalidImageReferenceException { + try { + JavaContainerBuilder.fromDistroless().setMainClass("Hello").toContainerBuilder(); + Assert.fail(); + + } catch (IllegalStateException ex) { + Assert.assertEquals( + "Failed to construct entrypoint because no files were added to the JavaContainerBuilder", + ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar new file mode 100644 index 0000000000..4d14054965 --- /dev/null +++ b/jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar @@ -0,0 +1,2 @@ +]e$Ềx®²–,² +.3øÐIÝ…°Ü×ó3ö8® V©KAŒ§ÓMÓÞ)=5ü~Ú'qÑŽ—$[¬Ó- :çÇ&%¿‘ ÙæâÈEÌošÙÍ÷7N±†˜s`Þi€Z0MT.œÖÉ9J[¾}?\E î¶æ½}Uõv‘ú›JÆdÉúo(©ÏiŸ"MŒìÔ›¼½_+/ñ›c£õÛI<ŠÝZje·£44%žÛd2?¶l>.ž¨-OŠ=HÃiÜ \ No newline at end of file From cfd6621b1bb8e693f9d167b4d33f599dfd82a7a6 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Dec 2018 14:47:50 -0500 Subject: [PATCH 0415/2020] Reduce visibility (#1372) --- .../common/PluginConfigurationProcessor.java | 134 +++++++++--------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 8ed15329d5..606b9afa51 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -55,72 +55,6 @@ */ public class PluginConfigurationProcessor { - /** - * Compute the container entrypoint, in this order: - * - *

    - *
  1. null (inheriting from the base image), if the user specified value is {@code INHERIT} - *
  2. the user specified one, if set - *
  3. for a WAR project, null (it must be inherited from base image) - *
  4. for a non-WAR project, by resolving the main class - *
- * - * @param rawConfiguration raw configuration data - * @param projectProperties used for providing additional information - * @return the entrypoint - * @throws MainClassInferenceException if no valid main class is configured or discovered - * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path - */ - @Nullable - public static List computeEntrypoint( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws MainClassInferenceException, InvalidAppRootException { - Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); - if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { - if (rawConfiguration.getMainClass().isPresent() - || !rawConfiguration.getJvmFlags().isEmpty()) { - new DefaultEventDispatcher(projectProperties.getEventHandlers()) - .dispatch( - LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); - } - - if (rawEntrypoint.get().size() == 1 && "INHERIT".equals(rawEntrypoint.get().get(0))) { - return null; - } - return rawEntrypoint.get(); - } - - if (projectProperties.isWarProject()) { - return null; - } - - AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); - String mainClass = - MainClassResolver.resolveMainClass( - rawConfiguration.getMainClass().orElse(null), projectProperties); - return JavaEntrypointConstructor.makeDefaultEntrypoint( - appRoot, rawConfiguration.getJvmFlags(), mainClass); - } - - /** - * Gets the suitable value for the base image. If the raw base image parameter is null, returns - * {@code "gcr.io/distroless/java/jetty"} for WAR projects or {@code "gcr.io/distroless/java"} for - * non-WAR. - * - * @param rawConfiguration raw configuration data - * @param projectProperties used for providing additional information - * @return the base image - */ - public static String getBaseImage( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) { - return rawConfiguration - .getFromImage() - .orElse( - projectProperties.isWarProject() - ? "gcr.io/distroless/java/jetty" - : "gcr.io/distroless/java"); - } - public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, @@ -280,6 +214,74 @@ static PluginConfigurationProcessor processCommonConfiguration( isTargetImageCredentialPresent); } + /** + * Compute the container entrypoint, in this order: + * + *
    + *
  1. null (inheriting from the base image), if the user specified value is {@code INHERIT} + *
  2. the user specified one, if set + *
  3. for a WAR project, null (it must be inherited from base image) + *
  4. for a non-WAR project, by resolving the main class + *
+ * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the entrypoint + * @throws MainClassInferenceException if no valid main class is configured or discovered + * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path + */ + @Nullable + @VisibleForTesting + static List computeEntrypoint( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws MainClassInferenceException, InvalidAppRootException { + Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); + if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { + if (rawConfiguration.getMainClass().isPresent() + || !rawConfiguration.getJvmFlags().isEmpty()) { + new DefaultEventDispatcher(projectProperties.getEventHandlers()) + .dispatch( + LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + } + + if (rawEntrypoint.get().size() == 1 && "INHERIT".equals(rawEntrypoint.get().get(0))) { + return null; + } + return rawEntrypoint.get(); + } + + if (projectProperties.isWarProject()) { + return null; + } + + AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); + String mainClass = + MainClassResolver.resolveMainClass( + rawConfiguration.getMainClass().orElse(null), projectProperties); + return JavaEntrypointConstructor.makeDefaultEntrypoint( + appRoot, rawConfiguration.getJvmFlags(), mainClass); + } + + /** + * Gets the suitable value for the base image. If the raw base image parameter is null, returns + * {@code "gcr.io/distroless/java/jetty"} for WAR projects or {@code "gcr.io/distroless/java"} for + * non-WAR. + * + * @param rawConfiguration raw configuration data + * @param projectProperties used for providing additional information + * @return the base image + */ + @VisibleForTesting + static String getBaseImage( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) { + return rawConfiguration + .getFromImage() + .orElse( + projectProperties.isWarProject() + ? "gcr.io/distroless/java/jetty" + : "gcr.io/distroless/java"); + } + /** * Parses the list of raw volumes directories to a set of {@link AbsoluteUnixPath} * From 31b49b963a5d01f8ea8509275f89d63860d623b6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Dec 2018 16:35:25 -0500 Subject: [PATCH 0416/2020] Apply README suggestions (#1379) --- README.md | 2 +- jib-core/README.md | 18 +++++++----------- jib-gradle-plugin/README.md | 15 ++++++--------- jib-maven-plugin/README.md | 17 +++++++---------- 4 files changed, 21 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 9cc83afda1..a84b129287 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ## What is Jib? -Jib builds Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. It is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin) and as a Java library. +Jib builds optimized Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications without a Docker daemon - and without deep mastery of Docker best-practices. It is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin) and as a Java library. [Maven](https://maven.apache.org/): See documentation for [jib-maven-plugin](jib-maven-plugin).\ [Gradle](https://gradle.org/): See documentation for [jib-gradle-plugin](jib-gradle-plugin).\ diff --git a/jib-core/README.md b/jib-core/README.md index f32ab5a114..8144cd0e0f 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -14,13 +14,6 @@ For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin) For information about the Jib project, see the [Jib project README](../README.md). -## Upcoming features - -- Extensions to make building Java and other language-specific containers easier -- Structured events to react to parts of Jib Core's execution - -See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. - ## Adding Jib Core to your build Add Jib Core as a dependency using Maven: @@ -105,14 +98,17 @@ Some other parts of Jib Core internals include: - an eventing system to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) - support for fully-concurrent multi-threaded executions -## How Jib Works - -See the [Jib project README](/../../#how-jib-works). - ## Frequently Asked Questions (FAQ) See the [Jib project FAQ](../docs/faq.md). +## Upcoming features + +- Extensions to make building Java and other language-specific containers easier +- Structured events to react to parts of Jib Core's execution + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. + ## Community See the [Jib project README](/../../#community). diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index ad9659d17d..39cb9ff310 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -6,9 +6,10 @@ Jib is a [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. -For information about the project, see the [Jib project README](../README.md). For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). +For information about the project, see the [Jib project README](../README.md). + ## Table of Contents * [Upcoming Features](#upcoming-features) @@ -32,10 +33,6 @@ For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). * [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) * [Community](#community) -## Upcoming Features - -See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. - ## Quickstart ### Setup @@ -399,14 +396,14 @@ jib { } ``` -## How Jib Works - -See the [Jib project README](/../../#how-jib-works). - ## Frequently Asked Questions (FAQ) See the [Jib project FAQ](../docs/faq.md). +## Upcoming Features + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. + ## Community See the [Jib project README](/../../#community). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 26687e8241..ced7dcdf0a 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -6,9 +6,10 @@ Jib is a [Maven](https://maven.apache.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. -For information about the project, see the [Jib project README](../README.md). For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). +For information about the project, see the [Jib project README](../README.md). + ## Table of Contents * [Upcoming Features](#upcoming-features) @@ -32,10 +33,6 @@ For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin) * [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) * [Community](#community) -## Upcoming Features - -See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. - ## Quickstart You can containerize your application easily with one command: @@ -484,7 +481,7 @@ To use a different Servlet engine base image, you can customize `tomcat:8.5-jre8-alpine
- @@ -493,14 +490,14 @@ To use a different Servlet engine base image, you can customize ` ``` -## How Jib Works - -See the [Jib project README](/../../#how-jib-works). - ## Frequently Asked Questions (FAQ) See the [Jib project FAQ](../docs/faq.md). +## Upcoming Features + +See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. + ## Community See the [Jib project README](/../../#community). From f1dc4f0d2dbf70dae140a3ea6fb18364081f713f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Dec 2018 17:05:36 -0500 Subject: [PATCH 0417/2020] Add JibContainerBuilder#addLayer overload with String instead of AbsoluteUnixPath (#1377) --- .../tools/jib/api/JibContainerBuilder.java | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 2d6eecc387..0ecb044f0c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -119,6 +119,33 @@ public JibContainerBuilder addLayer(List files, AbsoluteUnixPath pathInCon return addLayer(layerConfigurationBuilder.build()); } + /** + * Adds a new layer to the container with {@code files} as the source files and {@code + * pathInContainer} as the path to copy the source files to in the container file system. + * + * @param files the source files to copy to a new layer in the container + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @return this + * @throws IOException if an exception occurred when recursively listing any directories + * @throws IllegalArgumentException if {@code pathInContainer} is not an absolute Unix-style path + * @see #addLayer(List, AbsoluteUnixPath) + */ + public JibContainerBuilder addLayer(List files, String pathInContainer) throws IOException { + return addLayer(files, AbsoluteUnixPath.get(pathInContainer)); + } + + /** + * Adds a layer (defined by a {@link LayerConfiguration}). + * + * @param layerConfiguration the {@link LayerConfiguration} + * @return this + */ + public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { + layerConfigurations.add(layerConfiguration); + return this; + } + /** * Sets the layers (defined by a list of {@link LayerConfiguration}s). This replaces any * previously-added layers. @@ -141,17 +168,6 @@ public JibContainerBuilder setLayers(LayerConfiguration... layerConfigurations) return setLayers(Arrays.asList(layerConfigurations)); } - /** - * Adds a layer (defined by a {@link LayerConfiguration}). - * - * @param layerConfiguration the {@link LayerConfiguration} - * @return this - */ - public JibContainerBuilder addLayer(LayerConfiguration layerConfiguration) { - layerConfigurations.add(layerConfiguration); - return this; - } - /** * Sets the container entrypoint. This is the beginning of the command that is run when the * container starts. {@link #setProgramArguments} sets additional tokens. From 91c7fb9ac841e13905fe8eec094072417d7c6b44 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Dec 2018 17:56:33 -0500 Subject: [PATCH 0418/2020] Fix javadoc 'see' (#1382) --- .../cloud/tools/jib/api/JibContainerBuilder.java | 14 +++++++------- .../google/cloud/tools/jib/api/RegistryImage.java | 5 +++-- .../tools/jib/builder/TimerEventDispatcher.java | 2 +- .../tools/jib/event/events/ProgressEvent.java | 2 +- .../jib/image/json/BuildableManifestTemplate.java | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 0ecb044f0c..0cdfd93768 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -191,7 +191,7 @@ public JibContainerBuilder setEntrypoint(@Nullable List entrypoint) { * * @param entrypoint the entrypoint command * @return this - * @see #setEntrypoint(List) for more details + * @see #setEntrypoint(List) */ public JibContainerBuilder setEntrypoint(String... entrypoint) { return setEntrypoint(Arrays.asList(entrypoint)); @@ -223,7 +223,7 @@ public JibContainerBuilder setProgramArguments(@Nullable List programArg * * @param programArguments program arguments tokens * @return this - * @see #setProgramArguments(List) for more details + * @see #setProgramArguments(List) */ public JibContainerBuilder setProgramArguments(String... programArguments) { return setProgramArguments(Arrays.asList(programArguments)); @@ -253,7 +253,7 @@ public JibContainerBuilder setEnvironment(Map environmentMap) { * @param name the environment variable name * @param value the environment variable value * @return this - * @see #setEnvironment for more details + * @see #setEnvironment */ public JibContainerBuilder addEnvironmentVariable(String name, String value) { containerConfigurationBuilder.addEnvironment(name, value); @@ -279,7 +279,7 @@ public JibContainerBuilder setVolumes(Set volumes) { * * @param volumes the directory paths on the container filesystem to set as volumes * @return this - * @see #setVolumes(Set) for more details + * @see #setVolumes(Set) */ public JibContainerBuilder setVolumes(AbsoluteUnixPath... volumes) { return setVolumes(new HashSet<>(Arrays.asList(volumes))); @@ -290,7 +290,7 @@ public JibContainerBuilder setVolumes(AbsoluteUnixPath... volumes) { * * @param volume a directory path on the container filesystem to represent a volume * @return this - * @see #setVolumes(Set) for more details + * @see #setVolumes(Set) */ public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { containerConfigurationBuilder.addVolume(volume); @@ -321,7 +321,7 @@ public JibContainerBuilder setExposedPorts(Set ports) { * * @param ports the ports to expose * @return this - * @see #setExposedPorts(Set) for more details + * @see #setExposedPorts(Set) */ public JibContainerBuilder setExposedPorts(Port... ports) { return setExposedPorts(new HashSet<>(Arrays.asList(ports))); @@ -332,7 +332,7 @@ public JibContainerBuilder setExposedPorts(Port... ports) { * * @param port the port to expose * @return this - * @see #setExposedPorts(Set) for more details + * @see #setExposedPorts(Set) */ public JibContainerBuilder addExposedPort(Port port) { containerConfigurationBuilder.addExposedPort(port); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 52f5912c03..43a1063cd0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -89,7 +89,8 @@ public RegistryImage addCredential(String username, String password) { /** * Adds {@link CredentialRetriever} to fetch push/pull credentials for the image. Credential * retrievers are attempted in the order in which they are specified until credentials are - * successfully retrieved. + * successfully retrieved. See {@link CredentialRetrieverFactory} for useful pre-defined + * CredentialRetrievers. * *

Example usage: * @@ -110,7 +111,7 @@ public RegistryImage addCredential(String username, String password) { * * @param credentialRetriever the {@link CredentialRetriever} to add * @return this - * @see CredentialRetrieverFactory for useful pre-defined {@link CredentialRetriever}s + * @see CredentialRetrieverFactory */ public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetriever) { credentialRetrievers.add(credentialRetriever); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java index a3670557e4..e08eb0703a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java @@ -74,7 +74,7 @@ public TimerEventDispatcher subTimer(String description) { * Captures the time since last lap or creation and dispatches an {@link State#LAP} {@link * TimerEvent}. * - * @see #lap(String) for using a different description + * @see #lap(String) */ public void lap() { dispatchTimerEvent(State.LAP, timer.lap(), description); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java index feb116f624..53665ea250 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -23,7 +23,7 @@ * Event representing progress. The progress accounts for allocation units in an {@link Allocation}, * which makes up a Decentralized Allocation Tree. * - * @see Allocation for more details + * @see Allocation */ public class ProgressEvent implements JibEvent { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index b7aa818591..397512f3ff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -25,8 +25,8 @@ /** * Parent class for image manifest JSON templates that can be built. * - * @see V22ManifestTemplate for Docker V2.2 format - * @see OCIManifestTemplate for OCI format + * @see V22ManifestTemplate Docker V2.2 format + * @see OCIManifestTemplate OCI format */ public interface BuildableManifestTemplate extends ManifestTemplate { From 1397b6f79ef78e3a38654da0c81581a04e5da55e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 26 Dec 2018 20:16:33 -0500 Subject: [PATCH 0419/2020] Archive progress output proposal (#1376) --- proposals/{ => archives}/progress_output.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{ => archives}/progress_output.md (100%) diff --git a/proposals/progress_output.md b/proposals/archives/progress_output.md similarity index 100% rename from proposals/progress_output.md rename to proposals/archives/progress_output.md From 448e0082941ecf225338bc0abd4ee4865e0345f3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 28 Dec 2018 12:56:35 -0500 Subject: [PATCH 0420/2020] Rename Maven profile id (#1385) --- CONTRIBUTING.md | 2 +- build.sh | 2 +- jib-maven-plugin/pom.xml | 4 ++-- jib-maven-plugin/scripts/prepare_release.sh | 2 +- kokoro/continuous.sh | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1566bb99a2..5619b54db7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ To build, use the provided `build.sh` which builds and tests each of the compone * In `jib-core`, run `./gradlew clean goJF build integrationTest` * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` * In `jib-plugins-common`, run `./gradlew clean goJF build` - * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -Pintegration-tests` + * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -PintegrationTest` 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! diff --git a/build.sh b/build.sh index 05c8bd46cd..33ba046a21 100755 --- a/build.sh +++ b/build.sh @@ -90,7 +90,7 @@ for target in "$@"; do it) doBuild jib-core ./gradlew $gradleOptions integrationTest - doBuild jib-maven-plugin ./mvnw $mavenOptions -Pintegration-tests verify -U + doBuild jib-maven-plugin ./mvnw $mavenOptions -PintegrationTest verify -U doBuild jib-gradle-plugin ./gradlew $gradleOptions integrationTest ;; diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 46dd07a1f6..0bd6965249 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -141,7 +141,7 @@ - integration-tests + integrationTest @@ -160,7 +160,7 @@ - integration-tests + integration-test integration-test verify diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index 188c91b488..9ac075555b 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -52,7 +52,7 @@ if [[ $(git status -uno --porcelain) ]]; then fi # Runs integration tests. -./mvnw -X -Pintegration-tests verify +./mvnw -X -PintegrationTest verify # Checks out a new branch for this version release (eg. 1.5.7). BRANCH=maven_release_v${VERSION} diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index b6e580e0c1..dfd137a492 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -32,5 +32,5 @@ fi (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd github/jib/jib-plugins-common; ./gradlew clean build --info --stacktrace) -(cd github/jib/jib-maven-plugin; ./mvnw clean install -P integration-tests -B -U -X) +(cd github/jib/jib-maven-plugin; ./mvnw clean install -PintegrationTest -B -U -X) (cd github/jib/jib-gradle-plugin; ./gradlew clean build integrationTest --info --stacktrace) From 78dac4cea0211d98b66276e09008e0ba40b4e77e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 2 Jan 2019 11:07:22 -0500 Subject: [PATCH 0421/2020] Fail build if using default base image and Java version >= 9 (#1353) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../tools/jib/gradle/BuildDockerTask.java | 1 + .../tools/jib/gradle/BuildImageTask.java | 1 + .../cloud/tools/jib/gradle/BuildTarTask.java | 1 + .../jib/gradle/GradleProjectProperties.java | 23 ++++ .../gradle/GradleProjectPropertiesTest.java | 26 +++++ jib-maven-plugin/CHANGELOG.md | 2 + .../tools/jib/maven/BuildDockerMojo.java | 5 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../cloud/tools/jib/maven/BuildTarMojo.java | 4 +- .../jib/maven/MavenProjectProperties.java | 76 +++++++++++++ .../jib/maven/MavenProjectPropertiesTest.java | 105 ++++++++++++++++++ .../common/PluginConfigurationProcessor.java | 17 +++ 13 files changed, 261 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3c338e4bfe..5c04d8d8fb 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) + ### Fixed ## 1.0.0-rc1 diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 190212611f..b669402575 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -113,6 +113,7 @@ public void buildDocker() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index ddec930f07..be19bbbfae 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -89,6 +89,7 @@ public void buildImage() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 3923fb199f..304bcf4459 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -112,6 +112,7 @@ public void buildTar() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); + projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 4e134b3876..c34d2639e3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -44,10 +45,12 @@ import javax.annotation.Nullable; import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; @@ -254,6 +257,26 @@ public String getVersion() { return project.getVersion().toString(); } + void validateAgainstDefaultBaseImageVersion(@Nullable String baseImage) { + if (!PluginConfigurationProcessor.usingDefaultBaseImage(baseImage)) { + return; + } + JavaVersion version = JavaVersion.current(); + JavaPluginConvention javaPluginConvention = + project.getConvention().findPlugin(JavaPluginConvention.class); + if (javaPluginConvention != null) { + version = JavaVersion.toVersion(javaPluginConvention.getTargetCompatibility()); + } + if (version.isJava9Compatible()) { + throw new GradleException( + "Jib's default base image uses Java 8, but project is using Java " + + version.getMajorVersion() + + "; perhaps you should configure a Java " + + version.getMajorVersion() + + "-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 1.8 in your build configuration"); + } + } + /** * Validates and converts a {@code String->String} file-path-to-file-permissions map to an * equivalent {@code AbsoluteUnixPath->FilePermission} map. diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 57b7e16cff..f0b64fd85d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -23,6 +23,8 @@ import com.google.common.collect.ImmutableSet; import java.util.Collections; import org.gradle.StartParameter; +import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.invocation.Gradle; @@ -31,6 +33,7 @@ import org.gradle.api.logging.Logger; import org.gradle.api.logging.configuration.ConsoleOutput; import org.gradle.api.plugins.Convention; +import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.bundling.War; @@ -58,6 +61,7 @@ public class GradleProjectPropertiesTest { @Mock private Logger mockLogger; @Mock private Gradle mockGradle; @Mock private StartParameter mockStartParameter; + @Mock private JavaPluginConvention mockJavaPluginConvention; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -145,4 +149,26 @@ public void testConvertPermissionsMap() { // pass } } + + @Test + public void testValidateBaseImageVersion() { + gradleProjectProperties.validateAgainstDefaultBaseImageVersion("nonDefault"); + + Mockito.when(mockConvention.findPlugin(JavaPluginConvention.class)) + .thenReturn(mockJavaPluginConvention); + Mockito.when(mockJavaPluginConvention.getTargetCompatibility()) + .thenReturn(JavaVersion.VERSION_1_8); + gradleProjectProperties.validateAgainstDefaultBaseImageVersion(null); + + Mockito.when(mockJavaPluginConvention.getTargetCompatibility()) + .thenReturn(JavaVersion.VERSION_11); + try { + gradleProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertEquals( + "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 1.8 in your build configuration", + ex.getMessage()); + } + } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 00c6031f13..70231fadab 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) + ### Fixed ## 1.0.0-rc1 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 8d97831527..f8678109bc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -37,6 +37,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -51,7 +52,7 @@ public class BuildDockerMojo extends JibPluginConfiguration { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; @Override - public void execute() throws MojoExecutionException { + public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; @@ -75,7 +76,7 @@ public void execute() throws MojoExecutionException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); - + projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index eeb569fd9c..8ad945a769 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -95,6 +95,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index beb93ac698..3561354dd6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -35,6 +35,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; @@ -51,7 +52,7 @@ public class BuildTarMojo extends JibPluginConfiguration { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Building image tarball failed"; @Override - public void execute() throws MojoExecutionException { + public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; @@ -70,6 +71,7 @@ public void execute() throws MojoExecutionException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); + projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 1cb4e4ba41..4285f06fe7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -38,6 +39,7 @@ import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.Os; @@ -140,6 +142,36 @@ private static boolean isProgressFooterEnabled() { return System.console() != null && !"dumb".equals(System.getenv("TERM")); } + /** + * Gets the major version number from a Java version string. + * + *

Examples: {@code "1.7" -> 7, "1.8.0_161" -> 8, "10" -> 10, "11.0.1" -> 11} + * + * @param versionString the string to convert + * @return the major version number as an integer, or 0 if the string is invalid + */ + @VisibleForTesting + static int getVersionFromString(String versionString) { + // Parse version starting with "1." + if (versionString.startsWith("1.")) { + if (versionString.length() >= 3 && Character.isDigit(versionString.charAt(2))) { + return versionString.charAt(2) - '0'; + } + return 0; + } + + // Parse string starting with major version number + int dotIndex = versionString.indexOf("."); + try { + if (dotIndex == -1) { + return Integer.parseInt(versionString); + } + return Integer.parseInt(versionString.substring(0, versionString.indexOf("."))); + } catch (NumberFormatException ex) { + return 0; + } + } + private final MavenProject project; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; @@ -229,4 +261,48 @@ public String getName() { public String getVersion() { return project.getVersion(); } + + void validateAgainstDefaultBaseImageVersion(@Nullable String baseImage) + throws MojoFailureException { + if (!PluginConfigurationProcessor.usingDefaultBaseImage(baseImage)) { + return; + } + + // maven-compiler-plugin default is 1.6 + int version = 6; + + // Check properties for version + if (project.getProperties().getProperty("maven.compiler.target") != null) { + version = getVersionFromString(project.getProperties().getProperty("maven.compiler.target")); + } else if (project.getProperties().getProperty("maven.compiler.release") != null) { + version = getVersionFromString(project.getProperties().getProperty("maven.compiler.release")); + } else { + // Check maven-compiler-plugin for version + Plugin mavenCompilerPlugin = + project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin"); + if (mavenCompilerPlugin != null) { + Xpp3Dom pluginConfiguration = (Xpp3Dom) mavenCompilerPlugin.getConfiguration(); + if (pluginConfiguration != null) { + Xpp3Dom target = pluginConfiguration.getChild("target"); + if (target != null) { + version = getVersionFromString(target.getValue()); + } else { + Xpp3Dom release = pluginConfiguration.getChild("release"); + if (release != null) { + version = getVersionFromString(release.getValue()); + } + } + } + } + } + + if (version > 8) { + throw new MojoFailureException( + "Jib's default base image uses Java 8, but project is using Java " + + version + + "; perhaps you should configure a Java " + + version + + "-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration"); + } + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 4a2ccf54e6..03225807cb 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -17,7 +17,9 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import java.util.Properties; import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; @@ -34,8 +36,10 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; + @Mock private Properties mockMavenProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; + @Mock private Plugin mockCompilerPlugin; @Mock private Log mockLog; private Xpp3Dom jarPluginConfiguration; @@ -43,6 +47,10 @@ public class MavenProjectPropertiesTest { private Xpp3Dom manifest; private Xpp3Dom jarPluginMainClass; + @Mock private Xpp3Dom compilerPluginConfiguration; + @Mock private Xpp3Dom compilerTarget; + @Mock private Xpp3Dom compilerRelease; + private MavenProjectProperties mavenProjectProperties; @Before @@ -53,6 +61,8 @@ public void setup() { archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); jarPluginMainClass = new Xpp3Dom("mainClass"); + + Mockito.when(mockMavenProject.getProperties()).thenReturn(mockMavenProperties); } @Test @@ -115,4 +125,99 @@ public void testGetMainClassFromJar_missingPlugin() { public void testIsWarProject() { Assert.assertFalse(mavenProjectProperties.isWarProject()); } + + @Test + public void testGetVersionFromString() { + Assert.assertEquals(8, MavenProjectProperties.getVersionFromString("1.8")); + Assert.assertEquals(8, MavenProjectProperties.getVersionFromString("1.8.0_123")); + Assert.assertEquals(11, MavenProjectProperties.getVersionFromString("11")); + Assert.assertEquals(11, MavenProjectProperties.getVersionFromString("11.0.1")); + + Assert.assertEquals(0, MavenProjectProperties.getVersionFromString("asdfasdf")); + Assert.assertEquals(0, MavenProjectProperties.getVersionFromString("")); + Assert.assertEquals(0, MavenProjectProperties.getVersionFromString("11abc")); + Assert.assertEquals(0, MavenProjectProperties.getVersionFromString("1.abc")); + } + + @Test + public void testValidateBaseImageVersion_nonDefaultBaseImage() throws MojoFailureException { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion("non-default"); + } + + @Test + public void testValidateBaseImageVersion_allNull() throws MojoFailureException { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + } + + @Test + public void testValidateBaseImageVersion_targetProperty() throws MojoFailureException { + Mockito.when(mockMavenProperties.getProperty("maven.compiler.target")).thenReturn("1.8"); + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + + Mockito.when(mockMavenProperties.getProperty("maven.compiler.target")).thenReturn("11"); + try { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.fail(); + } catch (MojoFailureException ex) { + Assert.assertEquals( + "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", + ex.getMessage()); + } + } + + @Test + public void testValidateBaseImageVersion_releaseProperty() throws MojoFailureException { + Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("8"); + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + + Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("11.0"); + try { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.fail(); + } catch (MojoFailureException ex) { + Assert.assertEquals( + "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", + ex.getMessage()); + } + } + + @Test + public void testValidateBaseImageVersion_compilerPluginTarget() throws MojoFailureException { + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) + .thenReturn(mockCompilerPlugin); + Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); + Mockito.when(compilerPluginConfiguration.getChild("target")).thenReturn(compilerTarget); + Mockito.when(compilerTarget.getValue()).thenReturn("1.8"); + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + + Mockito.when(compilerTarget.getValue()).thenReturn("11"); + try { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.fail(); + } catch (MojoFailureException ex) { + Assert.assertEquals( + "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", + ex.getMessage()); + } + } + + @Test + public void testValidateBaseImageVersion_compilerPluginRelease() throws MojoFailureException { + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) + .thenReturn(mockCompilerPlugin); + Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); + Mockito.when(compilerPluginConfiguration.getChild("release")).thenReturn(compilerRelease); + Mockito.when(compilerRelease.getValue()).thenReturn("1.8"); + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + + Mockito.when(compilerRelease.getValue()).thenReturn("11"); + try { + mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.fail(); + } catch (MojoFailureException ex) { + Assert.assertEquals( + "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", + ex.getMessage()); + } + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 606b9afa51..218174dd0e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -55,6 +55,23 @@ */ public class PluginConfigurationProcessor { + private static final String DEFAULT_BASE_IMAGE = "gcr.io/distroless/java"; + private static final String DEFAULT_BASE_IMAGE_WAR = "gcr.io/distroless/java/jetty"; + + /** + * Checks whether or not the default distroless base image is being used. + * + * @param baseImageConfiguration the configured base image + * @return {@code true} if the base image is null or equal to one of the distroless java images, + * else {@code false} + */ + public static boolean usingDefaultBaseImage(@Nullable String baseImageConfiguration) { + // TODO: check for other Java 8 base images? + return baseImageConfiguration == null + || baseImageConfiguration.equals(DEFAULT_BASE_IMAGE) + || baseImageConfiguration.equals(DEFAULT_BASE_IMAGE_WAR); + } + public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, From 5d00a085cefa6b7b4020dd4d9ee0ddfbb4198f6f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 2 Jan 2019 13:45:32 -0500 Subject: [PATCH 0422/2020] Add Automatic-Module-Name: in MANIFEST.MF (#1359) --- jib-core/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index b4c0587b41..fe135e9ad5 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -60,6 +60,7 @@ dependencies { jar { manifest { attributes 'Implementation-Version': version + attributes 'Automatic-Module-Name': 'com.google.cloud.tools.jib' // OSGi metadata attributes 'Bundle-SymbolicName': 'com.google.cloud.tools.jib' From b60e50821cb1c610bb7a07366973a904a0ad62c9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 3 Jan 2019 17:34:53 -0500 Subject: [PATCH 0423/2020] Check for system properties in pom as well as commandline (#1389) --- jib-maven-plugin/CHANGELOG.md | 1 + .../jib/maven/JibPluginConfiguration.java | 138 +++++++++------- .../maven/BuildImageMojoIntegrationTest.java | 29 +++- .../jib/maven/JibPluginConfigurationTest.java | 150 ++++++++++++------ .../simple/pom-complex-properties.xml | 72 +++++++++ 5 files changed, 280 insertions(+), 110 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 70231fadab..9af94bc815 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. - Maven proxy settings are now supported. +- Now checks for system properties in pom as well as commandline. ([#1201](https://github.com/GoogleContainerTools/jib/issues/1201)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index ad39352050..d8e7f87ac0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -249,8 +248,9 @@ MavenProject getProject() { */ @Nullable String getBaseImage() { - if (System.getProperty(PropertyNames.FROM_IMAGE) != null) { - return System.getProperty(PropertyNames.FROM_IMAGE); + String property = getProperty(PropertyNames.FROM_IMAGE); + if (property != null) { + return property; } return Preconditions.checkNotNull(from).image; } @@ -262,14 +262,15 @@ String getBaseImage() { */ @Nullable String getBaseImageCredentialHelperName() { - if (System.getProperty(PropertyNames.FROM_CRED_HELPER) != null) { - return System.getProperty(PropertyNames.FROM_CRED_HELPER); + String property = getProperty(PropertyNames.FROM_CRED_HELPER); + if (property != null) { + return property; } return Preconditions.checkNotNull(from).credHelper; } AuthConfiguration getBaseImageAuth() { - // System properties are handled in ConfigurationPropertyValidator + // TODO: handle properties here instead of in ConfigurationPropertyValidator return from.auth; } @@ -280,11 +281,13 @@ AuthConfiguration getBaseImageAuth() { */ @Nullable String getTargetImage() { - if (System.getProperty(PropertyNames.TO_IMAGE_ALTERNATE) != null) { - return System.getProperty(PropertyNames.TO_IMAGE_ALTERNATE); + String propertyAlternate = getProperty(PropertyNames.TO_IMAGE_ALTERNATE); + if (propertyAlternate != null) { + return propertyAlternate; } - if (System.getProperty(PropertyNames.TO_IMAGE) != null) { - return System.getProperty(PropertyNames.TO_IMAGE); + String property = getProperty(PropertyNames.TO_IMAGE); + if (property != null) { + return property; } return to.image; } @@ -295,10 +298,9 @@ String getTargetImage() { * @return the configured extra tags. */ Set getTargetImageAdditionalTags() { - if (System.getProperty(PropertyNames.TO_TAGS) != null) { - return ImmutableSet.copyOf( - ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.TO_TAGS))); + String property = getProperty(PropertyNames.TO_TAGS); + if (property != null) { + return new HashSet<>(ConfigurationPropertyValidator.parseListProperty(property)); } return new HashSet<>(to.tags); } @@ -310,14 +312,15 @@ Set getTargetImageAdditionalTags() { */ @Nullable String getTargetImageCredentialHelperName() { - if (System.getProperty(PropertyNames.TO_CRED_HELPER) != null) { - return System.getProperty(PropertyNames.TO_CRED_HELPER); + String property = getProperty(PropertyNames.TO_CRED_HELPER); + if (property != null) { + return property; } return Preconditions.checkNotNull(to).credHelper; } AuthConfiguration getTargetImageAuth() { - // System properties are handled in ConfigurationPropertyValidator + // TODO: handle properties here instead of in ConfigurationPropertyValidator return to.auth; } @@ -327,8 +330,9 @@ AuthConfiguration getTargetImageAuth() { * @return {@code true} if the build should use the current timestamp, {@code false} if not */ boolean getUseCurrentTimestamp() { - if (System.getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP) != null) { - return Boolean.getBoolean(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); + String property = getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); + if (property != null) { + return Boolean.parseBoolean(property); } return container.useCurrentTimestamp; } @@ -340,9 +344,9 @@ boolean getUseCurrentTimestamp() { */ @Nullable List getEntrypoint() { - if (System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT) != null) { - return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.CONTAINER_ENTRYPOINT)); + String property = getProperty(PropertyNames.CONTAINER_ENTRYPOINT); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); } return container.entrypoint; } @@ -353,9 +357,9 @@ List getEntrypoint() { * @return the configured jvm flags */ List getJvmFlags() { - if (System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS) != null) { - return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.CONTAINER_JVM_FLAGS)); + String property = getProperty(PropertyNames.CONTAINER_JVM_FLAGS); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); } return container.jvmFlags; } @@ -366,9 +370,9 @@ List getJvmFlags() { * @return the configured environment variables */ Map getEnvironment() { - if (System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT) != null) { - return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.CONTAINER_ENVIRONMENT)); + String property = getProperty(PropertyNames.CONTAINER_ENVIRONMENT); + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property); } return container.environment; } @@ -380,8 +384,9 @@ Map getEnvironment() { */ @Nullable String getMainClass() { - if (System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS) != null) { - return System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS); + String property = getProperty(PropertyNames.CONTAINER_MAIN_CLASS); + if (property != null) { + return property; } return container.mainClass; } @@ -393,8 +398,9 @@ String getMainClass() { */ @Nullable String getUser() { - if (System.getProperty(PropertyNames.CONTAINER_USER) != null) { - return System.getProperty(PropertyNames.CONTAINER_USER); + String property = getProperty(PropertyNames.CONTAINER_USER); + if (property != null) { + return property; } return container.user; } @@ -406,8 +412,9 @@ String getUser() { */ @Nullable String getWorkingDirectory() { - if (System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY) != null) { - return System.getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY); + String property = getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY); + if (property != null) { + return property; } return container.workingDirectory; } @@ -419,9 +426,9 @@ String getWorkingDirectory() { */ @Nullable List getArgs() { - if (System.getProperty(PropertyNames.CONTAINER_ARGS) != null) { - return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.CONTAINER_ARGS)); + String property = getProperty(PropertyNames.CONTAINER_ARGS); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); } return container.args; } @@ -432,9 +439,9 @@ List getArgs() { * @return the configured exposed ports */ List getExposedPorts() { - if (System.getProperty(PropertyNames.CONTAINER_PORTS) != null) { - return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.CONTAINER_PORTS)); + String property = getProperty(PropertyNames.CONTAINER_PORTS); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); } return container.ports; } @@ -445,9 +452,9 @@ List getExposedPorts() { * @return the configured volumes */ List getVolumes() { - if (System.getProperty(PropertyNames.CONTAINER_VOLUMES) != null) { - return ConfigurationPropertyValidator.parseListProperty( - System.getProperty(PropertyNames.CONTAINER_VOLUMES)); + String property = getProperty(PropertyNames.CONTAINER_VOLUMES); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); } return container.volumes; } @@ -458,9 +465,9 @@ List getVolumes() { * @return the configured labels */ Map getLabels() { - if (System.getProperty(PropertyNames.CONTAINER_LABELS) != null) { - return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.CONTAINER_LABELS)); + String property = getProperty(PropertyNames.CONTAINER_LABELS); + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property); } return container.labels; } @@ -471,8 +478,9 @@ Map getLabels() { * @return the configured app root directory */ String getAppRoot() { - if (System.getProperty(PropertyNames.CONTAINER_APP_ROOT) != null) { - return System.getProperty(PropertyNames.CONTAINER_APP_ROOT); + String property = getProperty(PropertyNames.CONTAINER_APP_ROOT); + if (property != null) { + return property; } return container.appRoot; } @@ -483,8 +491,9 @@ String getAppRoot() { * @return the configured container image format */ String getFormat() { - if (System.getProperty(PropertyNames.CONTAINER_FORMAT) != null) { - return System.getProperty(PropertyNames.CONTAINER_FORMAT); + String property = getProperty(PropertyNames.CONTAINER_FORMAT); + if (property != null) { + return property; } return Preconditions.checkNotNull(container.format); } @@ -496,8 +505,9 @@ String getFormat() { */ Optional getExtraDirectoryPath() { // TODO: Should inform user about nonexistent directory if using custom directory. - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { - return Optional.of(Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH))); + String property = getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + if (property != null) { + return Optional.of(Paths.get(property)); } return extraDirectory.path == null ? Optional.empty() @@ -510,9 +520,9 @@ Optional getExtraDirectoryPath() { * @return the configured extra layer file permissions */ List getExtraDirectoryPermissions() { - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { - return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS)) + String property = getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property) .entrySet() .stream() .map(entry -> new PermissionConfiguration(entry.getKey(), entry.getValue())) @@ -537,4 +547,22 @@ SettingsDecrypter getSettingsDecrypter() { void setProject(MavenProject project) { this.project = project; } + + /** + * Gets a system property with the given name. First checks for a -D commandline argument, then + * checks for a property defined in the POM, then returns null if neither are defined. + * + * @param propertyName the name of the system property + * @return the value of the system property, or null if not defined + */ + @Nullable + private String getProperty(String propertyName) { + if (session != null && session.getSystemProperties().containsKey(propertyName)) { + return session.getSystemProperties().getProperty(propertyName); + } + if (project != null && project.getProperties().containsKey(propertyName)) { + return project.getProperties().getProperty(propertyName); + } + return null; + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 5d1589cd6b..64ee68dbfb 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -172,7 +172,11 @@ private static String buildAndRunAdditionalTag( } private static String buildAndRunComplex( - String imageReference, String username, String password, LocalRegistry targetRegistry) + String imageReference, + String username, + String password, + LocalRegistry targetRegistry, + String pomFile) throws VerificationException, IOException, InterruptedException, DigestException { Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); @@ -183,7 +187,7 @@ private static String buildAndRunComplex( verifier.setSystemProperty("sendCredentialsOverHttp", "true"); verifier.setAutoclean(false); verifier.addCliOption("-X"); - verifier.addCliOption("--file=pom-complex.xml"); + verifier.addCliOption("--file=" + pomFile); verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); verifier.verifyErrorFreeLog(); @@ -378,7 +382,8 @@ public void testExecute_complex() String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); + buildAndRunComplex( + targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml")); assertWorkingDirectory("", targetImage); } @@ -388,7 +393,23 @@ public void testExecute_complex_sameFromAndToRegistry() String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); + buildAndRunComplex( + targetImage, "testuser", "testpassword", localRegistry1, "pom-complex.xml")); + assertWorkingDirectory("", targetImage); + } + + @Test + public void testExecute_complexProperties() + throws InterruptedException, DigestException, VerificationException, IOException { + String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); + Assert.assertEquals( + "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + buildAndRunComplex( + targetImage, + "testuser2", + "testpassword2", + localRegistry2, + "pom-complex-properties.xml")); assertWorkingDirectory("", targetImage); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 42e48b6f10..0d2ad69c6e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -22,52 +22,36 @@ import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; import java.util.List; -import org.junit.After; +import java.util.Properties; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.project.MavenProject; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link JibPluginConfiguration}. */ +@RunWith(MockitoJUnitRunner.class) public class JibPluginConfigurationTest { + private final MavenProject project = new MavenProject(); + private final Properties sessionProperties = new Properties(); + @Mock private MavenSession session; private JibPluginConfiguration testPluginConfiguration; - private static void clearProperties() { - System.clearProperty("jib.from.image"); - System.clearProperty("jib.from.credHelper"); - System.clearProperty("image"); - System.clearProperty("jib.to.image"); - System.clearProperty("jib.to.tags"); - System.clearProperty("jib.to.credHelper"); - System.clearProperty("jib.container.appRoot"); - System.clearProperty("jib.container.args"); - System.clearProperty("jib.container.entrypoint"); - System.clearProperty("jib.container.environment"); - System.clearProperty("jib.container.format"); - System.clearProperty("jib.container.jvmFlags"); - System.clearProperty("jib.container.labels"); - System.clearProperty("jib.container.mainClass"); - System.clearProperty("jib.container.ports"); - System.clearProperty("jib.container.useCurrentTimestamp"); - System.clearProperty("jib.container.user"); - System.clearProperty("jib.container.workingDirectory"); - System.clearProperty("jib.extraDirectory.path"); - System.clearProperty("jib.extraDirectory.permissions"); - } - @Before public void setup() { - clearProperties(); + Mockito.when(session.getSystemProperties()).thenReturn(sessionProperties); testPluginConfiguration = new JibPluginConfiguration() { @Override public void execute() {} }; - } - - @After - public void teardown() { - clearProperties(); + testPluginConfiguration.setProject(project); + testPluginConfiguration.session = session; } @Test @@ -78,58 +62,122 @@ public void testDefaults() { @Test public void testSystemProperties() { - System.setProperty("jib.from.image", "fromImage"); + sessionProperties.put("jib.from.image", "fromImage"); + Assert.assertEquals("fromImage", testPluginConfiguration.getBaseImage()); + sessionProperties.put("jib.from.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testPluginConfiguration.getBaseImageCredentialHelperName()); + + sessionProperties.put("image", "toImage"); + Assert.assertEquals("toImage", testPluginConfiguration.getTargetImage()); + sessionProperties.remove("image"); + sessionProperties.put("jib.to.image", "toImage2"); + Assert.assertEquals("toImage2", testPluginConfiguration.getTargetImage()); + sessionProperties.put("jib.to.tags", "tag1,tag2,tag3"); + Assert.assertEquals( + ImmutableSet.of("tag1", "tag2", "tag3"), + testPluginConfiguration.getTargetImageAdditionalTags()); + sessionProperties.put("jib.to.credHelper", "credHelper"); + Assert.assertEquals("credHelper", testPluginConfiguration.getTargetImageCredentialHelperName()); + + sessionProperties.put("jib.container.appRoot", "appRoot"); + Assert.assertEquals("appRoot", testPluginConfiguration.getAppRoot()); + sessionProperties.put("jib.container.args", "arg1,arg2,arg3"); + Assert.assertEquals( + ImmutableList.of("arg1", "arg2", "arg3"), testPluginConfiguration.getArgs()); + sessionProperties.put("jib.container.entrypoint", "entry1,entry2,entry3"); + Assert.assertEquals( + ImmutableList.of("entry1", "entry2", "entry3"), testPluginConfiguration.getEntrypoint()); + sessionProperties.put("jib.container.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), testPluginConfiguration.getEnvironment()); + sessionProperties.put("jib.container.format", "format"); + Assert.assertEquals("format", testPluginConfiguration.getFormat()); + sessionProperties.put("jib.container.jvmFlags", "flag1,flag2,flag3"); + Assert.assertEquals( + ImmutableList.of("flag1", "flag2", "flag3"), testPluginConfiguration.getJvmFlags()); + sessionProperties.put("jib.container.labels", "label1=val1,label2=val2"); + Assert.assertEquals( + ImmutableMap.of("label1", "val1", "label2", "val2"), testPluginConfiguration.getLabels()); + sessionProperties.put("jib.container.mainClass", "main"); + Assert.assertEquals("main", testPluginConfiguration.getMainClass()); + sessionProperties.put("jib.container.ports", "port1,port2,port3"); + Assert.assertEquals( + ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); + sessionProperties.put("jib.container.useCurrentTimestamp", "true"); + Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); + sessionProperties.put("jib.container.user", "myUser"); + Assert.assertEquals("myUser", testPluginConfiguration.getUser()); + sessionProperties.put("jib.container.workingDirectory", "working directory"); + Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); + + sessionProperties.put("jib.extraDirectory.path", "custom-jib"); + Assert.assertEquals( + Paths.get("custom-jib"), testPluginConfiguration.getExtraDirectoryPath().get()); + sessionProperties.put("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); + List permissions = + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); + Assert.assertEquals("123", permissions.get(0).getMode().get()); + Assert.assertEquals("/another/file", permissions.get(1).getFile().get()); + Assert.assertEquals("456", permissions.get(1).getMode().get()); + } + + @Test + public void testPomProperties() { + project.getProperties().setProperty("jib.from.image", "fromImage"); Assert.assertEquals("fromImage", testPluginConfiguration.getBaseImage()); - System.setProperty("jib.from.credHelper", "credHelper"); + project.getProperties().setProperty("jib.from.credHelper", "credHelper"); Assert.assertEquals("credHelper", testPluginConfiguration.getBaseImageCredentialHelperName()); - System.setProperty("image", "toImage"); + project.getProperties().setProperty("image", "toImage"); Assert.assertEquals("toImage", testPluginConfiguration.getTargetImage()); - System.clearProperty("image"); - System.setProperty("jib.to.image", "toImage2"); + project.getProperties().remove("image"); + project.getProperties().setProperty("jib.to.image", "toImage2"); Assert.assertEquals("toImage2", testPluginConfiguration.getTargetImage()); - System.setProperty("jib.to.tags", "tag1,tag2,tag3"); + project.getProperties().setProperty("jib.to.tags", "tag1,tag2,tag3"); Assert.assertEquals( ImmutableSet.of("tag1", "tag2", "tag3"), testPluginConfiguration.getTargetImageAdditionalTags()); - System.setProperty("jib.to.credHelper", "credHelper"); + project.getProperties().setProperty("jib.to.credHelper", "credHelper"); Assert.assertEquals("credHelper", testPluginConfiguration.getTargetImageCredentialHelperName()); - System.setProperty("jib.container.appRoot", "appRoot"); + project.getProperties().setProperty("jib.container.appRoot", "appRoot"); Assert.assertEquals("appRoot", testPluginConfiguration.getAppRoot()); - System.setProperty("jib.container.args", "arg1,arg2,arg3"); + project.getProperties().setProperty("jib.container.args", "arg1,arg2,arg3"); Assert.assertEquals( ImmutableList.of("arg1", "arg2", "arg3"), testPluginConfiguration.getArgs()); - System.setProperty("jib.container.entrypoint", "entry1,entry2,entry3"); + project.getProperties().setProperty("jib.container.entrypoint", "entry1,entry2,entry3"); Assert.assertEquals( ImmutableList.of("entry1", "entry2", "entry3"), testPluginConfiguration.getEntrypoint()); - System.setProperty("jib.container.environment", "env1=val1,env2=val2"); + project.getProperties().setProperty("jib.container.environment", "env1=val1,env2=val2"); Assert.assertEquals( ImmutableMap.of("env1", "val1", "env2", "val2"), testPluginConfiguration.getEnvironment()); - System.setProperty("jib.container.format", "format"); + project.getProperties().setProperty("jib.container.format", "format"); Assert.assertEquals("format", testPluginConfiguration.getFormat()); - System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); + project.getProperties().setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); Assert.assertEquals( ImmutableList.of("flag1", "flag2", "flag3"), testPluginConfiguration.getJvmFlags()); - System.setProperty("jib.container.labels", "label1=val1,label2=val2"); + project.getProperties().setProperty("jib.container.labels", "label1=val1,label2=val2"); Assert.assertEquals( ImmutableMap.of("label1", "val1", "label2", "val2"), testPluginConfiguration.getLabels()); - System.setProperty("jib.container.mainClass", "main"); + project.getProperties().setProperty("jib.container.mainClass", "main"); Assert.assertEquals("main", testPluginConfiguration.getMainClass()); - System.setProperty("jib.container.ports", "port1,port2,port3"); + project.getProperties().setProperty("jib.container.ports", "port1,port2,port3"); Assert.assertEquals( ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); - System.setProperty("jib.container.useCurrentTimestamp", "true"); + project.getProperties().setProperty("jib.container.useCurrentTimestamp", "true"); Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); - System.setProperty("jib.container.user", "myUser"); + project.getProperties().setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); - System.setProperty("jib.container.workingDirectory", "working directory"); + project.getProperties().setProperty("jib.container.workingDirectory", "working directory"); Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); - System.setProperty("jib.extraDirectory.path", "custom-jib"); + project.getProperties().setProperty("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( Paths.get("custom-jib"), testPluginConfiguration.getExtraDirectoryPath().get()); - System.setProperty("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); + project + .getProperties() + .setProperty("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); List permissions = testPluginConfiguration.getExtraDirectoryPermissions(); Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml new file mode 100644 index 0000000000..04b625befc --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml @@ -0,0 +1,72 @@ + + 4.0.0 + + com.test + hello-world + 1 + + + 1.8 + UTF-8 + UTF-8 + @@PluginVersion@@ + + localhost:5000/distroless/java + ${_TARGET_IMAGE} + true + An argument. + com.test.HelloWorld + -Xms512m,-Xdebug + env1=envvalue1,env2=envvalue2 + 1000/tcp,2000-2003/udp + key1=value1,key2=value2 + /var/log,/var/log2 + Docker + ${project.basedir}/src/main/jib-custom + /foo=755,/bar/cat=777 + true + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + + testuser + testpassword + + + + + ${_TARGET_USERNAME} + ${_TARGET_PASSWORD} + + + + + + + From 9459d94959aa42a34bd236027fe8428c23a0210e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 3 Jan 2019 17:36:44 -0500 Subject: [PATCH 0424/2020] Set up Travis build matrix for additional OpenJDK 11 / upgrade Error Prone, NullAway, Javassist (#1386) * Build with JDK 11 * Build plugins-common with JDK 11 * Build gradle-plugin with JDK 11 * Build maven-plugin with JDK 11 * Run Travis on JDK 11 * Upgrade javassist * Target Java 8 * Fix ErrorProne on JDK 8 * Set up Travis matrix * Use annotationProcessor instead of apt * Update pom.xml comment * Add comments in build.gradle * Run simple Gradle build test against Gradle 4.6 --- .travis.yml | 9 ++- jib-core/build.gradle | 22 ++++-- .../hash/CountingDigestOutputStreamTest.java | 30 +++----- jib-gradle-plugin/build.gradle | 22 ++++-- .../jib/gradle/GradleLayerConfigurations.java | 3 +- .../gradle/GradleLayerConfigurationsTest.java | 13 ++++ .../gradle/GradleRawConfigurationTest.java | 4 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 17 ++++- jib-maven-plugin/pom.xml | 68 ++++++++++++------- .../jib/maven/MavenLayerConfigurations.java | 3 +- jib-plugins-common/build.gradle | 22 ++++-- .../logging/SingleThreadedExecutorTest.java | 1 + 12 files changed, 143 insertions(+), 71 deletions(-) diff --git a/.travis.yml b/.travis.yml index 145fc2dcc2..21507dd4a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,13 @@ sudo: false git: depth: 3 language: java -# using `openjdk8` leads to javadoc errors -jdk: oraclejdk8 +matrix: + include: + - name: Oracle JDK 8 + # using `openjdk8` leads to javadoc errors + jdk: oraclejdk8 + - name: OpenJDK 11 + jdk: openjdk11 env: global: - TERM=dumb # for gradle diff --git a/jib-core/build.gradle b/jib-core/build.gradle index fe135e9ad5..d408426da8 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -2,8 +2,8 @@ plugins { id 'java-library' id 'checkstyle' id 'com.github.sherter.google-java-format' version '0.8' - id 'net.ltgt.apt' version '0.13' - id 'net.ltgt.errorprone' version '0.0.13' + id 'net.ltgt.apt' version '0.19' + id 'net.ltgt.errorprone' version '0.6' id 'biz.aQute.bnd.builder' version '4.0.0' // For release. @@ -39,15 +39,20 @@ dependencies { implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:23.5-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - implementation 'org.javassist:javassist:3.22.0-GA' + implementation 'org.javassist:javassist:3.24.1-GA' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' testImplementation 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin - apt 'com.uber.nullaway:nullaway:0.4.2' - errorprone 'com.google.errorprone:error_prone_core:2.2.0' + annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' + errorprone 'com.google.errorprone:error_prone_core:2.3.2' + // Using github.com/google/error-prone-javac is required when running on + // JDK 8. Remove when migrating to JDK 11. + if (System.getProperty("java.version").startsWith("1.8.")) { + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") + } integrationTestCompile sourceSets.main.output integrationTestCompile sourceSets.test.output @@ -87,10 +92,15 @@ test { } } +import net.ltgt.gradle.errorprone.CheckSeverity + // Adds NullAway errorprone checks. tasks.withType(JavaCompile) { if (!name.toLowerCase().contains("test")) { - options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] + options.errorprone { + check('NullAway', CheckSeverity.ERROR) + option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index 865a63136f..a2b1b40f7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -26,36 +27,25 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.DigestException; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; /** Tests for {@link CountingDigestOutputStream}. */ public class CountingDigestOutputStreamTest { - private Map knownSha256Hashes; - - @Before - public void setUp() { - knownSha256Hashes = - Collections.unmodifiableMap( - new HashMap() { - { - put( - "crepecake", - "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); - put("12345", "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); - put("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - } - }); - } + private final Map KNOWN_SHA256_HASHES = + ImmutableMap.of( + "crepecake", + "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c", + "12345", + "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5", + "", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); @Test public void test_smokeTest() throws IOException, DigestException { - for (Map.Entry knownHash : knownSha256Hashes.entrySet()) { + for (Map.Entry knownHash : KNOWN_SHA256_HASHES.entrySet()) { String toHash = knownHash.getKey(); String expectedHash = knownHash.getValue(); diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index e2cb93296c..fb387ef3c4 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -7,8 +7,8 @@ plugins { id 'maven' // Error-prone checker - id 'net.ltgt.apt' version '0.13' - id 'net.ltgt.errorprone' version '0.0.13' + id 'net.ltgt.apt' version '0.19' + id 'net.ltgt.errorprone' version '0.6' // Prepare release id 'net.researchgate.release' version '2.6.0' @@ -65,7 +65,7 @@ dependencies { compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.javassist:javassist:3.22.0-GA' + compile 'org.javassist:javassist:3.24.1-GA' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' @@ -74,8 +74,13 @@ dependencies { compile gradleApi() // NullAway errorprone plugin - apt 'com.uber.nullaway:nullaway:0.4.2' - errorprone 'com.google.errorprone:error_prone_core:2.2.0' + annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' + errorprone 'com.google.errorprone:error_prone_core:2.3.2' + // Using github.com/google/error-prone-javac is required when running on + // JDK 8. Remove when migrating to JDK 11. + if (System.getProperty("java.version").startsWith("1.8.")) { + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") + } } // Integration tests must be run explicitly @@ -93,10 +98,15 @@ test { } } +import net.ltgt.gradle.errorprone.CheckSeverity + // Adds NullAway errorprone checks. tasks.withType(JavaCompile) { if (!name.toLowerCase().contains("test")) { - options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] + options.errorprone { + check('NullAway', CheckSeverity.ERROR) + option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') + } } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index ceae0c388f..7c5e8b937c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; import java.io.File; @@ -97,7 +96,7 @@ private static JavaLayerConfigurations getForNonWarProject( AbsoluteUnixPath classesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - Builder layerBuilder = JavaLayerConfigurations.builder(); + JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 8a0c3a719b..922f1d2e60 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -36,6 +36,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; import org.gradle.api.logging.Logger; @@ -46,6 +47,7 @@ import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.SourceSetOutput; import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskDependency; import org.gradle.api.tasks.bundling.War; import org.junit.Assert; import org.junit.Before; @@ -79,6 +81,17 @@ public String getDisplayName() { public Set getFiles() { return files; } + + @Override + public TaskDependency getBuildDependencies() { + return new TaskDependency() { + + @Override + public Set getDependencies(Task task) { + return Collections.emptySet(); + } + }; + } } private static void assertLayerEntriesUnordered( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index cd77320f0e..083ebde5c6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -91,7 +92,8 @@ public void testGetters() { Assert.assertEquals( Arrays.asList("--log", "info"), rawConfiguration.getProgramArguments().get()); Assert.assertEquals( - new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); + new HashSet<>(Arrays.asList("additional", "tags")), + Sets.newHashSet(rawConfiguration.getToTags())); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 060862c50f..a1bce65717 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -35,6 +35,7 @@ import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.Assert; +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -48,10 +49,22 @@ public class JibPluginTest { JibPlugin.BUILD_DOCKER_TASK_NAME, JibPlugin.BUILD_TAR_TASK_NAME); + private static boolean isJava8Runtime() { + return System.getProperty("java.version").startsWith("1.8."); + } + @Rule public final TemporaryFolder testProjectRoot = new TemporaryFolder(); @Test - public void testCheckGradleVersion_pass() { + public void testCheckGradleVersion_pass() throws IOException { + Assume.assumeTrue(isJava8Runtime()); + + // Copy build file to temp dir + Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); + InputStream buildFileContent = + getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); + Files.copy(buildFileContent, buildFile); + GradleRunner.create() .withProjectDir(testProjectRoot.getRoot()) .withPluginClasspath() @@ -62,6 +75,8 @@ public void testCheckGradleVersion_pass() { @Test public void testCheckGradleVersion_fail() throws IOException { + Assume.assumeTrue(isJava8Runtime()); + // Copy build file to temp dir Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); InputStream buildFileContent = diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0bd6965249..0312d0e485 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -41,8 +41,9 @@ UTF-8 UTF-8 - 1.8 - 1.8 + + 8 + 8 @@ -74,7 +75,7 @@ org.javassist javassist - 3.22.0-GA + 3.24.1-GA compile @@ -206,6 +207,34 @@ + + + + jdk8 + + 1.8 + + + 9+181-r4173-1 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + -J-Xbootclasspath/p:${settings.localRepository}/com/google/errorprone/javac/${javac.version}/javac-${javac.version}.jar + + + + + + @@ -318,41 +347,30 @@ compile - javac-with-errorprone - true - + ${maven.compiler.source} ${maven.compiler.target} + true true + + com.google.errorprone + error_prone_core + 2.3.2 + com.uber.nullaway nullaway - 0.4.2 + 0.6.4 - -Xep:NullAway:ERROR - -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools + -XDcompilePolicy=simple + -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools - - - org.codehaus.plexus - plexus-compiler-javac-errorprone - 2.8 - - - - com.google.errorprone - error_prone_core - 2.2.0 - - @@ -371,7 +389,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.5 + 3.6.0 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java index f48f4c3d30..07f0911be7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.Builder; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; import java.io.File; @@ -88,7 +87,7 @@ private static JavaLayerConfigurations getForNonWarProject( AbsoluteUnixPath classesExtractionPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - Builder layerBuilder = JavaLayerConfigurations.builder(); + JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); // Gets all the dependencies. List duplicates = diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 81d83d98b8..a137b98bce 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -2,8 +2,8 @@ plugins { id 'java' id 'checkstyle' id 'com.github.sherter.google-java-format' version '0.8' - id 'net.ltgt.apt' version '0.13' - id 'net.ltgt.errorprone' version '0.0.13' + id 'net.ltgt.apt' version '0.19' + id 'net.ltgt.errorprone' version '0.6' } group 'com.google.cloud.tools' @@ -33,15 +33,20 @@ dependencies { compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' - compile 'org.javassist:javassist:3.22.0-GA' + compile 'org.javassist:javassist:3.24.1-GA' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' // NullAway errorprone plugin - apt 'com.uber.nullaway:nullaway:0.4.2' - errorprone 'com.google.errorprone:error_prone_core:2.2.0' + annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' + errorprone 'com.google.errorprone:error_prone_core:2.3.2' + // Using github.com/google/error-prone-javac is required when running on + // JDK 8. Remove when migrating to JDK 11. + if (System.getProperty("java.version").startsWith("1.8.")) { + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") + } } test { @@ -51,10 +56,15 @@ test { } } +import net.ltgt.gradle.errorprone.CheckSeverity + // Adds NullAway errorprone checks. tasks.withType(JavaCompile) { if (!name.toLowerCase().contains("test")) { - options.compilerArgs += ["-Xep:NullAway:ERROR", "-XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools"] + options.errorprone { + check('NullAway', CheckSeverity.ERROR) + option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') + } } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java index 4fe5572310..ca55e04269 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java @@ -28,6 +28,7 @@ /** Tests for {@link SingleThreadedExecutor}. */ public class SingleThreadedExecutorTest { + @SuppressWarnings("ThreadPriorityCheck") // use of Thread.yield() @Test public void testExecute_mutualExclusion() throws IOException, ExecutionException, InterruptedException { From f6ee1643d28a5bc33605578698ebe15c09582d9a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 4 Jan 2019 15:19:26 -0500 Subject: [PATCH 0425/2020] Update com.fasterxml.jackson.core:jackson-databind to 2.9.8 (#1392) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index d408426da8..ac0d789809 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'com.google.http-client:google-http-client:1.23.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:23.5-jre' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' implementation 'org.javassist:javassist:3.24.1-GA' testImplementation 'junit:junit:4.12' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index fb387ef3c4..4dc53b9b7b 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -64,7 +64,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.23.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' compile 'org.javassist:javassist:3.24.1-GA' testCompile 'junit:junit:4.12' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0312d0e485..b6627affe0 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -69,7 +69,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.6 + 2.9.8 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index a137b98bce..62d4dbb74d 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -32,7 +32,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.23.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' compile 'org.javassist:javassist:3.24.1-GA' testCompile 'junit:junit:4.12' From b648ad47c7781af7b6f9bb9ed8d86a882099e83b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 7 Jan 2019 12:03:48 -0500 Subject: [PATCH 0426/2020] Minor fixes and cleanups (#1395) --- examples/dropwizard/pom.xml | 2 +- examples/helloworld/pom.xml | 4 +- examples/java-agent/pom.xml | 4 +- examples/multi-module/pom.xml | 5 ++- examples/spring-boot-kubernetes/pom.xml | 2 +- .../jib/builder/TimerEventDispatcherTest.java | 42 ------------------- .../AllocationCompletionTrackerTest.java | 3 +- .../progress/ProgressEventHandlerTest.java | 4 +- .../cloud/tools/jib/gradle/JibPlugin.java | 3 +- jib-maven-plugin/pom.xml | 4 +- .../resources/projects/default-target/pom.xml | 4 +- .../projects/empty/pom-broken-user.xml | 4 +- .../src/test/resources/projects/empty/pom.xml | 4 +- .../test/resources/projects/multi/lib/pom.xml | 3 +- .../src/test/resources/projects/multi/pom.xml | 1 + .../simple/pom-complex-properties.xml | 4 +- .../resources/projects/simple/pom-complex.xml | 4 +- .../projects/simple/pom-no-to-image.xml | 4 +- .../test/resources/projects/simple/pom.xml | 4 +- .../projects/war_servlet25/pom-tomcat.xml | 4 +- .../resources/projects/war_servlet25/pom.xml | 4 +- 21 files changed, 46 insertions(+), 67 deletions(-) diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index cb2435cbdd..91b732c6cb 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -2,7 +2,7 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 3a9d8bc418..a9045a706c 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 example diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index be4638a35c..b875278894 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 example diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 7b8fe174ea..f8fb176af4 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -1,7 +1,8 @@ + + http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 @@ -52,4 +53,4 @@ spring-boot-starter-web - \ No newline at end of file + diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index 1a7e21822d..b0e6c1e862 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 example diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java index 69fe670cd9..1c0948edb5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java @@ -27,7 +27,6 @@ import java.time.Instant; import java.util.ArrayDeque; import java.util.Deque; -import javax.annotation.Nullable; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -92,47 +91,6 @@ public void testLogging() { Assert.assertTrue(timerEventQueue.isEmpty()); } - /** - * Verifies the next {@link TimerEvent} on the {@link #timerEventQueue}. - * - * @param expectedParentTimer the expected parent {@link TimerEvent.Timer}, or {@code null} if - * none expected - * @param expectedState the expected {@link TimerEvent.State} - * @param expectedDescription the expected description - * @param hasLapped {@code true} if the timer has already lapped; {@code false} if it has not - * @return the verified {@link TimerEvent} - */ - private TimerEvent.Timer verifyNextTimerEvent( - @Nullable TimerEvent.Timer expectedParentTimer, - State expectedState, - String expectedDescription, - boolean hasLapped) { - TimerEvent timerEvent = timerEventQueue.poll(); - Assert.assertNotNull(timerEvent); - - if (expectedParentTimer == null) { - Assert.assertFalse(timerEvent.getTimer().getParent().isPresent()); - } else { - Assert.assertTrue(timerEvent.getTimer().getParent().isPresent()); - Assert.assertSame(expectedParentTimer, timerEvent.getTimer().getParent().get()); - } - Assert.assertEquals(expectedState, timerEvent.getState()); - if (expectedState == State.START) { - Assert.assertEquals(Duration.ZERO, timerEvent.getDuration()); - Assert.assertEquals(Duration.ZERO, timerEvent.getElapsed()); - } else { - Assert.assertTrue(timerEvent.getDuration().compareTo(Duration.ZERO) > 0); - if (hasLapped) { - Assert.assertTrue(timerEvent.getElapsed().compareTo(timerEvent.getDuration()) > 0); - } else { - Assert.assertEquals(0, timerEvent.getElapsed().compareTo(timerEvent.getDuration())); - } - } - Assert.assertEquals(expectedDescription, timerEvent.getDescription()); - - return timerEvent.getTimer(); - } - /** * Verifies that the {@code timerEvent}'s timer has no parent. * diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java index 25d2f429b9..0fcc7a1c8d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import org.junit.Assert; import org.junit.Test; @@ -101,7 +100,7 @@ public void testGetUnfinishedAllocations_singleThread() { @Test public void testGetUnfinishedAllocations_multipleThreads() - throws InterruptedException, ExecutionException, IOException, TimeoutException { + throws InterruptedException, ExecutionException, IOException { try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { AllocationCompletionTracker allocationCompletionTracker = new AllocationCompletionTracker(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 33740d5b13..353771bc87 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.DoubleAccumulator; import org.junit.Assert; import org.junit.Test; @@ -56,8 +55,7 @@ private AllocationTree() {} private static final double DOUBLE_ERROR_MARGIN = 1e-10; @Test - public void testAccept() - throws ExecutionException, InterruptedException, IOException, TimeoutException { + public void testAccept() throws ExecutionException, InterruptedException, IOException { try (MultithreadedExecutor multithreadedExecutor = new MultithreadedExecutor()) { DoubleAccumulator maxProgress = new DoubleAccumulator(Double::max, 0); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index aa9af4f1a6..d186b6559a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -99,8 +99,7 @@ public void apply(Project project) { .getTasks() .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) .setJibExtension(jibExtension); - Task filesTask = - project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); + project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); project.afterEvaluate( projectAfterEvaluation -> { diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index b6627affe0..22fa363113 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.google.cloud.tools diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml index 05be41b219..bbbd3abe1f 100644 --- a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml index 02872d9742..248cae963e 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 3c29682028..767f36018f 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml index 4f0786ce65..11a689fbd8 100644 --- a/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml @@ -1,7 +1,6 @@ - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.jib.test diff --git a/jib-maven-plugin/src/test/resources/projects/multi/pom.xml b/jib-maven-plugin/src/test/resources/projects/multi/pom.xml index f1a68eba56..6b1f514b70 100644 --- a/jib-maven-plugin/src/test/resources/projects/multi/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/multi/pom.xml @@ -1,3 +1,4 @@ + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml index 1e304a1ebe..423d941937 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml index eef6fd30ae..b609317781 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index af682afed8..8306a8427c 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml index f8fd908873..19a0962aee 100644 --- a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml index 6b38a8f882..440d258466 100644 --- a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test From bd350c6a67ec3c5b29bf4db245ee3dd89ce63f58 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 8 Jan 2019 12:00:36 -0500 Subject: [PATCH 0427/2020] Add docker client executable/environment configuration to Maven (#1393) * Add docker client parameters to docker mojo * Add integration tests for maven and gradle docker client configuration --- .../jib/builder/steps/LoadDockerStep.java | 7 ++- .../cloud/tools/jib/docker/DockerClient.java | 1 - .../gradle/SingleProjectIntegrationTest.java | 26 ++++++++++ .../projects/simple/build-dockerclient.gradle | 28 +++++++++++ .../resources/projects/simple/mock-docker.sh | 6 +++ jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 38 ++++++++++++-- .../maven/BuildDockerMojoIntegrationTest.java | 23 +++++++++ .../resources/projects/simple/mock-docker.sh | 6 +++ .../projects/simple/pom-dockerclient.xml | 50 +++++++++++++++++++ 10 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle create mode 100755 jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh create mode 100755 jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh create mode 100644 jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 2b9571b218..1f605118c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -100,7 +100,12 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. - dockerClient.load(new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)); + buildConfiguration + .getEventDispatcher() + .dispatch( + LogEvent.debug( + dockerClient.load( + new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)))); // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. for (String tag : buildConfiguration.getAllTargetImageTags()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 693b6dd0ab..af1c4df2a5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -127,7 +127,6 @@ static Function, ProcessBuilder> defaultProcessBuilderFactory( * * @param dockerExecutable path to {@code docker} * @param dockerEnvironment environment variables for {@code docker} - * @return a new {@link DockerClient} */ private DockerClient(Path dockerExecutable, ImmutableMap dockerEnvironment) { this(defaultProcessBuilderFactory(dockerExecutable.toString(), dockerEnvironment)); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 26f10755ee..ef264424b4 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -26,6 +26,7 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -203,6 +204,31 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException, assertWorkingDirectory("/home", targetImage); } + @Test + public void testExecute_dockerClient() throws IOException, InterruptedException, DigestException { + Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); + new Command( + "chmod", "+x", simpleTestProject.getProjectRoot().resolve("mock-docker.sh").toString()) + .run(); + + String targetImage = "simpleimage:gradle" + System.nanoTime(); + + BuildResult buildResult = + simpleTestProject.build( + "clean", + "jibDockerBuild", + "-Djib.useOnlyProjectCache=true", + "-D_TARGET_IMAGE=" + targetImage, + "-b=build-dockerclient.gradle", + "--debug"); + JibRunHelper.assertBuildSuccess( + buildResult, "jibDockerBuild", "Built image to Docker daemon as "); + JibRunHelper.assertImageDigestAndId(simpleTestProject.getProjectRoot()); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(targetImage)); + Assert.assertThat( + buildResult.getOutput(), CoreMatchers.containsString("Docker load called. value1 value2")); + } + @Test public void testBuildTar_simple() throws IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle new file mode 100644 index 0000000000..36d0ff075c --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } +} + +jibDockerBuild { + dockerClient { + executable = file('mock-docker.sh') + environment = [envvar1:'value1', envvar2:'value2'] + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh b/jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh new file mode 100755 index 0000000000..e4b78a0f4c --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Read stdin to avoid broken pipe +cat > /dev/null + +echo "Docker load called. $envvar1 $envvar2" \ No newline at end of file diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9af94bc815..cd6e65b1ab 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. - Maven proxy settings are now supported. - Now checks for system properties in pom as well as commandline. ([#1201](https://github.com/GoogleContainerTools/jib/issues/1201)) +- `` and `` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary ([#468](https://github.com/GoogleContainerTools/jib/issues/468)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index f8678109bc..387d28a109 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -33,12 +33,16 @@ import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.common.annotations.VisibleForTesting; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; +import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; /** Builds a container image and exports to the default Docker daemon. */ @@ -47,10 +51,31 @@ requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) public class BuildDockerMojo extends JibPluginConfiguration { - @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; + /** + * Object that configures the Docker executable and the additional environment variables to use + * when executing the executable. + */ + public static class DockerClientConfiguration { + + @Nullable @Parameter private File executable; + @Nullable @Parameter private Map environment; + @Nullable + private Path getExecutable() { + return executable == null ? null : executable.toPath(); + } + + @Nullable + private Map getEnvironment() { + return environment; + } + } + + @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; + @Parameter private DockerClientConfiguration dockerClient = new DockerClientConfiguration(); + @Override public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { @@ -62,7 +87,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - if (!DockerClient.isDefaultDockerInstalled()) { + Path dockerExecutable = dockerClient.getExecutable(); + boolean isDockerInstalled = + dockerExecutable == null + ? DockerClient.isDefaultDockerInstalled() + : DockerClient.isDockerInstalled(dockerExecutable); + if (!isDockerInstalled) { throw new MojoExecutionException( HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } @@ -89,8 +119,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, - null, - null, + dockerExecutable, + dockerClient.getEnvironment(), mavenHelpfulSuggestionsBuilder.build()); ProxyProvider.init(getSession().getSettings()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 08585e4210..e6725e2d0c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -25,6 +25,7 @@ import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Assume; import org.junit.ClassRule; import org.junit.Test; @@ -109,6 +110,28 @@ public void testExecute_simple() Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); } + @Test + public void testExecute_dockerClient() + throws VerificationException, IOException, InterruptedException { + Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); + new Command( + "chmod", "+x", simpleTestProject.getProjectRoot().resolve("mock-docker.sh").toString()) + .run(); + + String targetImage = "simpleimage:maven" + System.nanoTime(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setAutoclean(false); + verifier.addCliOption("--file=pom-dockerclient.xml"); + verifier.addCliOption("--debug"); + verifier.executeGoal("package"); + + verifier.executeGoal("jib:dockerBuild"); + verifier.verifyTextInLog("Docker load called. value1 value2"); + verifier.verifyErrorFreeLog(); + } + @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException, DigestException { diff --git a/jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh b/jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh new file mode 100755 index 0000000000..e4b78a0f4c --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Read stdin to avoid broken pipe +cat > /dev/null + +echo "Docker load called. $envvar1 $envvar2" \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml new file mode 100644 index 0000000000..c4ed7262ce --- /dev/null +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + ${project.basedir}/mock-docker.sh + + value1 + value2 + + + + + + + From 3e1af6dd4c99740a650118684652f023dbf89f9d Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 8 Jan 2019 12:49:49 -0500 Subject: [PATCH 0428/2020] minor fix (#1397) --- .../src/test/resources/projects/simple/pom-dockerclient.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml index c4ed7262ce..6f1e612976 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml @@ -26,6 +26,10 @@ org.apache.maven.plugins maven-compiler-plugin + + 1.8 + 1.8 + From 29a7358b2bf601fb16dcee1ce374b681e14cefc6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 9 Jan 2019 10:50:36 -0500 Subject: [PATCH 0429/2020] Add system properties for docker client configuration (#1400) --- .../jib/gradle/DockerClientParameters.java | 9 +++++ .../tools/jib/maven/BuildDockerMojo.java | 34 ++++++++++++------- .../jib/maven/JibPluginConfiguration.java | 2 +- .../jib/plugins/common/PropertyNames.java | 2 ++ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java index bf057483fe..d9b06ff3d5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -37,6 +39,9 @@ public class DockerClientParameters { @Nullable @Optional public String getExecutable() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE) != null) { + return System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE); + } return executable == null ? null : executable.toString(); } @@ -54,6 +59,10 @@ public void setExecutable(String executable) { @Nullable @Optional public Map getEnvironment() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT) != null) { + return ConfigurationPropertyValidator.parseMapProperty( + System.getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT)); + } return environment; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 387d28a109..190fbaec9b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; @@ -32,6 +33,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; @@ -59,16 +61,6 @@ public static class DockerClientConfiguration { @Nullable @Parameter private File executable; @Nullable @Parameter private Map environment; - - @Nullable - private Path getExecutable() { - return executable == null ? null : executable.toPath(); - } - - @Nullable - private Map getEnvironment() { - return environment; - } } @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; @@ -87,7 +79,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - Path dockerExecutable = dockerClient.getExecutable(); + Path dockerExecutable = getDockerClientExecutable(); boolean isDockerInstalled = dockerExecutable == null ? DockerClient.isDefaultDockerInstalled() @@ -120,7 +112,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, dockerExecutable, - dockerClient.getEnvironment(), + getDockerClientEnvironment(), mavenHelpfulSuggestionsBuilder.build()); ProxyProvider.init(getSession().getSettings()); @@ -179,4 +171,22 @@ targetImageReference, getTargetImageAdditionalTags()) throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } } + + @Nullable + private Path getDockerClientExecutable() { + String property = getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE); + if (property != null) { + return Paths.get(property); + } + return dockerClient.executable == null ? null : dockerClient.executable.toPath(); + } + + @Nullable + private Map getDockerClientEnvironment() { + String property = getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT); + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property); + } + return dockerClient.environment; + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index d8e7f87ac0..b01bc9857b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -556,7 +556,7 @@ void setProject(MavenProject project) { * @return the value of the system property, or null if not defined */ @Nullable - private String getProperty(String propertyName) { + String getProperty(String propertyName) { if (session != null && session.getSystemProperties().containsKey(propertyName)) { return session.getSystemProperties().getProperty(propertyName); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index b66531e25d..5347625eb6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -48,6 +48,8 @@ public class PropertyNames { public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; + public static final String DOCKER_CLIENT_EXECUTABLE = "jib.dockerClient.executable"; + public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; public static final String SKIP = "jib.skip"; public static final String CONSOLE = "jib.console"; From eae264db24f79cb1bb939b814572b78838235be6 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 9 Jan 2019 18:28:05 -0500 Subject: [PATCH 0430/2020] Avoid nested Futures by calling combiners in a blocking manner (#1378) * Avoid nested Futures * Update AsyncDependencies.java * Update Javadoc * Remove helper method --- .../cloud/tools/jib/async/AsyncDependencies.java | 3 ++- .../tools/jib/builder/steps/PushImageStep.java | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java index b562282bc1..7f43957e94 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java @@ -71,7 +71,8 @@ public AsyncDependencies addSteps(List> asyncSteps) { } /** - * Calls {@code combiner} when all the added futures succeed. + * Creates the {@link ListenableFuture} which will return the result of calling {@code combiner} + * when all the added futures succeed. * * @param combiner the {@link Callable} * @param the return type of {@code combiner} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 97cb96d96a..d60ff07038 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -98,13 +98,10 @@ public BuildResult call() throws ExecutionException, InterruptedException { .addStep(NonBlockingSteps.get(pushContainerConfigurationStep)) .addStep(NonBlockingSteps.get(buildImageStep)) .whenAllSucceed(this::afterPushSteps) - .get() - .get() .get(); } - private ListenableFuture> afterPushSteps() - throws ExecutionException { + private BuildResult afterPushSteps() throws ExecutionException, InterruptedException { AsyncDependencies dependencies = AsyncDependencies.using(listeningExecutorService); for (AsyncStep pushBaseImageLayerStep : NonBlockingSteps.get(pushBaseImageLayersStep)) { @@ -116,10 +113,12 @@ private ListenableFuture> afterPushSteps() } return dependencies .addStep(NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep))) - .whenAllSucceed(this::afterAllPushed); + .whenAllSucceed(this::afterAllPushed) + .get(); } - private ListenableFuture afterAllPushed() throws ExecutionException, IOException { + private BuildResult afterAllPushed() + throws ExecutionException, IOException, InterruptedException { ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); @@ -176,7 +175,8 @@ private ListenableFuture afterAllPushed() throws ExecutionException progressEventDispatcher.close(); return result; }, - listeningExecutorService); + listeningExecutorService) + .get(); } } } From 08bcf0211b7b5ec0d60500c4904ecc75a2644346 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 10 Jan 2019 14:42:27 -0500 Subject: [PATCH 0431/2020] Get maven auth system properties from pom and commandline (#1391) --- .../jib/gradle/GradleRawConfiguration.java | 5 ++++ .../jib/maven/JibPluginConfiguration.java | 6 ++-- .../jib/maven/MavenRawConfiguration.java | 5 ++++ .../simple/pom-complex-properties.xml | 18 +++--------- .../ConfigurationPropertyValidator.java | 14 ++++------ .../common/PluginConfigurationProcessor.java | 6 ++-- .../jib/plugins/common/RawConfiguration.java | 2 ++ .../ConfigurationPropertyValidatorTest.java | 28 +++++++++---------- 8 files changed, 43 insertions(+), 41 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 6511be1bf8..4be98543f2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -136,4 +136,9 @@ public boolean getAllowInsecureRegistries() { public ImageFormat getImageFormat() { return jibExtension.getContainer().getFormat(); } + + @Override + public Optional getProperty(String propertyName) { + return Optional.ofNullable(System.getProperty(propertyName)); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index b01bc9857b..a04a9daec8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -270,7 +270,7 @@ String getBaseImageCredentialHelperName() { } AuthConfiguration getBaseImageAuth() { - // TODO: handle properties here instead of in ConfigurationPropertyValidator + // System/pom properties for auth are handled in ConfigurationPropertyValidator return from.auth; } @@ -320,7 +320,7 @@ String getTargetImageCredentialHelperName() { } AuthConfiguration getTargetImageAuth() { - // TODO: handle properties here instead of in ConfigurationPropertyValidator + // System/pom properties for auth are handled in ConfigurationPropertyValidator return to.auth; } @@ -556,7 +556,7 @@ void setProject(MavenProject project) { * @return the value of the system property, or null if not defined */ @Nullable - String getProperty(String propertyName) { + public String getProperty(String propertyName) { if (session != null && session.getSystemProperties().containsKey(propertyName)) { return session.getSystemProperties().getProperty(propertyName); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index e84dca3b0c..0b9c901de6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -141,4 +141,9 @@ public boolean getAllowInsecureRegistries() { public ImageFormat getImageFormat() { return ImageFormat.valueOf(jibPluginConfiguration.getFormat()); } + + @Override + public Optional getProperty(String propertyName) { + return Optional.ofNullable(jibPluginConfiguration.getProperty(propertyName)); + } } diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml index 84947d00bd..134f0f91a8 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml @@ -14,7 +14,11 @@ @@PluginVersion@@ localhost:5000/distroless/java + testuser + testpassword ${_TARGET_IMAGE} + ${_TARGET_USERNAME} + ${_TARGET_PASSWORD} true An argument. com.test.HelloWorld @@ -54,20 +58,6 @@ com.google.cloud.tools jib-maven-plugin ${jib-maven-plugin.version} - - - - testuser - testpassword - - - - - ${_TARGET_USERNAME} - ${_TARGET_PASSWORD} - - - diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 11455887eb..3995dc6f2b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -48,8 +48,7 @@ public class ConfigurationPropertyValidator { * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials - * @param authUsernameDescriptor the name of the auth username parameter - * @param authPasswordDescriptor the name of the auth password parameter + * @param rawConfiguration the {@link RawConfiguration} that provides system properties * @return a new {@link Authorization} from the system properties or build configuration, or * {@link Optional#empty} if neither is configured. */ @@ -58,11 +57,10 @@ public static Optional getImageCredential( String usernameProperty, String passwordProperty, AuthProperty auth, - String authUsernameDescriptor, - String authPasswordDescriptor) { + RawConfiguration rawConfiguration) { // System property takes priority over build configuration - String commandlineUsername = System.getProperty(usernameProperty); - String commandlinePassword = System.getProperty(passwordProperty); + String commandlineUsername = rawConfiguration.getProperty(usernameProperty).orElse(null); + String commandlinePassword = rawConfiguration.getProperty(passwordProperty).orElse(null); if (!Strings.isNullOrEmpty(commandlineUsername) && !Strings.isNullOrEmpty(commandlinePassword)) { return Optional.of(Credential.basic(commandlineUsername, commandlinePassword)); @@ -93,14 +91,14 @@ public static Optional getImageCredential( if (Strings.isNullOrEmpty(auth.getUsername())) { eventDispatcher.dispatch( LogEvent.warn( - authUsernameDescriptor + auth.getUsernameDescriptor() + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { eventDispatcher.dispatch( LogEvent.warn( - authPasswordDescriptor + auth.getPasswordDescriptor() + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 218174dd0e..0cccbcf98c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -141,6 +141,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry new DefaultEventDispatcher(projectProperties.getEventHandlers()); boolean isTargetImageCredentialPresent = configureCredentialRetrievers( + rawConfiguration, eventDispatcher, targetImage, targetImageReference, @@ -191,6 +192,7 @@ static PluginConfigurationProcessor processCommonConfiguration( RegistryImage baseImage = RegistryImage.named(baseImageReference); boolean isBaseImageCredentialPresent = configureCredentialRetrievers( + rawConfiguration, eventDispatcher, baseImage, baseImageReference, @@ -367,6 +369,7 @@ static Optional getWorkingDirectoryChecked(RawConfiguration ra // TODO: find a way to reduce the number of arguments. private static boolean configureCredentialRetrievers( + RawConfiguration rawConfiguration, EventDispatcher eventDispatcher, RegistryImage registryImage, ImageReference imageReference, @@ -385,8 +388,7 @@ private static boolean configureCredentialRetrievers( usernamePropertyName, passwordPropertyName, knownAuth, - knownAuth.getUsernameDescriptor(), - knownAuth.getPasswordDescriptor()); + rawConfiguration); boolean credentialPresent = optionalCredential.isPresent(); if (optionalCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index a20772cb7e..1d55b9fd8c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -68,4 +68,6 @@ public interface RawConfiguration { boolean getAllowInsecureRegistries(); ImageFormat getImageFormat(); + + Optional getProperty(String propertyName); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index e702690316..c7941de0cf 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -38,15 +38,20 @@ public class ConfigurationPropertyValidatorTest { @Mock private EventDispatcher mockEventDispatcher; @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; + @Mock private RawConfiguration mockConfiguration; @Test public void testGetImageAuth() { + Mockito.when(mockAuth.getUsernameDescriptor()).thenReturn("user"); + Mockito.when(mockAuth.getPasswordDescriptor()).thenReturn("pass"); Mockito.when(mockAuth.getUsername()).thenReturn("vwxyz"); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); // System properties set - System.setProperty("jib.test.auth.user", "abcde"); - System.setProperty("jib.test.auth.pass", "12345"); + Mockito.when(mockConfiguration.getProperty("jib.test.auth.user")) + .thenReturn(Optional.of("abcde")); + Mockito.when(mockConfiguration.getProperty("jib.test.auth.pass")) + .thenReturn(Optional.of("12345")); Credential expected = Credential.basic("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( @@ -54,14 +59,13 @@ public void testGetImageAuth() { "jib.test.auth.user", "jib.test.auth.pass", mockAuth, - "user", - "pass"); + mockConfiguration); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); // Auth set in configuration - System.clearProperty("jib.test.auth.user"); - System.clearProperty("jib.test.auth.pass"); + Mockito.when(mockConfiguration.getProperty("jib.test.auth.user")).thenReturn(Optional.empty()); + Mockito.when(mockConfiguration.getProperty("jib.test.auth.pass")).thenReturn(Optional.empty()); expected = Credential.basic("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( @@ -69,8 +73,7 @@ public void testGetImageAuth() { "jib.test.auth.user", "jib.test.auth.pass", mockAuth, - "user", - "pass"); + mockConfiguration); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); Mockito.verify(mockEventDispatcher, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); @@ -84,8 +87,7 @@ public void testGetImageAuth() { "jib.test.auth.user", "jib.test.auth.pass", mockAuth, - "user", - "pass"); + mockConfiguration); Assert.assertFalse(actual.isPresent()); // Password missing @@ -97,8 +99,7 @@ public void testGetImageAuth() { "jib.test.auth.user", "jib.test.auth.pass", mockAuth, - "user", - "pass"); + mockConfiguration); Assert.assertFalse(actual.isPresent()); Mockito.verify(mockEventDispatcher) .dispatch( @@ -113,8 +114,7 @@ public void testGetImageAuth() { "jib.test.auth.user", "jib.test.auth.pass", mockAuth, - "user", - "pass"); + mockConfiguration); Assert.assertFalse(actual.isPresent()); Mockito.verify(mockEventDispatcher) .dispatch( From 7ca8c806172452397055f84fdb90bd012348216f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 10 Jan 2019 14:56:59 -0500 Subject: [PATCH 0432/2020] Add clearer error messages for null elements in config lists (#1401) --- .../configuration/ContainerConfiguration.java | 25 +++++++---- .../jib/configuration/ImageConfiguration.java | 3 +- .../ContainerConfigurationTest.java | 44 +++++++++++++------ 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index b539dc1dd8..2fe9e5bce1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -72,7 +72,8 @@ public Builder setProgramArguments(@Nullable List programArguments) { if (programArguments == null) { this.programArguments = null; } else { - Preconditions.checkArgument(!programArguments.contains(null)); + Preconditions.checkArgument( + !programArguments.contains(null), "program arguments list contains null elements"); this.programArguments = ImmutableList.copyOf(programArguments); } return this; @@ -88,8 +89,12 @@ public Builder setEnvironment(@Nullable Map environmentMap) { if (environmentMap == null) { this.environmentMap = null; } else { - Preconditions.checkArgument(!Iterables.any(environmentMap.keySet(), Objects::isNull)); - Preconditions.checkArgument(!Iterables.any(environmentMap.values(), Objects::isNull)); + Preconditions.checkArgument( + !Iterables.any(environmentMap.keySet(), Objects::isNull), + "environment map contains null keys"); + Preconditions.checkArgument( + !Iterables.any(environmentMap.values(), Objects::isNull), + "environment map contains null values"); this.environmentMap = new HashMap<>(environmentMap); } return this; @@ -112,7 +117,8 @@ public Builder setExposedPorts(@Nullable Set exposedPorts) { if (exposedPorts == null) { this.exposedPorts = null; } else { - Preconditions.checkArgument(!exposedPorts.contains(null)); + Preconditions.checkArgument( + !exposedPorts.contains(null), "ports list contains null elements"); this.exposedPorts = new HashSet<>(exposedPorts); } return this; @@ -135,7 +141,7 @@ public Builder setVolumes(@Nullable Set volumes) { if (volumes == null) { this.volumes = null; } else { - Preconditions.checkArgument(!volumes.contains(null)); + Preconditions.checkArgument(!volumes.contains(null), "volumes list contains null elements"); this.volumes = new HashSet<>(volumes); } return this; @@ -158,8 +164,10 @@ public Builder setLabels(@Nullable Map labels) { if (labels == null) { this.labels = null; } else { - Preconditions.checkArgument(!Iterables.any(labels.keySet(), Objects::isNull)); - Preconditions.checkArgument(!Iterables.any(labels.values(), Objects::isNull)); + Preconditions.checkArgument( + !Iterables.any(labels.keySet(), Objects::isNull), "labels map contains null keys"); + Preconditions.checkArgument( + !Iterables.any(labels.values(), Objects::isNull), "labels map contains null values"); this.labels = new HashMap<>(labels); } return this; @@ -182,7 +190,8 @@ public Builder setEntrypoint(@Nullable List entrypoint) { if (entrypoint == null) { this.entrypoint = null; } else { - Preconditions.checkArgument(!entrypoint.contains(null)); + Preconditions.checkArgument( + !entrypoint.contains(null), "entrypoint contains null elements"); this.entrypoint = ImmutableList.copyOf(entrypoint); } return this; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index 0ab65af9d9..c1ddde05fc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -39,7 +39,8 @@ public static class Builder { * @return this */ public Builder setCredentialRetrievers(List credentialRetrievers) { - Preconditions.checkArgument(!credentialRetrievers.contains(null)); + Preconditions.checkArgument( + !credentialRetrievers.contains(null), "credential retriever list contains null elements"); this.credentialRetrievers = ImmutableList.copyOf(credentialRetrievers); return this; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index eb32817bf4..f2a0cd949d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -38,7 +38,7 @@ public void testBuilder_nullValues() { ContainerConfiguration.builder().setProgramArguments(Arrays.asList("first", null)); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("program arguments list contains null elements", ex.getMessage()); } // Entrypoint element should not be null. @@ -46,7 +46,7 @@ public void testBuilder_nullValues() { ContainerConfiguration.builder().setEntrypoint(Arrays.asList("first", null)); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("entrypoint contains null elements", ex.getMessage()); } // Exposed ports element should not be null. @@ -55,38 +55,54 @@ public void testBuilder_nullValues() { ContainerConfiguration.builder().setExposedPorts(badPorts); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("ports list contains null elements", ex.getMessage()); } - // Labels element should not be null. - Map badLabels = new HashMap<>(); - badLabels.put("label-key", null); + // Volume element should not be null. + Set badVolumes = + new HashSet<>(Arrays.asList(AbsoluteUnixPath.get("/"), null)); try { - ContainerConfiguration.builder().setLabels(badLabels); + ContainerConfiguration.builder().setVolumes(badVolumes); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("volumes list contains null elements", ex.getMessage()); } - // Environment keys element should not be null. Map nullKeyMap = new HashMap<>(); nullKeyMap.put(null, "value"); + Map nullValueMap = new HashMap<>(); + nullValueMap.put("key", null); + + // Label keys should not be null. + try { + ContainerConfiguration.builder().setLabels(nullKeyMap); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("labels map contains null keys", ex.getMessage()); + } + // Labels values should not be null. + try { + ContainerConfiguration.builder().setLabels(nullValueMap); + Assert.fail("The IllegalArgumentException should be thrown."); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("labels map contains null values", ex.getMessage()); + } + + // Environment keys should not be null. try { ContainerConfiguration.builder().setEnvironment(nullKeyMap); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("environment map contains null keys", ex.getMessage()); } - // Environment values element should not be null. - Map nullValueMap = new HashMap<>(); - nullValueMap.put("key", null); + // Environment values should not be null. try { ContainerConfiguration.builder().setEnvironment(nullValueMap); Assert.fail("The IllegalArgumentException should be thrown."); } catch (IllegalArgumentException ex) { - Assert.assertNull(ex.getMessage()); + Assert.assertEquals("environment map contains null values", ex.getMessage()); } } From 1c88a315e361b4f75b753588b660989bd562db1a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 10 Jan 2019 15:18:38 -0500 Subject: [PATCH 0433/2020] Gradle release v1.0.0-rc2 (#1404) * [Gradle Release Plugin] - pre tag commit: 'v1.0.0-rc2-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.0.0-SNAPSHOT-gradle'. From 9fa248f074a458247278b0699a8c0b15e775913d Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 10 Jan 2019 16:15:00 -0500 Subject: [PATCH 0434/2020] Maven release v1.0.0-rc2 (#1405) * preparing release 1.0.0-rc2 * 1.0.0-SNAPSHOT From 4bd101d40ca0acbbda08601d1107793850ed4593 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 10 Jan 2019 16:15:44 -0500 Subject: [PATCH 0435/2020] Update changelogs for rc2 (#1406) --- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 5c04d8d8fb..87e7e6170d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,14 +5,20 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.0.0-rc2 + +### Added + - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. ### Changed - Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) -### Fixed - ## 1.0.0-rc1 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index cd6e65b1ab..b2f168ac49 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.0.0-rc2 + +### Added + - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. - Maven proxy settings are now supported. - Now checks for system properties in pom as well as commandline. ([#1201](https://github.com/GoogleContainerTools/jib/issues/1201)) @@ -14,8 +22,6 @@ All notable changes to this project will be documented in this file. - Java 9+ projects using the default distroless Java 8 base image will now fail to build. ([#1143](https://github.com/GoogleContainerTools/jib/issues/1143)) -### Fixed - ## 1.0.0-rc1 ### Added From b3118a6d43a4b9b9199d82f8c4d78e7d003a56ce Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 10 Jan 2019 16:18:34 -0500 Subject: [PATCH 0436/2020] Add dockerClient configuration to READMEs (#1398) --- jib-gradle-plugin/README.md | 10 ++++++++++ jib-maven-plugin/README.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 39cb9ff310..6e029cc6e2 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -128,6 +128,8 @@ eval $(minikube docker-env) gradle jibDockerBuild ``` +Alternatively, you can set environment variables in the Jib configuration. See [`dockerClient`](#dockerclient-closure) for more configuration options. + #### Build an image tarball You can build and save your image to disk as a tarball with: @@ -218,6 +220,13 @@ Property | Type `path` | `File` `permissions` | `Map` +**(`jibDockerBuild` only)** `dockerClient` is an object that can be configured directly on the `jibDockerBuild` task, and has the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`executable` | `File` | `docker` | Sets the path to the Docker executable that is called to load the image into the Docker daemon. +`environment` | `Map` | *None* | Sets environment variables used by the Docker executable. + #### System Properties Each of these parameters is configurable via commandline using system properties. Jib's system properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: @@ -228,6 +237,7 @@ gradle jib \ -Djib.to.auth.password=$PASSWORD gradle jibDockerBuild \ + -Djib.dockerClient.executable=/path/to/docker \ -Djib.container.environment=key1="value1",key2="value2" \ -Djib.container.args=arg1,arg2,arg3 ``` diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index ced7dcdf0a..9000c4cdc5 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -153,6 +153,8 @@ eval $(minikube docker-env) mvn compile jib:dockerBuild ``` +Alternatively, you can set environment variables in the Jib configuration. See [`dockerClient`](#dockerclient-object) for more configuration options. + #### Build an image tarball You can build and save your image to disk as a tarball with: @@ -260,6 +262,13 @@ Property | Type `path` | string `permissions` | list +**(`jib:dockerBuild` only)** `dockerClient` is an object with the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`executable` | string | `docker` | Sets the path to the Docker executable that is called to load the image into the Docker daemon. +`environment` | map | *None* | Sets environment variables used by the Docker executable. + #### System Properties Each of these parameters is configurable via commandline using system properties. Jib's system properties follow the same naming convention as the configuration parameters, with each level separated by dots (i.e. `-Djib.parameterName[.nestedParameter.[...]]=value`). Some examples are below: @@ -270,6 +279,7 @@ mvn compile jib:build \ -Djib.to.auth.password=$PASSWORD mvn compile jib:dockerBuild \ + -Djib.dockerClient.executable=/path/to/docker \ -Djib.container.environment=key1="value1",key2="value2" \ -Djib.container.args=arg1,arg2,arg3 ``` From 6cb7c337418c1a6baa049109ef905343cbc1733b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 11 Jan 2019 14:29:08 -0500 Subject: [PATCH 0437/2020] Update version strings for 1.0.0-rc2 (#1407) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 6 +++--- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5619b54db7..a04fc1b6ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:0.10.2-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.0-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '0.10.1' + // id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 91b732c6cb..685026f6ea 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 0.10.1 + 1.0.0-rc2 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 398b1079b2..b0147059fc 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index a9045a706c..2ff499e316 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 0.10.1 + 1.0.0-rc2 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 9b98c01b7b..751daa999e 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index b875278894..04faa577a3 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 0.10.1 + 1.0.0-rc2 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index f780f94389..5c5e70073d 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "0.10.1" + id("com.google.cloud.tools.jib") version "1.0.0-rc2" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 1ecd6b3516..73b4d45047 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 499891f07b..9a4979f146 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.10.1' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.0-rc2' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index f8fb176af4..245436822d 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.1 + 1.0.0-rc2 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 1dd75184d1..04367c79e9 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 73d446b274..78ea3a0814 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 0.10.1 + 1.0.0-rc2 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index 3a4c3a3ca2..bee2aa6b06 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index b0e6c1e862..3ce02d9c75 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 0.10.1 + 1.0.0-rc2 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index c9bc842cd8..35cd29c871 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } repositories { diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 6e029cc6e2..4717146ef2 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -43,7 +43,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '0.10.1' + id 'com.google.cloud.tools.jib' version '1.0.0-rc2' } ``` diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 9000c4cdc5..bd28753bfc 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -38,7 +38,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0-rc2:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -46,7 +46,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:0.10.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0-rc2:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -64,7 +64,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 0.10.1 + 1.0.0-rc2 myimage From 17bc14e0821d15aaf52990686c840b6d647a7693 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 14 Jan 2019 14:18:36 -0500 Subject: [PATCH 0438/2020] Removes FinalizingStep. (#1412) --- .../cloud/tools/jib/builder/BuildSteps.java | 3 - .../jib/builder/steps/FinalizingStep.java | 88 ------------------- .../tools/jib/builder/steps/StepsRunner.java | 25 ------ 3 files changed, 116 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java index 19355f0aff..13d41cc5ee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java @@ -50,7 +50,6 @@ public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfig .buildImage() .pushContainerConfiguration() .pushApplicationLayers() - .finalizingPush() .pushImage()); } @@ -71,7 +70,6 @@ public static BuildSteps forBuildToDockerDaemon( .pullAndCacheBaseImageLayers() .buildAndCacheApplicationLayers() .buildImage() - .finalizingBuild() .loadDocker(dockerClient)); } @@ -91,7 +89,6 @@ public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration build .pullAndCacheBaseImageLayers() .buildAndCacheApplicationLayers() .buildImage() - .finalizingBuild() .writeTarFile(outputPath)); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java deleted file mode 100644 index a460519566..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/FinalizingStep.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -/** Logs the message before finalizing an image build. */ -class FinalizingStep implements AsyncStep, Callable { - - private final BuildConfiguration buildConfiguration; - private final List>>> - futureDependencyLists; - - private final ListeningExecutorService listeningExecutorService; - private final ListenableFuture listenableFuture; - - /** - * @param wrappedDependencyLists {@link AsyncStep}s that must be unwrapped for additional {@link - * AsyncStep}s to depend on - * @param dependencyList list of additional {@link AsyncStep}s to depend on directly - */ - FinalizingStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - List>>> wrappedDependencyLists, - List> dependencyList) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.futureDependencyLists = wrappedDependencyLists; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addSteps(wrappedDependencyLists) - .addSteps(dependencyList) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; - } - - @Override - public Void call() throws ExecutionException { - AsyncDependencies asyncDependencies = AsyncDependencies.using(listeningExecutorService); - - // Unwrap the wrapped dependencies. - for (AsyncStep>> wrappedDependency : - futureDependencyLists) { - asyncDependencies.addSteps(NonBlockingSteps.get(wrappedDependency)); - } - - // This suppresses any exceptions of this future. - Future ignored = - asyncDependencies.whenAllSucceed( - () -> { - buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress("Finalizing...")); - return null; - }); - - return null; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 6581dbcf2f..2f37677169 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -27,8 +27,6 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import javax.annotation.Nullable; @@ -198,29 +196,6 @@ public StepsRunner pushApplicationLayers() { Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); } - public StepsRunner finalizingPush() { - return enqueueStep( - () -> - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Arrays.asList( - Preconditions.checkNotNull(steps.pushBaseImageLayersStep), - Preconditions.checkNotNull(steps.pushApplicationLayersStep)), - Collections.emptyList())); - } - - public StepsRunner finalizingBuild() { - return enqueueStep( - () -> - new FinalizingStep( - listeningExecutorService, - buildConfiguration, - Collections.singletonList( - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep)), - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); - } - public StepsRunner pushImage() { rootProgressAllocationDescription = "building image to registry"; From aa99b76f678a3bb3c83252148394bb0da881a09a Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 14 Jan 2019 14:30:33 -0500 Subject: [PATCH 0439/2020] Makes progress footer at most 50 characters wide. (#1411) --- .../tools/jib/builder/steps/PushBlobStep.java | 2 +- .../common/logging/AnsiLoggerWithFooter.java | 37 +++++++++++++++++-- .../logging/ProgressDisplayGenerator.java | 2 +- .../logging/AnsiLoggerWithFooterTest.java | 13 +++++++ .../logging/ProgressDisplayGeneratorTest.java | 14 ++----- 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 46a41572ed..88e0a91bfa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -96,7 +96,7 @@ public ListenableFuture getFuture() { public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDipatcherFactory.create( - "push blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); + "pushing blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index 1dc31bca10..dc4e201b5d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -17,8 +17,10 @@ package com.google.cloud.tools.jib.plugins.common.logging; import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -29,6 +31,12 @@ */ class AnsiLoggerWithFooter implements ConsoleLogger { + /** + * Maximum width of a footer line. Having width too large can mess up the display when the console + * width is too small. + */ + private static final int MAX_FOOTER_WIDTH = 50; + /** ANSI escape sequence for moving the cursor up one line. */ private static final String CURSOR_UP_SEQUENCE = "\033[1A"; @@ -41,6 +49,27 @@ class AnsiLoggerWithFooter implements ConsoleLogger { /** ANSI escape sequence for setting all further characters to not bold. */ private static final String UNBOLD = "\033[0m"; + /** + * Makes sure each line of text in {@code lines} is at most {@link #MAX_FOOTER_WIDTH} characters + * long. If a line of text exceeds {@link #MAX_FOOTER_WIDTH} characters, the line is truncated to + * {@link #MAX_FOOTER_WIDTH} characters with the last 3 characters as {@code ...}. + * + * @param lines the lines of text + * @return the truncated lines of text + */ + @VisibleForTesting + static List truncateToMaxWidth(List lines) { + List truncatedLines = new ArrayList<>(); + for (String line : lines) { + if (line.length() > MAX_FOOTER_WIDTH) { + truncatedLines.add(line.substring(0, MAX_FOOTER_WIDTH - 3) + "..."); + } else { + truncatedLines.add(line); + } + } + return truncatedLines; + } + private final ImmutableMap> messageConsumers; private final Consumer lifecycleConsumer; private final SingleThreadedExecutor singleThreadedExecutor; @@ -95,7 +124,9 @@ public void log(Level logLevel, String message) { */ @Override public void setFooter(List newFooterLines) { - if (newFooterLines.equals(footerLines)) { + List truncatedNewFooterLines = truncateToMaxWidth(newFooterLines); + + if (truncatedNewFooterLines.equals(footerLines)) { return; } @@ -106,12 +137,12 @@ public void setFooter(List newFooterLines) { // If a previous footer was erased, the first new footer line needs to go up a line. String newFooterPrefix = didErase ? CURSOR_UP_SEQUENCE : ""; - for (String newFooterLine : newFooterLines) { + for (String newFooterLine : truncatedNewFooterLines) { lifecycleConsumer.accept(newFooterPrefix + BOLD + newFooterLine + UNBOLD); newFooterPrefix = ""; } - footerLines = newFooterLines; + footerLines = truncatedNewFooterLines; }); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java index a32c1c2e8a..39b6b82cb6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java @@ -39,7 +39,7 @@ public class ProgressDisplayGenerator { private static final String HEADER = "Executing tasks:"; /** Maximum number of bars in the progress display. */ - private static final int PROGRESS_BAR_COUNT = 50; + private static final int PROGRESS_BAR_COUNT = 30; /** * Generates a progress display. diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index 79ea315722..b01d4895ff 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -55,6 +55,19 @@ public void setUp() { new AnsiLoggerWithFooter(messageConsumers.build(), singleThreadedExecutor); } + @Test + public void testTruncateToMaxWidth() { + List lines = + Arrays.asList( + "this line of text is way too long and will be truncated", + "this line will not be truncated"); + Assert.assertEquals( + Arrays.asList( + "this line of text is way too long and will be t...", + "this line will not be truncated"), + AnsiLoggerWithFooter.truncateToMaxWidth(lines)); + } + @Test public void testNoLifecycle() { try { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java index 157b388a84..0795a47eec 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java @@ -28,27 +28,21 @@ public class ProgressDisplayGeneratorTest { @Test public void testGenerateProgressDisplay_progressBar_0() { Assert.assertEquals( - Arrays.asList( - "Executing tasks:", - "[ ] 0.0% complete"), + Arrays.asList("Executing tasks:", "[ ] 0.0% complete"), ProgressDisplayGenerator.generateProgressDisplay(0, Collections.emptyList())); } @Test public void testGenerateProgressDisplay_progressBar_50() { Assert.assertEquals( - Arrays.asList( - "Executing tasks:", - "[========================= ] 50.0% complete"), + Arrays.asList("Executing tasks:", "[=============== ] 50.0% complete"), ProgressDisplayGenerator.generateProgressDisplay(0.5, Collections.emptyList())); } @Test public void testGenerateProgressDisplay_progressBar_100() { Assert.assertEquals( - Arrays.asList( - "Executing tasks:", - "[==================================================] 100.0% complete"), + Arrays.asList("Executing tasks:", "[==============================] 100.0% complete"), ProgressDisplayGenerator.generateProgressDisplay(1, Collections.emptyList())); } @@ -62,7 +56,7 @@ public void testGenerateProgressDisplay_unfinishedTasks() { Assert.assertEquals( Arrays.asList( "Executing tasks:", - "[========================= ] 50.0% complete", + "[=============== ] 50.0% complete", "> childLeftDown", "> childRight"), ProgressDisplayGenerator.generateProgressDisplay( From 48958f2863a8add38af9fe6a6ac0844124ce9a57 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 14 Jan 2019 15:45:56 -0500 Subject: [PATCH 0440/2020] Updates CHANGELOG for shortened progress display. (#1414) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 87e7e6170d..0f997bd704 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + ### Fixed ## 1.0.0-rc2 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b2f168ac49..9e55272338 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + ### Fixed ## 1.0.0-rc2 From 816829f56c05bb5a969cfedf377ead610f367d8e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Jan 2019 11:08:00 -0500 Subject: [PATCH 0441/2020] Replace javassist with org.ow2.asm (#1413) --- jib-core/build.gradle | 2 +- .../tools/jib/frontend/MainClassFinder.java | 80 +++++++++++-------- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 6 +- jib-plugins-common/build.gradle | 2 +- 5 files changed, 54 insertions(+), 38 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index ac0d789809..f46a4f60c6 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:23.5-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - implementation 'org.javassist:javassist:3.24.1-GA' + implementation 'org.ow2.asm:asm:7.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index dff27c699c..a0152ee260 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -22,17 +22,18 @@ import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.CtMethod; -import javassist.NotFoundException; +import javax.annotation.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; /** * Finds main classes in a list of class files. Main classes are classes that define the {@code @@ -106,6 +107,35 @@ public List getFoundMainClasses() { } } + /** {@link ClassVisitor} that keeps track of whether or not it has visited a main class. */ + private static class MainClassVisitor extends ClassVisitor { + + /** The return/argument types for main. */ + private static final String MAIN_DESCRIPTOR = + Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String[].class)); + + /** Accessors that main may or may not have. */ + private static final int OPTIONAL_ACCESS = Opcodes.ACC_FINAL | Opcodes.ACC_DEPRECATED; + + private boolean visitedMainClass; + + private MainClassVisitor() { + super(Opcodes.ASM7); + } + + @Override + @Nullable + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if ((access & ~OPTIONAL_ACCESS) == (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC) + && name.equals("main") + && descriptor.equals(MAIN_DESCRIPTOR)) { + visitedMainClass = true; + } + return null; + } + } + private final ImmutableList files; private final EventDispatcher eventDispatcher; @@ -157,35 +187,21 @@ private Optional findMainClass(Path file) { return Optional.empty(); } - ClassPool classPool = new ClassPool(); - classPool.appendSystemPath(); - - try { - CtClass[] mainMethodParams = new CtClass[] {classPool.get("java.lang.String[]")}; - - try (InputStream classFileInputStream = Files.newInputStream(file)) { - CtClass fileClass = classPool.makeClass(classFileInputStream); - - // Check if class contains 'psvm' (see class javadoc). - CtMethod mainMethod = fileClass.getDeclaredMethod("main", mainMethodParams); - - if (CtClass.voidType.equals(mainMethod.getReturnType()) - && Modifier.isStatic(mainMethod.getModifiers()) - && Modifier.isPublic(mainMethod.getModifiers())) { - return Optional.of(fileClass.getName()); - } - - } catch (NotFoundException ex) { - // Ignores main method not found. - - } catch (IOException ex) { - // Could not read class file. - eventDispatcher.dispatch(LogEvent.warn("Could not read file: " + file)); + MainClassVisitor mainClassVisitor = new MainClassVisitor(); + try (InputStream classFileInputStream = Files.newInputStream(file)) { + ClassReader reader = new ClassReader(classFileInputStream); + reader.accept(mainClassVisitor, 0); + if (mainClassVisitor.visitedMainClass) { + return Optional.of(reader.getClassName().replace('/', '.')); } - } catch (NotFoundException ex) { - // Thrown if 'java.lang.String' is not found in classPool. - throw new RuntimeException(ex); + } catch (ArrayIndexOutOfBoundsException ignored) { + // Not a valid class file (thrown by ClassReader if it reads an invalid format) + eventDispatcher.dispatch(LogEvent.warn("Invalid class file found: " + file)); + + } catch (IOException ignored) { + // Could not read class file. + eventDispatcher.dispatch(LogEvent.warn("Could not read file: " + file)); } return Optional.empty(); diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 4dc53b9b7b..109694627b 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -65,7 +65,7 @@ dependencies { compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - compile 'org.javassist:javassist:3.24.1-GA' + compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 22fa363113..60fd566fba 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -75,9 +75,9 @@ compile - org.javassist - javassist - 3.24.1-GA + org.ow2.asm + asm + 7.0 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 62d4dbb74d..9b7354d172 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -33,7 +33,7 @@ dependencies { compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - compile 'org.javassist:javassist:3.24.1-GA' + compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' From b8c1b795a448cf242e3a0b658fdcea55aea01bba Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Jan 2019 11:36:20 -0500 Subject: [PATCH 0442/2020] Core release v0.1.1 (#1425) * [Gradle Release Plugin] - pre tag commit: 'v0.1.1-core'. * [Gradle Release Plugin] - new version commit: 'v0.1.2-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 29ea34fd9d..742f39eff8 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.1-SNAPSHOT +version = 0.1.2-SNAPSHOT From d4e2a1939299149937e21e61f82160ebf9a033cb Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Jan 2019 12:55:46 -0500 Subject: [PATCH 0443/2020] Update core changelog for 0.1.1 (#1426) --- jib-core/CHANGELOG.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 5f5d6a03f6..c9f9590655 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,9 +5,13 @@ All notable changes to this project will be documented in this file. ### Added -- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) -- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) - ### Changed -### Fixed \ No newline at end of file +### Fixed + +## 0.1.1 + +### Added + +- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) \ No newline at end of file From 2e728b060fa7a91c4c8cbbd777c8bcce9ebbfd5c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Jan 2019 16:42:26 -0500 Subject: [PATCH 0444/2020] Rename test resource directories to avoid overlaps between plugins and core (#1424) --- .../builder/BuildStepsIntegrationTest.java | 8 ++-- .../jib/api/JavaContainerBuilderTest.java | 26 ++++++------- .../google/cloud/tools/jib/blob/BlobTest.java | 2 +- ...BuildAndCacheApplicationLayerStepTest.java | 14 ++++--- .../configuration/LayerConfigurationTest.java | 4 +- .../docker/ImageToTarballTranslatorTest.java | 4 +- .../json/DockerLoadManifestTemplateTest.java | 2 +- .../jib/filesystem/DirectoryWalkerTest.java | 2 +- .../jib/filesystem/FileOperationsTest.java | 6 +-- .../filesystem/TemporaryDirectoryTest.java | 2 +- .../frontend/JavaLayerConfigurationsTest.java | 4 +- .../jib/frontend/MainClassFinderTest.java | 19 ++++++---- .../cloud/tools/jib/http/TestWebServer.java | 2 +- .../image/ReproducibleLayerBuilderTest.java | 10 ++--- .../ContainerConfigurationTemplateTest.java | 4 +- .../image/json/ImageToJsonTranslatorTest.java | 6 +-- .../image/json/JsonToImageTranslatorTest.java | 9 +++-- .../image/json/OCIManifestTemplateTest.java | 4 +- .../image/json/V21ManifestTemplateTest.java | 2 +- .../image/json/V22ManifestTemplateTest.java | 4 +- .../jib/json/JsonTemplateMapperTest.java | 8 ++-- .../jib/registry/ManifestPullerTest.java | 4 +- .../jib/registry/ManifestPusherTest.java | 2 +- .../DockerConfigCredentialRetrieverTest.java | 6 +-- .../credentials/DockerConfigTest.java | 18 +++++---- .../tools/jib/tar/TarStreamBuilderTest.java | 6 +-- .../{ => core}/TestWebServer-keystore | Bin .../application/classes/HelloWorld.class | Bin .../{ => core}/application/classes/some.class | 0 .../dependencies/dependency-1.0.0.jar | Bin .../application/dependencies/libraryA.jar | 0 .../application/dependencies/libraryB.jar | 0 .../dependencies/more/dependency-1.0.0.jar | 0 .../application/resources/resourceA | 0 .../application/resources/resourceB | 0 .../{ => core}/application/resources/world | 0 .../dependency-1.0.0-SNAPSHOT.jar | Bin jib-core/src/test/resources/{ => core}/blobA | 0 .../extension/main/MainClass.class | Bin .../extension/pack/Apple.class | Bin .../extension/pack/Orange.class | Bin .../external-classes/main/MainClass.class | Bin .../imported-methods/main/MainClass.class | Bin .../imported-methods/pack/Apple.class | Bin .../imported-methods/pack/Orange.class | Bin .../inner-classes/HelloWorld$InnerClass.class | Bin .../inner-classes/HelloWorld.class | Bin .../inner-classes/NotMain.class | Bin .../multiple/HelloWorld.class | Bin .../class-finder-tests/multiple/NotMain.class | Bin .../multiple/multi/Layer1Class.class | Bin .../multiple/multi/Layer2Class.class | Bin .../multiple/multi/layered/HelloMoon.class | Bin .../class-finder-tests/no-main/NotMain.class | Bin .../no-main/multi/Layer1Class.class | Bin .../no-main/multi/Layer2Class.class | Bin .../simple/HelloWorld.class | Bin .../simple/NotAClassButMightBe.class | 0 .../simple/NotEvenAClass.txt | 0 .../class-finder-tests/simple/NotMain.class | Bin .../subdirectories/multi/Layer1Class.class | Bin .../multi/layered/HelloWorld.class | Bin .../multi/layered/Layer2Class.class | Bin .../resources/{ => core}/directoryA/.gitkeep | 0 jib-core/src/test/resources/{ => core}/fileA | 0 jib-core/src/test/resources/{ => core}/fileB | 0 .../test/resources/{ => core}/json/basic.json | 0 .../resources/{ => core}/json/basic_list.json | 0 .../{ => core}/json/containerconfig.json | 0 .../{ => core}/json/dockerconfig.json | 0 .../json/dockerconfig_extra_matches.json | 0 .../json/dockerconfig_index_docker_io_v1.json | 0 .../{ => core}/json/loadmanifest.json | 0 .../{ => core}/json/loadmanifest_notag.json | 0 .../{ => core}/json/metadata-v2.json | 0 .../{ => core}/json/metadata-v3.json | 0 .../resources/{ => core}/json/metadata.json | 0 .../{ => core}/json/metadata_corrupted.json | 0 .../{ => core}/json/metadata_windows-v2.json | 0 .../{ => core}/json/metadata_windows.json | 0 .../{ => core}/json/ocimanifest.json | 0 .../json/translated_ocimanifest.json | 0 .../json/translated_v22manifest.json | 0 .../{ => core}/json/v21manifest.json | 0 .../{ => core}/json/v22manifest.json | 0 .../test/resources/{ => core}/layer/a/b/bar | 0 .../src/test/resources/{ => core}/layer/c/cat | 0 .../src/test/resources/{ => core}/layer/foo | 0 .../{ => core}/random-contents/file1 | 0 .../{ => core}/random-contents/file2 | 0 .../random-contents/sub-directory/file3 | 0 .../random-contents/sub-directory/file4 | 0 .../random-contents/sub-directory/leaf/file5 | 0 .../random-contents/sub-directory/leaf/file6 | 0 .../{ => core}/webAppSampleDockerfile | 0 .../projects/default-target/build.gradle | 0 .../projects/default-target/gradle.properties | 0 .../default-target/libs/dependency-1.0.0.jar | Bin .../projects/default-target/settings.gradle | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../default-target/src/main/resources/world | 0 .../projects/empty/build-broken-user.gradle | 0 .../{ => gradle}/projects/empty/build.gradle | 0 .../empty/src/main/java/com/test/Empty.java | 0 .../multiproject/a_packaged/build.gradle | 0 .../src/main/java/com/test/Empty.java | 0 .../multiproject/b_dependency/build.gradle | 0 .../src/main/java/com/test/Empty.java | 0 .../projects/multiproject/build.gradle | 0 .../projects/multiproject/settings.gradle | 0 .../projects/simple/build-dockerclient.gradle | 0 .../{ => gradle}/projects/simple/build.gradle | 0 .../projects/simple/complex-build.gradle | 0 .../projects/simple/libs/dependency-1.0.0.jar | Bin .../projects/simple/mock-docker.sh | 0 .../simple/src/main/custom-extra-dir/bar/cat | 0 .../simple/src/main/custom-extra-dir/foo | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../projects/simple/src/main/resources/world | 0 .../war_servlet25/build-tomcat.gradle | 0 .../projects/war_servlet25/build.gradle | 0 .../war_servlet25/src/extra_js/bogus.js | 0 .../war_servlet25/src/extra_static/bogus.html | 0 .../src/main/java/example/HelloWorld.java | 0 .../war_servlet25/src/main/resources/world | 0 .../src/main/webapp/META-INF/MANIFEST.MF | 0 .../src/main/webapp/WEB-INF/web.xml | 0 .../war_servlet25/src/main/webapp/index.html | 0 .../gradle/GradleLayerConfigurationsTest.java | 35 +++++++++++------- .../cloud/tools/jib/gradle/JibPluginTest.java | 4 +- .../cloud/tools/jib/gradle/TestProject.java | 2 +- .../application/classes/HelloWorld.class | Bin .../application/classes/some.class | 0 .../another/one/dependency-1.0.0.jar | Bin .../dependencies/dependency-1.0.0.jar | Bin .../dependencyX-1.0.0-SNAPSHOT.jar | Bin .../application/dependencies/library.jarC.jar | 0 .../application/dependencies/libraryA.jar | 0 .../application/dependencies/libraryB.jar | 0 .../dependencies/more/dependency-1.0.0.jar | 0 .../application/resources/resourceA | 0 .../application/resources/resourceB | 0 .../{ => gradle}/application/resources/world | 0 .../{ => gradle}/plugin-test/build.gradle | 0 .../projects/multi-service/build.gradle | 0 .../complex-service/build.gradle | 0 .../src/main/extra-resources-1/resource1.txt | 0 .../src/main/extra-resources-2/resource2.txt | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../src/main/other-jib/extra-file | 0 .../projects/multi-service/gradle.properties | 0 .../projects/multi-service/lib/build.gradle | 0 .../lib/src/main/java/com/lib/Lib.java | 0 .../lib/src/main/resources/hi.txt | 0 .../lib/src/test/java/com/lib/LibTest.java | 0 .../projects/multi-service/settings.gradle | 0 .../multi-service/simple-service/build.gradle | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../{ => gradle}/projects/simple/build.gradle | 0 .../projects/simple/libs/dependency-1.0.0.jar | Bin .../simple/src/main/custom-extra-dir/bar/cat | 0 .../simple/src/main/custom-extra-dir/foo | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../projects/simple/src/main/resources/world | 0 .../jib-exploded-war/META-INF/context.xml | 0 .../webapp/jib-exploded-war/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 0 .../WEB-INF/classes/package/Other.class | 0 .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin .../webapp/jib-exploded-war/WEB-INF/web.xml | 0 .../maven/MavenLayerConfigurationsTest.java | 16 ++++---- .../cloud/tools/jib/maven/TestProject.java | 2 +- .../cloud/tools/jib/maven/TestRepository.java | 2 +- .../src/test/resources/layer/a/b/bar | 1 - .../src/test/resources/layer/c/cat | 1 - jib-maven-plugin/src/test/resources/layer/foo | 1 - .../another/one/dependency-1.0.0.jar | Bin .../dependencies/dependency-1.0.0.jar | Bin .../dependencyX-1.0.0-SNAPSHOT.jar | Bin .../application/dependencies/library.jarC.jar | 0 .../application/dependencies/libraryA.jar | 0 .../application/dependencies/libraryB.jar | 0 .../dependencies/more/dependency-1.0.0.jar | 0 .../application/output/HelloWorld.class | Bin .../application/output/directory/somefile | 0 .../application/output/package/some.class | 0 .../{ => maven}/application/output/resourceA | 0 .../{ => maven}/application/output/resourceB | 0 .../{ => maven}/application/output/some.class | 0 .../{ => maven}/application/output/world | 0 .../application/source/HelloWorld.java | 0 .../application/source/package/some.java | 0 .../{ => maven}/application/source/some.java | 0 .../default-target/libs/dependency-1.0.0.jar | Bin .../projects/default-target/pom.xml | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../default-target/src/main/resources/world | 0 .../projects/empty/pom-broken-user.xml | 0 .../{ => maven}/projects/empty/pom.xml | 0 .../empty/src/main/java/com/test/Empty.java | 0 .../projects/multi/complex-service/pom.xml | 0 .../src/main/extra-resources-1/resource1.txt | 0 .../src/main/extra-resources-2/resource2.txt | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../{ => maven}/projects/multi/lib/pom.xml | 0 .../multi/lib/src/main/java/com/lib/Lib.java | 0 .../lib/src/test/java/com/lib/LibTest.java | 0 .../{ => maven}/projects/multi/pom.xml | 0 .../projects/multi/simple-service/pom.xml | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../projects/simple/libs/dependency-1.0.0.jar | Bin .../projects/simple/mock-docker.sh | 0 .../simple/pom-complex-properties.xml | 0 .../projects/simple/pom-complex.xml | 0 .../projects/simple/pom-dockerclient.xml | 0 .../projects/simple/pom-no-to-image.xml | 0 .../{ => maven}/projects/simple/pom.xml | 0 .../src/main/java/com/test/HelloWorld.java | 0 .../simple/src/main/jib-custom/bar/cat | 0 .../projects/simple/src/main/jib-custom/foo | 0 .../projects/simple/src/main/resources/world | 0 .../projects/war_servlet25/pom-tomcat.xml | 0 .../projects/war_servlet25/pom.xml | 0 .../war_servlet25/src/extra_js/bogus.js | 0 .../war_servlet25/src/extra_static/bogus.html | 0 .../src/main/java/example/HelloWorld.java | 0 .../war_servlet25/src/main/resources/world | 0 .../src/main/webapp/META-INF/MANIFEST.MF | 0 .../src/main/webapp/WEB-INF/web.xml | 0 .../war_servlet25/src/main/webapp/index.html | 0 .../dependency/1.0.0/_remote.repositories | 0 .../dependency/1.0.0/dependency-1.0.0.jar | Bin .../dependency/1.0.0/dependency-1.0.0.pom | 0 .../test/dependency/maven-metadata-local.xml | 0 .../1.0.0-SNAPSHOT/_remote.repositories | 0 .../dependencyX-1.0.0-SNAPSHOT.jar | Bin .../dependencyX-1.0.0-SNAPSHOT.pom | 0 .../1.0.0-SNAPSHOT/maven-metadata-local.xml | 0 .../test/dependencyX/maven-metadata-local.xml | 0 .../webapp/final-name/META-INF/context.xml | 0 .../{ => maven}/webapp/final-name/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 0 .../WEB-INF/classes/package/Other.class | 0 .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin .../webapp/final-name/WEB-INF/web.xml | 0 .../JavaLayerConfigurationsHelperTest.java | 5 ++- .../plugins/common/MainClassResolverTest.java | 4 +- .../tools/jib/plugins/common/ZipUtilTest.java | 9 +++-- .../exploded-war/META-INF/context.xml | 0 .../exploded-war/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 0 .../WEB-INF/classes/package/Other.class | 0 .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin .../exploded-war/WEB-INF/web.xml | 0 .../test-archives/test.zip | Bin .../test-archives/zip-slip-win.zip | Bin .../test-archives/zip-slip.zip | Bin 263 files changed, 140 insertions(+), 120 deletions(-) rename jib-core/src/test/resources/{ => core}/TestWebServer-keystore (100%) rename jib-core/src/test/resources/{ => core}/application/classes/HelloWorld.class (100%) rename jib-core/src/test/resources/{ => core}/application/classes/some.class (100%) rename jib-core/src/test/resources/{ => core}/application/dependencies/dependency-1.0.0.jar (100%) rename jib-core/src/test/resources/{ => core}/application/dependencies/libraryA.jar (100%) rename jib-core/src/test/resources/{ => core}/application/dependencies/libraryB.jar (100%) rename jib-core/src/test/resources/{ => core}/application/dependencies/more/dependency-1.0.0.jar (100%) rename jib-core/src/test/resources/{ => core}/application/resources/resourceA (100%) rename jib-core/src/test/resources/{ => core}/application/resources/resourceB (100%) rename jib-core/src/test/resources/{ => core}/application/resources/world (100%) rename jib-core/src/test/resources/{ => core}/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar (100%) rename jib-core/src/test/resources/{ => core}/blobA (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/extension/main/MainClass.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/extension/pack/Apple.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/extension/pack/Orange.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/external-classes/main/MainClass.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/imported-methods/main/MainClass.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/imported-methods/pack/Apple.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/imported-methods/pack/Orange.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/inner-classes/HelloWorld$InnerClass.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/inner-classes/HelloWorld.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/inner-classes/NotMain.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/multiple/HelloWorld.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/multiple/NotMain.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/multiple/multi/Layer1Class.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/multiple/multi/Layer2Class.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/multiple/multi/layered/HelloMoon.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/no-main/NotMain.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/no-main/multi/Layer1Class.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/no-main/multi/Layer2Class.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/simple/HelloWorld.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/simple/NotAClassButMightBe.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/simple/NotEvenAClass.txt (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/simple/NotMain.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/subdirectories/multi/Layer1Class.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/subdirectories/multi/layered/HelloWorld.class (100%) rename jib-core/src/test/resources/{ => core}/class-finder-tests/subdirectories/multi/layered/Layer2Class.class (100%) rename jib-core/src/test/resources/{ => core}/directoryA/.gitkeep (100%) rename jib-core/src/test/resources/{ => core}/fileA (100%) rename jib-core/src/test/resources/{ => core}/fileB (100%) rename jib-core/src/test/resources/{ => core}/json/basic.json (100%) rename jib-core/src/test/resources/{ => core}/json/basic_list.json (100%) rename jib-core/src/test/resources/{ => core}/json/containerconfig.json (100%) rename jib-core/src/test/resources/{ => core}/json/dockerconfig.json (100%) rename jib-core/src/test/resources/{ => core}/json/dockerconfig_extra_matches.json (100%) rename jib-core/src/test/resources/{ => core}/json/dockerconfig_index_docker_io_v1.json (100%) rename jib-core/src/test/resources/{ => core}/json/loadmanifest.json (100%) rename jib-core/src/test/resources/{ => core}/json/loadmanifest_notag.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata-v2.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata-v3.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata_corrupted.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata_windows-v2.json (100%) rename jib-core/src/test/resources/{ => core}/json/metadata_windows.json (100%) rename jib-core/src/test/resources/{ => core}/json/ocimanifest.json (100%) rename jib-core/src/test/resources/{ => core}/json/translated_ocimanifest.json (100%) rename jib-core/src/test/resources/{ => core}/json/translated_v22manifest.json (100%) rename jib-core/src/test/resources/{ => core}/json/v21manifest.json (100%) rename jib-core/src/test/resources/{ => core}/json/v22manifest.json (100%) rename jib-core/src/test/resources/{ => core}/layer/a/b/bar (100%) rename jib-core/src/test/resources/{ => core}/layer/c/cat (100%) rename jib-core/src/test/resources/{ => core}/layer/foo (100%) rename jib-core/src/test/resources/{ => core}/random-contents/file1 (100%) rename jib-core/src/test/resources/{ => core}/random-contents/file2 (100%) rename jib-core/src/test/resources/{ => core}/random-contents/sub-directory/file3 (100%) rename jib-core/src/test/resources/{ => core}/random-contents/sub-directory/file4 (100%) rename jib-core/src/test/resources/{ => core}/random-contents/sub-directory/leaf/file5 (100%) rename jib-core/src/test/resources/{ => core}/random-contents/sub-directory/leaf/file6 (100%) rename jib-core/src/test/resources/{ => core}/webAppSampleDockerfile (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/gradle.properties (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/libs/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/settings.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/src/main/java/com/test/HelloWorld.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/default-target/src/main/resources/world (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/empty/build-broken-user.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/empty/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/empty/src/main/java/com/test/Empty.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/a_packaged/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/b_dependency/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/multiproject/settings.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/build-dockerclient.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/complex-build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/libs/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/mock-docker.sh (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/src/main/custom-extra-dir/bar/cat (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/src/main/custom-extra-dir/foo (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/src/main/java/com/test/HelloWorld.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/simple/src/main/resources/world (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/build-tomcat.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/build.gradle (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/extra_js/bogus.js (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/extra_static/bogus.html (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/main/java/example/HelloWorld.java (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/main/resources/world (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml (100%) rename jib-gradle-plugin/src/integration-test/resources/{ => gradle}/projects/war_servlet25/src/main/webapp/index.html (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/classes/HelloWorld.class (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/classes/some.class (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/another/one/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/library.jarC.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/libraryA.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/libraryB.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/dependencies/more/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/resources/resourceA (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/resources/resourceB (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/application/resources/world (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/plugin-test/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/complex-service/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/complex-service/src/main/other-jib/extra-file (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/gradle.properties (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/lib/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/lib/src/main/java/com/lib/Lib.java (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/lib/src/main/resources/hi.txt (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/lib/src/test/java/com/lib/LibTest.java (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/settings.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/simple-service/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/build.gradle (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/libs/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/src/main/custom-extra-dir/bar/cat (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/src/main/custom-extra-dir/foo (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/src/main/java/com/test/HelloWorld.java (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/projects/simple/src/main/resources/world (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/META-INF/context.xml (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/Test.jsp (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-gradle-plugin/src/test/resources/{ => gradle}/webapp/jib-exploded-war/WEB-INF/web.xml (100%) delete mode 100644 jib-maven-plugin/src/test/resources/layer/a/b/bar delete mode 100644 jib-maven-plugin/src/test/resources/layer/c/cat delete mode 100644 jib-maven-plugin/src/test/resources/layer/foo rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/another/one/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/library.jarC.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/libraryA.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/libraryB.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/dependencies/more/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/HelloWorld.class (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/directory/somefile (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/package/some.class (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/resourceA (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/resourceB (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/some.class (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/output/world (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/source/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/source/package/some.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/application/source/some.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/default-target/libs/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/default-target/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/default-target/src/main/java/com/test/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/default-target/src/main/resources/world (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/empty/pom-broken-user.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/empty/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/empty/src/main/java/com/test/Empty.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/complex-service/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/lib/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/lib/src/main/java/com/lib/Lib.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/lib/src/test/java/com/lib/LibTest.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/simple-service/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/libs/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/mock-docker.sh (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/pom-complex-properties.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/pom-complex.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/pom-dockerclient.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/pom-no-to-image.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/src/main/java/com/test/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/src/main/jib-custom/bar/cat (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/src/main/jib-custom/foo (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/simple/src/main/resources/world (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/pom-tomcat.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/pom.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/extra_js/bogus.js (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/extra_static/bogus.html (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/main/java/example/HelloWorld.java (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/main/resources/world (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/projects/war_servlet25/src/main/webapp/index.html (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependency/1.0.0/_remote.repositories (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependency/maven-metadata-local.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/testM2/com/test/dependencyX/maven-metadata-local.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/META-INF/context.xml (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/Test.jsp (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/classes/HelloWorld.class (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/classes/package/Other.class (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/classes/package/test.properties (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-maven-plugin/src/test/resources/{ => maven}/webapp/final-name/WEB-INF/web.xml (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/META-INF/context.xml (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/Test.jsp (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/classes/HelloWorld.class (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/classes/package/Other.class (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/classes/package/test.properties (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/lib/dependency-1.0.0.jar (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/exploded-war/WEB-INF/web.xml (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/test-archives/test.zip (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/test-archives/zip-slip-win.zip (100%) rename jib-plugins-common/src/test/resources/{ => plugins-common}/test-archives/zip-slip.zip (100%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index 0fdbe739dc..ea269d54ad 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -148,10 +148,12 @@ private static void assertDockerInspect(String imageReference) public void setUp() throws IOException, URISyntaxException { fakeLayerConfigurations = ImmutableList.of( - makeLayerConfiguration("application/dependencies", AbsoluteUnixPath.get("/app/libs/")), makeLayerConfiguration( - "application/resources", AbsoluteUnixPath.get("/app/resources/")), - makeLayerConfiguration("application/classes", AbsoluteUnixPath.get("/app/classes/"))); + "core/application/dependencies", AbsoluteUnixPath.get("/app/libs/")), + makeLayerConfiguration( + "core/application/resources", AbsoluteUnixPath.get("/app/resources/")), + makeLayerConfiguration( + "core/application/classes", AbsoluteUnixPath.get("/app/classes/"))); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 46493641f3..44c07ab44f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -66,15 +66,15 @@ public void testToJibContainerBuilder_all() CacheDirectoryCreationException { BuildConfiguration buildConfiguration = JavaContainerBuilder.fromDistroless() - .addResources(getResource("application/resources")) - .addClasses(getResource("application/classes")) + .addResources(getResource("core/application/resources")) + .addClasses(getResource("core/application/classes")) .addDependencies( - getResource("application/dependencies/dependency-1.0.0.jar"), - getResource("application/dependencies/more/dependency-1.0.0.jar"), - getResource("application/dependencies/libraryA.jar"), - getResource("application/dependencies/libraryB.jar"), - getResource("application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) - .addToClasspath(getResource("fileA"), getResource("fileB")) + getResource("core/application/dependencies/dependency-1.0.0.jar"), + getResource("core/application/dependencies/more/dependency-1.0.0.jar"), + getResource("core/application/dependencies/libraryA.jar"), + getResource("core/application/dependencies/libraryB.jar"), + getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addToClasspath(getResource("core/fileA"), getResource("core/fileB")) .addJvmFlags("-xflag1", "-xflag2") .setMainClass("HelloWorld") .toContainerBuilder() @@ -141,12 +141,12 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() CacheDirectoryCreationException { BuildConfiguration buildConfiguration = JavaContainerBuilder.fromDistroless() - .addDependencies(getResource("application/dependencies/libraryA.jar")) - .addDependencies(getResource("application/dependencies/libraryB.jar")) + .addDependencies(getResource("core/application/dependencies/libraryA.jar")) + .addDependencies(getResource("core/application/dependencies/libraryB.jar")) .addDependencies( - getResource("application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) - .addClasses(getResource("application/classes/")) - .addClasses(getResource("class-finder-tests/extension")) + getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addClasses(getResource("core/application/classes/")) + .addClasses(getResource("core/class-finder-tests/extension")) .setMainClass("HelloWorld") .toContainerBuilder() .toBuildConfiguration( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index 7bee8fe1de..623068664d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -46,7 +46,7 @@ public void testFromInputStream() throws IOException { @Test public void testFromFile() throws IOException, URISyntaxException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); + Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); String expected = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); verifyBlobWriteTo(expected, Blobs.from(fileA)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 912582b02f..7f00635f89 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -100,21 +100,23 @@ private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOExce @Before public void setUp() throws IOException, URISyntaxException { fakeDependenciesLayerConfiguration = - makeLayerConfiguration("application/dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); + makeLayerConfiguration( + "core/application/dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); fakeSnapshotDependenciesLayerConfiguration = makeLayerConfiguration( - "application/snapshot-dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); + "core/application/snapshot-dependencies", EXTRACTION_PATH_ROOT.resolve("libs")); fakeResourcesLayerConfiguration = - makeLayerConfiguration("application/resources", EXTRACTION_PATH_ROOT.resolve("resources")); + makeLayerConfiguration( + "core/application/resources", EXTRACTION_PATH_ROOT.resolve("resources")); fakeClassesLayerConfiguration = - makeLayerConfiguration("application/classes", EXTRACTION_PATH_ROOT.resolve("classes")); + makeLayerConfiguration("core/application/classes", EXTRACTION_PATH_ROOT.resolve("classes")); fakeExtraFilesLayerConfiguration = LayerConfiguration.builder() .addEntry( - Paths.get(Resources.getResource("fileA").toURI()), + Paths.get(Resources.getResource("core/fileA").toURI()), EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileA")) .addEntry( - Paths.get(Resources.getResource("fileB").toURI()), + Paths.get(Resources.getResource("core/fileB").toURI()), EXTRA_FILES_LAYER_EXTRACTION_PATH.resolve("fileB")) .build(); emptyLayerConfiguration = LayerConfiguration.builder().build(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java index ec38245641..9367b2ee31 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -32,8 +32,8 @@ public class LayerConfigurationTest { @Test public void testAddEntryRecursive() throws IOException, URISyntaxException { - Path testDirectory = Paths.get(Resources.getResource("layer").toURI()).toAbsolutePath(); - Path testFile = Paths.get(Resources.getResource("fileA").toURI()); + Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); + Path testFile = Paths.get(Resources.getResource("core/fileA").toURI()); LayerConfiguration layerConfiguration = LayerConfiguration.builder() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java index 564d77736d..0759ef9e18 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java @@ -60,8 +60,8 @@ public class ImageToTarballTranslatorTest { public void testToTarballBlob() throws InvalidImageReferenceException, IOException, URISyntaxException, LayerPropertyNotFoundException, DigestException { - Path fileA = Paths.get(Resources.getResource("fileA").toURI()); - Path fileB = Paths.get(Resources.getResource("fileB").toURI()); + Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); + Path fileB = Paths.get(Resources.getResource("core/fileB").toURI()); long fileASize = Files.size(fileA); long fileBSize = Files.size(fileB); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java index 74e22cfbe6..8669f6467f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java @@ -37,7 +37,7 @@ public class DockerLoadManifestTemplateTest { @Test public void testToJson() throws URISyntaxException, IOException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/loadmanifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/loadmanifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); DockerLoadManifestEntryTemplate template = new DockerLoadManifestEntryTemplate(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java index e37009e3b1..565163eab4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java @@ -38,7 +38,7 @@ public class DirectoryWalkerTest { @Before public void setUp() throws URISyntaxException { - testDir = Paths.get(Resources.getResource("layer").toURI()); + testDir = Paths.get(Resources.getResource("core/layer").toURI()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java index ac751a9ecd..6ca3ee91a3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java @@ -72,10 +72,10 @@ private static void verifyWriteWithLock(Path file) throws IOException { public void testCopy() throws IOException, URISyntaxException { Path destDir = temporaryFolder.newFolder().toPath(); Path libraryA = - Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI()); + Paths.get(Resources.getResource("core/application/dependencies/libraryA.jar").toURI()); Path libraryB = - Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI()); - Path dirLayer = Paths.get(Resources.getResource("layer").toURI()); + Paths.get(Resources.getResource("core/application/dependencies/libraryB.jar").toURI()); + Path dirLayer = Paths.get(Resources.getResource("core/layer").toURI()); FileOperations.copy(ImmutableList.of(libraryA, libraryB, dirLayer), destDir); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java index 1d41843e04..af129c2a0c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java @@ -32,7 +32,7 @@ public class TemporaryDirectoryTest { private static void createFilesInDirectory(Path directory) throws IOException, URISyntaxException { - Path testFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + Path testFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); new DirectoryWalker(testFilesDirectory) .filterRoot() .walk(path -> Files.copy(path, directory.resolve(testFilesDirectory.relativize(path)))); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 636b599c36..6b87188412 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -113,7 +113,7 @@ public void testAddFile() { @Test public void testAddFile_directories() throws URISyntaxException { - Path sourceDirectory = Paths.get(Resources.getResource("random-contents").toURI()); + Path sourceDirectory = Paths.get(Resources.getResource("core/random-contents").toURI()); JavaLayerConfigurations configurations = JavaLayerConfigurations.builder() @@ -152,7 +152,7 @@ public void testAddFile_directories() throws URISyntaxException { @Test public void testAddFile_regularFiles() throws URISyntaxException { Path sourceFile = - Paths.get(Resources.getResource("random-contents/sub-directory/leaf/file6").toURI()); + Paths.get(Resources.getResource("core/random-contents/sub-directory/leaf/file6").toURI()); JavaLayerConfigurations configurations = JavaLayerConfigurations.builder() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 9fe35aa93a..7e84e0e591 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -39,7 +39,7 @@ public class MainClassFinderTest { @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); + Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); @@ -50,7 +50,7 @@ public void testFindMainClass_simple() throws URISyntaxException, IOException { @Test public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); + Paths.get(Resources.getResource("core/class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); @@ -61,7 +61,8 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce @Test public void testFindMainClass_noClass() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); @@ -69,7 +70,8 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { @Test public void testFindMainClass_multiple() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertEquals( @@ -82,7 +84,8 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException @Test public void testFindMainClass_extension() throws URISyntaxException, IOException { - Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/extension").toURI()); + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); @@ -93,7 +96,7 @@ public void testFindMainClass_extension() throws URISyntaxException, IOException @Test public void testFindMainClass_importedMethods() throws URISyntaxException, IOException { Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/imported-methods").toURI()); + Paths.get(Resources.getResource("core/class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); @@ -104,7 +107,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc @Test public void testFindMainClass_externalClasses() throws URISyntaxException, IOException { Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/external-classes").toURI()); + Paths.get(Resources.getResource("core/class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); @@ -115,7 +118,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc @Test public void testFindMainClass_innerClasses() throws URISyntaxException, IOException { Path rootDirectory = - Paths.get(Resources.getResource("class-finder-tests/inner-classes").toURI()); + Paths.get(Resources.getResource("core/class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 3561e58b00..38a1bb2e0f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -68,7 +68,7 @@ private ServerSocket createServerSocket(boolean https) if (https) { KeyStore keyStore = KeyStore.getInstance("JKS"); // generated with: keytool -genkey -keyalg RSA -keystore ./TestWebServer-keystore - Path keyStoreFile = Paths.get(Resources.getResource("TestWebServer-keystore").toURI()); + Path keyStoreFile = Paths.get(Resources.getResource("core/TestWebServer-keystore").toURI()); try (InputStream in = Files.newInputStream(keyStoreFile)) { keyStore.load(in, "password".toCharArray()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 9dffc527ae..8c79a8dcc0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -95,8 +95,8 @@ private static void verifyNextTarArchiveEntryIsDirectory( @Test public void testBuild() throws URISyntaxException, IOException { - Path layerDirectory = Paths.get(Resources.getResource("layer").toURI()); - Path blobA = Paths.get(Resources.getResource("blobA").toURI()); + Path layerDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + Path blobA = Paths.get(Resources.getResource("core/blobA").toURI()); ReproducibleLayerBuilder layerBuilder = new ReproducibleLayerBuilder( @@ -129,16 +129,16 @@ public void testBuild() throws URISyntaxException, IOException { verifyNextTarArchiveEntry( tarArchiveInputStream, "extract/here/apple/layer/a/b/bar", - Paths.get(Resources.getResource("layer/a/b/bar").toURI())); + Paths.get(Resources.getResource("core/layer/a/b/bar").toURI())); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); verifyNextTarArchiveEntry( tarArchiveInputStream, "extract/here/apple/layer/c/cat", - Paths.get(Resources.getResource("layer/c/cat").toURI())); + Paths.get(Resources.getResource("core/layer/c/cat").toURI())); verifyNextTarArchiveEntry( tarArchiveInputStream, "extract/here/apple/layer/foo", - Paths.get(Resources.getResource("layer/foo").toURI())); + Paths.get(Resources.getResource("core/layer/foo").toURI())); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/banana/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/banana/blobA", blobA); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index e688d2ede4..e5dd9eb81d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -41,7 +41,7 @@ public class ContainerConfigurationTemplateTest { @Test public void testToJson() throws IOException, URISyntaxException, DigestException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. @@ -98,7 +98,7 @@ public void testToJson() throws IOException, URISyntaxException, DigestException @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); // Deserializes into a manifest JSON object. ContainerConfigurationTemplate containerConfigJson = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 9a7fe5f093..666c95ce7b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -121,7 +121,7 @@ public void testGetContainerConfiguration() setUp(V22ManifestTemplate.class); // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/containerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the container configuration and writes the JSON string. @@ -137,13 +137,13 @@ public void testGetContainerConfiguration() @Test public void testGetManifest_v22() throws URISyntaxException, IOException, DigestException { setUp(V22ManifestTemplate.class); - testGetManifest(V22ManifestTemplate.class, "json/translated_v22manifest.json"); + testGetManifest(V22ManifestTemplate.class, "core/json/translated_v22manifest.json"); } @Test public void testGetManifest_oci() throws URISyntaxException, IOException, DigestException { setUp(OCIManifestTemplate.class); - testGetManifest(OCIManifestTemplate.class, "json/translated_ocimanifest.json"); + testGetManifest(OCIManifestTemplate.class, "core/json/translated_ocimanifest.json"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index d3188ce63d..7eea1d466e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -50,7 +50,7 @@ public void testToImage_v21() throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException { // Loads the JSON string. Path jsonFile = - Paths.get(getClass().getClassLoader().getResource("json/v21manifest.json").toURI()); + Paths.get(getClass().getClassLoader().getResource("core/json/v21manifest.json").toURI()); // Deserializes into a manifest JSON object. V21ManifestTemplate manifestTemplate = @@ -70,14 +70,14 @@ public void testToImage_v21() public void testToImage_v22() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { - testToImage_buildable("json/v22manifest.json", V22ManifestTemplate.class); + testToImage_buildable("core/json/v22manifest.json", V22ManifestTemplate.class); } @Test public void testToImage_oci() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { - testToImage_buildable("json/ocimanifest.json", OCIManifestTemplate.class); + testToImage_buildable("core/json/ocimanifest.json", OCIManifestTemplate.class); } @Test @@ -164,7 +164,8 @@ private void testToImage_buildable( DigestException, URISyntaxException, BadContainerConfigurationFormatException { // Loads the container configuration JSON. Path containerConfigurationJsonFile = - Paths.get(getClass().getClassLoader().getResource("json/containerconfig.json").toURI()); + Paths.get( + getClass().getClassLoader().getResource("core/json/containerconfig.json").toURI()); ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJsonFromFile( containerConfigurationJsonFile, ContainerConfigurationTemplate.class); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java index 15c5e0dbf9..40ff8e4fc1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java @@ -36,7 +36,7 @@ public class OCIManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/ocimanifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. @@ -62,7 +62,7 @@ public void testToJson() throws DigestException, IOException, URISyntaxException @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/ocimanifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); // Deserializes into a manifest JSON object. OCIManifestTemplate manifestJson = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index 95db92aeda..27a1b47f2b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -33,7 +33,7 @@ public class V21ManifestTemplateTest { @Test public void testFromJson() throws URISyntaxException, IOException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v21manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); // Deserializes into a manifest JSON object. V21ManifestTemplate manifestJson = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index 91f4f0f99a..58eb1a3486 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -36,7 +36,7 @@ public class V22ManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. @@ -62,7 +62,7 @@ public void testToJson() throws DigestException, IOException, URISyntaxException @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); // Deserializes into a manifest JSON object. V22ManifestTemplate manifestJson = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 307e4afe6e..1da54bad3d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -54,7 +54,7 @@ private static class InnerObject implements JsonTemplate { @Test public void testWriteJson() throws DigestException, IOException, URISyntaxException { - Path jsonFile = Paths.get(Resources.getResource("json/basic.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/basic.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); TestJson testJson = new TestJson(); @@ -93,7 +93,7 @@ public void testWriteJson() throws DigestException, IOException, URISyntaxExcept @Test public void testReadJsonWithLock() throws IOException, URISyntaxException, DigestException { - Path jsonFile = Paths.get(Resources.getResource("json/basic.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/basic.json").toURI()); // Deserializes into a metadata JSON object. TestJson testJson = JsonTemplateMapper.readJsonFromFileWithLock(jsonFile, TestJson.class); @@ -122,7 +122,7 @@ public void testReadJsonWithLock() throws IOException, URISyntaxException, Diges @Test public void testReadListOfJson() throws IOException, URISyntaxException, DigestException { - Path jsonFile = Paths.get(Resources.getResource("json/basic_list.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/basic_list.json").toURI()); String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); List listofJsons = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); @@ -150,7 +150,7 @@ public void testReadListOfJson() throws IOException, URISyntaxException, DigestE @Test public void testToBlob_listOfJson() throws IOException, URISyntaxException { - Path jsonFile = Paths.get(Resources.getResource("json/basic_list.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/basic_list.json").toURI()); String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); List listOfJson = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index 57d10cd471..05747193e3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -55,7 +55,7 @@ public class ManifestPullerTest { @Test public void testHandleResponse_v21() throws URISyntaxException, IOException, UnknownManifestFormatException { - Path v21ManifestFile = Paths.get(Resources.getResource("json/v21manifest.json").toURI()); + Path v21ManifestFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v21ManifestFile)); ManifestTemplate manifestTemplate = @@ -69,7 +69,7 @@ public void testHandleResponse_v21() @Test public void testHandleResponse_v22() throws URISyntaxException, IOException, UnknownManifestFormatException { - Path v22ManifestFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + Path v22ManifestFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v22ManifestFile)); ManifestTemplate manifestTemplate = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index c2a3697423..57b8fdb0f8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -61,7 +61,7 @@ public class ManifestPusherTest { @Before public void setUp() throws URISyntaxException, IOException { - v22manifestJsonFile = Paths.get(Resources.getResource("json/v22manifest.json").toURI()); + v22manifestJsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); fakeManifestTemplate = JsonTemplateMapper.readJsonFromFile(v22manifestJsonFile, V22ManifestTemplate.class); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 3d3d0103ae..af60a7d55d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -46,7 +46,7 @@ public class DockerConfigCredentialRetrieverTest { public void setUp() throws URISyntaxException, CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { - dockerConfigFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + dockerConfigFile = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIAL); } @@ -136,7 +136,7 @@ public void testRetrieve_credentialFromAlias() { @Test public void testRetrieve_suffixMatching() throws IOException, URISyntaxException { Path dockerConfigFile = - Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); + Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); @@ -150,7 +150,7 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException @Test public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntaxException { Path dockerConfigFile = - Paths.get(Resources.getResource("json/dockerconfig_index_docker_io_v1.json").toURI()); + Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index 8a13d2d473..b971c1d430 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -38,7 +38,7 @@ private static String decodeBase64(String base64String) { @Test public void test_fromJson() throws URISyntaxException, IOException { // Loads the JSON string. - Path jsonFile = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path jsonFile = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); // Deserializes into a docker config JSON object. DockerConfig dockerConfig = @@ -71,7 +71,8 @@ public void test_fromJson() throws URISyntaxException, IOException { @Test public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); + Path json = + Paths.get(Resources.getResource("core/json/dockerconfig_extra_matches.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -87,7 +88,8 @@ public void testGetAuthFor_orderOfMatchPreference() throws URISyntaxException, I @Test public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig_extra_matches.json").toURI()); + Path json = + Paths.get(Resources.getResource("core/json/dockerconfig_extra_matches.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -97,7 +99,7 @@ public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IO @Test public void testGetCredentialHelperFor() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -109,7 +111,7 @@ public void testGetCredentialHelperFor() throws URISyntaxException, IOException @Test public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -121,7 +123,7 @@ public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IO @Test public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -134,7 +136,7 @@ public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, I @Test public void testGetCredentialHelperFor_withProtocolAndSuffix() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); @@ -147,7 +149,7 @@ public void testGetCredentialHelperFor_withProtocolAndSuffix() @Test public void testGetCredentialHelperFor_correctSuffixMatching() throws URISyntaxException, IOException { - Path json = Paths.get(Resources.getResource("json/dockerconfig.json").toURI()); + Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index 4486e9d514..f119b892dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -51,9 +51,9 @@ public class TarStreamBuilderTest { @Before public void setup() throws URISyntaxException, IOException { // Gets the test resource files. - fileA = Paths.get(Resources.getResource("fileA").toURI()); - fileB = Paths.get(Resources.getResource("fileB").toURI()); - directoryA = Paths.get(Resources.getResource("directoryA").toURI()); + fileA = Paths.get(Resources.getResource("core/fileA").toURI()); + fileB = Paths.get(Resources.getResource("core/fileB").toURI()); + directoryA = Paths.get(Resources.getResource("core/directoryA").toURI()); fileAContents = Files.readAllBytes(fileA); fileBContents = Files.readAllBytes(fileB); diff --git a/jib-core/src/test/resources/TestWebServer-keystore b/jib-core/src/test/resources/core/TestWebServer-keystore similarity index 100% rename from jib-core/src/test/resources/TestWebServer-keystore rename to jib-core/src/test/resources/core/TestWebServer-keystore diff --git a/jib-core/src/test/resources/application/classes/HelloWorld.class b/jib-core/src/test/resources/core/application/classes/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/application/classes/HelloWorld.class rename to jib-core/src/test/resources/core/application/classes/HelloWorld.class diff --git a/jib-core/src/test/resources/application/classes/some.class b/jib-core/src/test/resources/core/application/classes/some.class similarity index 100% rename from jib-core/src/test/resources/application/classes/some.class rename to jib-core/src/test/resources/core/application/classes/some.class diff --git a/jib-core/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-core/src/test/resources/core/application/dependencies/dependency-1.0.0.jar similarity index 100% rename from jib-core/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-core/src/test/resources/core/application/dependencies/dependency-1.0.0.jar diff --git a/jib-core/src/test/resources/application/dependencies/libraryA.jar b/jib-core/src/test/resources/core/application/dependencies/libraryA.jar similarity index 100% rename from jib-core/src/test/resources/application/dependencies/libraryA.jar rename to jib-core/src/test/resources/core/application/dependencies/libraryA.jar diff --git a/jib-core/src/test/resources/application/dependencies/libraryB.jar b/jib-core/src/test/resources/core/application/dependencies/libraryB.jar similarity index 100% rename from jib-core/src/test/resources/application/dependencies/libraryB.jar rename to jib-core/src/test/resources/core/application/dependencies/libraryB.jar diff --git a/jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar similarity index 100% rename from jib-core/src/test/resources/application/dependencies/more/dependency-1.0.0.jar rename to jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar diff --git a/jib-core/src/test/resources/application/resources/resourceA b/jib-core/src/test/resources/core/application/resources/resourceA similarity index 100% rename from jib-core/src/test/resources/application/resources/resourceA rename to jib-core/src/test/resources/core/application/resources/resourceA diff --git a/jib-core/src/test/resources/application/resources/resourceB b/jib-core/src/test/resources/core/application/resources/resourceB similarity index 100% rename from jib-core/src/test/resources/application/resources/resourceB rename to jib-core/src/test/resources/core/application/resources/resourceB diff --git a/jib-core/src/test/resources/application/resources/world b/jib-core/src/test/resources/core/application/resources/world similarity index 100% rename from jib-core/src/test/resources/application/resources/world rename to jib-core/src/test/resources/core/application/resources/world diff --git a/jib-core/src/test/resources/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar b/jib-core/src/test/resources/core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-core/src/test/resources/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar rename to jib-core/src/test/resources/core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar diff --git a/jib-core/src/test/resources/blobA b/jib-core/src/test/resources/core/blobA similarity index 100% rename from jib-core/src/test/resources/blobA rename to jib-core/src/test/resources/core/blobA diff --git a/jib-core/src/test/resources/class-finder-tests/extension/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/extension/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/extension/pack/Apple.class b/jib-core/src/test/resources/core/class-finder-tests/extension/pack/Apple.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/pack/Apple.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/pack/Apple.class diff --git a/jib-core/src/test/resources/class-finder-tests/extension/pack/Orange.class b/jib-core/src/test/resources/core/class-finder-tests/extension/pack/Orange.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/extension/pack/Orange.class rename to jib-core/src/test/resources/core/class-finder-tests/extension/pack/Orange.class diff --git a/jib-core/src/test/resources/class-finder-tests/external-classes/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/external-classes/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/external-classes/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/external-classes/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/main/MainClass.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/main/MainClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/main/MainClass.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/main/MainClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Apple.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Apple.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Apple.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Apple.class diff --git a/jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Orange.class b/jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Orange.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/imported-methods/pack/Orange.class rename to jib-core/src/test/resources/core/class-finder-tests/imported-methods/pack/Orange.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld$InnerClass.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld$InnerClass.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld$InnerClass.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld$InnerClass.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/inner-classes/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/inner-classes/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/inner-classes/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/inner-classes/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/Layer2Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/layered/HelloMoon.class b/jib-core/src/test/resources/core/class-finder-tests/multiple/multi/layered/HelloMoon.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/multiple/multi/layered/HelloMoon.class rename to jib-core/src/test/resources/core/class-finder-tests/multiple/multi/layered/HelloMoon.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/no-main/multi/Layer2Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/simple/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/simple/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/simple/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotAClassButMightBe.class b/jib-core/src/test/resources/core/class-finder-tests/simple/NotAClassButMightBe.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotAClassButMightBe.class rename to jib-core/src/test/resources/core/class-finder-tests/simple/NotAClassButMightBe.class diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotEvenAClass.txt b/jib-core/src/test/resources/core/class-finder-tests/simple/NotEvenAClass.txt similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotEvenAClass.txt rename to jib-core/src/test/resources/core/class-finder-tests/simple/NotEvenAClass.txt diff --git a/jib-core/src/test/resources/class-finder-tests/simple/NotMain.class b/jib-core/src/test/resources/core/class-finder-tests/simple/NotMain.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/simple/NotMain.class rename to jib-core/src/test/resources/core/class-finder-tests/simple/NotMain.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/Layer1Class.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/Layer1Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/Layer1Class.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/Layer1Class.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/HelloWorld.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/HelloWorld.class diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class b/jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/Layer2Class.class similarity index 100% rename from jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class rename to jib-core/src/test/resources/core/class-finder-tests/subdirectories/multi/layered/Layer2Class.class diff --git a/jib-core/src/test/resources/directoryA/.gitkeep b/jib-core/src/test/resources/core/directoryA/.gitkeep similarity index 100% rename from jib-core/src/test/resources/directoryA/.gitkeep rename to jib-core/src/test/resources/core/directoryA/.gitkeep diff --git a/jib-core/src/test/resources/fileA b/jib-core/src/test/resources/core/fileA similarity index 100% rename from jib-core/src/test/resources/fileA rename to jib-core/src/test/resources/core/fileA diff --git a/jib-core/src/test/resources/fileB b/jib-core/src/test/resources/core/fileB similarity index 100% rename from jib-core/src/test/resources/fileB rename to jib-core/src/test/resources/core/fileB diff --git a/jib-core/src/test/resources/json/basic.json b/jib-core/src/test/resources/core/json/basic.json similarity index 100% rename from jib-core/src/test/resources/json/basic.json rename to jib-core/src/test/resources/core/json/basic.json diff --git a/jib-core/src/test/resources/json/basic_list.json b/jib-core/src/test/resources/core/json/basic_list.json similarity index 100% rename from jib-core/src/test/resources/json/basic_list.json rename to jib-core/src/test/resources/core/json/basic_list.json diff --git a/jib-core/src/test/resources/json/containerconfig.json b/jib-core/src/test/resources/core/json/containerconfig.json similarity index 100% rename from jib-core/src/test/resources/json/containerconfig.json rename to jib-core/src/test/resources/core/json/containerconfig.json diff --git a/jib-core/src/test/resources/json/dockerconfig.json b/jib-core/src/test/resources/core/json/dockerconfig.json similarity index 100% rename from jib-core/src/test/resources/json/dockerconfig.json rename to jib-core/src/test/resources/core/json/dockerconfig.json diff --git a/jib-core/src/test/resources/json/dockerconfig_extra_matches.json b/jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json similarity index 100% rename from jib-core/src/test/resources/json/dockerconfig_extra_matches.json rename to jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json diff --git a/jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json b/jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json similarity index 100% rename from jib-core/src/test/resources/json/dockerconfig_index_docker_io_v1.json rename to jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json diff --git a/jib-core/src/test/resources/json/loadmanifest.json b/jib-core/src/test/resources/core/json/loadmanifest.json similarity index 100% rename from jib-core/src/test/resources/json/loadmanifest.json rename to jib-core/src/test/resources/core/json/loadmanifest.json diff --git a/jib-core/src/test/resources/json/loadmanifest_notag.json b/jib-core/src/test/resources/core/json/loadmanifest_notag.json similarity index 100% rename from jib-core/src/test/resources/json/loadmanifest_notag.json rename to jib-core/src/test/resources/core/json/loadmanifest_notag.json diff --git a/jib-core/src/test/resources/json/metadata-v2.json b/jib-core/src/test/resources/core/json/metadata-v2.json similarity index 100% rename from jib-core/src/test/resources/json/metadata-v2.json rename to jib-core/src/test/resources/core/json/metadata-v2.json diff --git a/jib-core/src/test/resources/json/metadata-v3.json b/jib-core/src/test/resources/core/json/metadata-v3.json similarity index 100% rename from jib-core/src/test/resources/json/metadata-v3.json rename to jib-core/src/test/resources/core/json/metadata-v3.json diff --git a/jib-core/src/test/resources/json/metadata.json b/jib-core/src/test/resources/core/json/metadata.json similarity index 100% rename from jib-core/src/test/resources/json/metadata.json rename to jib-core/src/test/resources/core/json/metadata.json diff --git a/jib-core/src/test/resources/json/metadata_corrupted.json b/jib-core/src/test/resources/core/json/metadata_corrupted.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_corrupted.json rename to jib-core/src/test/resources/core/json/metadata_corrupted.json diff --git a/jib-core/src/test/resources/json/metadata_windows-v2.json b/jib-core/src/test/resources/core/json/metadata_windows-v2.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_windows-v2.json rename to jib-core/src/test/resources/core/json/metadata_windows-v2.json diff --git a/jib-core/src/test/resources/json/metadata_windows.json b/jib-core/src/test/resources/core/json/metadata_windows.json similarity index 100% rename from jib-core/src/test/resources/json/metadata_windows.json rename to jib-core/src/test/resources/core/json/metadata_windows.json diff --git a/jib-core/src/test/resources/json/ocimanifest.json b/jib-core/src/test/resources/core/json/ocimanifest.json similarity index 100% rename from jib-core/src/test/resources/json/ocimanifest.json rename to jib-core/src/test/resources/core/json/ocimanifest.json diff --git a/jib-core/src/test/resources/json/translated_ocimanifest.json b/jib-core/src/test/resources/core/json/translated_ocimanifest.json similarity index 100% rename from jib-core/src/test/resources/json/translated_ocimanifest.json rename to jib-core/src/test/resources/core/json/translated_ocimanifest.json diff --git a/jib-core/src/test/resources/json/translated_v22manifest.json b/jib-core/src/test/resources/core/json/translated_v22manifest.json similarity index 100% rename from jib-core/src/test/resources/json/translated_v22manifest.json rename to jib-core/src/test/resources/core/json/translated_v22manifest.json diff --git a/jib-core/src/test/resources/json/v21manifest.json b/jib-core/src/test/resources/core/json/v21manifest.json similarity index 100% rename from jib-core/src/test/resources/json/v21manifest.json rename to jib-core/src/test/resources/core/json/v21manifest.json diff --git a/jib-core/src/test/resources/json/v22manifest.json b/jib-core/src/test/resources/core/json/v22manifest.json similarity index 100% rename from jib-core/src/test/resources/json/v22manifest.json rename to jib-core/src/test/resources/core/json/v22manifest.json diff --git a/jib-core/src/test/resources/layer/a/b/bar b/jib-core/src/test/resources/core/layer/a/b/bar similarity index 100% rename from jib-core/src/test/resources/layer/a/b/bar rename to jib-core/src/test/resources/core/layer/a/b/bar diff --git a/jib-core/src/test/resources/layer/c/cat b/jib-core/src/test/resources/core/layer/c/cat similarity index 100% rename from jib-core/src/test/resources/layer/c/cat rename to jib-core/src/test/resources/core/layer/c/cat diff --git a/jib-core/src/test/resources/layer/foo b/jib-core/src/test/resources/core/layer/foo similarity index 100% rename from jib-core/src/test/resources/layer/foo rename to jib-core/src/test/resources/core/layer/foo diff --git a/jib-core/src/test/resources/random-contents/file1 b/jib-core/src/test/resources/core/random-contents/file1 similarity index 100% rename from jib-core/src/test/resources/random-contents/file1 rename to jib-core/src/test/resources/core/random-contents/file1 diff --git a/jib-core/src/test/resources/random-contents/file2 b/jib-core/src/test/resources/core/random-contents/file2 similarity index 100% rename from jib-core/src/test/resources/random-contents/file2 rename to jib-core/src/test/resources/core/random-contents/file2 diff --git a/jib-core/src/test/resources/random-contents/sub-directory/file3 b/jib-core/src/test/resources/core/random-contents/sub-directory/file3 similarity index 100% rename from jib-core/src/test/resources/random-contents/sub-directory/file3 rename to jib-core/src/test/resources/core/random-contents/sub-directory/file3 diff --git a/jib-core/src/test/resources/random-contents/sub-directory/file4 b/jib-core/src/test/resources/core/random-contents/sub-directory/file4 similarity index 100% rename from jib-core/src/test/resources/random-contents/sub-directory/file4 rename to jib-core/src/test/resources/core/random-contents/sub-directory/file4 diff --git a/jib-core/src/test/resources/random-contents/sub-directory/leaf/file5 b/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file5 similarity index 100% rename from jib-core/src/test/resources/random-contents/sub-directory/leaf/file5 rename to jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file5 diff --git a/jib-core/src/test/resources/random-contents/sub-directory/leaf/file6 b/jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file6 similarity index 100% rename from jib-core/src/test/resources/random-contents/sub-directory/leaf/file6 rename to jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file6 diff --git a/jib-core/src/test/resources/webAppSampleDockerfile b/jib-core/src/test/resources/core/webAppSampleDockerfile similarity index 100% rename from jib-core/src/test/resources/webAppSampleDockerfile rename to jib-core/src/test/resources/core/webAppSampleDockerfile diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/gradle.properties b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/gradle.properties similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/gradle.properties rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/gradle.properties diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/libs/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/settings.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/settings.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/settings.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/default-target/src/main/resources/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/empty/build-broken-user.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/src/main/java/com/test/Empty.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/empty/src/main/java/com/test/Empty.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/src/main/java/com/test/Empty.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/multiproject/settings.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/build-dockerclient.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/complex-build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/mock-docker.sh rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/bar/cat b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/bar/cat rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/foo b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/custom-extra-dir/foo rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/simple/src/main/resources/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build-tomcat.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/build.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_js/bogus.js rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/extra_static/bogus.html rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/resources/world b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/resources/world rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/resources/world diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html similarity index 100% rename from jib-gradle-plugin/src/integration-test/resources/projects/war_servlet25/src/main/webapp/index.html rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java index 922f1d2e60..35ad7a5be4 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java @@ -131,28 +131,35 @@ private static void assertExtractionPathsUnordered( @Before public void setUp() throws URISyntaxException, IOException { Set classesFiles = - ImmutableSet.of(Paths.get(Resources.getResource("application/classes").toURI())); + ImmutableSet.of(Paths.get(Resources.getResource("gradle/application/classes").toURI())); FileCollection classesFileCollection = new TestFileCollection(classesFiles); - Path resourcesOutputDir = Paths.get(Resources.getResource("application/resources").toURI()); + Path resourcesOutputDir = + Paths.get(Resources.getResource("gradle/application/resources").toURI()); Set allFiles = new HashSet<>(classesFiles); allFiles.add(resourcesOutputDir); allFiles.add( - Paths.get(Resources.getResource("application/dependencies/library.jarC.jar").toURI())); - allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryB.jar").toURI())); - allFiles.add(Paths.get(Resources.getResource("application/dependencies/libraryA.jar").toURI())); + Paths.get( + Resources.getResource("gradle/application/dependencies/library.jarC.jar").toURI())); + allFiles.add( + Paths.get(Resources.getResource("gradle/application/dependencies/libraryB.jar").toURI())); allFiles.add( - Paths.get(Resources.getResource("application/dependencies/dependency-1.0.0.jar").toURI())); + Paths.get(Resources.getResource("gradle/application/dependencies/libraryA.jar").toURI())); allFiles.add( Paths.get( - Resources.getResource("application/dependencies/more/dependency-1.0.0.jar").toURI())); + Resources.getResource("gradle/application/dependencies/dependency-1.0.0.jar").toURI())); + allFiles.add( + Paths.get( + Resources.getResource("gradle/application/dependencies/more/dependency-1.0.0.jar") + .toURI())); allFiles.add( Paths.get( - Resources.getResource("application/dependencies/another/one/dependency-1.0.0.jar") + Resources.getResource( + "gradle/application/dependencies/another/one/dependency-1.0.0.jar") .toURI())); allFiles.add( Paths.get( - Resources.getResource("application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") + Resources.getResource("gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") .toURI())); FileCollection runtimeFileCollection = new TestFileCollection(allFiles); @@ -167,16 +174,16 @@ public void setUp() throws URISyntaxException, IOException { Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); // We can't commit an empty directory in Git, so create (if not exist). Path emptyDirectory = - Paths.get(Resources.getResource("webapp").toURI()) + Paths.get(Resources.getResource("gradle/webapp").toURI()) .resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); - extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); } @Test public void test_correctFiles() throws URISyntaxException, IOException { - Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); + Path applicationDirectory = Paths.get(Resources.getResource("gradle/application").toURI()); ImmutableList expectedDependenciesFiles = ImmutableList.of( applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), @@ -347,7 +354,7 @@ public void testGetForProject_defaultAppRoot() throws IOException { @Test public void testWebApp() throws URISyntaxException, IOException { - Path webAppDirectory = Paths.get(Resources.getResource("webapp").toURI()); + Path webAppDirectory = Paths.get(Resources.getResource("gradle/webapp").toURI()); setUpWarProject(webAppDirectory); JavaLayerConfigurations configuration = @@ -431,7 +438,7 @@ public void testWebApp() throws URISyntaxException, IOException { @Test public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOException { - setUpWarProject(Paths.get(Resources.getResource("webapp").toURI())); + setUpWarProject(Paths.get(Resources.getResource("gradle/webapp").toURI())); JavaLayerConfigurations configuration = GradleLayerConfigurations.getForProject( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index a1bce65717..bcdf0845ed 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -62,7 +62,7 @@ public void testCheckGradleVersion_pass() throws IOException { // Copy build file to temp dir Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); InputStream buildFileContent = - getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); + getClass().getClassLoader().getResourceAsStream("gradle/plugin-test/build.gradle"); Files.copy(buildFileContent, buildFile); GradleRunner.create() @@ -80,7 +80,7 @@ public void testCheckGradleVersion_fail() throws IOException { // Copy build file to temp dir Path buildFile = testProjectRoot.getRoot().toPath().resolve("build.gradle"); InputStream buildFileContent = - getClass().getClassLoader().getResourceAsStream("plugin-test/build.gradle"); + getClass().getClassLoader().getResourceAsStream("gradle/plugin-test/build.gradle"); Files.copy(buildFileContent, buildFile); GradleRunner gradleRunner = diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index fd3b39fc76..c07b2c258f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -32,7 +32,7 @@ /** Works with the test Gradle projects in the {@code resources/projects} directory. */ class TestProject extends TemporaryFolder implements Closeable { - private static final String PROJECTS_PATH_IN_RESOURCES = "projects/"; + private static final String PROJECTS_PATH_IN_RESOURCES = "gradle/projects/"; /** Copies test project {@code projectName} to {@code destination} folder. */ private static void copyProject(String projectName, Path destination) diff --git a/jib-gradle-plugin/src/test/resources/application/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/gradle/application/classes/HelloWorld.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/classes/HelloWorld.class rename to jib-gradle-plugin/src/test/resources/gradle/application/classes/HelloWorld.class diff --git a/jib-gradle-plugin/src/test/resources/application/classes/some.class b/jib-gradle-plugin/src/test/resources/gradle/application/classes/some.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/classes/some.class rename to jib-gradle-plugin/src/test/resources/gradle/application/classes/some.class diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/another/one/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/another/one/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/library.jarC.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/library.jarC.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/library.jarC.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/library.jarC.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/libraryA.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryA.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/libraryA.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryA.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/libraryB.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryB.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/libraryB.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryB.jar diff --git a/jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/application/resources/resourceA b/jib-gradle-plugin/src/test/resources/gradle/application/resources/resourceA similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/resourceA rename to jib-gradle-plugin/src/test/resources/gradle/application/resources/resourceA diff --git a/jib-gradle-plugin/src/test/resources/application/resources/resourceB b/jib-gradle-plugin/src/test/resources/gradle/application/resources/resourceB similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/resourceB rename to jib-gradle-plugin/src/test/resources/gradle/application/resources/resourceB diff --git a/jib-gradle-plugin/src/test/resources/application/resources/world b/jib-gradle-plugin/src/test/resources/gradle/application/resources/world similarity index 100% rename from jib-gradle-plugin/src/test/resources/application/resources/world rename to jib-gradle-plugin/src/test/resources/gradle/application/resources/world diff --git a/jib-gradle-plugin/src/test/resources/plugin-test/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/plugin-test/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/plugin-test/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/plugin-test/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/other-jib/extra-file b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/other-jib/extra-file similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/complex-service/src/main/other-jib/extra-file rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/other-jib/extra-file diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/gradle.properties b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/gradle.properties similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/gradle.properties rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/gradle.properties diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/lib/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/java/com/lib/Lib.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/main/resources/hi.txt rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/lib/src/test/java/com/lib/LibTest.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/settings.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/build.gradle rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/libs/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/bar/cat b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/bar/cat rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/foo b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/src/main/custom-extra-dir/foo rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/src/test/resources/projects/simple/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world similarity index 100% rename from jib-gradle-plugin/src/test/resources/projects/simple/src/main/resources/world rename to jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/META-INF/context.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/META-INF/context.xml similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/META-INF/context.xml rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/META-INF/context.xml diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/Test.jsp b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/Test.jsp similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/Test.jsp rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/Test.jsp diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/web.xml similarity index 100% rename from jib-gradle-plugin/src/test/resources/webapp/jib-exploded-war/WEB-INF/web.xml rename to jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/web.xml diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java index b242e482e3..56b0081ec4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java @@ -113,8 +113,9 @@ private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuratio @Before public void setUp() throws URISyntaxException, IOException { - Path outputPath = Paths.get(Resources.getResource("application/output").toURI()); - Path dependenciesPath = Paths.get(Resources.getResource("application/dependencies").toURI()); + Path outputPath = Paths.get(Resources.getResource("maven/application/output").toURI()); + Path dependenciesPath = + Paths.get(Resources.getResource("maven/application/dependencies").toURI()); Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); @@ -134,16 +135,17 @@ public void setUp() throws URISyntaxException, IOException { Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); Path emptyDirectory = - Paths.get(Resources.getResource("webapp").toURI()) + Paths.get(Resources.getResource("maven/webapp").toURI()) .resolve("final-name/WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); - extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); } @Test public void test_correctFiles() throws URISyntaxException, IOException { - Path dependenciesPath = Paths.get(Resources.getResource("application/dependencies").toURI()); + Path dependenciesPath = + Paths.get(Resources.getResource("maven/application/dependencies").toURI()); ImmutableList expectedDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), @@ -155,7 +157,7 @@ public void test_correctFiles() throws URISyntaxException, IOException { ImmutableList expectedSnapshotDependenciesFiles = ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); - Path applicationDirectory = Paths.get(Resources.getResource("application").toURI()); + Path applicationDirectory = Paths.get(Resources.getResource("maven/application").toURI()); ImmutableList expectedResourcesFiles = ImmutableList.of( applicationDirectory.resolve("output/directory"), @@ -227,7 +229,7 @@ private Artifact makeArtifact(Path path) { @Test public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, IOException { - Path outputPath = Paths.get(Resources.getResource("webapp").toURI()); + Path outputPath = Paths.get(Resources.getResource("maven/webapp").toURI()); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index 71f70a676c..83cb1e655a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -29,7 +29,7 @@ /** Works with the test Maven projects in the {@code resources/projects} directory. */ public class TestProject extends TemporaryFolder implements Closeable { - private static final String PROJECTS_PATH_IN_RESOURCES = "/projects/"; + private static final String PROJECTS_PATH_IN_RESOURCES = "/maven/projects/"; private static boolean isPomXml(Path path) { String filename = path.getFileName().toString(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java index 701fa470ed..5eb6276526 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java @@ -22,7 +22,7 @@ /** A test helper to resolve artifacts from a local repository in test/resources */ public class TestRepository extends ExternalResource { - private static final String TEST_M2 = "testM2"; + private static final String TEST_M2 = "maven/testM2"; private MojoRule testHarness; private ArtifactRepositoryFactory artifactRepositoryFactory; diff --git a/jib-maven-plugin/src/test/resources/layer/a/b/bar b/jib-maven-plugin/src/test/resources/layer/a/b/bar deleted file mode 100644 index 5716ca5987..0000000000 --- a/jib-maven-plugin/src/test/resources/layer/a/b/bar +++ /dev/null @@ -1 +0,0 @@ -bar diff --git a/jib-maven-plugin/src/test/resources/layer/c/cat b/jib-maven-plugin/src/test/resources/layer/c/cat deleted file mode 100644 index ef07ddcd0a..0000000000 --- a/jib-maven-plugin/src/test/resources/layer/c/cat +++ /dev/null @@ -1 +0,0 @@ -cat diff --git a/jib-maven-plugin/src/test/resources/layer/foo b/jib-maven-plugin/src/test/resources/layer/foo deleted file mode 100644 index 257cc5642c..0000000000 --- a/jib-maven-plugin/src/test/resources/layer/foo +++ /dev/null @@ -1 +0,0 @@ -foo diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/another/one/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/another/one/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/another/one/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/library.jarC.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/library.jarC.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/library.jarC.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/library.jarC.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/libraryA.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/libraryA.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/libraryA.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/libraryA.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/libraryB.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/libraryB.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/libraryB.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/libraryB.jar diff --git a/jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/application/dependencies/more/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/application/dependencies/more/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/application/dependencies/more/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/application/output/HelloWorld.class b/jib-maven-plugin/src/test/resources/maven/application/output/HelloWorld.class similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/HelloWorld.class rename to jib-maven-plugin/src/test/resources/maven/application/output/HelloWorld.class diff --git a/jib-maven-plugin/src/test/resources/application/output/directory/somefile b/jib-maven-plugin/src/test/resources/maven/application/output/directory/somefile similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/directory/somefile rename to jib-maven-plugin/src/test/resources/maven/application/output/directory/somefile diff --git a/jib-maven-plugin/src/test/resources/application/output/package/some.class b/jib-maven-plugin/src/test/resources/maven/application/output/package/some.class similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/package/some.class rename to jib-maven-plugin/src/test/resources/maven/application/output/package/some.class diff --git a/jib-maven-plugin/src/test/resources/application/output/resourceA b/jib-maven-plugin/src/test/resources/maven/application/output/resourceA similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/resourceA rename to jib-maven-plugin/src/test/resources/maven/application/output/resourceA diff --git a/jib-maven-plugin/src/test/resources/application/output/resourceB b/jib-maven-plugin/src/test/resources/maven/application/output/resourceB similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/resourceB rename to jib-maven-plugin/src/test/resources/maven/application/output/resourceB diff --git a/jib-maven-plugin/src/test/resources/application/output/some.class b/jib-maven-plugin/src/test/resources/maven/application/output/some.class similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/some.class rename to jib-maven-plugin/src/test/resources/maven/application/output/some.class diff --git a/jib-maven-plugin/src/test/resources/application/output/world b/jib-maven-plugin/src/test/resources/maven/application/output/world similarity index 100% rename from jib-maven-plugin/src/test/resources/application/output/world rename to jib-maven-plugin/src/test/resources/maven/application/output/world diff --git a/jib-maven-plugin/src/test/resources/application/source/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/application/source/package/some.java b/jib-maven-plugin/src/test/resources/maven/application/source/package/some.java similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/package/some.java rename to jib-maven-plugin/src/test/resources/maven/application/source/package/some.java diff --git a/jib-maven-plugin/src/test/resources/application/source/some.java b/jib-maven-plugin/src/test/resources/maven/application/source/some.java similarity index 100% rename from jib-maven-plugin/src/test/resources/application/source/some.java rename to jib-maven-plugin/src/test/resources/maven/application/source/some.java diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/libs/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/projects/default-target/libs/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/libs/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/projects/default-target/libs/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/default-target/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/src/main/java/com/test/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/projects/default-target/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/projects/default-target/src/main/resources/world b/jib-maven-plugin/src/test/resources/maven/projects/default-target/src/main/resources/world similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/default-target/src/main/resources/world rename to jib-maven-plugin/src/test/resources/maven/projects/default-target/src/main/resources/world diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/empty/pom-broken-user.xml rename to jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/empty/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/empty/src/main/java/com/test/Empty.java b/jib-maven-plugin/src/test/resources/maven/projects/empty/src/main/java/com/test/Empty.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/empty/src/main/java/com/test/Empty.java rename to jib-maven-plugin/src/test/resources/maven/projects/empty/src/main/java/com/test/Empty.java diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/complex-service/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/extra-resources-1/resource1.txt diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/extra-resources-2/resource2.txt diff --git a/jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/lib/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/main/java/com/lib/Lib.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/lib/src/main/java/com/lib/Lib.java rename to jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/main/java/com/lib/Lib.java diff --git a/jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/lib/src/test/java/com/lib/LibTest.java rename to jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java diff --git a/jib-maven-plugin/src/test/resources/projects/multi/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/simple-service/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/projects/simple/libs/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/libs/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/projects/simple/libs/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh b/jib-maven-plugin/src/test/resources/maven/projects/simple/mock-docker.sh similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/mock-docker.sh rename to jib-maven-plugin/src/test/resources/maven/projects/simple/mock-docker.sh diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/pom-complex-properties.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/pom-complex.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/pom-dockerclient.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/pom-no-to-image.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/java/com/test/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/bar/cat b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom/bar/cat similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/bar/cat rename to jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom/bar/cat diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/foo b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom/foo similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/jib-custom/foo rename to jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom/foo diff --git a/jib-maven-plugin/src/test/resources/projects/simple/src/main/resources/world b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/resources/world similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/simple/src/main/resources/world rename to jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/resources/world diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/pom-tomcat.xml rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/pom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/extra_js/bogus.js similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_js/bogus.js rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/extra_js/bogus.js diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/extra_static/bogus.html similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/extra_static/bogus.html rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/extra_static/bogus.html diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/java/example/HelloWorld.java similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/java/example/HelloWorld.java rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/java/example/HelloWorld.java diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/resources/world similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/resources/world rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/resources/world diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml diff --git a/jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/index.html similarity index 100% rename from jib-maven-plugin/src/test/resources/projects/war_servlet25/src/main/webapp/index.html rename to jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/src/main/webapp/index.html diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/_remote.repositories similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/_remote.repositories rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/_remote.repositories diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/1.0.0/dependency-1.0.0.pom diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/maven-metadata-local.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependency/maven-metadata-local.xml rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependency/maven-metadata-local.xml diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/_remote.repositories diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/dependencyX-1.0.0-SNAPSHOT.pom diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/1.0.0-SNAPSHOT/maven-metadata-local.xml diff --git a/jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/maven-metadata-local.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/testM2/com/test/dependencyX/maven-metadata-local.xml rename to jib-maven-plugin/src/test/resources/maven/testM2/com/test/dependencyX/maven-metadata-local.xml diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/META-INF/context.xml b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/META-INF/context.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/META-INF/context.xml rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/META-INF/context.xml diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/Test.jsp b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/Test.jsp similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/Test.jsp rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/Test.jsp diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/HelloWorld.class similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/HelloWorld.class rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/HelloWorld.class diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/package/Other.class similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/Other.class rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/package/Other.class diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/test.properties b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/package/test.properties similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/classes/package/test.properties rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/classes/package/test.properties diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/web.xml b/jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/web.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/webapp/final-name/WEB-INF/web.xml rename to jib-maven-plugin/src/test/resources/maven/webapp/final-name/WEB-INF/web.xml diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java index bd5c61aab5..02197686b9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java @@ -63,12 +63,13 @@ private static void assertExtractionPathsUnordered( @Test public void testFromExplodedWar() throws URISyntaxException, IOException { // Copy test files to a temporary directory that we can safely operate on - Path resourceExplodedWar = Paths.get(Resources.getResource("exploded-war").toURI()); + Path resourceExplodedWar = + Paths.get(Resources.getResource("plugins-common/exploded-war").toURI()); FileOperations.copy(ImmutableList.of(resourceExplodedWar), temporaryFolder.getRoot().toPath()); Path temporaryExplodedWar = temporaryFolder.getRoot().toPath().resolve("exploded-war"); Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); - Path extraFilesDirectory = Paths.get(Resources.getResource("layer").toURI()); + Path extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); JavaLayerConfigurations configuration = JavaLayerConfigurationsHelper.fromExplodedWar( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 79d31d8568..27d5452eca 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -88,7 +88,7 @@ public void testResolveMainClass_multipleInferredWithBackup() Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( new DirectoryWalker( - Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) + Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI())) .walk() .stream() .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) @@ -106,7 +106,7 @@ public void testResolveMainClass_multipleInferredWithoutBackup() Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) .thenReturn( new DirectoryWalker( - Paths.get(Resources.getResource("class-finder-tests/multiple").toURI())) + Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI())) .walk() .stream() .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java index 03e83ffec5..a45de47a7e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ZipUtilTest.java @@ -51,7 +51,8 @@ public void testUnzip_nonExistingDestination() throws URISyntaxException, IOExce public void testZipSlipVulnerability_windows() throws URISyntaxException { Assume.assumeTrue(System.getProperty("os.name").startsWith("Windows")); - Path archive = Paths.get(Resources.getResource("test-archives/zip-slip-win.zip").toURI()); + Path archive = + Paths.get(Resources.getResource("plugins-common/test-archives/zip-slip-win.zip").toURI()); verifyZipSlipSafe(archive); } @@ -59,12 +60,14 @@ public void testZipSlipVulnerability_windows() throws URISyntaxException { public void testZipSlipVulnerability_unix() throws URISyntaxException { Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); - Path archive = Paths.get(Resources.getResource("test-archives/zip-slip.zip").toURI()); + Path archive = + Paths.get(Resources.getResource("plugins-common/test-archives/zip-slip.zip").toURI()); verifyZipSlipSafe(archive); } private void verifyUnzip(Path destination) throws URISyntaxException, IOException { - Path archive = Paths.get(Resources.getResource("test-archives/test.zip").toURI()); + Path archive = + Paths.get(Resources.getResource("plugins-common/test-archives/test.zip").toURI()); ZipUtil.unzip(archive, destination); diff --git a/jib-plugins-common/src/test/resources/exploded-war/META-INF/context.xml b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/META-INF/context.xml similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/META-INF/context.xml rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/META-INF/context.xml diff --git a/jib-plugins-common/src/test/resources/exploded-war/Test.jsp b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/Test.jsp similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/Test.jsp rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/Test.jsp diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/HelloWorld.class similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/HelloWorld.class rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/HelloWorld.class diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/package/Other.class similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/Other.class rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/package/Other.class diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/test.properties b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/package/test.properties similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/classes/package/test.properties rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/classes/package/test.properties diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependency-1.0.0.jar b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependency-1.0.0.jar rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-plugins-common/src/test/resources/exploded-war/WEB-INF/web.xml b/jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/web.xml similarity index 100% rename from jib-plugins-common/src/test/resources/exploded-war/WEB-INF/web.xml rename to jib-plugins-common/src/test/resources/plugins-common/exploded-war/WEB-INF/web.xml diff --git a/jib-plugins-common/src/test/resources/test-archives/test.zip b/jib-plugins-common/src/test/resources/plugins-common/test-archives/test.zip similarity index 100% rename from jib-plugins-common/src/test/resources/test-archives/test.zip rename to jib-plugins-common/src/test/resources/plugins-common/test-archives/test.zip diff --git a/jib-plugins-common/src/test/resources/test-archives/zip-slip-win.zip b/jib-plugins-common/src/test/resources/plugins-common/test-archives/zip-slip-win.zip similarity index 100% rename from jib-plugins-common/src/test/resources/test-archives/zip-slip-win.zip rename to jib-plugins-common/src/test/resources/plugins-common/test-archives/zip-slip-win.zip diff --git a/jib-plugins-common/src/test/resources/test-archives/zip-slip.zip b/jib-plugins-common/src/test/resources/plugins-common/test-archives/zip-slip.zip similarity index 100% rename from jib-plugins-common/src/test/resources/test-archives/zip-slip.zip rename to jib-plugins-common/src/test/resources/plugins-common/test-archives/zip-slip.zip From 69a470a4cf1b4add3084c42617ae0caf4488d366 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 17 Jan 2019 16:52:03 -0500 Subject: [PATCH 0445/2020] Removes incorrect description about jib.applicationCache. (#1429) --- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 4717146ef2..a418e73596 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -249,7 +249,7 @@ Property | Type | Default | Description `jib.httpTimeout` | `int` | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. `jib.useOnlyProjectCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Maven projects. `jib.baseImageCache` | `File` | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. -`jib.applicationCache` | `File` | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. If not set, a temporary directory will be used as the application layers cache. +`jib.applicationCache` | `File` | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. `jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index bd28753bfc..5d30e8442c 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -291,7 +291,7 @@ Property | Type | Default | Description `jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. `jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects (i.e. `jib.baseImageCache` defaults to `[project dir]/target/jib-cache` instead of `[user cache home]/google-cloud-tools-java/jib`). `jib.baseImageCache` | string | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. -`jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. If not set, a temporary directory will be used as the application layers cache. +`jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. `jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* From e291ad0189bbf4199909fc77546a4300451fd729 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 17 Jan 2019 16:52:36 -0500 Subject: [PATCH 0446/2020] Update core readme for 0.1.1 (#1422) --- jib-core/README.md | 51 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/jib-core/README.md b/jib-core/README.md index 8144cd0e0f..88328dc888 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.1.0 + 0.1.1 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.1.0' + compile 'com.google.cloud.tools:jib-core:0.1.1' } ``` @@ -45,13 +45,13 @@ Jib.from("busybox") .addCredential("myusername", "mypassword"))); ``` -1. [`Jib.from("busybox")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. -1. [`.addLayer(...)`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. -1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. -1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. -1. [`.addCredential`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). -1. [`Containerizer.to`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. -1. [`.containerize`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. +1. [`Jib.from("busybox")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. +1. [`.addLayer(...)`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. +1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. +1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. +1. [`.addCredential`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). +1. [`Containerizer.to`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. +1. [`.containerize`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. ## Tutorials @@ -59,30 +59,33 @@ Jib.from("busybox") ## API overview -[`Jib`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core +[`Jib`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core -[`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build +[`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build -[`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to +[`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to -[`JibContainer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container +[`JibContainer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container Three `TargetImage` types define the 3 different targets Jib can build to: -- [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry -- [`DockerDaemonImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon -- [`TarImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive +- [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry +- [`DockerDaemonImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon +- [`TarImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive Other useful classes: +- [`ImageReference`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references +- [`LayerConfiguration`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build +- [`CredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry +- [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers +- [`EventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution -- [`ImageReference`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references -- [`LayerConfiguration`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build -- [`CredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry -- [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers -- [`EventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution +Java-specific API: +- [`JavaContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JavaContainerBuilder.html) - configures a `JibContainerBuilder` for Java-specific applications +- [`MainClassFinder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/MainClassFinder.html) - find the main Java class in a given list of class files ## API reference -[API reference](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/package-summary.html) +[API reference](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/package-summary.html) ## How Jib Core works @@ -94,8 +97,8 @@ The Jib Core system consists 3 main parts: Some other parts of Jib Core internals include: -- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) -- an eventing system to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) +- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) +- an eventing system to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) - support for fully-concurrent multi-threaded executions ## Frequently Asked Questions (FAQ) From e6ae400386404d63408a9596f4adcffb05bb0cf2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 17 Jan 2019 17:20:50 -0500 Subject: [PATCH 0447/2020] Maven release v1.0.0 (#1431) * preparing release 1.0.0 * 1.0.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 60fd566fba..1feee1f6b0 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.0-SNAPSHOT + 1.0.1-SNAPSHOT maven-plugin Jib From 2d2996f3bb5be8200ba8e17e9e7a08984be09e71 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Jan 2019 17:38:53 -0500 Subject: [PATCH 0448/2020] Gradle release v1.0.0 (#1432) * [Gradle Release Plugin] - pre tag commit: 'v1.0.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.0.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 617dd212c6..4e224f959a 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.0.0-SNAPSHOT +version = 1.0.1-SNAPSHOT From f7844b7987960d9420f1c460a5fd86f31a889edc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Jan 2019 19:04:24 -0500 Subject: [PATCH 0449/2020] Update versions to 1.0.0 (#1433) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- examples/vertx/build.gradle | 4 ++-- jib-gradle-plugin/CHANGELOG.md | 8 ++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++-- jib-maven-plugin/README.md | 6 +++--- 19 files changed, 33 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a04fc1b6ab..e803db0501 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.0-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + // id 'com.google.cloud.tools.jib' version '1.0.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 685026f6ea..52ff11d72e 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.0.0-rc2 + 1.0.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index b0147059fc..3d41b988ce 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 2ff499e316..8bae69615f 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.0-rc2 + 1.0.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 751daa999e..11c26edd6e 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 04faa577a3..ae9d4f3a4c 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.0-rc2 + 1.0.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 5c5e70073d..85d373140d 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.0.0-rc2" + id("com.google.cloud.tools.jib") version "1.0.0" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 73b4d45047..821c292bbd 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 9a4979f146..a9f3c36825 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.0-rc2' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 245436822d..8ebc5fdf2f 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.0-rc2 + 1.0.0 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 04367c79e9..9c8a639e25 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 78ea3a0814..ff45ba415a 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 1.0.0-rc2 + 1.0.0 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index bee2aa6b06..3b77812299 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index 3ce02d9c75..93eadd70e7 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.0-rc2 + 1.0.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 35cd29c871..30e17ca71c 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } repositories { @@ -22,4 +22,4 @@ jib { container { ports = ['8080'] } -} \ No newline at end of file +} diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 0f997bd704..efdfbc6f7c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,10 +7,14 @@ All notable changes to this project will be documented in this file. ### Changed -- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) - ### Fixed +## 1.0.0 + +### Changed + +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + ## 1.0.0-rc2 ### Added diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index a418e73596..1b80fcd637 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -43,7 +43,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.0.0-rc2' + id 'com.google.cloud.tools.jib' version '1.0.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9e55272338..5bf81ac60e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,10 +7,14 @@ All notable changes to this project will be documented in this file. ### Changed -- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) - ### Fixed +## 1.0.0 + +### Changed + +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) + ## 1.0.0-rc2 ### Added diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 5d30e8442c..3dd941c5f7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -38,7 +38,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0-rc2:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -46,7 +46,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0-rc2:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -64,7 +64,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.0.0-rc2 + 1.0.0 myimage From f2fe6aee6a32d0e23ab2de62801d95b10c86a103 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 18 Jan 2019 10:39:06 -0500 Subject: [PATCH 0450/2020] Upgrade Google HTTP Client dependency (#1415) * Upgrade google-http-client * SSLException for all --- jib-core/build.gradle | 2 +- .../cloud/tools/jib/registry/RegistryEndpointCaller.java | 6 +++--- .../cloud/tools/jib/http/WithServerConnectionTest.java | 4 ++-- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index f46a4f60c6..8480e34064 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -35,7 +35,7 @@ configurations { dependencies { // Make sure these are consistent with jib-maven-plugin. - implementation 'com.google.http-client:google-http-client:1.23.0' + implementation 'com.google.http-client:google-http-client:1.27.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:23.5-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index e852cda092..743dcba954 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -36,7 +36,7 @@ import java.security.GeneralSecurityException; import java.util.function.Function; import javax.annotation.Nullable; -import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLException; import org.apache.http.NoHttpResponseException; import org.apache.http.conn.HttpHostConnectException; @@ -151,7 +151,7 @@ private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, Reg try { return call(url, connectionFactory); - } catch (SSLPeerUnverifiedException ex) { + } catch (SSLException ex) { return handleUnverifiableServerException(url); } catch (HttpHostConnectException ex) { @@ -176,7 +176,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr "Cannot verify server at " + url + ". Attempting again with no TLS verification.")); return call(url, getInsecureConnectionFactory()); - } catch (SSLPeerUnverifiedException ex) { + } catch (SSLException ex) { return fallBackToHttp(url); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index 0a3ef3a125..e29cceaa27 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -22,7 +22,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; -import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLException; import org.junit.Assert; import org.junit.Test; @@ -70,7 +70,7 @@ public void testSecureConnectionOnInsecureHttpsServer() try { connection.send("GET", new Request.Builder().build()); Assert.fail("Should fail if cannot verify peer"); - } catch (SSLPeerUnverifiedException ex) { + } catch (SSLException ex) { Assert.assertNotNull(ex.getMessage()); } } diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 109694627b..5d49f7d984 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -61,7 +61,7 @@ configurations { dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. - compile 'com.google.http-client:google-http-client:1.23.0' + compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 1feee1f6b0..bf31692c81 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -53,7 +53,7 @@ com.google.http-client google-http-client - 1.23.0 + 1.27.0 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 9b7354d172..3030da8a0e 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -29,7 +29,7 @@ sourceSets { dependencies { // Make sure these are consistent with jib-maven-plugin. - compile 'com.google.http-client:google-http-client:1.23.0' + compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:23.5-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' From 653ed72f36d0d5467d61757e2b3cd389c06ce496 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 18 Jan 2019 17:52:46 +0100 Subject: [PATCH 0451/2020] Upgrade micronaut example (#1417) --- .gitignore | 1 + examples/micronaut/build.gradle | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../main/groovy/example/micronaut/Application.groovy | 12 ++++++++++++ .../groovy/example/micronaut/HelloController.groovy | 4 ++++ .../src/main/java/example/micronaut/Application.java | 10 ---------- .../micronaut/src/main/resources/application.yml | 4 +--- .../example/micronaut/HelloControllerSpec.groovy | 1 - 8 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 examples/micronaut/src/main/groovy/example/micronaut/Application.groovy delete mode 100644 examples/micronaut/src/main/java/example/micronaut/Application.java diff --git a/.gitignore b/.gitignore index 898930fb52..7d9acc3bab 100755 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ out /.settings /.classpath /.project +.DS_Store diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 821c292bbd..941e8d40d1 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -16,7 +16,7 @@ repositories { dependencyManagement { imports { - mavenBom 'io.micronaut:micronaut-bom:1.0.0' + mavenBom 'io.micronaut:micronaut-bom:1.0.3' } } @@ -31,7 +31,7 @@ dependencies { compileOnly 'io.micronaut:micronaut-inject-java' runtime 'ch.qos.logback:logback-classic:1.2.3' testCompile 'io.micronaut:micronaut-inject-groovy' - testCompile('org.spockframework:spock-core:1.1-groovy-2.4') { + testCompile('org.spockframework:spock-core:1.2-groovy-2.5') { exclude group: 'org.codehaus.groovy', module: 'groovy-all' } testCompile 'io.micronaut:micronaut-inject-java' diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.properties b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d70..6b3851a8ad 100644 --- a/examples/micronaut/gradle/wrapper/gradle-wrapper.properties +++ b/examples/micronaut/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy b/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy new file mode 100644 index 0000000000..7db7c36e04 --- /dev/null +++ b/examples/micronaut/src/main/groovy/example/micronaut/Application.groovy @@ -0,0 +1,12 @@ +package example.micronaut + +import groovy.transform.CompileStatic +import io.micronaut.runtime.Micronaut + +@CompileStatic +class Application { + + static void main(String[] args) { + Micronaut.run(Application.class) + } +} \ No newline at end of file diff --git a/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy index 8566266880..c3414a3051 100644 --- a/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy +++ b/examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy @@ -1,12 +1,16 @@ package example.micronaut import groovy.transform.CompileStatic +import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces @CompileStatic @Controller("/hello") // <1> class HelloController { + + @Produces(MediaType.TEXT_PLAIN) @Get("/") // <2> String index() { "Hello World" // <3> diff --git a/examples/micronaut/src/main/java/example/micronaut/Application.java b/examples/micronaut/src/main/java/example/micronaut/Application.java deleted file mode 100644 index 63711495e4..0000000000 --- a/examples/micronaut/src/main/java/example/micronaut/Application.java +++ /dev/null @@ -1,10 +0,0 @@ -package example.micronaut; - -import io.micronaut.runtime.Micronaut; - -public class Application { - - public static void main(String[] args) { - Micronaut.run(Application.class); - } -} \ No newline at end of file diff --git a/examples/micronaut/src/main/resources/application.yml b/examples/micronaut/src/main/resources/application.yml index 078029e9f6..0a6f9ed238 100644 --- a/examples/micronaut/src/main/resources/application.yml +++ b/examples/micronaut/src/main/resources/application.yml @@ -1,5 +1,3 @@ micronaut: application: - name: examples - server: - port: 8080 \ No newline at end of file + name: examples \ No newline at end of file diff --git a/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy index 5680fa0158..8517a5c5cc 100644 --- a/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy +++ b/examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy @@ -10,7 +10,6 @@ import spock.lang.Specification class HelloControllerSpec extends Specification { - @Shared @AutoCleanup // <1> EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) // <2> From 177ba33248e422411a74d130ba9974861f9353f5 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 22 Jan 2019 10:14:25 -0500 Subject: [PATCH 0452/2020] Do not fail when attempting to delete temp directory (#1439) --- .../cloud/tools/jib/filesystem/TemporaryDirectory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java index b55d573fdb..99977dddbb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java @@ -19,7 +19,9 @@ import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import java.io.Closeable; +import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; @@ -53,7 +55,11 @@ public Path getDirectory() { @Override public void close() throws IOException { if (Files.exists(temporaryDirectory)) { - MoreFiles.deleteRecursively(temporaryDirectory, RecursiveDeleteOption.ALLOW_INSECURE); + try { + MoreFiles.deleteRecursively(temporaryDirectory, RecursiveDeleteOption.ALLOW_INSECURE); + } catch (FileNotFoundException | FileSystemException ex) { + // TODO log error; deletion is best-effort + } } } } From 29e87e74bed845eff3010f482b6345f96dd15723 Mon Sep 17 00:00:00 2001 From: sullis Date: Tue, 22 Jan 2019 08:57:48 -0800 Subject: [PATCH 0453/2020] guava 27.0.1-jre (#1437) --- jib-core/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 8480e34064..7274ac5d9d 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -37,7 +37,7 @@ dependencies { // Make sure these are consistent with jib-maven-plugin. implementation 'com.google.http-client:google-http-client:1.27.0' implementation 'org.apache.commons:commons-compress:1.18' - implementation 'com.google.guava:guava:23.5-jre' + implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' implementation 'org.ow2.asm:asm:7.0' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index bf31692c81..71730f0749 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -65,7 +65,7 @@ com.google.guava guava - 23.5-jre + 27.0.1-jre compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 3030da8a0e..3060a5ab46 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -31,7 +31,7 @@ dependencies { // Make sure these are consistent with jib-maven-plugin. compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:23.5-jre' + compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' compile 'org.ow2.asm:asm:7.0' From 66afa6047de88f4e8fed9b3e0d581f70ce94ca3a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 22 Jan 2019 12:40:20 -0500 Subject: [PATCH 0454/2020] Upgrade Guava to 27.0.1-jre (#1441) --- jib-gradle-plugin/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 5d49f7d984..12cc9423a9 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -63,7 +63,7 @@ dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:23.5-jre' + compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' compile 'org.ow2.asm:asm:7.0' From fdffc6fdc71e0c370fe14bd185172c92572937dd Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 23 Jan 2019 17:15:31 -0500 Subject: [PATCH 0455/2020] Fix progress display tests ignoring locale (#1444) --- .../common/logging/ProgressDisplayGeneratorTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java index 0795a47eec..cd57690e5e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java @@ -25,24 +25,28 @@ /** Tests for {@link com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator}. */ public class ProgressDisplayGeneratorTest { + private static String getBar(String bar, double value) { + return String.format("%s %.1f%% complete", bar, value); + } + @Test public void testGenerateProgressDisplay_progressBar_0() { Assert.assertEquals( - Arrays.asList("Executing tasks:", "[ ] 0.0% complete"), + Arrays.asList("Executing tasks:", getBar("[ ]", 0.0)), ProgressDisplayGenerator.generateProgressDisplay(0, Collections.emptyList())); } @Test public void testGenerateProgressDisplay_progressBar_50() { Assert.assertEquals( - Arrays.asList("Executing tasks:", "[=============== ] 50.0% complete"), + Arrays.asList("Executing tasks:", getBar("[=============== ]", 50.0)), ProgressDisplayGenerator.generateProgressDisplay(0.5, Collections.emptyList())); } @Test public void testGenerateProgressDisplay_progressBar_100() { Assert.assertEquals( - Arrays.asList("Executing tasks:", "[==============================] 100.0% complete"), + Arrays.asList("Executing tasks:", getBar("[==============================]", 100.0)), ProgressDisplayGenerator.generateProgressDisplay(1, Collections.emptyList())); } @@ -56,7 +60,7 @@ public void testGenerateProgressDisplay_unfinishedTasks() { Assert.assertEquals( Arrays.asList( "Executing tasks:", - "[=============== ] 50.0% complete", + getBar("[=============== ]", 50.0), "> childLeftDown", "> childRight"), ProgressDisplayGenerator.generateProgressDisplay( From 704d276148a7950f553534950c4df9c265057437 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 24 Jan 2019 13:28:31 -0500 Subject: [PATCH 0456/2020] Changes stability badge to stable. (#1448) --- README.md | 2 +- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a84b129287..33ada16f38 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) [![Maven Central](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/com/google/cloud/tools/jib-maven-plugin/maven-metadata.xml.svg?colorB=007ec6)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) [![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) ![Build Status](https://storage.googleapis.com/cloud-tools-for-java-kokoro-build-badges/jib-ubuntu-master-orb.svg) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 1b80fcd637..9dfa3465c3 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -1,4 +1,4 @@ -![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) [![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/google/cloud/tools/jib/com.google.cloud.tools.jib.gradle.plugin/maven-metadata.xml.svg?colorB=007ec6&label=gradle)](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 3dd941c5f7..f90426cba4 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -1,4 +1,4 @@ -![beta](https://img.shields.io/badge/stability-beta-darkorange.svg) +![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-maven-plugin) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) From 6b6b0fd32493593f3fc4bc5597ce6e664108e83b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 28 Jan 2019 12:52:21 -0500 Subject: [PATCH 0457/2020] Update README about WAR configuration (#1450) --- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 9dfa3465c3..6f25e43139 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -394,7 +394,7 @@ Note that Jib will work slightly differently for WAR projects from JAR projects: - `container.mainClass` and `container.jvmFlags` are ignored. - The WAR will be exploded into `/jetty/webapps/ROOT`, which is the expected WAR location for the distroless Jetty base image. -To use a different Servlet engine base image, you can customize `container.appRoot`, `container.entrypoint`, and `container.args`. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may need to configure them. However, you will most likely have to set `container.appRoot` to a proper location depending on the base image. Here is an example of using a Tomcat image: +To use a different Servlet engine base image, you can customize `container.appRoot`, `container.entrypoint`, and `container.args`. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may not need to configure them. However, you will most likely have to set `container.appRoot` to a proper location depending on the base image. Here is an example of using a Tomcat image: ```gradle jib { diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index f90426cba4..450bdcc915 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -483,7 +483,7 @@ Note that Jib will work slightly differently for WAR projects from JAR projects: - `` and `` are ignored. - The WAR will be exploded into `/jetty/webapps/ROOT`, which is the expected WAR location for the distroless Jetty base image. -To use a different Servlet engine base image, you can customize ``, ``, and ``. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may need to configure them. However, you will most likely have to set `` to a proper location depending on the base image. Here is an example of using a Tomcat image: +To use a different Servlet engine base image, you can customize ``, ``, and ``. If you do not set `entrypoint` or `args`, Jib will inherit the `ENTRYPOINT` and `CMD` of the base image, so in many cases, you may not need to configure them. However, you will most likely have to set `` to a proper location depending on the base image. Here is an example of using a Tomcat image: ```xml From 83ab265831daada12e2c1cbd79912b5ff322ad92 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 29 Jan 2019 11:55:50 -0500 Subject: [PATCH 0458/2020] Add enum for matching progress events to build steps (#1449) --- .../tools/jib/builder/BuildStepType.java | 60 +++++++++++++++++++ .../jib/builder/ProgressEventDispatcher.java | 22 ++++--- .../builder/steps/AuthenticatePushStep.java | 4 +- .../BuildAndCacheApplicationLayerStep.java | 10 +++- .../jib/builder/steps/BuildImageStep.java | 4 +- .../jib/builder/steps/LoadDockerStep.java | 4 +- .../ProgressEventDispatcherContainer.java | 9 ++- .../steps/PullAndCacheBaseImageLayerStep.java | 9 ++- .../PullAndCacheBaseImageLayersStep.java | 5 +- .../jib/builder/steps/PullBaseImageStep.java | 7 ++- .../tools/jib/builder/steps/PushBlobStep.java | 10 +++- .../steps/PushContainerConfigurationStep.java | 7 ++- .../jib/builder/steps/PushImageStep.java | 7 ++- .../jib/builder/steps/PushLayersStep.java | 7 ++- .../RetrieveRegistryCredentialsStep.java | 18 ++++-- .../jib/builder/steps/WriteTarFileStep.java | 4 +- .../tools/jib/event/events/ProgressEvent.java | 16 ++++- .../builder/ProgressEventDispatcherTest.java | 2 +- .../jib/event/events/ProgressEventTest.java | 31 +++++++--- .../progress/ProgressEventHandlerTest.java | 13 ++-- 20 files changed, 199 insertions(+), 50 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java new file mode 100644 index 0000000000..002f230aab --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder; + +/** Types corresponding to steps in the containerization process. */ +public enum BuildStepType { + + /** Root type that corresponds to build startup. */ + ALL, + + /** Authentication step for pushing to target registry. */ + AUTHENTICATE_PUSH, + + /** Step for building/caching an application layer. */ + BUILD_AND_CACHE_APPLICATION_LAYER, + + /** Step for building image layers/configuration. */ + BUILD_IMAGE, + + /** Step for loading the image into the Docker daemon. */ + LOAD_DOCKER, + + /** Step for pulling/caching a base image layer. */ + PULL_AND_CACHE_BASE_IMAGE_LAYER, + + /** Step for pulling the base image manifest. */ + PULL_BASE_IMAGE, + + /** Step for pushing the container configuration to the target registry. */ + PUSH_CONTAINER_CONFIGURATION, + + /** Step for pushing the image manifest to the target registry. */ + PUSH_IMAGE, + + /** Step for pushing the image layers to the target registry. */ + PUSH_LAYERS, + + /** Step for retrieving credentials for the base image registry. */ + RETRIEVE_REGISTRY_CREDENTIALS_BASE, + + /** Step for retrieving credentials for the target image registry. */ + RETRIEVE_REGISTRY_CREDENTIALS_TARGET, + + /** Step for writing the image tarball to disk. */ + WRITE_TAR_FILE +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index 5b01cc3f06..fbfeefbaef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -43,11 +43,13 @@ public interface Factory { /** * Creates the {@link ProgressEventDispatcher} with an associated {@link Allocation}. * + * @param buildStepType the build step that the progress events correspond to * @param description user-facing description of what the allocation represents * @param allocationUnits number of allocation units * @return the new {@link ProgressEventDispatcher} */ - ProgressEventDispatcher create(String description, long allocationUnits); + ProgressEventDispatcher create( + BuildStepType buildStepType, String description, long allocationUnits); } /** @@ -61,7 +63,7 @@ public interface Factory { public static ProgressEventDispatcher newRoot( EventDispatcher eventDispatcher, String description, long allocationUnits) { return newProgressEventDispatcher( - eventDispatcher, Allocation.newRoot(description, allocationUnits)); + eventDispatcher, Allocation.newRoot(description, allocationUnits), BuildStepType.ALL); } /** @@ -73,22 +75,25 @@ public static ProgressEventDispatcher newRoot( * @return a new {@link ProgressEventDispatcher} */ private static ProgressEventDispatcher newProgressEventDispatcher( - EventDispatcher eventDispatcher, Allocation allocation) { + EventDispatcher eventDispatcher, Allocation allocation, BuildStepType buildStepType) { ProgressEventDispatcher progressEventDispatcher = - new ProgressEventDispatcher(eventDispatcher, allocation); + new ProgressEventDispatcher(eventDispatcher, allocation, buildStepType); progressEventDispatcher.dispatchProgress(0); return progressEventDispatcher; } private final EventDispatcher eventDispatcher; private final Allocation allocation; + private final BuildStepType buildStepType; private long remainingAllocationUnits; private boolean closed = false; - private ProgressEventDispatcher(EventDispatcher eventDispatcher, Allocation allocation) { + private ProgressEventDispatcher( + EventDispatcher eventDispatcher, Allocation allocation, BuildStepType buildStepType) { this.eventDispatcher = eventDispatcher; this.allocation = allocation; + this.buildStepType = buildStepType; remainingAllocationUnits = allocation.getAllocationUnits(); } @@ -108,11 +113,12 @@ public Factory newChildProducer() { private boolean used = false; @Override - public ProgressEventDispatcher create(String description, long allocationUnits) { + public ProgressEventDispatcher create( + BuildStepType buildStepType, String description, long allocationUnits) { Preconditions.checkState(!used); used = true; return newProgressEventDispatcher( - eventDispatcher, allocation.newChild(description, allocationUnits)); + eventDispatcher, allocation.newChild(description, allocationUnits), buildStepType); } }; } @@ -134,7 +140,7 @@ public void close() { */ public void dispatchProgress(long progressUnits) { decrementRemainingAllocationUnits(progressUnits); - eventDispatcher.dispatch(new ProgressEvent(allocation, progressUnits)); + eventDispatcher.dispatch(new ProgressEvent(allocation, progressUnits, buildStepType)); } private void decrementRemainingAllocationUnits(long units) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 25180b4b11..6f9ad4cc5d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -80,7 +81,8 @@ public Authorization call() String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("authenticating push to " + registry, 1); + progressEventDispatcherFactory.create( + BuildStepType.AUTHENTICATE_PUSH, "authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index c36ceb6e98..c3da01b053 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; @@ -51,7 +52,9 @@ static ImmutableList makeList( try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "setting up to build application layers", layerCount); + BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, + "setting up to build application layers", + layerCount); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = @@ -108,7 +111,10 @@ public CachedLayer call() throws IOException, CacheCorruptedException { buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("building " + layerType + " layer", 1); + progressEventDispatcherFactory.create( + BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, + "building " + layerType + " layer", + 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 7fce8d5afd..64443c37bd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -92,7 +93,8 @@ public AsyncStep> call() throws ExecutionException { private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("building image format", 1); + progressEventDispatcherFactory.create( + BuildStepType.BUILD_IMAGE, "building image format", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 1f605118c4..02bf62b57b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -94,7 +95,8 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() .dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("loading to Docker daemon", 1)) { + progressEventDispatcherFactory.create( + BuildStepType.LOAD_DOCKER, "loading to Docker daemon", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java index dec33f0f5e..d1ff257010 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.common.base.Preconditions; @@ -35,12 +36,16 @@ class ProgressEventDispatcherContainer implements BlobProgressListener, Closeabl private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String description; + private final BuildStepType type; @Nullable private ProgressEventDispatcher progressEventDispatcher; ProgressEventDispatcherContainer( - ProgressEventDispatcher.Factory progressEventDispatcherFactory, String description) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + String description, + BuildStepType type) { this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.description = description; + this.type = type; } @Override @@ -62,6 +67,6 @@ public void close() { void initializeWithBlobSize(long blobSize) { Preconditions.checkState(progressEventDispatcher == null); - progressEventDispatcher = progressEventDispatcherFactory.create(description, blobSize); + progressEventDispatcher = progressEventDispatcherFactory.create(type, description, blobSize); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 53e897d959..9c10f3060d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; @@ -68,7 +69,10 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); + progressEventDispatcherFactory.create( + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, + "checking base image layer " + layerDigest, + 1); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { @@ -89,7 +93,8 @@ public CachedLayer call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcherContainer progressEventDispatcherContainer = new ProgressEventDispatcherContainer( progressEventDispatcher.newChildProducer(), - "pulling base image layer " + layerDigest)) { + "pulling base image layer " + layerDigest, + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER)) { return cache.writeCompressedLayer( registryClient.pullBlob( layerDigest, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index efb038e28b..28312072ec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; @@ -75,7 +76,9 @@ public ImmutableList call() try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "checking base image layers", baseImageLayers.size()); + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, + "checking base image layers", + baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index dadbe8cc9a..42219745ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; @@ -118,7 +119,8 @@ public BaseImageWithAuthorization call() + "...")); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pulling base image manifest", 2); + progressEventDispatcherFactory.create( + BuildStepType.PULL_BASE_IMAGE, "pulling base image manifest", 2); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // First, try with no credentials. @@ -233,7 +235,8 @@ private Image pullBaseImage( try (ProgressEventDispatcherContainer progressEventDispatcherContainer = new ProgressEventDispatcherContainer( progressEventDispatcher.newChildProducer(), - "pull container configuration " + containerConfigurationDigest)) { + "pull container configuration " + containerConfigurationDigest, + BuildStepType.PULL_BASE_IMAGE)) { String containerConfigurationString = Blobs.writeToString( registryClient.pullBlob( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 88e0a91bfa..cdc996549a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -65,6 +66,7 @@ public Duration getDelayBetweenCallbacks() { private final AuthenticatePushStep authenticatePushStep; private final BlobDescriptor blobDescriptor; private final Blob blob; + private final BuildStepType buildStepType; private final ListenableFuture listenableFuture; @@ -74,12 +76,14 @@ public Duration getDelayBetweenCallbacks() { ProgressEventDispatcher.Factory progressEventDipatcherFactory, AuthenticatePushStep authenticatePushStep, BlobDescriptor blobDescriptor, - Blob blob) { + Blob blob, + BuildStepType buildStepType) { this.buildConfiguration = buildConfiguration; this.progressEventDipatcherFactory = progressEventDipatcherFactory; this.authenticatePushStep = authenticatePushStep; this.blobDescriptor = blobDescriptor; this.blob = blob; + this.buildStepType = buildStepType; listenableFuture = AsyncDependencies.using(listeningExecutorService) @@ -96,7 +100,9 @@ public ListenableFuture getFuture() { public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDipatcherFactory.create( - "pushing blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); + buildStepType, + "pushing blob " + blobDescriptor.getDigest(), + blobDescriptor.getSize()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 03305ff20d..4d6835c59d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -85,7 +86,8 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pushing container configuration", 1); + progressEventDispatcherFactory.create( + BuildStepType.PUSH_CONTAINER_CONFIGURATION, "pushing container configuration", 1); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); @@ -100,7 +102,8 @@ private PushBlobStep afterBuildConfigurationFutureFuture() progressEventDispatcher.newChildProducer(), authenticatePushStep, blobDescriptor, - containerConfigurationBlob); + containerConfigurationBlob, + BuildStepType.PUSH_CONTAINER_CONFIGURATION); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index d60ff07038..4d7821b2ec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -121,7 +122,8 @@ private BuildResult afterAllPushed() throws ExecutionException, IOException, InterruptedException { ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); + progressEventDispatcherFactory.create( + BuildStepType.PUSH_IMAGE, "pushing image manifest", targetImageTags.size()); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { @@ -152,7 +154,8 @@ private BuildResult afterAllPushed() listeningExecutorService.submit( () -> { try (ProgressEventDispatcher ignored2 = - progressEventDispatcherFactory.create("tagging with " + tag, 1)) { + progressEventDispatcherFactory.create( + BuildStepType.PUSH_IMAGE, "tagging with " + tag, 1)) { buildConfiguration .getEventDispatcher() .dispatch(LogEvent.info("Tagging with " + tag + "...")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 52339a0c52..b0efa481ae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; @@ -79,7 +80,8 @@ public ImmutableList> call() throws ExecutionException { NonBlockingSteps.get(cachedLayerStep); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("setting up to push layers", cachedLayers.size())) { + progressEventDispatcherFactory.create( + BuildStepType.PUSH_LAYERS, "setting up to push layers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); @@ -111,6 +113,7 @@ private PushBlobStep makePushBlobStep( progressEventDispatcherFactory, authenticatePushStep, new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), - cachedLayer.getBlob()); + cachedLayer.getBlob(), + BuildStepType.PUSH_LAYERS); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 090d565918..fe2a82403e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -49,7 +50,8 @@ static RetrieveRegistryCredentialsStep forBaseImage( buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); + buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers(), + BuildStepType.RETRIEVE_REGISTRY_CREDENTIALS_BASE); } /** Retrieves credentials for the target image. */ @@ -62,7 +64,8 @@ static RetrieveRegistryCredentialsStep forTargetImage( buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); + buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers(), + BuildStepType.RETRIEVE_REGISTRY_CREDENTIALS_TARGET); } private final BuildConfiguration buildConfiguration; @@ -70,6 +73,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( private final String registry; private final ImmutableList credentialRetrievers; + private final BuildStepType buildStepType; private final ListenableFuture listenableFuture; @@ -79,11 +83,13 @@ static RetrieveRegistryCredentialsStep forTargetImage( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String registry, - ImmutableList credentialRetrievers) { + ImmutableList credentialRetrievers, + BuildStepType buildStepType) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registry = registry; this.credentialRetrievers = credentialRetrievers; + this.buildStepType = buildStepType; listenableFuture = listeningExecutorService.submit(this); } @@ -101,7 +107,8 @@ public Credential call() throws CredentialRetrievalException { buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("retrieving credentials for " + registry, 1); + progressEventDispatcherFactory.create( + buildStepType, "retrieving credentials for " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { @@ -111,8 +118,7 @@ public Credential call() throws CredentialRetrievalException { } } - // If no credentials found, give an info (not warning because in most cases, the base image - // is + // If no credentials found, give an info (not warning because in most cases, the base image is // public and does not need extra credentials) and return null. buildConfiguration .getEventDispatcher() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 661c05da09..da3089a01c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; @@ -94,7 +95,8 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("writing to tar file", 1)) { + progressEventDispatcherFactory.create( + BuildStepType.WRITE_TAR_FILE, "writing to tar file", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Builds the image to a tarball. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java index 53665ea250..e8da3fa584 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event.events; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.progress.Allocation; @@ -36,9 +37,13 @@ public class ProgressEvent implements JibEvent { /** Units of progress. */ private final long progressUnits; - public ProgressEvent(Allocation allocation, long progressUnits) { + /** The step in the build process that the progress event corresponds to. */ + private final BuildStepType buildStepType; + + public ProgressEvent(Allocation allocation, long progressUnits, BuildStepType buildStepType) { this.allocation = allocation; this.progressUnits = progressUnits; + this.buildStepType = buildStepType; } /** @@ -59,4 +64,13 @@ public Allocation getAllocation() { public long getUnits() { return progressUnits; } + + /** + * Gets the build step that the progress event corresponds to. + * + * @return the build step + */ + public BuildStepType getBuildStepType() { + return buildStepType; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java index 2c92cd8929..16cc527773 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -39,7 +39,7 @@ public void testDispatch() { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 10); ProgressEventDispatcher ignored = - progressEventDispatcher.newChildProducer().create("ignored", 20)) { + progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 20)) { // empty } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java index 4c5452f9c5..705b56bc21 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event.events; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; @@ -71,16 +72,16 @@ public void testAccumulateProgress() { EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2 / 100 * 50, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 10)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 10, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2 + 1.0 / 2 / 200 * 10, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 190)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 190, BuildStepType.ALL)); Assert.assertEquals(1.0, progress, DOUBLE_ERROR_MARGIN); } @@ -96,19 +97,19 @@ public void testSmoke() { EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1, allocationCompletionMap.size()); Assert.assertEquals(50, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(3, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.root).longValue()); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 200)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 200, BuildStepType.ALL)); Assert.assertEquals(4, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); @@ -117,6 +118,22 @@ public void testSmoke() { Assert.assertEquals(2, allocationCompletionMap.get(AllocationTree.root).longValue()); } + @Test + public void testType() { + // Used to test whether or not progress event was consumed + boolean[] called = new boolean[] {false}; + Consumer buildImageConsumer = + progressEvent -> { + Assert.assertEquals(BuildStepType.BUILD_IMAGE, progressEvent.getBuildStepType()); + called[0] = true; + }; + + EventDispatcher buildImageDispatcher = makeEventDispatcher(buildImageConsumer); + buildImageDispatcher.dispatch( + new ProgressEvent(AllocationTree.child1, 50, BuildStepType.BUILD_IMAGE)); + Assert.assertTrue(called[0]); + } + /** * Updates the {@link #allocationCompletionMap} with {@code units} more progress for {@code * allocation}. This also updates {@link Allocation} parents if {@code allocation} is complete in diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 353771bc87..2632906694 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -68,17 +68,17 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce // Adds root, child1, and child1Child. multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.root, 0L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.root, 0L, null)); return null; }); multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); return null; }); multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L, null)); return null; }); Assert.assertEquals(0.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); @@ -89,18 +89,19 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 50, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L, null)); return null; })); callables.addAll( Collections.nCopies( 100, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 1L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 1L, null)); return null; })); multithreadedExecutor.invokeAll(callables); + Assert.assertEquals( 1.0 / 2 / 100 * 50 + 1.0 / 2 / 200 * 100, maxProgress.get(), DOUBLE_ERROR_MARGIN); @@ -109,7 +110,7 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 100, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); + eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); return null; })); Assert.assertEquals( From 9090f85ed78feedab5ba4eccf0691104814ea427 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 30 Jan 2019 11:30:24 -0500 Subject: [PATCH 0459/2020] Update changelog for progress event changes (#1453) --- jib-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index c9f9590655..a0d294c256 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) + ### Changed ### Fixed From 73d505073cb4268a795bf46189763bd9b7cc0fd8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 30 Jan 2019 16:10:38 -0500 Subject: [PATCH 0460/2020] Fix main class finder missing main defined with varargs (#1457) --- jib-core/CHANGELOG.md | 2 ++ .../cloud/tools/jib/frontend/MainClassFinder.java | 3 ++- .../tools/jib/frontend/MainClassFinderTest.java | 11 +++++++++++ .../class-finder-tests/varargs/HelloWorld.class | Bin 0 -> 760 bytes jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 6 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index a0d294c256..8da24a01db 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- `MainClassFinder` failure when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + ## 0.1.1 ### Added diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index a0152ee260..7a28b4414c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -115,7 +115,8 @@ private static class MainClassVisitor extends ClassVisitor { Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String[].class)); /** Accessors that main may or may not have. */ - private static final int OPTIONAL_ACCESS = Opcodes.ACC_FINAL | Opcodes.ACC_DEPRECATED; + private static final int OPTIONAL_ACCESS = + Opcodes.ACC_FINAL | Opcodes.ACC_DEPRECATED | Opcodes.ACC_VARARGS; private boolean visitedMainClass; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index 7e84e0e591..e0f379cf48 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -126,4 +126,15 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld$InnerClass")); } + + @Test + public void testMainClass_varargs() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("core/class-finder-tests/varargs").toURI()); + MainClassFinder.Result mainClassFinderResult = + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); + Assert.assertThat( + mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); + } } diff --git a/jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class b/jib-core/src/test/resources/core/class-finder-tests/varargs/HelloWorld.class new file mode 100644 index 0000000000000000000000000000000000000000..82a32d82a3d8620723f1a615ca0938a1eb836fbd GIT binary patch literal 760 zcmZuvQBM;=5dP+_^xEUF*wP|a1O)MV2xo|I5FRu!m{cXHsWCon+f7}(-ZjUSgm+*3 z8@~9g4<*rrAHZ+mml&hW9!(mgm+Z{#H{Z;+GrQ-1f1Uw&iYGQ~RP(rpMGNfcr)xw&EhlK2|3RSX4Fx^^{ka-?;B%x4Op?p2)wPgHOv;szo^{6d^rihi^ zkHAdwQ5_MS`o0W;=v@>CogJ>x6DlNB+z<6baV-2mgk8Up#47CWXbn=lUNTR_UCu5| zpb=&oiD-X%CHf;NyAciIwtS)VVG3itEiK_-8jgi^2an(q=Kq&{Hc&xF#@yt-T6R!_ z=U@YmEo?g2f={r=1qrj`^f#?T*-oxOjgzB9_PA6u;6VARMHTr6++V_7O3~vPWJSL( z!wzB7oiKzO(CC1im`kFoQ4!|c+6^KzO6l>gD-x`})(?2@K8g8BXw znc~zCS+Bayb_i>T+-IV1|1PwG39V4%T<{~!VG*nBYgkIvDqPXvi=0C_)%8%pJTvd` S%ER~rgMMS3LkkQTxO)kocc+d3 literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index efdfbc6f7c..e557189d18 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + ## 1.0.0 ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 5bf81ac60e..16141b662b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) + ## 1.0.0 ### Changed From 06bfb90a0ff3615335a22db472b2b934a960093c Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 31 Jan 2019 11:51:31 -0500 Subject: [PATCH 0461/2020] Explicitly note JVM flags for Java 8 (#1454) --- examples/dropwizard/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 52ff11d72e..339c579bdc 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -70,7 +70,7 @@ 8080 - + -server -Djava.awt.headless=true diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 85d373140d..a2a7319fce 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -43,7 +43,7 @@ jib { ports = listOf("8080") mainClass = main_class - // good defauls intended for containers + // good defauls intended for Java 8 containers jvmFlags = listOf( "-server", "-Djava.awt.headless=true", From fb3ac748e429524e982a654ec041d8ebbc99e4b1 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Tue, 5 Feb 2019 13:52:24 -0500 Subject: [PATCH 0462/2020] Adds examples to Jib Core. (#1463) --- examples/README.md | 2 + jib-core/examples/README.md | 9 +++++ jib-core/examples/build.gradle/README.md | 51 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 jib-core/examples/README.md create mode 100644 jib-core/examples/build.gradle/README.md diff --git a/examples/README.md b/examples/README.md index 6cce6ac8c1..0bd5693822 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,8 @@ Please [file an issue](/../../issues/new) if you find any problems with the examples or would like to request other examples. +For examples on using Jib Core, see [jib-core/examples](../jib-core/examples). + ### Simple example See [helloworld](helloworld) for containerizing a simple `Hello World` application. diff --git a/jib-core/examples/README.md b/jib-core/examples/README.md new file mode 100644 index 0000000000..0a14ab6345 --- /dev/null +++ b/jib-core/examples/README.md @@ -0,0 +1,9 @@ +# Jib Core examples + +Please [file an issue](/../../issues/new) if you find any problems with the examples or would like to request other examples. + +For examples on using Jib plugins, see [examples](../../examples). + +### Using Jib Core in Gradle builds + +See [build.gradle](build.gradle) for examples using Jib Core to build and manipulate container images within a `build.gradle` script. diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md new file mode 100644 index 0000000000..ef94060b2a --- /dev/null +++ b/jib-core/examples/build.gradle/README.md @@ -0,0 +1,51 @@ +# Examples using Jib Core in Gradle builds + +Jib Core is a containerization library for JVM languages, so it works well in Groovy as well. You can use Jib Core directly within a Gradle `build.gradle` to make tasks that build and manipulate container images. + +For example, the following snippet is a simple example that creates a Gradle task that adds an environment variable to an existing image: + +`build.gradle`: + +```groovy +// Imports Jib Core as a library to use in this build script. +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.cloud.tools:jib-core:0.1.1' + } +} + +import com.google.cloud.tools.jib.api.Jib +import com.google.cloud.tools.jib.api.Containerizer +import com.google.cloud.tools.jib.api.RegistryImage +import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory +import com.google.cloud.tools.jib.image.ImageReference + +// Creates a task called 'dojib'. +task('dojib') { + doLast { + def targetImage = '' + Jib + // Starts with the existing image. + .from(' Date: Fri, 8 Feb 2019 15:06:19 -0500 Subject: [PATCH 0463/2020] Add event for counting layers in pull/build/push steps (#1461) --- jib-core/CHANGELOG.md | 1 + .../builder/BuildStepsIntegrationTest.java | 60 ++++++++++++++++--- .../tools/jib/builder/BuildStepType.java | 7 ++- .../BuildAndCacheApplicationLayerStep.java | 9 ++- .../PullAndCacheBaseImageLayersStep.java | 6 ++ .../jib/builder/steps/PushLayersStep.java | 13 +++- .../tools/jib/builder/steps/StepsRunner.java | 7 ++- .../cloud/tools/jib/event/JibEventType.java | 5 ++ .../jib/event/events/LayerCountEvent.java | 56 +++++++++++++++++ 9 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 8da24a01db..f97d16821f 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) +- `LayerCountEvent` that is dispatched at the beginning of certain pull/build/push build steps to indicate the number of layers being processed ([#1461](https://github.com/GoogleContainerTools/jib/pull/1461)) ### Changed diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index ea269d54ad..d6d2d94506 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; +import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -44,9 +45,12 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; import java.util.stream.Stream; import org.hamcrest.CoreMatchers; import org.junit.AfterClass; @@ -86,6 +90,8 @@ private void checkCompletion() { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); private static final ExecutorService executorService = Executors.newCachedThreadPool(); private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); + private static final String DISTROLESS_DIGEST = + "sha256:f488c213f278bc5f9ffe3ddf30c5dbb2303a15a74146b738d12453088e662880"; private static final double DOUBLE_ERROR_MARGIN = 1e-10; @@ -144,6 +150,14 @@ private static void assertDockerInspect(String imageReference) private ImmutableList fakeLayerConfigurations; + private final Map layerCounts = new ConcurrentHashMap<>(); + private final Consumer layerCountConsumer = + layerCountEvent -> + layerCounts.merge( + layerCountEvent.getBuildStepType(), + layerCountEvent.getCount(), + (oldValue, count) -> oldValue + count); + @Before public void setUp() throws IOException, URISyntaxException { fakeLayerConfigurations = @@ -165,15 +179,27 @@ public void testSteps_forBuildToDockerRegistry() BuildResult image1 = BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of("localhost:5000", "testimage", "testtag")) .setEventDispatcher( new DefaultEventDispatcher( new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) + .add(JibEventType.LAYER_COUNT, layerCountConsumer))) .build()) .run(); progressChecker.checkCompletion(); + Assert.assertEquals( + layerCounts, + ImmutableMap.of( + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, + 4, + BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, + 3, + BuildStepType.PUSH_BASE_LAYERS, + 4, + BuildStepType.PUSH_APPLICATION_LAYERS, + 3)); logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); @@ -181,7 +207,7 @@ public void testSteps_forBuildToDockerRegistry() BuildResult image2 = BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of("localhost:5000", "testimage", "testtag")) .build()) .run(); @@ -209,7 +235,7 @@ public void testSteps_forBuildToDockerRegistry_multipleTags() BuildSteps buildImageSteps = BuildSteps.forBuildToDockerRegistry( getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of("localhost:5000", "testimage", "testtag")) .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) .build()); @@ -265,16 +291,24 @@ public void testSteps_forBuildToDockerDaemon() BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of(null, "testdocker", null)) .setEventDispatcher( new DefaultEventDispatcher( new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) + .add(JibEventType.LAYER_COUNT, layerCountConsumer))) .build(); BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); progressChecker.checkCompletion(); + Assert.assertEquals( + layerCounts, + ImmutableMap.of( + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, + 4, + BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, + 3)); assertDockerInspect("testdocker"); Assert.assertEquals( @@ -287,7 +321,7 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() String imageReference = "testdocker"; BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of(null, imageReference, null)) .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) .build(); @@ -313,17 +347,25 @@ public void testSteps_forBuildToTarball() BuildConfiguration buildConfiguration = getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", "latest"), + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), ImageReference.of(null, "testtar", null)) .setEventDispatcher( new DefaultEventDispatcher( new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler))) + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) + .add(JibEventType.LAYER_COUNT, layerCountConsumer))) .build(); Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); BuildSteps.forBuildToTar(outputPath, buildConfiguration).run(); progressChecker.checkCompletion(); + Assert.assertEquals( + layerCounts, + ImmutableMap.of( + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, + 4, + BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, + 3)); new Command("docker", "load", "--input", outputPath.toString()).run(); Assert.assertEquals( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java index 002f230aab..bd38e04913 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java @@ -46,8 +46,11 @@ public enum BuildStepType { /** Step for pushing the image manifest to the target registry. */ PUSH_IMAGE, - /** Step for pushing the image layers to the target registry. */ - PUSH_LAYERS, + /** Step for pushing the application image layers to the target registry. */ + PUSH_APPLICATION_LAYERS, + + /** Step for pushing the base image layers to the target registry. */ + PUSH_BASE_LAYERS, /** Step for retrieving credentials for the base image registry. */ RETRIEVE_REGISTRY_CREDENTIALS_BASE, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index c3da01b053..dc33132d21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; @@ -73,7 +74,13 @@ static ImmutableList makeList( layerConfiguration.getName(), layerConfiguration)); } - return buildAndCacheApplicationLayerSteps.build(); + ImmutableList steps = + buildAndCacheApplicationLayerSteps.build(); + buildConfiguration + .getEventDispatcher() + .dispatch( + new LayerCountEvent(BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, steps.size())); + return steps; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 28312072ec..db47168c88 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -83,6 +84,11 @@ public ImmutableList call() new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = ImmutableList.builderWithExpectedSize(baseImageLayers.size()); + buildConfiguration + .getEventDispatcher() + .dispatch( + new LayerCountEvent( + BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, baseImageLayers.size())); for (Layer layer : baseImageLayers) { pullAndCacheBaseImageLayerStepsBuilder.add( new PullAndCacheBaseImageLayerStep( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index b0efa481ae..6cb22307b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -45,6 +46,7 @@ class PushLayersStep private final AuthenticatePushStep authenticatePushStep; private final AsyncStep>> cachedLayerStep; + private final BuildStepType buildStepType; private final ListenableFuture>> listenableFuture; @@ -54,12 +56,14 @@ class PushLayersStep ProgressEventDispatcher.Factory progressEventDispatcherFactory, AuthenticatePushStep authenticatePushStep, AsyncStep>> - cachedLayerStep) { + cachedLayerStep, + BuildStepType buildStepType) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authenticatePushStep = authenticatePushStep; this.cachedLayerStep = cachedLayerStep; + this.buildStepType = buildStepType; listenableFuture = AsyncDependencies.using(listeningExecutorService) @@ -81,10 +85,13 @@ public ImmutableList> call() throws ExecutionException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - BuildStepType.PUSH_LAYERS, "setting up to push layers", cachedLayers.size())) { + buildStepType, "setting up to push layers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); + buildConfiguration + .getEventDispatcher() + .dispatch(new LayerCountEvent(buildStepType, cachedLayers.size())); for (AsyncStep cachedLayerStep : cachedLayers) { ProgressEventDispatcher.Factory childProgressEventDispatcherFactory = progressEventDispatcher.newChildProducer(); @@ -114,6 +121,6 @@ private PushBlobStep makePushBlobStep( authenticatePushStep, new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), cachedLayer.getBlob(), - BuildStepType.PUSH_LAYERS); + buildStepType); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 2f37677169..7dcd359d0b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.AsyncSteps; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -145,7 +146,8 @@ public StepsRunner pushBaseImageLayers() { buildConfiguration, Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), + BuildStepType.PUSH_BASE_LAYERS)); } public StepsRunner buildAndCacheApplicationLayers() { @@ -193,7 +195,8 @@ public StepsRunner pushApplicationLayers() { Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), AsyncSteps.immediate( - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)), + BuildStepType.PUSH_APPLICATION_LAYERS)); } public StepsRunner pushImage() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java index 6f9c47dae7..70ccaed091 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event; +import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; @@ -40,6 +41,10 @@ public class JibEventType { public static final JibEventType PROGRESS = new JibEventType<>(ProgressEvent.class); + /** Event used for counting layers processed during a build step. */ + public static final JibEventType LAYER_COUNT = + new JibEventType<>(LayerCountEvent.class); + // TODO: Add entries for all JibEvent types. private final Class eventClass; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java new file mode 100644 index 0000000000..b3ee4e11f5 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.events; + +import com.google.cloud.tools.jib.builder.BuildStepType; +import com.google.cloud.tools.jib.event.JibEvent; + +/** Event used for counting layers processed during a build step. */ +public class LayerCountEvent implements JibEvent { + + private final BuildStepType buildStepType; + private final int count; + + /** + * Creates a new {@link LayerCountEvent}. For internal use only. + * + * @param buildStepType the build step associated with the event + * @param count the number of layers counted + */ + public LayerCountEvent(BuildStepType buildStepType, int count) { + this.buildStepType = buildStepType; + this.count = count; + } + + /** + * Gets the type of build step that fired the event. + * + * @return the type of build step that fired the event + */ + public BuildStepType getBuildStepType() { + return buildStepType; + } + + /** + * Gets the number of layers. + * + * @return the number of layers + */ + public int getCount() { + return count; + } +} From 6204e4f1b5d73c3d87fb4b8059890c2212c7465c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 8 Feb 2019 15:24:42 -0500 Subject: [PATCH 0464/2020] Rethrow ExecutionException's cause in jib-core (#1466) --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JibIntegrationTest.java | 9 ++- .../cloud/tools/jib/api/Containerizer.java | 1 - .../tools/jib/api/JibContainerBuilder.java | 35 +++++++-- ...RegistryAuthenticationFailedException.java | 2 +- .../jib/plugins/common/BuildStepsRunner.java | 63 +++++++--------- .../plugins/common/BuildStepsRunnerTest.java | 75 ++++++++----------- 7 files changed, 91 insertions(+), 96 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index f97d16821f..a63e65ee3a 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed +- `JibContainerBuilder#containerize()` throws multiple sub-types of `RegistryException` rather than wrapping them in an `ExecutionException` ([#1440](https://github.com/GoogleContainerTools/jib/issues/1440)) + ### Fixed - `MainClassFinder` failure when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 44498cc1b0..892ca81957 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.cloud.tools.jib.registry.RegistryException; import java.io.IOException; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -65,8 +66,8 @@ public void tearDown() { @Test public void testBasic_helloWorld() - throws InvalidImageReferenceException, InterruptedException, ExecutionException, - CacheDirectoryCreationException, IOException { + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { ImageReference targetImageReference = ImageReference.of("localhost:5000", "jib-core", "basic-helloworld"); JibContainer jibContainer = @@ -89,8 +90,8 @@ public void testBasic_helloWorld() /** Ensure that a provided executor is not disposed. */ @Test public void testProvidedExecutorNotDisposed() - throws InvalidImageReferenceException, InterruptedException, ExecutionException, - CacheDirectoryCreationException, IOException { + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { ImageReference targetImageReference = ImageReference.of("localhost:5000", "jib-core", "basic-helloworld"); Containerizer containerizer = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index a98ea7f4ae..2f74a4a778 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -40,7 +40,6 @@ public class Containerizer { * The default directory for caching the base image layers, in {@code [user cache * home]/google-cloud-tools-java/jib}. */ - // TODO: Reduce scope once plugins are migrated to use the new Jib Core API. public static final Path DEFAULT_BASE_CACHE_DIRECTORY = UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 0cdfd93768..9f73aec358 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -26,8 +26,13 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.registry.InsecureRegistryException; +import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.registry.RegistryException; +import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.net.UnknownHostException; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; @@ -41,6 +46,7 @@ import java.util.concurrent.Executors; import java.util.function.Supplier; import javax.annotation.Nullable; +import org.apache.http.conn.HttpHostConnectException; /** * Builds a container with Jib. @@ -427,24 +433,31 @@ public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workin * * @param containerizer the {@link Containerizer} that configures how to containerize * @return the built container + * @throws IOException if an I/O exception occurs * @throws CacheDirectoryCreationException if a directory to be used for the cache could not be * created - * @throws ExecutionException if an exception occurred during execution + * @throws HttpHostConnectException if jib failed to connect to a registry + * @throws RegistryUnauthorizedException if a registry request is unauthorized and needs + * authentication + * @throws RegistryAuthenticationFailedException if registry authentication failed + * @throws UnknownHostException if the registry does not exist + * @throws InsecureRegistryException if a server could not be verified due to an insecure + * connection + * @throws RegistryException if some other error occurred while interacting with a registry + * @throws ExecutionException if some other exception occurred during execution * @throws InterruptedException if the execution was interrupted - * @throws IOException if an I/O exception occurs */ public JibContainer containerize(Containerizer containerizer) - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - + throws InterruptedException, RegistryException, IOException, CacheDirectoryCreationException, + ExecutionException { return containerize(containerizer, Executors::newCachedThreadPool); } @VisibleForTesting JibContainer containerize( Containerizer containerizer, Supplier defaultExecutorServiceFactory) - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + throws IOException, CacheDirectoryCreationException, InterruptedException, RegistryException, + ExecutionException { boolean shutdownExecutorService = !containerizer.getExecutorService().isPresent(); ExecutorService executorService = @@ -455,6 +468,14 @@ JibContainer containerize( try { BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); return new JibContainer(result.getImageDigest(), result.getImageId()); + + } catch (ExecutionException ex) { + // If an ExecutionException occurs, re-throw the cause to be more easily handled by the user + if (ex.getCause() instanceof RegistryException) { + throw (RegistryException) ex.getCause(); + } + throw ex; + } finally { if (shutdownExecutorService) { executorService.shutdown(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java index d574d62206..4e4926b7d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java @@ -19,7 +19,7 @@ import java.text.MessageFormat; /** Thrown because registry authentication failed. */ -public class RegistryAuthenticationFailedException extends Exception { +public class RegistryAuthenticationFailedException extends RegistryException { private static final String REASON = "Failed to authenticate with registry {0}/{1} because: {2}"; private final String serverUrl; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 64e1658404..724330e160 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -31,7 +31,7 @@ import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; -import com.google.cloud.tools.jib.registry.RegistryErrorException; +import com.google.cloud.tools.jib.registry.RegistryException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; @@ -226,53 +226,40 @@ public JibContainer build( return jibContainer; - } catch (ExecutionException executionException) { - Throwable exceptionDuringBuildSteps = executionException.getCause(); + } catch (HttpHostConnectException ex) { + // Failed to connect to registry. + throw new BuildStepsExecutionException(helpfulSuggestions.forHttpHostConnect(), ex); - if (exceptionDuringBuildSteps instanceof HttpHostConnectException) { - // Failed to connect to registry. - throw new BuildStepsExecutionException( - helpfulSuggestions.forHttpHostConnect(), exceptionDuringBuildSteps); + } catch (RegistryUnauthorizedException ex) { + handleRegistryUnauthorizedException(ex, helpfulSuggestions); - } else if (exceptionDuringBuildSteps instanceof RegistryUnauthorizedException) { - handleRegistryUnauthorizedException( - (RegistryUnauthorizedException) exceptionDuringBuildSteps, helpfulSuggestions); - - } else if (exceptionDuringBuildSteps instanceof RegistryCredentialsNotSentException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forCredentialsNotSent(), exceptionDuringBuildSteps); + } catch (RegistryCredentialsNotSentException ex) { + throw new BuildStepsExecutionException(helpfulSuggestions.forCredentialsNotSent(), ex); - } else if (exceptionDuringBuildSteps instanceof RegistryAuthenticationFailedException - && exceptionDuringBuildSteps.getCause() instanceof HttpResponseException) { - RegistryAuthenticationFailedException failureException = - (RegistryAuthenticationFailedException) exceptionDuringBuildSteps; + } catch (RegistryAuthenticationFailedException ex) { + if (ex.getCause() instanceof HttpResponseException) { handleRegistryUnauthorizedException( new RegistryUnauthorizedException( - failureException.getServerUrl(), - failureException.getImageName(), - (HttpResponseException) exceptionDuringBuildSteps.getCause()), + ex.getServerUrl(), ex.getImageName(), (HttpResponseException) ex.getCause()), helpfulSuggestions); + } else { + // Unknown cause + throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); + } - } else if (exceptionDuringBuildSteps instanceof UnknownHostException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forUnknownHost(), exceptionDuringBuildSteps); + } catch (UnknownHostException ex) { + throw new BuildStepsExecutionException(helpfulSuggestions.forUnknownHost(), ex); - } else if (exceptionDuringBuildSteps instanceof InsecureRegistryException) { - throw new BuildStepsExecutionException( - helpfulSuggestions.forInsecureRegistry(), exceptionDuringBuildSteps); + } catch (InsecureRegistryException ex) { + throw new BuildStepsExecutionException(helpfulSuggestions.forInsecureRegistry(), ex); - } else if (exceptionDuringBuildSteps instanceof RegistryErrorException) { - // RegistryErrorExceptions have good messages - RegistryErrorException registryErrorException = - (RegistryErrorException) exceptionDuringBuildSteps; - String message = - Verify.verifyNotNull(registryErrorException.getMessage()); // keep null-away happy - throw new BuildStepsExecutionException(message, exceptionDuringBuildSteps); + } catch (RegistryException ex) { + String message = Verify.verifyNotNull(ex.getMessage()); // keep null-away happy + throw new BuildStepsExecutionException(message, ex); - } else { - throw new BuildStepsExecutionException( - helpfulSuggestions.none(), executionException.getCause()); - } + } catch (ExecutionException ex) { + String message = Verify.verifyNotNull(ex.getCause().getMessage()); // keep null-away happy + throw new BuildStepsExecutionException(message, ex.getCause()); } catch (InterruptedException ex) { // TODO: Add more suggestions for various build failures. diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index f39611ddb0..7bd97288d5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; +import com.google.cloud.tools.jib.registry.RegistryException; import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import java.io.IOException; import java.net.UnknownHostException; @@ -69,7 +70,6 @@ public class BuildStepsRunnerTest { @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; - @Mock private ExecutionException mockExecutionException; private BuildStepsRunner testBuildImageStepsRunner; @@ -90,13 +90,12 @@ public void testBuildImage_pass() } @Test - public void testBuildImage_executionException_httpHostConnectException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + public void testBuildImage_httpHostConnectException() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { HttpHostConnectException mockHttpHostConnectException = Mockito.mock(HttpHostConnectException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockHttpHostConnectException); - Mockito.doThrow(mockExecutionException) + Mockito.doThrow(mockHttpHostConnectException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -111,17 +110,15 @@ public void testBuildImage_executionException_httpHostConnectException() } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect(), ex.getMessage()); - Assert.assertEquals(mockHttpHostConnectException, ex.getCause()); } } @Test - public void testBuildImage_executionException_unknownHostException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + public void testBuildImage_unknownHostException() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { UnknownHostException mockUnknownHostException = Mockito.mock(UnknownHostException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockUnknownHostException); - Mockito.doThrow(mockExecutionException) + Mockito.doThrow(mockUnknownHostException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -136,18 +133,16 @@ public void testBuildImage_executionException_unknownHostException() } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forUnknownHost(), ex.getMessage()); - Assert.assertEquals(mockUnknownHostException, ex.getCause()); } } @Test - public void testBuildImage_executionException_insecureRegistryException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + public void testBuildImage_insecureRegistryException() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { InsecureRegistryException mockInsecureRegistryException = Mockito.mock(InsecureRegistryException.class); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockInsecureRegistryException); - Mockito.doThrow(mockExecutionException) + Mockito.doThrow(mockInsecureRegistryException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -162,14 +157,13 @@ public void testBuildImage_executionException_insecureRegistryException() } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage()); - Assert.assertEquals(mockInsecureRegistryException, ex.getCause()); } } @Test - public void testBuildImage_executionException_registryUnauthorizedException_statusCodeForbidden() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getImageReference()) @@ -177,8 +171,7 @@ public void testBuildImage_executionException_registryUnauthorizedException_stat Mockito.when(mockHttpResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_FORBIDDEN); - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException) + Mockito.doThrow(mockRegistryUnauthorizedException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -195,22 +188,20 @@ public void testBuildImage_executionException_registryUnauthorizedException_stat Assert.assertEquals( TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("someregistry/somerepository"), ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); } } @Test - public void testBuildImage_executionException_registryUnauthorizedException_noCredentials() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { + public void testBuildImage_registryUnauthorizedException_noCredentials() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown - Mockito.when(mockExecutionException.getCause()).thenReturn(mockRegistryUnauthorizedException); - Mockito.doThrow(mockExecutionException) + Mockito.doThrow(mockRegistryUnauthorizedException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -227,17 +218,14 @@ public void testBuildImage_executionException_registryUnauthorizedException_noCr Assert.assertEquals( TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry", "somerepository"), ex.getMessage()); - Assert.assertEquals(mockRegistryUnauthorizedException, ex.getCause()); } } @Test - public void testBuildImage_executionException_registryCredentialsNotSentException() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Mockito.when(mockExecutionException.getCause()) - .thenReturn(mockRegistryCredentialsNotSentException); - Mockito.doThrow(mockExecutionException) + public void testBuildImage_registryCredentialsNotSentException() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { + Mockito.doThrow(mockRegistryCredentialsNotSentException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -252,17 +240,15 @@ public void testBuildImage_executionException_registryCredentialsNotSentExceptio } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage()); - Assert.assertEquals(mockRegistryCredentialsNotSentException, ex.getCause()); } } @Test - public void testBuildImage_executionException_other() - throws InterruptedException, ExecutionException, IOException, - CacheDirectoryCreationException { - Throwable throwable = new Throwable(); - Mockito.when(mockExecutionException.getCause()).thenReturn(throwable); - Mockito.doThrow(mockExecutionException) + public void testBuildImage_other() + throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, + ExecutionException { + RegistryException registryException = new RegistryException("messagePrefix"); + Mockito.doThrow(registryException) .when(mockJibContainerBuilder) .containerize(mockContainerizer); @@ -277,7 +263,6 @@ public void testBuildImage_executionException_other() } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); - Assert.assertEquals(throwable, ex.getCause()); } } } From c057895b375294259817ce3ad9423422c9313adf Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 11 Feb 2019 12:43:53 -0500 Subject: [PATCH 0465/2020] Core release v0.1.2 (#1472) * [Gradle Release Plugin] - pre tag commit: 'v0.1.2-core'. * [Gradle Release Plugin] - new version commit: 'v0.1.3-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 742f39eff8..8cb23a76c0 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.2-SNAPSHOT +version = 0.1.3-SNAPSHOT From b3bc5f8c983a9be8671a4859bc314d1c1ad9a77b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 11 Feb 2019 14:37:35 -0500 Subject: [PATCH 0466/2020] Add event system explanation to jib-core readme (#1465) * Add event system explanation to jib-core readme * Remove events as upcoming feature * Update links * Update changelog/readme version strings --- jib-core/CHANGELOG.md | 8 +++++ jib-core/README.md | 79 ++++++++++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index a63e65ee3a..069a5ffb75 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.1.2 + +### Added + - `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) - `LayerCountEvent` that is dispatched at the beginning of certain pull/build/push build steps to indicate the number of layers being processed ([#1461](https://github.com/GoogleContainerTools/jib/pull/1461)) diff --git a/jib-core/README.md b/jib-core/README.md index 88328dc888..099c9f353d 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.1.1 + 0.1.2 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.1.1' + compile 'com.google.cloud.tools:jib-core:0.1.2' } ``` @@ -45,13 +45,13 @@ Jib.from("busybox") .addCredential("myusername", "mypassword"))); ``` -1. [`Jib.from("busybox")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. -1. [`.addLayer(...)`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. -1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. -1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. -1. [`.addCredential`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). -1. [`Containerizer.to`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. -1. [`.containerize`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. +1. [`Jib.from("busybox")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. +1. [`.addLayer(...)`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. +1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. +1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. +1. [`.addCredential`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). +1. [`Containerizer.to`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. +1. [`.containerize`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. ## Tutorials @@ -59,33 +59,33 @@ Jib.from("busybox") ## API overview -[`Jib`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core +[`Jib`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Jib.html) - the main entrypoint for using Jib Core -[`JibContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build +[`JibContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html) - configures the container to build -[`Containerizer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to +[`Containerizer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html) - configures how and where to containerize to -[`JibContainer`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container +[`JibContainer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container Three `TargetImage` types define the 3 different targets Jib can build to: -- [`RegistryImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry -- [`DockerDaemonImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon -- [`TarImage`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive +- [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry +- [`DockerDaemonImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon +- [`TarImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive Other useful classes: -- [`ImageReference`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references -- [`LayerConfiguration`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build -- [`CredentialRetriever`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry -- [`CredentialRetrieverFactory`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers -- [`EventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution +- [`ImageReference`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references +- [`LayerConfiguration`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build +- [`CredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry +- [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers +- [`EventHandlers`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution Java-specific API: -- [`JavaContainerBuilder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/JavaContainerBuilder.html) - configures a `JibContainerBuilder` for Java-specific applications -- [`MainClassFinder`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/frontend/MainClassFinder.html) - find the main Java class in a given list of class files +- [`JavaContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JavaContainerBuilder.html) - configures a `JibContainerBuilder` for Java-specific applications +- [`MainClassFinder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/MainClassFinder.html) - find the main Java class in a given list of class files ## API reference -[API reference](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/package-summary.html) +[API reference](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/package-summary.html) ## How Jib Core works @@ -97,10 +97,36 @@ The Jib Core system consists 3 main parts: Some other parts of Jib Core internals include: -- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) -- an eventing system to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://static.javadoc.io/com.google.cloud.tools/jib-core/0.1.1/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) +- a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) +- an [eventing system](#events) to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) - support for fully-concurrent multi-threaded executions +## Events + +Throughout the build process, Jib Core dispatches events that provide useful information. These events implement the type [`JibEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/JibEvent.html), and can be handled by registering event handlers with the event dispatcher that is passed to Jib. + +```java +// Setup event handlers +EventHandlers eventHandlers = new EventHandlers() + .add(JibEventType.LOGGING, logEvent -> System.out.println(logEvent.getLevel() + ": " + logEvent.getMessage())) + .add(JibEventType.TIMING, timeEvent -> ...); + +// Register with Jib +Jib.from(...) + ... + .containerize( + Containerizer.to(...) + ... + .setEventHandlers(eventHandlers)); +``` + +When Jib dispatches events, the event handlers you defined for that event type will be called. The following are the types of events you can listen for in Jib core (see [API reference](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/package-summary.html) for more information): + +- [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. +- [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. +- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Since Jib consists of a hierarchy of build steps, each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. Progress events also carries the type of its corresponding build step, which can be retrieved using `getBuildStepType()`. +- [`LayerCountEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LayerCountEvent.html) - Indicates the number of layers in pull/build/push steps in the build process. Use `getBuildStepType()` to retrieve the step that the event is part of, and use `getCount()` to get the number of layers being processed. + ## Frequently Asked Questions (FAQ) See the [Jib project FAQ](../docs/faq.md). @@ -108,7 +134,6 @@ See the [Jib project FAQ](../docs/faq.md). ## Upcoming features - Extensions to make building Java and other language-specific containers easier -- Structured events to react to parts of Jib Core's execution See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. From 7280cc0aa7fc0df3fbc5045a2db14c2558e9e832 Mon Sep 17 00:00:00 2001 From: Q Chen Date: Mon, 11 Feb 2019 14:41:23 -0500 Subject: [PATCH 0467/2020] Mentions sbt-jib and AutoJib in Jib Core examples. (#1473) --- jib-core/examples/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jib-core/examples/README.md b/jib-core/examples/README.md index 0a14ab6345..0b9887d259 100644 --- a/jib-core/examples/README.md +++ b/jib-core/examples/README.md @@ -4,6 +4,14 @@ Please [file an issue](/../../issues/new) if you find any problems with the exam For examples on using Jib plugins, see [examples](../../examples). +### [sbt](https://index.scala-lang.org/schmitch/sbt-jib) plugin + +Jib Core is used in a third-party project, [sbt-jib](https://github.com/schmitch/sbt-jib), that brings the benefits of Jib to [sbt](https://www.scala-sbt.org/) users. + ### Using Jib Core in Gradle builds See [build.gradle](build.gradle) for examples using Jib Core to build and manipulate container images within a `build.gradle` script. + +### AutoJib - make your application self-containerize + +See [AutoJib](https://github.com/coollog/autojib) for an example of a library that, when added as a dependency to a Java application, can make your application containerize itself. From 6d1b23a3b654b4fccab87781175cf500f3f83041 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 11 Feb 2019 15:10:26 -0500 Subject: [PATCH 0468/2020] Fix typo in jib-core readme (#1474) --- jib-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/README.md b/jib-core/README.md index 099c9f353d..2b61a439b2 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -124,7 +124,7 @@ When Jib dispatches events, the event handlers you defined for that event type w - [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. - [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. -- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Since Jib consists of a hierarchy of build steps, each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. Progress events also carries the type of its corresponding build step, which can be retrieved using `getBuildStepType()`. +- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Since Jib consists of a hierarchy of build steps, each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. Each progress event also carries the type of its corresponding build step, which can be retrieved using `getBuildStepType()`. - [`LayerCountEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LayerCountEvent.html) - Indicates the number of layers in pull/build/push steps in the build process. Use `getBuildStepType()` to retrieve the step that the event is part of, and use `getCount()` to get the number of layers being processed. ## Frequently Asked Questions (FAQ) From 9e4053d04b12cb3e75564ae92a7422a043f7f122 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 12 Feb 2019 18:36:46 -0500 Subject: [PATCH 0469/2020] Add setAppRoot() and fromDistrolessJetty() to JavaContainerBuilder (#1475) --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JavaContainerBuilder.java | 114 +++++++++++++----- .../jib/api/JavaContainerBuilderTest.java | 43 ++++--- 3 files changed, 113 insertions(+), 46 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 069a5ffb75..0111f641bd 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) + ### Changed ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 106eeaaef0..5ffa999289 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -40,38 +40,37 @@ /** Creates a {@link JibContainerBuilder} for containerizing Java applications. */ public class JavaContainerBuilder { - /** The default root directory of the application on the container. */ - private static final AbsoluteUnixPath APP_ROOT = AbsoluteUnixPath.get("/app"); - - /** Absolute path of directory containing application resources on container. */ - private static final AbsoluteUnixPath RESOURCES_PATH = - APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - - /** Absolute path of directory containing classes on container. */ - private static final AbsoluteUnixPath CLASSES_PATH = - APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - - /** Absolute path of directory containing dependencies on container. */ - private static final AbsoluteUnixPath DEPENDENCIES_PATH = - APP_ROOT.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - - /** The entrypoint classpath element corresponding to dependencies. */ - private static final AbsoluteUnixPath DEPENDENCIES_CLASSPATH = DEPENDENCIES_PATH.resolve("*"); - - /** Absolute path of directory containing additional classpath files on container. */ - private static final AbsoluteUnixPath OTHERS_PATH = APP_ROOT.resolve("classpath"); - /** * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For * more information on {@code gcr.io/distroless/java}, see the distroless repository. * * @return a new {@link JavaContainerBuilder} - * @throws InvalidImageReferenceException if creating the base image reference fails * @see The distroless repository */ - public static JavaContainerBuilder fromDistroless() throws InvalidImageReferenceException { - return from(RegistryImage.named("gcr.io/distroless/java")); + public static JavaContainerBuilder fromDistroless() { + try { + return from(RegistryImage.named("gcr.io/distroless/java")); + } catch (InvalidImageReferenceException ignored) { + throw new IllegalStateException("Unreachable"); + } + } + + /** + * Creates a new {@link JavaContainerBuilder} that uses distroless jetty as the base image. For + * more information on {@code gcr.io/distroless/java}, see the distroless repository. + * + * @return a new {@link JavaContainerBuilder} + * @see The distroless repository + */ + public static JavaContainerBuilder fromDistrolessJetty() { + try { + return from(RegistryImage.named("gcr.io/distroless/java/jetty")) + .setAppRoot(AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); + } catch (InvalidImageReferenceException ignored) { + throw new IllegalStateException("Unreachable"); + } } /** @@ -113,10 +112,63 @@ public static JavaContainerBuilder from(RegistryImage registryImage) { private final List jvmFlags = new ArrayList<>(); private final LinkedHashSet classpath = new LinkedHashSet<>(4); + /** Absolute path of directory containing application resources on container. */ + private AbsoluteUnixPath resourcesPath; + + /** Absolute path of directory containing classes on container. */ + private AbsoluteUnixPath classesPath; + + /** Absolute path of directory containing dependencies on container. */ + private AbsoluteUnixPath dependenciesPath; + + /** The entrypoint classpath element corresponding to dependencies. */ + private AbsoluteUnixPath dependenciesClasspath; + + /** Absolute path of directory containing additional classpath files on container. */ + private AbsoluteUnixPath othersPath; + @Nullable private String mainClass; private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { this.jibContainerBuilder = jibContainerBuilder; + AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); + resourcesPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); + classesPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + dependenciesPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); + dependenciesClasspath = dependenciesPath.resolve("*"); + othersPath = appRoot.resolve("classpath"); + } + + /** + * Sets the app root of the container image (useful for building WAR containers). + * + * @param appRoot the absolute path of the app on the container ({@code /app} by default) + * @return this + */ + public JavaContainerBuilder setAppRoot(String appRoot) { + return setAppRoot(AbsoluteUnixPath.get(appRoot)); + } + + /** + * Sets the app root of the container image (useful for building WAR containers). + * + * @param appRoot the absolute path of the app on the container ({@code /app} by default) + * @return this + */ + public JavaContainerBuilder setAppRoot(AbsoluteUnixPath appRoot) { + if (!classpath.isEmpty()) { + throw new IllegalStateException("You cannot change the app root after files are added"); + } + resourcesPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); + classesPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); + dependenciesPath = + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); + dependenciesClasspath = dependenciesPath.resolve("*"); + othersPath = appRoot.resolve("classpath"); + return this; } /** @@ -153,13 +205,13 @@ public JavaContainerBuilder addDependencies(List dependencyFiles) throws I ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES, file, - DEPENDENCIES_PATH.resolve( + dependenciesPath.resolve( duplicates.contains(file.getFileName().toString()) ? file.getFileName().toString().replaceFirst("\\.jar$", "-" + Files.size(file)) + ".jar" : file.getFileName().toString())); } - classpath.add(DEPENDENCIES_CLASSPATH.toString()); + classpath.add(dependenciesClasspath.toString()); return this; } @@ -196,7 +248,7 @@ public JavaContainerBuilder addResources(Path resourceFilesDirectory) throws IOE */ public JavaContainerBuilder addResources(Path resourceFilesDirectory, Predicate pathFilter) throws IOException { - return addDirectory(resourceFilesDirectory, RESOURCES_PATH, LayerType.RESOURCES, pathFilter); + return addDirectory(resourceFilesDirectory, resourcesPath, LayerType.RESOURCES, pathFilter); } /** @@ -220,7 +272,7 @@ public JavaContainerBuilder addClasses(Path classFilesDirectory) throws IOExcept */ public JavaContainerBuilder addClasses(Path classFilesDirectory, Predicate pathFilter) throws IOException { - return addDirectory(classFilesDirectory, CLASSES_PATH, LayerType.CLASSES, pathFilter); + return addDirectory(classFilesDirectory, classesPath, LayerType.CLASSES, pathFilter); } /** @@ -245,13 +297,13 @@ public JavaContainerBuilder addToClasspath(List otherFiles) throws IOExcep for (Path file : otherFiles) { if (Files.isDirectory(file)) { layerConfigurationsBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, file, path -> true, OTHERS_PATH); + LayerType.EXTRA_FILES, file, path -> true, othersPath); } else { layerConfigurationsBuilder.addFile( - LayerType.EXTRA_FILES, file, OTHERS_PATH.resolve(file.getFileName())); + LayerType.EXTRA_FILES, file, othersPath.resolve(file.getFileName())); } } - classpath.add(OTHERS_PATH.toString()); + classpath.add(othersPath.toString()); return this; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 44c07ab44f..9247095104 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -66,6 +66,7 @@ public void testToJibContainerBuilder_all() CacheDirectoryCreationException { BuildConfiguration buildConfiguration = JavaContainerBuilder.fromDistroless() + .setAppRoot("/hello") .addResources(getResource("core/application/resources")) .addClasses(getResource("core/application/classes")) .addDependencies( @@ -91,23 +92,23 @@ public void testToJibContainerBuilder_all() "-xflag1", "-xflag2", "-cp", - "/app/resources:/app/classes:/app/libs/*:/app/classpath", + "/hello/resources:/hello/classes:/hello/libs/*:/hello/classpath", "HelloWorld"), containerConfiguration.getEntrypoint()); // Check dependencies List expectedDependencies = ImmutableList.of( - AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-770.jar"), - AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-200.jar"), - AbsoluteUnixPath.get("/app/libs/libraryA.jar"), - AbsoluteUnixPath.get("/app/libs/libraryB.jar")); + AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-770.jar"), + AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-200.jar"), + AbsoluteUnixPath.get("/hello/libs/libraryA.jar"), + AbsoluteUnixPath.get("/hello/libs/libraryB.jar")); Assert.assertEquals( expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); // Check snapshots List expectedSnapshotDependencies = - ImmutableList.of(AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-SNAPSHOT.jar")); + ImmutableList.of(AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-SNAPSHOT.jar")); Assert.assertEquals( expectedSnapshotDependencies, getExtractionPaths(buildConfiguration, "snapshot dependencies")); @@ -115,23 +116,23 @@ public void testToJibContainerBuilder_all() // Check resources List expectedResources = ImmutableList.of( - AbsoluteUnixPath.get("/app/resources/resourceA"), - AbsoluteUnixPath.get("/app/resources/resourceB"), - AbsoluteUnixPath.get("/app/resources/world")); + AbsoluteUnixPath.get("/hello/resources/resourceA"), + AbsoluteUnixPath.get("/hello/resources/resourceB"), + AbsoluteUnixPath.get("/hello/resources/world")); Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); // Check classes List expectedClasses = ImmutableList.of( - AbsoluteUnixPath.get("/app/classes/HelloWorld.class"), - AbsoluteUnixPath.get("/app/classes/some.class")); + AbsoluteUnixPath.get("/hello/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/hello/classes/some.class")); Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); // Check additional classpath files List expectedOthers = ImmutableList.of( - AbsoluteUnixPath.get("/app/classpath/fileA"), - AbsoluteUnixPath.get("/app/classpath/fileB")); + AbsoluteUnixPath.get("/hello/classpath/fileA"), + AbsoluteUnixPath.get("/hello/classpath/fileB")); Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); } @@ -193,7 +194,19 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() } @Test - public void testToJibContainerBuilder_mainClassNull() throws InvalidImageReferenceException { + public void testToJibContainerBuilder_setAppRootLate() throws URISyntaxException, IOException { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.fromDistroless().addClasses(getResource("core/application/classes")); + try { + javaContainerBuilder.setAppRoot("/oh no"); + Assert.fail(); + } catch (IllegalStateException ex) { + Assert.assertEquals("You cannot change the app root after files are added", ex.getMessage()); + } + } + + @Test + public void testToJibContainerBuilder_mainClassNull() { try { JavaContainerBuilder.fromDistroless().toContainerBuilder(); Assert.fail(); @@ -208,7 +221,7 @@ public void testToJibContainerBuilder_mainClassNull() throws InvalidImageReferen } @Test - public void testToJibContainerBuilder_classpathEmpty() throws InvalidImageReferenceException { + public void testToJibContainerBuilder_classpathEmpty() { try { JavaContainerBuilder.fromDistroless().setMainClass("Hello").toContainerBuilder(); Assert.fail(); From 75027d6b4740251456ffd9ab3ba31199bafb79ef Mon Sep 17 00:00:00 2001 From: Prince Rachit Sinha Date: Wed, 13 Feb 2019 21:21:25 +0530 Subject: [PATCH 0470/2020] Fix broken anchor (#1478) --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index da20a2c569..0eae754c0d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -9,7 +9,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [Where is bash?](#where-is-bash)\ [How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ -[Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint)\ +[Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint-at-runtime)\ [I want to containerize an executable JAR.](#i-want-to-containerize-an-executable-jar)\ [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ From dc1a031d281ae29b29acb16739a307fee1e39f39 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 13 Feb 2019 16:56:58 -0500 Subject: [PATCH 0471/2020] Add Cram to jib-core examples (#1480) --- jib-core/examples/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jib-core/examples/README.md b/jib-core/examples/README.md index 0b9887d259..c7ad83c2fe 100644 --- a/jib-core/examples/README.md +++ b/jib-core/examples/README.md @@ -8,6 +8,10 @@ For examples on using Jib plugins, see [examples](../../examples). Jib Core is used in a third-party project, [sbt-jib](https://github.com/schmitch/sbt-jib), that brings the benefits of Jib to [sbt](https://www.scala-sbt.org/) users. +### Cram + +See [Cram](https://github.com/briandealwis/cram) for a simple example of a command-line utility that uses Jib Core to build Docker containers from file system contents. + ### Using Jib Core in Gradle builds See [build.gradle](build.gradle) for examples using Jib Core to build and manipulate container images within a `build.gradle` script. From 3b32ae3cce619485ff8e343204dec690e3ae309f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 19 Feb 2019 17:01:49 -0500 Subject: [PATCH 0472/2020] Add JSON template for Skaffold file-watching (#1485) --- .../plugins/common/SkaffoldFilesOutput.java | 113 ++++++++++++++++++ .../common/SkaffoldFilesOutputTest.java | 47 ++++++++ 2 files changed, 160 insertions(+) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java new file mode 100644 index 0000000000..d809b99a6b --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Builds a JSON string containing files and directories that Skaffold should watch for changes + * (and consequently trigger rebuilds). + * + *

{@code build} consists of build definitions. Changes in these files/directories indicate that + * the project structure may have changed, so Skaffold will refresh the file watch list when this + * happens. + * + *

{@code inputs} consist of source/resource files/directories. Skaffold will trigger a rebuild + * when changes are detected in these files. + * + *

{@code ignore} consists of files/directories that the Skaffold file watcher should not watch. + * + *

Example: + * + *

{@code
+ * {
+ *   "build": [
+ *     "buildFile1",
+ *     "buildFile2"
+ *   ],
+ *   "inputs": [
+ *     "src/main/java/",
+ *     "src/main/resources/"
+ *   ],
+ *   "ignore": [
+ *     "pathToIgnore"
+ *   ]
+ * }
+ * }
+ */ +public class SkaffoldFilesOutput { + + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + private static class SkaffoldFilesTemplate { + + private final List build = new ArrayList<>(); + + private final List inputs = new ArrayList<>(); + + private final List ignore = new ArrayList<>(); + } + + private final SkaffoldFilesTemplate skaffoldFilesTemplate = new SkaffoldFilesTemplate(); + + /** + * Adds a build file/directory. + * + * @param build the path to the file/directory + */ + public void addBuild(Path build) { + skaffoldFilesTemplate.build.add(build.toString()); + } + + /** + * Adds an input file/directory. + * + * @param inputFile the path to the file/directory + */ + public void addInput(Path inputFile) { + skaffoldFilesTemplate.inputs.add(inputFile.toString()); + } + + /** + * Adds an ignored file/directory. + * + * @param ignoreFile the path to the file/directory + */ + public void addIgnore(Path ignoreFile) { + skaffoldFilesTemplate.ignore.add(ignoreFile.toString()); + } + + /** + * Gets the added files in JSON format. + * + * @return the files in a JSON string + * @throws IOException if writing out the JSON fails + */ + public String getJsonString() throws IOException { + try (OutputStream outputStream = new ByteArrayOutputStream()) { + new ObjectMapper().writeValue(outputStream, skaffoldFilesTemplate); + return outputStream.toString(); + } + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java new file mode 100644 index 0000000000..5d9ef05af4 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.io.IOException; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link SkaffoldFilesOutput}. */ +public class SkaffoldFilesOutputTest { + + @Test + public void testGetJsonString() throws IOException { + SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); + skaffoldFilesOutput.addBuild(Paths.get("buildFile1")); + skaffoldFilesOutput.addBuild(Paths.get("buildFile2")); + skaffoldFilesOutput.addInput(Paths.get("input1")); + skaffoldFilesOutput.addInput(Paths.get("input2")); + skaffoldFilesOutput.addIgnore(Paths.get("ignore1")); + skaffoldFilesOutput.addIgnore(Paths.get("ignore2")); + Assert.assertEquals( + "{\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}", + skaffoldFilesOutput.getJsonString()); + } + + @Test + public void testGetJsonString_empty() throws IOException { + SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); + Assert.assertEquals( + "{\"build\":[],\"inputs\":[],\"ignore\":[]}", skaffoldFilesOutput.getJsonString()); + } +} From 7d1cf3293a344dcdf33a5712800aa5364aa19398 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 20 Feb 2019 16:21:27 -0500 Subject: [PATCH 0473/2020] Add _jibSkaffoldFilesV2 task (#1492) --- .../cloud/tools/jib/gradle/FilesTaskV2.java | 191 ++++++++++++++++++ .../cloud/tools/jib/gradle/JibPlugin.java | 2 + .../tools/jib/gradle/FilesTaskV2Test.java | 142 +++++++++++++ .../plugins/common/SkaffoldFilesOutput.java | 34 +++- .../common/SkaffoldFilesOutputTest.java | 17 +- 5 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java new file mode 100644 index 0000000000..e9c02d48a6 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.base.Preconditions; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.PublishArtifact; +import org.gradle.api.initialization.Settings; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out changing source dependencies on a project. + * + *

Expected use: {@code ./gradlew _jibSkaffoldFiles -q} or {@code ./gradlew + * ::_jibSkaffoldFiles -q} + */ +public class FilesTaskV2 extends DefaultTask { + + private final SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); + + @Nullable private JibExtension jibExtension; + + public FilesTaskV2 setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + @TaskAction + public void listFiles() throws IOException { + Preconditions.checkNotNull(jibExtension); + Project project = getProject(); + + // If this is not the root project, add the root project's build.gradle and settings.gradle + if (project != project.getRootProject()) { + addGradleFiles(project.getRootProject()); + } + + addProjectFiles(project); + + // Add extra layer + if (Files.exists(jibExtension.getExtraDirectory().getPath())) { + skaffoldFilesOutput.addInput(jibExtension.getExtraDirectory().getPath()); + } + + // Find project dependencies + Set projectDependencies = findProjectDependencies(project); + + Set projectDependencyJars = new HashSet<>(); + for (ProjectDependency projectDependency : projectDependencies) { + addProjectFiles(projectDependency.getDependencyProject()); + + // Keep track of project dependency jars for filtering out later + String configurationName = projectDependency.getTargetConfiguration(); + if (configurationName == null) { + configurationName = "default"; + } + Project dependencyProject = projectDependency.getDependencyProject(); + for (Configuration targetConfiguration : + dependencyProject.getConfigurations().getByName(configurationName).getHierarchy()) { + for (PublishArtifact artifact : targetConfiguration.getArtifacts()) { + projectDependencyJars.add(artifact.getFile()); + } + } + } + + // Add SNAPSHOT, non-project dependency jars + for (File file : project.getConfigurations().getByName("runtime")) { + if (!projectDependencyJars.contains(file) && file.toString().contains("SNAPSHOT")) { + skaffoldFilesOutput.addInput(file.toPath()); + projectDependencyJars.add(file); // Add to set to avoid printing the same files twice + } + } + + // Print files + System.out.println(skaffoldFilesOutput.getJsonString()); + } + + /** + * Adds the locations of a project's build.gradle, settings.gradle, and gradle.properties. + * + * @param project the project + */ + private void addGradleFiles(Project project) { + Path projectPath = project.getProjectDir().toPath(); + + // Add build.gradle + skaffoldFilesOutput.addBuild(project.getBuildFile().toPath()); + + // Add settings.gradle + if (project.getGradle().getStartParameter().getSettingsFile() != null) { + skaffoldFilesOutput.addBuild( + project.getGradle().getStartParameter().getSettingsFile().toPath()); + } else if (Files.exists(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE))) { + skaffoldFilesOutput.addBuild(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE)); + } + + // Add gradle.properties + if (Files.exists(projectPath.resolve("gradle.properties"))) { + skaffoldFilesOutput.addBuild(projectPath.resolve("gradle.properties")); + } + } + + /** + * Prints build files, sources, and resources associated with a project. + * + * @param project the project + */ + private void addProjectFiles(Project project) { + // Add build config, settings, etc. + addGradleFiles(project); + + // Add sources + resources + JavaPluginConvention javaConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = + javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet != null) { + mainSourceSet + .getAllSource() + .getSourceDirectories() + .forEach( + sourceDirectory -> { + if (sourceDirectory.exists()) { + skaffoldFilesOutput.addInput(sourceDirectory.toPath()); + } + }); + } + } + + /** + * Collects a project's project dependencies, including all transitive project dependencies. + * + * @param project the project to find the project dependencies for + * @return the set of project dependencies + */ + private Set findProjectDependencies(Project project) { + Set projectDependencies = new HashSet<>(); + Deque projects = new ArrayDeque<>(); + projects.push(project); + + while (!projects.isEmpty()) { + Project currentProject = projects.pop(); + + // Search through all dependencies + for (Configuration configuration : + currentProject.getConfigurations().getByName("runtime").getHierarchy()) { + for (Dependency dependency : configuration.getDependencies()) { + if (dependency instanceof ProjectDependency) { + // If this is a project dependency, save it + ProjectDependency projectDependency = (ProjectDependency) dependency; + if (!projectDependencies.contains(projectDependency)) { + projects.push(projectDependency.getDependencyProject()); + projectDependencies.add(projectDependency); + } + } + } + } + } + return projectDependencies; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index d186b6559a..0704766779 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -40,6 +40,7 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; + @VisibleForTesting static final String FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; /** @@ -100,6 +101,7 @@ public void apply(Project project) { .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) .setJibExtension(jibExtension); project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); + project.getTasks().create(FILES_TASK_V2_NAME, FilesTaskV2.class).setJibExtension(jibExtension); project.afterEvaluate( projectAfterEvaluation -> { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java new file mode 100644 index 0000000000..d1345d2a1e --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import javax.annotation.Nullable; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link FilesTaskV2}. */ +public class FilesTaskV2Test { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + + /** + * Verifies that the files task succeeded and returns the list of paths it prints out. + * + * @param project the project to run the task on + * @param moduleName the name of the sub-project, or {@code null} if no sub-project + * @return the list of paths printed by the task + */ + private static String verifyTaskSuccess(TestProject project, @Nullable String moduleName) { + String taskName = + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.FILES_TASK_V2_NAME; + BuildResult buildResult = project.build(taskName, "-q"); + BuildTask jibTask = buildResult.task(taskName); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + + return buildResult.getOutput().trim(); + } + + /** + * Asserts that two lists contain the same paths. Required to avoid Mac's /var/ vs. /private/var/ + * symlink issue. + * + * @param expected the expected list of paths + * @param actual the actual list of paths + * @throws IOException if checking if two files are the same fails + */ + private static void assertPathListsAreEqual(List expected, List actual) + throws IOException { + Assert.assertEquals(expected.size(), actual.size()); + for (int index = 0; index < expected.size(); index++) { + Assert.assertEquals( + expected.get(index).toRealPath(), Paths.get(actual.get(index)).toRealPath()); + } + } + + @Test + public void testFilesTask_singleProject() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(simpleTestProject, null)); + assertPathListsAreEqual( + ImmutableList.of(projectRoot.resolve("build.gradle")), result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/custom-extra-dir")), + result.getInputs()); + Assert.assertEquals(result.getIgnore().size(), 0); + } + + @Test + public void testFilesTask_multiProjectSimpleService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path simpleServiceRoot = projectRoot.resolve("simple-service"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(multiTestProject, "simple-service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + simpleServiceRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of(simpleServiceRoot.resolve("src/main/java")), result.getInputs()); + Assert.assertEquals(result.getIgnore().size(), 0); + } + + @Test + public void testFilesTask_multiProjectComplexService() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(multiTestProject, "complex-service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + projectRoot.resolve("gradle.properties"), + complexServiceRoot.resolve("build.gradle"), + libRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of( + complexServiceRoot.resolve("src/main/extra-resources-1"), + complexServiceRoot.resolve("src/main/extra-resources-2"), + complexServiceRoot.resolve("src/main/java"), + complexServiceRoot.resolve("src/main/other-jib"), + libRoot.resolve("src/main/resources"), + libRoot.resolve("src/main/java")), + result.getInputs().subList(0, 6)); + // guava jar is in a temporary-looking directory, so we need to do some extra processing to + // match this + Assert.assertThat( + result.getInputs().get(result.getInputs().size() - 1), + CoreMatchers.endsWith("guava-HEAD-jre-SNAPSHOT.jar")); + Assert.assertEquals(7, result.getInputs().size()); + Assert.assertEquals(result.getIgnore().size(), 0); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java index d809b99a6b..ba19d3b824 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -69,7 +70,23 @@ private static class SkaffoldFilesTemplate { private final List ignore = new ArrayList<>(); } - private final SkaffoldFilesTemplate skaffoldFilesTemplate = new SkaffoldFilesTemplate(); + private final SkaffoldFilesTemplate skaffoldFilesTemplate; + + /** Creates an empty {@link SkaffoldFilesOutput}. */ + public SkaffoldFilesOutput() { + skaffoldFilesTemplate = new SkaffoldFilesTemplate(); + } + + /** + * Creates a {@link SkaffoldFilesOutput} from a JSON string. + * + * @param json the JSON string + * @throws IOException if reading the JSON string fails + */ + @VisibleForTesting + public SkaffoldFilesOutput(String json) throws IOException { + this.skaffoldFilesTemplate = new ObjectMapper().readValue(json, SkaffoldFilesTemplate.class); + } /** * Adds a build file/directory. @@ -98,6 +115,21 @@ public void addIgnore(Path ignoreFile) { skaffoldFilesTemplate.ignore.add(ignoreFile.toString()); } + @VisibleForTesting + public List getBuild() { + return skaffoldFilesTemplate.build; + } + + @VisibleForTesting + public List getInputs() { + return skaffoldFilesTemplate.inputs; + } + + @VisibleForTesting + public List getIgnore() { + return skaffoldFilesTemplate.ignore; + } + /** * Gets the added files in JSON format. * diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java index 5d9ef05af4..e25d2070f2 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Paths; import org.junit.Assert; @@ -24,6 +25,9 @@ /** Tests for {@link SkaffoldFilesOutput}. */ public class SkaffoldFilesOutputTest { + private static final String TEST_JSON = + "{\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}"; + @Test public void testGetJsonString() throws IOException { SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); @@ -33,9 +37,7 @@ public void testGetJsonString() throws IOException { skaffoldFilesOutput.addInput(Paths.get("input2")); skaffoldFilesOutput.addIgnore(Paths.get("ignore1")); skaffoldFilesOutput.addIgnore(Paths.get("ignore2")); - Assert.assertEquals( - "{\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}", - skaffoldFilesOutput.getJsonString()); + Assert.assertEquals(TEST_JSON, skaffoldFilesOutput.getJsonString()); } @Test @@ -44,4 +46,13 @@ public void testGetJsonString_empty() throws IOException { Assert.assertEquals( "{\"build\":[],\"inputs\":[],\"ignore\":[]}", skaffoldFilesOutput.getJsonString()); } + + @Test + public void testConstructor_json() throws IOException { + SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(TEST_JSON); + Assert.assertEquals( + ImmutableList.of("buildFile1", "buildFile2"), skaffoldFilesOutput.getBuild()); + Assert.assertEquals(ImmutableList.of("input1", "input2"), skaffoldFilesOutput.getInputs()); + Assert.assertEquals(ImmutableList.of("ignore1", "ignore2"), skaffoldFilesOutput.getIgnore()); + } } From dd09a3774873caeff325fd4fa4ee7443c34f981a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 21 Feb 2019 12:44:20 -0500 Subject: [PATCH 0474/2020] Use getAppRootChecked from PluginConfigurationProcessor instead of Task/MojoCommon (#1496) --- .../tools/jib/gradle/BuildDockerTask.java | 9 +++-- .../tools/jib/gradle/BuildImageTask.java | 10 +++-- .../cloud/tools/jib/gradle/BuildTarTask.java | 9 +++-- .../jib/gradle/GradleLayerConfigurations.java | 2 +- .../jib/gradle/GradleProjectProperties.java | 15 +------ .../cloud/tools/jib/gradle/JibPlugin.java | 2 +- .../cloud/tools/jib/gradle/TaskCommon.java | 40 +++++++------------ .../gradle/GradleProjectPropertiesTest.java | 6 +-- .../tools/jib/maven/BuildDockerMojo.java | 8 +++- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 +++- .../cloud/tools/jib/maven/BuildTarMojo.java | 8 +++- .../jib/maven/MavenProjectProperties.java | 1 - .../cloud/tools/jib/maven/MojoCommon.java | 29 -------------- .../common/PluginConfigurationProcessor.java | 12 +++--- .../PluginConfigurationProcessorTest.java | 14 +++---- 15 files changed, 68 insertions(+), 105 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index b669402575..0af9d8d053 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -104,8 +105,10 @@ public void buildDocker() TaskCommon.disableHttpLogging(); try { - AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); - + RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = GradleProjectProperties.getForProject( getProject(), @@ -120,7 +123,7 @@ public void buildDocker() PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - new GradleRawConfiguration(jibExtension), + gradleRawConfiguration, ignored -> java.util.Optional.empty(), projectProperties, dockerClientParameters.getExecutablePath(), diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index be19bbbfae..31686c8c75 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; @@ -81,7 +82,10 @@ public void buildImage() TaskCommon.disableHttpLogging(); try { - AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); + RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = GradleProjectProperties.getForProject( getProject(), @@ -102,9 +106,7 @@ public void buildImage() PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - new GradleRawConfiguration(jibExtension), - ignored -> Optional.empty(), - projectProperties); + gradleRawConfiguration, ignored -> Optional.empty(), projectProperties); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 304bcf4459..9f76c68cc9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -103,8 +104,10 @@ public void buildTar() TaskCommon.disableHttpLogging(); try { - AbsoluteUnixPath appRoot = TaskCommon.getAppRootChecked(jibExtension, getProject()); - + RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = GradleProjectProperties.getForProject( getProject(), @@ -120,7 +123,7 @@ public void buildTar() Path tarOutputPath = getTargetPath(); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( - new GradleRawConfiguration(jibExtension), + gradleRawConfiguration, ignored -> Optional.empty(), projectProperties, tarOutputPath, diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java index 7c5e8b937c..fb6d1392c2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java @@ -61,7 +61,7 @@ static JavaLayerConfigurations getForProject( Map extraDirectoryPermissions, AbsoluteUnixPath appRoot) throws IOException { - if (GradleProjectProperties.getWarTask(project) != null) { + if (TaskCommon.getWarTask(project) != null) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); } else { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index c34d2639e3..53b1668119 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -51,8 +51,6 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.plugins.WarPluginConvention; -import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ @@ -86,16 +84,6 @@ static GradleProjectProperties getForProject( } } - @Nullable - static War getWarTask(Project project) { - WarPluginConvention warPluginConvention = - project.getConvention().findPlugin(WarPluginConvention.class); - if (warPluginConvention == null) { - return null; - } - return (War) warPluginConvention.getProject().getTasks().findByName("war"); - } - static Path getExplodedWarDirectory(Project project) { return project.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME); } @@ -141,7 +129,6 @@ private static EventHandlers makeEventHandlers( } private static boolean isProgressFooterEnabled(Project project) { - // TODO: Consolidate with MavenProjectProperties? if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; } @@ -220,7 +207,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - return getWarTask(project) != null; + return TaskCommon.isWarProject(project); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 0704766779..5df9ed98ea 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -106,7 +106,7 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { try { - War warTask = GradleProjectProperties.getWarTask(project); + War warTask = TaskCommon.getWarTask(project); Task dependsOnTask; if (warTask != null) { ExplodedWarTask explodedWarTask = diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 0e58ec1a38..65f47d4ccc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -17,12 +17,11 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import java.util.logging.Level; -import org.gradle.api.GradleException; +import javax.annotation.Nullable; import org.gradle.api.Project; +import org.gradle.api.plugins.WarPluginConvention; +import org.gradle.api.tasks.bundling.War; import org.gradle.internal.logging.events.LogEvent; import org.gradle.internal.logging.events.OutputEventListener; import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; @@ -31,29 +30,18 @@ /** Collection of common methods to share between Gradle tasks. */ class TaskCommon { - /** - * Gets the value of the {@code container.appRoot} parameter. Throws {@link GradleException} if it - * is not an absolute path in Unix-style. - * - * @param jibExtension the {@link JibExtension} providing the configuration data - * @return the app root value - * @throws InvalidAppRootException if the app root is not an absolute path in Unix-style - */ - // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead - static AbsoluteUnixPath getAppRootChecked(JibExtension jibExtension, Project project) - throws InvalidAppRootException { - String appRoot = jibExtension.getContainer().getAppRoot(); - if (appRoot.isEmpty()) { - appRoot = - GradleProjectProperties.getWarTask(project) != null - ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT - : JavaLayerConfigurations.DEFAULT_APP_ROOT; - } - try { - return AbsoluteUnixPath.get(appRoot); - } catch (IllegalArgumentException ex) { - throw new InvalidAppRootException(appRoot, appRoot, ex); + static boolean isWarProject(Project project) { + return getWarTask(project) != null; + } + + @Nullable + static War getWarTask(Project project) { + WarPluginConvention warPluginConvention = + project.getConvention().findPlugin(WarPluginConvention.class); + if (warPluginConvention == null) { + return null; } + return (War) warPluginConvention.getProject().getTasks().findByName("war"); } /** Disables annoying Apache HTTP client logging. */ diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index f0b64fd85d..5ea972bfbf 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -113,21 +113,21 @@ public void testIsWarProject() { @Test public void testGetWar_warProject() { - Assert.assertNotNull(GradleProjectProperties.getWarTask(mockProject)); + Assert.assertNotNull(TaskCommon.getWarTask(mockProject)); } @Test public void testGetWar_noWarPlugin() { Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)).thenReturn(null); - Assert.assertNull(GradleProjectProperties.getWarTask(mockProject)); + Assert.assertNull(TaskCommon.getWarTask(mockProject)); } @Test public void testGetWar_noWarTask() { Mockito.when(mockTaskContainer.findByName("war")).thenReturn(null); - Assert.assertNull(GradleProjectProperties.getWarTask(mockProject)); + Assert.assertNull(TaskCommon.getWarTask(mockProject)); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 190fbaec9b..3f3bb9d3e0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -34,6 +34,7 @@ import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; @@ -90,7 +91,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } try { - AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), @@ -107,7 +111,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - new MavenRawConfiguration(this), + mavenRawConfiguration, new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 8ad945a769..f69308f920 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -32,6 +32,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.io.IOException; @@ -87,7 +88,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } try { - AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), @@ -101,7 +105,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - new MavenRawConfiguration(this), + mavenRawConfiguration, new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 3561354dd6..a97c367c5a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -63,7 +64,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } try { - AbsoluteUnixPath appRoot = MojoCommon.getAppRootChecked(this); + RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); + AbsoluteUnixPath appRoot = + PluginConfigurationProcessor.getAppRootChecked( + mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), @@ -82,7 +86,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Path tarOutputPath = buildOutput.resolve("jib-image.tar"); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( - new MavenRawConfiguration(this), + mavenRawConfiguration, new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), projectProperties, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 4285f06fe7..e13f22d302 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -129,7 +129,6 @@ private static EventHandlers makeEventHandlers( } private static boolean isProgressFooterEnabled() { - // TODO: Consolidate with GradleProjectProperties? if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 7961e09bb2..95d917221c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -18,9 +18,7 @@ import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; -import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.nio.file.Path; @@ -44,33 +42,6 @@ static boolean isWarProject(MavenProject project) { return "war".equals(packaging) || "gwt-app".equals(packaging); } - /** - * Gets the value of the {@code } parameter. If the parameter is empty, - * returns {@link JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for project with WAR packaging or - * {@link JavaLayerConfigurations#DEFAULT_APP_ROOT} for other packaging. - * - * @param jibPluginConfiguration the Jib plugin configuration - * @return the app root value - * @throws InvalidAppRootException if the app root is not an absolute path in Unix-style - */ - // TODO: find a way to use PluginConfigurationProcessor.getAppRootChecked() instead - static AbsoluteUnixPath getAppRootChecked(JibPluginConfiguration jibPluginConfiguration) - throws InvalidAppRootException { - String appRoot = jibPluginConfiguration.getAppRoot(); - if (appRoot.isEmpty()) { - boolean isWarProject = isWarProject(jibPluginConfiguration.getProject()); - appRoot = - isWarProject - ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT - : JavaLayerConfigurations.DEFAULT_APP_ROOT; - } - try { - return AbsoluteUnixPath.get(appRoot); - } catch (IllegalArgumentException ex) { - throw new InvalidAppRootException(appRoot, appRoot, ex); - } - } - /** * Gets the extra directory path from a {@link JibPluginConfiguration}. Returns {@code (project * dir)/src/main/jib} if null. diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 0cccbcf98c..eff597810f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -273,7 +273,8 @@ static List computeEntrypoint( return null; } - AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); + AbsoluteUnixPath appRoot = + getAppRootChecked(rawConfiguration, projectProperties.isWarProject()); String mainClass = MainClassResolver.resolveMainClass( rawConfiguration.getMainClass().orElse(null), projectProperties); @@ -330,18 +331,17 @@ static Set getVolumesSet(RawConfiguration rawConfiguration) * JavaLayerConfigurations#DEFAULT_APP_ROOT} for other projects. * * @param rawConfiguration raw configuration data - * @param projectProperties used for providing additional information + * @param isWarProject whether or not the project is a WAR project * @return the app root value * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path */ @VisibleForTesting - static AbsoluteUnixPath getAppRootChecked( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws InvalidAppRootException { + public static AbsoluteUnixPath getAppRootChecked( + RawConfiguration rawConfiguration, boolean isWarProject) throws InvalidAppRootException { String appRoot = rawConfiguration.getAppRoot(); if (appRoot.isEmpty()) { appRoot = - projectProperties.isWarProject() + isWarProject ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT : JavaLayerConfigurations.DEFAULT_APP_ROOT; } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 819689a56f..bf5f65a5a1 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -332,7 +332,7 @@ public void testGetAppRootChecked() throws InvalidAppRootException { Assert.assertEquals( AbsoluteUnixPath.get("/some/root"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false)); } @Test @@ -340,7 +340,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("relative/path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("relative/path", ex.getMessage()); @@ -352,7 +352,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("\\windows\\path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("\\windows\\path", ex.getMessage()); @@ -364,7 +364,7 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("C:\\windows\\path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("C:\\windows\\path", ex.getMessage()); @@ -374,21 +374,19 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { @Test public void testGetAppRootChecked_defaultNonWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); - Mockito.when(projectProperties.isWarProject()).thenReturn(false); Assert.assertEquals( AbsoluteUnixPath.get("/app"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false)); } @Test public void testGetAppRootChecked_defaultWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); - Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( AbsoluteUnixPath.get("/jetty/webapps/ROOT"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, true)); } @Test From 46874d7c4972d01e0912ede7552de3f68543d290 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 21 Feb 2019 12:47:34 -0500 Subject: [PATCH 0475/2020] Choose Base Distroless Java version based on project version (#1483) --- .../DefaultTargetProjectIntegrationTest.java | 4 +- .../gradle/EmptyProjectIntegrationTest.java | 3 +- .../cloud/tools/jib/gradle/JibRunHelper.java | 8 +- .../gradle/SingleProjectIntegrationTest.java | 45 +++++- .../simple/build-java11-incompatible.gradle | 18 +++ .../projects/simple/build-java11.gradle | 17 +++ .../tools/jib/gradle/BuildDockerTask.java | 8 +- .../tools/jib/gradle/BuildImageTask.java | 8 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 8 +- .../jib/gradle/GradleProjectProperties.java | 18 +-- .../gradle/GradleProjectPropertiesTest.java | 39 +++-- .../tools/jib/maven/BuildDockerMojo.java | 8 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 8 +- .../jib/maven/MavenProjectProperties.java | 61 +++----- .../maven/BuildImageMojoIntegrationTest.java | 47 +++++- .../jib/maven/MavenProjectPropertiesTest.java | 90 +++++------- .../maven/projects/default-target/pom.xml | 1 - .../maven/projects/empty/pom-broken-user.xml | 1 - .../resources/maven/projects/empty/pom.xml | 1 - .../simple/pom-complex-properties.xml | 1 - .../maven/projects/simple/pom-complex.xml | 1 - .../projects/simple/pom-dockerclient.xml | 4 +- .../simple/pom-java11-incompatible.xml | 63 ++++++++ .../maven/projects/simple/pom-java11.xml | 60 ++++++++ .../maven/projects/simple/pom-no-to-image.xml | 1 - .../resources/maven/projects/simple/pom.xml | 1 - .../projects/war_servlet25/pom-tomcat.xml | 1 - .../maven/projects/war_servlet25/pom.xml | 1 - .../plugins/common/HelpfulSuggestions.java | 37 ++++- ...mpatibleBaseImageJavaVersionException.java | 42 ++++++ .../common/PluginConfigurationProcessor.java | 101 +++++++++---- .../jib/plugins/common/ProjectProperties.java | 2 + .../common/HelpfulSuggestionsTest.java | 15 +- .../PluginConfigurationProcessorTest.java | 138 +++++++++++++++--- 35 files changed, 670 insertions(+), 199 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11-incompatible.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11.xml create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/IncompatibleBaseImageJavaVersionException.java diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java index 7965c8ac63..a29c0452ac 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java @@ -82,7 +82,9 @@ public void testDockerDaemon_defaultTarget() Assert.assertEquals( "Hello, world. An argument.\n", JibRunHelper.buildToDockerDaemonAndRun( - defaultTargetTestProject, "default-target-name:default-target-version")); + defaultTargetTestProject, + "default-target-name:default-target-version", + "build.gradle")); assertDockerInspect("default-target-name:default-target-version"); } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index 8519413c0b..e55243608f 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -87,7 +87,8 @@ public void testBuild_multipleTags() @Test public void testDockerDaemon_empty() throws IOException, InterruptedException, DigestException { String targetImage = "emptyimage:gradle" + System.nanoTime(); - Assert.assertEquals("", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage)); + Assert.assertEquals( + "", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage, "build.gradle")); Assert.assertEquals( "1970-01-01T00:00:00Z", new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 57fd144dfa..b2cb9a63ef 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -53,6 +53,7 @@ static String buildAndRun( "clean", "jib", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); @@ -70,6 +71,7 @@ static void buildAndRunAdditionalTag( "clean", "jib", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, "-D_ADDITIONAL_TAG=" + additionalTag); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); @@ -95,6 +97,7 @@ static void buildToDockerDaemon( "clean", "jibDockerBuild", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); @@ -105,9 +108,10 @@ static void buildToDockerDaemon( Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); } - static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) + static String buildToDockerDaemonAndRun( + TestProject testProject, String imageReference, String gradleBuildFile) throws IOException, InterruptedException, DigestException { - buildToDockerDaemon(testProject, imageReference, "build.gradle"); + buildToDockerDaemon(testProject, imageReference, gradleBuildFile); return new Command("docker", "run", "--rm", imageReference).run(); } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index ef264424b4..d007954722 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.base.Splitter; import java.io.IOException; import java.security.DigestException; import java.time.Instant; @@ -44,6 +45,11 @@ public class SingleProjectIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + private static boolean isJava11RuntimeOrHigher() { + Iterable split = Splitter.on(".").split(System.getProperty("java.version")); + return Integer.valueOf(split.iterator().next()) >= 11; + } + /** * Asserts that the creation time of the simple test project is set. If the time parsed from the * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the @@ -114,6 +120,7 @@ private static String buildAndRunComplex( "clean", "jib", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, "-D_TARGET_USERNAME=" + username, "-D_TARGET_PASSWORD=" + password, @@ -150,6 +157,7 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE "clean", "jib", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-x=classes", "-D_TARGET_IMAGE=" + targetImage); Assert.fail(); @@ -170,6 +178,39 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertWorkingDirectory("/home", targetImage); } + @Test + public void testDockerDaemon_simpleOnJava11() + throws DigestException, IOException, InterruptedException { + Assume.assumeTrue(isJava11RuntimeOrHigher()); + + String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); + Assert.assertEquals( + "Hello, world. \n", + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-java11.gradle")); + } + + @Test + public void testDockerDaemon_simpleWithIncompatibleJava11() + throws DigestException, IOException, InterruptedException { + Assume.assumeTrue(isJava11RuntimeOrHigher()); + + try { + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, "willnotbuild", "build-java11-incompatible.gradle"); + Assert.fail(); + + } catch (UnexpectedBuildFailure ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + + "configure a Java 11-compatible base image using the 'jib.from.image' " + + "parameter, or set targetCompatibility = 8 or below in your build " + + "configuration")); + } + } + @Test public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); @@ -198,7 +239,7 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException, Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", - JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage)); + JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage, "build.gradle")); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertDockerInspect(targetImage); assertWorkingDirectory("/home", targetImage); @@ -218,6 +259,7 @@ public void testExecute_dockerClient() throws IOException, InterruptedException, "clean", "jibDockerBuild", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + targetImage, "-b=build-dockerclient.gradle", "--debug"); @@ -241,6 +283,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { "clean", "jibBuildTar", "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", "-D_TARGET_IMAGE=" + targetImage); JibRunHelper.assertBuildSuccess(buildResult, "jibBuildTar", "Built image tarball at "); diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle new file mode 100644 index 0000000000..ed86033374 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.from.image = 'gcr.io/distroless/java' +jib.to.image = System.getProperty("_TARGET_IMAGE") \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle new file mode 100644 index 0000000000..62b07c9c24 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") \ No newline at end of file diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 0af9d8d053..8f6f87bb64 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -116,7 +117,6 @@ public void buildDocker() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); @@ -171,6 +171,12 @@ public void buildDocker() } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 31686c8c75..50e19110b1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -93,7 +94,6 @@ public void buildImage() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( @@ -150,6 +150,12 @@ public void buildImage() } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 9f76c68cc9..05a8976e0c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -115,7 +116,6 @@ public void buildTar() jibExtension.getExtraDirectory().getPath(), jibExtension.getExtraDirectory().getPermissions(), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(jibExtension.getFrom().getImage()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); @@ -168,6 +168,12 @@ public void buildTar() } catch (InvalidContainerVolumeException ex) { throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new GradleException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 53b1668119..156110cc5a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -244,24 +243,15 @@ public String getVersion() { return project.getVersion().toString(); } - void validateAgainstDefaultBaseImageVersion(@Nullable String baseImage) { - if (!PluginConfigurationProcessor.usingDefaultBaseImage(baseImage)) { - return; - } + @Override + public int getMajorJavaVersion() { JavaVersion version = JavaVersion.current(); JavaPluginConvention javaPluginConvention = project.getConvention().findPlugin(JavaPluginConvention.class); if (javaPluginConvention != null) { - version = JavaVersion.toVersion(javaPluginConvention.getTargetCompatibility()); - } - if (version.isJava9Compatible()) { - throw new GradleException( - "Jib's default base image uses Java 8, but project is using Java " - + version.getMajorVersion() - + "; perhaps you should configure a Java " - + version.getMajorVersion() - + "-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 1.8 in your build configuration"); + version = javaPluginConvention.getTargetCompatibility(); } + return Integer.valueOf(version.getMajorVersion()); } /** diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 5ea972bfbf..a5e0eaaa39 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableSet; import java.util.Collections; import org.gradle.StartParameter; -import org.gradle.api.GradleException; import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.internal.file.FileResolver; @@ -38,7 +37,9 @@ import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; +import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -151,24 +152,34 @@ public void testConvertPermissionsMap() { } @Test - public void testValidateBaseImageVersion() { - gradleProjectProperties.validateAgainstDefaultBaseImageVersion("nonDefault"); - + public void testGetMajorJavaVersion() { Mockito.when(mockConvention.findPlugin(JavaPluginConvention.class)) .thenReturn(mockJavaPluginConvention); + Mockito.when(mockJavaPluginConvention.getTargetCompatibility()) - .thenReturn(JavaVersion.VERSION_1_8); - gradleProjectProperties.validateAgainstDefaultBaseImageVersion(null); + .thenReturn(JavaVersion.VERSION_1_3); + Assert.assertEquals(3, gradleProjectProperties.getMajorJavaVersion()); Mockito.when(mockJavaPluginConvention.getTargetCompatibility()) .thenReturn(JavaVersion.VERSION_11); - try { - gradleProjectProperties.validateAgainstDefaultBaseImageVersion(null); - Assert.fail(); - } catch (GradleException ex) { - Assert.assertEquals( - "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 1.8 in your build configuration", - ex.getMessage()); - } + Assert.assertEquals(11, gradleProjectProperties.getMajorJavaVersion()); + + Mockito.when(mockJavaPluginConvention.getTargetCompatibility()) + .thenReturn(JavaVersion.VERSION_1_9); + Assert.assertEquals(9, gradleProjectProperties.getMajorJavaVersion()); + } + + @Test + public void testGetMajorJavaVersion_jvm8() { + Assume.assumeThat(JavaVersion.current(), CoreMatchers.is(JavaVersion.VERSION_1_8)); + + Assert.assertEquals(8, gradleProjectProperties.getMajorJavaVersion()); + } + + @Test + public void testGetMajorJavaVersion_jvm11() { + Assume.assumeThat(JavaVersion.current(), CoreMatchers.is(JavaVersion.VERSION_11)); + + Assert.assertEquals(11, gradleProjectProperties.getMajorJavaVersion()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 3f3bb9d3e0..3a7230f876 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -102,7 +103,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); @@ -164,6 +164,12 @@ targetImageReference, getTargetImageAdditionalTags()) throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index f69308f920..350ff6c64f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -99,7 +100,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); @@ -156,6 +156,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a97c367c5a..9840d711df 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; +import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; @@ -75,7 +76,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), appRoot); - projectProperties.validateAgainstDefaultBaseImageVersion(getBaseImage()); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); @@ -134,6 +134,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), + ex); + } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index e13f22d302..9f67b9221a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -39,7 +38,6 @@ import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.Os; @@ -261,47 +259,32 @@ public String getVersion() { return project.getVersion(); } - void validateAgainstDefaultBaseImageVersion(@Nullable String baseImage) - throws MojoFailureException { - if (!PluginConfigurationProcessor.usingDefaultBaseImage(baseImage)) { - return; - } - - // maven-compiler-plugin default is 1.6 - int version = 6; - + @Override + public int getMajorJavaVersion() { // Check properties for version if (project.getProperties().getProperty("maven.compiler.target") != null) { - version = getVersionFromString(project.getProperties().getProperty("maven.compiler.target")); - } else if (project.getProperties().getProperty("maven.compiler.release") != null) { - version = getVersionFromString(project.getProperties().getProperty("maven.compiler.release")); - } else { - // Check maven-compiler-plugin for version - Plugin mavenCompilerPlugin = - project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin"); - if (mavenCompilerPlugin != null) { - Xpp3Dom pluginConfiguration = (Xpp3Dom) mavenCompilerPlugin.getConfiguration(); - if (pluginConfiguration != null) { - Xpp3Dom target = pluginConfiguration.getChild("target"); - if (target != null) { - version = getVersionFromString(target.getValue()); - } else { - Xpp3Dom release = pluginConfiguration.getChild("release"); - if (release != null) { - version = getVersionFromString(release.getValue()); - } - } - } - } + return getVersionFromString(project.getProperties().getProperty("maven.compiler.target")); + } + if (project.getProperties().getProperty("maven.compiler.release") != null) { + return getVersionFromString(project.getProperties().getProperty("maven.compiler.release")); } - if (version > 8) { - throw new MojoFailureException( - "Jib's default base image uses Java 8, but project is using Java " - + version - + "; perhaps you should configure a Java " - + version - + "-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration"); + // Check maven-compiler-plugin for version + Plugin mavenCompilerPlugin = + project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin"); + if (mavenCompilerPlugin != null) { + Xpp3Dom pluginConfiguration = (Xpp3Dom) mavenCompilerPlugin.getConfiguration(); + if (pluginConfiguration != null) { + Xpp3Dom target = pluginConfiguration.getChild("target"); + if (target != null) { + return getVersionFromString(target.getValue()); + } + Xpp3Dom release = pluginConfiguration.getChild("release"); + if (release != null) { + return getVersionFromString(release.getValue()); + } + } } + return 6; // maven-compiler-plugin default is 1.6 } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 64ee68dbfb..8b3801e552 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.base.Splitter; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -39,6 +40,7 @@ import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -91,17 +93,24 @@ static String assertImageId(Path projectRoot) throws IOException, DigestExceptio return DescriptorDigest.fromDigest(id).toString(); } + private static boolean isJava11RuntimeOrHigher() { + Iterable split = Splitter.on(".").split(System.getProperty("java.version")); + return Integer.valueOf(split.iterator().next()) >= 11; + } + /** * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code * imageReference}. */ - private static String buildAndRun(Path projectRoot, String imageReference, boolean runTwice) + private static String buildAndRun( + Path projectRoot, String imageReference, String pomXml, boolean runTwice) throws VerificationException, IOException, InterruptedException, DigestException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setAutoclean(false); verifier.addCliOption("-X"); + verifier.addCliOption("--file=" + pomXml); verifier.executeGoals(Arrays.asList("clean", "compile")); // Builds twice, and checks if the second build took less time. @@ -328,7 +337,7 @@ public void testExecute_simple() Assert.assertEquals( "Hello, " + before + ". An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", - buildAndRun(simpleTestProject.getProjectRoot(), targetImage, true)); + buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom.xml", true)); Instant buildTime = Instant.parse( @@ -337,11 +346,43 @@ public void testExecute_simple() assertWorkingDirectory("/home", targetImage); } + @Test + public void testExecute_simpleOnJava11() + throws DigestException, VerificationException, IOException, InterruptedException { + Assume.assumeTrue(isJava11RuntimeOrHigher()); + + String targetImage = getGcrImageReference("simpleimage:maven"); + Assert.assertEquals( + "Hello, world. An argument.\n", + buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-java11.xml", false)); + } + + @Test + public void testExecute_simpleWithIncomptiableJava11() + throws DigestException, IOException, InterruptedException { + Assume.assumeTrue(isJava11RuntimeOrHigher()); + + try { + buildAndRun( + simpleTestProject.getProjectRoot(), "willnotbuild", "pom-java11-incompatible.xml", false); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + + "configure a Java 11-compatible base image using the '' " + + "parameter, or set maven-compiler-plugin's '' or '' version " + + "to 8 or below in your build configuration")); + } + } + @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException, DigestException { String targetImage = getGcrImageReference("emptyimage:maven"); - Assert.assertEquals("", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, false)); + Assert.assertEquals( + "", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, "pom.xml", false)); assertCreationTimeEpoch(targetImage); assertWorkingDirectory("", targetImage); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 03225807cb..5c3d5ad0d5 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.util.Properties; import org.apache.maven.model.Plugin; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; @@ -140,84 +139,65 @@ public void testGetVersionFromString() { } @Test - public void testValidateBaseImageVersion_nonDefaultBaseImage() throws MojoFailureException { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion("non-default"); + public void testGetMajorJavaVersion_undefinedDefaultsTo6() { + Assert.assertEquals(6, mavenProjectProperties.getMajorJavaVersion()); } @Test - public void testValidateBaseImageVersion_allNull() throws MojoFailureException { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - } - - @Test - public void testValidateBaseImageVersion_targetProperty() throws MojoFailureException { + public void testGetMajorJavaVersion_targetProperty() { Mockito.when(mockMavenProperties.getProperty("maven.compiler.target")).thenReturn("1.8"); - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); + Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(mockMavenProperties.getProperty("maven.compiler.target")).thenReturn("1.7"); + Assert.assertEquals(7, mavenProjectProperties.getMajorJavaVersion()); Mockito.when(mockMavenProperties.getProperty("maven.compiler.target")).thenReturn("11"); - try { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - Assert.fail(); - } catch (MojoFailureException ex) { - Assert.assertEquals( - "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", - ex.getMessage()); - } + Assert.assertEquals(11, mavenProjectProperties.getMajorJavaVersion()); } @Test - public void testValidateBaseImageVersion_releaseProperty() throws MojoFailureException { - Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("8"); - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - - Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("11.0"); - try { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - Assert.fail(); - } catch (MojoFailureException ex) { - Assert.assertEquals( - "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", - ex.getMessage()); - } + public void testValidateBaseImageVersion_releaseProperty() { + Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("1.8"); + Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("1.7"); + Assert.assertEquals(7, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(mockMavenProperties.getProperty("maven.compiler.release")).thenReturn("9"); + Assert.assertEquals(9, mavenProjectProperties.getMajorJavaVersion()); } @Test - public void testValidateBaseImageVersion_compilerPluginTarget() throws MojoFailureException { + public void testValidateBaseImageVersion_compilerPluginTarget() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) .thenReturn(mockCompilerPlugin); Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); Mockito.when(compilerPluginConfiguration.getChild("target")).thenReturn(compilerTarget); + Mockito.when(compilerTarget.getValue()).thenReturn("1.8"); - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - - Mockito.when(compilerTarget.getValue()).thenReturn("11"); - try { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - Assert.fail(); - } catch (MojoFailureException ex) { - Assert.assertEquals( - "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", - ex.getMessage()); - } + Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(compilerTarget.getValue()).thenReturn("1.6"); + Assert.assertEquals(6, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(compilerTarget.getValue()).thenReturn("13"); + Assert.assertEquals(13, mavenProjectProperties.getMajorJavaVersion()); } @Test - public void testValidateBaseImageVersion_compilerPluginRelease() throws MojoFailureException { + public void testValidateBaseImageVersion_compilerPluginRelease() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) .thenReturn(mockCompilerPlugin); Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); Mockito.when(compilerPluginConfiguration.getChild("release")).thenReturn(compilerRelease); + Mockito.when(compilerRelease.getValue()).thenReturn("1.8"); - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - - Mockito.when(compilerRelease.getValue()).thenReturn("11"); - try { - mavenProjectProperties.validateAgainstDefaultBaseImageVersion(null); - Assert.fail(); - } catch (MojoFailureException ex) { - Assert.assertEquals( - "Jib's default base image uses Java 8, but project is using Java 11; perhaps you should configure a Java 11-compatible base image using the '' parameter, or set maven-compiler-plugin's target or release version to 1.8 in your build configuration", - ex.getMessage()); - } + Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(compilerRelease.getValue()).thenReturn("10"); + Assert.assertEquals(10, mavenProjectProperties.getMajorJavaVersion()); + + Mockito.when(compilerRelease.getValue()).thenReturn("13"); + Assert.assertEquals(13, mavenProjectProperties.getMajorJavaVersion()); } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml index bbbd3abe1f..7e31c67d14 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/default-target/pom.xml @@ -8,7 +8,6 @@ default-target-version - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml index 248cae963e..1457b7bb99 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom-broken-user.xml @@ -8,7 +8,6 @@ 1 - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml index 767f36018f..a6186c51ce 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/empty/pom.xml @@ -8,7 +8,6 @@ 1 - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml index 134f0f91a8..9e392e7e84 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml @@ -8,7 +8,6 @@ 1 - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml index 423d941937..f7ce521dd8 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml @@ -8,7 +8,6 @@ 1 - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml index 6f1e612976..2f3200dc5b 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-dockerclient.xml @@ -1,4 +1,6 @@ - + + 4.0.0 com.test diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11-incompatible.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11-incompatible.xml new file mode 100644 index 0000000000..1db5ed7a4b --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11-incompatible.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java + + + ${_TARGET_IMAGE} + + + An argument. + + 1000/tcp + 2000-2003/udp + + + value1 + value2 + + + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11.xml new file mode 100644 index 0000000000..be1ed2dc59 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-java11.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + An argument. + + 1000/tcp + 2000-2003/udp + + + value1 + value2 + + + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml index b609317781..75bad72779 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-no-to-image.xml @@ -10,7 +10,6 @@ Name not Usable as Image Reference - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml index 8306a8427c..6d6776aa38 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml @@ -8,7 +8,6 @@ 1 - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml index 19a0962aee..2626a734dc 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom-tomcat.xml @@ -9,7 +9,6 @@ war - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml index 440d258466..c51d357f05 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/war_servlet25/pom.xml @@ -9,7 +9,6 @@ war - 1.8 UTF-8 UTF-8 @@PluginVersion@@ diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index ff2260a61a..6681fbd4f8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -54,10 +54,25 @@ public static String forMainClassNotFound(String messagePrefix, String pluginNam return suggest(messagePrefix, "add a `mainClass` configuration to " + pluginName); } - public static String forDockerContextInsecureRecursiveDelete( - String messagePrefix, String directory) { - return suggest( - messagePrefix, "clear " + directory + " manually before creating the Docker context"); + public static String forIncompatibleBaseImageJavaVesionForGradle( + int baseImageMajorJavaVersion, int projectMajorJavaVersion) { + return forIncompatibleBaseImageJavaVesion( + baseImageMajorJavaVersion, + projectMajorJavaVersion, + "using the 'jib.from.image' parameter, or set targetCompatibility = " + + baseImageMajorJavaVersion + + " or below"); + } + + public static String forIncompatibleBaseImageJavaVesionForMaven( + int baseImageMajorJavaVersion, int projectMajorJavaVersion) { + return forIncompatibleBaseImageJavaVesion( + baseImageMajorJavaVersion, + projectMajorJavaVersion, + "using the '' parameter, or set maven-compiler-plugin's '' or " + + "'' version to " + + baseImageMajorJavaVersion + + " or below"); } /** @@ -69,6 +84,20 @@ public static String suggest(String messagePrefix, String suggestion) { return messagePrefix + ", perhaps you should " + suggestion; } + private static String forIncompatibleBaseImageJavaVesion( + int baseImageMajorJavaVersion, int projectMajorJavaVersion, String parameterInstructions) { + return suggest( + "Your project is using Java " + + projectMajorJavaVersion + + " but the base image is for Java " + + baseImageMajorJavaVersion, + "configure a Java " + + projectMajorJavaVersion + + "-compatible base image " + + parameterInstructions + + " in your build configuration"); + } + private final String messagePrefix; private final String clearCacheCommand; @Nullable private final ImageReference baseImageReference; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/IncompatibleBaseImageJavaVersionException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/IncompatibleBaseImageJavaVersionException.java new file mode 100644 index 0000000000..a578047726 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/IncompatibleBaseImageJavaVersionException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Exception when the Java version in the base image is incompatible with the Java version of the + * application to be containerized. For example, when the project is Java 11 but the base image is + * {@code gcr.io/distroless/java:8}. + */ +public class IncompatibleBaseImageJavaVersionException extends Exception { + + private final int baseImageMajorJavaVersion; + private final int projectMajorJavaVersion; + + public IncompatibleBaseImageJavaVersionException( + int baseImageMajorJavaVersion, int projectMajorJavaVersion) { + this.baseImageMajorJavaVersion = baseImageMajorJavaVersion; + this.projectMajorJavaVersion = projectMajorJavaVersion; + } + + public int getBaseImageMajorJavaVersion() { + return baseImageMajorJavaVersion; + } + + public int getProjectMajorJavaVersion() { + return projectMajorJavaVersion; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index eff597810f..cf480df676 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -55,23 +55,6 @@ */ public class PluginConfigurationProcessor { - private static final String DEFAULT_BASE_IMAGE = "gcr.io/distroless/java"; - private static final String DEFAULT_BASE_IMAGE_WAR = "gcr.io/distroless/java/jetty"; - - /** - * Checks whether or not the default distroless base image is being used. - * - * @param baseImageConfiguration the configured base image - * @return {@code true} if the base image is null or equal to one of the distroless java images, - * else {@code false} - */ - public static boolean usingDefaultBaseImage(@Nullable String baseImageConfiguration) { - // TODO: check for other Java 8 base images? - return baseImageConfiguration == null - || baseImageConfiguration.equals(DEFAULT_BASE_IMAGE) - || baseImageConfiguration.equals(DEFAULT_BASE_IMAGE_WAR); - } - public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, @@ -81,7 +64,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -110,7 +93,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -131,7 +114,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry ProjectProperties projectProperties) throws InferredAuthRetrievalException, InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -173,7 +157,7 @@ static PluginConfigurationProcessor processCommonConfiguration( boolean isTargetImageCredentialPresent) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); @@ -290,16 +274,37 @@ static List computeEntrypoint( * @param rawConfiguration raw configuration data * @param projectProperties used for providing additional information * @return the base image + * @throws IncompatibleBaseImageJavaVersionException when the Java version in the base image is + * incompatible with the Java version of the application to be containerized */ @VisibleForTesting - static String getBaseImage( - RawConfiguration rawConfiguration, ProjectProperties projectProperties) { - return rawConfiguration - .getFromImage() - .orElse( - projectProperties.isWarProject() - ? "gcr.io/distroless/java/jetty" - : "gcr.io/distroless/java"); + static String getBaseImage(RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws IncompatibleBaseImageJavaVersionException { + int javaVersion = projectProperties.getMajorJavaVersion(); + + if (rawConfiguration.getFromImage().isPresent()) { + String baseImage = rawConfiguration.getFromImage().get(); + + if (isKnownDistrolessJava8Image(baseImage) && javaVersion > 8) { + throw new IncompatibleBaseImageJavaVersionException(8, javaVersion); + } + if (isKnownDistrolessJava11Image(baseImage) && javaVersion > 11) { + throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); + } + return baseImage; + } + + // Base image not configured; auto-pick Distroless. + if (projectProperties.isWarProject()) { + return "gcr.io/distroless/java/jetty"; + } + if (javaVersion <= 8) { + return "gcr.io/distroless/java:8"; + } + if (javaVersion <= 11) { + return "gcr.io/distroless/java:11"; + } + throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); } /** @@ -471,6 +476,44 @@ private static Path getCheckedCacheDirectory(String property, Path defaultPath) return defaultPath; } + /** + * Checks if the given image is a known Java 8 distroless image. Checking against only images + * known to Java 8, the method may to return {@code false} for Java 8 distroless unknown to it. + * + * @param imageReference the image reference + * @return {@code true} if the image is equal to one of the known Java 8 distroless images, else + * {@code false} + */ + private static boolean isKnownDistrolessJava8Image(String imageReference) { + // TODO: drop "latest", "debug", and the like once they no longer point to Java 8. + return imageReference.equals("gcr.io/distroless/java") + || imageReference.equals("gcr.io/distroless/java:latest") + || imageReference.equals("gcr.io/distroless/java:debug") + || imageReference.equals("gcr.io/distroless/java:8") + || imageReference.equals("gcr.io/distroless/java:8-debug") + || imageReference.equals("gcr.io/distroless/java/jetty") + || imageReference.equals("gcr.io/distroless/java/jetty:latest") + || imageReference.equals("gcr.io/distroless/java/jetty:debug") + || imageReference.equals("gcr.io/distroless/java/jetty:java8") + || imageReference.equals("gcr.io/distroless/java/jetty:java8-debug"); + } + + /** + * Checks if the given image is a known Java 11 distroless image. Checking against only images + * known to Java 11, the method may to return {@code false} for Java 11 distroless unknown to it. + * + * @param imageReference the image reference + * @return {@code true} if the image is equal to one of the known Java 11 distroless images, else + * {@code false} + */ + private static boolean isKnownDistrolessJava11Image(String imageReference) { + // TODO: add "latest", "debug", and the like to this list once they point to Java 11. + return imageReference.equals("gcr.io/distroless/java:11") + || imageReference.equals("gcr.io/distroless/java:11-debug") + || imageReference.equals("gcr.io/distroless/java/jetty:java11") + || imageReference.equals("gcr.io/distroless/java/jetty:java11-debug"); + } + private final JibContainerBuilder jibContainerBuilder; private final ImageReference baseImageReference; private final ImageReference targetImageReference; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index a21c9dc28a..aaaefb6d9b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -58,4 +58,6 @@ public interface ProjectProperties { String getName(); String getVersion(); + + int getMajorJavaVersion(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 96726ec98a..e3d40c4efc 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -63,9 +63,6 @@ public void testSuggestions_smoke() { Assert.assertEquals( "messagePrefix, perhaps you should make sure your credentials for 'targetregistry' are set up correctly", TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("targetregistry", "targetrepository")); - Assert.assertEquals( - "messagePrefix, perhaps you should clear directory manually before creating the Docker context", - HelpfulSuggestions.forDockerContextInsecureRecursiveDelete("messagePrefix", "directory")); Assert.assertEquals( "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", HelpfulSuggestions.forMainClassNotFound("messagePrefix", "plugin")); @@ -73,6 +70,18 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should add a parameter configuration parameter to your buildFile or set the parameter via the commandline (e.g. 'command').", HelpfulSuggestions.forToNotConfigured( "messagePrefix", "parameter", "buildFile", "command")); + Assert.assertEquals( + "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + + "configure a Java 11-compatible base image using the 'jib.from.image' " + + "parameter, or set targetCompatibility = 8 or below in your build " + + "configuration", + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle(8, 11)); + Assert.assertEquals( + "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + + "configure a Java 11-compatible base image using the '' " + + "parameter, or set maven-compiler-plugin's '' or '' version " + + "to 8 or below in your build configuration", + HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven(8, 11)); Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); Assert.assertEquals( "messagePrefix, perhaps you should use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true", diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index bf5f65a5a1..ce14482939 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -92,7 +92,8 @@ public void setUp() { public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -114,7 +115,8 @@ public void testPluginConfigurationProcessor_defaults() public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() throws InferredAuthRetrievalException, InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, IOException, - InvalidWorkingDirectoryException, InvalidImageReferenceException { + InvalidWorkingDirectoryException, InvalidImageReferenceException, + IncompatibleBaseImageJavaVersionException { System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); @@ -131,7 +133,7 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -146,7 +148,8 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -185,7 +188,8 @@ public void testComputeEntrypoint_inheritKeywordInNonSingletonList() public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -202,7 +206,8 @@ public void testEntrypoint_defaultWarPackaging() public void testEntrypoint_defaulNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -223,7 +228,8 @@ public void testEntrypoint_defaulNonWarPackaging() public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -238,7 +244,8 @@ public void testUser() public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -251,7 +258,8 @@ public void testUser_null() public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -272,7 +280,8 @@ public void testEntrypoint_warningOnJvmFlags() public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -293,7 +302,8 @@ public void testEntrypoint_warningOnMainclass() public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -315,7 +325,8 @@ public void testEntrypointClasspath_nonDefaultAppRoot() public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException { + InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -421,16 +432,109 @@ public void testGetWorkingDirectoryChecked_notAbsolute() { } @Test - public void testGetBaseImage_defaultNonWarPackaging() { + public void testGetBaseImage_chooseJava8Distroless() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(6); + Assert.assertEquals( + "gcr.io/distroless/java:8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(7); + Assert.assertEquals( + "gcr.io/distroless/java:8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(8); + Assert.assertEquals( + "gcr.io/distroless/java:8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_chooseJava11Distroless() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(9); + Assert.assertEquals( + "gcr.io/distroless/java:11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(10); + Assert.assertEquals( + "gcr.io/distroless/java:11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); + Assert.assertEquals( + "gcr.io/distroless/java:11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_projectHigherThanJava11() { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(12); + + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(12, ex.getProjectMajorJavaVersion()); + } + } + + @Test + public void testGetBaseImage_incompatibleJava8BaseImage() { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); + + Mockito.when(rawConfiguration.getFromImage()) + .thenReturn(Optional.of("gcr.io/distroless/java:8")); + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(11, ex.getProjectMajorJavaVersion()); + } + + Mockito.when(rawConfiguration.getFromImage()) + .thenReturn(Optional.of("gcr.io/distroless/java:latest")); + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(11, ex.getProjectMajorJavaVersion()); + } + } + + @Test + public void testGetBaseImage_incompatibleJava11BaseImage() { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(15); + + Mockito.when(rawConfiguration.getFromImage()) + .thenReturn(Optional.of("gcr.io/distroless/java:11")); + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(15, ex.getProjectMajorJavaVersion()); + } + } + + @Test + public void testGetBaseImage_defaultNonWarPackaging() + throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(false); Assert.assertEquals( - "gcr.io/distroless/java", + "gcr.io/distroless/java:8", PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); } @Test - public void testGetBaseImage_defaultWarProject() { + public void testGetBaseImage_defaultWarProject() + throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( @@ -439,7 +543,7 @@ public void testGetBaseImage_defaultWarProject() { } @Test - public void testGetBaseImage_nonDefault() { + public void testGetBaseImage_nonDefault() throws IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("tomcat")); Assert.assertEquals( @@ -471,7 +575,7 @@ public void testGetInvalidVolumesList() { private PluginConfigurationProcessor createPluginConfigurationProcessor() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { return PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, ignored -> Optional.empty(), From 6f69a9c6665175094b93bf57d5e720307f786afd Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 21 Feb 2019 13:47:34 -0500 Subject: [PATCH 0476/2020] Update CHANGELOG for distroless 11 (#1497) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e557189d18..67d9523a69 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 16141b662b..00624fe999 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + ### Changed ### Fixed From 1245bcdc5d4a8bbeb0203fafc5dd82748f4a430a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 21 Feb 2019 14:35:03 -0500 Subject: [PATCH 0477/2020] Gradle release v1.0.1 (#1498) * [Gradle Release Plugin] - pre tag commit: 'v1.0.1-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.0.2-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 4e224f959a..63d32434ac 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.0.1-SNAPSHOT +version = 1.0.2-SNAPSHOT From c001e3253f32d30025e1e86a47b5ba2c6db7826b Mon Sep 17 00:00:00 2001 From: Q Chen Date: Thu, 21 Feb 2019 14:46:03 -0500 Subject: [PATCH 0478/2020] Maven release v1.0.1 (#1499) * preparing release 1.0.1 * 1.0.2-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 71730f0749..ab5dfbbf11 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.1-SNAPSHOT + 1.0.2-SNAPSHOT maven-plugin Jib From a5feeea7cb63704736f7baf50ff79d3a2c2dc980 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 22 Feb 2019 12:14:40 -0500 Subject: [PATCH 0479/2020] Update version strings for 1.0.1 (#1505) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 19 files changed, 36 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e803db0501..d16af1e302 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.2-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.0.0' + // id 'com.google.cloud.tools.jib' version '1.0.1' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 339c579bdc..bdbaf761c0 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.0.0 + 1.0.1 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 3d41b988ce..82fff3bdf9 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 8bae69615f..df471f88f2 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.0 + 1.0.1 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 11c26edd6e..03e4a30210 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index ae9d4f3a4c..afea86b688 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.0 + 1.0.1 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index a2a7319fce..1d9ffe0c59 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.0.0" + id("com.google.cloud.tools.jib") version "1.0.1" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 941e8d40d1..258b2b2cd9 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index a9f3c36825..a172dde78f 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.1' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 8ebc5fdf2f..1aa4d05333 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.0 + 1.0.1 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 9c8a639e25..3a3f1434ab 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index ff45ba415a..723aa862dd 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 1.0.0 + 1.0.1 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index 3b77812299..9754881320 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index 93eadd70e7..cf531c0e5b 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.0 + 1.0.1 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 30e17ca71c..273ae68d05 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 67d9523a69..5b579531d9 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,12 +5,18 @@ All notable changes to this project will be documented in this file. ### Added -- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) - ### Changed ### Fixed +## 1.0.1 + +### Added + +- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + +### Fixed + - Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) ## 1.0.0 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 6f25e43139..0ae8317ad6 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -43,7 +43,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.0.0' + id 'com.google.cloud.tools.jib' version '1.0.1' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 00624fe999..86df0f2f99 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,12 +5,18 @@ All notable changes to this project will be documented in this file. ### Added -- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) - ### Changed ### Fixed +## 1.0.1 + +### Added + +- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) + +### Fixed + - Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) ## 1.0.0 diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 450bdcc915..37e2360b07 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -38,7 +38,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.1:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -46,7 +46,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -64,7 +64,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.0.0 + 1.0.1 myimage From d76674793c8a32d735f4df25e99741c0da83159c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 22 Feb 2019 14:28:37 -0500 Subject: [PATCH 0480/2020] Restart docker as workaround to fix clock skew (#1506) --- kokoro/presubmit.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index 674f8f4ecf..3a4770dbbb 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -10,6 +10,17 @@ export PATH=$PATH:/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/b docker stop $(docker ps --all --quiet) || true docker kill $(docker ps --all --quiet) || true +# Restarting Docker for Mac to get around the certificate expiration issue: +# b/112707824 +# https://github.com/GoogleContainerTools/jib/issues/730#issuecomment-413603874 +# https://github.com/moby/moby/issues/11534 +# TODO: remove this temporary fix once b/112707824 is permanently fixed. +if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then + osascript -e 'quit app "Docker"' + open -a Docker + while ! docker info > /dev/null 2>&1; do sleep 1; done +fi + cd github/jib # Workaround for issue with calling 'docker login'. It defaults to using docker-credential-osxkeychain and errors with: From b1c5adc55667eb9e31abbda5adee801ed47c9e8e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 25 Feb 2019 10:57:14 -0500 Subject: [PATCH 0481/2020] Add jib:_skaffold-files-v2 goal (#1504) --- .../cloud/tools/jib/gradle/FilesTaskV2.java | 2 + .../tools/jib/gradle/FilesTaskV2Test.java | 8 +- .../jib/maven/JibPluginConfiguration.java | 17 +- .../jib/maven/MavenProjectProperties.java | 20 ++ .../tools/jib/maven/skaffold/FilesMojoV2.java | 204 ++++++++++++++++++ .../jib/maven/skaffold/FilesMojoV2Test.java | 135 ++++++++++++ .../projects/multi/complex-service/pom.xml | 2 +- .../plugins/common/SkaffoldFilesOutput.java | 2 +- 8 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java index e9c02d48a6..44e9ae09cf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java @@ -102,7 +102,9 @@ public void listFiles() throws IOException { } // Print files + System.out.println("\nBEGIN JIB JSON"); System.out.println(skaffoldFilesOutput.getJsonString()); + System.out.println("END JIB JSON"); } /** diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java index d1345d2a1e..b5dc3c7d1c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java @@ -43,7 +43,7 @@ public class FilesTaskV2Test { * * @param project the project to run the task on * @param moduleName the name of the sub-project, or {@code null} if no sub-project - * @return the list of paths printed by the task + * @return the JSON string printed by the task */ private static String verifyTaskSuccess(TestProject project, @Nullable String moduleName) { String taskName = @@ -52,8 +52,12 @@ private static String verifyTaskSuccess(TestProject project, @Nullable String mo BuildTask jibTask = buildResult.task(taskName); Assert.assertNotNull(jibTask); Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + String output = buildResult.getOutput().trim(); + Assert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); + Assert.assertThat(output, CoreMatchers.endsWith("END JIB JSON")); - return buildResult.getOutput().trim(); + // Return task output with header/footer removed + return output.replace("BEGIN JIB JSON", "").replace("END JIB JSON", "").trim(); } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index a04a9daec8..716682494d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -548,21 +548,8 @@ void setProject(MavenProject project) { this.project = project; } - /** - * Gets a system property with the given name. First checks for a -D commandline argument, then - * checks for a property defined in the POM, then returns null if neither are defined. - * - * @param propertyName the name of the system property - * @return the value of the system property, or null if not defined - */ @Nullable - public String getProperty(String propertyName) { - if (session != null && session.getSystemProperties().containsKey(propertyName)) { - return session.getSystemProperties().getProperty(propertyName); - } - if (project != null && project.getProperties().containsKey(propertyName)) { - return project.getProperties().getProperty(propertyName); - } - return null; + String getProperty(String propertyName) { + return MavenProjectProperties.getProperty(propertyName, project, session); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 9f67b9221a..a9ae4b9479 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -36,6 +36,7 @@ import java.nio.file.Paths; import java.util.Map; import javax.annotation.Nullable; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; @@ -89,6 +90,25 @@ static MavenProjectProperties getForProject( } } + /** + * Gets a system property with the given name. First checks for a -D commandline argument, then + * checks for a property defined in the POM, then returns null if neither are defined. + * + * @param propertyName the name of the system property + * @return the value of the system property, or null if not defined + */ + @Nullable + public static String getProperty( + String propertyName, @Nullable MavenProject project, @Nullable MavenSession session) { + if (session != null && session.getSystemProperties().containsKey(propertyName)) { + return session.getSystemProperties().getProperty(propertyName); + } + if (project != null && project.getProperties().containsKey(propertyName)) { + return project.getProperties().getProperty(propertyName); + } + return null; + } + private static EventHandlers makeEventHandlers( Log log, SingleThreadedExecutor singleThreadedExecutor) { ConsoleLoggerBuilder logEventHandlerBuilder = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java new file mode 100644 index 0000000000..e5c8361e1c --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -0,0 +1,204 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.FileSet; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.DefaultDependencyResolutionRequest; +import org.apache.maven.project.DependencyResolutionException; +import org.apache.maven.project.DependencyResolutionResult; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectDependenciesResolver; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.eclipse.aether.graph.DependencyFilter; + +/** + * Print out changing source dependencies on a module. In multimodule applications it should be run + * by activating a single module and its dependent modules. Dependency collection will ignore + * project level snapshots (sub-modules) unless the user has explicitly installed them (by only + * requiring dependencyCollection). For use only within skaffold. + * + *

Expected use: "./mvnw jib:_skaffold-files-v2 -q" or "./mvnw jib:_skaffold-files-v2 -pl module + * -am -q" + */ +@Mojo( + name = FilesMojoV2.GOAL_NAME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, + aggregator = true) +public class FilesMojoV2 extends AbstractMojo { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-files-v2"; + + @Nullable + @Parameter(defaultValue = "${session}", required = true, readonly = true) + private MavenSession session; + + @Nullable + @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) + private List projects; + + // TODO: This is internal maven, we should find a better way to do this + @Nullable @Component private ProjectDependenciesResolver projectDependenciesResolver; + + private final SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + Preconditions.checkNotNull(projects); + Preconditions.checkNotNull(session); + Preconditions.checkNotNull(projectDependenciesResolver); + + for (MavenProject project : projects) { + // Add pom configuration files + skaffoldFilesOutput.addBuild(project.getFile().toPath()); + if ("pom".equals(project.getPackaging())) { + // done if pom + continue; + } + + // Add sources directory (resolved by maven to be an absolute path) + skaffoldFilesOutput.addInput(Paths.get(project.getBuild().getSourceDirectory())); + + // Add resources directory (resolved by maven to be an absolute path) + project + .getBuild() + .getResources() + .stream() + .map(FileSet::getDirectory) + .map(Paths::get) + .forEach(skaffoldFilesOutput::addInput); + + // This seems weird, but we will only print out the jib "extraFiles" directory on projects + // where the plugin is explicitly configured (even though _skaffold-files-v2 is a + // jib-maven-plugin goal and is expected to run on all projects irrespective of their + // configuring of the jib plugin). + if (project.getPlugin(MavenProjectProperties.PLUGIN_KEY) != null) { + // Add extra directory + skaffoldFilesOutput.addInput(resolveExtraDirectory(project)); + } + + // Grab non-project SNAPSHOT dependencies for this project + // TODO: this whole sections relies on internal maven API, it could break. We need to explore + // TODO: better ways to resolve dependencies using the public maven API. + Set projectArtifacts = + projects + .stream() + .map(MavenProject::getArtifact) + .map(Artifact::toString) + .collect(Collectors.toSet()); + + DependencyFilter ignoreProjectDependenciesFilter = + (node, parents) -> { + if (node == null || node.getDependency() == null) { + // if nothing, then ignore + return false; + } + if (projectArtifacts.contains(node.getArtifact().toString())) { + // ignore project dependency artifacts + return false; + } + // we only want compile/runtime deps + return Artifact.SCOPE_COMPILE_PLUS_RUNTIME.contains(node.getDependency().getScope()); + }; + + try { + DependencyResolutionResult resolutionResult = + projectDependenciesResolver.resolve( + new DefaultDependencyResolutionRequest(project, session.getRepositorySession()) + .setResolutionFilter(ignoreProjectDependenciesFilter)); + resolutionResult + .getDependencies() + .stream() + .map(org.eclipse.aether.graph.Dependency::getArtifact) + .filter(org.eclipse.aether.artifact.Artifact::isSnapshot) + .map(org.eclipse.aether.artifact.Artifact::getFile) + .map(File::toPath) + .forEach(skaffoldFilesOutput::addInput); + + } catch (DependencyResolutionException ex) { + throw new MojoExecutionException("Failed to resolve dependencies", ex); + } + } + + try { + // Print JSON string + System.out.println("\nBEGIN JIB JSON"); + System.out.println(skaffoldFilesOutput.getJsonString()); + System.out.println("END JIB JSON"); + } catch (IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } + + private Path resolveExtraDirectory(MavenProject project) { + // Try getting extra directory from project/session properties + String extraDirectoryProperty = + MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); + if (extraDirectoryProperty != null) { + return Paths.get(extraDirectoryProperty); + } + + // Try getting extra directory from project pom + Plugin jibMavenPlugin = project.getPlugin(MavenProjectProperties.PLUGIN_KEY); + if (jibMavenPlugin != null) { + Xpp3Dom pluginConfiguration = (Xpp3Dom) jibMavenPlugin.getConfiguration(); + if (pluginConfiguration != null) { + Xpp3Dom extraDirectoryConfiguration = pluginConfiguration.getChild("extraDirectory"); + if (extraDirectoryConfiguration != null) { + Xpp3Dom child = extraDirectoryConfiguration.getChild("path"); + if (child != null) { + // ... + return Paths.get(child.getValue()); + } + // ... + return Paths.get(extraDirectoryConfiguration.getValue()); + } + } + } + + // Return default if not found + return Preconditions.checkNotNull(project) + .getBasedir() + .getAbsoluteFile() + .toPath() + .resolve("src") + .resolve("main") + .resolve("jib"); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java new file mode 100644 index 0000000000..b81d1aead8 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.TestPlugin; +import com.google.cloud.tools.jib.maven.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; +import com.google.common.base.Strings; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link FilesMojoV2}. */ +public class FilesMojoV2Test { + + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + + @ClassRule + public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + + private static void verifyFiles( + Path projectRoot, String module, List buildFiles, List inputFiles) + throws VerificationException, IOException { + + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-q"); + if (!Strings.isNullOrEmpty(module)) { + verifier.addCliOption("-pl"); + verifier.addCliOption(module); + verifier.addCliOption("-am"); + } + verifier.executeGoal("jib:" + FilesMojoV2.GOAL_NAME); + + verifier.verifyErrorFreeLog(); + Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); + List log = Files.readAllLines(logFile, StandardCharsets.UTF_8); + + int begin = log.indexOf("BEGIN JIB JSON"); + Assert.assertTrue(begin > -1); + Assert.assertTrue( + "log.size() is " + log.size() + ", but begin + 2 is " + begin + 2, log.size() > begin + 2); + Assert.assertEquals("END JIB JSON", log.get(begin + 2)); + + SkaffoldFilesOutput output = new SkaffoldFilesOutput(log.get(begin + 1)); + Assert.assertEquals(buildFiles, output.getBuild()); + Assert.assertEquals(inputFiles, output.getInputs()); + Assert.assertEquals(0, output.getIgnore().size()); + } + + @Test + public void testFilesMojo_singleModule() throws VerificationException, IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + + verifyFiles( + projectRoot, + null, + Collections.singletonList(projectRoot.resolve("pom.xml").toString()), + Arrays.asList( + projectRoot.resolve("src/main/java").toString(), + projectRoot.resolve("src/main/resources").toString(), + projectRoot.resolve("src/main/jib-custom").toString())); + } + + @Test + public void testFilesMojo_multiModuleSimpleService() throws VerificationException, IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path simpleServiceRoot = projectRoot.resolve("simple-service"); + + verifyFiles( + projectRoot, + "simple-service", + Arrays.asList( + projectRoot.resolve("pom.xml").toString(), + simpleServiceRoot.resolve("pom.xml").toString()), + Arrays.asList( + simpleServiceRoot.resolve("src/main/java").toString(), + simpleServiceRoot.resolve("src/main/resources").toString(), + simpleServiceRoot.resolve("src/main/jib").toString())); + } + + @Test + public void testFilesMojo_multiModuleComplexService() throws VerificationException, IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + + verifyFiles( + projectRoot, + "complex-service", + Arrays.asList( + projectRoot.resolve("pom.xml").toString(), + libRoot.resolve("pom.xml").toString(), + complexServiceRoot.resolve("pom.xml").toString()), + Arrays.asList( + libRoot.resolve("src/main/java").toString(), + libRoot.resolve("src/main/resources").toString(), + complexServiceRoot.resolve("src/main/java").toString(), + complexServiceRoot.resolve("src/main/resources1").toString(), + complexServiceRoot.resolve("src/main/resources2").toString(), + complexServiceRoot.resolve("src/main/other-jib").toString(), + // this test expects standard .m2 locations + Paths.get(System.getProperty("user.home")) + .resolve( + ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar") + .toString())); + } +} diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml index eaaa5cecc6..38100405f6 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml @@ -18,7 +18,7 @@ com.google.cloud.tools jib-maven-plugin - src/main/other-jib + ${project.basedir}/src/main/other-jib diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java index ba19d3b824..33594d3648 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java @@ -85,7 +85,7 @@ public SkaffoldFilesOutput() { */ @VisibleForTesting public SkaffoldFilesOutput(String json) throws IOException { - this.skaffoldFilesTemplate = new ObjectMapper().readValue(json, SkaffoldFilesTemplate.class); + skaffoldFilesTemplate = new ObjectMapper().readValue(json, SkaffoldFilesTemplate.class); } /** From fd2b030ca9729554e4a1b2ae3e52d7be7c66374a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 25 Feb 2019 14:47:38 -0500 Subject: [PATCH 0482/2020] Define mainClassName in Micronaut example (#1502) --- examples/micronaut/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 258b2b2cd9..da5537f621 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -39,7 +39,7 @@ dependencies { compileJava.options.compilerArgs += '-parameters' compileTestJava.options.compilerArgs += '-parameters' -run.main = 'example.micronaut.Application' +mainClassName = 'example.micronaut.Application' /** CHANGE THIS TO THE IMAGE YOU WANT TO PUSH TO */ // jib.to.image = 'gcr.io/PROJECT_ID/micronaut-jib' From f36a57c67b08e1b104131e93266a42c3c65877a0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 26 Feb 2019 11:40:21 -0500 Subject: [PATCH 0483/2020] Remove END JIB JSON string from files task/mojo (#1509) --- .../java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java | 1 - .../com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java | 5 ++--- .../google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java | 1 - .../cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java | 4 ---- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java index 44e9ae09cf..eb3fdd5ee8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java @@ -104,7 +104,6 @@ public void listFiles() throws IOException { // Print files System.out.println("\nBEGIN JIB JSON"); System.out.println(skaffoldFilesOutput.getJsonString()); - System.out.println("END JIB JSON"); } /** diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java index b5dc3c7d1c..815c29206f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java @@ -54,10 +54,9 @@ private static String verifyTaskSuccess(TestProject project, @Nullable String mo Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); String output = buildResult.getOutput().trim(); Assert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); - Assert.assertThat(output, CoreMatchers.endsWith("END JIB JSON")); - // Return task output with header/footer removed - return output.replace("BEGIN JIB JSON", "").replace("END JIB JSON", "").trim(); + // Return task output with header removed + return output.replace("BEGIN JIB JSON", "").trim(); } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index e5c8361e1c..180d04d637 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -160,7 +160,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Print JSON string System.out.println("\nBEGIN JIB JSON"); System.out.println(skaffoldFilesOutput.getJsonString()); - System.out.println("END JIB JSON"); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index b81d1aead8..b2de9aa764 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -65,10 +65,6 @@ private static void verifyFiles( int begin = log.indexOf("BEGIN JIB JSON"); Assert.assertTrue(begin > -1); - Assert.assertTrue( - "log.size() is " + log.size() + ", but begin + 2 is " + begin + 2, log.size() > begin + 2); - Assert.assertEquals("END JIB JSON", log.get(begin + 2)); - SkaffoldFilesOutput output = new SkaffoldFilesOutput(log.get(begin + 1)); Assert.assertEquals(buildFiles, output.getBuild()); Assert.assertEquals(inputFiles, output.getInputs()); From b0174eb35e008286f828000152f7a6abbae86ab5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 27 Feb 2019 14:11:29 -0500 Subject: [PATCH 0484/2020] clean up (#1515) --- .../tools/jib/http/WithServerConnectionTest.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index e29cceaa27..f870911d26 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -16,11 +16,10 @@ package com.google.cloud.tools.jib.http; -import java.io.ByteArrayOutputStream; +import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import javax.net.ssl.SSLException; import org.junit.Assert; @@ -38,10 +37,7 @@ public void testGet() Response response = connection.send("GET", new Request.Builder().build()); Assert.assertEquals(200, response.getStatusCode()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - response.getBody().writeTo(out); - Assert.assertEquals("Hello World!", new String(out.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals("Hello World!", Blobs.writeToString(response.getBody())); } } @@ -85,10 +81,7 @@ public void testInsecureConnection() Response response = connection.send("GET", new Request.Builder().build()); Assert.assertEquals(200, response.getStatusCode()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - response.getBody().writeTo(out); - Assert.assertEquals("Hello World!", new String(out.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals("Hello World!", Blobs.writeToString(response.getBody())); } } } From 083c2550c7eb4a782373d7726bba6a55f55f6204 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 28 Feb 2019 11:19:12 -0500 Subject: [PATCH 0485/2020] Support Java 11 WAR projects (#1516) --- jib-gradle-plugin/CHANGELOG.md | 4 +- jib-maven-plugin/CHANGELOG.md | 4 +- .../common/PluginConfigurationProcessor.java | 11 +-- .../PluginConfigurationProcessorTest.java | 74 ++++++++++++++++++- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 5b579531d9..e0650a3195 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) + ### Changed ### Fixed @@ -13,7 +15,7 @@ All notable changes to this project will be documented in this file. ### Added -- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) +- Java 9+ projects are now supported and run on the distroless Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 86df0f2f99..7e7aa5a27f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) + ### Changed ### Fixed @@ -13,7 +15,7 @@ All notable changes to this project will be documented in this file. ### Added -- Java 9+ projects are now supported and run on the distroless Java 11 image by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) +- Java 9+ projects are now supported and run on the distroless Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Java 8 image. ([#1279](https://github.com/GoogleContainerTools/jib/issues/1279)) ### Fixed diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index cf480df676..4c57b61aa8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -295,14 +295,15 @@ static String getBaseImage(RawConfiguration rawConfiguration, ProjectProperties } // Base image not configured; auto-pick Distroless. - if (projectProperties.isWarProject()) { - return "gcr.io/distroless/java/jetty"; - } if (javaVersion <= 8) { - return "gcr.io/distroless/java:8"; + return projectProperties.isWarProject() + ? "gcr.io/distroless/java/jetty:java8" + : "gcr.io/distroless/java:8"; } if (javaVersion <= 11) { - return "gcr.io/distroless/java:11"; + return projectProperties.isWarProject() + ? "gcr.io/distroless/java/jetty:java11" + : "gcr.io/distroless/java:11"; } throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index ce14482939..67b68bd349 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -139,7 +139,7 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); Assert.assertEquals( - ImageReference.parse("gcr.io/distroless/java/jetty").toString(), + ImageReference.parse("gcr.io/distroless/java/jetty:java8").toString(), processor.getBaseImageReference().toString()); Mockito.verifyNoMoreInteractions(logger); } @@ -469,6 +469,46 @@ public void testGetBaseImage_chooseJava11Distroless() PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); } + @Test + public void testGetBaseImage_chooseJava8JettyDistroless() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(6); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(7); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(8); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + + @Test + public void testGetBaseImage_chooseJava11JettyDistroless() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(9); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(10); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java11", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + } + @Test public void testGetBaseImage_projectHigherThanJava11() { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(12); @@ -522,6 +562,36 @@ public void testGetBaseImage_incompatibleJava11BaseImage() { } } + @Test + public void testGetBaseImage_incompatibleJava8JettyBaseImage() { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); + + Mockito.when(rawConfiguration.getFromImage()) + .thenReturn(Optional.of("gcr.io/distroless/java/jetty:java8")); + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(11, ex.getProjectMajorJavaVersion()); + } + } + + @Test + public void testGetBaseImage_incompatibleJava11JettyBaseImage() { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(15); + + Mockito.when(rawConfiguration.getFromImage()) + .thenReturn(Optional.of("gcr.io/distroless/java/jetty:java11")); + try { + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(15, ex.getProjectMajorJavaVersion()); + } + } + @Test public void testGetBaseImage_defaultNonWarPackaging() throws IncompatibleBaseImageJavaVersionException { @@ -538,7 +608,7 @@ public void testGetBaseImage_defaultWarProject() Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( - "gcr.io/distroless/java/jetty", + "gcr.io/distroless/java/jetty:java8", PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); } From 73a7a05a9762e99986163b5f6b1a4daa32fb5f19 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 28 Feb 2019 15:42:25 -0500 Subject: [PATCH 0486/2020] Handle more general ConnectException (#1518) --- .../tools/jib/registry/RegistryEndpointCaller.java | 4 ++-- .../jib/registry/RegistryEndpointCallerTest.java | 12 ++++++------ jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 743dcba954..a2fa5c0092 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -31,6 +31,7 @@ import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.net.ConnectException; import java.net.MalformedURLException; import java.net.URL; import java.security.GeneralSecurityException; @@ -38,7 +39,6 @@ import javax.annotation.Nullable; import javax.net.ssl.SSLException; import org.apache.http.NoHttpResponseException; -import org.apache.http.conn.HttpHostConnectException; /** * Makes requests to a registry endpoint. @@ -154,7 +154,7 @@ private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, Reg } catch (SSLException ex) { return handleUnverifiableServerException(url); - } catch (HttpHostConnectException ex) { + } catch (ConnectException ex) { if (allowInsecureRegistries && isHttpsProtocol(url) && url.getPort() == -1) { // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS // port 443) and we could not connect to 443. It's worth trying port 80. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 9b83a8ddeb..3182d7b198 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.http.MockConnection; import com.google.cloud.tools.jib.http.Response; import java.io.IOException; +import java.net.ConnectException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -38,7 +39,6 @@ import javax.annotation.Nullable; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.NoHttpResponseException; -import org.apache.http.conn.HttpHostConnectException; import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; @@ -206,7 +206,7 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() throws IOException, RegistryException { Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(HttpHostConnectException.class)) // server is not listening on 443 + .thenThrow(Mockito.mock(ConnectException.class)) // server is not listening on 443 .thenReturn(mockResponse); // respond when connected through 80 RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); @@ -230,12 +230,12 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() public void testCall_secureCallerOnNonListeningServerAndNoPortSpecified() throws IOException, RegistryException { Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(HttpHostConnectException.class)); // server is not listening on 443 + .thenThrow(Mockito.mock(ConnectException.class)); // server is not listening on 443 try { secureEndpointCaller.call(); Assert.fail("Should not fall back to HTTP if not allowInsecureRegistries"); - } catch (HttpHostConnectException ex) { + } catch (ConnectException ex) { Assert.assertNull(ex.getMessage()); } @@ -251,14 +251,14 @@ public void testCall_secureCallerOnNonListeningServerAndNoPortSpecified() public void testCall_insecureCallerOnNonListeningServerAndPortSpecified() throws IOException, RegistryException { Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(HttpHostConnectException.class)); // server is not listening on 5000 + .thenThrow(Mockito.mock(ConnectException.class)); // server is not listening on 5000 RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, 5000); try { insecureEndpointCaller.call(); Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); - } catch (HttpHostConnectException ex) { + } catch (ConnectException ex) { Assert.assertNull(ex.getMessage()); } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e0650a3195..606a17f2dc 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where setting `allowInsecureRegistries` may fail to try HTTP. + ## 1.0.1 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7e7aa5a27f..b9c04ac9be 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where setting `` may fail to try HTTP. + ## 1.0.1 ### Added From 86f7385cc4b7f3ff1269fb6f085ccfb0dbc08a0b Mon Sep 17 00:00:00 2001 From: Andrei Ivantsov Date: Thu, 28 Feb 2019 23:56:34 +0100 Subject: [PATCH 0487/2020] Describe Jib tasks to list them when running `gradle tasks` (#1520) --- .../cloud/tools/jib/gradle/JibPlugin.java | 24 ++++++++++++++++--- .../cloud/tools/jib/gradle/JibPluginTest.java | 13 ++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 5df9ed98ea..9f1ec9e1cc 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -88,17 +88,35 @@ public void apply(Project project) { Task buildImageTask = project .getTasks() - .create(BUILD_IMAGE_TASK_NAME, BuildImageTask.class) + .create( + BUILD_IMAGE_TASK_NAME, + BuildImageTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a registry."); + }) .setJibExtension(jibExtension); Task buildDockerTask = project .getTasks() - .create(BUILD_DOCKER_TASK_NAME, BuildDockerTask.class) + .create( + BUILD_DOCKER_TASK_NAME, + BuildDockerTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a Docker daemon."); + }) .setJibExtension(jibExtension); Task buildTarTask = project .getTasks() - .create(BUILD_TAR_TASK_NAME, BuildTarTask.class) + .create( + BUILD_TAR_TASK_NAME, + BuildTarTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a tarball."); + }) .setJibExtension(jibExtension); project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); project.getTasks().create(FILES_TASK_V2_NAME, FilesTaskV2.class).setJibExtension(jibExtension); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index bcdf0845ed..f69d283153 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -223,4 +223,17 @@ public void testNonWebAppProject() { Assert.assertNotNull(ex.getMessage()); } } + + @Test + public void testJibTaskGroupIsSet() { + Project rootProject = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + rootProject.getPluginManager().apply("java"); + rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + ((ProjectInternal) rootProject).evaluate(); + TaskContainer tasks = rootProject.getTasks(); + + KNOWN_JIB_TASKS.forEach( + taskName -> Assert.assertEquals(taskName, "Jib", tasks.getByPath(taskName).getGroup())); + } } From 4214794f266f934e7d0be5f9b88110445e982c30 Mon Sep 17 00:00:00 2001 From: "Andy Xu(devdiv)" Date: Fri, 1 Mar 2019 23:48:31 +0800 Subject: [PATCH 0488/2020] Handle "" as Username from credential helpers. (#1511) --- .../builder/steps/AuthenticatePushStep.java | 4 +- .../jib/builder/steps/PullBaseImageStep.java | 4 +- .../configuration/credentials/Credential.java | 12 ++ .../jib/registry/RegistryAuthenticator.java | 128 ++++++++++++------ .../credentials/CredentialTest.java | 14 ++ .../registry/RegistryAuthenticatorTest.java | 23 ++++ 6 files changed, 139 insertions(+), 46 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 6f9ad4cc5d..cfb2442fb6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -88,7 +88,7 @@ public Authorization call() buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); Authorization registryAuthorization = - registryCredential == null + registryCredential == null || registryCredential.isOAuth2RefreshToken() ? null : Authorizations.withBasicCredentials( registryCredential.getUsername(), registryCredential.getPassword()); @@ -103,7 +103,7 @@ public Authorization call() if (registryAuthenticator == null) { return registryAuthorization; } - return registryAuthenticator.setAuthorization(registryAuthorization).authenticatePush(); + return registryAuthenticator.setCredential(registryCredential).authenticatePush(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 42219745ef..c09bd15583 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -148,7 +148,7 @@ public BaseImageWithAuthorization call() Credential registryCredential = NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); Authorization registryAuthorization = - registryCredential == null + registryCredential == null || registryCredential.isOAuth2RefreshToken() ? null : Authorizations.withBasicCredentials( registryCredential.getUsername(), registryCredential.getPassword()); @@ -176,7 +176,7 @@ public BaseImageWithAuthorization call() throw registryUnauthorizedException; } registryAuthorization = - registryAuthenticator.setAuthorization(registryAuthorization).authenticatePull(); + registryAuthenticator.setCredential(registryCredential).authenticatePull(); return new BaseImageWithAuthorization( pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index 3da9613f0b..6b918c4123 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -21,6 +21,9 @@ // TODO: Move to lower-level package - probably at same level as Authorization. /** Holds credentials (username and password). */ public class Credential { + // if the username is set to the secret would be an refresh token, see details at + // https://github.com/docker/cli/blob/master/docs/reference/commandline/login.md#credential-helper-protocol + private static final String OAUTH2_TOKEN_USER_NAME = ""; /** * Gets a {@link Credential} configured with a username and password. @@ -59,6 +62,15 @@ public String getPassword() { return password; } + /** + * Check whether this credential is an OAuth 2.0 refresh token. + * + * @return true if this credential is an OAuth 2.0 refresh token. + */ + public boolean isOAuth2RefreshToken() { + return OAUTH2_TOKEN_USER_NAME.equals(this.username); + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index e5ea7f259c..e86a792342 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -18,16 +18,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.MediaType; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -97,6 +100,17 @@ public RegistryAuthenticator initialize() } } + /** + * Sets a {@code Credential} to help the authentication. + * + * @param credential the credential used to authenticate. + * @return this + */ + public RegistryAuthenticator setCredential(@Nullable Credential credential) { + this.credential = credential; + return this; + } + /** * Gets a new initializer for {@link RegistryAuthenticator}. * @@ -198,35 +212,20 @@ private String getToken() { } } - private final String authenticationUrlBase; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - @Nullable private Authorization authorization; + private final String realm; + private final String service; + @Nullable private Credential credential; RegistryAuthenticator( String realm, String service, RegistryEndpointRequestProperties registryEndpointRequestProperties) { - authenticationUrlBase = - realm - + "?service=" - + service - + "&scope=repository:" - + registryEndpointRequestProperties.getImageName() - + ":"; + this.realm = realm; + this.service = service; this.registryEndpointRequestProperties = registryEndpointRequestProperties; } - /** - * Sets an {@code Authorization} header to authenticate with. - * - * @param authorization the authorization header - * @return this - */ - public RegistryAuthenticator setAuthorization(@Nullable Authorization authorization) { - this.authorization = authorization; - return this; - } - /** * Authenticates permissions to pull. * @@ -249,7 +248,26 @@ public Authorization authenticatePush() throws RegistryAuthenticationFailedExcep @VisibleForTesting URL getAuthenticationUrl(String scope) throws MalformedURLException { - return new URL(authenticationUrlBase + scope); + return new URL(realm + "?" + getUrlEncodedServiceScope(scope)); + } + + @VisibleForTesting + BlobHttpContent getOAuth2AuthRequestBody(String scope, String oauth2RefreshToken) { + String formDataBody = + "grant_type=refresh_token&" + + getUrlEncodedServiceScope(scope) + + "&refresh_token=" + + oauth2RefreshToken; + return new BlobHttpContent(Blobs.from(formDataBody), MediaType.FORM_DATA.toString(), null); + } + + private String getUrlEncodedServiceScope(String scope) { + return "service=" + + service + + "&scope=repository:" + + registryEndpointRequestProperties.getImageName() + + ":" + + scope; } /** @@ -263,28 +281,19 @@ URL getAuthenticationUrl(String scope) throws MalformedURLException { */ private Authorization authenticate(String scope) throws RegistryAuthenticationFailedException { try { - URL authenticationUrl = getAuthenticationUrl(scope); - - try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { - Request.Builder requestBuilder = - Request.builder().setHttpTimeout(JibSystemProperties.getHttpTimeout()); - if (authorization != null) { - requestBuilder.setAuthorization(authorization); - } - Response response = connection.get(requestBuilder.build()); - String responseString = Blobs.writeToString(response.getBody()); - - AuthenticationResponseTemplate responseJson = - JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); - if (responseJson.getToken() == null) { - throw new RegistryAuthenticationFailedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - "Did not get token in authentication response from " + authenticationUrl); - } - return Authorizations.withBearerToken(responseJson.getToken()); - } + AuthenticationResponseTemplate responseJson = + credential != null && credential.isOAuth2RefreshToken() + // in this case, credential.getPassword() is an OAuth2 refresh token. + ? fetchTokenWithOAuth2(scope, credential.getPassword()) + : fetchTokenWithBasicAuth(scope); + if (responseJson.getToken() == null) { + throw new RegistryAuthenticationFailedException( + registryEndpointRequestProperties.getServerUrl(), + registryEndpointRequestProperties.getImageName(), + "Did not get token in authentication response from " + getAuthenticationUrl(scope)); + } + return Authorizations.withBearerToken(responseJson.getToken()); } catch (IOException ex) { throw new RegistryAuthenticationFailedException( registryEndpointRequestProperties.getServerUrl(), @@ -292,4 +301,39 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa ex); } } + + private AuthenticationResponseTemplate fetchTokenWithOAuth2( + String scope, String oauth2RefreshToken) throws IOException { + URL authenticationUrl = new URL(realm); + + try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { + Request.Builder requestBuilder = + Request.builder() + .setHttpTimeout(JibSystemProperties.getHttpTimeout()) + .setBody(getOAuth2AuthRequestBody(scope, oauth2RefreshToken)); + + Response response = connection.post(requestBuilder.build()); + String responseString = Blobs.writeToString(response.getBody()); + + return JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); + } + } + + private AuthenticationResponseTemplate fetchTokenWithBasicAuth(String scope) throws IOException { + URL authenticationUrl = getAuthenticationUrl(scope); + + try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { + Request.Builder requestBuilder = + Request.builder().setHttpTimeout(JibSystemProperties.getHttpTimeout()); + if (credential != null) { + requestBuilder.setAuthorization( + Authorizations.withBasicCredentials( + credential.getUsername(), credential.getPassword())); + } + Response response = connection.get(requestBuilder.build()); + String responseString = Blobs.writeToString(response.getBody()); + + return JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index 3ddab3ccd2..9e7d725fd0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -41,4 +41,18 @@ public void testCredentialsHash() { new HashSet<>(Arrays.asList(credentialA1, credentialA2, credentialB1, credentialB2)); Assert.assertEquals(new HashSet<>(Arrays.asList(credentialA2, credentialB1)), credentialSet); } + + @Test + public void testCredentialsOAuth2RefreshToken() { + + Credential oauth2Credential = Credential.basic("", "eyJhbGciOi...3gw"); + + Assert.assertTrue( + "Credential should be an auth2 token when username is ", + oauth2Credential.isOAuth2RefreshToken()); + Assert.assertEquals( + "OAuth2 token credential should take password as refresh token", + "eyJhbGciOi...3gw", + oauth2Credential.getPassword()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index ec12297c82..8a9c2f791f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -16,6 +16,9 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.http.BlobHttpContent; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import org.junit.Assert; @@ -46,6 +49,26 @@ public void testFromAuthenticationMethod_bearer() registryAuthenticator.getAuthenticationUrl("scope")); } + @Test + public void testFromAuthenticationMethod_oauth2() + throws RegistryAuthenticationFailedException, IOException { + RegistryAuthenticator registryAuthenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties); + BlobHttpContent blobHttpContent = + registryAuthenticator.getOAuth2AuthRequestBody("scope", "sometoken"); + // the content type should be "application/x-www-form-urlencoded" + Assert.assertEquals("application/x-www-form-urlencoded", blobHttpContent.getType()); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + blobHttpContent.writeTo(out); + Assert.assertEquals( + "grant_type=refresh_token&service=someservice&" + + "scope=repository:someimage:scope&refresh_token=sometoken", + out.toString()); + } + } + @Test public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFailedException { Assert.assertNull( From 4c4392008e77e06e333ae1ddfe24e063e8707557 Mon Sep 17 00:00:00 2001 From: Francis Lalonde Date: Fri, 1 Mar 2019 14:06:07 -0500 Subject: [PATCH 0489/2020] Stop displaying progress when Maven --batch-mode is specified (#1514) --- .../tools/jib/maven/BuildDockerMojo.java | 1 + .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../cloud/tools/jib/maven/BuildTarMojo.java | 1 + .../jib/maven/MavenProjectProperties.java | 20 ++++++++++++++----- .../jib/maven/MavenProjectPropertiesTest.java | 8 +++++++- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 3a7230f876..55120bec69 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -99,6 +99,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), + getSession(), getLog(), MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 350ff6c64f..78e7ea0dee 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -96,6 +96,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), + getSession(), getLog(), MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 9840d711df..3007ea1a2f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -72,6 +72,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), + getSession(), getLog(), MojoCommon.getExtraDirectoryPath(this), MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a9ae4b9479..479eb301f5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -61,6 +61,7 @@ public class MavenProjectProperties implements ProjectProperties { /** * @param project the {@link MavenProject} for the plugin. + * @param session the {@link MavenSession} for the plugin. * @param log the Maven {@link Log} to log messages during Jib execution * @param extraDirectory path to the directory for the extra files layer * @param permissions map from path on container to file permissions for extra-layer files @@ -70,6 +71,7 @@ public class MavenProjectProperties implements ProjectProperties { */ static MavenProjectProperties getForProject( MavenProject project, + MavenSession session, Log log, Path extraDirectory, Map permissions, @@ -78,6 +80,7 @@ static MavenProjectProperties getForProject( try { return new MavenProjectProperties( project, + session, log, MavenLayerConfigurations.getForProject(project, extraDirectory, permissions, appRoot)); @@ -110,9 +113,9 @@ public static String getProperty( } private static EventHandlers makeEventHandlers( - Log log, SingleThreadedExecutor singleThreadedExecutor) { + MavenSession session, Log log, SingleThreadedExecutor singleThreadedExecutor) { ConsoleLoggerBuilder logEventHandlerBuilder = - (isProgressFooterEnabled() + (isProgressFooterEnabled(session) ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) .lifecycle(log::info); @@ -146,7 +149,11 @@ private static EventHandlers makeEventHandlers( update.getProgress(), update.getUnfinishedAllocations())))); } - private static boolean isProgressFooterEnabled() { + private static boolean isProgressFooterEnabled(MavenSession session) { + if (!session.getRequest().isInteractiveMode()) { + return false; + } + if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; } @@ -196,11 +203,14 @@ static int getVersionFromString(String versionString) { @VisibleForTesting MavenProjectProperties( - MavenProject project, Log log, JavaLayerConfigurations javaLayerConfigurations) { + MavenProject project, + MavenSession session, + Log log, + JavaLayerConfigurations javaLayerConfigurations) { this.project = project; this.javaLayerConfigurations = javaLayerConfigurations; - eventHandlers = makeEventHandlers(log, singleThreadedExecutor); + eventHandlers = makeEventHandlers(session, log, singleThreadedExecutor); } @Override diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 5c3d5ad0d5..28850659f9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import java.util.Properties; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; @@ -35,6 +37,8 @@ public class MavenProjectPropertiesTest { @Mock private MavenProject mockMavenProject; + @Mock private MavenSession mockMavenSession; + @Mock private MavenExecutionRequest mockMavenRequest; @Mock private Properties mockMavenProperties; @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @@ -54,8 +58,10 @@ public class MavenProjectPropertiesTest { @Before public void setup() { + Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = - new MavenProjectProperties(mockMavenProject, mockLog, mockJavaLayerConfigurations); + new MavenProjectProperties( + mockMavenProject, mockMavenSession, mockLog, mockJavaLayerConfigurations); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); From 3f0d52b06bbfb8ffc7e2012a73260afecef99dc3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 1 Mar 2019 14:34:01 -0500 Subject: [PATCH 0490/2020] Add CHANGELOG and test (#1525) --- jib-maven-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/maven/MavenProjectProperties.java | 3 ++- .../cloud/tools/jib/maven/MavenProjectPropertiesTest.java | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b9c04ac9be..17a6275a11 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) +- Batch mode now disables build progress bar. ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 479eb301f5..d975f4c0b5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -149,7 +149,8 @@ private static EventHandlers makeEventHandlers( update.getProgress(), update.getUnfinishedAllocations())))); } - private static boolean isProgressFooterEnabled(MavenSession session) { + @VisibleForTesting + static boolean isProgressFooterEnabled(MavenSession session) { if (!session.getRequest().isInteractiveMode()) { return false; } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 28850659f9..4e410b8402 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -206,4 +206,10 @@ public void testValidateBaseImageVersion_compilerPluginRelease() { Mockito.when(compilerRelease.getValue()).thenReturn("13"); Assert.assertEquals(13, mavenProjectProperties.getMajorJavaVersion()); } + + @Test + public void isProgressFooterEnabled() { + Mockito.when(mockMavenRequest.isInteractiveMode()).thenReturn(false); + Assert.assertFalse(MavenProjectProperties.isProgressFooterEnabled(mockMavenSession)); + } } From b18bc6a352536db07b4ec8f72791f81a74987ed2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 4 Mar 2019 13:13:55 -0500 Subject: [PATCH 0491/2020] Do not crash on inaccurate progress reporting (#1521) --- .../jib/builder/ProgressEventDispatcher.java | 29 +++++--- .../tools/jib/event/progress/Allocation.java | 2 +- .../builder/ProgressEventDispatcherTest.java | 70 ++++++++++++------- jib-gradle-plugin/CHANGELOG.md | 3 +- jib-maven-plugin/CHANGELOG.md | 3 +- 5 files changed, 69 insertions(+), 38 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index fbfeefbaef..414d928ccb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.common.base.Preconditions; -import com.google.common.base.Verify; import java.io.Closeable; /** @@ -139,18 +138,28 @@ public void close() { * @param progressUnits units of progress */ public void dispatchProgress(long progressUnits) { - decrementRemainingAllocationUnits(progressUnits); - eventDispatcher.dispatch(new ProgressEvent(allocation, progressUnits, buildStepType)); + long unitsDecremented = decrementRemainingAllocationUnits(progressUnits); + eventDispatcher.dispatch(new ProgressEvent(allocation, unitsDecremented, buildStepType)); } - private void decrementRemainingAllocationUnits(long units) { + /** + * Decrements remaining allocation units by {@code units} but no more than the remaining + * allocation units (which may be 0). Returns the actual units decremented, which never exceeds + * {@code units}. + * + * @param units units to decrement + * @return units actually decremented + */ + private long decrementRemainingAllocationUnits(long units) { Preconditions.checkState(!closed); - remainingAllocationUnits -= units; - Verify.verify( - remainingAllocationUnits >= 0, - "Remaining allocation units less than 0 for '%s': %s", - allocation.getDescription(), - remainingAllocationUnits); + if (remainingAllocationUnits > units) { + remainingAllocationUnits -= units; + return units; + } + + long actualDecrement = remainingAllocationUnits; + remainingAllocationUnits = 0; + return actualDecrement; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java index e9ae352b4b..ef874dddab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -65,7 +65,7 @@ public static Allocation newRoot(String description, long allocationUnits) { private Allocation(String description, long allocationUnits, @Nullable Allocation parent) { this.description = description; - this.allocationUnits = allocationUnits; + this.allocationUnits = allocationUnits < 0 ? 0 : allocationUnits; this.parent = parent; this.fractionOfRoot = (parent == null ? 1.0 : parent.fractionOfRoot) / allocationUnits; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java index 16cc527773..f114da01c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.common.base.VerifyException; import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -59,37 +58,58 @@ public void testDispatch() { } @Test - public void testDispatch_tooMuchProgress() { + public void testDispatch_safeWithtooMuchProgress() { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 10)) { - progressEventDispatcher.dispatchProgress(10); - try { - progressEventDispatcher.dispatchProgress(10); - Assert.fail(); - - } catch (VerifyException ex) { - Assert.assertEquals( - "Remaining allocation units less than 0 for 'allocation description': -10", - ex.getMessage()); - } + progressEventDispatcher.dispatchProgress(6); + progressEventDispatcher.dispatchProgress(8); + progressEventDispatcher.dispatchProgress(1); } + + ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventDispatcher, Mockito.times(4)).dispatch(eventsCaptor.capture()); + List progressEvents = eventsCaptor.getAllValues(); + + Assert.assertSame(progressEvents.get(0).getAllocation(), progressEvents.get(1).getAllocation()); + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(2).getAllocation()); + Assert.assertSame(progressEvents.get(2).getAllocation(), progressEvents.get(3).getAllocation()); + + Assert.assertEquals(10, progressEvents.get(0).getAllocation().getAllocationUnits()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); + Assert.assertEquals(6, progressEvents.get(1).getUnits()); + Assert.assertEquals(4, progressEvents.get(2).getUnits()); + Assert.assertEquals(0, progressEvents.get(3).getUnits()); } @Test - public void testDispatch_tooManyChildren() { + public void testDispatch_safeWithTooManyChildren() { try (ProgressEventDispatcher progressEventDispatcher = - ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 1)) { - progressEventDispatcher.newChildProducer(); - - try { - progressEventDispatcher.newChildProducer(); - Assert.fail(); - - } catch (VerifyException ex) { - Assert.assertEquals( - "Remaining allocation units less than 0 for 'allocation description': -1", - ex.getMessage()); - } + ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 1); + ProgressEventDispatcher ignored1 = + progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 5); + ProgressEventDispatcher ignored2 = + progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 4)) { + // empty } + + ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); + Mockito.verify(mockEventDispatcher, Mockito.times(5)).dispatch(eventsCaptor.capture()); + List progressEvents = eventsCaptor.getAllValues(); + + Assert.assertEquals(1, progressEvents.get(0).getAllocation().getAllocationUnits()); + Assert.assertEquals(5, progressEvents.get(1).getAllocation().getAllocationUnits()); + Assert.assertEquals(4, progressEvents.get(2).getAllocation().getAllocationUnits()); + + // child1 (of allocation 5) opening and closing + Assert.assertSame(progressEvents.get(1).getAllocation(), progressEvents.get(4).getAllocation()); + // child1 (of allocation 4) opening and closing + Assert.assertSame(progressEvents.get(2).getAllocation(), progressEvents.get(3).getAllocation()); + + Assert.assertEquals(0, progressEvents.get(0).getUnits()); // 0-progress sent when root creation + Assert.assertEquals(0, progressEvents.get(1).getUnits()); // when child1 creation + Assert.assertEquals(0, progressEvents.get(2).getUnits()); // when child2 creation + Assert.assertEquals(4, progressEvents.get(3).getUnits()); // when child2 closes + Assert.assertEquals(5, progressEvents.get(4).getUnits()); // when child1 closes } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 606a17f2dc..833c0a11e8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,7 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed an issue where setting `allowInsecureRegistries` may fail to try HTTP. +- Fixed an issue where setting `allowInsecureRegistries` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) +- Crash on talking to servers that do not set the `Content-Length` HTTP header or send an incorrect value. ([#1512](https://github.com/GoogleContainerTools/jib/issues/1512)) ## 1.0.1 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 17a6275a11..68ff67c3b4 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,7 +12,8 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed an issue where setting `` may fail to try HTTP. +- Fixed an issue where setting `` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) +- Crash on talking to servers that do not set the `Content-Length` HTTP header or send an incorrect value. ([#1512](https://github.com/GoogleContainerTools/jib/issues/1512)) ## 1.0.1 From 0dc97d4f1363209df2fd2c3c80bf74e7a64dac78 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 5 Mar 2019 12:20:34 -0500 Subject: [PATCH 0492/2020] CHANGELOG for ACR OAuth2 auth support / refactor and clean up code (#1524) --- .../builder/steps/AuthenticatePushStep.java | 15 ++- .../jib/registry/RegistryAuthenticator.java | 106 +++++++++--------- .../credentials/CredentialTest.java | 2 - .../registry/RegistryAuthenticatorTest.java | 76 +++++++++---- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 3 +- 6 files changed, 116 insertions(+), 87 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index cfb2442fb6..79e9dc18ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -87,11 +87,6 @@ public Authorization call() new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); - Authorization registryAuthorization = - registryCredential == null || registryCredential.isOAuth2RefreshToken() - ? null - : Authorizations.withBasicCredentials( - registryCredential.getUsername(), registryCredential.getPassword()); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( @@ -100,10 +95,14 @@ public Authorization call() buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); - if (registryAuthenticator == null) { - return registryAuthorization; + if (registryAuthenticator != null) { + return registryAuthenticator.setCredential(registryCredential).authenticatePush(); } - return registryAuthenticator.setCredential(registryCredential).authenticatePush(); + + return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) + ? null + : Authorizations.withBasicCredentials( + registryCredential.getUsername(), registryCredential.getPassword()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index e86a792342..5070f6d3d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; import com.google.common.net.MediaType; import java.io.IOException; import java.net.MalformedURLException; @@ -247,21 +248,7 @@ public Authorization authenticatePush() throws RegistryAuthenticationFailedExcep } @VisibleForTesting - URL getAuthenticationUrl(String scope) throws MalformedURLException { - return new URL(realm + "?" + getUrlEncodedServiceScope(scope)); - } - - @VisibleForTesting - BlobHttpContent getOAuth2AuthRequestBody(String scope, String oauth2RefreshToken) { - String formDataBody = - "grant_type=refresh_token&" - + getUrlEncodedServiceScope(scope) - + "&refresh_token=" - + oauth2RefreshToken; - return new BlobHttpContent(Blobs.from(formDataBody), MediaType.FORM_DATA.toString(), null); - } - - private String getUrlEncodedServiceScope(String scope) { + String getServiceScopeRequestParameters(String scope) { return "service=" + service + "&scope=repository:" @@ -270,6 +257,29 @@ private String getUrlEncodedServiceScope(String scope) { + scope; } + @VisibleForTesting + URL getAuthenticationUrl(String scope) throws MalformedURLException { + return isOAuth2Auth() + ? new URL(realm) // Required parameters will be sent via POST . + : new URL(realm + "?" + getServiceScopeRequestParameters(scope)); + } + + @VisibleForTesting + String getAuthRequestParameters(String scope) { + String serviceScope = getServiceScopeRequestParameters(scope); + return isOAuth2Auth() + ? serviceScope + + "&grant_type=refresh_token&refresh_token=" + // If OAuth2, credential.getPassword() is a refresh token. + + Verify.verifyNotNull(credential).getPassword() + : serviceScope; + } + + @VisibleForTesting + boolean isOAuth2Auth() { + return credential != null && credential.isOAuth2RefreshToken(); + } + /** * Sends the authentication request and retrieves the Bearer authorization token. * @@ -280,20 +290,39 @@ private String getUrlEncodedServiceScope(String scope) { * href="https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate">https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ private Authorization authenticate(String scope) throws RegistryAuthenticationFailedException { - try { + try (Connection connection = + Connection.getConnectionFactory().apply(getAuthenticationUrl(scope))) { + Request.Builder requestBuilder = + Request.builder().setHttpTimeout(JibSystemProperties.getHttpTimeout()); + + if (isOAuth2Auth()) { + String parameters = getAuthRequestParameters(scope); + requestBuilder.setBody( + new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString(), null)); + } else if (credential != null) { + requestBuilder.setAuthorization( + Authorizations.withBasicCredentials( + credential.getUsername(), credential.getPassword())); + } + + Request request = requestBuilder.build(); + Response response = isOAuth2Auth() ? connection.post(request) : connection.get(request); + String responseString = Blobs.writeToString(response.getBody()); + AuthenticationResponseTemplate responseJson = - credential != null && credential.isOAuth2RefreshToken() - // in this case, credential.getPassword() is an OAuth2 refresh token. - ? fetchTokenWithOAuth2(scope, credential.getPassword()) - : fetchTokenWithBasicAuth(scope); + JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), - "Did not get token in authentication response from " + getAuthenticationUrl(scope)); + "Did not get token in authentication response from " + + getAuthenticationUrl(scope) + + "; parameters: " + + getAuthRequestParameters(scope)); } return Authorizations.withBearerToken(responseJson.getToken()); + } catch (IOException ex) { throw new RegistryAuthenticationFailedException( registryEndpointRequestProperties.getServerUrl(), @@ -301,39 +330,4 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa ex); } } - - private AuthenticationResponseTemplate fetchTokenWithOAuth2( - String scope, String oauth2RefreshToken) throws IOException { - URL authenticationUrl = new URL(realm); - - try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { - Request.Builder requestBuilder = - Request.builder() - .setHttpTimeout(JibSystemProperties.getHttpTimeout()) - .setBody(getOAuth2AuthRequestBody(scope, oauth2RefreshToken)); - - Response response = connection.post(requestBuilder.build()); - String responseString = Blobs.writeToString(response.getBody()); - - return JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); - } - } - - private AuthenticationResponseTemplate fetchTokenWithBasicAuth(String scope) throws IOException { - URL authenticationUrl = getAuthenticationUrl(scope); - - try (Connection connection = Connection.getConnectionFactory().apply(authenticationUrl)) { - Request.Builder requestBuilder = - Request.builder().setHttpTimeout(JibSystemProperties.getHttpTimeout()); - if (credential != null) { - requestBuilder.setAuthorization( - Authorizations.withBasicCredentials( - credential.getUsername(), credential.getPassword())); - } - Response response = connection.get(requestBuilder.build()); - String responseString = Blobs.writeToString(response.getBody()); - - return JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); - } - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index 9e7d725fd0..e923374643 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -44,9 +44,7 @@ public void testCredentialsHash() { @Test public void testCredentialsOAuth2RefreshToken() { - Credential oauth2Credential = Credential.basic("", "eyJhbGciOi...3gw"); - Assert.assertTrue( "Credential should be an auth2 token when username is ", oauth2Credential.isOAuth2RefreshToken()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 8a9c2f791f..43fe8e79f8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -16,12 +16,11 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.http.BlobHttpContent; -import java.io.ByteArrayOutputStream; -import java.io.IOException; +import com.google.cloud.tools.jib.configuration.credentials.Credential; import java.net.MalformedURLException; import java.net.URL; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; /** Tests for {@link RegistryAuthenticator}. */ @@ -29,6 +28,16 @@ public class RegistryAuthenticatorTest { private final RegistryEndpointRequestProperties registryEndpointRequestProperties = new RegistryEndpointRequestProperties("someserver", "someimage"); + private RegistryAuthenticator registryAuthenticator; + + @Before + public void setUp() throws RegistryAuthenticationFailedException { + registryAuthenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties); + } + @Test public void testFromAuthenticationMethod_bearer() throws MalformedURLException, RegistryAuthenticationFailedException { @@ -50,23 +59,50 @@ public void testFromAuthenticationMethod_bearer() } @Test - public void testFromAuthenticationMethod_oauth2() - throws RegistryAuthenticationFailedException, IOException { - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties); - BlobHttpContent blobHttpContent = - registryAuthenticator.getOAuth2AuthRequestBody("scope", "sometoken"); - // the content type should be "application/x-www-form-urlencoded" - Assert.assertEquals("application/x-www-form-urlencoded", blobHttpContent.getType()); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - blobHttpContent.writeTo(out); - Assert.assertEquals( - "grant_type=refresh_token&service=someservice&" - + "scope=repository:someimage:scope&refresh_token=sometoken", - out.toString()); - } + public void testAuthRequestParameters_basicAuth() { + Assert.assertEquals( + "service=someservice&scope=repository:someimage:scope", + registryAuthenticator.getAuthRequestParameters("scope")); + } + + @Test + public void testAuthRequestParameters_oauth2() { + registryAuthenticator.setCredential(Credential.basic("", "oauth2_access_token")); + Assert.assertEquals( + "service=someservice&scope=repository:someimage:scope" + + "&grant_type=refresh_token&refresh_token=oauth2_access_token", + registryAuthenticator.getAuthRequestParameters("scope")); + } + + @Test + public void isOAuth2Auth_nullCredential() { + Assert.assertFalse(registryAuthenticator.isOAuth2Auth()); + } + + @Test + public void isOAuth2Auth_basicAuth() { + registryAuthenticator.setCredential(Credential.basic("name", "password")); + Assert.assertFalse(registryAuthenticator.isOAuth2Auth()); + } + + @Test + public void isOAuth2Auth_oauth2() { + registryAuthenticator.setCredential(Credential.basic("", "oauth2_token")); + Assert.assertTrue(registryAuthenticator.isOAuth2Auth()); + } + + @Test + public void getAuthenticationUrl_basicAuth() throws MalformedURLException { + Assert.assertEquals( + new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), + registryAuthenticator.getAuthenticationUrl("scope")); + } + + @Test + public void istAuthenticationUrl_oauth2() throws MalformedURLException { + registryAuthenticator.setCredential(Credential.basic("", "oauth2_token")); + Assert.assertEquals( + new URL("https://somerealm"), registryAuthenticator.getAuthenticationUrl("scope")); } @Test diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 833c0a11e8..6fd8b07149 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) +- Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 68ff67c3b4..db4a04c26e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file. ### Added - Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) -- Batch mode now disables build progress bar. +- Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) +- Batch mode now disables build progress bar. ([#1513](https://github.com/GoogleContainerTools/jib/issues/1513)) ### Changed From db403e9311760d507e01817873198ac7a58d2f9c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 5 Mar 2019 12:50:19 -0500 Subject: [PATCH 0493/2020] Gradle release v1.0.2 (#1531) * [Gradle Release Plugin] - pre tag commit: 'v1.0.2-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.0.3-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 63d32434ac..b9111c5b31 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.0.2-SNAPSHOT +version = 1.0.3-SNAPSHOT From aec061eed1f9bf89a02c92eb40fe071043de6e62 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 5 Mar 2019 13:00:57 -0500 Subject: [PATCH 0494/2020] Maven release v1.0.2 (#1532) * preparing release 1.0.2 * 1.0.3-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index ab5dfbbf11..19e589a2fe 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.2-SNAPSHOT + 1.0.3-SNAPSHOT maven-plugin Jib From a1a3eb692f3ff7db3342f4b3ba203a067c5eed40 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 5 Mar 2019 15:36:32 -0500 Subject: [PATCH 0495/2020] Update version strings for 1.0.2 (#1533) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot-draft/build.gradle | 2 +- examples/spring-boot-draft/pom.xml | 2 +- examples/spring-boot-kubernetes/build.gradle | 2 +- examples/spring-boot-kubernetes/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 19 files changed, 36 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d16af1e302..2b0694e7aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.2-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.3-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.0.1' + // id 'com.google.cloud.tools.jib' version '1.0.2' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index bdbaf761c0..f49cfc093d 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.0.1 + 1.0.2 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 82fff3bdf9..39ffac5c54 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index df471f88f2..19d141bc41 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.1 + 1.0.2 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 03e4a30210..03342b393d 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index afea86b688..af6cffec44 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.1 + 1.0.2 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 1d9ffe0c59..577c069bb6 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.0.1" + id("com.google.cloud.tools.jib") version "1.0.2" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index da5537f621..32a732ec83 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index a172dde78f..6074dca10f 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.1' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.2' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 1aa4d05333..364d85d8bd 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.1 + 1.0.2 diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle index 3a3f1434ab..fa00e826fb 100644 --- a/examples/spring-boot-draft/build.gradle +++ b/examples/spring-boot-draft/build.gradle @@ -10,7 +10,7 @@ buildscript { } } plugins { - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } apply plugin: 'java' diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml index 723aa862dd..2a88f93962 100644 --- a/examples/spring-boot-draft/pom.xml +++ b/examples/spring-boot-draft/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 1.0.1 + 1.0.2 diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot-kubernetes/build.gradle index 9754881320..894c036993 100644 --- a/examples/spring-boot-kubernetes/build.gradle +++ b/examples/spring-boot-kubernetes/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } repositories { diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot-kubernetes/pom.xml index cf531c0e5b..3054eb98a8 100644 --- a/examples/spring-boot-kubernetes/pom.xml +++ b/examples/spring-boot-kubernetes/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.1 + 1.0.2 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 273ae68d05..cfeec32082 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 6fd8b07149..26263e6678 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,11 +5,17 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.0.2 + +### Added + - Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) - Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) -### Changed - ### Fixed - Fixed an issue where setting `allowInsecureRegistries` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 0ae8317ad6..adfc7fa5a4 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -43,7 +43,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.0.1' + id 'com.google.cloud.tools.jib' version '1.0.2' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index db4a04c26e..fa9fabb9e2 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,12 +5,18 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.0.2 + +### Added + - Java 9+ WAR projects are now supported and run on the distroless Jetty Java 11 image (https://github.com/GoogleContainerTools/distroless) by default. Java 8 projects remain on the distroless Jetty Java 8 image. ([#1510](https://github.com/GoogleContainerTools/jib/issues/1510)) - Now supports authentication against Azure Container Registry using `docker-credential-acr-*` credential helpers. ([#1490](https://github.com/GoogleContainerTools/jib/issues/1490)) - Batch mode now disables build progress bar. ([#1513](https://github.com/GoogleContainerTools/jib/issues/1513)) -### Changed - ### Fixed - Fixed an issue where setting `` may fail to try HTTP. ([#1517](https://github.com/GoogleContainerTools/jib/issues/1517)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 37e2360b07..f990ebf22a 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -38,7 +38,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.2:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -46,7 +46,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.2:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -64,7 +64,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.0.1 + 1.0.2 myimage From d25f1e05e62a58f21ab705e5dfb884ffde69f14a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 7 Mar 2019 14:17:24 -0500 Subject: [PATCH 0496/2020] Update doc for ACR credential helper configuration (#1536) --- jib-gradle-plugin/README.md | 14 ++++++++++---- jib-maven-plugin/README.md | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index adfc7fa5a4..a89d2fbcb3 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -29,7 +29,6 @@ For information about the project, see the [Jib project README](../README.md). * [Using Docker Credential Helpers](#using-docker-credential-helpers) * [Using Specific Credentials](#using-specific-credentials) * [WAR Projects](#war-projects) -* [How Jib Works](#how-jib-works) * [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) * [Community](#community) @@ -99,7 +98,15 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b jib.to.image = 'my-docker-id/my-app' ``` -#### *TODO: Add more examples for common registries.* +#### Using [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/)... + +*Make sure you have a [`ACR Docker Credential Helper`](https://github.com/Azure/acr-docker-credential-helper) installed and set up. For example, on Windows, the credential helper would be `docker-credential-acr-windows`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my_acr_name.azurecr.io/my-app`, the configuration would be: + +```groovy +jib.to.image = 'my_acr_name.azurecr.io/my-app' +``` ### Build Your Image @@ -328,8 +335,7 @@ Some common credential helpers include: * Google Container Registry: [`docker-credential-gcr`](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper) * AWS Elastic Container Registry: [`docker-credential-ecr-login`](https://github.com/awslabs/amazon-ecr-credential-helper) * Docker Hub Registry: [`docker-credential-*`](https://github.com/docker/docker-credential-helpers) - +* Azure Container Registry: [`docker-credential-acr-*`](https://github.com/Azure/acr-docker-credential-helper) Configure credential helpers to use by specifying them as a `credHelper` for their respective image in the `jib` extension. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index f990ebf22a..b39a5c0f20 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -29,7 +29,6 @@ For information about the project, see the [Jib project README](../README.md). * [Using Docker Credential Helpers](#using-docker-credential-helpers) * [Using Specific Credentials](#using-specific-credentials) * [WAR Projects](#war-projects) -* [How Jib Works](#how-jib-works) * [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) * [Community](#community) @@ -124,7 +123,19 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b ``` -#### *TODO: Add more examples for common registries.* +#### Using [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/)... + +*Make sure you have a [`ACR Docker Credential Helper`](https://github.com/Azure/acr-docker-credential-helper) installed and set up. For example, on Windows, the credential helper would be `docker-credential-acr-windows`. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* + +For example, to build the image `my_acr_name.azurecr.io/my-app`, the configuration would be: + +```xml + + + my_acr_name.azurecr.io/my-app + + +``` ### Build your image @@ -392,8 +403,7 @@ Some common credential helpers include: * Google Container Registry: [`docker-credential-gcr`](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper) * AWS Elastic Container Registry: [`docker-credential-ecr-login`](https://github.com/awslabs/amazon-ecr-credential-helper) * Docker Hub Registry: [`docker-credential-*`](https://github.com/docker/docker-credential-helpers) - +* Azure Container Registry: [`docker-credential-acr-*`](https://github.com/Azure/acr-docker-credential-helper) Configure credential helpers to use by specifying them as a `credHelper` for their respective image. From f2e4db0f25ef921a3d5275aa981314dd2b4a9546 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 8 Mar 2019 15:01:50 -0500 Subject: [PATCH 0497/2020] Allow setting approot any time during JavaContainerBuilder construction (#1541) --- .../tools/jib/api/JavaContainerBuilder.java | 191 ++++++++++-------- .../jib/api/JavaContainerBuilderTest.java | 67 +++++- 2 files changed, 166 insertions(+), 92 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 5ffa999289..9b581b37e8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; @@ -40,6 +41,23 @@ /** Creates a {@link JibContainerBuilder} for containerizing Java applications. */ public class JavaContainerBuilder { + /** Holds a directory and a filter. */ + private static class PathPredicatePair { + + private final Path path; + private final Predicate predicate; + + private PathPredicatePair(Path path, Predicate predicate) { + this.path = path; + this.predicate = predicate; + } + } + + private static final RelativeUnixPath CLASSES_CLASSPATH = RelativeUnixPath.get("classes"); + private static final RelativeUnixPath RESOURCES_CLASSPATH = RelativeUnixPath.get("resources"); + private static final RelativeUnixPath DEPENDENCIES_CLASSPATH = RelativeUnixPath.get("libs/*"); + private static final RelativeUnixPath OTHERS_CLASSPATH = RelativeUnixPath.get("classpath"); + /** * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For * more information on {@code gcr.io/distroless/java}, see jvmFlags = new ArrayList<>(); - private final LinkedHashSet classpath = new LinkedHashSet<>(4); - - /** Absolute path of directory containing application resources on container. */ - private AbsoluteUnixPath resourcesPath; - - /** Absolute path of directory containing classes on container. */ - private AbsoluteUnixPath classesPath; - - /** Absolute path of directory containing dependencies on container. */ - private AbsoluteUnixPath dependenciesPath; + private final LinkedHashSet relativeClasspaths = new LinkedHashSet<>(4); - /** The entrypoint classpath element corresponding to dependencies. */ - private AbsoluteUnixPath dependenciesClasspath; - - /** Absolute path of directory containing additional classpath files on container. */ - private AbsoluteUnixPath othersPath; + // Keeps track of files to add to the image, by system path + private final List addedResources = new ArrayList<>(); + private final List addedClasses = new ArrayList<>(); + private final List addedDependencies = new ArrayList<>(); + private final List addedOthers = new ArrayList<>(); + private AbsoluteUnixPath appRoot; @Nullable private String mainClass; private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { this.jibContainerBuilder = jibContainerBuilder; - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - resourcesPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - classesPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - dependenciesPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - dependenciesClasspath = dependenciesPath.resolve("*"); - othersPath = appRoot.resolve("classpath"); + this.appRoot = AbsoluteUnixPath.get("/app"); } /** @@ -158,16 +159,7 @@ public JavaContainerBuilder setAppRoot(String appRoot) { * @return this */ public JavaContainerBuilder setAppRoot(AbsoluteUnixPath appRoot) { - if (!classpath.isEmpty()) { - throw new IllegalStateException("You cannot change the app root after files are added"); - } - resourcesPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - classesPath = appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - dependenciesPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - dependenciesClasspath = dependenciesPath.resolve("*"); - othersPath = appRoot.resolve("classpath"); + this.appRoot = appRoot; return this; } @@ -186,32 +178,8 @@ public JavaContainerBuilder addDependencies(List dependencyFiles) throws I throw new NoSuchFileException(file.toString()); } } - - // Detect duplicate filenames and rename with filesize to avoid collisions - List duplicates = - dependencyFiles - .stream() - .map(Path::getFileName) - .map(Path::toString) - .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) - .entrySet() - .stream() - .filter(entry -> entry.getValue() > 1) - .map(Entry::getKey) - .collect(Collectors.toList()); - for (Path file : dependencyFiles) { - layerConfigurationsBuilder.addFile( - file.getFileName().toString().contains("SNAPSHOT") - ? LayerType.SNAPSHOT_DEPENDENCIES - : LayerType.DEPENDENCIES, - file, - dependenciesPath.resolve( - duplicates.contains(file.getFileName().toString()) - ? file.getFileName().toString().replaceFirst("\\.jar$", "-" + Files.size(file)) - + ".jar" - : file.getFileName().toString())); - } - classpath.add(dependenciesClasspath.toString()); + addedDependencies.addAll(dependencyFiles); + relativeClasspaths.add(DEPENDENCIES_CLASSPATH); return this; } @@ -248,7 +216,8 @@ public JavaContainerBuilder addResources(Path resourceFilesDirectory) throws IOE */ public JavaContainerBuilder addResources(Path resourceFilesDirectory, Predicate pathFilter) throws IOException { - return addDirectory(resourceFilesDirectory, resourcesPath, LayerType.RESOURCES, pathFilter); + relativeClasspaths.add(RESOURCES_CLASSPATH); + return addDirectory(addedResources, resourceFilesDirectory, pathFilter); } /** @@ -272,7 +241,8 @@ public JavaContainerBuilder addClasses(Path classFilesDirectory) throws IOExcept */ public JavaContainerBuilder addClasses(Path classFilesDirectory, Predicate pathFilter) throws IOException { - return addDirectory(classFilesDirectory, classesPath, LayerType.CLASSES, pathFilter); + relativeClasspaths.add(CLASSES_CLASSPATH); + return addDirectory(addedClasses, classFilesDirectory, pathFilter); } /** @@ -293,17 +263,8 @@ public JavaContainerBuilder addToClasspath(List otherFiles) throws IOExcep throw new NoSuchFileException(file.toString()); } } - - for (Path file : otherFiles) { - if (Files.isDirectory(file)) { - layerConfigurationsBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, file, path -> true, othersPath); - } else { - layerConfigurationsBuilder.addFile( - LayerType.EXTRA_FILES, file, othersPath.resolve(file.getFileName())); - } - } - classpath.add(othersPath.toString()); + relativeClasspaths.add(OTHERS_CLASSPATH); + addedOthers.addAll(otherFiles); return this; } @@ -374,36 +335,104 @@ public JavaContainerBuilder setMainClass(String mainClass) { * * @return a new {@link JibContainerBuilder} using the parameters specified on the {@link * JavaContainerBuilder} + * @throws IOException if building the {@link JibContainerBuilder} fails. */ - public JibContainerBuilder toContainerBuilder() { + public JibContainerBuilder toContainerBuilder() throws IOException { if (mainClass == null) { throw new IllegalStateException( "mainClass is null on JavaContainerBuilder; specify the main class using " + "JavaContainerBuilder#setMainClass(String), or consider using a " + "jib.frontend.MainClassFinder to infer the main class"); } - if (classpath.isEmpty()) { + if (relativeClasspaths.isEmpty()) { throw new IllegalStateException( "Failed to construct entrypoint because no files were added to the JavaContainerBuilder"); } + JavaLayerConfigurations.Builder layerConfigurationsBuilder = JavaLayerConfigurations.builder(); + + // Add classes to layer configuration + for (PathPredicatePair directory : addedClasses) { + layerConfigurationsBuilder.addDirectoryContents( + LayerType.CLASSES, + directory.path, + directory.predicate, + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE)); + } + + // Add resources to layer configuration + for (PathPredicatePair directory : addedResources) { + layerConfigurationsBuilder.addDirectoryContents( + LayerType.RESOURCES, + directory.path, + directory.predicate, + appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE)); + } + + // Detect duplicate filenames and rename with filesize to avoid collisions + List duplicates = + addedDependencies + .stream() + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Entry::getKey) + .collect(Collectors.toList()); + for (Path file : addedDependencies) { + // Add dependencies to layer configuration + layerConfigurationsBuilder.addFile( + file.getFileName().toString().contains("SNAPSHOT") + ? LayerType.SNAPSHOT_DEPENDENCIES + : LayerType.DEPENDENCIES, + file, + appRoot + .resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .resolve( + duplicates.contains(file.getFileName().toString()) + ? file.getFileName() + .toString() + .replaceFirst("\\.jar$", "-" + Files.size(file)) + + ".jar" + : file.getFileName().toString())); + } + + // Add others to layer configuration + for (Path path : addedOthers) { + if (Files.isDirectory(path)) { + layerConfigurationsBuilder.addDirectoryContents( + LayerType.EXTRA_FILES, path, path1 -> true, appRoot.resolve(OTHERS_CLASSPATH)); + } else { + layerConfigurationsBuilder.addFile( + LayerType.EXTRA_FILES, + path, + appRoot.resolve(OTHERS_CLASSPATH).resolve(path.getFileName())); + } + } + + // Construct entrypoint + List classpathElements = new ArrayList<>(); + for (RelativeUnixPath path : relativeClasspaths) { + classpathElements.add(appRoot.resolve(path).toString()); + } jibContainerBuilder.setEntrypoint( - JavaEntrypointConstructor.makeEntrypoint(new ArrayList<>(classpath), jvmFlags, mainClass)); + JavaEntrypointConstructor.makeEntrypoint(classpathElements, jvmFlags, mainClass)); jibContainerBuilder.setLayers(layerConfigurationsBuilder.build().getLayerConfigurations()); return jibContainerBuilder; } private JavaContainerBuilder addDirectory( - Path directory, AbsoluteUnixPath destination, LayerType layerType, Predicate pathFilter) - throws IOException { + List addedPaths, Path directory, Predicate filter) + throws NoSuchFileException, NotDirectoryException { if (!Files.exists(directory)) { throw new NoSuchFileException(directory.toString()); } if (!Files.isDirectory(directory)) { throw new NotDirectoryException(directory.toString()); } - layerConfigurationsBuilder.addDirectoryContents(layerType, directory, pathFilter, destination); - classpath.add(destination.toString()); + addedPaths.add(new PathPredicatePair(directory, filter)); return this; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 9247095104..3e6cb4df8b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -194,19 +194,64 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() } @Test - public void testToJibContainerBuilder_setAppRootLate() throws URISyntaxException, IOException { - JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.fromDistroless().addClasses(getResource("core/application/classes")); - try { - javaContainerBuilder.setAppRoot("/oh no"); - Assert.fail(); - } catch (IllegalStateException ex) { - Assert.assertEquals("You cannot change the app root after files are added", ex.getMessage()); - } + public void testToJibContainerBuilder_setAppRootLate() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + BuildConfiguration buildConfiguration = + JavaContainerBuilder.fromDistroless() + .addClasses(getResource("core/application/classes")) + .addResources(getResource("core/application/resources")) + .addDependencies(getResource("core/application/dependencies/libraryA.jar")) + .addToClasspath(getResource("core/fileA")) + .setAppRoot("/different") + .setMainClass("HelloWorld") + .toContainerBuilder() + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("hello")), + MoreExecutors.newDirectExecutorService()); + + // Check entrypoint + ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + Assert.assertNotNull(containerConfiguration); + Assert.assertEquals( + ImmutableList.of( + "java", + "-cp", + "/different/classes:/different/resources:/different/libs/*:/different/classpath", + "HelloWorld"), + containerConfiguration.getEntrypoint()); + + // Check classes + List expectedClasses = + ImmutableList.of( + AbsoluteUnixPath.get("/different/classes/HelloWorld.class"), + AbsoluteUnixPath.get("/different/classes/some.class")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + + // Check resources + List expectedResources = + ImmutableList.of( + AbsoluteUnixPath.get("/different/resources/resourceA"), + AbsoluteUnixPath.get("/different/resources/resourceB"), + AbsoluteUnixPath.get("/different/resources/world")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); + + // Check dependencies + List expectedDependencies = + ImmutableList.of(AbsoluteUnixPath.get("/different/libs/libraryA.jar")); + Assert.assertEquals( + expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + + Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + + // Check additional classpath files + List expectedOthers = + ImmutableList.of(AbsoluteUnixPath.get("/different/classpath/fileA")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); } @Test - public void testToJibContainerBuilder_mainClassNull() { + public void testToJibContainerBuilder_mainClassNull() throws IOException { try { JavaContainerBuilder.fromDistroless().toContainerBuilder(); Assert.fail(); @@ -221,7 +266,7 @@ public void testToJibContainerBuilder_mainClassNull() { } @Test - public void testToJibContainerBuilder_classpathEmpty() { + public void testToJibContainerBuilder_classpathEmpty() throws IOException { try { JavaContainerBuilder.fromDistroless().setMainClass("Hello").toContainerBuilder(); Assert.fail(); From 02f07311974d23375028f9dfa460ccb34c93ffcd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 8 Mar 2019 15:59:40 -0500 Subject: [PATCH 0498/2020] Fix NPE in ImageReference#isValidTag() and add more descriptive error messages (#1535) --- .../google/cloud/tools/jib/api/Containerizer.java | 2 +- .../cloud/tools/jib/api/ContainerizerTest.java | 15 +++++++++++++++ .../tools/jib/maven/JibPluginConfiguration.java | 12 ++++++++---- .../jib/maven/JibPluginConfigurationTest.java | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 2f74a4a778..4113ccd8c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -103,7 +103,7 @@ private Containerizer(TargetImage targetImage) { * @return this */ public Containerizer withAdditionalTag(String tag) { - Preconditions.checkArgument(ImageReference.isValidTag(tag)); + Preconditions.checkArgument(ImageReference.isValidTag(tag), "invalid tag '%s'", tag); additionalTags.add(tag); return this; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index ecc36e71d2..7734393fbc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -86,4 +86,19 @@ private void verifyTo(Containerizer containerizer, TargetImage expectedTargetIma Assert.assertTrue(containerizer.getAllowInsecureRegistries()); Assert.assertEquals("tool", containerizer.getToolName()); } + + @Test + public void testWithAdditionalTag() { + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named(ImageReference.of(null, "repository", null)); + Containerizer containerizer = Containerizer.to(dockerDaemonImage); + + containerizer.withAdditionalTag("tag"); + try { + containerizer.withAdditionalTag("+invalid+"); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("invalid tag '+invalid+'", ex.getMessage()); + } + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 716682494d..d4cc638bac 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -299,10 +300,13 @@ String getTargetImage() { */ Set getTargetImageAdditionalTags() { String property = getProperty(PropertyNames.TO_TAGS); - if (property != null) { - return new HashSet<>(ConfigurationPropertyValidator.parseListProperty(property)); - } - return new HashSet<>(to.tags); + List tags = + property != null ? ConfigurationPropertyValidator.parseListProperty(property) : to.tags; + String source = property != null ? PropertyNames.TO_TAGS : ""; + tags.forEach( + tag -> + Preconditions.checkArgument(!Strings.isNullOrEmpty(tag), "%s has empty tag", source)); + return new HashSet<>(tags); } /** diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 0d2ad69c6e..db5ac94194 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -185,4 +185,18 @@ public void testPomProperties() { Assert.assertEquals("/another/file", permissions.get(1).getFile().get()); Assert.assertEquals("456", permissions.get(1).getMode().get()); } + + @Test + public void testEmptyOrNullTags() { + // https://github.com/GoogleContainerTools/jib/issues/1534 + // Maven turns empty tags into null entries, and its possible + // to have empty tags in jib.to.tags + sessionProperties.put("jib.to.tags", "a,,b"); + try { + testPluginConfiguration.getTargetImageAdditionalTags(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("jib.to.tags has empty tag", ex.getMessage()); + } + } } From 546ec30a0ff1f5f7b8445d482c5f320391f66d14 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 12 Mar 2019 14:35:03 -0400 Subject: [PATCH 0499/2020] Allow setting scratch base image in jib-core (#1550) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JibIntegrationTest.java | 22 +++++++++++++++++++ .../com/google/cloud/tools/jib/api/Jib.java | 9 ++++++++ .../jib/builder/steps/PullBaseImageStep.java | 16 +++++++++++--- .../cloud/tools/jib/image/ImageReference.java | 20 +++++++++++++++++ .../tools/jib/image/ImageReferenceTest.java | 7 ++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 0111f641bd..95f3753e7c 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) +- `Jib#fromScratch()` to start building from an empty base image ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) ### Changed diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 892ca81957..96615a9364 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -87,6 +87,28 @@ public void testBasic_helloWorld() targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); } + @Test + public void testScratch() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-scratch"); + Jib.fromScratch() + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.basic("username", "password")))) + .setAllowInsecureRegistries(true)); + + // Check that resulting image has no layers + localRegistry.pull(targetImageReference.toString()); + String inspectOutput = new Command("docker", "inspect", targetImageReference.toString()).run(); + Assert.assertFalse( + "docker inspect output contained layers: " + inspectOutput, + inspectOutput.contains("\"Layers\": [")); + } + /** Ensure that a provided executor is not disposed. */ @Test public void testProvidedExecutorNotDisposed() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index 46e6e19160..ac52bee3f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -60,5 +60,14 @@ public static JibContainerBuilder from(RegistryImage registryImage) { return new JibContainerBuilder(registryImage); } + /** + * Starts building the container from an empty base image. + * + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder fromScratch() { + return from(ImageReference.scratch()); + } + private Jib() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index c09bd15583..e94f6169b1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; @@ -42,7 +43,6 @@ import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.RegistryException; @@ -108,8 +108,18 @@ public ListenableFuture getFuture() { @Override public BaseImageWithAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, - LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, - RegistryAuthenticationFailedException { + LayerCountMismatchException, ExecutionException, + BadContainerConfigurationFormatException { + // Skip this step if this is a scratch image + ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); + if (baseImageConfiguration.getImage().isScratch()) { + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.progress("Getting scratch base image...")); + return new BaseImageWithAuthorization( + Image.builder(buildConfiguration.getTargetFormat()).build(), null); + } + buildConfiguration .getEventDispatcher() .dispatch( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index da5824b13a..5adf54eee6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -168,6 +168,17 @@ public static ImageReference of( return new ImageReference(registry, repository, tag); } + /** + * Constructs an {@link ImageReference} with an empty registry and tag component, and repository + * set to "scratch". + * + * @return an {@link ImageReference} with an empty registry and tag component, and repository set + * to "scratch" + */ + public static ImageReference scratch() { + return new ImageReference("", "scratch", ""); + } + /** * Returns {@code true} if {@code registry} is a valid registry string. For example, a valid * registry could be {@code gcr.io} or {@code localhost:5000}. @@ -271,6 +282,15 @@ public boolean isTagDigest() { return tag.matches(DescriptorDigest.DIGEST_REGEX); } + /** + * Returns {@code true} if the {@link ImageReference} is a scratch image; {@code false} if not. + * + * @return {@code true} if the {@link ImageReference} is a scratch image; {@code false} if not + */ + public boolean isScratch() { + return "".equals(registry) && "scratch".equals(repository) && "".equals(tag); + } + /** * Gets an {@link ImageReference} with the same registry and repository, but a different tag. * diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java index 826a98497b..11a7a5e36d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java @@ -180,6 +180,13 @@ public void testIsTagDigest() throws InvalidImageReferenceException { .isTagDigest()); } + @Test + public void testIsScratch() { + Assert.assertTrue(ImageReference.scratch().isScratch()); + Assert.assertFalse(ImageReference.of("", "scratch", "").isScratch()); + Assert.assertFalse(ImageReference.of(null, "scratch", null).isScratch()); + } + private void verifyParse(String registry, String repository, String tagSeparator, String tag) throws InvalidImageReferenceException { // Gets the expected parsed components. From 0f944750f9bb611abcf2a91dda89935ad4d30721 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 12 Mar 2019 14:40:11 -0400 Subject: [PATCH 0500/2020] Send client_id for OAuth2 registry auth (#1546) * Send client_id for OAuth2 registry auth * Use MD5 hash of "jib" * Put "jib." in client_id --- .../google/cloud/tools/jib/registry/RegistryAuthenticator.java | 2 ++ .../cloud/tools/jib/registry/RegistryAuthenticatorTest.java | 1 + 2 files changed, 3 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 5070f6d3d0..1dc690afe9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -269,6 +269,8 @@ String getAuthRequestParameters(String scope) { String serviceScope = getServiceScopeRequestParameters(scope); return isOAuth2Auth() ? serviceScope + // https://github.com/GoogleContainerTools/jib/pull/1545 + + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + "&grant_type=refresh_token&refresh_token=" // If OAuth2, credential.getPassword() is a refresh token. + Verify.verifyNotNull(credential).getPassword() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 43fe8e79f8..09f95d8e54 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -70,6 +70,7 @@ public void testAuthRequestParameters_oauth2() { registryAuthenticator.setCredential(Credential.basic("", "oauth2_access_token")); Assert.assertEquals( "service=someservice&scope=repository:someimage:scope" + + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + "&grant_type=refresh_token&refresh_token=oauth2_access_token", registryAuthenticator.getAuthRequestParameters("scope")); } From ac15e0f95946aafe646427ccad85c1914adb17bd Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 13 Mar 2019 10:26:38 -0400 Subject: [PATCH 0501/2020] Add links to examples on landing page (#1548) * Add links to examples on landing page * Add Kafka Stream example --- README.md | 17 ++++++++++------- examples/README.md | 6 +++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 33ada16f38..a47d438f7d 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,20 @@ For more information, check out the [official blog post](https://cloudplatform.g ## Quickstart -### Maven +* **Maven** - See the jib-maven-plugin [Quickstart](jib-maven-plugin#quickstart). -See documentation for using [jib-maven-plugin](jib-maven-plugin#quickstart). +* **Gradle** - See the jib-gradle-plugin [Quickstart](jib-gradle-plugin#quickstart). -### Gradle +* **Jib Core** - See the Jib Core [Quickstart](jib-core#adding-jib-core-to-your-build). -See documentation for using [jib-gradle-plugin](jib-gradle-plugin#quickstart). +## Examples -### Jib Core - -See documentation for using [Jib Core](jib-core#adding-jib-core-to-your-build) +The [examples](examples) directory includes the following examples (and more). + * [helloworld](examples/helloworld) + * [Spring Boot](examples/spring-boot-kubernetes) + * [Micronaut](examples/micronaut) + * [Multi-module project](examples/multi-module) + * [Spark Java using Java Agent](examples/java-agent) ## How Jib Works diff --git a/examples/README.md b/examples/README.md index 0bd5693822..f0b40698ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,7 +26,7 @@ You can find usage by Gradle or Maven with SpringBoot in [spring-boot](spring-bo See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). -## Ktor example +### Ktor example See [ktor](ktor) for containerizing a [Ktor](https://ktor.io) Kotlin Application using the Kotlin Gradle DSL. @@ -41,3 +41,7 @@ See [micronaut](micronaut) for containerizing a [Micronaut framework](https://mi ### Java agents example See [java-agent](java-agent) for launching with the Stackdriver Debugger Java agent. + +### Kafka Streams example + +See [cricket007/kafka-streams-jib-example](https://github.com/cricket007/kafka-streams-jib-example) for containerizing a [Kafka Streams](http://kafka.apache.org/documentation/streams) application. From 5999550f641dba04b814e0ab7ee088b71bb5ee8d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 14 Mar 2019 10:02:17 -0400 Subject: [PATCH 0502/2020] Allow setting destinations on JavaContainerBuilder (#1552) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JavaContainerBuilder.java | 109 ++++++++++++++---- .../jib/api/JavaContainerBuilderTest.java | 32 ++--- 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 95f3753e7c..add02bb61a 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) - `Jib#fromScratch()` to start building from an empty base image ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) +- Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files ### Changed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 9b581b37e8..3f53679dd2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; @@ -53,11 +54,6 @@ private PathPredicatePair(Path path, Predicate predicate) { } } - private static final RelativeUnixPath CLASSES_CLASSPATH = RelativeUnixPath.get("classes"); - private static final RelativeUnixPath RESOURCES_CLASSPATH = RelativeUnixPath.get("resources"); - private static final RelativeUnixPath DEPENDENCIES_CLASSPATH = RelativeUnixPath.get("libs/*"); - private static final RelativeUnixPath OTHERS_CLASSPATH = RelativeUnixPath.get("classpath"); - /** * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For * more information on {@code gcr.io/distroless/java}, see jvmFlags = new ArrayList<>(); - private final LinkedHashSet relativeClasspaths = new LinkedHashSet<>(4); + private final LinkedHashSet classpathOrder = new LinkedHashSet<>(4); // Keeps track of files to add to the image, by system path private final List addedResources = new ArrayList<>(); @@ -134,12 +130,18 @@ public static JavaContainerBuilder from(RegistryImage registryImage) { private final List addedDependencies = new ArrayList<>(); private final List addedOthers = new ArrayList<>(); - private AbsoluteUnixPath appRoot; + private AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); + private RelativeUnixPath classesDestination = + JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE; + private RelativeUnixPath resourcesDestination = + JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE; + private RelativeUnixPath dependenciesDestination = + JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE; + private RelativeUnixPath othersDestination = RelativeUnixPath.get("classpath"); @Nullable private String mainClass; private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { this.jibContainerBuilder = jibContainerBuilder; - this.appRoot = AbsoluteUnixPath.get("/app"); } /** @@ -163,6 +165,54 @@ public JavaContainerBuilder setAppRoot(AbsoluteUnixPath appRoot) { return this; } + /** + * Sets the destination directory of the classes added to the container (relative to the app + * root). + * + * @param classesDestination the path to the classes directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setClassesDestination(RelativeUnixPath classesDestination) { + this.classesDestination = classesDestination; + return this; + } + + /** + * Sets the destination directory of the resources added to the container (relative to the app + * root). + * + * @param resourcesDestination the path to the resources directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setResourcesDestination(RelativeUnixPath resourcesDestination) { + this.resourcesDestination = resourcesDestination; + return this; + } + + /** + * Sets the destination directory of the dependencies added to the container (relative to the app + * root). + * + * @param dependenciesDestination the path to the dependencies directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setDependenciesDestination(RelativeUnixPath dependenciesDestination) { + this.dependenciesDestination = dependenciesDestination; + return this; + } + + /** + * Sets the destination directory of additional classpath files added to the container (relative + * to the app root). + * + * @param othersDestination the additional classpath directory, relative to the app root + * @return this + */ + public JavaContainerBuilder setOthersDestination(RelativeUnixPath othersDestination) { + this.othersDestination = othersDestination; + return this; + } + /** * Adds dependency JARs to the image. Duplicate JAR filenames are renamed with the filesize in * order to avoid collisions. @@ -179,7 +229,7 @@ public JavaContainerBuilder addDependencies(List dependencyFiles) throws I } } addedDependencies.addAll(dependencyFiles); - relativeClasspaths.add(DEPENDENCIES_CLASSPATH); + classpathOrder.add(LayerType.DEPENDENCIES); return this; } @@ -216,7 +266,7 @@ public JavaContainerBuilder addResources(Path resourceFilesDirectory) throws IOE */ public JavaContainerBuilder addResources(Path resourceFilesDirectory, Predicate pathFilter) throws IOException { - relativeClasspaths.add(RESOURCES_CLASSPATH); + classpathOrder.add(LayerType.RESOURCES); return addDirectory(addedResources, resourceFilesDirectory, pathFilter); } @@ -241,7 +291,7 @@ public JavaContainerBuilder addClasses(Path classFilesDirectory) throws IOExcept */ public JavaContainerBuilder addClasses(Path classFilesDirectory, Predicate pathFilter) throws IOException { - relativeClasspaths.add(CLASSES_CLASSPATH); + classpathOrder.add(LayerType.CLASSES); return addDirectory(addedClasses, classFilesDirectory, pathFilter); } @@ -263,7 +313,7 @@ public JavaContainerBuilder addToClasspath(List otherFiles) throws IOExcep throw new NoSuchFileException(file.toString()); } } - relativeClasspaths.add(OTHERS_CLASSPATH); + classpathOrder.add(LayerType.EXTRA_FILES); addedOthers.addAll(otherFiles); return this; } @@ -344,7 +394,7 @@ public JibContainerBuilder toContainerBuilder() throws IOException { + "JavaContainerBuilder#setMainClass(String), or consider using a " + "jib.frontend.MainClassFinder to infer the main class"); } - if (relativeClasspaths.isEmpty()) { + if (classpathOrder.isEmpty()) { throw new IllegalStateException( "Failed to construct entrypoint because no files were added to the JavaContainerBuilder"); } @@ -357,7 +407,7 @@ public JibContainerBuilder toContainerBuilder() throws IOException { LayerType.CLASSES, directory.path, directory.predicate, - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE)); + appRoot.resolve(classesDestination)); } // Add resources to layer configuration @@ -366,7 +416,7 @@ public JibContainerBuilder toContainerBuilder() throws IOException { LayerType.RESOURCES, directory.path, directory.predicate, - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE)); + appRoot.resolve(resourcesDestination)); } // Detect duplicate filenames and rename with filesize to avoid collisions @@ -389,7 +439,7 @@ public JibContainerBuilder toContainerBuilder() throws IOException { : LayerType.DEPENDENCIES, file, appRoot - .resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE) + .resolve(dependenciesDestination) .resolve( duplicates.contains(file.getFileName().toString()) ? file.getFileName() @@ -403,19 +453,36 @@ public JibContainerBuilder toContainerBuilder() throws IOException { for (Path path : addedOthers) { if (Files.isDirectory(path)) { layerConfigurationsBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, path, path1 -> true, appRoot.resolve(OTHERS_CLASSPATH)); + LayerType.EXTRA_FILES, path, path1 -> true, appRoot.resolve(othersDestination)); } else { layerConfigurationsBuilder.addFile( LayerType.EXTRA_FILES, path, - appRoot.resolve(OTHERS_CLASSPATH).resolve(path.getFileName())); + appRoot.resolve(othersDestination).resolve(path.getFileName())); } } - // Construct entrypoint + // Construct entrypoint. Ensure classpath elements are in the same order as the files were added + // to the JavaContainerBuilder. List classpathElements = new ArrayList<>(); - for (RelativeUnixPath path : relativeClasspaths) { - classpathElements.add(appRoot.resolve(path).toString()); + for (LayerType path : classpathOrder) { + switch (path) { + case CLASSES: + classpathElements.add(appRoot.resolve(classesDestination).toString()); + break; + case RESOURCES: + classpathElements.add(appRoot.resolve(resourcesDestination).toString()); + break; + case DEPENDENCIES: + classpathElements.add(appRoot.resolve(dependenciesDestination).resolve("*").toString()); + break; + case EXTRA_FILES: + classpathElements.add(appRoot.resolve(othersDestination).toString()); + break; + default: + throw new RuntimeException( + "Bug in jib-core; please report the bug at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); + } } jibContainerBuilder.setEntrypoint( JavaEntrypointConstructor.makeEntrypoint(classpathElements, jvmFlags, mainClass)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 3e6cb4df8b..c5397ebb41 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; @@ -76,6 +77,10 @@ public void testToJibContainerBuilder_all() getResource("core/application/dependencies/libraryB.jar"), getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) .addToClasspath(getResource("core/fileA"), getResource("core/fileB")) + .setClassesDestination(RelativeUnixPath.get("different-classes")) + .setResourcesDestination(RelativeUnixPath.get("different-resources")) + .setDependenciesDestination(RelativeUnixPath.get("different-libs")) + .setOthersDestination(RelativeUnixPath.get("different-classpath")) .addJvmFlags("-xflag1", "-xflag2") .setMainClass("HelloWorld") .toContainerBuilder() @@ -92,23 +97,24 @@ public void testToJibContainerBuilder_all() "-xflag1", "-xflag2", "-cp", - "/hello/resources:/hello/classes:/hello/libs/*:/hello/classpath", + "/hello/different-resources:/hello/different-classes:/hello/different-libs/*:/hello/different-classpath", "HelloWorld"), containerConfiguration.getEntrypoint()); // Check dependencies List expectedDependencies = ImmutableList.of( - AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-770.jar"), - AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-200.jar"), - AbsoluteUnixPath.get("/hello/libs/libraryA.jar"), - AbsoluteUnixPath.get("/hello/libs/libraryB.jar")); + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-770.jar"), + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar"), + AbsoluteUnixPath.get("/hello/different-libs/libraryA.jar"), + AbsoluteUnixPath.get("/hello/different-libs/libraryB.jar")); Assert.assertEquals( expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); // Check snapshots List expectedSnapshotDependencies = - ImmutableList.of(AbsoluteUnixPath.get("/hello/libs/dependency-1.0.0-SNAPSHOT.jar")); + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-SNAPSHOT.jar")); Assert.assertEquals( expectedSnapshotDependencies, getExtractionPaths(buildConfiguration, "snapshot dependencies")); @@ -116,23 +122,23 @@ public void testToJibContainerBuilder_all() // Check resources List expectedResources = ImmutableList.of( - AbsoluteUnixPath.get("/hello/resources/resourceA"), - AbsoluteUnixPath.get("/hello/resources/resourceB"), - AbsoluteUnixPath.get("/hello/resources/world")); + AbsoluteUnixPath.get("/hello/different-resources/resourceA"), + AbsoluteUnixPath.get("/hello/different-resources/resourceB"), + AbsoluteUnixPath.get("/hello/different-resources/world")); Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); // Check classes List expectedClasses = ImmutableList.of( - AbsoluteUnixPath.get("/hello/classes/HelloWorld.class"), - AbsoluteUnixPath.get("/hello/classes/some.class")); + AbsoluteUnixPath.get("/hello/different-classes/HelloWorld.class"), + AbsoluteUnixPath.get("/hello/different-classes/some.class")); Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); // Check additional classpath files List expectedOthers = ImmutableList.of( - AbsoluteUnixPath.get("/hello/classpath/fileA"), - AbsoluteUnixPath.get("/hello/classpath/fileB")); + AbsoluteUnixPath.get("/hello/different-classpath/fileA"), + AbsoluteUnixPath.get("/hello/different-classpath/fileB")); Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); } From e0fc2553c348cf40ed322e013f78946ec93ae566 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 14 Mar 2019 13:05:26 -0400 Subject: [PATCH 0503/2020] Allow skipping main class in JavaContainerBuilder (#1553) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JavaContainerBuilder.java | 61 ++++++++++--------- .../jib/api/JavaContainerBuilderTest.java | 21 +++++-- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index add02bb61a..8981ea6cbd 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files ### Changed +- Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 3f53679dd2..fc01bbb07f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -367,8 +367,9 @@ public JavaContainerBuilder addJvmFlags(String... jvmFlags) { } /** - * Sets the main class used to start the application on the image. To find the main class from - * {@code .class} files, use {@link MainClassFinder}. + * Sets the container entrypoint with the specified main class. The entrypoint will be left + * unconfigured if this method is not called. To find the main class from {@code .class} files, + * use {@link MainClassFinder}. * * @param mainClass the main class used to start the application * @return this @@ -388,11 +389,12 @@ public JavaContainerBuilder setMainClass(String mainClass) { * @throws IOException if building the {@link JibContainerBuilder} fails. */ public JibContainerBuilder toContainerBuilder() throws IOException { - if (mainClass == null) { + if (mainClass == null && !jvmFlags.isEmpty()) { throw new IllegalStateException( - "mainClass is null on JavaContainerBuilder; specify the main class using " + "Failed to construct entrypoint on JavaContainerBuilder; " + + "jvmFlags were set, but mainClass is null. Specify the main class using " + "JavaContainerBuilder#setMainClass(String), or consider using a " - + "jib.frontend.MainClassFinder to infer the main class"); + + "jib.frontend.MainClassFinder to infer the main class."); } if (classpathOrder.isEmpty()) { throw new IllegalStateException( @@ -461,32 +463,35 @@ public JibContainerBuilder toContainerBuilder() throws IOException { appRoot.resolve(othersDestination).resolve(path.getFileName())); } } + jibContainerBuilder.setLayers(layerConfigurationsBuilder.build().getLayerConfigurations()); - // Construct entrypoint. Ensure classpath elements are in the same order as the files were added - // to the JavaContainerBuilder. - List classpathElements = new ArrayList<>(); - for (LayerType path : classpathOrder) { - switch (path) { - case CLASSES: - classpathElements.add(appRoot.resolve(classesDestination).toString()); - break; - case RESOURCES: - classpathElements.add(appRoot.resolve(resourcesDestination).toString()); - break; - case DEPENDENCIES: - classpathElements.add(appRoot.resolve(dependenciesDestination).resolve("*").toString()); - break; - case EXTRA_FILES: - classpathElements.add(appRoot.resolve(othersDestination).toString()); - break; - default: - throw new RuntimeException( - "Bug in jib-core; please report the bug at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); + if (mainClass != null) { + // Construct entrypoint. Ensure classpath elements are in the same order as the files were + // added to the JavaContainerBuilder. + List classpathElements = new ArrayList<>(); + for (LayerType path : classpathOrder) { + switch (path) { + case CLASSES: + classpathElements.add(appRoot.resolve(classesDestination).toString()); + break; + case RESOURCES: + classpathElements.add(appRoot.resolve(resourcesDestination).toString()); + break; + case DEPENDENCIES: + classpathElements.add(appRoot.resolve(dependenciesDestination).resolve("*").toString()); + break; + case EXTRA_FILES: + classpathElements.add(appRoot.resolve(othersDestination).toString()); + break; + default: + throw new RuntimeException( + "Bug in jib-core; please report the bug at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); + } } + jibContainerBuilder.setEntrypoint( + JavaEntrypointConstructor.makeEntrypoint(classpathElements, jvmFlags, mainClass)); } - jibContainerBuilder.setEntrypoint( - JavaEntrypointConstructor.makeEntrypoint(classpathElements, jvmFlags, mainClass)); - jibContainerBuilder.setLayers(layerConfigurationsBuilder.build().getLayerConfigurations()); + return jibContainerBuilder; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index c5397ebb41..be2c441f7c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -257,16 +257,29 @@ public void testToJibContainerBuilder_setAppRootLate() } @Test - public void testToJibContainerBuilder_mainClassNull() throws IOException { + public void testToJibContainerBuilder_mainClassNull() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, + URISyntaxException { + BuildConfiguration buildConfiguration = + JavaContainerBuilder.fromDistroless() + .addClasses(getResource("core/application/classes/")) + .toContainerBuilder() + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("hello")), + MoreExecutors.newDirectExecutorService()); + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + try { - JavaContainerBuilder.fromDistroless().toContainerBuilder(); + JavaContainerBuilder.fromDistroless().addJvmFlags("-flag1", "-flag2").toContainerBuilder(); Assert.fail(); } catch (IllegalStateException ex) { Assert.assertEquals( - "mainClass is null on JavaContainerBuilder; specify the main class using " + "Failed to construct entrypoint on JavaContainerBuilder; jvmFlags were set, but " + + "mainClass is null. Specify the main class using " + "JavaContainerBuilder#setMainClass(String), or consider using a " - + "jib.frontend.MainClassFinder to infer the main class", + + "jib.frontend.MainClassFinder to infer the main class.", ex.getMessage()); } } From e74cf5a8f0c023453071bd4bbd8621fe51fe917c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 14 Mar 2019 13:44:55 -0400 Subject: [PATCH 0504/2020] Upgrade checkstyle to 8.18 (#1556) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 7274ac5d9d..0b63a07c0c 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -123,7 +123,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = '8.15' + toolVersion = '8.18' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 12cc9423a9..41c6ef9a0b 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -128,7 +128,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = '8.15' + toolVersion = '8.18' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 19e589a2fe..7d768b468a 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -424,7 +424,7 @@ com.puppycrawl.tools checkstyle - 8.15 + 8.18 diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 3060a5ab46..8d22d24b4c 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -87,7 +87,7 @@ check.dependsOn verifyGoogleJavaFormat /* CHECKSTYLE */ checkstyle { - toolVersion = '8.15' + toolVersion = '8.18' // get the google_checks.xml file from the checkstyle jar and take out the java checks def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() From 23963eba663d44966ad7dcceaf38164a8ad9b6b4 Mon Sep 17 00:00:00 2001 From: Igor Konoplyanko <431522+CauchyPeano@users.noreply.github.com> Date: Thu, 14 Mar 2019 19:37:13 +0100 Subject: [PATCH 0505/2020] Added documentation on adding javaagent to container (#1540) --- docs/faq.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 0eae754c0d..7a52035bae 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -415,9 +415,27 @@ jib.container.useCurrentTimestamp = true ### I would like to run my application with a javaagent. -See [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image). +You can run your container with a javaagent by placing it in under `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. -*TODO: Provide more comprehensive solution.* +#### Maven + +```xml + + + + -javaagent:/myfolder/agent.jar + + + +``` + +#### Gradle + +```groovy +jib.container.jvmFlags = ['-javaagent:/myfolder/agent.jar'] +``` + +See also [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image). ### How can I tag my image with a timestamp? From cd7ac5fbe7751eb2bbb2a92bbff0a0b7a89e12c2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 14 Mar 2019 15:04:35 -0400 Subject: [PATCH 0506/2020] Minor FAQ grammar fix (#1558) --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 7a52035bae..4cf90d9e5a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -415,7 +415,7 @@ jib.container.useCurrentTimestamp = true ### I would like to run my application with a javaagent. -You can run your container with a javaagent by placing it in under `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. +You can run your container with a javaagent by placing it somewhere in the `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. #### Maven @@ -435,7 +435,7 @@ You can run your container with a javaagent by placing it in under `src/main/jib jib.container.jvmFlags = ['-javaagent:/myfolder/agent.jar'] ``` -See also [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image). +See also [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image) ### How can I tag my image with a timestamp? From 1b9b01699fca6daa19f24a9c715792212337dbf7 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 15 Mar 2019 11:27:43 -0400 Subject: [PATCH 0507/2020] Rename from Credential.basic() to Credential.from() (#1559) --- .../google/cloud/tools/jib/api/JibIntegrationTest.java | 6 +++--- .../com/google/cloud/tools/jib/api/RegistryImage.java | 2 +- .../jib/configuration/credentials/Credential.java | 5 ++--- .../credentials/DockerConfigCredentialRetriever.java | 2 +- .../registry/credentials/DockerCredentialHelper.java | 2 +- .../cloud/tools/jib/api/JibContainerBuilderTest.java | 2 +- .../google/cloud/tools/jib/api/RegistryImageTest.java | 2 +- .../steps/RetrieveRegistryCredentialsStepTest.java | 10 +++++----- .../jib/configuration/BuildConfigurationTest.java | 4 ++-- .../jib/configuration/credentials/CredentialTest.java | 10 +++++----- .../jib/frontend/CredentialRetrieverFactoryTest.java | 2 +- .../tools/jib/registry/RegistryAuthenticatorTest.java | 8 ++++---- .../DockerConfigCredentialRetrieverTest.java | 2 +- .../plugins/common/ConfigurationPropertyValidator.java | 4 ++-- .../plugins/common/PluginConfigurationProcessor.java | 2 +- .../common/ConfigurationPropertyValidatorTest.java | 4 ++-- .../common/DefaultCredentialRetrieversTest.java | 4 ++-- 17 files changed, 35 insertions(+), 36 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 96615a9364..355b02a6db 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -77,7 +77,7 @@ public void testBasic_helloWorld() Containerizer.to( RegistryImage.named(targetImageReference) .addCredentialRetriever( - () -> Optional.of(Credential.basic("username", "password")))) + () -> Optional.of(Credential.from("username", "password")))) .setAllowInsecureRegistries(true)); Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); @@ -98,7 +98,7 @@ public void testScratch() Containerizer.to( RegistryImage.named(targetImageReference) .addCredentialRetriever( - () -> Optional.of(Credential.basic("username", "password")))) + () -> Optional.of(Credential.from("username", "password")))) .setAllowInsecureRegistries(true)); // Check that resulting image has no layers @@ -120,7 +120,7 @@ public void testProvidedExecutorNotDisposed() Containerizer.to( RegistryImage.named(targetImageReference) .addCredentialRetriever( - () -> Optional.of(Credential.basic("username", "password")))) + () -> Optional.of(Credential.from("username", "password")))) .setAllowInsecureRegistries(true); ExecutorService executorService = Executors.newCachedThreadPool(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 43a1063cd0..faa1e87585 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -82,7 +82,7 @@ private RegistryImage(ImageReference imageReference) { * @return this */ public RegistryImage addCredential(String username, String password) { - addCredentialRetriever(() -> Optional.of(Credential.basic(username, password))); + addCredentialRetriever(() -> Optional.of(Credential.from(username, password))); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java index 6b918c4123..cefc9b1484 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java @@ -18,10 +18,9 @@ import java.util.Objects; -// TODO: Move to lower-level package - probably at same level as Authorization. /** Holds credentials (username and password). */ public class Credential { - // if the username is set to the secret would be an refresh token, see details at + // If the username is set to , the secret would be a refresh token. // https://github.com/docker/cli/blob/master/docs/reference/commandline/login.md#credential-helper-protocol private static final String OAUTH2_TOKEN_USER_NAME = ""; @@ -32,7 +31,7 @@ public class Credential { * @param password the password * @return a new {@link Credential} */ - public static Credential basic(String username, String password) { + public static Credential from(String username, String password) { return new Credential(username, password); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index bb29ff3d9f..cc3c57b310 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -98,7 +98,7 @@ Optional retrieve(DockerConfig dockerConfig) { new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); - return Optional.of(Credential.basic(username, password)); + return Optional.of(Credential.from(username, password)); } // Then, tries to use a defined credHelpers credential helper. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 010cc7b30b..91ec0aca34 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -119,7 +119,7 @@ public Credential retrieve() credentialHelper, serverUrl, output); } - return Credential.basic(dockerCredentials.Username, dockerCredentials.Secret); + return Credential.from(dockerCredentials.Username, dockerCredentials.Secret); } catch (JsonProcessingException ex) { throw new CredentialHelperUnhandledServerUrlException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index ced2a867d4..111d2466d7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -176,7 +176,7 @@ public void testToBuildConfiguration() Assert.assertEquals( 1, buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers().size()); Assert.assertEquals( - Credential.basic("username", "password"), + Credential.from("username", "password"), buildConfiguration .getTargetImageConfiguration() .getCredentialRetrievers() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java index 745d1a35e9..68f8f4125d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -49,7 +49,7 @@ public void testToImageConfiguration() Assert.assertEquals(2, imageConfiguration.getCredentialRetrievers().size()); Assert.assertSame(mockCredentialRetriever, imageConfiguration.getCredentialRetrievers().get(0)); Assert.assertEquals( - Credential.basic("username", "password"), + Credential.from("username", "password"), imageConfiguration .getCredentialRetrievers() .get(1) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 6f55fbd83a..4536f547f6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -54,13 +54,13 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio makeFakeBuildConfiguration( Arrays.asList( Optional::empty, - () -> Optional.of(Credential.basic("baseusername", "basepassword"))), + () -> Optional.of(Credential.from("baseusername", "basepassword"))), Arrays.asList( - () -> Optional.of(Credential.basic("targetusername", "targetpassword")), - () -> Optional.of(Credential.basic("ignored", "ignored")))); + () -> Optional.of(Credential.from("targetusername", "targetpassword")), + () -> Optional.of(Credential.from("ignored", "ignored")))); Assert.assertEquals( - Credential.basic("baseusername", "basepassword"), + Credential.from("baseusername", "basepassword"), RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration, @@ -68,7 +68,7 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio .newChildProducer()) .call()); Assert.assertEquals( - Credential.basic("targetusername", "targetpassword"), + Credential.from("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 4f179c37c2..8d1063ab76 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -54,7 +54,7 @@ public void testBuilder() throws Exception { Set additionalTargetImageTags = ImmutableSet.of("tag1", "tag2", "tag3"); Set expectedTargetImageTags = ImmutableSet.of("targettag", "tag1", "tag2", "tag3"); List credentialRetrievers = - Collections.singletonList(() -> Optional.of(Credential.basic("username", "password"))); + Collections.singletonList(() -> Optional.of(Credential.from("username", "password"))); Instant expectedCreationTime = Instant.ofEpochSecond(10000); List expectedEntrypoint = Arrays.asList("some", "entrypoint"); List expectedProgramArguments = Arrays.asList("arg1", "arg2"); @@ -126,7 +126,7 @@ public void testBuilder() throws Exception { expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAllTargetImageTags()); Assert.assertEquals( - Credential.basic("username", "password"), + Credential.from("username", "password"), buildConfiguration .getTargetImageConfiguration() .getCredentialRetrievers() diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java index e923374643..d5731c5f7b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java @@ -27,10 +27,10 @@ public class CredentialTest { @Test public void testCredentialsHash() { - Credential credentialA1 = Credential.basic("username", "password"); - Credential credentialA2 = Credential.basic("username", "password"); - Credential credentialB1 = Credential.basic("", ""); - Credential credentialB2 = Credential.basic("", ""); + Credential credentialA1 = Credential.from("username", "password"); + Credential credentialA2 = Credential.from("username", "password"); + Credential credentialB1 = Credential.from("", ""); + Credential credentialB2 = Credential.from("", ""); Assert.assertEquals(credentialA1, credentialA2); Assert.assertEquals(credentialB1, credentialB2); @@ -44,7 +44,7 @@ public void testCredentialsHash() { @Test public void testCredentialsOAuth2RefreshToken() { - Credential oauth2Credential = Credential.basic("", "eyJhbGciOi...3gw"); + Credential oauth2Credential = Credential.from("", "eyJhbGciOi...3gw"); Assert.assertTrue( "Credential should be an auth2 token when username is ", oauth2Credential.isOAuth2RefreshToken()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 0cf1ecfeb1..9b71ed1f2b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -42,7 +42,7 @@ @RunWith(MockitoJUnitRunner.class) public class CredentialRetrieverFactoryTest { - private static final Credential FAKE_CREDENTIALS = Credential.basic("username", "password"); + private static final Credential FAKE_CREDENTIALS = Credential.from("username", "password"); /** * Returns a {@link DockerCredentialHelperFactory} that checks given parameters upon creating a diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 09f95d8e54..95078b0bab 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -67,7 +67,7 @@ public void testAuthRequestParameters_basicAuth() { @Test public void testAuthRequestParameters_oauth2() { - registryAuthenticator.setCredential(Credential.basic("", "oauth2_access_token")); + registryAuthenticator.setCredential(Credential.from("", "oauth2_access_token")); Assert.assertEquals( "service=someservice&scope=repository:someimage:scope" + "&client_id=jib.da031fe481a93ac107a95a96462358f9" @@ -82,13 +82,13 @@ public void isOAuth2Auth_nullCredential() { @Test public void isOAuth2Auth_basicAuth() { - registryAuthenticator.setCredential(Credential.basic("name", "password")); + registryAuthenticator.setCredential(Credential.from("name", "password")); Assert.assertFalse(registryAuthenticator.isOAuth2Auth()); } @Test public void isOAuth2Auth_oauth2() { - registryAuthenticator.setCredential(Credential.basic("", "oauth2_token")); + registryAuthenticator.setCredential(Credential.from("", "oauth2_token")); Assert.assertTrue(registryAuthenticator.isOAuth2Auth()); } @@ -101,7 +101,7 @@ public void getAuthenticationUrl_basicAuth() throws MalformedURLException { @Test public void istAuthenticationUrl_oauth2() throws MalformedURLException { - registryAuthenticator.setCredential(Credential.basic("", "oauth2_token")); + registryAuthenticator.setCredential(Credential.from("", "oauth2_token")); Assert.assertEquals( new URL("https://somerealm"), registryAuthenticator.getAuthenticationUrl("scope")); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index af60a7d55d..3fb9c6d41c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -35,7 +35,7 @@ @RunWith(MockitoJUnitRunner.class) public class DockerConfigCredentialRetrieverTest { - private static final Credential FAKE_CREDENTIAL = Credential.basic("username", "password"); + private static final Credential FAKE_CREDENTIAL = Credential.from("username", "password"); @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 3995dc6f2b..c2dd0922d4 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -63,7 +63,7 @@ public static Optional getImageCredential( String commandlinePassword = rawConfiguration.getProperty(passwordProperty).orElse(null); if (!Strings.isNullOrEmpty(commandlineUsername) && !Strings.isNullOrEmpty(commandlinePassword)) { - return Optional.of(Credential.basic(commandlineUsername, commandlinePassword)); + return Optional.of(Credential.from(commandlineUsername, commandlinePassword)); } // Warn if a system property is missing @@ -103,7 +103,7 @@ public static Optional getImageCredential( return Optional.empty(); } - return Optional.of(Credential.basic(auth.getUsername(), auth.getPassword())); + return Optional.of(Credential.from(auth.getUsername(), auth.getPassword())); } /** diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 4c57b61aa8..853334756b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -407,7 +407,7 @@ private static boolean configureCredentialRetrievers( AuthProperty auth = optionalInferredAuth.get(); String username = Verify.verifyNotNull(auth.getUsername()); String password = Verify.verifyNotNull(auth.getPassword()); - Credential credential = Credential.basic(username, password); + Credential credential = Credential.from(username, password); defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index c7941de0cf..a7c794dd30 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -52,7 +52,7 @@ public void testGetImageAuth() { .thenReturn(Optional.of("abcde")); Mockito.when(mockConfiguration.getProperty("jib.test.auth.pass")) .thenReturn(Optional.of("12345")); - Credential expected = Credential.basic("abcde", "12345"); + Credential expected = Credential.from("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( mockEventDispatcher, @@ -66,7 +66,7 @@ public void testGetImageAuth() { // Auth set in configuration Mockito.when(mockConfiguration.getProperty("jib.test.auth.user")).thenReturn(Optional.empty()); Mockito.when(mockConfiguration.getProperty("jib.test.auth.pass")).thenReturn(Optional.empty()); - expected = Credential.basic("vwxyz", "98765"); + expected = Credential.from("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( mockEventDispatcher, diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index c98c724446..3f3ba8cb9f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -48,8 +48,8 @@ public class DefaultCredentialRetrieversTest { @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; - private final Credential knownCredential = Credential.basic("username", "password"); - private final Credential inferredCredential = Credential.basic("username2", "password2"); + private final Credential knownCredential = Credential.from("username", "password"); + private final Credential inferredCredential = Credential.from("username2", "password2"); @Before public void setUp() { From 882488b9c427479dc89157b4c003e83859bea281 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 15 Mar 2019 14:31:43 -0400 Subject: [PATCH 0508/2020] Add instructions for importing source into Eclipse (#1557) --- CONTRIBUTING.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b0694e7aa..6fed82d122 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,49 @@ To build, use the provided `build.sh` which builds and tests each of the compone # Development Tips +## Configuring Eclipse + +Jib is a mix of Gradle and Maven projects. But since we do not +publish all artifacts, we instead have the projects reference +source from their dependencies directly using `sourceSets` and +`build-helper-maven-plugin`. This complicates importing these projects +as the Gradle and Maven tooling for Eclipse faithfully replicate the +direct source references. This has a side-effect that classes +appear both in the originating project _and_ in the subprojects, +and can interfere with Eclipse's incremental compilation. + +The following instructions first import the projects, and then replaces +the source-set-style references to use Eclipse's project references +instead. + + 1. Ensure you have installed the Gradle tooling for Eclipse, called + _Buildship_ (available from [the Eclipse + Marketplace](https://marketplace.eclipse.org/content/buildship-gradle-integration)). + 2. **Import the Maven project:** Use _File → Open Projects from File System..._ + to use the Eclipe Smart Import feature and specify the root of the Jib project. + Import the `jib-maven-plugin` project. + 3. **Import the Gradle projects:** Buildship does [not yet support + Eclipse Smart Import](https://github.com/eclipse/buildship/issues/356). + Use _File → Import → Gradle → Existing Gradle Project_ + and import each of `jib-core`, `jib-plugins-common`, and `jib-gradle-plugin`. + 4. **Turn source-set references to project references:** For each of + `jib-plugins-common`, `jib-maven-plugin`, and `jib-gradle-plugin`: + - Right-click on the correponding project and select _Properties → Java Build Path_ + - Open the _Source_ panel and remove all _linked_ source folders: these are like + symbolic links to other locations and appear like + `jib-plugins-common/main-java - /path/to/jib-core/src/main/java`. + Only folders within the project should remain when complete. + - you may wish to remove `jib-gradle-plugin`'s `src/test/resources` and + `src/integration-test/resources` too as these contain test projects, + and are not linked in as separate projects and so seem have compilation errors + - Open the _Projects_ panel and click _Add_ to select the dependencies: + - `jib-plugins-common` depends on `jib-core` + - `jib-maven-plugin` depends on `jib-core` and `jib-plugins-common` + - `jib-gradle-plugin` depends on `jib-core` and `jib-plugins-common` + +Note that you will likely need to re-apply these changes whenever +you refresh or update these projects. + ## Debugging the Jib Maven Plugin (`jib-maven-plugin`) ### Build and use a local snapshot From da6aee89da1ae96b5ae8ddb08dab402b9a46eb26 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 18 Mar 2019 15:14:45 -0400 Subject: [PATCH 0509/2020] Move layer file info logging to containerize() (#1563) --- .../tools/jib/api/JibContainerBuilder.java | 31 +++++++++++++++++++ .../tools/jib/gradle/BuildDockerTask.java | 1 - .../tools/jib/gradle/BuildImageTask.java | 1 - .../cloud/tools/jib/gradle/BuildTarTask.java | 1 - .../tools/jib/maven/BuildDockerMojo.java | 1 - .../cloud/tools/jib/maven/BuildImageMojo.java | 1 - .../cloud/tools/jib/maven/BuildTarMojo.java | 1 - .../jib/plugins/common/BuildStepsRunner.java | 29 ----------------- .../plugins/common/BuildStepsRunnerTest.java | 14 +-------- 9 files changed, 32 insertions(+), 48 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 9f73aec358..6f72a43475 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -24,8 +24,10 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryException; @@ -67,6 +69,13 @@ // TODO: Add tests once containerize() is added. public class JibContainerBuilder { + private static String capitalizeFirstLetter(String string) { + if (string.length() == 0) { + return string; + } + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + private final ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder(); private final BuildConfiguration.Builder buildConfigurationBuilder; @@ -465,6 +474,28 @@ JibContainer containerize( BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer, executorService); + // Logs the different source files used. + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.info("Containerizing application with the following files:")); + + for (LayerConfiguration layerConfiguration : layerConfigurations) { + if (layerConfiguration.getLayerEntries().isEmpty()) { + continue; + } + + buildConfiguration + .getEventDispatcher() + .dispatch( + LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); + + for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { + buildConfiguration + .getEventDispatcher() + .dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); + } + } + try { BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); return new JibContainer(result.getImageDigest(), result.getImageId()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 8f6f87bb64..b6f0da5614 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -150,7 +150,6 @@ public void buildDocker() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 50e19110b1..b0f83d51bb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -129,7 +129,6 @@ public void buildImage() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 05a8976e0c..0120991465 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -147,7 +147,6 @@ public void buildTar() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), new DefaultEventDispatcher(projectProperties.getEventHandlers()), - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 55120bec69..ae0ae39ea4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -141,7 +141,6 @@ targetImageReference, getTargetImageAdditionalTags()) pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 78e7ea0dee..2a0637fb91 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -133,7 +133,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 3007ea1a2f..55ecf4b4c9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -111,7 +111,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), eventDispatcher, - projectProperties.getJavaLayerConfigurations().getLayerConfigurations(), helpfulSuggestions); } finally { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java index 724330e160..40e3702a5f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java @@ -23,11 +23,9 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; @@ -40,7 +38,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.ExecutionException; @@ -150,13 +147,6 @@ private static void handleRegistryUnauthorizedException( } } - private static String capitalizeFirstLetter(String string) { - if (string.length() == 0) { - return string; - } - return Character.toUpperCase(string.charAt(0)) + string.substring(1); - } - private final String startupMessage; private final String successMessage; @Nullable private Path imageDigestOutputPath; @@ -174,7 +164,6 @@ private static String capitalizeFirstLetter(String string) { * @param jibContainerBuilder the {@link JibContainerBuilder} * @param containerizer the {@link Containerizer} * @param eventDispatcher the {@link EventDispatcher} - * @param layerConfigurations the list of {@link LayerConfiguration}s * @param helpfulSuggestions suggestions to use in help messages for exceptions * @return the built {@link JibContainer} * @throws BuildStepsExecutionException if another exception is thrown during the build @@ -185,30 +174,12 @@ public JibContainer build( JibContainerBuilder jibContainerBuilder, Containerizer containerizer, EventDispatcher eventDispatcher, - List layerConfigurations, HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { try { eventDispatcher.dispatch(LogEvent.lifecycle("")); eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); - // Logs the different source files used. - eventDispatcher.dispatch( - LogEvent.info("Containerizing application with the following files:")); - - for (LayerConfiguration layerConfiguration : layerConfigurations) { - if (layerConfiguration.getLayerEntries().isEmpty()) { - continue; - } - - eventDispatcher.dispatch( - LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); - - for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); - } - } - JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); eventDispatcher.dispatch(LogEvent.lifecycle("")); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java index 7bd97288d5..59a9bbd6db 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java @@ -29,7 +29,6 @@ import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import java.io.IOException; import java.net.UnknownHostException; -import java.util.Collections; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; import org.junit.Assert; @@ -82,11 +81,7 @@ public void setUpMocks() { public void testBuildImage_pass() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { testBuildImageStepsRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - Collections.emptyList(), - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, TEST_HELPFUL_SUGGESTIONS); } @Test @@ -104,7 +99,6 @@ public void testBuildImage_httpHostConnectException() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -127,7 +121,6 @@ public void testBuildImage_unknownHostException() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -151,7 +144,6 @@ public void testBuildImage_insecureRegistryException() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -180,7 +172,6 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -210,7 +201,6 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -234,7 +224,6 @@ public void testBuildImage_registryCredentialsNotSentException() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); @@ -257,7 +246,6 @@ public void testBuildImage_other() mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, - Collections.emptyList(), TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); From 78e7adcd0e04080834ca9435510c5a4ba0950cc6 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 19 Mar 2019 10:47:47 -0400 Subject: [PATCH 0510/2020] Add support for docker.io (#1560) Adds support for registry host remapping to RegistryAliasGroup. --- .../builder/steps/AuthenticatePushStep.java | 2 +- .../cloud/tools/jib/image/ImageReference.java | 8 ++++-- .../jib/registry/RegistryAliasGroup.java | 24 ++++++++++++++-- .../tools/jib/image/ImageReferenceTest.java | 26 +++++++++++++---- .../jib/registry/RegistryAliasGroupTest.java | 28 +++++++++++++------ jib-maven-plugin/README.md | 2 +- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 79e9dc18ce..5562946d8a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -91,7 +91,7 @@ public Authorization call() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( buildConfiguration.getEventDispatcher(), - buildConfiguration.getTargetImageConfiguration().getImageRegistry(), + registry, buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) .initialize(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index 5adf54eee6..9aba1ef8d8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.util.regex.Matcher; @@ -32,7 +33,7 @@ */ public class ImageReference { - private static final String DOCKER_HUB_REGISTRY = "registry.hub.docker.com"; + private static final String DOCKER_HUB_REGISTRY = "registry-1.docker.io"; private static final String DEFAULT_TAG = "latest"; private static final String LIBRARY_REPOSITORY_PREFIX = "library/"; @@ -120,6 +121,7 @@ public static ImageReference parse(String reference) throws InvalidImageReferenc repository = registry + "/" + repository; registry = DOCKER_HUB_REGISTRY; } + /* * For Docker Hub, if the repository is only one component, then it should be prefixed with * 'library/'. @@ -230,7 +232,7 @@ public static boolean isDefaultTag(String tag) { /** Construct with {@link #parse}. */ private ImageReference(String registry, String repository, String tag) { - this.registry = registry; + this.registry = RegistryAliasGroup.getHost(registry); this.repository = repository; this.tag = tag; } @@ -238,7 +240,7 @@ private ImageReference(String registry, String repository, String tag) { /** * Gets the registry portion of the {@link ImageReference}. * - * @return the registry + * @return the registry host */ public String getRegistry() { return registry; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java index 112d5c494a..958b41b0c1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java @@ -17,19 +17,27 @@ package com.google.cloud.tools.jib.registry; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -/** Provides known aliases for a given registry. */ +/** Provides known aliases and alternative hosts for a given registry. */ public class RegistryAliasGroup { private static final ImmutableList> REGISTRY_ALIAS_GROUPS = ImmutableList.of( - // Docker Hub alias group - ImmutableSet.of("registry.hub.docker.com", "index.docker.io")); + // Docker Hub alias group (https://github.com/moby/moby/pull/28100) + ImmutableSet.of( + "registry.hub.docker.com", "index.docker.io", "registry-1.docker.io", "docker.io")); + + /** Some registry names are symbolic. */ + private static final ImmutableMap REGISTRY_HOST_MAP = + ImmutableMap.of( + // https://github.com/docker/hub-feedback/issues/1767 + "docker.io", "registry-1.docker.io"); /** * Returns the list of registry aliases for the given {@code registry}, including {@code registry} @@ -50,4 +58,14 @@ public static List getAliasesGroup(String registry) { return Collections.singletonList(registry); } + + /** + * Returns the server host name to use for the given registry. + * + * @param registry the name of the registry + * @return the registry host + */ + public static String getHost(String registry) { + return REGISTRY_HOST_MAP.getOrDefault(registry, registry); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java index 11a7a5e36d..65d91d827a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java @@ -66,7 +66,7 @@ public void testParse_dockerHub_official() throws InvalidImageReferenceException String imageReferenceString = "busybox"; ImageReference imageReference = ImageReference.parse(imageReferenceString); - Assert.assertEquals("registry.hub.docker.com", imageReference.getRegistry()); + Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("library/busybox", imageReference.getRepository()); Assert.assertEquals("latest", imageReference.getTag()); } @@ -76,7 +76,7 @@ public void testParse_dockerHub_user() throws InvalidImageReferenceException { String imageReferenceString = "someuser/someimage"; ImageReference imageReference = ImageReference.parse(imageReferenceString); - Assert.assertEquals("registry.hub.docker.com", imageReference.getRegistry()); + Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("someuser/someimage", imageReference.getRepository()); Assert.assertEquals("latest", imageReference.getTag()); } @@ -109,10 +109,10 @@ public void testOf_smoke() { Assert.assertEquals( expectedTag, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getTag()); Assert.assertEquals( - "registry.hub.docker.com", + "registry-1.docker.io", ImageReference.of(null, expectedRepository, expectedTag).getRegistry()); Assert.assertEquals( - "registry.hub.docker.com", ImageReference.of(null, expectedRepository, null).getRegistry()); + "registry-1.docker.io", ImageReference.of(null, expectedRepository, null).getRegistry()); Assert.assertEquals( "latest", ImageReference.of(expectedRegistry, expectedRepository, null).getTag()); Assert.assertEquals("latest", ImageReference.of(null, expectedRepository, null).getTag()); @@ -187,15 +187,29 @@ public void testIsScratch() { Assert.assertFalse(ImageReference.of(null, "scratch", null).isScratch()); } + @Test + public void testGetRegistry() { + Assert.assertEquals( + "registry-1.docker.io", ImageReference.of(null, "someimage", null).getRegistry()); + Assert.assertEquals( + "registry-1.docker.io", ImageReference.of("docker.io", "someimage", null).getRegistry()); + Assert.assertEquals( + "index.docker.io", ImageReference.of("index.docker.io", "someimage", null).getRegistry()); + Assert.assertEquals( + "registry.hub.docker.com", + ImageReference.of("registry.hub.docker.com", "someimage", null).getRegistry()); + Assert.assertEquals("gcr.io", ImageReference.of("gcr.io", "someimage", null).getRegistry()); + } + private void verifyParse(String registry, String repository, String tagSeparator, String tag) throws InvalidImageReferenceException { // Gets the expected parsed components. String expectedRegistry = registry; if (Strings.isNullOrEmpty(expectedRegistry)) { - expectedRegistry = "registry.hub.docker.com"; + expectedRegistry = "registry-1.docker.io"; } String expectedRepository = repository; - if ("registry.hub.docker.com".equals(expectedRegistry) && repository.indexOf('/') < 0) { + if ("registry-1.docker.io".equals(expectedRegistry) && repository.indexOf('/') < 0) { expectedRepository = "library/" + expectedRepository; } String expectedTag = tag; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java index 7481842154..4c78c26f8b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.registry; -import java.util.Arrays; +import com.google.common.collect.Sets; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.junit.Assert; import org.junit.Test; @@ -32,16 +34,24 @@ public void testGetAliasesGroup_noKnownAliases() { } @Test - public void testGetAliasesGroup_registryHubDockerCom() { - Assert.assertEquals( - Arrays.asList("registry.hub.docker.com", "index.docker.io"), - RegistryAliasGroup.getAliasesGroup("registry.hub.docker.com")); + public void testGetAliasesGroup_dockerHub() { + Set aliases = + Sets.newHashSet( + "registry.hub.docker.com", "index.docker.io", "registry-1.docker.io", "docker.io"); + for (String alias : aliases) { + Assert.assertEquals(aliases, new HashSet<>(RegistryAliasGroup.getAliasesGroup(alias))); + } } @Test - public void testGetAliasesGroup_indexDockerIo() { - Assert.assertEquals( - Arrays.asList("index.docker.io", "registry.hub.docker.com"), - RegistryAliasGroup.getAliasesGroup("index.docker.io")); + public void testGetHost_noAlias() { + String host = RegistryAliasGroup.getHost("something.gcr.io"); + Assert.assertEquals("something.gcr.io", host); + } + + @Test + public void testGetHost_dockerIo() { + String host = RegistryAliasGroup.getHost("docker.io"); + Assert.assertEquals("registry-1.docker.io", host); } } diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index b39a5c0f20..616e7fbcc1 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -118,7 +118,7 @@ For example, to build the image `my-docker-id/my-app`, the configuration would b ```xml - registry.hub.docker.com/my-docker-id/my-app + docker.io/my-docker-id/my-app ``` From 985e0a161fe6760329f9bd94ad05f8548ef771ed Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 19 Mar 2019 15:39:11 -0400 Subject: [PATCH 0511/2020] Propagate architecture and os from base to target images (#1564) --- jib-core/CHANGELOG.md | 3 +- .../jib/builder/steps/BuildImageStep.java | 2 + .../google/cloud/tools/jib/image/Image.java | 44 +++++++++++++++++++ .../json/ContainerConfigurationTemplate.java | 44 +++++++++++++++++++ .../jib/image/json/ImageToJsonTranslator.java | 2 + .../jib/image/json/JsonToImageTranslator.java | 7 +++ .../jib/builder/steps/BuildImageStepTest.java | 4 ++ .../cloud/tools/jib/image/ImageTest.java | 18 ++++++++ .../ContainerConfigurationTemplateTest.java | 4 ++ .../image/json/ImageToJsonTranslatorTest.java | 2 + .../resources/core/json/containerconfig.json | 2 +- .../core/json/translated_ocimanifest.json | 2 +- .../core/json/translated_v22manifest.json | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 15 files changed, 134 insertions(+), 4 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 8981ea6cbd..cc1ea2cc8b 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. ### Changed - Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint +- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed @@ -34,4 +35,4 @@ All notable changes to this project will be documented in this file. ### Added - Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) -- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) \ No newline at end of file +- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 64443c37bd..85d12e0b04 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -119,6 +119,8 @@ private Image afterCachedLayerSteps() } } imageBuilder + .setArchitecture(baseImage.getArchitecture()) + .setOs(baseImage.getOs()) .addEnvironment(baseImage.getEnvironment()) .addLabels(baseImage.getLabels()) .setHealthCheck(baseImage.getHealthCheck()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 2e5389d906..de83f08df9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -50,6 +50,8 @@ public static class Builder { private final Set volumesBuilder = new HashSet<>(); @Nullable private Instant created; + private String architecture = "amd64"; + private String os = "linux"; @Nullable private ImmutableList entrypoint; @Nullable private ImmutableList programArguments; @Nullable private DockerHealthCheck healthCheck; @@ -71,6 +73,28 @@ public Builder setCreated(Instant created) { return this; } + /** + * Sets the image architecture. + * + * @param architecture the architecture + * @return this + */ + public Builder setArchitecture(String architecture) { + this.architecture = architecture; + return this; + } + + /** + * Sets the image operating system. + * + * @param os the operating system + * @return this + */ + public Builder setOs(String os) { + this.os = os; + return this; + } + /** * Adds a map of environment variables to the current map. * @@ -230,6 +254,8 @@ public Image build() { return new Image<>( imageFormat, created, + architecture, + os, imageLayersBuilder.build(), historyBuilder.build(), ImmutableMap.copyOf(environmentBuilder), @@ -255,6 +281,12 @@ public static Builder builder( /** The image creation time. */ @Nullable private final Instant created; + /** The image architecture. */ + private final String architecture; + + /** The image operating system. */ + private final String os; + /** The layers of the image, in the order in which they are applied. */ private final ImageLayers layers; @@ -291,6 +323,8 @@ public static Builder builder( private Image( Class imageFormat, @Nullable Instant created, + String architecture, + String os, ImageLayers layers, ImmutableList history, @Nullable ImmutableMap environment, @@ -304,6 +338,8 @@ private Image( @Nullable String user) { this.imageFormat = imageFormat; this.created = created; + this.architecture = architecture; + this.os = os; this.layers = layers; this.history = history; this.environment = environment; @@ -326,6 +362,14 @@ public Instant getCreated() { return created; } + public String getArchitecture() { + return architecture; + } + + public String getOs() { + return os; + } + @Nullable public ImmutableMap getEnvironment() { return environment; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 9a7887ed3e..5818edfdb5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -172,6 +172,28 @@ public void setCreated(@Nullable String created) { this.created = created; } + /** + * Sets the architecture for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @param architecture value for the {@code architecture} field + */ + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + /** + * Sets the operating system for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @param os value for the {@code os} field + */ + public void setOs(String os) { + this.os = os; + } + public void setContainerEnvironment(@Nullable List environment) { config.Env = environment; } @@ -260,6 +282,28 @@ String getCreated() { return created; } + /** + * Returns the architecture for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @return the {@code architecture} field + */ + public String getArchitecture() { + return architecture; + } + + /** + * Returns the operating system for which this container was built. See the OCI Image + * Configuration specification for acceptable values. + * + * @return the {@code os} field + */ + public String getOs() { + return os; + } + @Nullable List getContainerEnvironment() { return config.Env; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index e64fbd86c4..bb504ffeac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -160,6 +160,8 @@ public Blob getContainerConfigurationBlob() { } template.setCreated(image.getCreated() == null ? null : image.getCreated().toString()); + template.setArchitecture(image.getArchitecture()); + template.setOs(image.getOs()); template.setContainerEnvironment(environmentMapToList(image.getEnvironment())); template.setContainerEntrypoint(image.getEntrypoint()); template.setContainerCmd(image.getProgramArguments()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 5f1a1d2a0c..ac1e8e193e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -140,6 +140,13 @@ public static Image toImage( } } + if (containerConfigurationTemplate.getArchitecture() != null) { + imageBuilder.setArchitecture(containerConfigurationTemplate.getArchitecture()); + } + if (containerConfigurationTemplate.getOs() != null) { + imageBuilder.setOs(containerConfigurationTemplate.getOs()); + } + if (containerConfigurationTemplate.getContainerEntrypoint() != null) { imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index d5141b1999..b1a0012005 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -130,6 +130,8 @@ public BlobDescriptor getBlobDescriptor() { Image baseImage = Image.builder(V22ManifestTemplate.class) + .setArchitecture("wasm") + .setOs("js") .addEnvironment(ImmutableMap.of("BASE_ENV", "BASE_ENV_VALUE", "BASE_ENV_2", "DEFAULT")) .addLabel("base.label", "base.label.value") .addLabel("base.label.2", "default") @@ -227,6 +229,8 @@ public void test_propagateBaseImageConfiguration() mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); Image image = buildImageStep.getFuture().get().getFuture().get(); + Assert.assertEquals("wasm", image.getArchitecture()); + Assert.assertEquals("js", image.getOs()); Assert.assertEquals( ImmutableMap.of( "BASE_ENV", "BASE_ENV_VALUE", "MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE"), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 13fb259257..748eede33d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import java.time.Instant; import java.util.Arrays; +import java.util.Collections; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -76,4 +77,21 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { image.getVolumes()); Assert.assertEquals("john", image.getUser()); } + + @Test + public void testDefaults() { + Image image = Image.builder(V22ManifestTemplate.class).build(); + Assert.assertEquals("amd64", image.getArchitecture()); + Assert.assertEquals("linux", image.getOs()); + Assert.assertEquals(Collections.emptyList(), image.getLayers()); + Assert.assertEquals(Collections.emptyList(), image.getHistory()); + } + + @Test + public void testOsArch() { + Image image = + Image.builder(V22ManifestTemplate.class).setArchitecture("wasm").setOs("js").build(); + Assert.assertEquals("wasm", image.getArchitecture()); + Assert.assertEquals("js", image.getOs()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index e5dd9eb81d..98366aacaa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -48,6 +48,8 @@ public void testToJson() throws IOException, URISyntaxException, DigestException ContainerConfigurationTemplate containerConfigJson = new ContainerConfigurationTemplate(); containerConfigJson.setCreated("1970-01-01T00:00:20Z"); + containerConfigJson.setArchitecture("wasm"); + containerConfigJson.setOs("js"); containerConfigJson.setContainerEnvironment(Arrays.asList("VAR1=VAL1", "VAR2=VAL2")); containerConfigJson.setContainerEntrypoint(Arrays.asList("some", "entrypoint", "command")); containerConfigJson.setContainerCmd(Arrays.asList("arg1", "arg2")); @@ -105,6 +107,8 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti JsonTemplateMapper.readJsonFromFile(jsonFile, ContainerConfigurationTemplate.class); Assert.assertEquals("1970-01-01T00:00:20Z", containerConfigJson.getCreated()); + Assert.assertEquals("wasm", containerConfigJson.getArchitecture()); + Assert.assertEquals("js", containerConfigJson.getOs()); Assert.assertEquals( Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), containerConfigJson.getContainerEnvironment()); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 666c95ce7b..f7a5f68524 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -58,6 +58,8 @@ private void setUp(Class imageFormat) Image.Builder testImageBuilder = Image.builder(imageFormat) .setCreated(Instant.ofEpochSecond(20)) + .setArchitecture("wasm") + .setOs("js") .addEnvironmentVariable("VAR1", "VAL1") .addEnvironmentVariable("VAR2", "VAL2") .setEntrypoint(Arrays.asList("some", "entrypoint", "command")) diff --git a/jib-core/src/test/resources/core/json/containerconfig.json b/jib-core/src/test/resources/core/json/containerconfig.json index 9c6bd20a55..850b612fd2 100644 --- a/jib-core/src/test/resources/core/json/containerconfig.json +++ b/jib-core/src/test/resources/core/json/containerconfig.json @@ -1 +1 @@ -{"created":"1970-01-01T00:00:20Z","architecture":"amd64","os":"linux","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"Healthcheck":{"Test":["CMD-SHELL","/checkhealth"],"Interval":3000000000,"Timeout":1000000000,"StartPeriod":2000000000,"Retries":3},"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file +{"created":"1970-01-01T00:00:20Z","architecture":"wasm","os":"js","config":{"Env":["VAR1=VAL1","VAR2=VAL2"],"Entrypoint":["some","entrypoint","command"],"Cmd":["arg1","arg2"],"Healthcheck":{"Test":["CMD-SHELL","/checkhealth"],"Interval":3000000000,"Timeout":1000000000,"StartPeriod":2000000000,"Retries":3},"ExposedPorts":{"1000/tcp":{},"2000/tcp":{},"3000/udp":{}},"Labels":{"key1":"value1","key2":"value2"},"WorkingDir":"/some/workspace","User":"tomcat","Volumes":{"/var/job-result-data":{},"/var/log/my-app-logs":{}}},"history":[{"created":"1970-01-01T00:00:00Z","author":"Bazel","created_by":"bazel build ...","empty_layer":true},{"created":"1970-01-01T00:00:20Z","author":"Jib","created_by":"jib"}],"rootfs":{"type":"layers","diff_ids":["sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/translated_ocimanifest.json b/jib-core/src/test/resources/core/json/translated_ocimanifest.json index 8b909b11b2..8429edcab6 100644 --- a/jib-core/src/test/resources/core/json/translated_ocimanifest.json +++ b/jib-core/src/test/resources/core/json/translated_ocimanifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:39feb4ae7cd3041613e19fcc33c7ec9bd14e6a2bd6fd182c404cd1ba9e20f00d","size":690},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:9df7a2b76e1b35dcf443c4b6a08e40a5d040120022293f7d03ae082f95835839","size":686},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/translated_v22manifest.json b/jib-core/src/test/resources/core/json/translated_v22manifest.json index 4a2f6dbaf3..13082f490d 100644 --- a/jib-core/src/test/resources/core/json/translated_v22manifest.json +++ b/jib-core/src/test/resources/core/json/translated_v22manifest.json @@ -1 +1 @@ -{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:7bf20598dd1d70fd26a4ff5c60106e021f48aabef7abc201d6d0a5d38f55b067","size":822},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:2000a70a1ce8bba401c493376fdb9eb81bcf7155212f4ce4c6ff96e577718a49","size":818},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000}]} \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 26263e6678..c8fd8ad430 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added ### Changed +- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index fa9fabb9e2..db59da92a8 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added ### Changed +- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed From 9f3bee41bdbacedda4ae88ceb834da8753283df0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 20 Mar 2019 14:35:25 -0400 Subject: [PATCH 0512/2020] Don't add empty layers in JavaLayerConfigurations (#1569) --- .../jib/frontend/JavaLayerConfigurations.java | 23 +++++++++++-------- .../frontend/JavaLayerConfigurationsTest.java | 10 ++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 8a9dd1f593..76f2faafcb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -68,11 +67,7 @@ public static class Builder { private final Map layerBuilders = new EnumMap<>(LayerType.class); - private Builder() { - for (LayerType layerType : LayerType.values()) { - layerBuilders.put(layerType, LayerConfiguration.builder()); - } - } + private Builder() {} /** * Adds a file to a layer. Only adds the single source file to the exact path in the container @@ -111,8 +106,10 @@ public Builder addFile( Path sourceFile, AbsoluteUnixPath pathInContainer, @Nullable FilePermissions permissions) { - Preconditions.checkNotNull(layerBuilders.get(layerType)) - .addEntry(sourceFile, pathInContainer, permissions); + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, LayerConfiguration.builder()); + } + layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer, permissions); return this; } @@ -167,7 +164,10 @@ public Builder addDirectoryContents( AbsoluteUnixPath basePathInContainer, Map permissionsMap) throws IOException { - LayerConfiguration.Builder builder = Preconditions.checkNotNull(layerBuilders.get(layerType)); + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, LayerConfiguration.builder()); + } + LayerConfiguration.Builder builder = layerBuilders.get(layerType); new DirectoryWalker(sourceRoot) .filterRoot() @@ -240,6 +240,9 @@ public ImmutableList getExtraFilesLayerEntries() { } private ImmutableList getLayerEntries(LayerType layerType) { - return Preconditions.checkNotNull(layerConfigurationMap.get(layerType)).getLayerEntries(); + if (!layerConfigurationMap.containsKey(layerType)) { + return ImmutableList.of(); + } + return layerConfigurationMap.get(layerType).getLayerEntries(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java index 6b87188412..520bffb1eb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java @@ -264,6 +264,11 @@ public void testAddDirectoryContents_file() throws IOException { Collections.singletonList( new LayerEntry(sourceRoot.resolve("file"), basePath.resolve("file"), null)), configurations.getExtraFilesLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getClassLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getResourceLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getDependencyLayerEntries()); + Assert.assertEquals( + Collections.emptyList(), configurations.getSnapshotDependencyLayerEntries()); } @Test @@ -281,6 +286,11 @@ public void testAddDirectoryContents_emptyDirectory() throws IOException { Collections.singletonList( new LayerEntry(sourceRoot.resolve("leaf"), basePath.resolve("leaf"), null)), configurations.getClassLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getResourceLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getDependencyLayerEntries()); + Assert.assertEquals( + Collections.emptyList(), configurations.getSnapshotDependencyLayerEntries()); + Assert.assertEquals(Collections.emptyList(), configurations.getExtraFilesLayerEntries()); } @Test From 450cf6b7f83fb7545ebd9b97190b60850bfe6257 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 20 Mar 2019 15:11:17 -0400 Subject: [PATCH 0513/2020] Add comment of ImmutableMap layer ordering (#1570) --- .../google/cloud/tools/jib/frontend/JavaLayerConfigurations.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 76f2faafcb..0177d852d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -185,6 +185,7 @@ public JavaLayerConfigurations build() { ImmutableMap.Builder layerConfigurationsMap = ImmutableMap.builderWithExpectedSize(layerBuilders.size()); + // ImmutableMap.Builder will preserve the order of EnumMap. layerBuilders.forEach( (type, builder) -> layerConfigurationsMap.put(type, builder.setName(type.getName()).build())); From 10d815149d4623569786556caa1e8a003fef85b1 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Fri, 22 Mar 2019 18:58:07 +0100 Subject: [PATCH 0514/2020] add lasttimestamp to LayerEntry to ensure the timestamp can be controlled for key files (#1571) --- .../jib/configuration/LayerConfiguration.java | 26 +++++++++++++-- .../cloud/tools/jib/image/LayerEntry.java | 33 ++++++++++++++++++- .../jib/image/ReproducibleLayerBuilder.java | 3 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index 7ccdf63976..cd24b27a19 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -80,15 +80,37 @@ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { * sourceFile} * @param permissions the file permissions on the container. If null, then default permissions * are used (644 for files, 755 for directories) + * @param lastModified the modification time of the file * @return this * @see Builder#addEntry(Path, AbsoluteUnixPath) */ public Builder addEntry( - Path sourceFile, AbsoluteUnixPath pathInContainer, @Nullable FilePermissions permissions) { - layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions)); + Path sourceFile, + AbsoluteUnixPath pathInContainer, + @Nullable FilePermissions permissions, + long lastModified) { + layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions, lastModified)); return this; } + /** + * Adds an entry to the layer with the given permissions. Only adds the single source file to + * the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container. If null, then default permissions + * are used (644 for files, 755 for directories) + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, @Nullable FilePermissions permissions) { + return addEntry(sourceFile, pathInContainer, permissions, 1000); + } + /** * Adds an entry to the layer. If the source file is a directory, the directory and its contents * will be added recursively. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 15900bef41..8b1da5e10a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -40,6 +40,7 @@ public class LayerEntry { private final Path sourceFile; private final AbsoluteUnixPath extractionPath; private final FilePermissions permissions; + private final long lastModifiedTime; /** * Instantiates with a source file and the path to place the source file in the container file @@ -58,9 +59,14 @@ public class LayerEntry { * sourceFile} * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 * for files, 755 for directories) + * @param lastModifiedTime the file modification time, default to 1 second since the epoch + * (https://github.com/GoogleContainerTools/jib/issues/1079) */ public LayerEntry( - Path sourceFile, AbsoluteUnixPath extractionPath, @Nullable FilePermissions permissions) { + Path sourceFile, + AbsoluteUnixPath extractionPath, + @Nullable FilePermissions permissions, + long lastModifiedTime) { this.sourceFile = sourceFile; this.extractionPath = extractionPath; this.permissions = @@ -69,6 +75,31 @@ public LayerEntry( ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS : permissions; + this.lastModifiedTime = lastModifiedTime; + } + + /** + * Instantiates with a source file and the path to place the source file in the container file + * system. + * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 + * for files, 755 for directories) + */ + public LayerEntry( + Path sourceFile, AbsoluteUnixPath extractionPath, @Nullable FilePermissions permissions) { + this(sourceFile, extractionPath, permissions, 1000); + } + + /** + * Returns the modification time of the file in the entry. + * + * @return the modification time + */ + public long getLastModifiedTime() { + return lastModifiedTime; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index e436583ccf..86553bcb06 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -107,6 +107,7 @@ public Blob build() { // Sets the entry's permissions by masking out the permission bits from the entry's mode (the // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. entry.setMode((entry.getMode() & ~0777) | layerEntry.getPermissions().getPermissionBits()); + entry.setModTime(layerEntry.getLastModifiedTime()); uniqueTarArchiveEntries.add(entry); } @@ -120,8 +121,6 @@ public Blob build() { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); for (TarArchiveEntry entry : sortedFilesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. - // 1 second since the epoch (https://github.com/GoogleContainerTools/jib/issues/1079) - entry.setModTime(1000); entry.setGroupId(0); entry.setUserId(0); entry.setUserName(""); From 847b18926814ab0f69bc7e274f63ebbbaa5d4d38 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 22 Mar 2019 21:58:01 -0400 Subject: [PATCH 0515/2020] No need for checking null Maven settings.xml Decryptor (#1573) * No need for checking null Decryptor * Make NullAway ignore @Component * Remove checkNotNull --- jib-maven-plugin/pom.xml | 2 +- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../jib/maven/JibPluginConfiguration.java | 4 +- .../maven/MavenSettingsServerCredentials.java | 71 +++++------------ .../MavenSettingsServerCredentialsTest.java | 79 +++++-------------- 7 files changed, 43 insertions(+), 119 deletions(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 7d768b468a..0bae4b587d 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -368,7 +368,7 @@ -XDcompilePolicy=simple - -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools + -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index ae0ae39ea4..2bd3d9b5be 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -114,7 +114,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( mavenRawConfiguration, new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), + getSession().getSettings(), getSettingsDecrypter()), projectProperties, dockerExecutable, getDockerClientEnvironment(), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 2a0637fb91..c5b15d2739 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -108,7 +108,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( mavenRawConfiguration, new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), + getSession().getSettings(), getSettingsDecrypter()), projectProperties); ProxyProvider.init(getSession().getSettings()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 55ecf4b4c9..209a8237f3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -89,7 +89,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.processCommonConfigurationForTarImage( mavenRawConfiguration, new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter(), eventDispatcher), + getSession().getSettings(), getSettingsDecrypter()), projectProperties, tarOutputPath, mavenHelpfulSuggestionsBuilder.build()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index d4cc638bac..10a91d82ec 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -232,7 +232,7 @@ public File getPath() { @Parameter(defaultValue = "false", property = PropertyNames.SKIP) private boolean skip; - @Nullable @Component protected SettingsDecrypter settingsDecrypter; + @Component protected SettingsDecrypter settingsDecrypter; MavenSession getSession() { return Preconditions.checkNotNull(session); @@ -544,7 +544,7 @@ boolean isSkipped() { } SettingsDecrypter getSettingsDecrypter() { - return Preconditions.checkNotNull(settingsDecrypter); + return settingsDecrypter; } @VisibleForTesting diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 6a816c82a0..7723567e9c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -16,16 +16,10 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.InferredAuthProvider; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; -import com.google.common.annotations.VisibleForTesting; import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -42,25 +36,8 @@ class MavenSettingsServerCredentials implements InferredAuthProvider { static final String CREDENTIAL_SOURCE = "Maven settings"; - // pattern cribbed directly from - // https://github.com/sonatype/plexus-cipher/blob/master/src/main/java/org/sonatype/plexus/components/cipher/DefaultPlexusCipher.java - private static final Pattern ENCRYPTED_STRING_PATTERN = - Pattern.compile(".*?[^\\\\]?\\{(.*?[^\\\\])\\}.*"); - - /** - * Return true if the given string appears to have been encrypted with the Maven - * password encryption. Such passwords appear between unescaped braces. - */ - @VisibleForTesting - static boolean isEncrypted(String password) { - Matcher matcher = ENCRYPTED_STRING_PATTERN.matcher(password); - return matcher.matches() || matcher.find(); - } - private final Settings settings; - @Nullable private final SettingsDecrypter settingsDecrypter; - private final EventDispatcher eventDispatcher; + private final SettingsDecrypter settingsDecrypter; /** * Create new instance. @@ -69,13 +46,9 @@ static boolean isEncrypted(String password) { * @param settingsDecrypter the Maven decrypter component * @param eventDispatcher the Jib event dispatcher */ - MavenSettingsServerCredentials( - Settings settings, - @Nullable SettingsDecrypter settingsDecrypter, - EventDispatcher eventDispatcher) { + MavenSettingsServerCredentials(Settings settings, SettingsDecrypter settingsDecrypter) { this.settings = settings; this.settingsDecrypter = settingsDecrypter; - this.eventDispatcher = eventDispatcher; } /** @@ -92,31 +65,23 @@ public Optional getAuth(String registry) throws InferredAuthRetrie return Optional.empty(); } - if (settingsDecrypter != null) { - // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return - // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers - // can be empty. We handle the results as follows: - // - if there are any ERROR or FATAL problems reported, then decryption failed - // - if no decrypted servers returned then treat as if no decryption was required - SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(registryServer); - SettingsDecryptionResult result = settingsDecrypter.decrypt(request); - // un-encrypted passwords are passed through, so a problem indicates a real issue - for (SettingsProblem problem : result.getProblems()) { - if (problem.getSeverity() == SettingsProblem.Severity.ERROR - || problem.getSeverity() == SettingsProblem.Severity.FATAL) { - throw new InferredAuthRetrievalException( - "Unable to decrypt password for " + registry + ": " + problem); - } - } - if (result.getServer() != null) { - registryServer = result.getServer(); + // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return + // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers + // can be empty. We handle the results as follows: + // - if there are any ERROR or FATAL problems reported, then decryption failed + // - if no decrypted servers returned then treat as if no decryption was required + SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(registryServer); + SettingsDecryptionResult result = settingsDecrypter.decrypt(request); + // un-encrypted passwords are passed through, so a problem indicates a real issue + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new InferredAuthRetrievalException( + "Unable to decrypt password for " + registry + ": " + problem); } - } else if (isEncrypted(registryServer.getPassword())) { - eventDispatcher.dispatch( - LogEvent.warn( - "Server password for registry " - + registry - + " appears to be encrypted, but there is no decrypter available")); + } + if (result.getServer() != null) { + registryServer = result.getServer(); } String username = registryServer.getUsername(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 48b81eeaf3..09e8d77e0c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import java.util.Collections; @@ -41,14 +39,20 @@ public class MavenSettingsServerCredentialsTest { @Mock private Settings mockSettings; @Mock private Server mockServer1; - @Mock private EventDispatcher mockEventDispatcher; + @Mock private SettingsDecrypter mockSettingsDecrypter; + @Mock private SettingsDecryptionResult mockSettingsDecryptionResult; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; @Before public void setUp() { + Mockito.when(mockSettingsDecryptionResult.getProblems()).thenReturn(Collections.emptyList()); + Mockito.when(mockSettingsDecryptionResult.getServer()).thenReturn(mockServer1); + Mockito.when(mockSettingsDecrypter.decrypt(Mockito.any())) + .thenReturn(mockSettingsDecryptionResult); + testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, null, mockEventDispatcher); + new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); } @Test @@ -62,8 +66,6 @@ public void testRetrieve_found() throws InferredAuthRetrievalException { Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); - - Mockito.verifyZeroInteractions(mockEventDispatcher); } @Test @@ -76,33 +78,10 @@ public void testRetrieve_withNullServer() throws InferredAuthRetrievalException Assert.assertFalse(testMavenSettingsServerCredentials.getAuth(null).isPresent()); } - @Test - public void testRetrieve_withNullDecrypter_encrypted() throws InferredAuthRetrievalException { - Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); - Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); - Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}"); - - Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); - Assert.assertTrue(auth.isPresent()); - Assert.assertEquals("server1 username", auth.get().getUsername()); - Assert.assertEquals("{COQLCE6DU6GtcS5P=}", auth.get().getPassword()); - String expectedWarning = - "Server password for registry server1 appears to be encrypted, but there is no decrypter" - + " available"; - Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn(expectedWarning)); - } - @Test public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalException { - SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class); - Mockito.when(mockResult.getProblems()).thenReturn(Collections.emptyList()); - Mockito.when(mockResult.getServer()).thenReturn(mockServer1); - - // don't actually perform encryption/decryption - SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); - Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockEventDispatcher); + new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); @@ -114,9 +93,9 @@ public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalExc Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); - Mockito.verify(mockDecrypter).decrypt(Mockito.any()); - Mockito.verify(mockResult).getProblems(); - Mockito.verify(mockResult, Mockito.atLeastOnce()).getServer(); + Mockito.verify(mockSettingsDecrypter).decrypt(Mockito.any()); + Mockito.verify(mockSettingsDecryptionResult).getProblems(); + Mockito.verify(mockSettingsDecryptionResult, Mockito.atLeastOnce()).getServer(); } @Test @@ -126,15 +105,11 @@ public void testRetrieve_withDecrypter_failure() { Mockito.when(mockProblem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR); // Maven's SettingsProblem has a more structured toString, but irrelevant here Mockito.when(mockProblem.toString()).thenReturn("MockProblemText"); + Mockito.when(mockSettingsDecryptionResult.getProblems()) + .thenReturn(Collections.singletonList(mockProblem)); - SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class); - Mockito.when(mockResult.getProblems()).thenReturn(Collections.singletonList(mockProblem)); - - // return an result with problems - SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class); - Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult); testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockEventDispatcher); + new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); // essentially the same as testRetrieve_found() Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); @@ -145,26 +120,10 @@ public void testRetrieve_withDecrypter_failure() { } catch (InferredAuthRetrievalException ex) { Assert.assertEquals( ex.getMessage(), "Unable to decrypt password for server1: MockProblemText"); - Mockito.verify(mockDecrypter).decrypt(Mockito.any()); - Mockito.verify(mockResult).getProblems(); - Mockito.verifyNoMoreInteractions(mockResult); // getServer() should never be called - } - } - - @Test - public void testIsEncrypted_plaintext() { - Assert.assertFalse(MavenSettingsServerCredentials.isEncrypted("plain text")); - } - - @Test - public void testIsEncrypted_encryptedPayload() { - String examples[] = { - "{COQLCE6DU6GtcS5P=}", - "expires on 2009-04-11 {COQLCE6DU6GtcS5P=}", // with note - "{jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+\\{EF1iFQyJQ=}" // with escaped brace - }; - for (String payload : examples) { - Assert.assertTrue(MavenSettingsServerCredentials.isEncrypted(payload)); + Mockito.verify(mockSettingsDecrypter).decrypt(Mockito.any()); + Mockito.verify(mockSettingsDecryptionResult).getProblems(); + Mockito.verifyNoMoreInteractions( + mockSettingsDecryptionResult); // getServer() should never be called } } } From 6fdc9906587136c62266b9fb0c92fcfd616d18d4 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 25 Mar 2019 15:54:17 -0400 Subject: [PATCH 0516/2020] Change JDWP example to specify localhost (#1580) --- docs/faq.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 4cf90d9e5a..412e48faa6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -382,17 +382,19 @@ Gradle: use `grade --debug -DjibSerialize=true` to enable more detailed logging ### How do I enable debugging? -If using the `distroless/java` base image, then use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept debug connections on port 5005, but not suspend: +If using the `distroless/java` base image, then use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept local debug connections on port 5005, but not suspend: ``` --agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005 ``` -Then connect your debugger to port 5005 on the given host. You can port-forward the container port to a localhost port for easy access. +Then connect your debugger to port 5005 on your local host. You can port-forward the container port to a localhost port for easy access. Using Docker: `docker run -p 5005:5005 ` Using Kubernetes: `kubectl port-forward 5005:5005` +Beware: in Java 8 and earlier, specifying only a port meant that the JDWP socket was open to all incoming connections which is insecure. It is recommended to limit the debug port to localhost. + ### Why is my image created 48 years ago? For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. From ce3732ad4125c66dcc35b9de8f97e31b3523ccdd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 25 Mar 2019 15:54:51 -0400 Subject: [PATCH 0517/2020] Add FAQ item on reproducible builds (#1565) --- docs/faq.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 412e48faa6..46960db638 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -415,6 +415,15 @@ For reproducibility purposes, Jib sets the creation time of the container images jib.container.useCurrentTimestamp = true ``` +#### Please tell me more about reproducibility! + +_Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. + +To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.useCurrentTimestamp=true` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. + +For more details see [reproducible-builds.org](https://reproducible-builds.org). + + ### I would like to run my application with a javaagent. You can run your container with a javaagent by placing it somewhere in the `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. From d69aec50c0dfab07a6b6cf3b77b0056901139c2d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 27 Mar 2019 10:53:10 -0400 Subject: [PATCH 0518/2020] Use JavaContainerBuilder in plugins (#1572) --- .../tools/jib/api/JibContainerBuilder.java | 2 +- .../jib/frontend/JavaLayerConfigurations.java | 2 +- .../tools/jib/frontend/MainClassFinder.java | 4 +- .../tools/jib/gradle/BuildDockerTask.java | 7 +- .../tools/jib/gradle/BuildImageTask.java | 7 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 8 +- .../jib/gradle/GradleLayerConfigurations.java | 201 ------- .../jib/gradle/GradleProjectProperties.java | 129 +++-- .../jib/gradle/GradleRawConfiguration.java | 13 + .../cloud/tools/jib/gradle/TaskCommon.java | 24 + .../gradle/GradleLayerConfigurationsTest.java | 530 ------------------ .../gradle/GradleProjectPropertiesTest.java | 415 +++++++++++++- .../tools/jib/maven/BuildDockerMojo.java | 8 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 8 +- .../jib/maven/MavenLayerConfigurations.java | 172 ------ .../jib/maven/MavenProjectProperties.java | 89 +-- .../jib/maven/MavenRawConfiguration.java | 13 + .../maven/MavenLayerConfigurationsTest.java | 392 ------------- .../jib/maven/MavenProjectPropertiesTest.java | 344 +++++++++++- .../common/JavaContainerBuilderHelper.java | 100 ++++ .../common/JavaLayerConfigurationsHelper.java | 95 ---- .../jib/plugins/common/MainClassResolver.java | 19 +- .../common/PluginConfigurationProcessor.java | 16 +- .../jib/plugins/common/ProjectProperties.java | 16 +- .../jib/plugins/common/RawConfiguration.java | 7 + ...va => JavaContainerBuilderHelperTest.java} | 92 ++- .../plugins/common/MainClassResolverTest.java | 45 +- .../PluginConfigurationProcessorTest.java | 86 ++- 29 files changed, 1230 insertions(+), 1622 deletions(-) delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java delete mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java rename jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/{JavaLayerConfigurationsHelperTest.java => JavaContainerBuilderHelperTest.java} (65%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 6f72a43475..a678b093d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -525,7 +525,7 @@ JibContainer containerize( * @throws IOException if an I/O exception occurs */ @VisibleForTesting - BuildConfiguration toBuildConfiguration( + public BuildConfiguration toBuildConfiguration( Containerizer containerizer, ExecutorService executorService) throws CacheDirectoryCreationException, IOException { buildConfigurationBuilder diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java index 0177d852d0..8b42bccf76 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java @@ -56,7 +56,7 @@ public enum LayerType { } @VisibleForTesting - String getName() { + public String getName() { return name; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 7a28b4414c..55313079ed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -146,8 +146,8 @@ public MethodVisitor visitMethod( * @param files the files to check * @param eventDispatcher used for dispatching log events. */ - public MainClassFinder(ImmutableList files, EventDispatcher eventDispatcher) { - this.files = files; + public MainClassFinder(List files, EventDispatcher eventDispatcher) { + this.files = ImmutableList.copyOf(files); this.eventDispatcher = eventDispatcher; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index b6f0da5614..92c980de00 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -111,12 +111,7 @@ public void buildDocker() PluginConfigurationProcessor.getAppRootChecked( gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); + GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index b0f83d51bb..dec9f1e5f4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -88,12 +88,7 @@ public void buildImage() PluginConfigurationProcessor.getAppRootChecked( gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); + GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 0120991465..0968d5c692 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -110,13 +110,7 @@ public void buildTar() PluginConfigurationProcessor.getAppRootChecked( gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject( - getProject(), - getLogger(), - jibExtension.getExtraDirectory().getPath(), - jibExtension.getExtraDirectory().getPermissions(), - appRoot); - + GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java deleted file mode 100644 index fb6d1392c2..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurations.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; -import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; -import org.gradle.api.logging.Logger; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.SourceSet; - -/** Builds {@link JavaLayerConfigurations} based on inputs from a {@link Project}. */ -class GradleLayerConfigurations { - - /** Name of the `main` {@link SourceSet} to use as source files. */ - private static final String MAIN_SOURCE_SET_NAME = "main"; - - /** - * Resolves the {@link JavaLayerConfigurations} for a Gradle {@link Project}. - * - * @param project the Gradle {@link Project} - * @param logger the logger for providing feedback about the resolution - * @param extraDirectory path to the source directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} - * @throws IOException if an I/O exception occurred during resolution - */ - static JavaLayerConfigurations getForProject( - Project project, - Logger logger, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - if (TaskCommon.getWarTask(project) != null) { - logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); - return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); - } else { - return getForNonWarProject( - project, logger, extraDirectory, extraDirectoryPermissions, appRoot); - } - } - - /** - * Resolves the {@link JavaLayerConfigurations} for a non-WAR Gradle {@link Project}. - * - * @param project the Gradle {@link Project} - * @param logger the logger for providing feedback about the resolution - * @param extraDirectory path to the source directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} - * @throws IOException if an I/O exception occurred during resolution - */ - private static JavaLayerConfigurations getForNonWarProject( - Project project, - Logger logger, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - AbsoluteUnixPath dependenciesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - AbsoluteUnixPath resourcesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - AbsoluteUnixPath classesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - - JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); - - JavaPluginConvention javaPluginConvention = - project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); - - FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs(); - Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); - FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); - FileCollection dependencyFiles = - allFiles - .minus(classesOutputDirectories) - .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); - - // Adds class files. - logger.info("Adding corresponding output directories of source sets to image"); - for (File classesOutputDirectory : classesOutputDirectories) { - if (Files.notExists(classesOutputDirectory.toPath())) { - logger.info("\t'" + classesOutputDirectory + "' (not found, skipped)"); - continue; - } - logger.info("\t'" + classesOutputDirectory + "'"); - layerBuilder.addDirectoryContents( - LayerType.CLASSES, classesOutputDirectory.toPath(), path -> true, classesExtractionPath); - } - if (classesOutputDirectories.filter(File::exists).isEmpty()) { - logger.warn("No classes files were found - did you compile your project?"); - } - - // Adds resource files. - if (Files.exists(resourcesOutputDirectory)) { - layerBuilder.addDirectoryContents( - LayerType.RESOURCES, resourcesOutputDirectory, path -> true, resourcesExtractionPath); - } - - // Adds dependency files. - List duplicates = - dependencyFiles - .getFiles() - .stream() - .map(File::getName) - .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) - .entrySet() - .stream() - .filter(entry -> entry.getValue() > 1) - .map(Entry::getKey) - .collect(Collectors.toList()); - for (File dependencyFile : dependencyFiles) { - if (dependencyFile.exists()) { - boolean isSnapshot = dependencyFile.getName().contains("SNAPSHOT"); - LayerType layerType = isSnapshot ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; - layerBuilder.addFile( - layerType, - dependencyFile.toPath(), - dependenciesExtractionPath.resolve( - duplicates.contains(dependencyFile.getName()) - ? dependencyFile - .getName() - .replaceFirst("\\.jar$", "-" + Files.size(dependencyFile.toPath())) - + ".jar" - : dependencyFile.getName())); - } else { - logger.info("\t'" + dependencyFile + "' (not found, skipped)"); - } - } - - // Adds all the extra files. - if (Files.exists(extraDirectory)) { - layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, - extraDirectory, - path -> true, - AbsoluteUnixPath.get("/"), - extraDirectoryPermissions); - } - - return layerBuilder.build(); - } - - /** - * Resolves the {@link JavaLayerConfigurations} for a WAR Gradle {@link Project}. - * - * @param project the Gradle {@link Project} - * @param extraDirectory path to the source directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return {@link JavaLayerConfigurations} for the layers for the Gradle {@link Project} - * @throws IOException if an I/O exception occurred during resolution - */ - private static JavaLayerConfigurations getForWarProject( - Project project, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); - return JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWarPath, appRoot, extraDirectory, extraDirectoryPermissions); - } - - private GradleLayerConfigurations() {} -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 156110cc5a..141dba3839 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,13 +16,16 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -36,11 +39,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.GradleException; @@ -50,6 +51,7 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ @@ -64,23 +66,13 @@ class GradleProjectProperties implements ProjectProperties { /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'jar' task"; + /** Name of the `main` {@link SourceSet} to use as source files. */ + private static final String MAIN_SOURCE_SET_NAME = "main"; + /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject( - Project project, - Logger logger, - Path extraDirectory, - Map permissions, - AbsoluteUnixPath appRoot) { - try { - return new GradleProjectProperties( - project, - logger, - GradleLayerConfigurations.getForProject( - project, logger, extraDirectory, convertPermissionsMap(permissions), appRoot)); - - } catch (IOException ex) { - throw new GradleException("Obtaining project build output files failed", ex); - } + Project project, Logger logger, AbsoluteUnixPath appRoot) { + return new GradleProjectProperties(project, logger, appRoot); } static Path getExplodedWarDirectory(Project project) { @@ -148,20 +140,86 @@ private static boolean isProgressFooterEnabled(Project project) { private final Project project; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; - private final JavaLayerConfigurations javaLayerConfigurations; + private final Logger logger; + private final AbsoluteUnixPath appRoot; @VisibleForTesting - GradleProjectProperties( - Project project, Logger logger, JavaLayerConfigurations javaLayerConfigurations) { + GradleProjectProperties(Project project, Logger logger, AbsoluteUnixPath appRoot) { this.project = project; - this.javaLayerConfigurations = javaLayerConfigurations; + this.logger = logger; + this.appRoot = appRoot; eventHandlers = makeEventHandlers(project, logger, singleThreadedExecutor); } @Override - public JavaLayerConfigurations getJavaLayerConfigurations() { - return javaLayerConfigurations; + public JibContainerBuilder createContainerBuilder(RegistryImage baseImage) { + try { + if (isWarProject()) { + logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); + Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); + return JavaContainerBuilderHelper.fromExplodedWar(baseImage, explodedWarPath, appRoot); + } + + JavaPluginConvention javaPluginConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = + javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); + + FileCollection classesOutputDirectories = + mainSourceSet.getOutput().getClassesDirs().filter(File::exists); + Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); + FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); + FileCollection dependencyFiles = + allFiles + .minus(classesOutputDirectories) + .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); + + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); + + // Adds resource files + if (Files.exists(resourcesOutputDirectory)) { + javaContainerBuilder.addResources(resourcesOutputDirectory); + } + + // Adds class files + for (File classesOutputDirectory : classesOutputDirectories) { + javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); + } + if (classesOutputDirectories.isEmpty()) { + logger.warn("No classes files were found - did you compile your project?"); + } + + // Adds dependency files + javaContainerBuilder.addDependencies( + dependencyFiles + .getFiles() + .stream() + .filter(File::exists) + .map(File::toPath) + .collect(Collectors.toList())); + + return javaContainerBuilder.toContainerBuilder(); + + } catch (IOException ex) { + throw new GradleException("Obtaining project build output files failed", ex); + } + } + + @Override + public List getClassFiles() throws IOException { + // TODO: Consolidate with createContainerBuilder + JavaPluginConvention javaPluginConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); + FileCollection classesOutputDirectories = + mainSourceSet.getOutput().getClassesDirs().filter(File::exists); + List classFiles = new ArrayList<>(); + for (File classesOutputDirectory : classesOutputDirectories) { + classFiles.addAll(new DirectoryWalker(classesOutputDirectory.toPath()).walk().asList()); + } + return classFiles; } @Override @@ -253,23 +311,4 @@ public int getMajorJavaVersion() { } return Integer.valueOf(version.getMajorVersion()); } - - /** - * Validates and converts a {@code String->String} file-path-to-file-permissions map to an - * equivalent {@code AbsoluteUnixPath->FilePermission} map. - * - * @param stringMap the map to convert (example entry: {@code "/path/on/container" -> "755"}) - * @return the converted map - */ - @VisibleForTesting - static Map convertPermissionsMap( - Map stringMap) { - Map permissionsMap = new HashMap<>(); - for (Entry entry : stringMap.entrySet()) { - AbsoluteUnixPath key = AbsoluteUnixPath.get(entry.getKey()); - FilePermissions value = FilePermissions.fromOctalString(entry.getValue()); - permissionsMap.put(key, value); - } - return permissionsMap; - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 4be98543f2..25d28dbd5d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; @@ -141,4 +144,14 @@ public ImageFormat getImageFormat() { public Optional getProperty(String propertyName) { return Optional.ofNullable(System.getProperty(propertyName)); } + + @Override + public Path getExtraDirectory() { + return jibExtension.getExtraDirectory().getPath(); + } + + @Override + public Map getExtraDirectoryPermissions() { + return TaskCommon.convertPermissionsMap(jibExtension.getExtraDirectory().getPermissions()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 65f47d4ccc..3464a0d197 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -17,6 +17,11 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.common.annotations.VisibleForTesting; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; import javax.annotation.Nullable; import org.gradle.api.Project; @@ -62,5 +67,24 @@ static void disableHttpLogging() { java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); } + /** + * Validates and converts a {@code String->String} file-path-to-file-permissions map to an + * equivalent {@code AbsoluteUnixPath->FilePermission} map. + * + * @param stringMap the map to convert (example entry: {@code "/path/on/container" -> "755"}) + * @return the converted map + */ + @VisibleForTesting + static Map convertPermissionsMap( + Map stringMap) { + Map permissionsMap = new HashMap<>(); + for (Map.Entry entry : stringMap.entrySet()) { + AbsoluteUnixPath key = AbsoluteUnixPath.get(entry.getKey()); + FilePermissions value = FilePermissions.fromOctalString(entry.getValue()); + permissionsMap.put(key, value); + } + return permissionsMap; + } + private TaskCommon() {} } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java deleted file mode 100644 index 35ad7a5be4..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationsTest.java +++ /dev/null @@ -1,530 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.io.Resources; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.file.FileCollection; -import org.gradle.api.internal.file.AbstractFileCollection; -import org.gradle.api.logging.Logger; -import org.gradle.api.plugins.Convention; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.plugins.WarPluginConvention; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.SourceSetOutput; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.api.tasks.TaskDependency; -import org.gradle.api.tasks.bundling.War; -import org.junit.Assert; -import org.junit.Before; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Test for {@link GradleLayerConfigurations}. */ -@RunWith(MockitoJUnitRunner.class) -public class GradleLayerConfigurationsTest { - - /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ - private static class TestFileCollection extends AbstractFileCollection { - - private final Set files; - - private TestFileCollection(Set files) { - this.files = files.stream().map(Path::toFile).collect(Collectors.toSet()); - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public Set getFiles() { - return files; - } - - @Override - public TaskDependency getBuildDependencies() { - return new TaskDependency() { - - @Override - public Set getDependencies(Task task) { - return Collections.emptySet(); - } - }; - } - } - - private static void assertLayerEntriesUnordered( - List expectedPaths, List entries, Function fieldSelector) { - List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); - List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); - Assert.assertEquals(expected, actual); - } - - private static void assertSourcePathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); - } - - private static void assertExtractionPathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered( - expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); - } - - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private Project mockProject; - @Mock private Project mockWebAppProject; - @Mock private Convention mockConvention; - @Mock private JavaPluginConvention mockJavaPluginConvention; - @Mock private WarPluginConvention mockWarPluginConvention; - @Mock private SourceSetContainer mockSourceSetContainer; - @Mock private SourceSet mockMainSourceSet; - @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private Logger mockLogger; - @Mock private TaskContainer taskContainer; - @Mock private War war; - - private Path extraFilesDirectory; - - @Before - public void setUp() throws URISyntaxException, IOException { - Set classesFiles = - ImmutableSet.of(Paths.get(Resources.getResource("gradle/application/classes").toURI())); - FileCollection classesFileCollection = new TestFileCollection(classesFiles); - Path resourcesOutputDir = - Paths.get(Resources.getResource("gradle/application/resources").toURI()); - - Set allFiles = new HashSet<>(classesFiles); - allFiles.add(resourcesOutputDir); - allFiles.add( - Paths.get( - Resources.getResource("gradle/application/dependencies/library.jarC.jar").toURI())); - allFiles.add( - Paths.get(Resources.getResource("gradle/application/dependencies/libraryB.jar").toURI())); - allFiles.add( - Paths.get(Resources.getResource("gradle/application/dependencies/libraryA.jar").toURI())); - allFiles.add( - Paths.get( - Resources.getResource("gradle/application/dependencies/dependency-1.0.0.jar").toURI())); - allFiles.add( - Paths.get( - Resources.getResource("gradle/application/dependencies/more/dependency-1.0.0.jar") - .toURI())); - allFiles.add( - Paths.get( - Resources.getResource( - "gradle/application/dependencies/another/one/dependency-1.0.0.jar") - .toURI())); - allFiles.add( - Paths.get( - Resources.getResource("gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar") - .toURI())); - FileCollection runtimeFileCollection = new TestFileCollection(allFiles); - - Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.getPlugin(JavaPluginConvention.class)) - .thenReturn(mockJavaPluginConvention); - Mockito.when(mockJavaPluginConvention.getSourceSets()).thenReturn(mockSourceSetContainer); - Mockito.when(mockSourceSetContainer.getByName("main")).thenReturn(mockMainSourceSet); - Mockito.when(mockMainSourceSet.getOutput()).thenReturn(mockMainSourceSetOutput); - Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); - Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); - Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - // We can't commit an empty directory in Git, so create (if not exist). - Path emptyDirectory = - Paths.get(Resources.getResource("gradle/webapp").toURI()) - .resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); - Files.createDirectories(emptyDirectory); - - extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); - } - - @Test - public void test_correctFiles() throws URISyntaxException, IOException { - Path applicationDirectory = Paths.get(Resources.getResource("gradle/application").toURI()); - ImmutableList expectedDependenciesFiles = - ImmutableList.of( - applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), - applicationDirectory.resolve("dependencies/more/dependency-1.0.0.jar"), - applicationDirectory.resolve("dependencies/another/one/dependency-1.0.0.jar"), - applicationDirectory.resolve("dependencies/libraryA.jar"), - applicationDirectory.resolve("dependencies/libraryB.jar"), - applicationDirectory.resolve("dependencies/library.jarC.jar")); - ImmutableList expectedSnapshotDependenciesFiles = - ImmutableList.of( - applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); - ImmutableList expectedResourcesFiles = - ImmutableList.of( - applicationDirectory.resolve("resources/resourceA"), - applicationDirectory.resolve("resources/resourceB"), - applicationDirectory.resolve("resources/world")); - ImmutableList expectedClassesFiles = - ImmutableList.of( - applicationDirectory.resolve("classes/HelloWorld.class"), - applicationDirectory.resolve("classes/some.class")); - ImmutableList expectedExtraFiles = ImmutableList.of(); - - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - JavaLayerConfigurations javaLayerConfigurations = - GradleLayerConfigurations.getForProject( - mockProject, - mockLogger, - Paths.get("nonexistent/path"), - Collections.emptyMap(), - appRoot); - assertSourcePathsUnordered( - expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedSnapshotDependenciesFiles, - javaLayerConfigurations.getSnapshotDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedResourcesFiles, javaLayerConfigurations.getResourceLayerEntries()); - assertSourcePathsUnordered( - expectedClassesFiles, javaLayerConfigurations.getClassLayerEntries()); - assertSourcePathsUnordered( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); - } - - @Test - public void test_noClassesFiles() throws IOException { - Path nonexistentFile = Paths.get("/nonexistent/file"); - Mockito.when(mockMainSourceSetOutput.getClassesDirs()) - .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - GradleLayerConfigurations.getForProject( - mockProject, mockLogger, Paths.get("nonexistent/path"), Collections.emptyMap(), appRoot); - - Mockito.verify(mockLogger) - .info("Adding corresponding output directories of source sets to image"); - Mockito.verify(mockLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); - Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); - } - - @Test - public void test_missingDependencyFiles() throws IOException { - Path nonexistentFile = Paths.get("/nonexistent/dependency"); - FileCollection runtimeFileCollection = - new TestFileCollection(Collections.singleton(nonexistentFile)); - Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - GradleLayerConfigurations.getForProject( - mockProject, mockLogger, extraFilesDirectory, Collections.emptyMap(), appRoot); - - Mockito.verify(mockLogger) - .info("Adding corresponding output directories of source sets to image"); - Mockito.verify(mockLogger).info("\t'" + nonexistentFile + "' (not found, skipped)"); - } - - @Test - public void test_extraFiles() throws IOException { - JavaLayerConfigurations javaLayerConfigurations = - GradleLayerConfigurations.getForProject( - mockProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get("/app")); - - ImmutableList expectedExtraFiles = - ImmutableList.of( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), - extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), - extraFilesDirectory.resolve("c/cat"), - extraFilesDirectory.resolve("foo")); - - assertSourcePathsUnordered( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); - } - - @Test - public void testGetForProject_nonDefaultAppRoot() throws IOException { - JavaLayerConfigurations configuration = - GradleLayerConfigurations.getForProject( - mockProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get("/my/app")); - - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/libs/dependency-1.0.0-770.jar", - "/my/app/libs/dependency-1.0.0-200.jar", - "/my/app/libs/dependency-1.0.0-480.jar", - "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar", - "/my/app/libs/library.jarC.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/resources/resourceA", - "/my/app/resources/resourceB", - "/my/app/resources/world"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/my/app/classes/HelloWorld.class", "/my/app/classes/some.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Test - public void testGetForProject_defaultAppRoot() throws IOException { - JavaLayerConfigurations configuration = - GradleLayerConfigurations.getForProject( - mockProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_APP_ROOT)); - - assertExtractionPathsUnordered( - Arrays.asList( - "/app/libs/dependency-1.0.0-770.jar", - "/app/libs/dependency-1.0.0-200.jar", - "/app/libs/dependency-1.0.0-480.jar", - "/app/libs/libraryA.jar", - "/app/libs/libraryB.jar", - "/app/libs/library.jarC.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/app/resources/resourceA", "/app/resources/resourceB", "/app/resources/world"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/app/classes/HelloWorld.class", "/app/classes/some.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Test - public void testWebApp() throws URISyntaxException, IOException { - Path webAppDirectory = Paths.get(Resources.getResource("gradle/webapp").toURI()); - setUpWarProject(webAppDirectory); - - JavaLayerConfigurations configuration = - GradleLayerConfigurations.getForProject( - mockWebAppProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get("/my/app")); - ImmutableList expectedDependenciesFiles = - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")); - ImmutableList expectedSnapshotDependenciesFiles = - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); - ImmutableList expectedResourcesFiles = - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/META-INF"), - webAppDirectory.resolve("jib-exploded-war/META-INF/context.xml"), - webAppDirectory.resolve("jib-exploded-war/Test.jsp"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")); - ImmutableList expectedClassesFiles = - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")); - ImmutableList expectedExtraFiles = - ImmutableList.of( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), - extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), - extraFilesDirectory.resolve("c/cat"), - extraFilesDirectory.resolve("foo")); - - assertSourcePathsUnordered( - expectedDependenciesFiles, configuration.getDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); - assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); - assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); - assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); - - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/META-INF", - "/my/app/META-INF/context.xml", - "/my/app/Test.jsp", - "/my/app/WEB-INF", - "/my/app/WEB-INF/classes", - "/my/app/WEB-INF/classes/empty_dir", - "/my/app/WEB-INF/classes/package", - "/my/app/WEB-INF/classes/package/test.properties", - "/my/app/WEB-INF/lib", - "/my/app/WEB-INF/web.xml"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/WEB-INF/classes/HelloWorld.class", - "/my/app/WEB-INF/classes/empty_dir", - "/my/app/WEB-INF/classes/package", - "/my/app/WEB-INF/classes/package/Other.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Test - public void testWebApp_defaultWebAppRoot() throws URISyntaxException, IOException { - setUpWarProject(Paths.get(Resources.getResource("gradle/webapp").toURI())); - - JavaLayerConfigurations configuration = - GradleLayerConfigurations.getForProject( - mockWebAppProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); - - assertExtractionPathsUnordered( - Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/jetty/webapps/ROOT/META-INF", - "/jetty/webapps/ROOT/META-INF/context.xml", - "/jetty/webapps/ROOT/Test.jsp", - "/jetty/webapps/ROOT/WEB-INF", - "/jetty/webapps/ROOT/WEB-INF/classes", - "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", - "/jetty/webapps/ROOT/WEB-INF/classes/package", - "/jetty/webapps/ROOT/WEB-INF/classes/package/test.properties", - "/jetty/webapps/ROOT/WEB-INF/lib", - "/jetty/webapps/ROOT/WEB-INF/web.xml"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/jetty/webapps/ROOT/WEB-INF/classes/HelloWorld.class", - "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", - "/jetty/webapps/ROOT/WEB-INF/classes/package", - "/jetty/webapps/ROOT/WEB-INF/classes/package/Other.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Test - public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOException { - temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); - setUpWarProject(temporaryFolder.getRoot().toPath()); - - GradleLayerConfigurations.getForProject( - mockWebAppProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass - } - - @Test - public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOException { - temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); - setUpWarProject(temporaryFolder.getRoot().toPath()); - - GradleLayerConfigurations.getForProject( - mockWebAppProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass - } - - @Test - public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOException { - temporaryFolder.newFolder("jib-exploded-war"); - setUpWarProject(temporaryFolder.getRoot().toPath()); - - GradleLayerConfigurations.getForProject( - mockWebAppProject, - mockLogger, - extraFilesDirectory, - Collections.emptyMap(), - AbsoluteUnixPath.get(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT)); // should pass - } - - private void setUpWarProject(Path webAppDirectory) { - Mockito.when(mockWebAppProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); - Mockito.when(mockWebAppProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockWebAppProject); - Mockito.when(mockWebAppProject.getTasks()).thenReturn(taskContainer); - Mockito.when(taskContainer.findByName("war")).thenReturn(war); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index a5e0eaaa39..da4a7cbf3f 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,15 +16,40 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.gradle.StartParameter; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.internal.file.AbstractFileCollection; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.invocation.Gradle; import org.gradle.api.java.archives.Manifest; @@ -34,14 +59,20 @@ import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.WarPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.SourceSetOutput; import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskDependency; import org.gradle.api.tasks.bundling.War; import org.gradle.jvm.tasks.Jar; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; +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.Mockito; @@ -51,39 +82,145 @@ @RunWith(MockitoJUnitRunner.class) public class GradleProjectPropertiesTest { + /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ + private static class TestFileCollection extends AbstractFileCollection { + + private final Set files; + + private TestFileCollection(Set files) { + this.files = files.stream().map(Path::toFile).collect(Collectors.toSet()); + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public Set getFiles() { + return files; + } + + @Override + public TaskDependency getBuildDependencies() { + return task -> Collections.emptySet(); + } + } + + /** Helper for reading back layers in a {@code BuildConfiguration}. */ + private static class ContainerBuilderLayers { + + private final List resourcesLayerEntries; + private final List classesLayerEntries; + private final List dependenciesLayerEntries; + private final List snapshotsLayerEntries; + + private ContainerBuilderLayers(BuildConfiguration configuration) { + resourcesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); + classesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + dependenciesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + snapshotsLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + } + } + + private static List getLayerConfigurationsByName( + BuildConfiguration buildConfiguration, String name) { + return buildConfiguration + .getLayerConfigurations() + .stream() + .filter(layer -> layer.getName().equals(name)) + .collect(Collectors.toList()); + } + + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); + } + + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); + } + + private static Path getResource(String path) throws URISyntaxException { + return Paths.get(Resources.getResource(path).toURI()); + } + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private FileResolver mockFileResolver; @Mock private Jar mockJar; @Mock private Jar mockJar2; @Mock private Project mockProject; @Mock private Convention mockConvention; - @Mock private WarPluginConvention mockWarPluginConvection; + @Mock private WarPluginConvention mockWarPluginConvention; @Mock private TaskContainer mockTaskContainer; - @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Logger mockLogger; @Mock private Gradle mockGradle; @Mock private StartParameter mockStartParameter; @Mock private JavaPluginConvention mockJavaPluginConvention; + @Mock private SourceSetContainer mockSourceSetContainer; + @Mock private SourceSet mockMainSourceSet; + @Mock private SourceSetOutput mockMainSourceSetOutput; + @Mock private TaskContainer taskContainer; + @Mock private War war; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @Before - public void setup() { + public void setup() throws URISyntaxException, IOException { manifest = new DefaultManifest(mockFileResolver); Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(mockWarPluginConvection); - Mockito.when(mockWarPluginConvection.getProject()).thenReturn(mockProject); + Mockito.when(mockConvention.getPlugin(JavaPluginConvention.class)) + .thenReturn(mockJavaPluginConvention); + Mockito.when(mockJavaPluginConvention.getSourceSets()).thenReturn(mockSourceSetContainer); Mockito.when(mockProject.getTasks()).thenReturn(mockTaskContainer); - Mockito.when(mockTaskContainer.findByName("war")).thenReturn(Mockito.mock(War.class)); Mockito.when(mockJar.getManifest()).thenReturn(manifest); - Mockito.when(mockProject.getGradle()).thenReturn(mockGradle); Mockito.when(mockGradle.getStartParameter()).thenReturn(mockStartParameter); Mockito.when(mockStartParameter.getConsoleOutput()).thenReturn(ConsoleOutput.Auto); + Set classesFiles = ImmutableSet.of(getResource("gradle/application/classes")); + FileCollection classesFileCollection = new TestFileCollection(classesFiles); + Path resourcesOutputDir = getResource("gradle/application/resources"); + + Set allFiles = new HashSet<>(classesFiles); + allFiles.add(resourcesOutputDir); + allFiles.add(getResource("gradle/application/dependencies/library.jarC.jar")); + allFiles.add(getResource("gradle/application/dependencies/libraryB.jar")); + allFiles.add(getResource("gradle/application/dependencies/libraryA.jar")); + allFiles.add(getResource("gradle/application/dependencies/dependency-1.0.0.jar")); + allFiles.add(getResource("gradle/application/dependencies/more/dependency-1.0.0.jar")); + allFiles.add(getResource("gradle/application/dependencies/another/one/dependency-1.0.0.jar")); + allFiles.add(getResource("gradle/application/dependencies/dependencyX-1.0.0-SNAPSHOT.jar")); + FileCollection runtimeFileCollection = new TestFileCollection(allFiles); + + Mockito.when(mockSourceSetContainer.getByName("main")).thenReturn(mockMainSourceSet); + Mockito.when(mockMainSourceSet.getOutput()).thenReturn(mockMainSourceSetOutput); + Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); + Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); + Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); + + // We can't commit an empty directory in Git, so create (if not exist). + Path emptyDirectory = + getResource("gradle/webapp").resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); + Files.createDirectories(emptyDirectory); + gradleProjectProperties = - new GradleProjectProperties(mockProject, mockLogger, mockJavaLayerConfigurations); + new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get("/app")); } @Test @@ -108,17 +245,20 @@ public void testGetMainClassFromJar_multiple() { } @Test - public void testIsWarProject() { + public void testIsWarProject() throws URISyntaxException { + setUpWarProject(getResource("gradle/webapp")); Assert.assertTrue(gradleProjectProperties.isWarProject()); } @Test - public void testGetWar_warProject() { + public void testGetWar_warProject() throws URISyntaxException { + setUpWarProject(getResource("gradle/webapp")); Assert.assertNotNull(TaskCommon.getWarTask(mockProject)); } @Test - public void testGetWar_noWarPlugin() { + public void testGetWar_noWarPlugin() throws URISyntaxException { + setUpWarProject(getResource("gradle/webapp")); Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)).thenReturn(null); Assert.assertNull(TaskCommon.getWarTask(mockProject)); @@ -126,8 +266,6 @@ public void testGetWar_noWarPlugin() { @Test public void testGetWar_noWarTask() { - Mockito.when(mockTaskContainer.findByName("war")).thenReturn(null); - Assert.assertNull(TaskCommon.getWarTask(mockProject)); } @@ -139,12 +277,11 @@ public void testConvertPermissionsMap() { FilePermissions.fromOctalString("123"), AbsoluteUnixPath.get("/test/file2"), FilePermissions.fromOctalString("456")), - GradleProjectProperties.convertPermissionsMap( + TaskCommon.convertPermissionsMap( ImmutableMap.of("/test/folder/file1", "123", "/test/file2", "456"))); try { - GradleProjectProperties.convertPermissionsMap( - ImmutableMap.of("a path", "not valid permission")); + TaskCommon.convertPermissionsMap(ImmutableMap.of("a path", "not valid permission")); Assert.fail(); } catch (IllegalArgumentException ignored) { // pass @@ -182,4 +319,248 @@ public void testGetMajorJavaVersion_jvm11() { Assert.assertEquals(11, gradleProjectProperties.getMajorJavaVersion()); } + + @Test + public void testCreateContainerBuilder_correctFiles() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + BuildConfiguration configuration = setupBuildConfiguration("/app"); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + + Path applicationDirectory = getResource("gradle/application"); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("dependencies/dependencyX-1.0.0-SNAPSHOT.jar")), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("dependencies/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/more/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/another/one/dependency-1.0.0.jar"), + applicationDirectory.resolve("dependencies/libraryA.jar"), + applicationDirectory.resolve("dependencies/libraryB.jar"), + applicationDirectory.resolve("dependencies/library.jarC.jar")), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("resources/resourceA"), + applicationDirectory.resolve("resources/resourceB"), + applicationDirectory.resolve("resources/world")), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("classes/HelloWorld.class"), + applicationDirectory.resolve("classes/some.class")), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageReferenceException { + Path nonexistentFile = Paths.get("/nonexistent/file"); + Mockito.when(mockMainSourceSetOutput.getClassesDirs()) + .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); + gradleProjectProperties.createContainerBuilder(RegistryImage.named("base")); + Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); + } + + @Test + public void testCreateContainerBuilder_nonDefaultAppRoot() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/libs/dependency-1.0.0-770.jar", + "/my/app/libs/dependency-1.0.0-200.jar", + "/my/app/libs/dependency-1.0.0-480.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar", + "/my/app/libs/library.jarC.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/my/app/classes/HelloWorld.class", "/my/app/classes/some.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_defaultAppRoot() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + BuildConfiguration configuration = + setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_APP_ROOT); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + assertExtractionPathsUnordered( + Arrays.asList( + "/app/libs/dependency-1.0.0-770.jar", + "/app/libs/dependency-1.0.0-200.jar", + "/app/libs/dependency-1.0.0-480.jar", + "/app/libs/libraryA.jar", + "/app/libs/libraryB.jar", + "/app/libs/library.jarC.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/app/resources/resourceA", "/app/resources/resourceB", "/app/resources/world"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/app/classes/HelloWorld.class", "/app/classes/some.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_war() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Path webAppDirectory = getResource("gradle/webapp"); + setUpWarProject(webAppDirectory); + + BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + assertSourcePathsUnordered( + ImmutableList.of( + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + webAppDirectory.resolve("jib-exploded-war/META-INF"), + webAppDirectory.resolve("jib-exploded-war/META-INF/context.xml"), + webAppDirectory.resolve("jib-exploded-war/Test.jsp"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), + webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")), + layers.classesLayerEntries.get(0).getLayerEntries()); + + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/META-INF", + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", + "/my/app/WEB-INF/web.xml"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/Other.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_defaultWebAppRoot() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + setUpWarProject(getResource("gradle/webapp")); + + BuildConfiguration configuration = + setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + assertExtractionPathsUnordered( + Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/jetty/webapps/ROOT/META-INF", + "/jetty/webapps/ROOT/META-INF/context.xml", + "/jetty/webapps/ROOT/Test.jsp", + "/jetty/webapps/ROOT/WEB-INF", + "/jetty/webapps/ROOT/WEB-INF/classes", + "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", + "/jetty/webapps/ROOT/WEB-INF/classes/package", + "/jetty/webapps/ROOT/WEB-INF/classes/package/test.properties", + "/jetty/webapps/ROOT/WEB-INF/lib", + "/jetty/webapps/ROOT/WEB-INF/web.xml"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/jetty/webapps/ROOT/WEB-INF/classes/HelloWorld.class", + "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", + "/jetty/webapps/ROOT/WEB-INF/classes/package", + "/jetty/webapps/ROOT/WEB-INF/classes/package/Other.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("jib-exploded-war"); + setUpWarProject(temporaryFolder.getRoot().toPath()); + setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + } + + private BuildConfiguration setupBuildConfiguration(String appRoot) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + return new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get(appRoot)) + .createContainerBuilder(RegistryImage.named("base")) + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("to")), MoreExecutors.newDirectExecutorService()); + } + + private void setUpWarProject(Path webAppDirectory) { + Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); + Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) + .thenReturn(mockWarPluginConvention); + Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockProject); + Mockito.when(mockProject.getTasks()).thenReturn(taskContainer); + Mockito.when(taskContainer.findByName("war")).thenReturn(war); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 2bd3d9b5be..a7d08f76a2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -97,13 +97,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.getAppRootChecked( mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject( - getProject(), - getSession(), - getLog(), - MojoCommon.getExtraDirectoryPath(this), - MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c5b15d2739..d852deb24b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -94,13 +94,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.getAppRootChecked( mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject( - getProject(), - getSession(), - getLog(), - MojoCommon.getExtraDirectoryPath(this), - MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 209a8237f3..da3c070241 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -70,13 +70,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { PluginConfigurationProcessor.getAppRootChecked( mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject( - getProject(), - getSession(), - getLog(), - MojoCommon.getExtraDirectoryPath(this), - MojoCommon.convertPermissionsList(getExtraDirectoryPermissions()), - appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java deleted file mode 100644 index 07f0911be7..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurations.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; -import com.google.cloud.tools.jib.plugins.common.JavaLayerConfigurationsHelper; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import org.apache.maven.artifact.Artifact; -import org.apache.maven.project.MavenProject; - -/** Builds {@link JavaLayerConfigurations} based on inputs from a {@link MavenProject}. */ -class MavenLayerConfigurations { - - /** - * Resolves the {@link JavaLayerConfigurations} for a {@link MavenProject}. - * - * @param project the {@link MavenProject} - * @param extraDirectory path to the directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the project - * @throws IOException if collecting the project files fails - */ - static JavaLayerConfigurations getForProject( - MavenProject project, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - if (MojoCommon.isWarProject(project)) { - return getForWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); - } else { - return getForNonWarProject(project, extraDirectory, extraDirectoryPermissions, appRoot); - } - } - - /** - * Resolves the {@link JavaLayerConfigurations} for a non-WAR {@link MavenProject}. - * - * @param project the {@link MavenProject} - * @param extraDirectory path to the directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the project - * @throws IOException if collecting the project files fails - */ - private static JavaLayerConfigurations getForNonWarProject( - MavenProject project, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - - AbsoluteUnixPath dependenciesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE); - AbsoluteUnixPath resourcesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE); - AbsoluteUnixPath classesExtractionPath = - appRoot.resolve(JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE); - - JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); - - // Gets all the dependencies. - List duplicates = - project - .getArtifacts() - .stream() - .map(Artifact::getFile) - .map(File::getName) - .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) - .entrySet() - .stream() - .filter(entry -> entry.getValue() > 1) - .map(Entry::getKey) - .collect(Collectors.toList()); - for (Artifact artifact : project.getArtifacts()) { - Path artifactPath = artifact.getFile().toPath(); - LayerType layerType = - artifact.isSnapshot() ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES; - String filename = artifactPath.getFileName().toString(); - layerBuilder.addFile( - layerType, - artifactPath, - dependenciesExtractionPath.resolve( - duplicates.contains(filename) - ? filename.replaceFirst("\\.jar$", "-" + Files.size(artifactPath)) + ".jar" - : filename)); - } - - Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); - - // Gets the classes files in the 'classes' output directory. - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - layerBuilder.addDirectoryContents( - LayerType.CLASSES, classesOutputDirectory, isClassFile, classesExtractionPath); - - // Gets the resources files in the 'classes' output directory. - layerBuilder.addDirectoryContents( - LayerType.RESOURCES, classesOutputDirectory, isClassFile.negate(), resourcesExtractionPath); - - // Adds all the extra files. - if (Files.exists(extraDirectory)) { - layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, - extraDirectory, - path -> true, - AbsoluteUnixPath.get("/"), - extraDirectoryPermissions); - } - - return layerBuilder.build(); - } - - /** - * Resolves the {@link JavaLayerConfigurations} for a WAR {@link MavenProject}. - * - * @param project the {@link MavenProject} - * @param extraDirectory path to the directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions for extra-layer - * files - * @param appRoot root directory in the image where the app will be placed - * @return a {@link JavaLayerConfigurations} for the project - * @throws IOException if collecting the project files fails - */ - private static JavaLayerConfigurations getForWarProject( - MavenProject project, - Path extraDirectory, - Map extraDirectoryPermissions, - AbsoluteUnixPath appRoot) - throws IOException { - - // TODO explode the WAR file rather than using this directory. The contents of the final WAR may - // be different from this directory (it's possible to include or exclude files when packaging a - // WAR). Also the exploded WAR directory is configurable with and may not be - // at build.getFinalName(). - Path explodedWarPath = - Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - return JavaLayerConfigurationsHelper.fromExplodedWar( - explodedWarPath, appRoot, extraDirectory, extraDirectoryPermissions); - } - - private MavenLayerConfigurations() {} -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index d975f4c0b5..ad34a38c44 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,13 +16,16 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; @@ -31,14 +34,17 @@ import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Map; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Plugin; -import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.Os; @@ -63,34 +69,12 @@ public class MavenProjectProperties implements ProjectProperties { * @param project the {@link MavenProject} for the plugin. * @param session the {@link MavenSession} for the plugin. * @param log the Maven {@link Log} to log messages during Jib execution - * @param extraDirectory path to the directory for the extra files layer - * @param permissions map from path on container to file permissions for extra-layer files * @param appRoot root directory in the image where the app will be placed * @return a MavenProjectProperties from the given project and logger. - * @throws MojoExecutionException if no class files are found in the output directory. */ static MavenProjectProperties getForProject( - MavenProject project, - MavenSession session, - Log log, - Path extraDirectory, - Map permissions, - AbsoluteUnixPath appRoot) - throws MojoExecutionException { - try { - return new MavenProjectProperties( - project, - session, - log, - MavenLayerConfigurations.getForProject(project, extraDirectory, permissions, appRoot)); - - } catch (IOException ex) { - throw new MojoExecutionException( - "Obtaining project build output files failed; make sure you have compiled your project " - + "before trying to build the image. (Did you accidentally run \"mvn clean " - + "jib:build\" instead of \"mvn clean compile jib:build\"?)", - ex); - } + MavenProject project, MavenSession session, Log log, AbsoluteUnixPath appRoot) { + return new MavenProjectProperties(project, session, log, appRoot); } /** @@ -200,23 +184,54 @@ static int getVersionFromString(String versionString) { private final MavenProject project; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; - private final JavaLayerConfigurations javaLayerConfigurations; + private final AbsoluteUnixPath appRoot; @VisibleForTesting MavenProjectProperties( - MavenProject project, - MavenSession session, - Log log, - JavaLayerConfigurations javaLayerConfigurations) { + MavenProject project, MavenSession session, Log log, AbsoluteUnixPath appRoot) { this.project = project; - this.javaLayerConfigurations = javaLayerConfigurations; - + this.appRoot = appRoot; eventHandlers = makeEventHandlers(session, log, singleThreadedExecutor); } @Override - public JavaLayerConfigurations getJavaLayerConfigurations() { - return javaLayerConfigurations; + public JibContainerBuilder createContainerBuilder(RegistryImage baseImage) throws IOException { + try { + if (isWarProject()) { + Path explodedWarPath = + Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); + return JavaContainerBuilderHelper.fromExplodedWar(baseImage, explodedWarPath, appRoot); + } + + Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + + // Add dependencies, resources, and classes + return JavaContainerBuilder.from(baseImage) + .setAppRoot(appRoot) + .addResources(classesOutputDirectory, isClassFile.negate()) + .addClasses(classesOutputDirectory, isClassFile) + .addDependencies( + project + .getArtifacts() + .stream() + .map(Artifact::getFile) + .map(File::toPath) + .collect(Collectors.toList())) + .toContainerBuilder(); + + } catch (IOException ex) { + throw new IOException( + "Obtaining project build output files failed; make sure you have compiled your project " + + "before trying to build the image. (Did you accidentally run \"mvn clean " + + "jib:build\" instead of \"mvn clean compile jib:build\"?)", + ex); + } + } + + @Override + public List getClassFiles() throws IOException { + return new DirectoryWalker(Paths.get(project.getBuild().getOutputDirectory())).walk().asList(); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 0b9c901de6..a94313f4e5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; @@ -146,4 +149,14 @@ public ImageFormat getImageFormat() { public Optional getProperty(String propertyName) { return Optional.ofNullable(jibPluginConfiguration.getProperty(propertyName)); } + + @Override + public Path getExtraDirectory() { + return MojoCommon.getExtraDirectoryPath(jibPluginConfiguration); + } + + @Override + public Map getExtraDirectoryPermissions() { + return MojoCommon.convertPermissionsList(jibPluginConfiguration.getExtraDirectoryPermissions()); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java deleted file mode 100644 index 56b0081ec4..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenLayerConfigurationsTest.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.maven.artifact.Artifact; -import org.apache.maven.model.Build; -import org.apache.maven.project.MavenProject; -import org.junit.Assert; -import org.junit.Before; -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.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link MavenLayerConfigurations}. */ -@RunWith(MockitoJUnitRunner.class) -public class MavenLayerConfigurationsTest { - - private static void assertLayerEntriesUnordered( - List expectedPaths, List entries, Function fieldSelector) { - List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); - List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); - Assert.assertEquals(expected, actual); - } - - private static void assertSourcePathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); - } - - private static void assertExtractionPathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered( - expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); - } - - private static void assertNonDefaultAppRoot(JavaLayerConfigurations configuration) { - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/libs/dependency-1.0.0-770.jar", - "/my/app/libs/dependency-1.0.0-200.jar", - "/my/app/libs/dependency-1.0.0-480.jar", - "/my/app/libs/libraryA.jar", - "/my/app/libs/libraryB.jar", - "/my/app/libs/library.jarC.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/resources/directory", - "/my/app/resources/directory/somefile", - "/my/app/resources/package", - "/my/app/resources/resourceA", - "/my/app/resources/resourceB", - "/my/app/resources/world"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/classes/HelloWorld.class", - "/my/app/classes/directory", - "/my/app/classes/package", - "/my/app/classes/package/some.class", - "/my/app/classes/some.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Rule public final TestRepository testRepository = new TestRepository(); - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Mock private MavenProject mockMavenProject; - @Mock private Build mockBuild; - - private Path extraFilesDirectory; - - @Before - public void setUp() throws URISyntaxException, IOException { - Path outputPath = Paths.get(Resources.getResource("maven/application/output").toURI()); - Path dependenciesPath = - Paths.get(Resources.getResource("maven/application/dependencies").toURI()); - - Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); - Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); - - Set artifacts = - ImmutableSet.of( - makeArtifact(dependenciesPath.resolve("library.jarC.jar")), - makeArtifact(dependenciesPath.resolve("libraryB.jar")), - makeArtifact(dependenciesPath.resolve("libraryA.jar")), - makeArtifact(dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar")), - makeArtifact( - dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar")), - // Maven reads and populates "Artifacts" with its own processing, so read some from a - // repository - testRepository.findArtifact("com.test", "dependency", "1.0.0"), - testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); - Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); - - Path emptyDirectory = - Paths.get(Resources.getResource("maven/webapp").toURI()) - .resolve("final-name/WEB-INF/classes/empty_dir"); - Files.createDirectories(emptyDirectory); - - extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); - } - - @Test - public void test_correctFiles() throws URISyntaxException, IOException { - Path dependenciesPath = - Paths.get(Resources.getResource("maven/application/dependencies").toURI()); - ImmutableList expectedDependenciesFiles = - ImmutableList.of( - testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), - dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar"), - dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar"), - dependenciesPath.resolve("libraryA.jar"), - dependenciesPath.resolve("libraryB.jar"), - dependenciesPath.resolve("library.jarC.jar")); - ImmutableList expectedSnapshotDependenciesFiles = - ImmutableList.of( - testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")); - Path applicationDirectory = Paths.get(Resources.getResource("maven/application").toURI()); - ImmutableList expectedResourcesFiles = - ImmutableList.of( - applicationDirectory.resolve("output/directory"), - applicationDirectory.resolve("output/directory/somefile"), - applicationDirectory.resolve("output/package"), - applicationDirectory.resolve("output/resourceA"), - applicationDirectory.resolve("output/resourceB"), - applicationDirectory.resolve("output/world")); - ImmutableList expectedClassesFiles = - ImmutableList.of( - applicationDirectory.resolve("output/HelloWorld.class"), - applicationDirectory.resolve("output/directory"), - applicationDirectory.resolve("output/package"), - applicationDirectory.resolve("output/package/some.class"), - applicationDirectory.resolve("output/some.class")); - - JavaLayerConfigurations javaLayerConfigurations = - MavenLayerConfigurations.getForProject( - mockMavenProject, - Paths.get("nonexistent/path"), - Collections.emptyMap(), - AbsoluteUnixPath.get("/app")); - assertSourcePathsUnordered( - expectedDependenciesFiles, javaLayerConfigurations.getDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedSnapshotDependenciesFiles, - javaLayerConfigurations.getSnapshotDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedResourcesFiles, javaLayerConfigurations.getResourceLayerEntries()); - assertSourcePathsUnordered( - expectedClassesFiles, javaLayerConfigurations.getClassLayerEntries()); - } - - @Test - public void test_extraFiles() throws IOException { - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - JavaLayerConfigurations javaLayerConfigurations = - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); - - ImmutableList expectedExtraFiles = - ImmutableList.of( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), - extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), - extraFilesDirectory.resolve("c/cat"), - extraFilesDirectory.resolve("foo")); - - assertSourcePathsUnordered( - expectedExtraFiles, javaLayerConfigurations.getExtraFilesLayerEntries()); - } - - @Test - public void testGetForProject_nonDefaultAppRoot() throws IOException { - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); - - assertNonDefaultAppRoot(configuration); - } - - private Artifact makeArtifact(Path path) { - Artifact artifact = Mockito.mock(Artifact.class); - Mockito.when(artifact.getFile()).thenReturn(path.toFile()); - return artifact; - } - - @Test - public void testGetForWarProject_nonDefaultAppRoot() throws URISyntaxException, IOException { - Path outputPath = Paths.get(Resources.getResource("maven/webapp").toURI()); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); - - ImmutableList expectedDependenciesFiles = - ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")); - ImmutableList expectedSnapshotDependenciesFiles = - ImmutableList.of( - outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")); - ImmutableList expectedResourcesFiles = - ImmutableList.of( - outputPath.resolve("final-name/META-INF"), - outputPath.resolve("final-name/META-INF/context.xml"), - outputPath.resolve("final-name/Test.jsp"), - outputPath.resolve("final-name/WEB-INF"), - outputPath.resolve("final-name/WEB-INF/classes"), - outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), - outputPath.resolve("final-name/WEB-INF/classes/package"), - outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), - outputPath.resolve("final-name/WEB-INF/lib"), - outputPath.resolve("final-name/WEB-INF/web.xml")); - ImmutableList expectedClassesFiles = - ImmutableList.of( - outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), - outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), - outputPath.resolve("final-name/WEB-INF/classes/package"), - outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")); - ImmutableList expectedExtraFiles = - ImmutableList.of( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), - extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), - extraFilesDirectory.resolve("c/cat"), - extraFilesDirectory.resolve("foo")); - - assertSourcePathsUnordered( - expectedDependenciesFiles, configuration.getDependencyLayerEntries()); - assertSourcePathsUnordered( - expectedSnapshotDependenciesFiles, configuration.getSnapshotDependencyLayerEntries()); - assertSourcePathsUnordered(expectedResourcesFiles, configuration.getResourceLayerEntries()); - assertSourcePathsUnordered(expectedClassesFiles, configuration.getClassLayerEntries()); - assertSourcePathsUnordered(expectedExtraFiles, configuration.getExtraFilesLayerEntries()); - - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), - configuration.getDependencyLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/META-INF", - "/my/app/META-INF/context.xml", - "/my/app/Test.jsp", - "/my/app/WEB-INF", - "/my/app/WEB-INF/classes", - "/my/app/WEB-INF/classes/empty_dir", - "/my/app/WEB-INF/classes/package", - "/my/app/WEB-INF/classes/package/test.properties", - "/my/app/WEB-INF/lib", - "/my/app/WEB-INF/web.xml"), - configuration.getResourceLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/my/app/WEB-INF/classes/HelloWorld.class", - "/my/app/WEB-INF/classes/empty_dir", - "/my/app/WEB-INF/classes/package", - "/my/app/WEB-INF/classes/package/Other.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); - } - - @Test - public void testGetForJarProject_nonDefaultAppRoot() throws IOException { - // Test when the default packaging is set - Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - JavaLayerConfigurations configuration = - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); - - assertNonDefaultAppRoot(configuration); - } - - @Test - public void testGetForWarProject_noErrorIfWebInfDoesNotExist() throws IOException { - temporaryFolder.newFolder("final-name"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass - } - - @Test - public void testGetForWarProject_noErrorIfWebInfLibDoesNotExist() throws IOException { - temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass - } - - @Test - public void testGetForWarProject_noErrorIfWebInfClassesDoesNotExist() throws IOException { - temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/my/app"); - - MavenLayerConfigurations.getForProject( - mockMavenProject, extraFilesDirectory, Collections.emptyMap(), appRoot); // should pass - } - - @Test - public void testIsWarProject_WarPackagingIsWar() { - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - - Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); - } - - @Test - public void testIsWarProject_GwtAppPackagingIsWar() { - Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-app"); - - Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); - } - - @Test - public void testIsWarProject_JarPackagingIsNotWar() { - Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - - Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); - } - - @Test - public void testIsWarProject_GwtLibPackagingIsNotWar() { - Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-lib"); - - Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 4e410b8402..80d56dae49 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,17 +16,44 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Properties; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.junit.Assert; import org.junit.Before; +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.Mockito; @@ -36,11 +63,98 @@ @RunWith(MockitoJUnitRunner.class) public class MavenProjectPropertiesTest { + /** Helper for reading back layers in a {@code BuildConfiguration}. */ + private static class ContainerBuilderLayers { + + private final List resourcesLayerEntries; + private final List classesLayerEntries; + private final List dependenciesLayerEntries; + private final List snapshotsLayerEntries; + + private ContainerBuilderLayers(BuildConfiguration configuration) { + resourcesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); + classesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + dependenciesLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + snapshotsLayerEntries = + getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + } + } + + private static List getLayerConfigurationsByName( + BuildConfiguration buildConfiguration, String name) { + return buildConfiguration + .getLayerConfigurations() + .stream() + .filter(layer -> layer.getName().equals(name)) + .collect(Collectors.toList()); + } + + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); + } + + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); + } + + private static void assertNonDefaultAppRoot(BuildConfiguration configuration) { + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/libs/dependency-1.0.0-770.jar", + "/my/app/libs/dependency-1.0.0-200.jar", + "/my/app/libs/dependency-1.0.0-480.jar", + "/my/app/libs/libraryA.jar", + "/my/app/libs/libraryB.jar", + "/my/app/libs/library.jarC.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/resources/directory", + "/my/app/resources/directory/somefile", + "/my/app/resources/package", + "/my/app/resources/resourceA", + "/my/app/resources/resourceB", + "/my/app/resources/world"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/classes/HelloWorld.class", + "/my/app/classes/directory", + "/my/app/classes/package", + "/my/app/classes/package/some.class", + "/my/app/classes/some.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + private static Path getResource(String path) throws URISyntaxException { + return Paths.get(Resources.getResource(path).toURI()); + } + + @Rule public final TestRepository testRepository = new TestRepository(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock private Build mockBuild; @Mock private MavenProject mockMavenProject; @Mock private MavenSession mockMavenSession; @Mock private MavenExecutionRequest mockMavenRequest; @Mock private Properties mockMavenProperties; - @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Mock private Plugin mockJarPlugin; @Mock private Plugin mockCompilerPlugin; @Mock private Log mockLog; @@ -57,19 +171,49 @@ public class MavenProjectPropertiesTest { private MavenProjectProperties mavenProjectProperties; @Before - public void setup() { + public void setup() throws IOException, URISyntaxException { Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = new MavenProjectProperties( - mockMavenProject, mockMavenSession, mockLog, mockJavaLayerConfigurations); + mockMavenProject, mockMavenSession, mockLog, AbsoluteUnixPath.get("/app")); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); jarPluginMainClass = new Xpp3Dom("mainClass"); + Path outputPath = getResource("maven/application/output"); + Path dependenciesPath = getResource("maven/application/dependencies"); + + Mockito.when(mockMavenProject.getBuild()).thenReturn(mockBuild); + Mockito.when(mockBuild.getOutputDirectory()).thenReturn(outputPath.toString()); + + Set artifacts = + ImmutableSet.of( + makeArtifact(dependenciesPath.resolve("library.jarC.jar")), + makeArtifact(dependenciesPath.resolve("libraryB.jar")), + makeArtifact(dependenciesPath.resolve("libraryA.jar")), + makeArtifact(dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar")), + makeArtifact( + dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar")), + // Maven reads and populates "Artifacts" with its own processing, so read some from a + // repository + testRepository.findArtifact("com.test", "dependency", "1.0.0"), + testRepository.findArtifact("com.test", "dependencyX", "1.0.0-SNAPSHOT")); + Mockito.when(mockMavenProject.getArtifacts()).thenReturn(artifacts); + + Path emptyDirectory = + getResource("maven/webapp").resolve("final-name/WEB-INF/classes/empty_dir"); + Files.createDirectories(emptyDirectory); + Mockito.when(mockMavenProject.getProperties()).thenReturn(mockMavenProperties); } + private Artifact makeArtifact(Path path) { + Artifact artifact = Mockito.mock(Artifact.class); + Mockito.when(artifact.getFile()).thenReturn(path.toFile()); + return artifact; + } + @Test public void testGetMainClassFromJar_success() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) @@ -212,4 +356,196 @@ public void isProgressFooterEnabled() { Mockito.when(mockMavenRequest.isInteractiveMode()).thenReturn(false); Assert.assertFalse(MavenProjectProperties.isProgressFooterEnabled(mockMavenSession)); } + + @Test + public void testCreateContainerBuilder_correctFiles() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/app")); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + + Path dependenciesPath = getResource("maven/application/dependencies"); + Path applicationDirectory = getResource("maven/application"); + assertSourcePathsUnordered( + ImmutableList.of( + testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), + dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar"), + dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar"), + dependenciesPath.resolve("libraryA.jar"), + dependenciesPath.resolve("libraryB.jar"), + dependenciesPath.resolve("library.jarC.jar")), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("output/directory"), + applicationDirectory.resolve("output/directory/somefile"), + applicationDirectory.resolve("output/package"), + applicationDirectory.resolve("output/resourceA"), + applicationDirectory.resolve("output/resourceB"), + applicationDirectory.resolve("output/world")), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + applicationDirectory.resolve("output/HelloWorld.class"), + applicationDirectory.resolve("output/directory"), + applicationDirectory.resolve("output/package"), + applicationDirectory.resolve("output/package/some.class"), + applicationDirectory.resolve("output/some.class")), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_nonDefaultAppRoot() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + assertNonDefaultAppRoot(configuration); + } + + @Test + public void testCreateContainerBuilder_warNonDefaultAppRoot() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Path outputPath = getResource("maven/webapp"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + assertSourcePathsUnordered( + ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + outputPath.resolve("final-name/META-INF"), + outputPath.resolve("final-name/META-INF/context.xml"), + outputPath.resolve("final-name/Test.jsp"), + outputPath.resolve("final-name/WEB-INF"), + outputPath.resolve("final-name/WEB-INF/classes"), + outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), + outputPath.resolve("final-name/WEB-INF/classes/package"), + outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), + outputPath.resolve("final-name/WEB-INF/lib"), + outputPath.resolve("final-name/WEB-INF/web.xml")), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertSourcePathsUnordered( + ImmutableList.of( + outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), + outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), + outputPath.resolve("final-name/WEB-INF/classes/package"), + outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")), + layers.classesLayerEntries.get(0).getLayerEntries()); + + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), + layers.dependenciesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/META-INF", + "/my/app/META-INF/context.xml", + "/my/app/Test.jsp", + "/my/app/WEB-INF", + "/my/app/WEB-INF/classes", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/test.properties", + "/my/app/WEB-INF/lib", + "/my/app/WEB-INF/web.xml"), + layers.resourcesLayerEntries.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList( + "/my/app/WEB-INF/classes/HelloWorld.class", + "/my/app/WEB-INF/classes/empty_dir", + "/my/app/WEB-INF/classes/package", + "/my/app/WEB-INF/classes/package/Other.class"), + layers.classesLayerEntries.get(0).getLayerEntries()); + } + + @Test + public void testCreateContainerBuilder_jarNonDefaultAppRoot() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + // Test when the default packaging is set + Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); + BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + assertNonDefaultAppRoot(configuration); + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("final-name"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + } + + @Test + public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { + temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Mockito.when(mockBuild.getDirectory()) + .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + } + + @Test + public void testIsWarProject_WarPackagingIsWar() { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_GwtAppPackagingIsWar() { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-app"); + Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_JarPackagingIsNotWar() { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); + Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + } + + @Test + public void testIsWarProject_GwtLibPackagingIsNotWar() { + Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-lib"); + Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + } + + private BuildConfiguration setupBuildConfiguration(AbsoluteUnixPath appRoot) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + return new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog, appRoot) + .createContainerBuilder(RegistryImage.named("base")) + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("to")), MoreExecutors.newDirectExecutorService()); + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java new file mode 100644 index 0000000000..78cd174e43 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Predicate; + +/** Helper for constructing {@link JavaContainerBuilder}-based {@link JibContainerBuilder}s. */ +public class JavaContainerBuilderHelper { + + /** + * Returns a {@link LayerConfiguration} for adding the extra directory to the container. + * + * @param extraDirectory the source extra directory path + * @param extraDirectoryPermissions map from path on container to file permissions + * @return a {@link LayerConfiguration} for adding the extra directory to the container + * @throws IOException if walking the extra directory fails + */ + public static LayerConfiguration extraDirectoryLayerConfiguration( + Path extraDirectory, Map extraDirectoryPermissions) + throws IOException { + LayerConfiguration.Builder builder = + LayerConfiguration.builder().setName(LayerType.EXTRA_FILES.getName()); + new DirectoryWalker(extraDirectory) + .filterRoot() + .walk( + localPath -> { + AbsoluteUnixPath pathOnContainer = + AbsoluteUnixPath.get("/").resolve(extraDirectory.relativize(localPath)); + builder.addEntry( + localPath, pathOnContainer, extraDirectoryPermissions.get(pathOnContainer)); + }); + return builder.build(); + } + + /** + * Constructs a new {@link JibContainerBuilder} for a WAR project. + * + * @param baseImage the base image of the container + * @param explodedWar the exploded WAR directory + * @param appRoot root directory in the image where the app will be placed + * @return {@link JibContainerBuilder} containing the layers for the exploded WAR + * @throws IOException if adding layer contents fails + */ + public static JibContainerBuilder fromExplodedWar( + RegistryImage baseImage, Path explodedWar, AbsoluteUnixPath appRoot) throws IOException { + Path webInfLib = explodedWar.resolve("WEB-INF/lib"); + Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); + Predicate isDependency = path -> path.startsWith(webInfLib); + Predicate isClassFile = + path -> path.startsWith(webInfClasses) && path.getFileName().toString().endsWith(".class"); + Predicate isResource = isDependency.or(isClassFile).negate(); + + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(baseImage) + .setAppRoot(appRoot) + .setResourcesDestination(RelativeUnixPath.get("")) + .setClassesDestination(RelativeUnixPath.get("WEB-INF/classes")) + .setDependenciesDestination(RelativeUnixPath.get("WEB-INF/lib")); + + if (Files.exists(explodedWar)) { + javaContainerBuilder.addResources(explodedWar, isResource); + } + if (Files.exists(webInfClasses)) { + javaContainerBuilder.addClasses(webInfClasses, isClassFile); + } + if (Files.exists(webInfLib)) { + javaContainerBuilder.addDependencies(new DirectoryWalker(webInfLib).filterRoot().walk()); + } + return javaContainerBuilder.toContainerBuilder(); + } + + private JavaContainerBuilderHelper() {} +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java deleted file mode 100644 index aec8abeef6..0000000000 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelper.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; -import java.util.function.Predicate; - -/** Helper for constructing {@link JavaLayerConfigurations}. */ -public class JavaLayerConfigurationsHelper { - - /** - * Constructs a new {@link JavaLayerConfigurations} for a WAR project. - * - * @param explodedWar the exploded WAR directory - * @param appRoot root directory in the image where the app will be placed - * @param extraFilesDirectory path to the source directory for the extra files layer - * @param extraDirectoryPermissions map from path on container to file permissions - * @return {@link JavaLayerConfigurations} for the layers for the exploded WAR - * @throws IOException if adding layer contents fails - */ - public static JavaLayerConfigurations fromExplodedWar( - Path explodedWar, - AbsoluteUnixPath appRoot, - Path extraFilesDirectory, - Map extraDirectoryPermissions) - throws IOException { - Path webInfLib = explodedWar.resolve("WEB-INF/lib"); - Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); - - Predicate nameHasSnapshot = path -> path.getFileName().toString().contains("SNAPSHOT"); - Predicate isSnapshotDependency = - path -> path.startsWith(webInfLib) && nameHasSnapshot.test(path); - Predicate isNonSnapshotDependency = - path -> path.startsWith(webInfLib) && !nameHasSnapshot.test(path); - Predicate isClassFile = - path -> path.startsWith(webInfClasses) && path.getFileName().toString().endsWith(".class"); - Predicate isResource = - isSnapshotDependency.or(isNonSnapshotDependency).or(isClassFile).negate(); - - JavaLayerConfigurations.Builder layerBuilder = JavaLayerConfigurations.builder(); - - // Gets all the dependencies. - if (Files.exists(webInfLib)) { - AbsoluteUnixPath basePathInContainer = appRoot.resolve("WEB-INF/lib"); - layerBuilder.addDirectoryContents( - LayerType.DEPENDENCIES, webInfLib, isNonSnapshotDependency, basePathInContainer); - layerBuilder.addDirectoryContents( - LayerType.SNAPSHOT_DEPENDENCIES, webInfLib, isSnapshotDependency, basePathInContainer); - } - - // Gets the classes files in the 'WEB-INF/classes' output directory. - if (Files.exists(webInfClasses)) { - layerBuilder.addDirectoryContents( - LayerType.CLASSES, webInfClasses, isClassFile, appRoot.resolve("WEB-INF/classes")); - } - - // Gets the resources. - layerBuilder.addDirectoryContents(LayerType.RESOURCES, explodedWar, isResource, appRoot); - - // Adds all the extra files. - if (Files.exists(extraFilesDirectory)) { - layerBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, - extraFilesDirectory, - path -> true, - AbsoluteUnixPath.get("/"), - extraDirectoryPermissions); - } - - return layerBuilder.build(); - } - - private JavaLayerConfigurationsHelper() {} -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 1f9d7a9b88..990ea36c99 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -19,12 +19,10 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.MainClassFinder; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import java.nio.file.Path; +import java.io.IOException; import javax.annotation.Nullable; import javax.lang.model.SourceVersion; @@ -47,10 +45,11 @@ public class MainClassResolver { * @param projectProperties properties containing plugin information and help messages * @return the name of the main class to be used for the container entrypoint * @throws MainClassInferenceException if no valid main class is configured or discovered + * @throws IOException if getting the class files from {@code projectProperties} fails */ public static String resolveMainClass( @Nullable String mainClass, ProjectProperties projectProperties) - throws MainClassInferenceException { + throws MainClassInferenceException, IOException { // If mainClass is null, try to find via projectProperties. if (mainClass == null) { mainClass = getMainClassFromJar(projectProperties); @@ -106,7 +105,7 @@ private static String getMainClassFromJar(ProjectProperties projectProperties) { } private static String findMainClassInClassFiles(ProjectProperties projectProperties) - throws MainClassInferenceException { + throws MainClassInferenceException, IOException { new DefaultEventDispatcher(projectProperties.getEventHandlers()) .dispatch( LogEvent.debug( @@ -114,17 +113,9 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + projectProperties.getJarPluginName() + "; attempting to infer main class.")); - ImmutableList classesSourceFiles = - projectProperties - .getJavaLayerConfigurations() - .getClassLayerEntries() - .stream() - .map(LayerEntry::getSourceFile) - .collect(ImmutableList.toImmutableList()); - MainClassFinder.Result mainClassFinderResult = new MainClassFinder( - classesSourceFiles, + projectProperties.getClassFiles(), new DefaultEventDispatcher(projectProperties.getEventHandlers())) .find(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 853334756b..2b33a86559 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerDaemonImage; -import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; @@ -39,6 +38,7 @@ import com.google.common.base.Verify; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -187,8 +187,8 @@ static PluginConfigurationProcessor processCommonConfiguration( rawConfiguration.getFromCredHelper().orElse(null)); JibContainerBuilder jibContainerBuilder = - Jib.from(baseImage) - .setLayers(projectProperties.getJavaLayerConfigurations().getLayerConfigurations()) + projectProperties + .createContainerBuilder(baseImage) .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) @@ -205,6 +205,14 @@ static PluginConfigurationProcessor processCommonConfiguration( jibContainerBuilder.setCreationTime(Instant.now()); } + // Adds all the extra files. + if (Files.exists(rawConfiguration.getExtraDirectory())) { + jibContainerBuilder.addLayer( + JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( + rawConfiguration.getExtraDirectory(), + rawConfiguration.getExtraDirectoryPermissions())); + } + PluginConfigurationProcessor.configureContainerizer( containerizer, rawConfiguration, projectProperties); @@ -237,7 +245,7 @@ static PluginConfigurationProcessor processCommonConfiguration( @VisibleForTesting static List computeEntrypoint( RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws MainClassInferenceException, InvalidAppRootException { + throws MainClassInferenceException, InvalidAppRootException, IOException { Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { if (rawConfiguration.getMainClass().isPresent() diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index aaaefb6d9b..e9716ddb6c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,9 +16,12 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import java.io.IOException; import java.nio.file.Path; +import java.util.List; import javax.annotation.Nullable; /** Project property methods that require maven/gradle-specific implementations. */ @@ -43,7 +46,16 @@ public interface ProjectProperties { String getPluginName(); - JavaLayerConfigurations getJavaLayerConfigurations(); + /** + * Starts the containerization process. + * + * @param baseImage the base image + * @return a {@link JibContainerBuilder} with classes, resources, and dependencies added to it + * @throws IOException if there is a problem walking the project files + */ + JibContainerBuilder createContainerBuilder(RegistryImage baseImage) throws IOException; + + List getClassFiles() throws IOException; Path getDefaultCacheDirectory(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 1d55b9fd8c..a2fd77aad7 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -16,7 +16,10 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; @@ -70,4 +73,8 @@ public interface RawConfiguration { ImageFormat getImageFormat(); Optional getProperty(String propertyName); + + Path getExtraDirectory(); + + Map getExtraDirectoryPermissions(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java similarity index 65% rename from jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 02197686b9..f6c3ef3348 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaLayerConfigurationsHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -16,12 +16,19 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; +import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -37,8 +44,8 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -/** Tests for {@link JavaLayerConfigurationsHelper}. */ -public class JavaLayerConfigurationsHelperTest { +/** Tests for {@link JavaContainerBuilderHelper}. */ +public class JavaContainerBuilderHelperTest { private static void assertLayerEntriesUnordered( List expectedPaths, List entries, Function fieldSelector) { @@ -58,10 +65,38 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } + private static List getLayerConfigurationsByName( + BuildConfiguration buildConfiguration, String name) { + return buildConfiguration + .getLayerConfigurations() + .stream() + .filter(layer -> layer.getName().equals(name)) + .collect(Collectors.toList()); + } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void testFromExplodedWar() throws URISyntaxException, IOException { + public void testExtraDirectoryLayerConfiguration() throws URISyntaxException, IOException { + Path extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + LayerConfiguration layerConfiguration = + JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( + extraFilesDirectory, Collections.emptyMap()); + assertSourcePathsUnordered( + Arrays.asList( + extraFilesDirectory.resolve("a"), + extraFilesDirectory.resolve("a/b"), + extraFilesDirectory.resolve("a/b/bar"), + extraFilesDirectory.resolve("c"), + extraFilesDirectory.resolve("c/cat"), + extraFilesDirectory.resolve("foo")), + layerConfiguration.getLayerEntries()); + } + + @Test + public void testFromExplodedWar() + throws URISyntaxException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { // Copy test files to a temporary directory that we can safely operate on Path resourceExplodedWar = Paths.get(Resources.getResource("plugins-common/exploded-war").toURI()); @@ -71,20 +106,29 @@ public void testFromExplodedWar() throws URISyntaxException, IOException { Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); Path extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); - JavaLayerConfigurations configuration = - JavaLayerConfigurationsHelper.fromExplodedWar( - temporaryExplodedWar, - AbsoluteUnixPath.get("/my/app"), - extraFilesDirectory, - Collections.emptyMap()); + BuildConfiguration configuration = + JavaContainerBuilderHelper.fromExplodedWar( + RegistryImage.named("base"), temporaryExplodedWar, AbsoluteUnixPath.get("/my/app")) + .toBuildConfiguration( + Containerizer.to(RegistryImage.named("target")), + MoreExecutors.newDirectExecutorService()); + + List resourcesLayerConfigurations = + getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); + List classesLayerConfigurations = + getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + List dependenciesLayerConfigurations = + getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + List snapshotsLayerConfigurations = + getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); assertSourcePathsUnordered( Collections.singletonList(temporaryExplodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), - configuration.getDependencyLayerEntries()); + dependenciesLayerConfigurations.get(0).getLayerEntries()); assertSourcePathsUnordered( Collections.singletonList( temporaryExplodedWar.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), - configuration.getSnapshotDependencyLayerEntries()); + snapshotsLayerConfigurations.get(0).getLayerEntries()); assertSourcePathsUnordered( Arrays.asList( temporaryExplodedWar.resolve("META-INF"), @@ -97,30 +141,21 @@ public void testFromExplodedWar() throws URISyntaxException, IOException { temporaryExplodedWar.resolve("WEB-INF/classes/package/test.properties"), temporaryExplodedWar.resolve("WEB-INF/lib"), temporaryExplodedWar.resolve("WEB-INF/web.xml")), - configuration.getResourceLayerEntries()); + resourcesLayerConfigurations.get(0).getLayerEntries()); assertSourcePathsUnordered( Arrays.asList( temporaryExplodedWar.resolve("WEB-INF/classes/HelloWorld.class"), temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir"), temporaryExplodedWar.resolve("WEB-INF/classes/package"), temporaryExplodedWar.resolve("WEB-INF/classes/package/Other.class")), - configuration.getClassLayerEntries()); - assertSourcePathsUnordered( - Arrays.asList( - extraFilesDirectory.resolve("a"), - extraFilesDirectory.resolve("a/b"), - extraFilesDirectory.resolve("a/b/bar"), - extraFilesDirectory.resolve("c"), - extraFilesDirectory.resolve("c/cat"), - extraFilesDirectory.resolve("foo")), - configuration.getExtraFilesLayerEntries()); + classesLayerConfigurations.get(0).getLayerEntries()); assertExtractionPathsUnordered( Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), - configuration.getDependencyLayerEntries()); + dependenciesLayerConfigurations.get(0).getLayerEntries()); assertExtractionPathsUnordered( Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), - configuration.getSnapshotDependencyLayerEntries()); + snapshotsLayerConfigurations.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/META-INF", @@ -133,16 +168,13 @@ public void testFromExplodedWar() throws URISyntaxException, IOException { "/my/app/WEB-INF/classes/package/test.properties", "/my/app/WEB-INF/lib", "/my/app/WEB-INF/web.xml"), - configuration.getResourceLayerEntries()); + resourcesLayerConfigurations.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/WEB-INF/classes/HelloWorld.class", "/my/app/WEB-INF/classes/empty_dir", "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/Other.class"), - configuration.getClassLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), - configuration.getExtraFilesLayerEntries()); + classesLayerConfigurations.get(0).getLayerEntries()); } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 27d5452eca..95c5d44fcb 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -19,10 +19,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; @@ -47,7 +44,6 @@ public class MainClassResolverTest { @Mock private Consumer mockJibEventConsumer; @Mock private ProjectProperties mockProjectProperties; - @Mock private JavaLayerConfigurations mockJavaLayerConfigurations; @Before public void setup() { @@ -55,12 +51,10 @@ public void setup() { .thenReturn(new EventHandlers().add(mockJibEventConsumer)); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations()) - .thenReturn(mockJavaLayerConfigurations); } @Test - public void testResolveMainClass() throws MainClassInferenceException { + public void testResolveMainClass() throws MainClassInferenceException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("some.main.class"); Assert.assertEquals( "some.main.class", MainClassResolver.resolveMainClass(null, mockProjectProperties)); @@ -69,12 +63,10 @@ public void testResolveMainClass() throws MainClassInferenceException { } @Test - public void testResolveMainClass_notValid() throws MainClassInferenceException { + public void testResolveMainClass_notValid() throws MainClassInferenceException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry(FAKE_CLASSES_PATH, AbsoluteUnixPath.get("/ignored"), null))); + Mockito.when(mockProjectProperties.getClassFiles()) + .thenReturn(ImmutableList.of(FAKE_CLASSES_PATH)); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockJibEventConsumer) @@ -85,14 +77,12 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException { public void testResolveMainClass_multipleInferredWithBackup() throws MainClassInferenceException, URISyntaxException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) + Mockito.when(mockProjectProperties.getClassFiles()) .thenReturn( new DirectoryWalker( Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI())) .walk() - .stream() - .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) - .collect(ImmutableList.toImmutableList())); + .asList()); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockJibEventConsumer) @@ -103,14 +93,12 @@ public void testResolveMainClass_multipleInferredWithBackup() public void testResolveMainClass_multipleInferredWithoutBackup() throws URISyntaxException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn(null); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) + Mockito.when(mockProjectProperties.getClassFiles()) .thenReturn( new DirectoryWalker( Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI())) .walk() - .stream() - .map(path -> new LayerEntry(path, AbsoluteUnixPath.get("/ignored"), null)) - .collect(ImmutableList.toImmutableList())); + .asList()); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); @@ -124,12 +112,11 @@ public void testResolveMainClass_multipleInferredWithoutBackup() } @Test - public void testResolveMainClass_noneInferredWithBackup() throws MainClassInferenceException { + public void testResolveMainClass_noneInferredWithBackup() + throws MainClassInferenceException, IOException { Mockito.when(mockProjectProperties.getMainClassFromJar()).thenReturn("${start-class}"); - Mockito.when(mockProjectProperties.getJavaLayerConfigurations().getClassLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored"), null))); + Mockito.when(mockProjectProperties.getClassFiles()) + .thenReturn(ImmutableList.of(Paths.get("ignored"))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); Mockito.verify(mockJibEventConsumer) @@ -137,11 +124,9 @@ public void testResolveMainClass_noneInferredWithBackup() throws MainClassInfere } @Test - public void testResolveMainClass_noneInferredWithoutBackup() { - Mockito.when(mockJavaLayerConfigurations.getClassLayerEntries()) - .thenReturn( - ImmutableList.of( - new LayerEntry(Paths.get("ignored"), AbsoluteUnixPath.get("/ignored"), null))); + public void testResolveMainClass_noneInferredWithoutBackup() throws IOException { + Mockito.when(mockProjectProperties.getClassFiles()) + .thenReturn(ImmutableList.of(Paths.get("ignored"))); try { MainClassResolver.resolveMainClass(null, mockProjectProperties); Assert.fail(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 67b68bd349..af0c098d55 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -17,11 +17,13 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -29,14 +31,21 @@ import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -56,6 +65,24 @@ private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibC jibContainerBuilder, Containerizer.to(RegistryImage.named("ignored"))); } + private static void assertLayerEntriesUnordered( + List expectedPaths, List entries, Function fieldSelector) { + List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); + List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); + Assert.assertEquals(expected, actual); + } + + private static void assertSourcePathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); + } + + private static void assertExtractionPathsUnordered( + List expectedPaths, List entries) { + assertLayerEntriesUnordered( + expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); + } + @Mock private RawConfiguration rawConfiguration; @Mock private ProjectProperties projectProperties; @Mock private Containerizer containerizer; @@ -64,18 +91,21 @@ private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibC @Mock private Consumer logger; @Before - public void setUp() { + public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(rawConfiguration.getFromAuth()).thenReturn(authProperty); Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); + Mockito.when(rawConfiguration.getExtraDirectory()).thenReturn(Paths.get("nonexistent/path")); Mockito.when(projectProperties.getToolName()).thenReturn("tool"); - Mockito.when(projectProperties.getJavaLayerConfigurations()) - .thenReturn(JavaLayerConfigurations.builder().build()); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getEventHandlers()) .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); + Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) + .thenReturn( + Jib.from("base") + .setLayers(JavaLayerConfigurations.builder().build().getLayerConfigurations())); Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) @@ -111,6 +141,52 @@ public void testPluginConfigurationProcessor_defaults() Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); } + @Test + public void testPluginConfigurationProcessor_extraDirectory() + throws URISyntaxException, InferredAuthRetrievalException, InvalidContainerVolumeException, + MainClassInferenceException, InvalidAppRootException, IOException, + IncompatibleBaseImageJavaVersionException, InvalidWorkingDirectoryException, + InvalidImageReferenceException, CacheDirectoryCreationException { + Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + Mockito.when(rawConfiguration.getExtraDirectory()).thenReturn(extraDirectory); + Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) + .thenReturn( + ImmutableMap.of(AbsoluteUnixPath.get("/foo"), FilePermissions.fromOctalString("123"))); + + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); + BuildConfiguration buildConfiguration = + getBuildConfiguration(processor.getJibContainerBuilder()); + List extraFiles = + buildConfiguration + .getLayerConfigurations() + .stream() + .filter(layer -> layer.getName().equals("extra files")) + .collect(Collectors.toList()) + .get(0) + .getLayerEntries(); + + assertSourcePathsUnordered( + Arrays.asList( + extraDirectory.resolve("a"), + extraDirectory.resolve("a/b"), + extraDirectory.resolve("a/b/bar"), + extraDirectory.resolve("c"), + extraDirectory.resolve("c/cat"), + extraDirectory.resolve("foo")), + extraFiles); + assertExtractionPathsUnordered( + Arrays.asList("/a", "/a/b", "/a/b/bar", "/c", "/c/cat", "/foo"), extraFiles); + + Optional fooEntry = + extraFiles + .stream() + .filter( + layerEntry -> layerEntry.getExtractionPath().equals(AbsoluteUnixPath.get("/foo"))) + .findFirst(); + Assert.assertTrue(fooEntry.isPresent()); + Assert.assertEquals("123", fooEntry.get().getPermissions().toOctalString()); + } + @Test public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() throws InferredAuthRetrievalException, InvalidContainerVolumeException, @@ -166,7 +242,7 @@ public void testEntrypoint() @Test public void testComputeEntrypoint_inheritKeyword() - throws MainClassInferenceException, InvalidAppRootException { + throws MainClassInferenceException, InvalidAppRootException, IOException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Collections.singletonList("INHERIT"))); @@ -176,7 +252,7 @@ public void testComputeEntrypoint_inheritKeyword() @Test public void testComputeEntrypoint_inheritKeywordInNonSingletonList() - throws MainClassInferenceException, InvalidAppRootException { + throws MainClassInferenceException, InvalidAppRootException, IOException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("INHERIT", ""))); From e8a6c39db532fecd998918a30d27b891ad3c872e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 27 Mar 2019 16:41:39 -0400 Subject: [PATCH 0519/2020] Update CHANGELOG for docker.io fix (#1583) --- jib-core/CHANGELOG.md | 1 + jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 3 files changed, 3 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index cc1ea2cc8b..54f5745836 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed +- `ImageReference` assumes `registry-1.docker.io` as the registry if the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 0.1.2 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c8fd8ad430..b31178d6c0 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index db59da92a8..55921485fa 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 From a132b7f0a3a104d39875023deda60f125cb7e6e6 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 27 Mar 2019 18:35:34 -0400 Subject: [PATCH 0520/2020] Decrypts Maven proxy settings (#1582) --- .../tools/jib/gradle/BuildDockerTask.java | 4 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 +- jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 12 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 12 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 12 +- .../jib/maven/DecryptedMavenSettings.java | 67 +++++++++++ .../maven/MavenSettingsServerCredentials.java | 55 +++------ .../cloud/tools/jib/maven/ProxyProvider.java | 5 +- .../jib/maven/DecryptedMavenSettingsTest.java | 104 ++++++++++++++++++ .../MavenSettingsServerCredentialsTest.java | 86 ++------------- .../tools/jib/maven/ProxyProviderTest.java | 7 +- .../plugins/common/InferredAuthProvider.java | 25 ----- .../InferredAuthRetrievalException.java | 25 ----- .../common/PluginConfigurationProcessor.java | 32 +++--- .../PluginConfigurationProcessorTest.java | 71 +++++------- 17 files changed, 266 insertions(+), 260 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java delete mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java delete mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 92c980de00..e56acb84a4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -89,8 +88,7 @@ public void dockerClient(Action action) { @TaskAction public void buildDocker() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException, MainClassInferenceException, - InferredAuthRetrievalException { + CacheDirectoryCreationException, MainClassInferenceException { Path dockerExecutable = dockerClientParameters.getExecutablePath(); boolean isDockerInstalled = dockerExecutable == null diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index dec9f1e5f4..9bba7a88f9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -76,8 +75,7 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildImage() throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException, MainClassInferenceException, - InferredAuthRetrievalException { + CacheDirectoryCreationException, MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); TaskCommon.disableHttpLogging(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 0968d5c692..8468d46d8b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -98,8 +97,7 @@ public String getOutputFile() { @TaskAction public void buildTar() throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, - CacheDirectoryCreationException, MainClassInferenceException, - InferredAuthRetrievalException { + CacheDirectoryCreationException, MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); TaskCommon.disableHttpLogging(); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 55921485fa..ccd76b07c5 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [unreleased] ### Added +- Can now decrypt proxy configurations in `settings.xml`. ([#1369](https://github.com/GoogleContainerTools/jib/issues/1369)) ### Changed - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index a7d08f76a2..abaacb58a1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -104,16 +103,18 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); + DecryptedMavenSettings decryptedSettings = + DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); + PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( mavenRawConfiguration, - new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter()), + new MavenSettingsServerCredentials(decryptedSettings), projectProperties, dockerExecutable, getDockerClientEnvironment(), mavenHelpfulSuggestionsBuilder.build()); - ProxyProvider.init(getSession().getSettings()); + ProxyProvider.init(decryptedSettings); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = @@ -167,8 +168,7 @@ targetImageReference, getTargetImageAdditionalTags()) } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException - | MainClassInferenceException - | InferredAuthRetrievalException ex) { + | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index d852deb24b..607e3c9e30 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -27,7 +27,6 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -98,13 +97,15 @@ public void execute() throws MojoExecutionException, MojoFailureException { EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); + DecryptedMavenSettings decryptedSettings = + DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); + PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( mavenRawConfiguration, - new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter()), + new MavenSettingsServerCredentials(decryptedSettings), projectProperties); - ProxyProvider.init(getSession().getSettings()); + ProxyProvider.init(decryptedSettings); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = @@ -159,8 +160,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException - | MainClassInferenceException - | InferredAuthRetrievalException ex) { + | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index da3c070241..b2b9294219 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; @@ -77,17 +76,19 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); + DecryptedMavenSettings decryptedSettings = + DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); + Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); Path tarOutputPath = buildOutput.resolve("jib-image.tar"); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( mavenRawConfiguration, - new MavenSettingsServerCredentials( - getSession().getSettings(), getSettingsDecrypter()), + new MavenSettingsServerCredentials(decryptedSettings), projectProperties, tarOutputPath, mavenHelpfulSuggestionsBuilder.build()); - ProxyProvider.init(getSession().getSettings()); + ProxyProvider.init(decryptedSettings); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder @@ -137,8 +138,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (InvalidImageReferenceException | IOException | CacheDirectoryCreationException - | MainClassInferenceException - | InferredAuthRetrievalException ex) { + | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java new file mode 100644 index 0000000000..23b19185f4 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import java.util.List; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; + +/** Provides decrypted Maven settings information. */ +class DecryptedMavenSettings { + + static DecryptedMavenSettings from(Settings settings, SettingsDecrypter decryptor) + throws MojoExecutionException { + SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(settings); + SettingsDecryptionResult result = decryptor.decrypt(request); + // Un-encrypted passwords are passed through, so a problem indicates a real issue. + // If there are any ERROR or FATAL problems reported, then decryption failed. + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new MojoExecutionException("Unable to decrypt settings.xml: " + problem); + } + } + return new DecryptedMavenSettings(result, settings); + } + + private final SettingsDecryptionResult result; + private final Settings settings; + + private DecryptedMavenSettings(SettingsDecryptionResult result, Settings settings) { + this.result = result; + this.settings = settings; + } + + List getServers() { + // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return + // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers + // can be empty. If the decrypted result returns an empty list, we fall back to the original + // settings. + return result.getServers().isEmpty() ? settings.getServers() : result.getServers(); + } + + List getProxies() { + return result.getProxies().isEmpty() ? settings.getProxies() : result.getProxies(); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 7723567e9c..322a563c5f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -17,75 +17,46 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import com.google.cloud.tools.jib.plugins.common.InferredAuthProvider; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.building.SettingsProblem; -import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; /** * Retrieves credentials for servers defined in Maven settings. */ -class MavenSettingsServerCredentials implements InferredAuthProvider { +class MavenSettingsServerCredentials implements Function> { static final String CREDENTIAL_SOURCE = "Maven settings"; - private final Settings settings; - private final SettingsDecrypter settingsDecrypter; + private final DecryptedMavenSettings settings; /** * Create new instance. * - * @param settings the Maven settings object - * @param settingsDecrypter the Maven decrypter component - * @param eventDispatcher the Jib event dispatcher + * @param settings decrypted Maven settings */ - MavenSettingsServerCredentials(Settings settings, SettingsDecrypter settingsDecrypter) { + MavenSettingsServerCredentials(DecryptedMavenSettings settings) { this.settings = settings; - this.settingsDecrypter = settingsDecrypter; } /** - * Attempts to retrieve credentials for {@code registry} from Maven settings. + * Retrieves credentials for {@code registry} from Maven settings. * * @param registry the registry * @return the auth info for the registry, or {@link Optional#empty} if none could be retrieved - * @throws InferredAuthRetrievalException if the credentials could not be retrieved */ @Override - public Optional getAuth(String registry) throws InferredAuthRetrievalException { - Server registryServer = settings.getServer(registry); - if (registryServer == null) { + public Optional apply(String registry) { + Predicate idMatches = server -> registry.equals(server.getId()); + Optional server = settings.getServers().stream().filter(idMatches).findFirst(); + if (!server.isPresent()) { return Optional.empty(); } - // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return - // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers - // can be empty. We handle the results as follows: - // - if there are any ERROR or FATAL problems reported, then decryption failed - // - if no decrypted servers returned then treat as if no decryption was required - SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(registryServer); - SettingsDecryptionResult result = settingsDecrypter.decrypt(request); - // un-encrypted passwords are passed through, so a problem indicates a real issue - for (SettingsProblem problem : result.getProblems()) { - if (problem.getSeverity() == SettingsProblem.Severity.ERROR - || problem.getSeverity() == SettingsProblem.Severity.FATAL) { - throw new InferredAuthRetrievalException( - "Unable to decrypt password for " + registry + ": " + problem); - } - } - if (result.getServer() != null) { - registryServer = result.getServer(); - } - - String username = registryServer.getUsername(); - String password = registryServer.getPassword(); + String username = server.get().getUsername(); + String password = server.get().getPassword(); return Optional.of( new AuthProperty() { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java index d32632fc2f..157ac8146c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java @@ -20,7 +20,6 @@ import com.google.common.collect.ImmutableList; import javax.annotation.Nullable; import org.apache.maven.settings.Proxy; -import org.apache.maven.settings.Settings; /** Propagates proxy configuration from Maven settings to system properties. */ class ProxyProvider { @@ -33,12 +32,12 @@ class ProxyProvider { * * @param settings Maven settings */ - static void init(Settings settings) { + static void init(DecryptedMavenSettings settings) { configureProxy(settings, "https"); configureProxy(settings, "http"); } - private static void configureProxy(Settings settings, String protocol) { + private static void configureProxy(DecryptedMavenSettings settings, String protocol) { settings .getProxies() .stream() diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java new file mode 100644 index 0000000000..934c11cf9a --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link DecryptedMavenSettings}. */ +@RunWith(MockitoJUnitRunner.class) +public class DecryptedMavenSettingsTest { + + @Mock private Server server1; + @Mock private Server server2; + @Mock private Proxy proxy; + @Mock private Settings settings; + @Mock private SettingsDecrypter settingsDecrypter; + @Mock private SettingsDecryptionResult decryptionResult; + + private final List servers = Arrays.asList(server1, server2); + private final List proxies = Arrays.asList(proxy); + + private DecryptedMavenSettings decryptedSettings; + + @Before + public void setUp() throws MojoExecutionException { + Mockito.when(settingsDecrypter.decrypt(Mockito.any())).thenReturn(decryptionResult); + Mockito.when(decryptionResult.getProblems()).thenReturn(Collections.emptyList()); + + decryptedSettings = DecryptedMavenSettings.from(settings, settingsDecrypter); + } + + @Test + public void testFrom_decrypterFailure() { + SettingsProblem problem = Mockito.mock(SettingsProblem.class); + Mockito.when(problem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR); + // Maven's SettingsProblem has a more structured toString, but irrelevant here + Mockito.when(problem.toString()).thenReturn("MockProblemText"); + Mockito.when(decryptionResult.getProblems()).thenReturn(Arrays.asList(problem)); + + try { + DecryptedMavenSettings.from(settings, settingsDecrypter); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertEquals("Unable to decrypt settings.xml: MockProblemText", ex.getMessage()); + } + } + + @Test + public void testGetServers() { + Mockito.when(decryptionResult.getServers()).thenReturn(servers); + Assert.assertEquals(servers, decryptedSettings.getServers()); + } + + @Test + public void testGetProxies() { + Mockito.when(decryptionResult.getProxies()).thenReturn(proxies); + Assert.assertEquals(proxies, decryptedSettings.getProxies()); + } + + @Test + public void testGetServers_emptyListFromDecryption() { + Mockito.when(decryptionResult.getServers()).thenReturn(Collections.emptyList()); + Mockito.when(settings.getServers()).thenReturn(servers); + + Assert.assertEquals(servers, decryptedSettings.getServers()); + } + + @Test + public void testGetProxies_emptyListFromDecryption() { + Mockito.when(decryptionResult.getProxies()).thenReturn(Collections.emptyList()); + Mockito.when(settings.getProxies()).thenReturn(proxies); + + Assert.assertEquals(proxies, decryptedSettings.getProxies()); + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 09e8d77e0c..f1c8663c78 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -17,14 +17,9 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import com.google.cloud.tools.jib.plugins.common.InferredAuthRetrievalException; -import java.util.Collections; +import java.util.Arrays; import java.util.Optional; import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.building.SettingsProblem; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -37,93 +32,30 @@ @RunWith(MockitoJUnitRunner.class) public class MavenSettingsServerCredentialsTest { - @Mock private Settings mockSettings; + @Mock private DecryptedMavenSettings mockSettings; @Mock private Server mockServer1; - @Mock private SettingsDecrypter mockSettingsDecrypter; - @Mock private SettingsDecryptionResult mockSettingsDecryptionResult; private MavenSettingsServerCredentials testMavenSettingsServerCredentials; @Before public void setUp() { - Mockito.when(mockSettingsDecryptionResult.getProblems()).thenReturn(Collections.emptyList()); - Mockito.when(mockSettingsDecryptionResult.getServer()).thenReturn(mockServer1); - Mockito.when(mockSettingsDecrypter.decrypt(Mockito.any())) - .thenReturn(mockSettingsDecryptionResult); - - testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); - } - - @Test - public void testRetrieve_found() throws InferredAuthRetrievalException { - Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); - + Mockito.when(mockSettings.getServers()).thenReturn(Arrays.asList(mockServer1)); + Mockito.when(mockServer1.getId()).thenReturn("server1"); Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - - Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); - Assert.assertTrue(auth.isPresent()); - Assert.assertEquals("server1 username", auth.get().getUsername()); - Assert.assertEquals("server1 password", auth.get().getPassword()); - } - - @Test - public void testRetrieve_notFound() throws InferredAuthRetrievalException { - Assert.assertFalse(testMavenSettingsServerCredentials.getAuth("serverUnknown").isPresent()); + testMavenSettingsServerCredentials = new MavenSettingsServerCredentials(mockSettings); } @Test - public void testRetrieve_withNullServer() throws InferredAuthRetrievalException { - Assert.assertFalse(testMavenSettingsServerCredentials.getAuth(null).isPresent()); - } - - @Test - public void testRetrieve_withDecrypter_success() throws InferredAuthRetrievalException { - testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); - - // essentially the same as testRetrieve_found() - Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); - Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); - Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - - Optional auth = testMavenSettingsServerCredentials.getAuth("server1"); + public void testRetrieve_found() { + Optional auth = testMavenSettingsServerCredentials.apply("server1"); Assert.assertTrue(auth.isPresent()); Assert.assertEquals("server1 username", auth.get().getUsername()); Assert.assertEquals("server1 password", auth.get().getPassword()); - - Mockito.verify(mockSettingsDecrypter).decrypt(Mockito.any()); - Mockito.verify(mockSettingsDecryptionResult).getProblems(); - Mockito.verify(mockSettingsDecryptionResult, Mockito.atLeastOnce()).getServer(); } @Test - public void testRetrieve_withDecrypter_failure() { - - SettingsProblem mockProblem = Mockito.mock(SettingsProblem.class); - Mockito.when(mockProblem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR); - // Maven's SettingsProblem has a more structured toString, but irrelevant here - Mockito.when(mockProblem.toString()).thenReturn("MockProblemText"); - Mockito.when(mockSettingsDecryptionResult.getProblems()) - .thenReturn(Collections.singletonList(mockProblem)); - - testMavenSettingsServerCredentials = - new MavenSettingsServerCredentials(mockSettings, mockSettingsDecrypter); - - // essentially the same as testRetrieve_found() - Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1); - - try { - testMavenSettingsServerCredentials.getAuth("server1"); - Assert.fail("decryption should have failed"); - } catch (InferredAuthRetrievalException ex) { - Assert.assertEquals( - ex.getMessage(), "Unable to decrypt password for server1: MockProblemText"); - Mockito.verify(mockSettingsDecrypter).decrypt(Mockito.any()); - Mockito.verify(mockSettingsDecryptionResult).getProblems(); - Mockito.verifyNoMoreInteractions( - mockSettingsDecryptionResult); // getServer() should never be called - } + public void testRetrieve_notFound() { + Assert.assertFalse(testMavenSettingsServerCredentials.apply("serverUnknown").isPresent()); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java index 26c57f00f3..caf5e158a6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.function.Consumer; import org.apache.maven.settings.Proxy; -import org.apache.maven.settings.Settings; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -199,7 +198,7 @@ public void testInit_noActiveProxy() { httpsProxy.setHost("proxy2 host"); httpsProxy.setActive(false); - Settings settings = Mockito.mock(Settings.class); + DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(httpProxy, httpsProxy)); ProxyProvider.init(settings); @@ -229,7 +228,7 @@ public void testInit_firstActiveHttpProxy() { proxy4.setHost("proxy4 host"); proxy4.setActive(true); - Settings settings = Mockito.mock(Settings.class); + DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); ProxyProvider.init(settings); @@ -259,7 +258,7 @@ public void testInit_firstActiveHttpsProxy() { proxy4.setHost("proxy4 host"); proxy4.setActive(true); - Settings settings = Mockito.mock(Settings.class); + DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); ProxyProvider.init(settings); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java deleted file mode 100644 index 01838fdf0a..0000000000 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -import java.util.Optional; - -/** Provides inferred auth information. */ -public interface InferredAuthProvider { - - Optional getAuth(String registry) throws InferredAuthRetrievalException; -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java deleted file mode 100644 index 5f729d5e3d..0000000000 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthRetrievalException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.plugins.common; - -/** Exception when trying to retrieve an inferred auth info. */ -public class InferredAuthRetrievalException extends Exception { - - public InferredAuthRetrievalException(String message) { - super(message); - } -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 2b33a86559..bbd4801e93 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -47,6 +47,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import javax.annotation.Nullable; /** @@ -57,14 +58,14 @@ public class PluginConfigurationProcessor { public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, - InferredAuthProvider inferredAuthProvider, + Function> inferredAuthProvider, ProjectProperties projectProperties, @Nullable Path dockerExecutable, @Nullable Map dockerEnvironment, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -87,13 +88,13 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa public static PluginConfigurationProcessor processCommonConfigurationForTarImage( RawConfiguration rawConfiguration, - InferredAuthProvider inferredAuthProvider, + Function> inferredAuthProvider, ProjectProperties projectProperties, Path tarImagePath, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -110,11 +111,10 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( RawConfiguration rawConfiguration, - InferredAuthProvider inferredAuthProvider, + Function> inferredAuthProvider, ProjectProperties projectProperties) - throws InferredAuthRetrievalException, InvalidImageReferenceException, - MainClassInferenceException, InvalidAppRootException, IOException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); @@ -150,14 +150,14 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry @VisibleForTesting static PluginConfigurationProcessor processCommonConfiguration( RawConfiguration rawConfiguration, - InferredAuthProvider inferredAuthProvider, + Function> inferredAuthProvider, ProjectProperties projectProperties, Containerizer containerizer, ImageReference targetImageReference, boolean isTargetImageCredentialPresent) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); @@ -390,9 +390,9 @@ private static boolean configureCredentialRetrievers( String usernamePropertyName, String passwordPropertyName, AuthProperty knownAuth, - InferredAuthProvider inferredAuthProvider, + Function> inferredAuthProvider, @Nullable String credHelper) - throws FileNotFoundException, InferredAuthRetrievalException { + throws FileNotFoundException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( CredentialRetrieverFactory.forImage(imageReference, eventDispatcher)); @@ -409,7 +409,7 @@ private static boolean configureCredentialRetrievers( optionalCredential.get(), knownAuth.getAuthDescriptor()); } else { Optional optionalInferredAuth = - inferredAuthProvider.getAuth(imageReference.getRegistry()); + inferredAuthProvider.apply(imageReference.getRegistry()); credentialPresent = optionalInferredAuth.isPresent(); if (optionalInferredAuth.isPresent()) { AuthProperty auth = optionalInferredAuth.get(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index af0c098d55..93817d52a9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -121,9 +121,8 @@ public void setUp() throws IOException, InvalidImageReferenceException { @Test public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -143,10 +142,10 @@ public void testPluginConfigurationProcessor_defaults() @Test public void testPluginConfigurationProcessor_extraDirectory() - throws URISyntaxException, InferredAuthRetrievalException, InvalidContainerVolumeException, - MainClassInferenceException, InvalidAppRootException, IOException, - IncompatibleBaseImageJavaVersionException, InvalidWorkingDirectoryException, - InvalidImageReferenceException, CacheDirectoryCreationException { + throws URISyntaxException, InvalidContainerVolumeException, MainClassInferenceException, + InvalidAppRootException, IOException, IncompatibleBaseImageJavaVersionException, + InvalidWorkingDirectoryException, InvalidImageReferenceException, + CacheDirectoryCreationException { Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Mockito.when(rawConfiguration.getExtraDirectory()).thenReturn(extraDirectory); Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) @@ -189,9 +188,8 @@ public void testPluginConfigurationProcessor_extraDirectory() @Test public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() - throws InferredAuthRetrievalException, InvalidContainerVolumeException, - MainClassInferenceException, InvalidAppRootException, IOException, - InvalidWorkingDirectoryException, InvalidImageReferenceException, + throws InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, + IOException, InvalidWorkingDirectoryException, InvalidImageReferenceException, IncompatibleBaseImageJavaVersionException { System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); @@ -208,8 +206,8 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() @Test public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -223,9 +221,8 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() @Test public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -263,9 +260,8 @@ public void testComputeEntrypoint_inheritKeywordInNonSingletonList() @Test public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -281,9 +277,8 @@ public void testEntrypoint_defaultWarPackaging() @Test public void testEntrypoint_defaulNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -303,9 +298,8 @@ public void testEntrypoint_defaulNonWarPackaging() @Test public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -319,9 +313,8 @@ public void testUser() @Test public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -333,9 +326,8 @@ public void testUser_null() @Test public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -355,9 +347,8 @@ public void testEntrypoint_warningOnJvmFlags() @Test public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -377,9 +368,8 @@ public void testEntrypoint_warningOnMainclass() @Test public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -400,9 +390,8 @@ public void testEntrypointClasspath_nonDefaultAppRoot() @Test public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, - MainClassInferenceException, InvalidAppRootException, InferredAuthRetrievalException, - InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -720,8 +709,8 @@ public void testGetInvalidVolumesList() { private PluginConfigurationProcessor createPluginConfigurationProcessor() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - InferredAuthRetrievalException, IOException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException { return PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, ignored -> Optional.empty(), From 890978e6832cf30e3991bf87ef7d7a06e9c7639f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 28 Mar 2019 11:04:13 -0400 Subject: [PATCH 0521/2020] Core release v0.9.0 (#1585) * [Gradle Release Plugin] - pre tag commit: 'v0.9.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.9.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 8cb23a76c0..968bf22310 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.1.3-SNAPSHOT +version = 0.9.1-SNAPSHOT From 4c06c08088b33cdb9509d134d0bbc138940d7ce9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 28 Mar 2019 12:01:56 -0400 Subject: [PATCH 0522/2020] Update jib-core version strings for 0.9.0 (#1587) --- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 54f5745836..4384e3521d 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.0 + +### Added + - `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) - `Jib#fromScratch()` to start building from an empty base image ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) - Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files diff --git a/jib-core/README.md b/jib-core/README.md index 2b61a439b2..cd72c50fa8 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.1.2 + 0.9.0 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.1.2' + compile 'com.google.cloud.tools:jib-core:0.9.0' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index ef94060b2a..5a20c550c3 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.1.1' + classpath 'com.google.cloud.tools:jib-core:0.9.0' } } From cbfef69e7025f0183aceb2b76870149e7b20d219 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 28 Mar 2019 16:09:15 -0400 Subject: [PATCH 0523/2020] Remove JavaLayerConfigurations (#1588) --- .../tools/jib/api/JavaContainerBuilder.java | 108 +++- .../jib/frontend/JavaLayerConfigurations.java | 249 --------- .../frontend/JavaLayerConfigurationsTest.java | 473 ------------------ .../gradle/GradleProjectPropertiesTest.java | 14 +- .../jib/maven/MavenProjectPropertiesTest.java | 2 +- .../common/JavaContainerBuilderHelper.java | 2 +- .../common/PluginConfigurationProcessor.java | 10 +- .../JavaContainerBuilderHelperTest.java | 2 +- .../PluginConfigurationProcessorTest.java | 5 +- 9 files changed, 113 insertions(+), 752 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index fc01bbb07f..bba22b798f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -17,14 +17,15 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -32,8 +33,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -54,6 +57,31 @@ private PathPredicatePair(Path path, Predicate predicate) { } } + /** Represents the different types of layers for a Java application. */ + @VisibleForTesting + public enum LayerType { + DEPENDENCIES("dependencies"), + SNAPSHOT_DEPENDENCIES("snapshot dependencies"), + RESOURCES("resources"), + CLASSES("classes"), + EXTRA_FILES("extra files"); + + private final String name; + + /** + * Initializes with a name for the layer. + * + * @param name name to set for the layer; does not affect the contents of the layer + */ + LayerType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + /** * Creates a new {@link JavaContainerBuilder} that uses distroless java as the base image. For * more information on {@code gcr.io/distroless/java}, see layerBuilders = new EnumMap<>(LayerType.class); // Add classes to layer configuration for (PathPredicatePair directory : addedClasses) { - layerConfigurationsBuilder.addDirectoryContents( + addDirectoryContentsToLayer( + layerBuilders, LayerType.CLASSES, directory.path, directory.predicate, @@ -414,7 +455,8 @@ public JibContainerBuilder toContainerBuilder() throws IOException { // Add resources to layer configuration for (PathPredicatePair directory : addedResources) { - layerConfigurationsBuilder.addDirectoryContents( + addDirectoryContentsToLayer( + layerBuilders, LayerType.RESOURCES, directory.path, directory.predicate, @@ -435,7 +477,8 @@ public JibContainerBuilder toContainerBuilder() throws IOException { .collect(Collectors.toList()); for (Path file : addedDependencies) { // Add dependencies to layer configuration - layerConfigurationsBuilder.addFile( + addFileToLayer( + layerBuilders, file.getFileName().toString().contains("SNAPSHOT") ? LayerType.SNAPSHOT_DEPENDENCIES : LayerType.DEPENDENCIES, @@ -454,16 +497,25 @@ public JibContainerBuilder toContainerBuilder() throws IOException { // Add others to layer configuration for (Path path : addedOthers) { if (Files.isDirectory(path)) { - layerConfigurationsBuilder.addDirectoryContents( - LayerType.EXTRA_FILES, path, path1 -> true, appRoot.resolve(othersDestination)); + addDirectoryContentsToLayer( + layerBuilders, + LayerType.EXTRA_FILES, + path, + ignored -> true, + appRoot.resolve(othersDestination)); } else { - layerConfigurationsBuilder.addFile( + addFileToLayer( + layerBuilders, LayerType.EXTRA_FILES, path, appRoot.resolve(othersDestination).resolve(path.getFileName())); } } - jibContainerBuilder.setLayers(layerConfigurationsBuilder.build().getLayerConfigurations()); + + // Add layer configurations to container builder + List layers = new ArrayList<>(); + layerBuilders.forEach((type, builder) -> layers.add(builder.setName(type.getName()).build())); + jibContainerBuilder.setLayers(layers); if (mainClass != null) { // Construct entrypoint. Ensure classpath elements are in the same order as the files were @@ -507,4 +559,38 @@ private JavaContainerBuilder addDirectory( addedPaths.add(new PathPredicatePair(directory, filter)); return this; } + + private void addFileToLayer( + Map layerBuilders, + LayerType layerType, + Path sourceFile, + AbsoluteUnixPath pathInContainer) { + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, LayerConfiguration.builder()); + } + layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer); + } + + private void addDirectoryContentsToLayer( + Map layerBuilders, + LayerType layerType, + Path sourceRoot, + Predicate pathFilter, + AbsoluteUnixPath basePathInContainer) + throws IOException { + if (!layerBuilders.containsKey(layerType)) { + layerBuilders.put(layerType, LayerConfiguration.builder()); + } + LayerConfiguration.Builder builder = layerBuilders.get(layerType); + + new DirectoryWalker(sourceRoot) + .filterRoot() + .filter(path -> Files.isDirectory(path) || pathFilter.test(path)) + .walk( + path -> { + AbsoluteUnixPath pathOnContainer = + basePathInContainer.resolve(sourceRoot.relativize(path)); + builder.addEntry(path, pathOnContainer); + }); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java deleted file mode 100644 index 8b42bccf76..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurations.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.NotDirectoryException; -import java.nio.file.Path; -import java.util.EnumMap; -import java.util.Map; -import java.util.function.Predicate; -import javax.annotation.Nullable; - -/** Builds {@link LayerConfiguration}s for a Java application. */ -public class JavaLayerConfigurations { - - /** Represents the different types of layers for a Java application. */ - public enum LayerType { - DEPENDENCIES("dependencies"), - SNAPSHOT_DEPENDENCIES("snapshot dependencies"), - RESOURCES("resources"), - CLASSES("classes"), - EXTRA_FILES("extra files"); - - private final String name; - - /** - * Initializes with a name for the layer. - * - * @param name name to set for the layer; does not affect the contents of the layer - */ - LayerType(String name) { - this.name = name; - } - - @VisibleForTesting - public String getName() { - return name; - } - } - - /** Builds with each layer's files. */ - public static class Builder { - - private final Map layerBuilders = - new EnumMap<>(LayerType.class); - - private Builder() {} - - /** - * Adds a file to a layer. Only adds the single source file to the exact path in the container - * file system. (If the source file is a directory, does not copy its contents but creates only - * the directory.) See {@link LayerConfiguration.Builder#addEntry} for concrete examples about - * how the file will be placed in the image. - * - * @param layerType the layer to add files into - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @return this - * @see LayerConfiguration.Builder#addEntry(Path, AbsoluteUnixPath) - */ - public Builder addFile(LayerType layerType, Path sourceFile, AbsoluteUnixPath pathInContainer) { - return addFile(layerType, sourceFile, pathInContainer, null); - } - - /** - * Adds a file to a layer. Only adds the single source file to the exact path in the container - * file system. (If the source file is a directory, does not copy its contents but creates only - * the directory.) See {@link LayerConfiguration.Builder#addEntry} for concrete examples about - * how the file will be placed in the image. - * - * @param layerType the layer to add files into - * @param sourceFile the source file to add to the layer - * @param pathInContainer the path in the container file system corresponding to the {@code - * sourceFile} - * @param permissions the file permissions on the container. Use {@code null} for defaults (644 - * for files, 755 for directories) - * @return this - * @see LayerConfiguration.Builder#addEntry(Path, AbsoluteUnixPath, FilePermissions) - */ - public Builder addFile( - LayerType layerType, - Path sourceFile, - AbsoluteUnixPath pathInContainer, - @Nullable FilePermissions permissions) { - if (!layerBuilders.containsKey(layerType)) { - layerBuilders.put(layerType, LayerConfiguration.builder()); - } - layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer, permissions); - return this; - } - - /** - * Adds directory contents to a layer selectively (via {@code pathFilter}) and recursively. - * {@code sourceRoot} must be a directory. Empty directories will always be added regardless of - * {@code pathFilter}, except that {@code sourceRoot} is never added. - * - *

The contents of {@code sourceRoot} will be placed into {@code basePathInContainer}. For - * example, if {@code sourceRoot} is {@code /usr/home}, {@code /usr/home/passwd} exists locally, - * and {@code basePathInContainer} is {@code /etc}, then the image will have {@code - * /etc/passwd}. - * - * @param layerType the layer to add files into - * @param sourceRoot root directory whose contents will be added - * @param pathFilter filter that determines which files (not directories) should be added - * @param basePathInContainer directory in the layer into which the source contents are added - * @return this - * @throws IOException error while listing directories - * @throws NotDirectoryException if {@code sourceRoot} is not a directory - */ - public Builder addDirectoryContents( - LayerType layerType, - Path sourceRoot, - Predicate pathFilter, - AbsoluteUnixPath basePathInContainer) - throws IOException { - return addDirectoryContents( - layerType, sourceRoot, pathFilter, basePathInContainer, ImmutableMap.of()); - } - - /** - * Adds directory contents to a layer selectively (via {@code pathFilter}) and recursively. - * {@code sourceRoot} must be a directory. Empty directories will always be added regardless of - * {@code pathFilter}, except that {@code sourceRoot} is never added. Permissions are specified - * via {@code permissionsMap}, which maps from extraction path on the container to file - * permissions. - * - * @param layerType the layer to add files into - * @param sourceRoot root directory whose contents will be added - * @param pathFilter filter that determines which files (not directories) should be added - * @param basePathInContainer directory in the layer into which the source contents are added - * @param permissionsMap the map from absolute path on container to file permission - * @return this - * @throws IOException error while listing directories - * @throws NotDirectoryException if {@code sourceRoot} is not a directory - */ - public Builder addDirectoryContents( - LayerType layerType, - Path sourceRoot, - Predicate pathFilter, - AbsoluteUnixPath basePathInContainer, - Map permissionsMap) - throws IOException { - if (!layerBuilders.containsKey(layerType)) { - layerBuilders.put(layerType, LayerConfiguration.builder()); - } - LayerConfiguration.Builder builder = layerBuilders.get(layerType); - - new DirectoryWalker(sourceRoot) - .filterRoot() - .filter(path -> Files.isDirectory(path) || pathFilter.test(path)) - .walk( - path -> { - AbsoluteUnixPath pathOnContainer = - basePathInContainer.resolve(sourceRoot.relativize(path)); - builder.addEntry(path, pathOnContainer, permissionsMap.get(pathOnContainer)); - }); - return this; - } - - public JavaLayerConfigurations build() { - ImmutableMap.Builder layerConfigurationsMap = - ImmutableMap.builderWithExpectedSize(layerBuilders.size()); - - // ImmutableMap.Builder will preserve the order of EnumMap. - layerBuilders.forEach( - (type, builder) -> - layerConfigurationsMap.put(type, builder.setName(type.getName()).build())); - return new JavaLayerConfigurations(layerConfigurationsMap.build()); - } - } - - public static Builder builder() { - return new Builder(); - } - - /** - * The default app root in the image. For example, if this is set to {@code "/app"}, dependency - * JARs will be in {@code "/app/libs"}. - */ - public static final String DEFAULT_APP_ROOT = "/app"; - - /** - * The default webapp root in the image. For example, if this is set to {@code - * "/jetty/webapps/ROOT"}, dependency JARs will be in {@code "/jetty/webapps/ROOT/WEB-INF/lib"}. - */ - public static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; - - private final ImmutableMap layerConfigurationMap; - - private JavaLayerConfigurations( - ImmutableMap layerConfigurationMap) { - this.layerConfigurationMap = layerConfigurationMap; - } - - public ImmutableList getLayerConfigurations() { - return layerConfigurationMap.values().asList(); - } - - public ImmutableList getDependencyLayerEntries() { - return getLayerEntries(LayerType.DEPENDENCIES); - } - - public ImmutableList getSnapshotDependencyLayerEntries() { - return getLayerEntries(LayerType.SNAPSHOT_DEPENDENCIES); - } - - public ImmutableList getResourceLayerEntries() { - return getLayerEntries(LayerType.RESOURCES); - } - - public ImmutableList getClassLayerEntries() { - return getLayerEntries(LayerType.CLASSES); - } - - public ImmutableList getExtraFilesLayerEntries() { - return getLayerEntries(LayerType.EXTRA_FILES); - } - - private ImmutableList getLayerEntries(LayerType layerType) { - if (!layerConfigurationMap.containsKey(layerType)) { - return ImmutableList.of(); - } - return layerConfigurationMap.get(layerType).getLayerEntries(); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java deleted file mode 100644 index 520bffb1eb..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaLayerConfigurationsTest.java +++ /dev/null @@ -1,473 +0,0 @@ -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; -import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.NotDirectoryException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** Tests for {@link JavaLayerConfigurations}. */ -public class JavaLayerConfigurationsTest { - - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private static void assertLayerEntriesUnordered( - List expectedPaths, List entries, Function fieldSelector) { - List expected = expectedPaths.stream().sorted().collect(Collectors.toList()); - List actual = entries.stream().map(fieldSelector).sorted().collect(Collectors.toList()); - Assert.assertEquals(expected, actual); - } - - private static void assertSourcePathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered(expectedPaths, entries, LayerEntry::getSourceFile); - } - - private static void assertExtractionPathsUnordered( - List expectedPaths, List entries) { - assertLayerEntriesUnordered( - expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); - } - - private static JavaLayerConfigurations createFakeConfigurations() { - return JavaLayerConfigurations.builder() - .addFile( - LayerType.DEPENDENCIES, - Paths.get("dependency"), - AbsoluteUnixPath.get("/dependency/path")) - .addFile( - LayerType.SNAPSHOT_DEPENDENCIES, - Paths.get("snapshot dependency"), - AbsoluteUnixPath.get("/snapshots")) - .addFile( - LayerType.RESOURCES, Paths.get("resource"), AbsoluteUnixPath.get("/resources/here")) - .addFile(LayerType.CLASSES, Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here")) - .addFile( - LayerType.EXTRA_FILES, Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras")) - .build(); - } - - @Test - public void testLabels() { - JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); - - List expectedLabels = new ArrayList<>(); - for (LayerType layerType : LayerType.values()) { - expectedLabels.add(layerType.getName()); - } - List actualLabels = new ArrayList<>(); - for (LayerConfiguration layerConfiguration : javaLayerConfigurations.getLayerConfigurations()) { - actualLabels.add(layerConfiguration.getName()); - } - Assert.assertEquals(expectedLabels, actualLabels); - } - - @Test - public void testAddFile() { - JavaLayerConfigurations javaLayerConfigurations = createFakeConfigurations(); - - Assert.assertEquals( - Collections.singletonList( - new LayerEntry( - Paths.get("dependency"), AbsoluteUnixPath.get("/dependency/path"), null)), - javaLayerConfigurations.getDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry( - Paths.get("snapshot dependency"), AbsoluteUnixPath.get("/snapshots"), null)), - javaLayerConfigurations.getSnapshotDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(Paths.get("resource"), AbsoluteUnixPath.get("/resources/here"), null)), - javaLayerConfigurations.getResourceLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(Paths.get("class"), AbsoluteUnixPath.get("/classes/go/here"), null)), - javaLayerConfigurations.getClassLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(Paths.get("extra file"), AbsoluteUnixPath.get("/some/extras"), null)), - javaLayerConfigurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddFile_directories() throws URISyntaxException { - Path sourceDirectory = Paths.get(Resources.getResource("core/random-contents").toURI()); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addFile(LayerType.DEPENDENCIES, sourceDirectory, AbsoluteUnixPath.get("/libs/dir")) - .addFile( - LayerType.SNAPSHOT_DEPENDENCIES, - sourceDirectory, - AbsoluteUnixPath.get("/snapshots/target")) - .addFile(LayerType.RESOURCES, sourceDirectory, AbsoluteUnixPath.get("/resources")) - .addFile(LayerType.CLASSES, sourceDirectory, AbsoluteUnixPath.get("/classes/here")) - .addFile(LayerType.EXTRA_FILES, sourceDirectory, AbsoluteUnixPath.get("/extra/files")) - .build(); - - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/libs/dir"), null)), - configurations.getDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/snapshots/target"), null)), - configurations.getSnapshotDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/resources"), null)), - configurations.getResourceLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/classes/here"), null)), - configurations.getClassLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceDirectory, AbsoluteUnixPath.get("/extra/files"), null)), - configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddFile_regularFiles() throws URISyntaxException { - Path sourceFile = - Paths.get(Resources.getResource("core/random-contents/sub-directory/leaf/file6").toURI()); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addFile(LayerType.DEPENDENCIES, sourceFile, AbsoluteUnixPath.get("/libs/file")) - .addFile( - LayerType.SNAPSHOT_DEPENDENCIES, - sourceFile, - AbsoluteUnixPath.get("/snapshots/target/file")) - .addFile(LayerType.RESOURCES, sourceFile, AbsoluteUnixPath.get("/resources-file")) - .addFile(LayerType.CLASSES, sourceFile, AbsoluteUnixPath.get("/classes/file")) - .addFile(LayerType.EXTRA_FILES, sourceFile, AbsoluteUnixPath.get("/some/file")) - .build(); - - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceFile, AbsoluteUnixPath.get("/libs/file"), null)), - configurations.getDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceFile, AbsoluteUnixPath.get("/snapshots/target/file"), null)), - configurations.getSnapshotDependencyLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceFile, AbsoluteUnixPath.get("/resources-file"), null)), - configurations.getResourceLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceFile, AbsoluteUnixPath.get("/classes/file"), null)), - configurations.getClassLayerEntries()); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceFile, AbsoluteUnixPath.get("/some/file"), null)), - configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddFile_webAppSample() { - AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/usr/local/tomcat/webapps/ROOT/"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addFile(LayerType.RESOURCES, Paths.get("test.jsp"), appRoot.resolve("test.jsp")) - .addFile(LayerType.RESOURCES, Paths.get("META-INF/"), appRoot.resolve("META-INF/")) - .addFile( - LayerType.RESOURCES, - Paths.get("context.xml"), - appRoot.resolve("WEB-INF/context.xml")) - .addFile( - LayerType.RESOURCES, Paths.get("sub_dir/"), appRoot.resolve("WEB-INF/sub_dir/")) - .addFile( - LayerType.DEPENDENCIES, - Paths.get("myLib.jar"), - appRoot.resolve("WEB-INF/lib/myLib.jar")) - .addFile( - LayerType.SNAPSHOT_DEPENDENCIES, - Paths.get("my-SNAPSHOT.jar"), - appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar")) - .addFile( - LayerType.CLASSES, - Paths.get("test.class"), - appRoot.resolve("WEB-INF/classes/test.class")) - .addFile( - LayerType.EXTRA_FILES, Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file")) - .build(); - - ImmutableList expectedDependenciesLayer = - ImmutableList.of( - new LayerEntry(Paths.get("myLib.jar"), appRoot.resolve("WEB-INF/lib/myLib.jar"), null)); - ImmutableList expectedSnapshotDependenciesLayer = - ImmutableList.of( - new LayerEntry( - Paths.get("my-SNAPSHOT.jar"), - appRoot.resolve("WEB-INF/lib/my-SNAPSHOT.jar"), - null)); - ImmutableList expectedResourcesLayer = - ImmutableList.of( - new LayerEntry(Paths.get("test.jsp"), appRoot.resolve("test.jsp"), null), - new LayerEntry(Paths.get("META-INF"), appRoot.resolve("META-INF"), null), - new LayerEntry(Paths.get("context.xml"), appRoot.resolve("WEB-INF/context.xml"), null), - new LayerEntry(Paths.get("sub_dir"), appRoot.resolve("WEB-INF/sub_dir"), null)); - ImmutableList expectedClassesLayer = - ImmutableList.of( - new LayerEntry( - Paths.get("test.class"), appRoot.resolve("WEB-INF/classes/test.class"), null)); - ImmutableList expectedExtraLayer = - ImmutableList.of( - new LayerEntry(Paths.get("extra.file"), AbsoluteUnixPath.get("/extra.file"), null)); - - Assert.assertEquals(expectedDependenciesLayer, configurations.getDependencyLayerEntries()); - Assert.assertEquals( - expectedSnapshotDependenciesLayer, configurations.getSnapshotDependencyLayerEntries()); - Assert.assertEquals(expectedResourcesLayer, configurations.getResourceLayerEntries()); - Assert.assertEquals(expectedClassesLayer, configurations.getClassLayerEntries()); - Assert.assertEquals(expectedExtraLayer, configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddDirectoryContents_file() throws IOException { - temporaryFolder.newFile("file"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, path -> true, basePath) - .build(); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceRoot.resolve("file"), basePath.resolve("file"), null)), - configurations.getExtraFilesLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getClassLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getResourceLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getDependencyLayerEntries()); - Assert.assertEquals( - Collections.emptyList(), configurations.getSnapshotDependencyLayerEntries()); - } - - @Test - public void testAddDirectoryContents_emptyDirectory() throws IOException { - temporaryFolder.newFolder("leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.CLASSES, sourceRoot, path -> true, basePath) - .build(); - Assert.assertEquals( - Collections.singletonList( - new LayerEntry(sourceRoot.resolve("leaf"), basePath.resolve("leaf"), null)), - configurations.getClassLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getResourceLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getDependencyLayerEntries()); - Assert.assertEquals( - Collections.emptyList(), configurations.getSnapshotDependencyLayerEntries()); - Assert.assertEquals(Collections.emptyList(), configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddDirectoryContents_directoriesAdded() throws IOException { - temporaryFolder.newFolder("non-empty", "leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.RESOURCES, sourceRoot, path -> true, basePath) - .build(); - Assert.assertEquals( - Arrays.asList( - new LayerEntry(sourceRoot.resolve("non-empty"), basePath.resolve("non-empty"), null), - new LayerEntry( - sourceRoot.resolve("non-empty/leaf"), basePath.resolve("non-empty/leaf"), null)), - configurations.getResourceLayerEntries()); - } - - @Test - public void testAddDirectoryContents_filter() throws IOException { - temporaryFolder.newFile("non-target"); - temporaryFolder.newFolder("sub"); - temporaryFolder.newFile("sub/target"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - - Predicate nameIsTarget = path -> "target".equals(path.getFileName().toString()); - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.DEPENDENCIES, sourceRoot, nameIsTarget, basePath) - .build(); - Assert.assertEquals( - Arrays.asList( - new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub"), null), - new LayerEntry(sourceRoot.resolve("sub/target"), basePath.resolve("sub/target"), null)), - configurations.getDependencyLayerEntries()); - } - - @Test - public void testAddDirectoryContents_directoriesForced() throws IOException { - temporaryFolder.newFolder("sub", "leaf"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/path/in/container"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, path -> false, basePath) - .build(); - Assert.assertEquals( - Arrays.asList( - new LayerEntry(sourceRoot.resolve("sub"), basePath.resolve("sub"), null), - new LayerEntry(sourceRoot.resolve("sub/leaf"), basePath.resolve("sub/leaf"), null)), - configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddDirectoryContents_fileAsSourceRoot() throws IOException { - Path sourceFile = temporaryFolder.newFile("foo").toPath(); - - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - JavaLayerConfigurations.Builder builder = JavaLayerConfigurations.builder(); - try { - builder.addDirectoryContents(LayerType.DEPENDENCIES, sourceFile, path -> true, basePath); - Assert.fail(); - } catch (NotDirectoryException ex) { - Assert.assertThat(ex.getMessage(), CoreMatchers.containsString("foo is not a directory")); - } - } - - @Test - public void testAddDirectoryContents_complex() throws IOException { - temporaryFolder.newFile("A.class"); - temporaryFolder.newFile("B.java"); - temporaryFolder.newFolder("example", "dir"); - temporaryFolder.newFile("example/dir/C.class"); - temporaryFolder.newFile("example/C.class"); - temporaryFolder.newFolder("test", "resources", "leaf"); - temporaryFolder.newFile("test/resources/D.java"); - temporaryFolder.newFile("test/D.class"); - - Path sourceRoot = temporaryFolder.getRoot().toPath(); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/base"); - - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents(LayerType.EXTRA_FILES, sourceRoot, isClassFile, basePath) - .build(); - - assertSourcePathsUnordered( - Arrays.asList( - sourceRoot.resolve("A.class"), - sourceRoot.resolve("example"), - sourceRoot.resolve("example/dir"), - sourceRoot.resolve("example/dir/C.class"), - sourceRoot.resolve("example/C.class"), - sourceRoot.resolve("test"), - sourceRoot.resolve("test/resources"), - sourceRoot.resolve("test/resources/leaf"), - sourceRoot.resolve("test/D.class")), - configurations.getExtraFilesLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/base/A.class", - "/base/example", - "/base/example/dir", - "/base/example/dir/C.class", - "/base/example/C.class", - "/base/test", - "/base/test/resources", - "/base/test/resources/leaf", - "/base/test/D.class"), - configurations.getExtraFilesLayerEntries()); - } - - @Test - public void testAddExtraDirectoryContents() throws IOException { - temporaryFolder.newFolder("src", "main", "jib", "folder", "folder2"); - temporaryFolder.newFile("src/main/jib/fileA"); - temporaryFolder.newFile("src/main/jib/fileB"); - temporaryFolder.newFile("src/main/jib/fileC"); - temporaryFolder.newFile("src/main/jib/folder/fileD"); - temporaryFolder.newFile("src/main/jib/folder/fileE"); - - Map permissions = - ImmutableMap.of( - AbsoluteUnixPath.get("/fileA"), - FilePermissions.fromOctalString("123"), - AbsoluteUnixPath.get("/fileB"), - FilePermissions.fromOctalString("456"), - AbsoluteUnixPath.get("/folder"), - FilePermissions.fromOctalString("111"), - AbsoluteUnixPath.get("/folder/fileD"), - FilePermissions.fromOctalString("222")); - - Path extraDirectory = - temporaryFolder.getRoot().toPath().resolve("src").resolve("main").resolve("jib"); - AbsoluteUnixPath basePath = AbsoluteUnixPath.get("/"); - JavaLayerConfigurations configurations = - JavaLayerConfigurations.builder() - .addDirectoryContents( - LayerType.EXTRA_FILES, extraDirectory, path -> true, basePath, permissions) - .build(); - - Assert.assertEquals( - Arrays.asList( - new LayerEntry( - extraDirectory.resolve("fileA"), - basePath.resolve("fileA"), - FilePermissions.fromOctalString("123")), - new LayerEntry( - extraDirectory.resolve("fileB"), - basePath.resolve("fileB"), - FilePermissions.fromOctalString("456")), - new LayerEntry(extraDirectory.resolve("fileC"), basePath.resolve("fileC"), null), - new LayerEntry( - extraDirectory.resolve("folder"), - basePath.resolve("folder"), - FilePermissions.fromOctalString("111")), - new LayerEntry( - extraDirectory.resolve("folder").resolve("fileD"), - basePath.resolve("folder").resolve("fileD"), - FilePermissions.fromOctalString("222")), - new LayerEntry( - extraDirectory.resolve("folder").resolve("fileE"), - basePath.resolve("folder").resolve("fileE"), - null), - new LayerEntry( - extraDirectory.resolve("folder").resolve("folder2"), - basePath.resolve("folder").resolve("folder2"), - null)), - configurations.getExtraFilesLayerEntries()); - } -} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index da4a7cbf3f..4c3858ab99 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -17,14 +17,14 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; @@ -396,7 +396,7 @@ public void testCreateContainerBuilder_nonDefaultAppRoot() public void testCreateContainerBuilder_defaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { BuildConfiguration configuration = - setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_APP_ROOT); + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_APP_ROOT); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( Arrays.asList( @@ -492,7 +492,7 @@ public void testCreateContainerBuilder_defaultWebAppRoot() setUpWarProject(getResource("gradle/webapp")); BuildConfiguration configuration = - setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), @@ -527,7 +527,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass } @Test @@ -535,7 +535,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass } @Test @@ -543,7 +543,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass } private BuildConfiguration setupBuildConfiguration(String appRoot) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 80d56dae49..387c21c731 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -17,12 +17,12 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 78cd174e43..5694942f43 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.FilePermissions; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index bbd4801e93..ab4167fae3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; @@ -29,7 +30,6 @@ import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -341,8 +341,8 @@ static Set getVolumesSet(RawConfiguration rawConfiguration) /** * Gets the value of the {@code appRoot} parameter. If the parameter is empty, returns {@link - * JavaLayerConfigurations#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link - * JavaLayerConfigurations#DEFAULT_APP_ROOT} for other projects. + * JavaContainerBuilder#DEFAULT_WEB_APP_ROOT} for WAR projects or {@link + * JavaContainerBuilder#DEFAULT_APP_ROOT} for other projects. * * @param rawConfiguration raw configuration data * @param isWarProject whether or not the project is a WAR project @@ -356,8 +356,8 @@ public static AbsoluteUnixPath getAppRootChecked( if (appRoot.isEmpty()) { appRoot = isWarProject - ? JavaLayerConfigurations.DEFAULT_WEB_APP_ROOT - : JavaLayerConfigurations.DEFAULT_APP_ROOT; + ? JavaContainerBuilder.DEFAULT_WEB_APP_ROOT + : JavaContainerBuilder.DEFAULT_APP_ROOT; } try { return AbsoluteUnixPath.get(appRoot); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index f6c3ef3348..845b229581 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -17,13 +17,13 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations.LayerType; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 93817d52a9..ca3ba54a2e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.frontend.JavaLayerConfigurations; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; @@ -103,9 +102,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) - .thenReturn( - Jib.from("base") - .setLayers(JavaLayerConfigurations.builder().build().getLayerConfigurations())); + .thenReturn(Jib.from("base")); Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) From 73197d5ac3afc25efb9cc9e16f1763b38d290671 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 1 Apr 2019 11:34:47 -0400 Subject: [PATCH 0524/2020] Check broken pipe while communicating to registries (#1589) * Check broken pipe while communicating to registries * Reduce visibilty of some Exceptions * Catch IOException * Prevent infinite loop --- .../registry/RegistryBrokenPipeException.java | 29 ++++++++++++++++++ .../jib/registry/RegistryEndpointCaller.java | 25 ++++++++++++++++ .../jib/registry/RegistryErrorException.java | 2 +- .../registry/RegistryNoResponseException.java | 2 +- .../UnexpectedBlobDigestException.java | 2 +- .../registry/RegistryEndpointCallerTest.java | 30 +++++++++++++++++++ 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java new file mode 100644 index 0000000000..dd090e78da --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +/** Thrown when the registry shut down the connection. */ +class RegistryBrokenPipeException extends RegistryException { + + RegistryBrokenPipeException(Throwable cause) { + super( + "I/O error due to broken pipe: the server shut down the connection. " + + "Check the server log if possible. This could also be a proxy issue. For example," + + "a proxy may prevent sending packets that are too large.", + cause); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index a2fa5c0092..ef8c1864ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -35,6 +35,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.security.GeneralSecurityException; +import java.util.Locale; import java.util.function.Function; import javax.annotation.Nullable; import javax.net.ssl.SSLException; @@ -59,6 +60,24 @@ private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); } + // https://github.com/GoogleContainerTools/jib/issues/1316 + @VisibleForTesting + static boolean isBrokenPipe(IOException original) { + Throwable exception = original; + while (exception != null) { + String message = exception.getMessage(); + if (message != null && message.toLowerCase(Locale.US).contains("broken pipe")) { + return true; + } + + exception = exception.getCause(); + if (exception == original) { // just in case if there's a circular chain + return false; + } + } + return false; + } + private final EventDispatcher eventDispatcher; private final URL initialRequestUrl; private final String userAgent; @@ -292,6 +311,12 @@ private T call(URL url, Function connectionFactory) } } catch (NoHttpResponseException ex) { throw new RegistryNoResponseException(ex); + + } catch (IOException ex) { + if (isBrokenPipe(ex)) { + throw new RegistryBrokenPipeException(ex); + } + throw ex; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java index 757f71b65c..8886636800 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java @@ -22,7 +22,7 @@ * Thrown when an HTTP request to a registry endpoint failed with errors as defined in {@link * ErrorCodes}. */ -public class RegistryErrorException extends RegistryException { +class RegistryErrorException extends RegistryException { RegistryErrorException(String message, @Nullable Throwable cause) { super(message, cause); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java index 6022614ce5..068b180778 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry; /** Thrown when a registry did not respond. */ -public class RegistryNoResponseException extends RegistryException { +class RegistryNoResponseException extends RegistryException { RegistryNoResponseException(Throwable cause) { super(cause); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java index 98ca83fe5b..d73a2243ac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry; /** Thrown when a pulled BLOB did not have the same digest as requested. */ -public class UnexpectedBlobDigestException extends RegistryException { +class UnexpectedBlobDigestException extends RegistryException { UnexpectedBlobDigestException(String message) { super(message); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 3182d7b198..a0134b7bf7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -32,11 +32,13 @@ import java.io.IOException; import java.net.ConnectException; import java.net.MalformedURLException; +import java.net.SocketException; import java.net.URL; import java.util.Collections; import java.util.List; import java.util.function.Function; import javax.annotation.Nullable; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.NoHttpResponseException; import org.hamcrest.CoreMatchers; @@ -485,6 +487,34 @@ public void testHttpTimeout() throws IOException, RegistryException { Assert.assertEquals(Integer.valueOf(7593), mockConnection.getRequestedHttpTimeout()); } + @Test + public void testIsBrokenPipe_notBrokenPipe() { + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new IOException())); + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new SocketException())); + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(new SSLException("mock"))); + } + + @Test + public void testIsBrokenPipe_brokenPipe() { + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new IOException("cool broken pipe !"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new SocketException("BROKEN PIPE"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(new SSLException("calm BrOkEn PiPe"))); + } + + @Test + public void testIsBrokenPipe_nestedBrokenPipe() { + IOException exception = new IOException(new SSLException(new SocketException("Broken pipe"))); + Assert.assertTrue(RegistryEndpointCaller.isBrokenPipe(exception)); + } + + @Test + public void testIsBrokenPipe_terminatesWhenCauseIsOriginal() { + IOException exception = Mockito.mock(IOException.class); + Mockito.when(exception.getCause()).thenReturn(exception); + + Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(exception)); + } + /** * Verifies that a response with {@code httpStatusCode} throws {@link * RegistryUnauthorizedException}. From e5e82d24580fc1b30cc6a584c388a35a49423877 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 1 Apr 2019 17:26:29 -0400 Subject: [PATCH 0525/2020] Better error output when JSON error parsing fails (#1591) --- .../jib/registry/RegistryEndpointCaller.java | 37 +++++++++++++------ .../registry/RegistryEndpointCallerTest.java | 35 ++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index ef8c1864ca..9898a4cb96 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -263,17 +263,7 @@ private T call(URL url, Function connectionFactory) || httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_METHOD_NOT_ALLOWED) { // The name or reference was invalid. - ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson( - httpResponseException.getContent(), ErrorResponseTemplate.class); - RegistryErrorExceptionBuilder registryErrorExceptionBuilder = - new RegistryErrorExceptionBuilder( - registryEndpointProvider.getActionDescription(), httpResponseException); - for (ErrorEntryTemplate errorEntry : errorResponse.getErrors()) { - registryErrorExceptionBuilder.addReason(errorEntry); - } - - throw registryErrorExceptionBuilder.build(); + throw newRegistryErrorException(httpResponseException); } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { throw new RegistryUnauthorizedException( @@ -319,4 +309,29 @@ private T call(URL url, Function connectionFactory) throw ex; } } + + @VisibleForTesting + RegistryErrorException newRegistryErrorException(HttpResponseException httpResponseException) { + RegistryErrorExceptionBuilder registryErrorExceptionBuilder = + new RegistryErrorExceptionBuilder( + registryEndpointProvider.getActionDescription(), httpResponseException); + + try { + ErrorResponseTemplate errorResponse = + JsonTemplateMapper.readJson( + httpResponseException.getContent(), ErrorResponseTemplate.class); + for (ErrorEntryTemplate errorEntry : errorResponse.getErrors()) { + registryErrorExceptionBuilder.addReason(errorEntry); + } + } catch (IOException ex) { + registryErrorExceptionBuilder.addReason( + "registry returned error code " + + httpResponseException.getStatusCode() + + "; possible causes include invalid or wrong reference. Actual error output follows:\n" + + httpResponseException.getContent() + + "\n"); + } + + return registryErrorExceptionBuilder.build(); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index a0134b7bf7..a01711d047 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -515,6 +515,41 @@ public void testIsBrokenPipe_terminatesWhenCauseIsOriginal() { Assert.assertFalse(RegistryEndpointCaller.isBrokenPipe(exception)); } + @Test + public void testNewRegistryErrorException_jsonErrorOutput() { + HttpResponseException httpException = Mockito.mock(HttpResponseException.class); + Mockito.when(httpException.getContent()) + .thenReturn( + "{\"errors\": [{\"code\": \"MANIFEST_UNKNOWN\", \"message\": \"manifest unknown\"}]}"); + + RegistryErrorException registryException = + secureEndpointCaller.newRegistryErrorException(httpException); + Assert.assertSame(httpException, registryException.getCause()); + Assert.assertEquals( + "Tried to actionDescription but failed because: manifest unknown | If this is a bug, " + + "please file an issue at https://github.com/GoogleContainerTools/jib/issues/new", + registryException.getMessage()); + } + + @Test + public void testNewRegistryErrorException_nonJsonErrorOutput() { + HttpResponseException httpException = Mockito.mock(HttpResponseException.class); + // Registry returning non-structured error output + Mockito.when(httpException.getContent()).thenReturn(">>>>> (404) page not found <<<<<"); + Mockito.when(httpException.getStatusCode()).thenReturn(404); + + RegistryErrorException registryException = + secureEndpointCaller.newRegistryErrorException(httpException); + Assert.assertSame(httpException, registryException.getCause()); + Assert.assertEquals( + "Tried to actionDescription but failed because: registry returned error code 404; " + + "possible causes include invalid or wrong reference. Actual error output follows:\n" + + ">>>>> (404) page not found <<<<<\n" + + " | If this is a bug, please file an issue at " + + "https://github.com/GoogleContainerTools/jib/issues/new", + registryException.getMessage()); + } + /** * Verifies that a response with {@code httpStatusCode} throws {@link * RegistryUnauthorizedException}. From 2ea9688b74bd79e87f42622cc20709eaa24e89cf Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 2 Apr 2019 16:15:41 -0400 Subject: [PATCH 0526/2020] Log warnings for docker config cred helper exceptions (#1593) --- .../frontend/CredentialRetrieverFactory.java | 3 +- .../DockerConfigCredentialRetriever.java | 21 +++++++-- .../CredentialRetrieverFactoryTest.java | 2 +- .../DockerConfigCredentialRetrieverTest.java | 43 +++++++++++++++---- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 3a9fc29192..e5553121b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -220,7 +220,8 @@ CredentialRetriever dockerConfig( DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { return () -> { try { - Optional dockerConfigCredentials = dockerConfigCredentialRetriever.retrieve(); + Optional dockerConfigCredentials = + dockerConfigCredentialRetriever.retrieve(eventDispatcher); if (dockerConfigCredentials.isPresent()) { dispatchEvent( LogEvent.info( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index cc3c57b310..b43c4bfbb1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -18,6 +18,8 @@ import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; @@ -28,6 +30,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; +import javax.annotation.Nullable; /** * Retrieves registry credentials from the Docker config. @@ -68,27 +71,31 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { /** * Retrieves credentials for a registry. Tries all possible known aliases. * + * @param eventDispatcher an event dispatcher for dispatching log events * @return {@link Credential} found for {@code registry}, or {@link Optional#empty} if not found * @throws IOException if failed to parse the config JSON */ - public Optional retrieve() throws IOException { + public Optional retrieve(@Nullable EventDispatcher eventDispatcher) + throws IOException { if (!Files.exists(dockerConfigFile)) { return Optional.empty(); } DockerConfig dockerConfig = new DockerConfig( JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class)); - return retrieve(dockerConfig); + return retrieve(dockerConfig, eventDispatcher); } /** * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @param dockerConfig the {@link DockerConfig} to retrieve from + * @param eventDispatcher an event dispatcher for dispatching log events * @return the retrieved credentials, or {@code Optional#empty} if none are found */ @VisibleForTesting - Optional retrieve(DockerConfig dockerConfig) { + Optional retrieve( + DockerConfig dockerConfig, @Nullable EventDispatcher eventDispatcher) { for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { // First, tries to find defined auth. String auth = dockerConfig.getAuthFor(registryAlias); @@ -112,7 +119,13 @@ Optional retrieve(DockerConfig dockerConfig) { } catch (IOException | CredentialHelperUnhandledServerUrlException | CredentialHelperNotFoundException ex) { - // Ignores credential helper retrieval exceptions. + // Warns the user that the specified credential helper cannot be used. + if (eventDispatcher != null && ex.getMessage() != null) { + eventDispatcher.dispatch(LogEvent.warn(ex.getMessage())); + if (ex.getCause() != null && ex.getCause().getMessage() != null) { + eventDispatcher.dispatch(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); + } + } } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 9b71ed1f2b..8063515995 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -147,7 +147,7 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException CredentialRetrieverFactory.forImage( ImageReference.of("registry", "repository", null), mockEventDispatcher); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve()) + Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockEventDispatcher)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 3fb9c6d41c..0620dbdd4c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -39,6 +41,7 @@ public class DockerConfigCredentialRetrieverTest { @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; + @Mock private EventDispatcher mockEventDispatcher; private Path dockerConfigFile; @@ -55,7 +58,7 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve().isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventDispatcher).isPresent()); } @Test @@ -63,7 +66,8 @@ public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Optional credentials = + dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("some", credentials.get().getUsername()); Assert.assertEquals("auth", credentials.get().getPassword()); @@ -79,7 +83,7 @@ public void testRetrieve_useCredsStore() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig) + .retrieve(mockDockerConfig, mockEventDispatcher) .orElseThrow(AssertionError::new)); } @@ -93,7 +97,7 @@ public void testRetrieve_useCredsStore_withProtocol() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig) + .retrieve(mockDockerConfig, mockEventDispatcher) .orElseThrow(AssertionError::new)); } @@ -107,16 +111,35 @@ public void testRetrieve_useCredHelper() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig) + .retrieve(mockDockerConfig, mockEventDispatcher) .orElseThrow(AssertionError::new)); } + @Test + public void testRetrieve_useCredHelper_warn() + throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, + IOException { + Mockito.when(mockDockerConfig.getCredentialHelperFor("another registry")) + .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.retrieve()) + .thenThrow( + new CredentialHelperNotFoundException( + Paths.get("docker-credential-path"), new Throwable("cause"))); + + new DockerConfigCredentialRetriever("another registry", dockerConfigFile) + .retrieve(mockDockerConfig, mockEventDispatcher); + + Mockito.verify(mockEventDispatcher) + .dispatch(LogEvent.warn("The system does not have docker-credential-path CLI")); + Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn(" Caused by: cause")); + } + @Test public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve().isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventDispatcher).isPresent()); } @Test @@ -129,7 +152,7 @@ public void testRetrieve_credentialFromAlias() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig) + .retrieve(mockDockerConfig, mockEventDispatcher) .orElseThrow(AssertionError::new)); } @@ -141,7 +164,8 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Optional credentials = + dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); @@ -155,7 +179,8 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(); + Optional credentials = + dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); From b24671885539ed70e0782bb5e6ca69aa6f254e87 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 2 Apr 2019 17:34:12 -0400 Subject: [PATCH 0527/2020] Better error message when using invalid image reference format (#1594) --- .../tools/jib/image/InvalidImageReferenceException.java | 7 +++++++ .../google/cloud/tools/jib/gradle/BuildDockerTask.java | 8 ++++++-- .../google/cloud/tools/jib/gradle/BuildImageTask.java | 8 ++++++-- .../com/google/cloud/tools/jib/gradle/BuildTarTask.java | 8 ++++++-- .../google/cloud/tools/jib/maven/BuildDockerMojo.java | 9 +++++---- .../com/google/cloud/tools/jib/maven/BuildImageMojo.java | 9 +++++---- .../com/google/cloud/tools/jib/maven/BuildTarMojo.java | 9 +++++---- .../tools/jib/plugins/common/HelpfulSuggestions.java | 8 ++++++++ .../tools/jib/plugins/common/HelpfulSuggestionsTest.java | 5 +++++ 9 files changed, 53 insertions(+), 18 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java index 225c8ee75d..32636405df 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java @@ -19,7 +19,14 @@ /** Thrown when attempting to parse an invalid image reference. */ public class InvalidImageReferenceException extends Exception { + private final String reference; + public InvalidImageReferenceException(String reference) { super("Invalid image reference: " + reference); + this.reference = reference; + } + + public String getInvalidReference() { + return reference; } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index e56acb84a4..2454ac5ee2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -87,8 +87,8 @@ public void dockerClient(Action action) { @TaskAction public void buildDocker() - throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException, MainClassInferenceException { + throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, + MainClassInferenceException { Path dockerExecutable = dockerClientParameters.getExecutablePath(); boolean isDockerInstalled = dockerExecutable == null @@ -169,6 +169,10 @@ public void buildDocker() HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); + + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 9bba7a88f9..b821d08462 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -74,8 +74,8 @@ public void setTargetImage(String targetImage) { @TaskAction public void buildImage() - throws InvalidImageReferenceException, IOException, BuildStepsExecutionException, - CacheDirectoryCreationException, MainClassInferenceException { + throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, + MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); TaskCommon.disableHttpLogging(); @@ -148,6 +148,10 @@ public void buildImage() HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); + + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 8468d46d8b..5d7cfd3a46 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -96,8 +96,8 @@ public String getOutputFile() { @TaskAction public void buildTar() - throws InvalidImageReferenceException, BuildStepsExecutionException, IOException, - CacheDirectoryCreationException, MainClassInferenceException { + throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException, + MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); TaskCommon.disableHttpLogging(); @@ -165,6 +165,10 @@ public void buildTar() HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); + + } catch (InvalidImageReferenceException ex) { + throw new GradleException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index abaacb58a1..78ff6cff07 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -165,10 +165,11 @@ targetImageReference, getTargetImageAdditionalTags()) ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); - } catch (InvalidImageReferenceException - | IOException - | CacheDirectoryCreationException - | MainClassInferenceException ex) { + } catch (InvalidImageReferenceException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (IOException | CacheDirectoryCreationException | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 607e3c9e30..164b79cafb 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -157,10 +157,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); - } catch (InvalidImageReferenceException - | IOException - | CacheDirectoryCreationException - | MainClassInferenceException ex) { + } catch (InvalidImageReferenceException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (IOException | CacheDirectoryCreationException | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index b2b9294219..8ede4498f8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -135,10 +135,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); - } catch (InvalidImageReferenceException - | IOException - | CacheDirectoryCreationException - | MainClassInferenceException ex) { + } catch (InvalidImageReferenceException ex) { + throw new MojoExecutionException( + HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } catch (IOException | CacheDirectoryCreationException | MainClassInferenceException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } catch (BuildStepsExecutionException ex) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 6681fbd4f8..4c97e99609 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -75,6 +75,14 @@ public static String forIncompatibleBaseImageJavaVesionForMaven( + " or below"); } + public static String forInvalidImageReference(String reference) { + return suggest( + "Invalid image reference " + reference, + "check that the reference is formatted correctly according to " + + "https://docs.docker.com/engine/reference/commandline/tag/#extended-description\n" + + "For example, slash-separated name components cannot have uppercase letters"); + } + /** * @param messagePrefix the initial message text * @param suggestion a suggested fix for the problem described by {@link #messagePrefix} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index e3d40c4efc..fbfd985e35 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -82,6 +82,11 @@ public void testSuggestions_smoke() { + "parameter, or set maven-compiler-plugin's '' or '' version " + "to 8 or below in your build configuration", HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven(8, 11)); + Assert.assertEquals( + "Invalid image reference gcr.io/invalid_REF, perhaps you should check that the reference " + + "is formatted correctly according to https://docs.docker.com/engine/reference/commandline/tag/#extended-description\n" + + "For example, slash-separated name components cannot have uppercase letters", + HelpfulSuggestions.forInvalidImageReference("gcr.io/invalid_REF")); Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); Assert.assertEquals( "messagePrefix, perhaps you should use a registry that supports HTTPS so credentials can be sent safely, or set the 'sendCredentialsOverHttp' system property to true", From 309120842a0a9120f06d74bd3b945809638f2445 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 3 Apr 2019 11:35:13 -0400 Subject: [PATCH 0528/2020] Remove spring-boot-draft and rework spring-boot-kubernetes example (#1596) --- examples/README.md | 8 +- examples/spring-boot-draft/README.md | 129 ------------- examples/spring-boot-draft/build.gradle | 61 ------- examples/spring-boot-draft/gradle.properties | 2 - examples/spring-boot-draft/pom.xml | 86 --------- examples/spring-boot-draft/settings.gradle | 1 - examples/spring-boot-draft/settings.xml | 14 -- .../java/com/example/jib/JibApplication.java | 20 -- .../src/main/resources/application.properties | 0 .../com/example/jib/JibApplicationTests.java | 16 -- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - examples/spring-boot-kubernetes/gradlew | 172 ------------------ examples/spring-boot-kubernetes/gradlew.bat | 84 --------- .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 .../README.md | 28 ++- .../build.gradle | 0 .../dockerize-spring-boot-jib.gif | Bin .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../mvnw | 0 .../mvnw.cmd | 0 .../pom.xml | 0 .../src/main/java/hello/Application.java | 0 .../src/main/java/hello/HelloController.java | 0 29 files changed, 23 insertions(+), 603 deletions(-) delete mode 100644 examples/spring-boot-draft/README.md delete mode 100644 examples/spring-boot-draft/build.gradle delete mode 100644 examples/spring-boot-draft/gradle.properties delete mode 100644 examples/spring-boot-draft/pom.xml delete mode 100644 examples/spring-boot-draft/settings.gradle delete mode 100644 examples/spring-boot-draft/settings.xml delete mode 100644 examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java delete mode 100644 examples/spring-boot-draft/src/main/resources/application.properties delete mode 100644 examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java delete mode 100644 examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar delete mode 100644 examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.properties delete mode 100755 examples/spring-boot-kubernetes/gradlew delete mode 100644 examples/spring-boot-kubernetes/gradlew.bat rename examples/{spring-boot-kubernetes => spring-boot}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename examples/{spring-boot-kubernetes => spring-boot}/.mvn/wrapper/maven-wrapper.jar (100%) rename examples/{spring-boot-kubernetes => spring-boot}/.mvn/wrapper/maven-wrapper.properties (100%) rename examples/{spring-boot-kubernetes => spring-boot}/README.md (69%) rename examples/{spring-boot-kubernetes => spring-boot}/build.gradle (100%) rename examples/{spring-boot-kubernetes => spring-boot}/dockerize-spring-boot-jib.gif (100%) rename examples/{spring-boot-draft => spring-boot}/gradle/wrapper/gradle-wrapper.jar (100%) rename examples/{spring-boot-draft => spring-boot}/gradle/wrapper/gradle-wrapper.properties (100%) rename examples/{spring-boot-draft => spring-boot}/gradlew (100%) rename examples/{spring-boot-draft => spring-boot}/gradlew.bat (100%) rename examples/{spring-boot-kubernetes => spring-boot}/mvnw (100%) rename examples/{spring-boot-kubernetes => spring-boot}/mvnw.cmd (100%) rename examples/{spring-boot-kubernetes => spring-boot}/pom.xml (100%) rename examples/{spring-boot-kubernetes => spring-boot}/src/main/java/hello/Application.java (100%) rename examples/{spring-boot-kubernetes => spring-boot}/src/main/java/hello/HelloController.java (100%) diff --git a/examples/README.md b/examples/README.md index f0b40698ba..24cd8b7a81 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,13 +18,9 @@ See [multi-module](multi-module) for containerizing projects with multiple modul See [vertx](vertx) for containerizing [Eclipse Vert.x](https://vertx.io/) applications. - - -### Spring Boot Kubernetes example - -See [spring-boot-kubernetes](spring-boot-kubernetes) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). +See [spring-boot](spring-boot) for containerizing a [Spring Boot](https://spring.io/projects/spring-boot) application and running it on [Kubernetes](https://kubernetes.io). ### Ktor example diff --git a/examples/spring-boot-draft/README.md b/examples/spring-boot-draft/README.md deleted file mode 100644 index 8ffc77a1dd..0000000000 --- a/examples/spring-boot-draft/README.md +++ /dev/null @@ -1,129 +0,0 @@ -SpringBoot Demo for using Jib - -## Tips: this is just a draft - -Please contribute if you have good suggestion/contribute if you have suggested edits . - -### Quickstart - -1. Login docker registry with password `hellojib`: - -``` -docker login -u hellojib -``` - -2. Modify configuration: - -Modify `credHelper` to match your platform . - -3. Build the image: - -Run `gradle jib` or `mvn compile jib:build` under project dictionary . - -4. Run container: - -Run `docker run --name hellojib -p 8080:8080 hellojib/hellojib:jib` . - -5. Access application: - -Run `curl localhost:8080` and it will return `"Hello Jib"` . - -### Custom base image and push registry - -You will need to add custom configuration (examples below). See the configuration reference for [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#extended-usage) or [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage) for more details: - -- Gradle - -build.gradle - -```gradle -jib { - from { - image = 'registry.hub.docker.com/openjdk:8-jdk-alpine' - credHelper = 'osxkeychain' - } - to { - image = 'CUSTOM_REGISTRY_URL/YOUR_USER_NAME//hellojib:jib' - credHelper = 'osxkeychain' - // Or use auth like below if you are not login - // Defined in gradle.properties - //auth { - // username = dockerUsername - // password = dockerPassword - //} - } - container { - jvmFlags = ['-Djava.security.egd=file:/dev/./urandom', '-Duser.timezone=GMT+08', '-Xdebug'] - mainClass = 'com.example.jib.JibApplication' - args = ['some args'] - ports = ['8080'] - } -} -``` - -gradle.properties - -```gradle -dockerUsername=YOUR_USERNAME -dockerPassword=YOUR_PASSWORD -``` - -- Maven - -pom.xml - -```xml - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - registry.hub.docker.com/openjdk:8-jdk-alpine - osxkeychain - - - CUSTOM_REGISTRY_URL/YOUR_USER_NAME/hellojib:jib - osxkeychain - - - - -Djava.security.egd=file:/dev/./urandom - -Xdebug - -Duser.timezone=GMT+08 - - com.example.jib.JibApplication - - some args - - - 8080 - - - - - - -``` - -settings.xml(`${MAVEN_HOME}/conf/settings.xml`)(You should config this is you are not login) - -```xml - - - - - CUSTOM_REGISTRY_URL - YOUR_USERNAME - YOUR_PASSWORD - - - -``` - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot-draft)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/spring-boot-draft/build.gradle b/examples/spring-boot-draft/build.gradle deleted file mode 100644 index fa00e826fb..0000000000 --- a/examples/spring-boot-draft/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -buildscript { - ext { - springBootVersion = '2.0.3.RELEASE' - } - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - } -} -plugins { - id 'com.google.cloud.tools.jib' version '1.0.2' -} - -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' - - -group = 'com.example' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile('org.springframework.boot:spring-boot-starter-web') - testCompile('org.springframework.boot:spring-boot-starter-test') -} - - -jib.to.image = 'registry.hub.docker.com/hellojib/hellojib:jib' - -// If you want custom base image and push registry, use below configuration replace above - -//jib { -// from { -// image = 'registry.hub.docker.com/openjdk:8-jdk-alpine' -// credHelper = 'osxkeychain' -// } -// to { -// image = 'registry.hub.docker.com/hellojib/hellojib:jib' -// credHelper = 'osxkeychain' -// // Or use auth like below if you are not login -// // Defined in gradle.properties -// //auth { -// // username = dockerUsername -// // password = dockerPassword -// //} -// } -// container { -// jvmFlags = ['-Djava.security.egd=file:/dev/./urandom', '-Duser.timezone=GMT+08', '-Xdebug'] -// mainClass = 'com.example.jib.JibApplication' -// args = ['some args'] -// ports = ['8080'] -// } -//} diff --git a/examples/spring-boot-draft/gradle.properties b/examples/spring-boot-draft/gradle.properties deleted file mode 100644 index 9b39b2fdda..0000000000 --- a/examples/spring-boot-draft/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -dockerUsername=hellojib -dockerPassword=hellojib \ No newline at end of file diff --git a/examples/spring-boot-draft/pom.xml b/examples/spring-boot-draft/pom.xml deleted file mode 100644 index 2a88f93962..0000000000 --- a/examples/spring-boot-draft/pom.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - 4.0.0 - - com.example - spring-boot - 0.0.1-SNAPSHOT - jar - - SpringBoot - Demo project for Jib with Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 2.0.3.RELEASE - - - - - UTF-8 - UTF-8 - 1.8 - 1.0.2 - - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - registry.hub.docker.com/hellojib/hellojib:jib - osxkeychain - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/spring-boot-draft/settings.gradle b/examples/spring-boot-draft/settings.gradle deleted file mode 100644 index 19b5204418..0000000000 --- a/examples/spring-boot-draft/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'spring-boot' diff --git a/examples/spring-boot-draft/settings.xml b/examples/spring-boot-draft/settings.xml deleted file mode 100644 index 5213b98fb0..0000000000 --- a/examples/spring-boot-draft/settings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - registry.hub.docker.com/hellojib/hellojib:jib - hellojib - {hXGhaewM6RYHG6jQDZjHyA3WVprcYjPQpto1ST8OJwI=} - - - \ No newline at end of file diff --git a/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java b/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java deleted file mode 100644 index 57194b937c..0000000000 --- a/examples/spring-boot-draft/src/main/java/com/example/jib/JibApplication.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.jib; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -@RestController -public class JibApplication { - - public static void main(String[] args) { - SpringApplication.run(JibApplication.class, args); - } - - @RequestMapping("/") - public String helloJib() { - return "Hello Jib"; - } -} diff --git a/examples/spring-boot-draft/src/main/resources/application.properties b/examples/spring-boot-draft/src/main/resources/application.properties deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java b/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java deleted file mode 100644 index 3867c1d687..0000000000 --- a/examples/spring-boot-draft/src/test/java/com/example/jib/JibApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.jib; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class JibApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot-kubernetes/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/examples/spring-boot-kubernetes/gradlew.bat b/examples/spring-boot-kubernetes/gradlew.bat deleted file mode 100644 index f9553162f1..0000000000 --- a/examples/spring-boot-kubernetes/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java b/examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from examples/spring-boot-kubernetes/.mvn/wrapper/MavenWrapperDownloader.java rename to examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.jar b/examples/spring-boot/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.jar rename to examples/spring-boot/.mvn/wrapper/maven-wrapper.jar diff --git a/examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties b/examples/spring-boot/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from examples/spring-boot-kubernetes/.mvn/wrapper/maven-wrapper.properties rename to examples/spring-boot/.mvn/wrapper/maven-wrapper.properties diff --git a/examples/spring-boot-kubernetes/README.md b/examples/spring-boot/README.md similarity index 69% rename from examples/spring-boot-kubernetes/README.md rename to examples/spring-boot/README.md index 23f2a5dbd2..4e2d876830 100644 --- a/examples/spring-boot-kubernetes/README.md +++ b/examples/spring-boot/README.md @@ -1,6 +1,22 @@ -# Dockerize and deploy a Spring Boot application to Kubernetes +# Dockerize a Spring Boot application using Jib -This is an example of how to easily build a Docker image for a Spring Boot application with Jib and deploy it to Kubernetes with `kubectl`. +This is an example of how to easily build a Docker image for a Spring Boot application with Jib. + +## Try it yourself + +You can containerize the application with one of the following commands. + +**Maven:** +```shell +./mvnw compile jib:build -Dimage= +``` + +**Gradle:** +```shell +./gradlew jib --image= +``` + +## Deploying to Kubernetes using `kubectl`

@@ -9,9 +25,7 @@ This is an example of how to easily build a Docker image for a Spring Boot appli

-## Try it yourself - -*Make sure you have `kubectl` [configured with a cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster).* +*Make sure you have `kubectl` installed and [configured with a cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster).* ```shell IMAGE= @@ -28,11 +42,11 @@ curl localhost:8080 > Greetings from Kubernetes! ``` +\* If you are using Gradle, use `./gradlew jib --image=$IMAGE` instead of the `./mvnw` command + Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run+a+%40springboot+app+on+%23Kubernetes+in+seconds+%40kubernetesio+%23jib+%23java&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot-kubernetes&hashtags=docker) -*For Gradle just run `./gradlew jib --image=$IMAGE` instead.* - ## More information Learn [more about Jib](https://github.com/GoogleContainerTools/jib). diff --git a/examples/spring-boot-kubernetes/build.gradle b/examples/spring-boot/build.gradle similarity index 100% rename from examples/spring-boot-kubernetes/build.gradle rename to examples/spring-boot/build.gradle diff --git a/examples/spring-boot-kubernetes/dockerize-spring-boot-jib.gif b/examples/spring-boot/dockerize-spring-boot-jib.gif similarity index 100% rename from examples/spring-boot-kubernetes/dockerize-spring-boot-jib.gif rename to examples/spring-boot/dockerize-spring-boot-jib.gif diff --git a/examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.jar rename to examples/spring-boot/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from examples/spring-boot-draft/gradle/wrapper/gradle-wrapper.properties rename to examples/spring-boot/gradle/wrapper/gradle-wrapper.properties diff --git a/examples/spring-boot-draft/gradlew b/examples/spring-boot/gradlew similarity index 100% rename from examples/spring-boot-draft/gradlew rename to examples/spring-boot/gradlew diff --git a/examples/spring-boot-draft/gradlew.bat b/examples/spring-boot/gradlew.bat similarity index 100% rename from examples/spring-boot-draft/gradlew.bat rename to examples/spring-boot/gradlew.bat diff --git a/examples/spring-boot-kubernetes/mvnw b/examples/spring-boot/mvnw similarity index 100% rename from examples/spring-boot-kubernetes/mvnw rename to examples/spring-boot/mvnw diff --git a/examples/spring-boot-kubernetes/mvnw.cmd b/examples/spring-boot/mvnw.cmd similarity index 100% rename from examples/spring-boot-kubernetes/mvnw.cmd rename to examples/spring-boot/mvnw.cmd diff --git a/examples/spring-boot-kubernetes/pom.xml b/examples/spring-boot/pom.xml similarity index 100% rename from examples/spring-boot-kubernetes/pom.xml rename to examples/spring-boot/pom.xml diff --git a/examples/spring-boot-kubernetes/src/main/java/hello/Application.java b/examples/spring-boot/src/main/java/hello/Application.java similarity index 100% rename from examples/spring-boot-kubernetes/src/main/java/hello/Application.java rename to examples/spring-boot/src/main/java/hello/Application.java diff --git a/examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java b/examples/spring-boot/src/main/java/hello/HelloController.java similarity index 100% rename from examples/spring-boot-kubernetes/src/main/java/hello/HelloController.java rename to examples/spring-boot/src/main/java/hello/HelloController.java From c1ceaef7e0e6817fc090263c3aac01b9e4322fc7 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 3 Apr 2019 13:09:18 -0400 Subject: [PATCH 0529/2020] cleanups (#1597) --- .../tools/jib/filesystem/UserCacheHome.java | 6 ++---- .../registry/credentials/DockerConfig.java | 19 +++++++++++-------- .../DockerConfigCredentialRetriever.java | 2 +- .../jib/filesystem/UserCacheHomeTest.java | 5 ++--- .../tools/jib/gradle/JibExtensionTest.java | 7 +++---- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java index 92ef1d49cc..5f4ea64d36 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java @@ -66,9 +66,7 @@ static Path getCacheHome(Properties properties, Map environment) } String userHome = properties.getProperty("user.home"); - - Path userHomeDirectory = Paths.get(userHome); - Path xdgPath = userHomeDirectory.resolve(".cache"); + Path xdgPath = Paths.get(userHome, ".cache"); String rawOsName = properties.getProperty("os.name"); String osName = rawOsName.toLowerCase(Locale.ENGLISH); @@ -92,7 +90,7 @@ static Path getCacheHome(Properties properties, Map environment) } else if (osName.contains("mac") || osName.contains("darwin")) { // Use '~/Library/Application Support/' for macOS. - Path applicationSupport = userHomeDirectory.resolve("Library").resolve("Application Support"); + Path applicationSupport = Paths.get(userHome, "Library", "Application Support"); if (!Files.exists(applicationSupport)) { logger.warning(applicationSupport + " does not exist"); return xdgPath; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index 64cdb6bb28..2dfd2800b1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -98,19 +98,22 @@ String getAuthFor(String registry) { @Nullable DockerCredentialHelper getCredentialHelperFor(String registry) { List> registryMatchers = getRegistryMatchersFor(registry); - Map.Entry firstMatchInAuths = + + Map.Entry firstAuthMatch = findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); - if (dockerConfigTemplate.getCredsStore() != null && firstMatchInAuths != null) { + if (firstAuthMatch != null && dockerConfigTemplate.getCredsStore() != null) { return new DockerCredentialHelper( - firstMatchInAuths.getKey(), dockerConfigTemplate.getCredsStore()); + firstAuthMatch.getKey(), dockerConfigTemplate.getCredsStore()); } - Map.Entry firstMatchInCredHelpers = + + Map.Entry firstCredHelperMatch = findFirstInMapByKey(dockerConfigTemplate.getCredHelpers(), registryMatchers); - if (firstMatchInCredHelpers == null) { - return null; + if (firstCredHelperMatch != null) { + return new DockerCredentialHelper( + firstCredHelperMatch.getKey(), firstCredHelperMatch.getValue()); } - return new DockerCredentialHelper( - firstMatchInCredHelpers.getKey(), firstMatchInCredHelpers.getValue()); + + return null; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index b43c4bfbb1..6a5d8ff9d4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -53,7 +53,7 @@ public class DockerConfigCredentialRetriever { * href="https://docs.docker.com/engine/reference/commandline/login/#privileged-user-requirement">https://docs.docker.com/engine/reference/commandline/login/#privileged-user-requirement */ private static final Path DOCKER_CONFIG_FILE = - Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("config.json"); + Paths.get(System.getProperty("user.home"), ".docker", "config.json"); private final String registry; private final Path dockerConfigFile; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java index 2da1bcfb24..4d5152fd7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java @@ -59,7 +59,7 @@ public void testGetCacheHome_linux() { fakeProperties.setProperty("os.name", "os is LiNuX"); Assert.assertEquals( - Paths.get(fakeCacheHome).resolve(".cache"), + Paths.get(fakeCacheHome, ".cache"), UserCacheHome.getCacheHome(fakeProperties, Collections.emptyMap())); } @@ -77,8 +77,7 @@ public void testGetCacheHome_windows() { @Test public void testGetCacheHome_mac() throws IOException { - Path libraryApplicationSupport = - Paths.get(fakeCacheHome).resolve("Library").resolve("Application Support"); + Path libraryApplicationSupport = Paths.get(fakeCacheHome, "Library", "Application Support"); Files.createDirectories(libraryApplicationSupport); Properties fakeProperties = new Properties(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 734e7ef8f6..2c2db8a55d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -148,18 +148,17 @@ public void testContainer() { @Test public void testExtraDirectory() { Assert.assertEquals( - fakeProject.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib"), + Paths.get(fakeProject.getProjectDir().getPath(), "src", "main", "jib"), testJibExtension.getExtraDirectory().getPath()); Assert.assertEquals( Collections.emptyMap(), testJibExtension.getExtraDirectory().getPermissions()); testJibExtension.extraDirectory( extraDirectory -> { - extraDirectory.setPath(Paths.get("test").resolve("path").toFile()); + extraDirectory.setPath(Paths.get("test", "path").toFile()); extraDirectory.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); }); - Assert.assertEquals( - Paths.get("test").resolve("path"), testJibExtension.getExtraDirectory().getPath()); + Assert.assertEquals(Paths.get("test", "path"), testJibExtension.getExtraDirectory().getPath()); Assert.assertEquals( ImmutableMap.of("file1", "123", "file2", "456"), testJibExtension.getExtraDirectory().getPermissions()); From 5adda20f649e5660a52eb72504578da01f68a4a1 Mon Sep 17 00:00:00 2001 From: Isaac Asensio Date: Wed, 3 Apr 2019 19:53:07 +0100 Subject: [PATCH 0530/2020] Update README.md to fix Spring Boot example link (#1599) --- README.md | 2 +- examples/spring-boot/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a47d438f7d..de2d3b1346 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For more information, check out the [official blog post](https://cloudplatform.g The [examples](examples) directory includes the following examples (and more). * [helloworld](examples/helloworld) - * [Spring Boot](examples/spring-boot-kubernetes) + * [Spring Boot](examples/spring-boot) * [Micronaut](examples/micronaut) * [Multi-module project](examples/multi-module) * [Spark Java using Java Agent](examples/java-agent) diff --git a/examples/spring-boot/README.md b/examples/spring-boot/README.md index 4e2d876830..d4840218dc 100644 --- a/examples/spring-boot/README.md +++ b/examples/spring-boot/README.md @@ -45,10 +45,10 @@ curl localhost:8080 \* If you are using Gradle, use `./gradlew jib --image=$IMAGE` instead of the `./mvnw` command -Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run+a+%40springboot+app+on+%23Kubernetes+in+seconds+%40kubernetesio+%23jib+%23java&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot-kubernetes&hashtags=docker) +Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Run+a+%40springboot+app+on+%23Kubernetes+in+seconds+%40kubernetesio+%23jib+%23java&url=https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot&hashtags=docker) ## More information Learn [more about Jib](https://github.com/GoogleContainerTools/jib). -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot-kubernetes)](https://github.com/igrigorik/ga-beacon) +[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot)](https://github.com/igrigorik/ga-beacon) From f7aeeeeec4e50651efec7e8f374e8d2b957f4298 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 5 Apr 2019 15:58:33 -0400 Subject: [PATCH 0531/2020] cleanups (#1600) --- examples/dropwizard/pom.xml | 4 +--- examples/ktor/build.gradle.kts | 4 +--- .../cloud/tools/jib/maven/JibPluginConfiguration.java | 8 ++++---- .../cloud/tools/jib/maven/skaffold/FilesMojoTest.java | 6 +++--- .../cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java | 4 ++-- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index f49cfc093d..9fe684bca7 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -70,12 +70,10 @@ 8080
- + -server -Djava.awt.headless=true - -XX:+UnlockExperimentalVMOptions - -XX:+UseCGroupMemoryLimitForHeap -XX:InitialRAMFraction=2 -XX:MinRAMFraction=2 -XX:MaxRAMFraction=2 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 577c069bb6..abdd279b15 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -43,12 +43,10 @@ jib { ports = listOf("8080") mainClass = main_class - // good defauls intended for Java 8 containers + // good defauls intended for Java 8 (>= 8u191) containers jvmFlags = listOf( "-server", "-Djava.awt.headless=true", - "-XX:+UnlockExperimentalVMOptions", - "-XX:+UseCGroupMemoryLimitForHeap", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 10a91d82ec..a58dccde2f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -302,10 +302,10 @@ Set getTargetImageAdditionalTags() { String property = getProperty(PropertyNames.TO_TAGS); List tags = property != null ? ConfigurationPropertyValidator.parseListProperty(property) : to.tags; - String source = property != null ? PropertyNames.TO_TAGS : ""; - tags.forEach( - tag -> - Preconditions.checkArgument(!Strings.isNullOrEmpty(tag), "%s has empty tag", source)); + if (tags.stream().anyMatch(Strings::isNullOrEmpty)) { + String source = property != null ? PropertyNames.TO_TAGS : ""; + throw new IllegalArgumentException(source + " has empty tag"); + } return new HashSet<>(tags); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index e6e5aa6950..6206818426 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -117,8 +117,8 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti complexServiceRoot.resolve("src/main/resources2"), complexServiceRoot.resolve("src/main/other-jib"), // this test expects standard .m2 locations - Paths.get(System.getProperty("user.home")) - .resolve( - ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar"))); + Paths.get( + System.getProperty("user.home"), + ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar"))); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index b2de9aa764..a1fefef110 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -123,8 +123,8 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti complexServiceRoot.resolve("src/main/resources2").toString(), complexServiceRoot.resolve("src/main/other-jib").toString(), // this test expects standard .m2 locations - Paths.get(System.getProperty("user.home")) - .resolve( + Paths.get( + System.getProperty("user.home"), ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar") .toString())); } From 922e6c6756f1cb2b20b352f745532b5f81578127 Mon Sep 17 00:00:00 2001 From: Matt Clegg Date: Mon, 8 Apr 2019 16:53:39 +0200 Subject: [PATCH 0532/2020] Update docs to be more future proof (#1605) --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 46960db638..78d0ea6d87 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -24,7 +24,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ [How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ [How do I enable debugging?](#how-do-i-enable-debugging)\ -[Why is my image created 48 years ago?](#why-is-my-image-created-48-years-ago)\ +[Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ [I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ [How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp) @@ -395,7 +395,7 @@ Using Kubernetes: `kubectl port-forward 5005:5005` Beware: in Java 8 and earlier, specifying only a port meant that the JDWP socket was open to all incoming connections which is insecure. It is recommended to limit the debug port to localhost. -### Why is my image created 48 years ago? +### Why is my image created 48+ years ago? For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. From bcd57ee1c523d395bc740d36ebe2e6d354bfea40 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 10 Apr 2019 10:54:43 -0400 Subject: [PATCH 0533/2020] Set tool name as User-Agent suffix (#1612) --- .../tools/jib/builder/steps/AuthenticatePushStep.java | 1 + .../cloud/tools/jib/builder/steps/PullBaseImageStep.java | 1 + .../cloud/tools/jib/registry/RegistryAuthenticator.java | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 5562946d8a..036e0fcd43 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -94,6 +94,7 @@ public Authorization call() registry, buildConfiguration.getTargetImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + .setUserAgentSuffix(buildConfiguration.getToolName()) .initialize(); if (registryAuthenticator != null) { return registryAuthenticator.setCredential(registryCredential).authenticatePush(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index e94f6169b1..1e96d4a310 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -176,6 +176,7 @@ public BaseImageWithAuthorization call() buildConfiguration.getBaseImageConfiguration().getImageRegistry(), buildConfiguration.getBaseImageConfiguration().getImageRepository()) .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) + .setUserAgentSuffix(buildConfiguration.getToolName()) .initialize(); if (registryAuthenticator == null) { buildConfiguration diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 1dc690afe9..ef6346ca0b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -54,6 +54,7 @@ public static class Initializer { private final String serverUrl; private final String repository; private boolean allowInsecureRegistries = false; + @Nullable private String userAgentSuffix; /** * Instantiates a new initializer for {@link RegistryAuthenticator}. @@ -73,6 +74,11 @@ public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { return this; } + public Initializer setUserAgentSuffix(@Nullable String userAgentSuffix) { + this.userAgentSuffix = userAgentSuffix; + return this; + } + /** * Gets a {@link RegistryAuthenticator} for a custom registry server and repository. * @@ -88,6 +94,7 @@ public RegistryAuthenticator initialize() try { return RegistryClient.factory(eventDispatcher, serverUrl, repository) .setAllowInsecureRegistries(allowInsecureRegistries) + .setUserAgentSuffix(userAgentSuffix) .newRegistryClient() .getRegistryAuthenticator(); From 6b252edf194d87ecf0a7ef281089e4522f39f584 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 10 Apr 2019 10:57:08 -0400 Subject: [PATCH 0534/2020] Set User-Agent HTTP header in RegistryAuthenticator (#1610) --- .../AuthenticationMethodRetriever.java | 6 ++- .../jib/registry/RegistryAuthenticator.java | 15 ++++-- .../tools/jib/registry/RegistryClient.java | 2 +- .../cloud/tools/jib/http/TestWebServer.java | 15 ++++-- .../AuthenticationMethodRetrieverTest.java | 2 +- .../registry/RegistryAuthenticatorTest.java | 49 +++++++++++++++---- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index 8ed80541f2..4ddb1f8bee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -31,10 +31,12 @@ class AuthenticationMethodRetriever implements RegistryEndpointProvider { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + private final String userAgent; AuthenticationMethodRetriever( - RegistryEndpointRequestProperties registryEndpointRequestProperties) { + RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; + this.userAgent = userAgent; } @Nullable @@ -96,7 +98,7 @@ public RegistryAuthenticator handleHttpResponseException( // Parses the header to retrieve the components. try { return RegistryAuthenticator.fromAuthenticationMethod( - authenticationMethod, registryEndpointRequestProperties); + authenticationMethod, registryEndpointRequestProperties, userAgent); } catch (RegistryAuthenticationFailedException ex) { throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index ef6346ca0b..0058a23062 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -138,6 +138,7 @@ public static Initializer initializer( * * @param authenticationMethod the {@code WWW-Authenticate} header value * @param registryEndpointRequestProperties the registry request properties + * @param userAgent the {@code User-Agent} header value to use in later authentication calls * @return a new {@link RegistryAuthenticator} for authenticating with the registry service * @throws RegistryAuthenticationFailedException if authentication fails * @see future) { // do nothing; to make Error Prone happy } + + public String getInputRead() { + return inputRead.toString(); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 27ecd37aea..ff5754f90b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -41,7 +41,7 @@ public class AuthenticationMethodRetrieverTest { private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private final AuthenticationMethodRetriever testAuthenticationMethodRetriever = - new AuthenticationMethodRetriever(fakeRegistryEndpointRequestProperties); + new AuthenticationMethodRetriever(fakeRegistryEndpointRequestProperties, "user-agent"); @Test public void testGetContent() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 95078b0bab..c84fcdbf0f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -17,8 +17,13 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.http.TestWebServer; +import java.io.IOException; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; +import java.security.GeneralSecurityException; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -35,7 +40,8 @@ public void setUp() throws RegistryAuthenticationFailedException { registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties); + registryEndpointRequestProperties, + "user-agent"); } @Test @@ -44,7 +50,8 @@ public void testFromAuthenticationMethod_bearer() RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties); + registryEndpointRequestProperties, + "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl("scope")); @@ -52,7 +59,8 @@ public void testFromAuthenticationMethod_bearer() registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties); + registryEndpointRequestProperties, + "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl("scope")); @@ -111,17 +119,20 @@ public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFa Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties)); + registryEndpointRequestProperties, + "user-agent")); Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties)); + registryEndpointRequestProperties, + "user-agent")); Assert.assertNull( RegistryAuthenticator.fromAuthenticationMethod( "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties)); + registryEndpointRequestProperties, + "user-agent")); } @Test @@ -129,7 +140,8 @@ public void testFromAuthenticationMethod_noBearer() { try { RegistryAuthenticator.fromAuthenticationMethod( "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties); + registryEndpointRequestProperties, + "user-agent"); Assert.fail("Authentication method without 'Bearer ' or 'Basic ' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -143,7 +155,7 @@ public void testFromAuthenticationMethod_noBearer() { public void testFromAuthenticationMethod_noRealm() { try { RegistryAuthenticator.fromAuthenticationMethod( - "Bearer scope=\"somescope\"", registryEndpointRequestProperties); + "Bearer scope=\"somescope\"", registryEndpointRequestProperties, "user-agent"); Assert.fail("Authentication method without 'realm' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -158,10 +170,29 @@ public void testFromAuthenticationMethod_noService() throws MalformedURLException, RegistryAuthenticationFailedException { RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties); + "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties, "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl("scope")); } + + @Test + public void testUserAgent() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(false)) { + try { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"" + server.getEndpoint() + "\"", + registryEndpointRequestProperties, + "Competent-Agent"); + authenticator.authenticatePush(); + } catch (RegistryAuthenticationFailedException ex) { + // Doesn't matter if auth fails. We only examine what we sent. + } + Assert.assertThat( + server.getInputRead(), CoreMatchers.containsString("User-Agent: Competent-Agent")); + } + } } From 4976cb09ffcdfa92dbc2a7e94ae357e56c5701b9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 12 Apr 2019 16:01:30 -0400 Subject: [PATCH 0535/2020] Allow setting permissions/timestamps with LayerConfiguration.addEntryRecursive (#1613) --- jib-core/CHANGELOG.md | 4 + .../jib/configuration/FilePermissions.java | 2 + .../jib/configuration/LayerConfiguration.java | 100 ++++++++++++++---- .../cloud/tools/jib/image/LayerEntry.java | 76 ++++++++----- .../jib/image/ReproducibleLayerBuilder.java | 2 +- .../cloud/tools/jib/cache/CacheTest.java | 5 +- .../jib/cache/LayerEntriesSelectorTest.java | 15 +-- .../configuration/LayerConfigurationTest.java | 88 +++++++++++++-- .../image/ReproducibleLayerBuilderTest.java | 40 +++++-- .../common/JavaContainerBuilderHelper.java | 8 +- 10 files changed, 265 insertions(+), 75 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 4384e3521d..6cf0b1c358 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,8 +5,12 @@ All notable changes to this project will be documented in this file. ### Added +- Overloads for `LayerConfiguration#addEntryRecursive` that take providers allowing for setting file permissions/file modification timestamps on a per-file basis ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) + ### Changed +- `LayerConfiguration` takes file modification time as an `Instant` instead of a `long` + ### Fixed ## 0.9.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java index 89b01eddab..c4a87f9717 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java @@ -25,8 +25,10 @@ /** Represents read/write/execute file permissions for owner, group, and others. */ public class FilePermissions { + /** Default permissions for files added to the container. */ public static final FilePermissions DEFAULT_FILE_PERMISSIONS = new FilePermissions(0644); + /** Default permissions for folders added to the container. */ public static final FilePermissions DEFAULT_FOLDER_PERMISSIONS = new FilePermissions(0755); /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index cd24b27a19..7f5c181549 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -22,9 +22,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nullable; /** Configures how to build a layer in the container image. Instantiate with {@link #builder}. */ public class LayerConfiguration { @@ -67,7 +68,10 @@ public Builder setName(String name) { * @return this */ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { - return addEntry(sourceFile, pathInContainer, null); + return addEntry( + sourceFile, + pathInContainer, + LayerEntry.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, pathInContainer)); } /** @@ -78,19 +82,19 @@ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { * @param sourceFile the source file to add to the layer * @param pathInContainer the path in the container file system corresponding to the {@code * sourceFile} - * @param permissions the file permissions on the container. If null, then default permissions - * are used (644 for files, 755 for directories) - * @param lastModified the modification time of the file + * @param permissions the file permissions on the container * @return this * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS */ public Builder addEntry( - Path sourceFile, - AbsoluteUnixPath pathInContainer, - @Nullable FilePermissions permissions, - long lastModified) { - layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions, lastModified)); - return this; + Path sourceFile, AbsoluteUnixPath pathInContainer, FilePermissions permissions) { + return addEntry( + sourceFile, + pathInContainer, + permissions, + LayerEntry.DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, pathInContainer)); } /** @@ -101,14 +105,20 @@ public Builder addEntry( * @param sourceFile the source file to add to the layer * @param pathInContainer the path in the container file system corresponding to the {@code * sourceFile} - * @param permissions the file permissions on the container. If null, then default permissions - * are used (644 for files, 755 for directories) + * @param permissions the file permissions on the container + * @param lastModifiedTime the file modification timestamp * @return this * @see Builder#addEntry(Path, AbsoluteUnixPath) + * @see FilePermissions#DEFAULT_FILE_PERMISSIONS + * @see FilePermissions#DEFAULT_FOLDER_PERMISSIONS */ public Builder addEntry( - Path sourceFile, AbsoluteUnixPath pathInContainer, @Nullable FilePermissions permissions) { - return addEntry(sourceFile, pathInContainer, permissions, 1000); + Path sourceFile, + AbsoluteUnixPath pathInContainer, + FilePermissions permissions, + Instant lastModifiedTime) { + layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions, lastModifiedTime)); + return this; } /** @@ -128,13 +138,67 @@ public Builder addEntry( */ public Builder addEntryRecursive(Path sourceFile, AbsoluteUnixPath pathInContainer) throws IOException { + return addEntryRecursive( + sourceFile, pathInContainer, LayerEntry.DEFAULT_FILE_PERMISSIONS_PROVIDER); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + BiFunction filePermissionProvider) + throws IOException { + return addEntryRecursive( + sourceFile, + pathInContainer, + filePermissionProvider, + LayerEntry.DEFAULT_MODIFIED_TIME_PROVIDER); + } + + /** + * Adds an entry to the layer. If the source file is a directory, the directory and its contents + * will be added recursively. + * + * @param sourceFile the source file to add to the layer recursively + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param filePermissionProvider a provider that takes a source path and destination path on the + * container and returns the file permissions that should be set for that path + * @param lastModifiedTimeProvider a provider that takes a source path and destination path on + * the container and returns the file modification time that should be set for that path + * @return this + * @throws IOException if an exception occurred when recursively listing the directory + */ + public Builder addEntryRecursive( + Path sourceFile, + AbsoluteUnixPath pathInContainer, + BiFunction filePermissionProvider, + BiFunction lastModifiedTimeProvider) + throws IOException { + FilePermissions permissions = filePermissionProvider.apply(sourceFile, pathInContainer); + Instant modifiedTime = lastModifiedTimeProvider.apply(sourceFile, pathInContainer); + addEntry(sourceFile, pathInContainer, permissions, modifiedTime); if (!Files.isDirectory(sourceFile)) { - return addEntry(sourceFile, pathInContainer); + return this; } - addEntry(sourceFile, pathInContainer); try (Stream files = Files.list(sourceFile)) { for (Path file : files.collect(Collectors.toList())) { - addEntryRecursive(file, pathInContainer.resolve(file.getFileName())); + addEntryRecursive( + file, + pathInContainer.resolve(file.getFileName()), + filePermissionProvider, + lastModifiedTimeProvider); } } return this; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 8b1da5e10a..8a974b3fed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -20,8 +20,9 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.Objects; -import javax.annotation.Nullable; +import java.util.function.BiFunction; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar @@ -37,10 +38,22 @@ */ public class LayerEntry { + /** Provider that returns default file permissions (644 for files, 755 for directories). */ + public static final BiFunction + DEFAULT_FILE_PERMISSIONS_PROVIDER = + (sourcePath, destinationPath) -> + Files.isDirectory(sourcePath) + ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS + : FilePermissions.DEFAULT_FILE_PERMISSIONS; + + /** Provider that returns default file modification time (EPOCH + 1 second). */ + public static final BiFunction DEFAULT_MODIFIED_TIME_PROVIDER = + (sourcePath, destinationPath) -> Instant.ofEpochSecond(1); + private final Path sourceFile; private final AbsoluteUnixPath extractionPath; private final FilePermissions permissions; - private final long lastModifiedTime; + private final Instant lastModifiedTime; /** * Instantiates with a source file and the path to place the source file in the container file @@ -57,25 +70,12 @@ public class LayerEntry { * @param sourceFile the source file to add to the layer * @param extractionPath the path in the container file system corresponding to the {@code * sourceFile} - * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 - * for files, 755 for directories) - * @param lastModifiedTime the file modification time, default to 1 second since the epoch - * (https://github.com/GoogleContainerTools/jib/issues/1079) */ - public LayerEntry( - Path sourceFile, - AbsoluteUnixPath extractionPath, - @Nullable FilePermissions permissions, - long lastModifiedTime) { - this.sourceFile = sourceFile; - this.extractionPath = extractionPath; - this.permissions = - permissions == null - ? Files.isDirectory(sourceFile) - ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS - : FilePermissions.DEFAULT_FILE_PERMISSIONS - : permissions; - this.lastModifiedTime = lastModifiedTime; + public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath) { + this( + sourceFile, + extractionPath, + DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, extractionPath)); } /** @@ -88,9 +88,34 @@ public LayerEntry( * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 * for files, 755 for directories) */ + public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath, FilePermissions permissions) { + this( + sourceFile, + extractionPath, + permissions, + DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, extractionPath)); + } + + /** + * Instantiates with a source file and the path to place the source file in the container file + * system. + * + * @param sourceFile the source file to add to the layer + * @param extractionPath the path in the container file system corresponding to the {@code + * sourceFile} + * @param permissions the file permissions on the container + * @param lastModifiedTime the file modification time, default to 1 second since the epoch + * (https://github.com/GoogleContainerTools/jib/issues/1079) + */ public LayerEntry( - Path sourceFile, AbsoluteUnixPath extractionPath, @Nullable FilePermissions permissions) { - this(sourceFile, extractionPath, permissions, 1000); + Path sourceFile, + AbsoluteUnixPath extractionPath, + FilePermissions permissions, + Instant lastModifiedTime) { + this.sourceFile = sourceFile; + this.extractionPath = extractionPath; + this.permissions = permissions; + this.lastModifiedTime = lastModifiedTime; } /** @@ -98,7 +123,7 @@ public LayerEntry( * * @return the modification time */ - public long getLastModifiedTime() { + public Instant getLastModifiedTime() { return lastModifiedTime; } @@ -142,11 +167,12 @@ public boolean equals(Object other) { LayerEntry otherLayerEntry = (LayerEntry) other; return sourceFile.equals(otherLayerEntry.sourceFile) && extractionPath.equals(otherLayerEntry.extractionPath) - && Objects.equals(permissions, otherLayerEntry.permissions); + && Objects.equals(permissions, otherLayerEntry.permissions) + && Objects.equals(lastModifiedTime, otherLayerEntry.lastModifiedTime); } @Override public int hashCode() { - return Objects.hash(sourceFile, extractionPath, permissions); + return Objects.hash(sourceFile, extractionPath, permissions, lastModifiedTime); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 86553bcb06..cbc1fec78f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -107,7 +107,7 @@ public Blob build() { // Sets the entry's permissions by masking out the permission bits from the entry's mode (the // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. entry.setMode((entry.getMode() & ~0777) | layerEntry.getPermissions().getPermissionBits()); - entry.setModTime(layerEntry.getLastModifiedTime()); + entry.setModTime(layerEntry.getLastModifiedTime().toEpochMilli()); uniqueTarArchiveEntries.add(entry); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 224d4baefe..16d3c1f0ab 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -128,11 +128,10 @@ public void setUp() throws IOException { layerEntries1 = ImmutableList.of( new LayerEntry( - directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path"), null), + directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")), new LayerEntry( directory.resolve("another/source/file"), - AbsoluteUnixPath.get("/another/extraction/path"), - null)); + AbsoluteUnixPath.get("/another/extraction/path"))); layerBlob2 = Blobs.from("layerBlob2"); layerDigest2 = digestOf(compress(layerBlob2)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 42bfc0f799..b8181d2e55 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -59,19 +59,15 @@ public void setUp() throws IOException { Path file2 = Files.createFile(folder.resolve("files").resolve("two")); Path file3 = Files.createFile(folder.resolve("gile")); - LayerEntry testLayerEntry1 = - new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path"), null); - LayerEntry testLayerEntry2 = - new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path"), null); - LayerEntry testLayerEntry3 = - new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path"), null); + LayerEntry testLayerEntry1 = new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry2 = new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry3 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); LayerEntry testLayerEntry4 = new LayerEntry( file3, AbsoluteUnixPath.get("/extraction/path"), FilePermissions.fromOctalString("755")); - LayerEntry testLayerEntry5 = - new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha"), null); + LayerEntry testLayerEntry5 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); LayerEntry testLayerEntry6 = new LayerEntry( file3, @@ -135,8 +131,7 @@ public void testGenerateSelector_fileModified() throws IOException { Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); - LayerEntry layerEntry = - new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path"), null); + LayerEntry layerEntry = new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); DescriptorDigest expectedSelector = LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java index 9367b2ee31..0770917665 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -24,6 +24,8 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; +import java.util.function.BiFunction; import org.junit.Assert; import org.junit.Test; @@ -31,7 +33,7 @@ public class LayerConfigurationTest { @Test - public void testAddEntryRecursive() throws IOException, URISyntaxException { + public void testAddEntryRecursive_defaults() throws IOException, URISyntaxException { Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); Path testFile = Paths.get(Resources.getResource("core/fileA").toURI()); @@ -43,20 +45,88 @@ public void testAddEntryRecursive() throws IOException, URISyntaxException { ImmutableSet expectedLayerEntries = ImmutableSet.of( - new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/"), null), - new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/"), null), + new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), + new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), + new LayerEntry(testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), new LayerEntry( - testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/"), null), + testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/")), + new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), + new LayerEntry( + testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat")), + new LayerEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), + new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); + + Assert.assertEquals( + expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); + } + + @Test + public void testAddEntryRecursive_permissionsAndTimestamps() + throws IOException, URISyntaxException { + Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); + Path testFile = Paths.get(Resources.getResource("core/fileA").toURI()); + + FilePermissions permissions1 = FilePermissions.fromOctalString("111"); + FilePermissions permissions2 = FilePermissions.fromOctalString("777"); + Instant timestamp1 = Instant.ofEpochSecond(123); + Instant timestamp2 = Instant.ofEpochSecond(987); + + BiFunction permissionsProvider = + (source, destination) -> + destination.toString().startsWith("/app/layer/a") ? permissions1 : permissions2; + BiFunction timestampProvider = + (source, destination) -> + destination.toString().startsWith("/app/layer/a") ? timestamp1 : timestamp2; + + LayerConfiguration layerConfiguration = + LayerConfiguration.builder() + .addEntryRecursive( + testDirectory, + AbsoluteUnixPath.get("/app/layer/"), + permissionsProvider, + timestampProvider) + .addEntryRecursive( + testFile, + AbsoluteUnixPath.get("/app/fileA"), + permissionsProvider, + timestampProvider) + .build(); + + ImmutableSet expectedLayerEntries = + ImmutableSet.of( + new LayerEntry( + testDirectory, AbsoluteUnixPath.get("/app/layer/"), permissions2, timestamp2), + new LayerEntry( + testDirectory.resolve("a"), + AbsoluteUnixPath.get("/app/layer/a/"), + permissions1, + timestamp1), + new LayerEntry( + testDirectory.resolve("a/b"), + AbsoluteUnixPath.get("/app/layer/a/b/"), + permissions1, + timestamp1), new LayerEntry( testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/"), - null), - new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c"), null), + permissions1, + timestamp1), + new LayerEntry( + testDirectory.resolve("c/"), + AbsoluteUnixPath.get("/app/layer/c"), + permissions2, + timestamp2), new LayerEntry( - testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat"), null), + testDirectory.resolve("c/cat/"), + AbsoluteUnixPath.get("/app/layer/c/cat"), + permissions2, + timestamp2), new LayerEntry( - testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo"), null), - new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"), null)); + testDirectory.resolve("foo"), + AbsoluteUnixPath.get("/app/layer/foo"), + permissions2, + timestamp2), + new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"), permissions2, timestamp2)); Assert.assertEquals( expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 8c79a8dcc0..069448cb12 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -167,14 +167,14 @@ public void testToBlob_reproducibility() throws IOException { Blob layer = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA"), null), - new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB"), null))) + new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), + new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) .build(); Blob reproduced = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB"), null), - new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA"), null))) + new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), + new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) .build(); byte[] layerContent = Blobs.writeToByteArray(layer); @@ -184,12 +184,12 @@ public void testToBlob_reproducibility() throws IOException { } @Test - public void testBuild_timestamp() throws IOException { + public void testBuild_timestampDefault() throws IOException { Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); Blob blob = new ReproducibleLayerBuilder( - ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA"), null))) + ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA")))) .build(); Path tarFile = temporaryFolder.newFile().toPath(); @@ -204,6 +204,32 @@ public void testBuild_timestamp() throws IOException { } } + @Test + public void testBuild_timestampNonDefault() throws IOException { + Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); + + Blob blob = + new ReproducibleLayerBuilder( + ImmutableList.of( + new LayerEntry( + file, + AbsoluteUnixPath.get("/fileA"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + Instant.ofEpochSecond(123)))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + blob.writeTo(out); + } + + // Reads the file back. + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + Assert.assertEquals( + Date.from(Instant.EPOCH.plusSeconds(123)), in.getNextEntry().getLastModifiedDate()); + } + } + @Test public void testBuild_permissions() throws IOException { Path testRoot = temporaryFolder.getRoot().toPath(); @@ -214,7 +240,7 @@ public void testBuild_permissions() throws IOException { Blob blob = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA"), null), + new LayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA")), new LayerEntry( fileB, AbsoluteUnixPath.get("/somewhere/fileB"), diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 5694942f43..63503cc16c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -53,8 +53,12 @@ public static LayerConfiguration extraDirectoryLayerConfiguration( localPath -> { AbsoluteUnixPath pathOnContainer = AbsoluteUnixPath.get("/").resolve(extraDirectory.relativize(localPath)); - builder.addEntry( - localPath, pathOnContainer, extraDirectoryPermissions.get(pathOnContainer)); + FilePermissions permissions = extraDirectoryPermissions.get(pathOnContainer); + if (permissions == null) { + builder.addEntry(localPath, pathOnContainer); + } else { + builder.addEntry(localPath, pathOnContainer, permissions); + } }); return builder.build(); } From 917e2c0e314361c3415f981504cac07df20114bd Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Apr 2019 10:34:17 -0400 Subject: [PATCH 0536/2020] Write after reading input (#1624) --- .../cloud/tools/jib/http/TestWebServer.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 874855ea78..3633b33302 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -17,9 +17,11 @@ package com.google.cloud.tools.jib.http; import com.google.common.io.Resources; +import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.net.URISyntaxException; @@ -43,7 +45,7 @@ public class TestWebServer implements Closeable { private final ServerSocket serverSocket; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final Semaphore threadStarted = new Semaphore(0); - private final StringBuffer inputRead = new StringBuffer(); + private final StringBuilder inputRead = new StringBuilder(); public TestWebServer(boolean https) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { @@ -89,14 +91,18 @@ private ServerSocket createServerSocket(boolean https) private Void serve200() throws IOException { threadStarted.release(); try (Socket socket = serverSocket.accept()) { - String response = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; - socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); - socket.getOutputStream().flush(); InputStream in = socket.getInputStream(); - for (int ch = in.read(); ch != -1; ch = in.read()) { - inputRead.append((char) ch); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + for (String line = reader.readLine(); + line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. + line = reader.readLine()) { + inputRead.append(line + "\n"); } + + String response = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); + socket.getOutputStream().flush(); } return null; } From 2213fbeddda02d1eaf913390dcd0491b13f1e4df Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 15 Apr 2019 14:58:47 -0400 Subject: [PATCH 0537/2020] Gradle release v1.1.0 (#1630) * [Gradle Release Plugin] - pre tag commit: 'v1.1.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.1.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index b9111c5b31..841073e90b 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.0.3-SNAPSHOT +version = 1.1.1-SNAPSHOT From a420e973f9f229fb7f916e23c9c09c84c777c461 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 15 Apr 2019 15:04:05 -0400 Subject: [PATCH 0538/2020] Maven release v1.1.0 (#1631) * preparing release 1.1.0 * 1.1.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0bae4b587d..1f42eb8f58 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.3-SNAPSHOT + 1.1.1-SNAPSHOT maven-plugin Jib From aece7c60b6e1b65332be6d192d697a7ac5e096ec Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 15 Apr 2019 16:41:47 -0400 Subject: [PATCH 0539/2020] Update version strings for 1.1.0 (#1632) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 17 files changed, 32 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fed82d122..10ea8e58a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.0.3-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.0.2' + // id 'com.google.cloud.tools.jib' version '1.1.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 9fe684bca7..96804b14f6 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.0.2 + 1.1.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 39ffac5c54..d1c7746c2e 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 19d141bc41..5127e05fd0 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.2 + 1.1.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 03342b393d..9f1dbb8bee 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index af6cffec44..0ab5c0f5ba 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.0.2 + 1.1.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index abdd279b15..a643ae81a4 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.0.2" + id("com.google.cloud.tools.jib") version "1.1.0" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 32a732ec83..c7999614f8 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 6074dca10f..e304aa6894 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.0.2' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 364d85d8bd..167284eb81 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.2 + 1.1.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 894c036993..3e3750d14e 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 3054eb98a8..0ce0d8898a 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.0.2 + 1.1.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index cfeec32082..9a98bf8988 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index b31178d6c0..208621a401 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.1.0 + ### Changed - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index a89d2fbcb3..fc1d42d7b3 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.0.2' + id 'com.google.cloud.tools.jib' version '1.1.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ccd76b07c5..a11bcfdeee 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. ## [unreleased] +### Added + +### Changed + +### Fixed + +## 1.1.0 + ### Added - Can now decrypt proxy configurations in `settings.xml`. ([#1369](https://github.com/GoogleContainerTools/jib/issues/1369)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 616e7fbcc1..cb23f326b7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.2:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.0.2:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.0.2 + 1.1.0 myimage From ee6cbc2ff7a93df52d3debafccaa3a32f10000de Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 16 Apr 2019 11:13:26 -0400 Subject: [PATCH 0540/2020] Update faq to include link to "build containers the hard way" (#1620) * Update faq.md --- docs/faq.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 78d0ea6d87..3987bd37dd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -26,7 +26,8 @@ If a question you have is not answered below, please [submit an issue](/../../is [How do I enable debugging?](#how-do-i-enable-debugging)\ [Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ [I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ -[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp) +[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp)\ +[Can I learn more about container images?](#can-i-learn-more-about-container-images) ### But, I'm not a Java developer. @@ -479,3 +480,7 @@ To tag the image with a timestamp, simply set the timestamp as the tag for `to.i ```groovy jib.to.image = 'gcr.io/my-gcp-project/my-app:' + System.nanoTime() ``` + +### Can I learn more about container images? + +If you'd like to learn more about container images, [@coollog](https://github.com/coollog) has a guide: [Build Containers the Hard Way](https://containers.gitbook.io/build-containers-the-hard-way/), which takes a deep dive into everything involved in getting your code into a container and onto a container registry. From f36e905fcc353037d8685c2feae11232e37fcbe8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Apr 2019 12:24:01 -0400 Subject: [PATCH 0541/2020] Reverse manifest V21 layers when adding (#1633) --- jib-core/CHANGELOG.md | 4 ++++ .../tools/jib/image/json/JsonToImageTranslator.java | 4 +++- .../jib/image/json/JsonToImageTranslatorTest.java | 8 ++++++-- .../src/test/resources/core/json/v21manifest.json | 12 +++++++++++- jib-gradle-plugin/CHANGELOG.md | 4 ++++ jib-maven-plugin/CHANGELOG.md | 5 +++++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 6cf0b1c358..ad878877da 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + ## 0.9.0 ### Added @@ -22,10 +24,12 @@ All notable changes to this project will be documented in this file. - Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files ### Changed + - Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed + - `ImageReference` assumes `registry-1.docker.io` as the registry if the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 0.1.2 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index ac1e8e193e..56b792d4c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeParseException; @@ -72,7 +73,8 @@ public static Image toImage(V21ManifestTemplate manifestTemplate) throws LayerPropertyNotFoundException { Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); - for (DescriptorDigest digest : manifestTemplate.getLayerDigests()) { + // V21 layers are in reverse order of V22. (The first layer is the latest one.) + for (DescriptorDigest digest : Lists.reverse(manifestTemplate.getLayerDigests())) { imageBuilder.addLayer(new DigestOnlyLayer(digest)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 7eea1d466e..1cd9465d63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -59,11 +59,15 @@ public void testToImage_v21() Image image = JsonToImageTranslator.toImage(manifestTemplate); List layers = image.getLayers(); - Assert.assertEquals(1, layers.size()); + Assert.assertEquals(2, layers.size()); Assert.assertEquals( DescriptorDigest.fromDigest( - "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + "sha256:5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3"), layers.get(0).getBlobDescriptor().getDigest()); + Assert.assertEquals( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + layers.get(1).getBlobDescriptor().getDigest()); } @Test diff --git a/jib-core/src/test/resources/core/json/v21manifest.json b/jib-core/src/test/resources/core/json/v21manifest.json index 4eaa389cf8..1eef70beb3 100644 --- a/jib-core/src/test/resources/core/json/v21manifest.json +++ b/jib-core/src/test/resources/core/json/v21manifest.json @@ -1 +1,11 @@ -{"schemaVersion":1,"fsLayers":[{"blobSum":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"}],"history":[{"v1Compatibility":"some v1-compatible object"}]} \ No newline at end of file +{ + "schemaVersion":1, + "fsLayers": [ + {"blobSum":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"}, + {"blobSum":"sha256:5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3"} + ], + "history": [ + {"v1Compatibility":"some v1-compatible object"}, + {"v1Compatibility":"another v1-compatible object"} + ] +} diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 208621a401..b2f12a7484 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,12 +9,16 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + ## 1.1.0 ### Changed + - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed + - Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a11bcfdeee..a452b378fe 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,15 +9,20 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) + ## 1.1.0 ### Added + - Can now decrypt proxy configurations in `settings.xml`. ([#1369](https://github.com/GoogleContainerTools/jib/issues/1369)) ### Changed + - `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed + - Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 From 1278f0f685643ffb953fd35fc60fbb6761112c54 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Apr 2019 14:28:29 -0400 Subject: [PATCH 0542/2020] Gradle release v1.1.1 (#1637) * [Gradle Release Plugin] - pre tag commit: 'v1.1.1-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.1.2-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 841073e90b..affd23d398 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.1.1-SNAPSHOT +version = 1.1.2-SNAPSHOT From 259554c25256b37f9ed89ca102adf64737c6533f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Apr 2019 14:28:47 -0400 Subject: [PATCH 0543/2020] Maven release v1.1.1 (#1636) * preparing release 1.1.1 * 1.1.2-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 1f42eb8f58..f07143d13e 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.1-SNAPSHOT + 1.1.2-SNAPSHOT maven-plugin Jib From 91e70ddbc9423a3bd414f54a3efe90e444223cea Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Apr 2019 17:38:04 -0400 Subject: [PATCH 0544/2020] Enable configuring multiple extra directories (#1602) * Allow specifying multiple extra directories * Add integration test * Fix bug in _skaffold-files-v2 goal --- .../gradle/SingleProjectIntegrationTest.java | 31 ++++++++++ .../projects/simple/build-extra-dirs.gradle | 18 ++++++ .../projects/simple/build-extra-dirs2.gradle | 21 +++++++ .../simple/src/main/custom-extra-dir2/baz | 1 + .../src/main/java/com/test/HelloWorld.java | 4 ++ .../cloud/tools/jib/gradle/BuildTarTask.java | 11 +++- .../jib/gradle/ExtraDirectoryParameters.java | 45 +++++++++------ .../cloud/tools/jib/gradle/FilesTask.java | 6 +- .../cloud/tools/jib/gradle/FilesTaskV2.java | 6 +- .../jib/gradle/GradleRawConfiguration.java | 4 +- .../cloud/tools/jib/gradle/JibExtension.java | 15 +++-- .../tools/jib/gradle/JibExtensionTest.java | 8 ++- .../jib/maven/JibPluginConfiguration.java | 26 +++++---- .../jib/maven/MavenRawConfiguration.java | 4 +- .../cloud/tools/jib/maven/MojoCommon.java | 26 ++++----- .../tools/jib/maven/skaffold/FilesMojo.java | 23 ++++---- .../tools/jib/maven/skaffold/FilesMojoV2.java | 37 +++++++----- .../maven/BuildImageMojoIntegrationTest.java | 19 +++++++ .../jib/maven/JibPluginConfigurationTest.java | 5 +- .../jib/maven/skaffold/FilesMojoTest.java | 23 +++++++- .../jib/maven/skaffold/FilesMojoV2Test.java | 27 ++++++++- .../maven/projects/simple/pom-extra-dirs.xml | 56 +++++++++++++++++++ .../src/main/java/com/test/HelloWorld.java | 4 ++ .../projects/simple/src/main/jib-custom-2/baz | 1 + .../common/PluginConfigurationProcessor.java | 11 ++-- .../jib/plugins/common/RawConfiguration.java | 2 +- .../PluginConfigurationProcessorTest.java | 5 +- 27 files changed, 339 insertions(+), 100 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom-2/baz diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index d007954722..21638f7caa 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -77,6 +77,14 @@ private static void assertWorkingDirectory(String expected, String imageReferenc .trim()); } + private static void assertLayerSizer(int expected, String imageReference) + throws IOException, InterruptedException { + Command command = + new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); + String layers = command.run().trim(); + Assert.assertEquals(expected, Splitter.on(",").splitToList(layers).size()); + } + /** * Asserts that the test project has the required exposed ports, labels and volumes. * @@ -176,6 +184,7 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("/home", targetImage); + assertLayerSizer(8, targetImage); } @Test @@ -211,6 +220,28 @@ public void testDockerDaemon_simpleWithIncompatibleJava11() } } + @Test + public void testDockerDaemon_simple_multipleExtraDirectories() + throws DigestException, IOException, InterruptedException { + String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); + Assert.assertEquals( + "Hello, world. \nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-extra-dirs.gradle")); + assertLayerSizer(9, targetImage); // one more than usual + } + + @Test + public void testDockerDaemon_simple_multipleExtraDirectoriesWithAlternativeConfig() + throws DigestException, IOException, InterruptedException { + String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); + Assert.assertEquals( + "Hello, world. \nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-extra-dirs2.gradle")); + assertLayerSizer(9, targetImage); // one more than usual + } + @Test public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle new file mode 100644 index 0000000000..d185ff2603 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") +jib.extraDirectory = ['src/main/custom-extra-dir', 'src/main/custom-extra-dir2'] diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle new file mode 100644 index 0000000000..07ebab8972 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") +jib.extraDirectory { + path = files('src/main/custom-extra-dir', 'src/main/custom-extra-dir2') + permissions = ['/baz':'705'] +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz new file mode 100644 index 0000000000..3f95386662 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz @@ -0,0 +1 @@ +baz \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java index cf30f12cdf..4080b0cfca 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java @@ -50,6 +50,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); } + // Prints the contents of the files in the second extra directory. + if (Files.exists(Paths.get("/baz"))) { + System.out.println(new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + } // Prints jvm flags for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5d7cfd3a46..c5c09a81cd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -33,6 +33,7 @@ import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; +import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -79,9 +80,13 @@ public void setTargetImage(String targetImage) { */ @InputFiles public FileCollection getInputFiles() { - return GradleProjectProperties.getInputFiles( - Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPath().toFile(), - getProject()); + List extraDirectories = + Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPaths(); + return extraDirectories + .stream() + .map(Path::toFile) + .map(directory -> GradleProjectProperties.getInputFiles(directory, getProject())) + .reduce(getProject().files(), getProject()::files); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java index 8ff93b188f..2d419becc5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java @@ -22,48 +22,59 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; +import org.gradle.api.Project; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; /** Object in {@link JibExtension} that configures the extra directory. */ public class ExtraDirectoryParameters { - private static Path resolveDefaultExtraDirectory(Path projectDirectory) { - return projectDirectory.resolve("src").resolve("main").resolve("jib"); - } + private final Project project; - private Path path; + private List paths; private Map permissions = Collections.emptyMap(); @Inject - public ExtraDirectoryParameters(Path projectDirectory) { - path = resolveDefaultExtraDirectory(projectDirectory); + public ExtraDirectoryParameters(Project project) { + this.project = project; + paths = + Collections.singletonList( + project.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib")); } @Input - public String getPathString() { + public List getPathStrings() { // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a // String to make them go away. - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { - return System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); - } - return path.toString(); + return getPaths().stream().map(Path::toString).collect(Collectors.toList()); } @Internal - public Path getPath() { + public List getPaths() { // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a // String to make them go away. - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null) { - return Paths.get(System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH)); + String property = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + if (property != null) { + List pathStrings = ConfigurationPropertyValidator.parseListProperty(property); + return pathStrings.stream().map(Paths::get).collect(Collectors.toList()); } - return path; + return paths; } - public void setPath(File path) { - this.path = path.toPath(); + /** + * Sets paths. {@code paths} can be any suitable object describing file paths convertible by + * {@link Project#files} (such as {@code List}). + * + * @param paths paths to set. + */ + // non-plural to retain backward-compatibility for the "jib.extraDirectory.path" config parameter + public void setPath(Object paths) { + this.paths = + project.files(paths).getFiles().stream().map(File::toPath).collect(Collectors.toList()); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java index fde1cf0456..d59523fe4b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java @@ -23,6 +23,7 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -149,9 +150,8 @@ public void listFiles() { printProjectFiles(project); // Print extra layer - if (Files.exists(jibExtension.getExtraDirectory().getPath())) { - System.out.println(jibExtension.getExtraDirectory().getPath()); - } + List extraDirectories = jibExtension.getExtraDirectory().getPaths(); + extraDirectories.stream().filter(Files::exists).forEach(System.out::println); // Find project dependencies Set projectDependencies = findProjectDependencies(project); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java index eb3fdd5ee8..1d8e4a4a6a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java @@ -25,6 +25,7 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -68,9 +69,8 @@ public void listFiles() throws IOException { addProjectFiles(project); // Add extra layer - if (Files.exists(jibExtension.getExtraDirectory().getPath())) { - skaffoldFilesOutput.addInput(jibExtension.getExtraDirectory().getPath()); - } + List extraDirectories = jibExtension.getExtraDirectory().getPaths(); + extraDirectories.stream().filter(Files::exists).forEach(skaffoldFilesOutput::addInput); // Find project dependencies Set projectDependencies = findProjectDependencies(project); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 25d28dbd5d..f9257336ea 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -146,8 +146,8 @@ public Optional getProperty(String propertyName) { } @Override - public Path getExtraDirectory() { - return jibExtension.getExtraDirectory().getPath(); + public List getExtraDirectories() { + return jibExtension.getExtraDirectory().getPaths(); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index a2cdf2a5e8..70d35d5255 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import java.io.File; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; @@ -78,8 +77,7 @@ public JibExtension(Project project) { from = objectFactory.newInstance(BaseImageParameters.class); to = objectFactory.newInstance(TargetImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); - extraDirectory = - objectFactory.newInstance(ExtraDirectoryParameters.class, project.getProjectDir().toPath()); + extraDirectory = objectFactory.newInstance(ExtraDirectoryParameters.class, project); allowInsecureRegistries = objectFactory.property(Boolean.class); @@ -103,8 +101,15 @@ public void extraDirectory(Action action) { action.execute(extraDirectory); } - public void setExtraDirectory(File extraDirectory) { - this.extraDirectory.setPath(extraDirectory); + /** + * Sets extra directory paths. {@code extraDirectories} can be any suitable object describing file + * paths convertible by {@link Project#files} (such as {@code List}). + * + * @param extraDirectories paths to set. + */ + // non-plural to retain backward-compatibility for the "jib.extraDirectory" config parameter + public void setExtraDirectory(Object extraDirectories) { + this.extraDirectory.setPath(extraDirectories); } public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 2c2db8a55d..fb9c99b723 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -148,8 +148,8 @@ public void testContainer() { @Test public void testExtraDirectory() { Assert.assertEquals( - Paths.get(fakeProject.getProjectDir().getPath(), "src", "main", "jib"), - testJibExtension.getExtraDirectory().getPath()); + Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "src", "main", "jib")), + testJibExtension.getExtraDirectory().getPaths()); Assert.assertEquals( Collections.emptyMap(), testJibExtension.getExtraDirectory().getPermissions()); @@ -158,7 +158,9 @@ public void testExtraDirectory() { extraDirectory.setPath(Paths.get("test", "path").toFile()); extraDirectory.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); }); - Assert.assertEquals(Paths.get("test", "path"), testJibExtension.getExtraDirectory().getPath()); + Assert.assertEquals( + Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "test", "path")), + testJibExtension.getExtraDirectory().getPaths()); Assert.assertEquals( ImmutableMap.of("file1", "123", "file2", "456"), testJibExtension.getExtraDirectory().getPermissions()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index a58dccde2f..b3ad9a5b6c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -186,7 +186,10 @@ public static class ContainerParameters { /** Configuration for the {@code extraDirectory} parameter. */ public static class ExtraDirectoryParameters { - @Nullable @Parameter private File path; + // retained for backward-compatibility for ... + @Deprecated @Nullable @Parameter private File path; + + @Parameter private List paths = Collections.emptyList(); @Parameter private List permissions = Collections.emptyList(); @@ -196,13 +199,13 @@ public static class ExtraDirectoryParameters { * * @param path the value to set {@code path} to */ + @Deprecated public void set(File path) { - this.path = path; + this.paths = Collections.singletonList(path); } - @Nullable - public File getPath() { - return path; + public List getPaths() { + return path != null ? Collections.singletonList(path) : paths; } } @@ -503,19 +506,18 @@ String getFormat() { } /** - * Gets the configured extra directory path. + * Gets the list of configured extra directory paths. * - * @return the configured extra directory path + * @return the list of configured extra directory paths */ - Optional getExtraDirectoryPath() { + List getExtraDirectories() { // TODO: Should inform user about nonexistent directory if using custom directory. String property = getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); if (property != null) { - return Optional.of(Paths.get(property)); + List paths = ConfigurationPropertyValidator.parseListProperty(property); + return paths.stream().map(Paths::get).collect(Collectors.toList()); } - return extraDirectory.path == null - ? Optional.empty() - : Optional.of(extraDirectory.path.toPath()); + return extraDirectory.getPaths().stream().map(File::toPath).collect(Collectors.toList()); } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index a94313f4e5..0d791450ef 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -151,8 +151,8 @@ public Optional getProperty(String propertyName) { } @Override - public Path getExtraDirectory() { - return MojoCommon.getExtraDirectoryPath(jibPluginConfiguration); + public List getExtraDirectories() { + return MojoCommon.getExtraDirectories(jibPluginConfiguration); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 95d917221c..e963827a8c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,22 +44,21 @@ static boolean isWarProject(MavenProject project) { } /** - * Gets the extra directory path from a {@link JibPluginConfiguration}. Returns {@code (project - * dir)/src/main/jib} if null. + * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code + * (project dir)/src/main/jib} by default if not configured. * * @param jibPluginConfiguration the build configuration - * @return the resolved extra directory + * @return the list of resolved extra directories */ - static Path getExtraDirectoryPath(JibPluginConfiguration jibPluginConfiguration) { - return jibPluginConfiguration - .getExtraDirectoryPath() - .orElse( - Preconditions.checkNotNull(jibPluginConfiguration.getProject()) - .getBasedir() - .toPath() - .resolve("src") - .resolve("main") - .resolve("jib")); + static List getExtraDirectories(JibPluginConfiguration jibPluginConfiguration) { + List paths = jibPluginConfiguration.getExtraDirectories(); + if (!paths.isEmpty()) { + return paths; + } + + MavenProject project = Preconditions.checkNotNull(jibPluginConfiguration.getProject()); + return Collections.singletonList( + project.getBasedir().toPath().resolve("src").resolve("main").resolve("jib")); } /** diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java index f923cfdacd..3e5a2f6b0e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -21,7 +21,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -105,7 +108,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { // and is expected to run on all projects irrespective of their configuring of the jib plugin). if (project.getPlugin(MavenProjectProperties.PLUGIN_KEY) != null) { // print out extra directory - System.out.println(resolveExtraDirectory()); + resolveExtraDirectories().stream().forEach(System.out::println); } // Grab non-project SNAPSHOT dependencies for this project @@ -150,16 +153,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private Path resolveExtraDirectory() { - if (extraDirectory.getPath() == null) { - return Preconditions.checkNotNull(project) - .getBasedir() - .getAbsoluteFile() - .toPath() - .resolve("src") - .resolve("main") - .resolve("jib"); + private List resolveExtraDirectories() { + List paths = extraDirectory.getPaths(); + if (paths.isEmpty()) { + Path projectBase = + Preconditions.checkNotNull(project).getBasedir().getAbsoluteFile().toPath(); + Path srcMainJib = Paths.get("src", "main", "jib"); + return Collections.singletonList(projectBase.resolve(srcMainJib)); } - return extraDirectory.getPath().getAbsoluteFile().toPath(); + return paths.stream().map(File::getAbsoluteFile).map(File::toPath).collect(Collectors.toList()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index 180d04d637..a73e74ca5a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -25,6 +25,8 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -110,7 +112,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { // configuring of the jib plugin). if (project.getPlugin(MavenProjectProperties.PLUGIN_KEY) != null) { // Add extra directory - skaffoldFilesOutput.addInput(resolveExtraDirectory(project)); + resolveExtraDirectories(project).forEach(skaffoldFilesOutput::addInput); } // Grab non-project SNAPSHOT dependencies for this project @@ -165,12 +167,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private Path resolveExtraDirectory(MavenProject project) { + private List resolveExtraDirectories(MavenProject project) { // Try getting extra directory from project/session properties String extraDirectoryProperty = MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); if (extraDirectoryProperty != null) { - return Paths.get(extraDirectoryProperty); + return Collections.singletonList(Paths.get(extraDirectoryProperty)); } // Try getting extra directory from project pom @@ -180,24 +182,31 @@ private Path resolveExtraDirectory(MavenProject project) { if (pluginConfiguration != null) { Xpp3Dom extraDirectoryConfiguration = pluginConfiguration.getChild("extraDirectory"); if (extraDirectoryConfiguration != null) { - Xpp3Dom child = extraDirectoryConfiguration.getChild("path"); - if (child != null) { + Xpp3Dom pathChild = extraDirectoryConfiguration.getChild("path"); + if (pathChild != null) { // ... - return Paths.get(child.getValue()); + return Collections.singletonList(Paths.get(pathChild.getValue())); + } + Xpp3Dom pathsChild = extraDirectoryConfiguration.getChild("paths"); + if (pathsChild != null) { + // ...... + return Arrays.stream(pathsChild.getChildren()) + .map(Xpp3Dom::getValue) + .map(Paths::get) + .collect(Collectors.toList()); } // ... - return Paths.get(extraDirectoryConfiguration.getValue()); + String value = extraDirectoryConfiguration.getValue(); + if (value != null) { + return Collections.singletonList(Paths.get(extraDirectoryConfiguration.getValue())); + } } } } // Return default if not found - return Preconditions.checkNotNull(project) - .getBasedir() - .getAbsoluteFile() - .toPath() - .resolve("src") - .resolve("main") - .resolve("jib"); + Path projectBase = Preconditions.checkNotNull(project).getBasedir().getAbsoluteFile().toPath(); + Path srcMainJib = Paths.get("src", "main", "jib"); + return Collections.singletonList(projectBase.resolve(srcMainJib)); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 8b3801e552..69ec8cb8a3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -283,6 +283,14 @@ private static void assertWorkingDirectory(String expected, String imageReferenc .trim()); } + private static void assertLayerSizer(int expected, String imageReference) + throws IOException, InterruptedException { + Command command = + new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); + String layers = command.run().trim(); + Assert.assertEquals(expected, Splitter.on(",").splitToList(layers).size()); + } + @Nullable private String detachedContainerName; @Before @@ -344,6 +352,7 @@ public void testExecute_simple() new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); assertWorkingDirectory("/home", targetImage); + assertLayerSizer(8, targetImage); } @Test @@ -398,6 +407,16 @@ public void testExecute_multipleTags() emptyTestProject.getProjectRoot(), targetImage, "maven-2" + System.nanoTime())); } + @Test + public void testExecute_multipleExtraDirectories() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = getGcrImageReference("simpleimage:maven"); + Assert.assertEquals( + "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", + buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-extra-dirs.xml", false)); + assertLayerSizer(9, targetImage); // one more than usual + } + @Test public void testExecute_defaultTarget() throws IOException { // Test error when 'to' is missing diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index db5ac94194..4a96dff604 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; +import java.util.Arrays; import java.util.List; import java.util.Properties; import org.apache.maven.execution.MavenSession; @@ -112,7 +113,7 @@ public void testSystemProperties() { sessionProperties.put("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( - Paths.get("custom-jib"), testPluginConfiguration.getExtraDirectoryPath().get()); + Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); sessionProperties.put("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); List permissions = testPluginConfiguration.getExtraDirectoryPermissions(); @@ -174,7 +175,7 @@ public void testPomProperties() { project.getProperties().setProperty("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( - Paths.get("custom-jib"), testPluginConfiguration.getExtraDirectoryPath().get()); + Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); project .getProperties() .setProperty("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index 6206818426..348109e98b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -44,11 +44,12 @@ public class FilesMojoTest { @ClassRule public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); - private static void verifyFiles(Path projectRoot, String module, List files) + private static void verifyFiles(Path projectRoot, String pomXml, String module, List files) throws VerificationException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setAutoclean(false); + verifier.addCliOption("--file=" + pomXml); verifier.addCliOption("-q"); if (!Strings.isNullOrEmpty(module)) { verifier.addCliOption("-pl"); @@ -73,6 +74,7 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti verifyFiles( projectRoot, + "pom.xml", null, ImmutableList.of( projectRoot.resolve("pom.xml"), @@ -81,6 +83,23 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti projectRoot.resolve("src/main/jib-custom"))); } + @Test + public void testFilesMojo_singleModuleWithMultipleExtraDirectories() + throws VerificationException, IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + + verifyFiles( + projectRoot, + "pom-extra-dirs.xml", + null, + ImmutableList.of( + projectRoot.resolve("pom-extra-dirs.xml"), + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/main/jib-custom"), + projectRoot.resolve("src/main/jib-custom-2"))); + } + @Test public void testFilesMojo_multiModuleSimpleService() throws VerificationException, IOException { Path projectRoot = multiTestProject.getProjectRoot(); @@ -88,6 +107,7 @@ public void testFilesMojo_multiModuleSimpleService() throws VerificationExceptio verifyFiles( projectRoot, + "pom.xml", "simple-service", ImmutableList.of( projectRoot.resolve("pom.xml"), @@ -105,6 +125,7 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti verifyFiles( projectRoot, + "pom.xml", "complex-service", ImmutableList.of( projectRoot.resolve("pom.xml"), diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index a1fefef110..4571bf6506 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -46,11 +46,16 @@ public class FilesMojoV2Test { public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); private static void verifyFiles( - Path projectRoot, String module, List buildFiles, List inputFiles) + Path projectRoot, + String pomXml, + String module, + List buildFiles, + List inputFiles) throws VerificationException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setAutoclean(false); + verifier.addCliOption("--file=" + pomXml); verifier.addCliOption("-q"); if (!Strings.isNullOrEmpty(module)) { verifier.addCliOption("-pl"); @@ -77,6 +82,7 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti verifyFiles( projectRoot, + "pom.xml", null, Collections.singletonList(projectRoot.resolve("pom.xml").toString()), Arrays.asList( @@ -85,6 +91,23 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti projectRoot.resolve("src/main/jib-custom").toString())); } + @Test + public void testFilesMojo_singleModuleWithMultipleExtraDirectories() + throws VerificationException, IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + + verifyFiles( + projectRoot, + "pom-extra-dirs.xml", + null, + Collections.singletonList(projectRoot.resolve("pom-extra-dirs.xml").toString()), + Arrays.asList( + projectRoot.resolve("src/main/java").toString(), + projectRoot.resolve("src/main/resources").toString(), + projectRoot.resolve("src/main/jib-custom").toString(), + projectRoot.resolve("src/main/jib-custom-2").toString())); + } + @Test public void testFilesMojo_multiModuleSimpleService() throws VerificationException, IOException { Path projectRoot = multiTestProject.getProjectRoot(); @@ -92,6 +115,7 @@ public void testFilesMojo_multiModuleSimpleService() throws VerificationExceptio verifyFiles( projectRoot, + "pom.xml", "simple-service", Arrays.asList( projectRoot.resolve("pom.xml").toString(), @@ -110,6 +134,7 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti verifyFiles( projectRoot, + "pom.xml", "complex-service", Arrays.asList( projectRoot.resolve("pom.xml").toString(), diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml new file mode 100644 index 0000000000..3e3f399c62 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + ${_TARGET_IMAGE} + An argument. + 1000/tcp,2000-2003/udp + key1=value1,key2=value2 + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + + ${project.basedir}/src/main/jib-custom + ${project.basedir}/src/main/jib-custom-2 + + + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java index cf30f12cdf..4080b0cfca 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java @@ -50,6 +50,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); } + // Prints the contents of the files in the second extra directory. + if (Files.exists(Paths.get("/baz"))) { + System.out.println(new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + } // Prints jvm flags for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom-2/baz b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom-2/baz new file mode 100644 index 0000000000..3f95386662 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/jib-custom-2/baz @@ -0,0 +1 @@ +baz \ No newline at end of file diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index ab4167fae3..d94f783738 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -206,11 +206,12 @@ static PluginConfigurationProcessor processCommonConfiguration( } // Adds all the extra files. - if (Files.exists(rawConfiguration.getExtraDirectory())) { - jibContainerBuilder.addLayer( - JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( - rawConfiguration.getExtraDirectory(), - rawConfiguration.getExtraDirectoryPermissions())); + for (Path directory : rawConfiguration.getExtraDirectories()) { + if (Files.exists(directory)) { + jibContainerBuilder.addLayer( + JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( + directory, rawConfiguration.getExtraDirectoryPermissions())); + } } PluginConfigurationProcessor.configureContainerizer( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index a2fd77aad7..3b6bcb5235 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -74,7 +74,7 @@ public interface RawConfiguration { Optional getProperty(String propertyName); - Path getExtraDirectory(); + List getExtraDirectories(); Map getExtraDirectoryPermissions(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index ca3ba54a2e..0461e2f20a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -94,7 +94,8 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(rawConfiguration.getFromAuth()).thenReturn(authProperty); Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); - Mockito.when(rawConfiguration.getExtraDirectory()).thenReturn(Paths.get("nonexistent/path")); + Mockito.when(rawConfiguration.getExtraDirectories()) + .thenReturn(Arrays.asList(Paths.get("nonexistent/path"))); Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); @@ -144,7 +145,7 @@ public void testPluginConfigurationProcessor_extraDirectory() InvalidWorkingDirectoryException, InvalidImageReferenceException, CacheDirectoryCreationException { Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); - Mockito.when(rawConfiguration.getExtraDirectory()).thenReturn(extraDirectory); + Mockito.when(rawConfiguration.getExtraDirectories()).thenReturn(Arrays.asList(extraDirectory)); Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) .thenReturn( ImmutableMap.of(AbsoluteUnixPath.get("/foo"), FilePermissions.fromOctalString("123"))); From 8fec8d851c1f0a8caa7e417db356f6e8fa8de588 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Apr 2019 18:22:59 -0400 Subject: [PATCH 0545/2020] Update to Jib 1.1.1 (#1639) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 4 ++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 4 ++++ jib-maven-plugin/README.md | 6 +++--- 17 files changed, 26 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10ea8e58a1..e8f2d3e384 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.2-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.1.0' + // id 'com.google.cloud.tools.jib' version '1.1.1' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 96804b14f6..cfd046c8e5 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.1.0 + 1.1.1 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index d1c7746c2e..e290673330 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 5127e05fd0..ed3c56bb38 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.0 + 1.1.1 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 9f1dbb8bee..31db5eb9b2 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 0ab5c0f5ba..29ee3fab54 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.0 + 1.1.1 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index a643ae81a4..0efb67862f 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.1.0" + id("com.google.cloud.tools.jib") version "1.1.1" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index c7999614f8..f6b83dad78 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index e304aa6894..cfa64c8636 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.1' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 167284eb81..6221dc1c0a 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.0 + 1.1.1 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 3e3750d14e..400be93539 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 0ce0d8898a..aea83a93d2 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.0 + 1.1.1 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 9a98bf8988..dee7b699b8 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index b2f12a7484..cd48309990 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.1.1 + +### Fixed + - Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 1.1.0 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index fc1d42d7b3..dba9c3d000 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.1.0' + id 'com.google.cloud.tools.jib' version '1.1.1' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a452b378fe..b44c76bf77 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.1.1 + +### Fixed + - Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 1.1.0 diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index cb23f326b7..35341d8ba2 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.1:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.1.0 + 1.1.1 myimage From ca07cbae99d471f5ad2a0f3ca1c4e798f0d22ed7 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 16 Apr 2019 21:10:58 -0400 Subject: [PATCH 0546/2020] Add helper LogEvent#toString() (#1638) --- .../com/google/cloud/tools/jib/event/events/LogEvent.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java index de7ccb692a..01c86ae184 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java @@ -118,4 +118,9 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(level, message); } + + @Override + public String toString() { + return "LogEvent [level=" + level + ", message=" + message + "]"; + } } From a800df55b0cb644378595773b17d72dd2bc72d83 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 19 Apr 2019 17:13:19 -0400 Subject: [PATCH 0547/2020] Add default timestamp to not-yet-existant parent dirs (#1649) * Add default timestamp to not-yet-existant parent dirs --- .../cloud/tools/jib/image/LayerEntry.java | 3 +- .../jib/image/ReproducibleLayerBuilder.java | 8 +- .../image/ReproducibleLayerBuilderTest.java | 80 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index 8a974b3fed..d691fd4601 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -46,9 +46,10 @@ public class LayerEntry { ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS; + public static final Instant DEFAULT_MODIFIED_TIME = Instant.ofEpochSecond(1); /** Provider that returns default file modification time (EPOCH + 1 second). */ public static final BiFunction DEFAULT_MODIFIED_TIME_PROVIDER = - (sourcePath, destinationPath) -> Instant.ofEpochSecond(1); + (sourcePath, destinationPath) -> DEFAULT_MODIFIED_TIME; private final Path sourceFile; private final AbsoluteUnixPath extractionPath; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index cbc1fec78f..8f2f6748bb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -55,7 +55,8 @@ private static class UniqueTarArchiveEntries { /** * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of - * the parent directories on the extraction path. + * the parent directories on the extraction path, if the parent does not exist. Parent will have + * modified time to set to {@link LayerEntry#DEFAULT_MODIFIED_TIME}. * * @param tarArchiveEntry the {@link TarArchiveEntry} */ @@ -68,7 +69,9 @@ private void add(TarArchiveEntry tarArchiveEntry) { // directories. Path namePath = Paths.get(tarArchiveEntry.getName()); if (namePath.getParent() != namePath.getRoot()) { - add(new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString())); + TarArchiveEntry dir = new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString()); + dir.setModTime(LayerEntry.DEFAULT_MODIFIED_TIME.toEpochMilli()); + add(dir); } entries.add(tarArchiveEntry); @@ -121,6 +124,7 @@ public Blob build() { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); for (TarArchiveEntry entry : sortedFilesystemEntries) { // Strips out all non-reproducible elements from tar archive entries. + // Modified time is configured per entry entry.setGroupId(0); entry.setUserId(0); entry.setUserName(""); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 069448cb12..5b6a70b652 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -183,6 +183,86 @@ public void testToBlob_reproducibility() throws IOException { Assert.assertThat(layerContent, CoreMatchers.is(reproducedLayerContent)); } + @Test + public void testBuild_parentDirBehavior() throws IOException { + Path testRoot = temporaryFolder.getRoot().toPath(); + + // the path doesn't really matter on source files, but these are structured + Path parent = Files.createDirectories(testRoot.resolve("aaa")); + Path fileA = Files.createFile(parent.resolve("fileA")); + Path ignoredParent = Files.createDirectories(testRoot.resolve("bbb-ignored")); + Path fileB = Files.createFile(ignoredParent.resolve("fileB")); + Path fileC = + Files.createFile(Files.createDirectories(testRoot.resolve("ccc-absent")).resolve("fileC")); + + Blob layer = + new ReproducibleLayerBuilder( + ImmutableList.of( + new LayerEntry( + parent, + AbsoluteUnixPath.get("/root/aaa"), + FilePermissions.fromOctalString("111"), + Instant.ofEpochSecond(10)), + new LayerEntry( + fileA, + AbsoluteUnixPath.get("/root/aaa/fileA"), + FilePermissions.fromOctalString("222"), + Instant.ofEpochSecond(20)), + new LayerEntry( + fileB, + AbsoluteUnixPath.get("/root/bbb-ignored/fileB"), + FilePermissions.fromOctalString("333"), + Instant.ofEpochSecond(30)), + new LayerEntry( + ignoredParent, + AbsoluteUnixPath.get("/root/bbb-ignored"), + FilePermissions.fromOctalString("444"), + Instant.ofEpochSecond(40)), + new LayerEntry( + fileC, + AbsoluteUnixPath.get("/root/ccc-absent/file3"), + FilePermissions.fromOctalString("555"), + Instant.ofEpochSecond(50)))) + .build(); + + Path tarFile = temporaryFolder.newFile().toPath(); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarFile))) { + layer.writeTo(out); + } + + try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(tarFile))) { + // root (default folder permissions) + TarArchiveEntry root = in.getNextTarEntry(); + Assert.assertEquals(040755, root.getMode()); + Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); + + // parentAAA (custom permissions, custom timestamp) + TarArchiveEntry rootParentAAA = in.getNextTarEntry(); + Assert.assertEquals(040111, rootParentAAA.getMode()); + Assert.assertEquals(Instant.ofEpochSecond(10), rootParentAAA.getModTime().toInstant()); + + // skip over fileA + in.getNextTarEntry(); + + // parentBBB (default permissions - ignored custom permissions, since fileB added first) + TarArchiveEntry rootParentBBB = in.getNextTarEntry(); + // TODO (#1650): we want 040444 here. + Assert.assertEquals(040755, rootParentBBB.getMode()); + // TODO (#1650): we want Instant.ofEpochSecond(40) here. + Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); + + // skip over fileB + in.getNextTarEntry(); + + // parentCCC (default permissions - no entry provided) + TarArchiveEntry rootParentCCC = in.getNextTarEntry(); + Assert.assertEquals(040755, rootParentCCC.getMode()); + Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); + + // we don't care about fileC + } + } + @Test public void testBuild_timestampDefault() throws IOException { Path file = createFile(temporaryFolder.getRoot().toPath(), "fileA", "some content", 54321); From b6e250b124c95f7fdd92bf84c55801bc4b8da4df Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 19 Apr 2019 19:07:58 -0400 Subject: [PATCH 0548/2020] Update changelog for 1.1.2 and #1649 (#1653) --- jib-core/CHANGELOG.md | 1 + jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-maven-plugin/CHANGELOG.md | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index ad878877da..98a0bbb2dc 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) - Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 0.9.0 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index cd48309990..a81c28fbe2 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.1.2 + +### Fixed + +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) + ## 1.1.1 ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b44c76bf77..da86ad4713 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.1.2 + +### Fixed + +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) + ## 1.1.1 ### Fixed From a36a24d42ba9d226b3b6b0e6846ddff9e91f38d4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 22 Apr 2019 13:14:40 -0400 Subject: [PATCH 0549/2020] Upgrade Jib version to 1.1.2 (#1655) * Upgrade Jib version to 1.1.2 * Update SNAPSHOT versions --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/README.md | 2 +- jib-gradle-plugin/gradle.properties | 2 +- jib-maven-plugin/README.md | 6 +++--- jib-maven-plugin/pom.xml | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8f2d3e384..dff9036760 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.2-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.3-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.1.1' + // id 'com.google.cloud.tools.jib' version '1.1.2' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index cfd046c8e5..6d38d9c382 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.1.1 + 1.1.2 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index e290673330..90a34dc7b5 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index ed3c56bb38..badf7ad98d 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.1 + 1.1.2 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 31db5eb9b2..4016b26f65 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 29ee3fab54..f6c5a97b90 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.1 + 1.1.2 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 0efb67862f..03938293e0 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.1.1" + id("com.google.cloud.tools.jib") version "1.1.2" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index f6b83dad78..9a4b1ec7f6 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index cfa64c8636..88fc77f441 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.1' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 6221dc1c0a..7d41d34057 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.1 + 1.1.2 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 400be93539..2f0513602a 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index aea83a93d2..769a364d81 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.1 + 1.1.2 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index dee7b699b8..81422794a5 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' } repositories { diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index dba9c3d000..d5fea4589b 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.1.1' + id 'com.google.cloud.tools.jib' version '1.1.2' } ``` diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index affd23d398..bc06e9292a 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.1.2-SNAPSHOT +version = 1.1.3-SNAPSHOT diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 35341d8ba2..76bfd429fb 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.2:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.2:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.1.1 + 1.1.2 myimage diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index f07143d13e..c35dbc9c80 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.2-SNAPSHOT + 1.1.3-SNAPSHOT maven-plugin Jib From 5c65b606e4829cf014627145e409172822673daf Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 22 Apr 2019 14:02:10 -0400 Subject: [PATCH 0550/2020] Core release v0.9.1 (#1657) * [Gradle Release Plugin] - pre tag commit: 'v0.9.1-core'. * [Gradle Release Plugin] - new version commit: 'v0.9.2-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 968bf22310..36fff9cf0c 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.9.1-SNAPSHOT +version = 0.9.2-SNAPSHOT From 35d6d5ccd5b0acff7df52277cc2112692279ab17 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 22 Apr 2019 14:42:45 -0400 Subject: [PATCH 0551/2020] Update version strings for core 0.9.1 (#1658) --- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 98a0bbb2dc..9c5010e42f 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.1 + +### Added + - Overloads for `LayerConfiguration#addEntryRecursive` that take providers allowing for setting file permissions/file modification timestamps on a per-file basis ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) ### Changed diff --git a/jib-core/README.md b/jib-core/README.md index cd72c50fa8..e7d3aee94d 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.9.0 + 0.9.1 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.9.0' + compile 'com.google.cloud.tools:jib-core:0.9.1' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index 5a20c550c3..21b0936450 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.9.0' + classpath 'com.google.cloud.tools:jib-core:0.9.1' } } From a9d660b7d4b69c881b32c00b05dcdeb76fb0c500 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 23 Apr 2019 12:11:16 -0400 Subject: [PATCH 0552/2020] Propagate labels from base image / propagate container config in case of V2.1 manifest (#1644) * Propagate config config when using V2.1 manifest * Add test for setting Label --- .../jib/image/json/JsonToImageTranslator.java | 36 ++++++++++-------- .../jib/image/json/V21ManifestTemplate.java | 38 +++++++++++++++---- .../image/json/JsonToImageTranslatorTest.java | 8 +++- .../image/json/V21ManifestTemplateTest.java | 9 ++++- .../test/resources/core/json/v21manifest.json | 2 +- 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 56b792d4c3..6b3b0042d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -68,9 +68,11 @@ public class JsonToImageTranslator { * @param manifestTemplate the template containing the image layers. * @return the translated {@link Image}. * @throws LayerPropertyNotFoundException if adding image layers fails. + * @throws BadContainerConfigurationFormatException if the container configuration is in a bad + * format */ public static Image toImage(V21ManifestTemplate manifestTemplate) - throws LayerPropertyNotFoundException { + throws LayerPropertyNotFoundException, BadContainerConfigurationFormatException { Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); // V21 layers are in reverse order of V22. (The first layer is the latest one.) @@ -78,6 +80,10 @@ public static Image toImage(V21ManifestTemplate manifestTemplate) imageBuilder.addLayer(new DigestOnlyLayer(digest)); } + if (manifestTemplate.getContainerConfiguration() != null) { + configureBuilderWithContainerConfiguration( + imageBuilder, manifestTemplate.getContainerConfiguration()); + } return imageBuilder.build(); } @@ -114,8 +120,6 @@ public static Image toImage( } List diffIds = containerConfigurationTemplate.getDiffIds(); - List historyObjects = containerConfigurationTemplate.getHistory(); - if (layers.size() != diffIds.size()) { throw new LayerCountMismatchException( "Mismatch between image manifest and container configuration"); @@ -129,9 +133,17 @@ public static Image toImage( imageBuilder.addLayer(new ReferenceLayer(noDiffIdLayer.getBlobDescriptor(), diffId)); } - for (HistoryEntry historyObject : historyObjects) { - imageBuilder.addHistory(historyObject); - } + + configureBuilderWithContainerConfiguration(imageBuilder, containerConfigurationTemplate); + return imageBuilder.build(); + } + + private static void configureBuilderWithContainerConfiguration( + Image.Builder imageBuilder, + ContainerConfigurationTemplate containerConfigurationTemplate) + throws BadContainerConfigurationFormatException { + + containerConfigurationTemplate.getHistory().forEach(imageBuilder::addHistory); if (containerConfigurationTemplate.getCreated() != null) { try { @@ -149,13 +161,8 @@ public static Image toImage( imageBuilder.setOs(containerConfigurationTemplate.getOs()); } - if (containerConfigurationTemplate.getContainerEntrypoint() != null) { - imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); - } - - if (containerConfigurationTemplate.getContainerCmd() != null) { - imageBuilder.setProgramArguments(containerConfigurationTemplate.getContainerCmd()); - } + imageBuilder.setEntrypoint(containerConfigurationTemplate.getContainerEntrypoint()); + imageBuilder.setProgramArguments(containerConfigurationTemplate.getContainerCmd()); List baseHealthCheckCommand = containerConfigurationTemplate.getContainerHealthTest(); if (baseHealthCheckCommand != null) { @@ -198,10 +205,9 @@ public static Image toImage( } } + imageBuilder.addLabels(containerConfigurationTemplate.getContainerLabels()); imageBuilder.setWorkingDirectory(containerConfigurationTemplate.getContainerWorkingDir()); imageBuilder.setUser(containerConfigurationTemplate.getContainerUser()); - - return imageBuilder.build(); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java index 03d72a09c6..320b0def8c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java @@ -18,7 +18,9 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -65,11 +67,12 @@ public class V21ManifestTemplate implements ManifestTemplate { /** The list of layer references. */ private final List fsLayers = new ArrayList<>(); - private final List history = new ArrayList<>(); + private final List history = new ArrayList<>(); /** * Template for inner JSON object representing a layer as part of the list of layer references. */ + @VisibleForTesting static class LayerObjectTemplate implements JsonTemplate { @Nullable private DescriptorDigest blobSum; @@ -80,10 +83,12 @@ DescriptorDigest getDigest() { } } - /** Template for inner JSON object representing the V1-compatible format for a layer. */ - private static class V1CompatibilityTemplate implements JsonTemplate { + /** Template for inner JSON object representing history for a layer. */ + private static class HistoryObjectTemplate implements JsonTemplate { - // TODO: Change to its own JSON template that can extract the layer diff ID. + // The value is basically free-form; they may be structured differently in practice, e.g., + // {"architecture": "amd64", "config": {"User": "1001", ...}, "parent": ...} + // {"id": ..., "container_config": {"Cmd":[""]}} @Nullable private String v1Compatibility; } @@ -106,9 +111,28 @@ public List getFsLayers() { return Collections.unmodifiableList(fsLayers); } - @VisibleForTesting + /** + * Attempts to parse the container configuration JSON (of format {@code + * application/vnd.docker.container.image.v1+json}) from the {@code v1Compatibility} value of the + * first {@code history} entry, which corresponds to the latest layer. + * + * @return container configuration if the first history string holds it; {@code null} otherwise + */ @Nullable - String getV1Compatibility(int index) { - return history.get(index).v1Compatibility; + public ContainerConfigurationTemplate getContainerConfiguration() { + try { + if (history.isEmpty()) { + return null; + } + String v1Compatibility = history.get(0).v1Compatibility; + if (v1Compatibility == null) { + return null; + } + + return JsonTemplateMapper.readJson(v1Compatibility, ContainerConfigurationTemplate.class); + } catch (IOException ex) { + // not a container configuration; ignore and continue + return null; + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 1cd9465d63..a480f51bdb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -47,7 +47,8 @@ public class JsonToImageTranslatorTest { @Test public void testToImage_v21() - throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException { + throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException, + BadContainerConfigurationFormatException { // Loads the JSON string. Path jsonFile = Paths.get(getClass().getClassLoader().getResource("core/json/v21manifest.json").toURI()); @@ -113,7 +114,7 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException } @Test - public void testvolumeMapToList() throws BadContainerConfigurationFormatException { + public void testVolumeMapToList() throws BadContainerConfigurationFormatException { ImmutableSortedMap> input = ImmutableSortedMap.of( "/var/job-result-data", ImmutableMap.of(), "/var/log/my-app-logs", ImmutableMap.of()); @@ -221,5 +222,8 @@ private void testToImage_buildable( AbsoluteUnixPath.get("/var/log/my-app-logs")), image.getVolumes()); Assert.assertEquals("tomcat", image.getUser()); + Assert.assertEquals("value1", image.getLabels().get("key1")); + Assert.assertEquals("value2", image.getLabels().get("key2")); + Assert.assertEquals(2, image.getLabels().size()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index 27a1b47f2b..1b2bca70f4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -44,6 +45,12 @@ public void testFromJson() throws URISyntaxException, IOException, DigestExcepti "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), manifestJson.getFsLayers().get(0).getDigest()); - Assert.assertEquals("some v1-compatible object", manifestJson.getV1Compatibility(0)); + ContainerConfigurationTemplate containerConfiguration = + manifestJson.getContainerConfiguration(); + Assert.assertEquals( + Arrays.asList("JAVA_HOME=/opt/openjdk", "PATH=/opt/openjdk/bin"), + containerConfiguration.getContainerEnvironment()); + Assert.assertEquals( + Arrays.asList("/opt/openjdk/bin/java"), containerConfiguration.getContainerEntrypoint()); } } diff --git a/jib-core/src/test/resources/core/json/v21manifest.json b/jib-core/src/test/resources/core/json/v21manifest.json index 1eef70beb3..1bbfa443e8 100644 --- a/jib-core/src/test/resources/core/json/v21manifest.json +++ b/jib-core/src/test/resources/core/json/v21manifest.json @@ -5,7 +5,7 @@ {"blobSum":"sha256:5bd451067f9ab05e97cda8476c82f86d9b69c2dffb60a8ad2fe3723942544ab3"} ], "history": [ - {"v1Compatibility":"some v1-compatible object"}, + {"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Env\":[\"JAVA_HOME=/opt/openjdk\",\"PATH=/opt/openjdk/bin\"],\"Entrypoint\":[\"/opt/openjdk/bin/java\"],\"Cmd\":[\"-version\"]},\"created\":\"2019-04-17T14:02:34.79404123Z\"}"}, {"v1Compatibility":"another v1-compatible object"} ] } From 66b7c751c39fa8f8c67b65c9da6b561ad39bcf49 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 23 Apr 2019 16:28:04 -0400 Subject: [PATCH 0553/2020] Add ability to add paths to the computed classpath (#1642) --- .../builder/BuildStepsIntegrationTest.java | 6 ++- .../frontend/JavaEntrypointConstructor.java | 14 ++---- .../JavaEntrypointConstructorTest.java | 49 +++++-------------- .../gradle/SingleProjectIntegrationTest.java | 12 +++++ .../tools/jib/gradle/ContainerParameters.java | 16 ++++++ .../jib/gradle/GradleRawConfiguration.java | 5 ++ .../tools/jib/gradle/JibExtensionTest.java | 8 +++ .../gradle/projects/simple/build.gradle | 3 ++ .../jib/maven/JibPluginConfiguration.java | 17 +++++++ .../jib/maven/MavenRawConfiguration.java | 5 ++ .../maven/BuildImageMojoIntegrationTest.java | 12 +++++ .../jib/maven/JibPluginConfigurationTest.java | 7 +++ .../maven/projects/simple/pom-complex.xml | 1 + .../common/PluginConfigurationProcessor.java | 19 ++++--- .../jib/plugins/common/PropertyNames.java | 1 + .../jib/plugins/common/RawConfiguration.java | 2 + .../PluginConfigurationProcessorTest.java | 34 +++++++++++-- 17 files changed, 153 insertions(+), 58 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java index d6d2d94506..942a2c92c8 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java @@ -379,8 +379,10 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( ContainerConfiguration containerConfiguration = ContainerConfiguration.builder() .setEntrypoint( - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get("/app"), Collections.emptyList(), "HelloWorld")) + JavaEntrypointConstructor.makeEntrypoint( + JavaEntrypointConstructor.defaultClasspath(AbsoluteUnixPath.get("/app")), + Collections.emptyList(), + "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) .setExposedPorts( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 4605085845..1a8be39d62 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -32,15 +32,11 @@ public class JavaEntrypointConstructor { public static final RelativeUnixPath DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE = RelativeUnixPath.get("libs"); - public static List makeDefaultEntrypoint( - AbsoluteUnixPath appRoot, List jvmFlags, String mainClass) { - return makeEntrypoint( - Arrays.asList( - appRoot.resolve(DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE).toString(), - appRoot.resolve(DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE).toString(), - appRoot.resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE).resolve("*").toString()), - jvmFlags, - mainClass); + public static List defaultClasspath(AbsoluteUnixPath appRoot) { + return Arrays.asList( + appRoot.resolve(DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE).toString(), + appRoot.resolve(DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE).toString(), + appRoot.resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE).resolve("*").toString()); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index ea0fcd9f5d..81bab6cfb8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.frontend; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.common.collect.ImmutableList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -26,51 +26,24 @@ /** Tests for {@link JavaEntrypointConstructor}. */ public class JavaEntrypointConstructorTest { + @Test + public void testDefaultClasspath() { + List classpath = + JavaEntrypointConstructor.defaultClasspath(AbsoluteUnixPath.get("/dir")); + Assert.assertEquals( + ImmutableList.of("/dir/resources", "/dir/classes", "/dir/libs/*"), classpath); + } + @Test public void testMakeEntrypoint() { - String expectedResourcesPath = "/app/resources"; - String expectedClassesPath = "/app/classes"; - String expectedDependenciesPath = "/app/libs/*"; List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag"); String expectedMainClass = "SomeMainClass"; List entrypoint = JavaEntrypointConstructor.makeEntrypoint( - Arrays.asList(expectedResourcesPath, expectedClassesPath, expectedDependenciesPath), - expectedJvmFlags, - expectedMainClass); + Arrays.asList("/d1", "/d2", "/d3"), expectedJvmFlags, expectedMainClass); Assert.assertEquals( - Arrays.asList( - "java", - "-flag", - "anotherFlag", - "-cp", - "/app/resources:/app/classes:/app/libs/*", - "SomeMainClass"), + Arrays.asList("java", "-flag", "anotherFlag", "-cp", "/d1:/d2:/d3", "SomeMainClass"), entrypoint); - - // Checks that this is also the default entrypoint. - Assert.assertEquals( - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get("/app"), expectedJvmFlags, expectedMainClass), - entrypoint); - } - - @Test - public void testMakeDefaultEntrypoint_classpathString() { - // Checks that this is also the default entrypoint. - List entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get("/app"), Collections.emptyList(), "MyMain"); - Assert.assertEquals("/app/resources:/app/classes:/app/libs/*", entrypoint.get(2)); - } - - @Test - public void testMakeDefaultEntrypoint_classpathStringWithNonDefaultAppRoot() { - // Checks that this is also the default entrypoint. - List entrypoint = - JavaEntrypointConstructor.makeDefaultEntrypoint( - AbsoluteUnixPath.get("/my/app"), Collections.emptyList(), "Main"); - Assert.assertEquals("/my/app/resources:/my/app/classes:/my/app/libs/*", entrypoint.get(2)); } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 21638f7caa..2dcefd38ed 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -77,6 +77,15 @@ private static void assertWorkingDirectory(String expected, String imageReferenc .trim()); } + private static void assertEntrypoint(String expected, String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + expected, + new Command("docker", "inspect", "-f", "{{.Config.Entrypoint}}", imageReference) + .run() + .trim()); + } + private static void assertLayerSizer(int expected, String imageReference) throws IOException, InterruptedException { Command command = @@ -184,6 +193,9 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertDockerInspect(targetImage); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("/home", targetImage); + assertEntrypoint( + "[java -cp /d1:/d2:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]", + targetImage); assertLayerSizer(8, targetImage); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 421bd8e5ce..92eecea59e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -37,6 +37,7 @@ public class ContainerParameters { private List jvmFlags = Collections.emptyList(); private Map environment = Collections.emptyMap(); @Nullable private List entrypoint; + private List extraClasspath = Collections.emptyList(); @Nullable private String mainClass; @Nullable private List args; private ImageFormat format = ImageFormat.Docker; @@ -107,6 +108,21 @@ public void setEnvironment(Map environment) { this.environment = environment; } + @Input + @Optional + public List getExtraClasspath() { + if (System.getProperty(PropertyNames.CONTAINER_EXTRA_CLASSPATH) != null) { + return ConfigurationPropertyValidator.parseListProperty( + System.getProperty(PropertyNames.CONTAINER_EXTRA_CLASSPATH)); + } + + return extraClasspath; + } + + public void setExtraClasspath(List classpath) { + this.extraClasspath = classpath; + } + @Input @Nullable @Optional diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index f9257336ea..de7ae2fd03 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -80,6 +80,11 @@ public Optional> getProgramArguments() { return Optional.ofNullable(jibExtension.getContainer().getArgs()); } + @Override + public List getExtraClasspath() { + return jibExtension.getContainer().getExtraClasspath(); + } + @Override public Optional getMainClass() { return Optional.ofNullable(jibExtension.getContainer().getMainClass()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index fb9c99b723..c4ca5edbb9 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -46,6 +46,7 @@ private static void clearProperties() { System.clearProperty("jib.container.args"); System.clearProperty("jib.container.entrypoint"); System.clearProperty("jib.container.environment"); + System.clearProperty("jib.container.extraClasspath"); System.clearProperty("jib.container.format"); System.clearProperty("jib.container.jvmFlags"); System.clearProperty("jib.container.labels"); @@ -112,6 +113,8 @@ public void testTo() { public void testContainer() { Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getJvmFlags()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getEnvironment()); + Assert.assertEquals( + Collections.emptyList(), testJibExtension.getContainer().getExtraClasspath()); Assert.assertNull(testJibExtension.getContainer().getMainClass()); Assert.assertNull(testJibExtension.getContainer().getArgs()); Assert.assertSame(ImageFormat.Docker, testJibExtension.getContainer().getFormat()); @@ -124,6 +127,7 @@ public void testContainer() { container.setJvmFlags(Arrays.asList("jvmFlag1", "jvmFlag2")); container.setEnvironment(ImmutableMap.of("var1", "value1", "var2", "value2")); container.setEntrypoint(Arrays.asList("foo", "bar", "baz")); + container.setExtraClasspath(Arrays.asList("/d1", "/d2", "/d3")); container.setMainClass("mainClass"); container.setArgs(Arrays.asList("arg1", "arg2", "arg3")); container.setPorts(Arrays.asList("1000", "2000-2010", "3000")); @@ -136,6 +140,7 @@ public void testContainer() { Assert.assertEquals(Arrays.asList("jvmFlag1", "jvmFlag2"), container.getJvmFlags()); Assert.assertEquals( ImmutableMap.of("var1", "value1", "var2", "value2"), container.getEnvironment()); + Assert.assertEquals(ImmutableList.of("/d1", "/d2", "/d3"), container.getExtraClasspath()); Assert.assertEquals("mainClass", testJibExtension.getContainer().getMainClass()); Assert.assertEquals(Arrays.asList("arg1", "arg2", "arg3"), container.getArgs()); Assert.assertEquals(Arrays.asList("1000", "2000-2010", "3000"), container.getPorts()); @@ -194,6 +199,9 @@ public void testProperties() { Assert.assertEquals( ImmutableMap.of("env1", "val1", "env2", "val2"), testJibExtension.getContainer().getEnvironment()); + System.setProperty("jib.container.extraClasspath", "/d1,/d2,/d3"); + Assert.assertEquals( + ImmutableList.of("/d1", "/d2", "/d3"), testJibExtension.getContainer().getExtraClasspath()); System.setProperty("jib.container.format", "OCI"); Assert.assertSame(ImageFormat.OCI, testJibExtension.getContainer().getFormat()); System.setProperty("jib.container.jvmFlags", "flag1,flag2,flag3"); diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle index 52d0f7cab5..6a6b57bf7a 100644 --- a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle @@ -21,4 +21,7 @@ jib { extraDirectory { path = file('src/main/custom-extra-dir') } + container { + extraClasspath = ['/d1','/d2'] + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index b3ad9a5b6c..e7407ac78c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -154,6 +154,8 @@ public void set(String image) { /** Configuration for {@code container} parameter. */ public static class ContainerParameters { + // Note: `entrypoint` and `args` are @Nullable to handle inheriting values from the base image + @Parameter private boolean useCurrentTimestamp = false; @Nullable @Parameter private List entrypoint; @@ -162,6 +164,8 @@ public static class ContainerParameters { @Parameter private Map environment = Collections.emptyMap(); + @Parameter private List extraClasspath = Collections.emptyList(); + @Nullable @Parameter private String mainClass; @Nullable @Parameter private List args; @@ -384,6 +388,19 @@ Map getEnvironment() { return container.environment; } + /** + * Gets the extra classpath elements. + * + * @return the extra classpath elements + */ + List getExtraClasspath() { + String property = getProperty(PropertyNames.CONTAINER_EXTRA_CLASSPATH); + if (property != null) { + return ConfigurationPropertyValidator.parseListProperty(property); + } + return container.extraClasspath; + } + /** * Gets the name of the main class. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 0d791450ef..2778a3a3a7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -85,6 +85,11 @@ public Optional> getProgramArguments() { return Optional.ofNullable(jibPluginConfiguration.getArgs()); } + @Override + public List getExtraClasspath() { + return jibPluginConfiguration.getExtraClasspath(); + } + @Override public Optional getMainClass() { return Optional.ofNullable(jibPluginConfiguration.getMainClass()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 69ec8cb8a3..7501a98a30 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -283,6 +283,15 @@ private static void assertWorkingDirectory(String expected, String imageReferenc .trim()); } + private static void assertEntrypoint(String expected, String imageReference) + throws IOException, InterruptedException { + Assert.assertEquals( + expected, + new Command("docker", "inspect", "-f", "{{.Config.Entrypoint}}", imageReference) + .run() + .trim()); + } + private static void assertLayerSizer(int expected, String imageReference) throws IOException, InterruptedException { Command command = @@ -445,6 +454,9 @@ public void testExecute_complex() buildAndRunComplex( targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml")); assertWorkingDirectory("", targetImage); + assertEntrypoint( + "[java -Xms512m -Xdebug -cp /other:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]", + targetImage); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 4a96dff604..64f99e50cf 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -59,6 +59,7 @@ public void execute() {} public void testDefaults() { Assert.assertEquals("", testPluginConfiguration.getAppRoot()); Assert.assertNull(testPluginConfiguration.getWorkingDirectory()); + Assert.assertTrue(testPluginConfiguration.getExtraClasspath().isEmpty()); } @Test @@ -110,6 +111,9 @@ public void testSystemProperties() { Assert.assertEquals("myUser", testPluginConfiguration.getUser()); sessionProperties.put("jib.container.workingDirectory", "working directory"); Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); + sessionProperties.put("jib.container.extraClasspath", "/foo,/bar"); + Assert.assertEquals( + ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); sessionProperties.put("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( @@ -172,6 +176,9 @@ public void testPomProperties() { Assert.assertEquals("myUser", testPluginConfiguration.getUser()); project.getProperties().setProperty("jib.container.workingDirectory", "working directory"); Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); + project.getProperties().setProperty("jib.container.extraClasspath", "/foo,/bar"); + Assert.assertEquals( + ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); project.getProperties().setProperty("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml index f7ce521dd8..6c3a379ef3 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml @@ -59,6 +59,7 @@ An argument. com.test.HelloWorld + /other -Xms512m -Xdebug diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index d94f783738..0d72227ffc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -42,6 +42,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -247,13 +248,19 @@ static PluginConfigurationProcessor processCommonConfiguration( static List computeEntrypoint( RawConfiguration rawConfiguration, ProjectProperties projectProperties) throws MainClassInferenceException, InvalidAppRootException, IOException { + AbsoluteUnixPath appRoot = + getAppRootChecked(rawConfiguration, projectProperties.isWarProject()); + Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); + List rawExtraClasspath = rawConfiguration.getExtraClasspath(); if (rawEntrypoint.isPresent() && !rawEntrypoint.get().isEmpty()) { if (rawConfiguration.getMainClass().isPresent() - || !rawConfiguration.getJvmFlags().isEmpty()) { + || !rawConfiguration.getJvmFlags().isEmpty() + || !rawExtraClasspath.isEmpty()) { new DefaultEventDispatcher(projectProperties.getEventHandlers()) .dispatch( - LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + LogEvent.warn( + "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } if (rawEntrypoint.get().size() == 1 && "INHERIT".equals(rawEntrypoint.get().get(0))) { @@ -266,13 +273,13 @@ static List computeEntrypoint( return null; } - AbsoluteUnixPath appRoot = - getAppRootChecked(rawConfiguration, projectProperties.isWarProject()); + List classpath = new ArrayList<>(rawExtraClasspath); + classpath.addAll(JavaEntrypointConstructor.defaultClasspath(appRoot)); String mainClass = MainClassResolver.resolveMainClass( rawConfiguration.getMainClass().orElse(null), projectProperties); - return JavaEntrypointConstructor.makeDefaultEntrypoint( - appRoot, rawConfiguration.getJvmFlags(), mainClass); + return JavaEntrypointConstructor.makeEntrypoint( + classpath, rawConfiguration.getJvmFlags(), mainClass); } /** diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 5347625eb6..db58421238 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -31,6 +31,7 @@ public class PropertyNames { public static final String TO_AUTH_PASSWORD = "jib.to.auth.password"; public static final String CONTAINER_APP_ROOT = "jib.container.appRoot"; public static final String CONTAINER_ARGS = "jib.container.args"; + public static final String CONTAINER_EXTRA_CLASSPATH = "jib.container.extraClasspath"; public static final String CONTAINER_ENTRYPOINT = "jib.container.entrypoint"; public static final String CONTAINER_ENVIRONMENT = "jib.container.environment"; public static final String CONTAINER_FORMAT = "jib.container.format"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 3b6bcb5235..9ad339f04f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -46,6 +46,8 @@ public interface RawConfiguration { Optional> getEntrypoint(); + List getExtraClasspath(); + Optional> getProgramArguments(); Optional getMainClass(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 0461e2f20a..a3bf273716 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -273,7 +273,7 @@ public void testEntrypoint_defaultWarPackaging() } @Test - public void testEntrypoint_defaulNonWarPackaging() + public void testEntrypoint_defaultNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { @@ -293,6 +293,30 @@ public void testEntrypoint_defaulNonWarPackaging() Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); } + @Test + public void testEntrypoint_extraClasspathNonWarPackaging() + throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); + Mockito.when(rawConfiguration.getExtraClasspath()) + .thenReturn(Collections.singletonList("/foo")); + Mockito.when(projectProperties.isWarProject()).thenReturn(false); + + PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); + JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); + BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertEquals( + Arrays.asList( + "java", "-cp", "/foo:/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), + buildConfiguration.getContainerConfiguration().getEntrypoint()); + + ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; + Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); + } + @Test public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, @@ -339,7 +363,9 @@ public void testEntrypoint_warningOnJvmFlags() Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(logger) - .accept(LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + .accept( + LogEvent.warn( + "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } @Test @@ -360,7 +386,9 @@ public void testEntrypoint_warningOnMainclass() Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); Mockito.verify(logger) - .accept(LogEvent.warn("mainClass and jvmFlags are ignored when entrypoint is specified")); + .accept( + LogEvent.warn( + "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } @Test From c7e6746a433be0cdf055633b516d44d958004976 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 23 Apr 2019 16:54:31 -0400 Subject: [PATCH 0554/2020] Update CHANGELOG for container config propagataion (#1661) * Update CHANGELOG for #1641 and #1643 * Remove periods --- jib-core/CHANGELOG.md | 6 +++++- jib-gradle-plugin/CHANGELOG.md | 6 +++++- jib-maven-plugin/CHANGELOG.md | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 9c5010e42f..cc6b64b047 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) + ### Changed ### Fixed +- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) + ## 0.9.1 ### Added @@ -21,7 +25,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) - Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 0.9.0 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index a81c28fbe2..4ad0e6ca4b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,15 +5,19 @@ All notable changes to this project will be documented in this file. ### Added +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) + ### Changed ### Fixed +- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) + ## 1.1.2 ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) ## 1.1.1 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index da86ad4713..7b491deac9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,15 +5,19 @@ All notable changes to this project will be documented in this file. ### Added +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) + ### Changed ### Fixed +- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) + ## 1.1.2 ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) ## 1.1.1 From 63ceb7a9cc7b1fffb6c696eaf54b7661876513cd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 24 Apr 2019 13:33:59 -0400 Subject: [PATCH 0555/2020] Move extraClasspath from test/simple to integration-test/simple (#1667) --- .../resources/gradle/projects/simple/build.gradle | 1 + .../src/test/resources/gradle/projects/simple/build.gradle | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle index 6b996ee339..b350cfeb4d 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle @@ -26,6 +26,7 @@ jib { labels = [key1:'value1', key2:'value2'] volumes = ['/var/log', '/var/log2'] workingDirectory = '/home' + extraClasspath = ['/d1','/d2'] } extraDirectory = file('src/main/custom-extra-dir') } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle index 6a6b57bf7a..52d0f7cab5 100644 --- a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle @@ -21,7 +21,4 @@ jib { extraDirectory { path = file('src/main/custom-extra-dir') } - container { - extraClasspath = ['/d1','/d2'] - } } From 758b571c809e5bc3b9dafa336f815d3d448386a4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 24 Apr 2019 14:05:18 -0400 Subject: [PATCH 0556/2020] Deprecate config in Maven (#1626) * Deprecate in Maven * No warn internal MojoFiles V1; fail build if both specified in MojoFiles V1 * Check new property in FilesMojoV2 / fail build if both used / no warning * Fail build if both old and new configs are used * Handle properties correctly in FilesMojoV2 * Add more tests --- .../jib/maven/JibPluginConfiguration.java | 105 ++++++++++++--- .../tools/jib/maven/skaffold/FilesMojo.java | 40 +++++- .../tools/jib/maven/skaffold/FilesMojoV2.java | 49 +++++-- .../maven/BuildImageMojoIntegrationTest.java | 47 +++++-- .../jib/maven/JibPluginConfigurationTest.java | 123 +++++++++++++++++- .../jib/maven/skaffold/FilesMojoV2Test.java | 23 ++++ .../pom-deprecated-and-new-extra-dir.xml | 52 ++++++++ .../simple/pom-deprecated-extra-dir.xml | 51 ++++++++ .../maven/projects/simple/pom-extra-dirs.xml | 4 +- .../ConfigurationPropertyValidator.java | 14 +- .../jib/plugins/common/PropertyNames.java | 7 +- 11 files changed, 454 insertions(+), 61 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index e7407ac78c..25cca343b0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -99,7 +99,7 @@ public ToAuthConfiguration() { } } - /** Used to configure {@code extraDirectory.permissions} parameter. */ + /** Used to configure {@code extraDirectories.permissions} parameter. */ public static class PermissionConfiguration { @Nullable @Parameter private String file; @@ -187,35 +187,44 @@ public static class ContainerParameters { @Nullable @Parameter private String workingDirectory; } + /** Configuration for the {@code extraDirectories} parameter. */ + public static class ExtraDirectoriesParameters { + + @Parameter private List paths = Collections.emptyList(); + + @Parameter private List permissions = Collections.emptyList(); + + public List getPaths() { + return paths; + } + } + /** Configuration for the {@code extraDirectory} parameter. */ + @Deprecated public static class ExtraDirectoryParameters { // retained for backward-compatibility for ... @Deprecated @Nullable @Parameter private File path; - @Parameter private List paths = Collections.emptyList(); - - @Parameter private List permissions = Collections.emptyList(); + @Deprecated @Parameter + private List permissions = Collections.emptyList(); - /** - * Allows users to configure {@code path} using just {@code } instead of {@code - * }. - * - * @param path the value to set {@code path} to - */ + // Allows users to configure a single path using just instead of + // . @Deprecated public void set(File path) { - this.paths = Collections.singletonList(path); + this.path = path; } + @Deprecated public List getPaths() { - return path != null ? Collections.singletonList(path) : paths; + return path == null ? Collections.emptyList() : Collections.singletonList(path); } } @Nullable @Parameter(defaultValue = "${session}", readonly = true) - MavenSession session; + private MavenSession session; @Nullable @Parameter(defaultValue = "${project}", readonly = true) @@ -228,7 +237,11 @@ public List getPaths() { @Parameter private ContainerParameters container = new ContainerParameters(); // this parameter is cloned in FilesMojo - @Parameter private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); + @Deprecated @Parameter + private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); + + // this parameter is cloned in FilesMojo + @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); @Parameter( defaultValue = "false", @@ -529,12 +542,38 @@ String getFormat() { */ List getExtraDirectories() { // TODO: Should inform user about nonexistent directory if using custom directory. - String property = getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + String deprecatedProperty = getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + String newProperty = getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); + + List deprecatedPaths = extraDirectory.getPaths(); + List newPaths = extraDirectories.getPaths(); + + if (deprecatedProperty != null) { + getLog() + .warn( + "The property 'jib.extraDirectory.path' is deprecated; " + + "use 'jib.extraDirectories.paths' instead"); + } + if (!deprecatedPaths.isEmpty()) { + getLog().warn(" is deprecated; use with "); + } + if (deprecatedProperty != null && newProperty != null) { + throw new IllegalArgumentException( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); + } + if (!deprecatedPaths.isEmpty() && !newPaths.isEmpty()) { + throw new IllegalArgumentException( + "You cannot configure both and "); + } + + String property = newProperty != null ? newProperty : deprecatedProperty; if (property != null) { List paths = ConfigurationPropertyValidator.parseListProperty(property); return paths.stream().map(Paths::get).collect(Collectors.toList()); } - return extraDirectory.getPaths().stream().map(File::toPath).collect(Collectors.toList()); + + List paths = !newPaths.isEmpty() ? newPaths : deprecatedPaths; + return paths.stream().map(File::toPath).collect(Collectors.toList()); } /** @@ -543,7 +582,29 @@ List getExtraDirectories() { * @return the configured extra layer file permissions */ List getExtraDirectoryPermissions() { - String property = getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); + String deprecatedProperty = getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); + String newProperty = getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); + + List deprecatedPermissions = extraDirectory.permissions; + List newPermissions = extraDirectories.permissions; + + if (deprecatedProperty != null) { + getLog() + .warn( + "The property 'jib.extraDirectory.permissions' is deprecated; " + + "use 'jib.extraDirectories.permissions' instead"); + } + if (deprecatedProperty != null && newProperty != null) { + throw new IllegalArgumentException( + "You cannot configure both 'jib.extraDirectory.permissions' and " + + "'jib.extraDirectories.permissions'"); + } + if (!deprecatedPermissions.isEmpty() && !newPermissions.isEmpty()) { + throw new IllegalArgumentException( + "You cannot configure both and "); + } + + String property = newProperty != null ? newProperty : deprecatedProperty; if (property != null) { return ConfigurationPropertyValidator.parseMapProperty(property) .entrySet() @@ -551,7 +612,10 @@ List getExtraDirectoryPermissions() { .map(entry -> new PermissionConfiguration(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } - return extraDirectory.permissions; + + return !extraDirectories.getPaths().isEmpty() + ? extraDirectories.permissions + : extraDirectory.permissions; } boolean getAllowInsecureRegistries() { @@ -571,6 +635,11 @@ void setProject(MavenProject project) { this.project = project; } + @VisibleForTesting + void setSession(MavenSession session) { + this.session = session; + } + @Nullable String getProperty(String propertyName) { return MavenProjectProperties.getProperty(propertyName, project, session); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java index 3e5a2f6b0e..4c77e6698e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -16,8 +16,11 @@ package com.google.cloud.tools.jib.maven.skaffold; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration.ExtraDirectoriesParameters; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.ExtraDirectoryParameters; import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; @@ -44,6 +47,7 @@ import org.apache.maven.project.DependencyResolutionResult; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectDependenciesResolver; +import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyFilter; /** @@ -78,7 +82,11 @@ public class FilesMojo extends AbstractMojo { @Nullable @Component private ProjectDependenciesResolver projectDependenciesResolver; // This parameter is cloned from JibPluginConfiguration - @Parameter private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); + @Deprecated @Parameter + private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); + + // This parameter is cloned from JibPluginConfiguration + @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -143,7 +151,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { resolutionResult .getDependencies() .stream() - .map(org.eclipse.aether.graph.Dependency::getArtifact) + .map(Dependency::getArtifact) .filter(org.eclipse.aether.artifact.Artifact::isSnapshot) .map(org.eclipse.aether.artifact.Artifact::getFile) .forEach(System.out::println); @@ -153,8 +161,32 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private List resolveExtraDirectories() { - List paths = extraDirectory.getPaths(); + private List resolveExtraDirectories() throws MojoExecutionException { + // TODO: Should inform user about nonexistent directory if using custom directory. + String deprecatedProperty = + MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); + String newProperty = + MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS, project, session); + + List deprecatedPaths = extraDirectory.getPaths(); + List newPaths = extraDirectories.getPaths(); + + if (deprecatedProperty != null && newProperty != null) { + throw new MojoExecutionException( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); + } + if (!deprecatedPaths.isEmpty() && !newPaths.isEmpty()) { + throw new MojoExecutionException( + "You cannot configure both and "); + } + + String property = newProperty != null ? newProperty : deprecatedProperty; + if (property != null) { + List paths = ConfigurationPropertyValidator.parseListProperty(property); + return paths.stream().map(Paths::get).map(Path::toAbsolutePath).collect(Collectors.toList()); + } + + List paths = !newPaths.isEmpty() ? newPaths : deprecatedPaths; if (paths.isEmpty()) { Path projectBase = Preconditions.checkNotNull(project).getBasedir().getAbsoluteFile().toPath(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index a73e74ca5a..656cf96551 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven.skaffold; import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; import com.google.common.annotations.VisibleForTesting; @@ -167,12 +168,22 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private List resolveExtraDirectories(MavenProject project) { + private List resolveExtraDirectories(MavenProject project) throws MojoExecutionException { // Try getting extra directory from project/session properties - String extraDirectoryProperty = + String deprecatedProperty = MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); - if (extraDirectoryProperty != null) { - return Collections.singletonList(Paths.get(extraDirectoryProperty)); + String newProperty = + MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS, project, session); + + if (deprecatedProperty != null && newProperty != null) { + throw new MojoExecutionException( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); + } + + String property = newProperty != null ? newProperty : deprecatedProperty; + if (property != null) { + List paths = ConfigurationPropertyValidator.parseListProperty(property); + return paths.stream().map(Paths::get).collect(Collectors.toList()); } // Try getting extra directory from project pom @@ -180,21 +191,31 @@ private List resolveExtraDirectories(MavenProject project) { if (jibMavenPlugin != null) { Xpp3Dom pluginConfiguration = (Xpp3Dom) jibMavenPlugin.getConfiguration(); if (pluginConfiguration != null) { + Xpp3Dom extraDirectoryConfiguration = pluginConfiguration.getChild("extraDirectory"); - if (extraDirectoryConfiguration != null) { - Xpp3Dom pathChild = extraDirectoryConfiguration.getChild("path"); - if (pathChild != null) { - // ... - return Collections.singletonList(Paths.get(pathChild.getValue())); - } - Xpp3Dom pathsChild = extraDirectoryConfiguration.getChild("paths"); - if (pathsChild != null) { - // ...... - return Arrays.stream(pathsChild.getChildren()) + Xpp3Dom extraDirectoriesConfiguration = pluginConfiguration.getChild("extraDirectories"); + if (extraDirectoryConfiguration != null && extraDirectoriesConfiguration != null) { + throw new MojoExecutionException( + "You cannot configure both and "); + } + + if (extraDirectoriesConfiguration != null) { + Xpp3Dom child = extraDirectoriesConfiguration.getChild("paths"); + if (child != null) { + // ...... + return Arrays.stream(child.getChildren()) .map(Xpp3Dom::getValue) .map(Paths::get) .collect(Collectors.toList()); } + } + + if (extraDirectoryConfiguration != null) { + Xpp3Dom child = extraDirectoryConfiguration.getChild("path"); + if (child != null) { + // ... + return Collections.singletonList(Paths.get(child.getValue())); + } // ... String value = extraDirectoryConfiguration.getValue(); if (value != null) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 7501a98a30..9e720b6718 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -98,13 +98,9 @@ private static boolean isJava11RuntimeOrHigher() { return Integer.valueOf(split.iterator().next()) >= 11; } - /** - * Builds and runs jib:build on a project at {@code projectRoot} pushing to {@code - * imageReference}. - */ - private static String buildAndRun( - Path projectRoot, String imageReference, String pomXml, boolean runTwice) - throws VerificationException, IOException, InterruptedException, DigestException { + private static Verifier build( + Path projectRoot, String imageReference, String pomXml, boolean buildTwice) + throws VerificationException, IOException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); @@ -117,7 +113,7 @@ private static String buildAndRun( verifier.executeGoal("jib:build"); float timeOne = getBuildTimeFromVerifierLog(verifier); - if (runTwice) { + if (buildTwice) { verifier.resetStreams(); verifier.executeGoal("jib:build"); float timeTwo = getBuildTimeFromVerifierLog(verifier); @@ -126,7 +122,17 @@ private static String buildAndRun( Assert.assertTrue(String.format(failMessage, timeOne, timeTwo), timeOne > timeTwo); } - verifier.verifyErrorFreeLog(); + return verifier; + } + + /** + * Builds with {@code jib:build} on a project at {@code projectRoot} pushing to {@code + * imageReference} and run the image after pulling it. + */ + private static String buildAndRun( + Path projectRoot, String imageReference, String pomXml, boolean buildTwice) + throws VerificationException, IOException, InterruptedException, DigestException { + build(projectRoot, imageReference, pomXml, buildTwice).verifyErrorFreeLog(); String output = pullAndRunBuiltImage(imageReference); @@ -426,6 +432,29 @@ public void testExecute_multipleExtraDirectories() assertLayerSizer(9, targetImage); // one more than usual } + @Test + public void testExecute_bothDeprecatedAndNewExtraDirectoryConfigUsed() throws IOException { + try { + build( + simpleTestProject.getProjectRoot(), "foo", "pom-deprecated-and-new-extra-dir.xml", false); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "You cannot configure both and ")); + } + } + + @Test + public void testExecute_deprecatedExtraDirectoryConfigUsed() + throws IOException, VerificationException { + String targetImage = getGcrImageReference("simpleimage:maven"); + build(simpleTestProject.getProjectRoot(), targetImage, "pom-deprecated-extra-dir.xml", false) + .verifyTextInLog( + " is deprecated; use with "); + } + @Test public void testExecute_defaultTarget() throws IOException { // Test error when 'to' is missing diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 64f99e50cf..1437e665f3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Properties; import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.junit.Assert; import org.junit.Before; @@ -41,6 +42,7 @@ public class JibPluginConfigurationTest { private final MavenProject project = new MavenProject(); private final Properties sessionProperties = new Properties(); @Mock private MavenSession session; + @Mock private Log log; private JibPluginConfiguration testPluginConfiguration; @Before @@ -50,9 +52,14 @@ public void setup() { new JibPluginConfiguration() { @Override public void execute() {} + + @Override + public Log getLog() { + return log; + } }; testPluginConfiguration.setProject(project); - testPluginConfiguration.session = session; + testPluginConfiguration.setSession(session); } @Test @@ -115,10 +122,10 @@ public void testSystemProperties() { Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); - sessionProperties.put("jib.extraDirectory.path", "custom-jib"); + sessionProperties.put("jib.extraDirectories.paths", "custom-jib"); Assert.assertEquals( Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); - sessionProperties.put("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); + sessionProperties.put("jib.extraDirectories.permissions", "/test/file1=123,/another/file=456"); List permissions = testPluginConfiguration.getExtraDirectoryPermissions(); Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); @@ -180,12 +187,12 @@ public void testPomProperties() { Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); - project.getProperties().setProperty("jib.extraDirectory.path", "custom-jib"); + project.getProperties().setProperty("jib.extraDirectories.paths", "custom-jib"); Assert.assertEquals( Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); project .getProperties() - .setProperty("jib.extraDirectory.permissions", "/test/file1=123,/another/file=456"); + .setProperty("jib.extraDirectories.permissions", "/test/file1=123,/another/file=456"); List permissions = testPluginConfiguration.getExtraDirectoryPermissions(); Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); @@ -207,4 +214,110 @@ public void testEmptyOrNullTags() { Assert.assertEquals("jib.to.tags has empty tag", ex.getMessage()); } } + + @Test + public void testDeprecatedSystemProperties() { + sessionProperties.put("jib.extraDirectory.path", "custom-jib"); + Assert.assertEquals( + Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); + sessionProperties.put("jib.extraDirectory.permissions", "/test/file13=650,/another/file24=777"); + List permissions = + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.assertEquals("/test/file13", permissions.get(0).getFile().get()); + Assert.assertEquals("650", permissions.get(0).getMode().get()); + Assert.assertEquals("/another/file24", permissions.get(1).getFile().get()); + Assert.assertEquals("777", permissions.get(1).getMode().get()); + + Mockito.verify(log, Mockito.times(1)) + .warn( + "The property 'jib.extraDirectory.path' is deprecated; " + + "use 'jib.extraDirectories.paths' instead"); + } + + @Test + public void testDeprecatedProperties() { + Properties projectProperties = project.getProperties(); + + projectProperties.setProperty("jib.extraDirectory.path", "this-is-extra"); + Assert.assertEquals( + Arrays.asList(Paths.get("this-is-extra")), testPluginConfiguration.getExtraDirectories()); + + projectProperties.setProperty( + "jib.extraDirectory.permissions", "/test/file1=654,/dir/file2=321"); + List permissions = + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); + Assert.assertEquals("654", permissions.get(0).getMode().get()); + Assert.assertEquals("/dir/file2", permissions.get(1).getFile().get()); + Assert.assertEquals("321", permissions.get(1).getMode().get()); + + Mockito.verify(log, Mockito.times(1)) + .warn( + "The property 'jib.extraDirectory.path' is deprecated; " + + "use 'jib.extraDirectories.paths' instead"); + } + + @Test + public void testGetExtraDirectories_bothSystemPropertiesUsed() { + sessionProperties.put("jib.extraDirectory.path", "deprecated-property"); + sessionProperties.put("jib.extraDirectories.paths", "new-property"); + + try { + testPluginConfiguration.getExtraDirectories(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", + ex.getMessage()); + } + } + + @Test + public void testGetExtraDirectories_bothPropertiesUsed() { + Properties projectProperties = project.getProperties(); + projectProperties.setProperty("jib.extraDirectory.path", "deprecated-property"); + projectProperties.setProperty("jib.extraDirectories.paths", "new-property"); + + try { + testPluginConfiguration.getExtraDirectories(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", + ex.getMessage()); + } + } + + @Test + public void testGetExtraDirectoryPermissions_bothSystemPropertiesUsed() { + sessionProperties.put("jib.extraDirectory.permissions", "deprecated-property"); + sessionProperties.put("jib.extraDirectories.permissions", "new-property"); + + try { + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.permissions' and " + + "'jib.extraDirectories.permissions'", + ex.getMessage()); + } + } + + @Test + public void testGetExtraDirectoryPermissions_bothPropertiesUsed() { + Properties projectProperties = project.getProperties(); + projectProperties.setProperty("jib.extraDirectory.permissions", "deprecated-property"); + projectProperties.setProperty("jib.extraDirectories.permissions", "new-property"); + + try { + testPluginConfiguration.getExtraDirectoryPermissions(); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.permissions' and " + + "'jib.extraDirectories.permissions'", + ex.getMessage()); + } + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index 4571bf6506..1941e3b31b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -49,6 +49,7 @@ private static void verifyFiles( Path projectRoot, String pomXml, String module, + List extraCliOptions, List buildFiles, List inputFiles) throws VerificationException, IOException { @@ -62,6 +63,7 @@ private static void verifyFiles( verifier.addCliOption(module); verifier.addCliOption("-am"); } + extraCliOptions.forEach(verifier::addCliOption); verifier.executeGoal("jib:" + FilesMojoV2.GOAL_NAME); verifier.verifyErrorFreeLog(); @@ -84,6 +86,7 @@ public void testFilesMojo_singleModule() throws VerificationException, IOExcepti projectRoot, "pom.xml", null, + Collections.emptyList(), Collections.singletonList(projectRoot.resolve("pom.xml").toString()), Arrays.asList( projectRoot.resolve("src/main/java").toString(), @@ -100,6 +103,7 @@ public void testFilesMojo_singleModuleWithMultipleExtraDirectories() projectRoot, "pom-extra-dirs.xml", null, + Collections.emptyList(), Collections.singletonList(projectRoot.resolve("pom-extra-dirs.xml").toString()), Arrays.asList( projectRoot.resolve("src/main/java").toString(), @@ -117,6 +121,7 @@ public void testFilesMojo_multiModuleSimpleService() throws VerificationExceptio projectRoot, "pom.xml", "simple-service", + Collections.emptyList(), Arrays.asList( projectRoot.resolve("pom.xml").toString(), simpleServiceRoot.resolve("pom.xml").toString()), @@ -136,6 +141,7 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti projectRoot, "pom.xml", "complex-service", + Collections.emptyList(), Arrays.asList( projectRoot.resolve("pom.xml").toString(), libRoot.resolve("pom.xml").toString(), @@ -153,4 +159,21 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar") .toString())); } + + @Test + public void testFilesMojo_extraDirectoriesProperty() throws VerificationException, IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + + verifyFiles( + projectRoot, + "pom.xml", + null, + Arrays.asList("-Djib.extraDirectories.paths=/some/extra/dir,/another/extra/dir"), + Collections.singletonList(projectRoot.resolve("pom.xml").toString()), + Arrays.asList( + projectRoot.resolve("src/main/java").toString(), + projectRoot.resolve("src/main/resources").toString(), + Paths.get("/some/extra/dir").toString(), + Paths.get("/another/extra/dir").toString())); + } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml new file mode 100644 index 0000000000..19f2919955 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + ${_TARGET_IMAGE} + An argument. + 1000/tcp,2000-2003/udp + key1=value1,key2=value2 + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + deprecated-config + good-config + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml new file mode 100644 index 0000000000..ec9beeaa69 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + ${_TARGET_IMAGE} + An argument. + 1000/tcp,2000-2003/udp + key1=value1,key2=value2 + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + deprecated-config + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml index 3e3f399c62..1c594f7d8f 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-extra-dirs.xml @@ -43,12 +43,12 @@ jib-maven-plugin ${jib-maven-plugin.version} - + ${project.basedir}/src/main/jib-custom ${project.basedir}/src/main/jib-custom-2 - + diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index c2dd0922d4..c4ee6a7c5a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -23,10 +23,8 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,7 +36,7 @@ public class ConfigurationPropertyValidator { /** Matches key-value pairs in the form of "key=value" */ - private static final Pattern ENVIRONMENT_PATTERN = Pattern.compile("(?[^=]+)=(?.*)"); + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("(?[^=]+)=(?.*)"); /** * Gets a {@link Credential} from a username and password. First tries system properties, then @@ -149,18 +147,18 @@ public static ImageReference getGeneratedTargetDockerTag( * @return the map of parsed values */ public static Map parseMapProperty(String property) { - Map result = new HashMap<>(); + Map result = new LinkedHashMap<>(); // LinkedHashMap to keep insertion order // Split on non-escaped commas List entries = parseListProperty(property); for (String entry : entries) { - Matcher matcher = ENVIRONMENT_PATTERN.matcher(entry); + Matcher matcher = KEY_VALUE_PATTERN.matcher(entry); if (!matcher.matches()) { throw new IllegalArgumentException("'" + entry + "' is not a valid key-value pair"); } result.put(matcher.group("name"), matcher.group("value")); } - return ImmutableMap.copyOf(result); + return result; } /** @@ -183,7 +181,7 @@ public static List parseListProperty(String property) { } } items.add(property.substring(startIndex)); - return ImmutableList.copyOf(items); + return items; } private ConfigurationPropertyValidator() {} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index db58421238..ad2a6d07b2 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -47,8 +47,13 @@ public class PropertyNames { public static final String BASE_IMAGE_CACHE = "jib.baseImageCache"; public static final String APPLICATION_CACHE = "jib.applicationCache"; public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; - public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; + @Deprecated public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; + public static final String EXTRA_DIRECTORIES_PATHS = "jib.extraDirectories.paths"; + + @Deprecated public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; + + public static final String EXTRA_DIRECTORIES_PERMISSIONS = "jib.extraDirectories.permissions"; public static final String DOCKER_CLIENT_EXECUTABLE = "jib.dockerClient.executable"; public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; public static final String SKIP = "jib.skip"; From bd4498c6f81b072b6a1f0a2463fa453c8ca1baf0 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 24 Apr 2019 15:14:13 -0400 Subject: [PATCH 0557/2020] Remove Blob conversion from ImageToTarballTranslator and TarStreamBuilder (#1647) * Decouple Blob * Rename ImageToTarTranslator * Decouple Blob from TarStreamBuilder --- .../jib/builder/steps/LoadDockerStep.java | 6 ++---- .../jib/builder/steps/WriteTarFileStep.java | 5 ++--- .../cloud/tools/jib/docker/DockerClient.java | 7 +++---- ...rballTranslator.java => ImageTarball.java} | 13 ++++++++---- .../jib/image/ReproducibleLayerBuilder.java | 3 ++- .../cloud/tools/jib/tar/TarStreamBuilder.java | 11 +++------- .../tools/jib/docker/DockerClientTest.java | 21 +++++++++++++------ ...nslatorTest.java => ImageTarballTest.java} | 20 +++++++++--------- .../tools/jib/tar/TarStreamBuilderTest.java | 12 +++-------- 9 files changed, 49 insertions(+), 49 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/docker/{ImageToTarballTranslator.java => ImageTarball.java} (88%) rename jib-core/src/test/java/com/google/cloud/tools/jib/docker/{ImageToTarballTranslatorTest.java => ImageTarballTest.java} (91%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 02bf62b57b..dc15cddcfb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; @@ -105,9 +105,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() buildConfiguration .getEventDispatcher() .dispatch( - LogEvent.debug( - dockerClient.load( - new ImageToTarballTranslator(image).toTarballBlob(targetImageReference)))); + LogEvent.debug(dockerClient.load(new ImageTarball(image, targetImageReference)))); // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. for (String tag : buildConfiguration.getAllTargetImageTags()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index da3089a01c..5a19c07ce5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.docker.ImageToTarballTranslator; +import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; @@ -103,8 +103,7 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { - new ImageToTarballTranslator(image) - .toTarballBlob(buildConfiguration.getTargetImageConfiguration().getImage()) + new ImageTarball(image, buildConfiguration.getTargetImageConfiguration().getImage()) .writeTo(outputStream); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index af1c4df2a5..ccf8292b7b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; @@ -164,18 +163,18 @@ public static boolean isDockerInstalled(Path dockerExecutable) { * * @see https://docs.docker.com/engine/reference/commandline/load - * @param imageTarballBlob the built container tarball. + * @param imageTarball the built container tarball. * @return stdout from {@code docker}. * @throws InterruptedException if the 'docker load' process is interrupted. * @throws IOException if streaming the blob to 'docker load' fails. */ - public String load(Blob imageTarballBlob) throws InterruptedException, IOException { + public String load(ImageTarball imageTarball) throws InterruptedException, IOException { // Runs 'docker load'. Process dockerProcess = docker("load"); try (OutputStream stdin = dockerProcess.getOutputStream()) { try { - imageTarballBlob.writeTo(stdin); + imageTarball.writeTo(stdin); } catch (IOException ex) { // Tries to read from stderr. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java similarity index 88% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index c87e876a12..f7ad06a162 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -26,10 +26,11 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import java.io.IOException; +import java.io.OutputStream; import java.util.Collections; /** Translates an {@link Image} to a tarball that can be loaded into Docker. */ -public class ImageToTarballTranslator { +public class ImageTarball { /** File name for the container configuration in the tarball. */ private static final String CONTAINER_CONFIGURATION_JSON_FILE_NAME = "config.json"; @@ -42,16 +43,20 @@ public class ImageToTarballTranslator { private final Image image; + private final ImageReference imageReference; + /** * Instantiate with an {@link Image}. * * @param image the image to convert into a tarball + * @param imageReference image reference to set in the manifest */ - public ImageToTarballTranslator(Image image) { + public ImageTarball(Image image, ImageReference imageReference) { this.image = image; + this.imageReference = imageReference; } - public Blob toTarballBlob(ImageReference imageReference) throws IOException { + public void writeTo(OutputStream out) throws IOException { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); DockerLoadManifestEntryTemplate manifestTemplate = new DockerLoadManifestEntryTemplate(); @@ -77,6 +82,6 @@ public Blob toTarballBlob(ImageReference imageReference) throws IOException { JsonTemplateMapper.toBlob(Collections.singletonList(manifestTemplate))), MANIFEST_JSON_FILE_NAME); - return tarStreamBuilder.toBlob(); + tarStreamBuilder.writeAsTarArchiveTo(out); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index 8f2f6748bb..d68d0d7e71 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; @@ -136,6 +137,6 @@ public Blob build() { tarStreamBuilder.addTarArchiveEntry(entry); } - return tarStreamBuilder.toBlob(); + return Blobs.from(outputStream -> tarStreamBuilder.writeAsTarArchiveTo(outputStream)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 8e38af8bb8..c67e85ae77 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -38,12 +38,12 @@ public class TarStreamBuilder { /** * Writes each entry in the filesystem to the tarball archive stream. * - * @param tarByteStream the stream to write to. + * @param out the stream to write to. * @throws IOException if building the tarball fails. */ - private void writeEntriesAsTarArchive(OutputStream tarByteStream) throws IOException { + public void writeAsTarArchiveTo(OutputStream out) throws IOException { try (TarArchiveOutputStream tarArchiveOutputStream = - new TarArchiveOutputStream(tarByteStream, StandardCharsets.UTF_8.name())) { + new TarArchiveOutputStream(out, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); for (Map.Entry entry : archiveMap.entrySet()) { @@ -90,9 +90,4 @@ public void addBlobEntry(Blob blob, long size, String name) { entry.setSize(size); archiveMap.put(entry, blob); } - - /** @return a new {@link Blob} that can stream the uncompressed tarball archive BLOB. */ - public Blob toBlob() { - return Blobs.from(this::writeEntriesAsTarArchive); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index b1ccf8c0f7..230c2a9a38 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; @@ -38,22 +37,32 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.VoidAnswer1; /** Tests for {@link DockerClient}. */ @RunWith(MockitoJUnitRunner.class) public class DockerClientTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private ProcessBuilder mockProcessBuilder; @Mock private Process mockProcess; + @Mock private ImageTarball imageTarball; @Before public void setUp() throws IOException { Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + + Mockito.doAnswer( + AdditionalAnswers.answerVoid( + (VoidAnswer1) + out -> out.write("jib".getBytes(StandardCharsets.UTF_8)))) + .when(imageTarball) + .writeTo(Mockito.any(OutputStream.class)); } @Test @@ -79,7 +88,7 @@ public void testLoad() throws IOException, InterruptedException { Mockito.when(mockProcess.getInputStream()) .thenReturn(new ByteArrayInputStream("output".getBytes(StandardCharsets.UTF_8))); - String output = testDockerClient.load(Blobs.from("jib")); + String output = testDockerClient.load(imageTarball); Assert.assertEquals( "jib", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); @@ -103,7 +112,7 @@ public void write(int b) throws IOException { .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.load(Blobs.from("jib")); + testDockerClient.load(imageTarball); Assert.fail("Write should have failed"); } catch (IOException ex) { @@ -136,7 +145,7 @@ public int read() throws IOException { }); try { - testDockerClient.load(Blobs.from("jib")); + testDockerClient.load(imageTarball); Assert.fail("Write should have failed"); } catch (IOException ex) { @@ -156,7 +165,7 @@ public void testLoad_stdoutFail() throws InterruptedException { .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.load(Blobs.from("jib")); + testDockerClient.load(imageTarball); Assert.fail("Process should have failed"); } catch (IOException ex) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java similarity index 91% rename from jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java index 0759ef9e18..146bd780c8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageToTarballTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; @@ -32,6 +31,7 @@ import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URISyntaxException; @@ -49,15 +49,15 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link ImageToTarballTranslator}. */ +/** Tests for {@link ImageTarball}. */ @RunWith(MockitoJUnitRunner.class) -public class ImageToTarballTranslatorTest { +public class ImageTarballTest { @Mock private Layer mockLayer1; @Mock private Layer mockLayer2; @Test - public void testToTarballBlob() + public void testWriteTo() throws InvalidImageReferenceException, IOException, URISyntaxException, LayerPropertyNotFoundException, DigestException { Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); @@ -83,13 +83,13 @@ public void testToTarballBlob() Image testImage = Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); - Blob tarballBlob = - new ImageToTarballTranslator(testImage).toTarballBlob(ImageReference.parse("my/image:tag")); + ImageTarball imageToTarball = new ImageTarball(testImage, ImageReference.parse("my/image:tag")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + imageToTarball.writeTo(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { - try (ByteArrayInputStream tarballBytesStream = - new ByteArrayInputStream(Blobs.writeToByteArray(tarballBlob)); - TarArchiveInputStream tarArchiveInputStream = - new TarArchiveInputStream(tarballBytesStream)) { // Verifies layer with fileA was added. TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextTarEntry(); Assert.assertEquals(fakeDigestA.getHash() + ".tar.gz", headerFileALayer.getName()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java index f119b892dc..6555c69c70 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.tar; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; @@ -101,12 +100,11 @@ public void testToBlob_multiByte() throws IOException { testTarStreamBuilder.addByteEntry("asdf".getBytes(StandardCharsets.UTF_8), "crepecake"); testTarStreamBuilder.addBlobEntry( Blobs.from("jib"), "jib".getBytes(StandardCharsets.UTF_8).length, "jib"); - Blob blob = testTarStreamBuilder.toBlob(); // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); OutputStream compressorStream = new GZIPOutputStream(tarByteOutputStream); - blob.writeTo(compressorStream); + testTarStreamBuilder.writeAsTarArchiveTo(compressorStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = @@ -173,12 +171,10 @@ private void setUpWithStringsAndTarEntries() { /** Creates a compressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithCompression() throws IOException { - Blob blob = testTarStreamBuilder.toBlob(); - // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); OutputStream compressorStream = new GZIPOutputStream(tarByteOutputStream); - blob.writeTo(compressorStream); + testTarStreamBuilder.writeAsTarArchiveTo(compressorStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = @@ -190,11 +186,9 @@ private void verifyBlobWithCompression() throws IOException { /** Creates an uncompressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithoutCompression() throws IOException { - Blob blob = testTarStreamBuilder.toBlob(); - // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); - blob.writeTo(tarByteOutputStream); + testTarStreamBuilder.writeAsTarArchiveTo(tarByteOutputStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = From a111265a47f4b20c17e4bcb54c3c0401e2f013b0 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 26 Apr 2019 16:32:26 -0400 Subject: [PATCH 0558/2020] Update changelogs for extraClasspath (#1662) --- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4ad0e6ca4b..b65fe2d9e8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7b491deac9..a7d7a5f6ae 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) ### Changed From 7a01e7fa33348080eef094f42407ec6e0c7623cd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 29 Apr 2019 14:20:38 -0400 Subject: [PATCH 0559/2020] Docs for extraClasspath (#1645) --- jib-gradle-plugin/README.md | 1 + jib-maven-plugin/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index d5fea4589b..e57f49b682 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -210,6 +210,7 @@ Property | Type | Default | Description `args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | `List` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. `labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 76bfd429fb..d1f6c7ca5e 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -256,6 +256,7 @@ Property | Type | Default | Description `args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `INHERIT` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | `list` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). From a1107229a213fd121bfb95d5a30de6498ae63ddf Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 29 Apr 2019 14:48:42 -0400 Subject: [PATCH 0560/2020] Revert "Docs for extraClasspath (#1645)" (#1674) This reverts commit 7a01e7fa33348080eef094f42407ec6e0c7623cd. --- jib-gradle-plugin/README.md | 1 - jib-maven-plugin/README.md | 1 - 2 files changed, 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index e57f49b682..d5fea4589b 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -210,7 +210,6 @@ Property | Type | Default | Description `args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). -`extraClasspath` | `List` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. `labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index d1f6c7ca5e..76bfd429fb 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -256,7 +256,6 @@ Property | Type | Default | Description `args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `INHERIT` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). -`extraClasspath` | `list` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). From 0392db2de675e1c005abf3315ac2faa44853dbf4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 29 Apr 2019 16:59:43 -0400 Subject: [PATCH 0561/2020] Add cache methods for saving manifest and container config (#1666) --- .../google/cloud/tools/jib/blob/Blobs.java | 15 ++++ .../google/cloud/tools/jib/cache/Cache.java | 32 +++++++ .../cloud/tools/jib/cache/CacheStorage.java | 28 +++++++ .../tools/jib/cache/DefaultCacheStorage.java | 20 +++++ .../jib/cache/DefaultCacheStorageFiles.java | 32 +++++++ .../jib/cache/DefaultCacheStorageWriter.java | 76 +++++++++++++++++ .../cloud/tools/jib/filesystem/LockFile.java | 83 +++++++++++++++++++ .../jib/image/json/JsonToImageTranslator.java | 4 +- .../jib/image/json/V21ManifestTemplate.java | 13 +-- .../cache/DefaultCacheStorageFilesTest.java | 30 +++++++ .../cache/DefaultCacheStorageWriterTest.java | 70 +++++++++++++++- .../tools/jib/filesystem/LockFileTest.java | 61 ++++++++++++++ .../image/json/V21ManifestTemplateTest.java | 2 +- 13 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 2d493d607f..4cec2f9b7e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; /** Static methods for {@link Blob}. */ @@ -73,6 +74,20 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { return byteArrayOutputStream.toByteArray(); } + /** + * Writes the BLOB to a file. + * + * @param blob the BLOB to to write + * @param blobFile the file to write to + * @return the {@link BlobDescriptor} of the written BLOB + * @throws IOException if writing the BLOB fails + */ + public static BlobDescriptor writeToFile(Blob blob, Path blobFile) throws IOException { + try (OutputStream outputStream = Files.newOutputStream(blobFile)) { + return blob.writeTo(outputStream); + } + } + /** * Writes the BLOB to a file with an exclusive lock. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index dc5d382986..447f92b730 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -18,7 +18,11 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Files; @@ -55,6 +59,34 @@ private Cache(CacheStorage cacheStorage) { this.cacheStorage = cacheStorage; } + /** + * Saves a manifest and container configuration for a V2.2 or OCI image. + * + * @param imageReference the image reference to save the manifest and container configuration for + * @param manifestTemplate the V2.2 or OCI manifest + * @param containerConfigurationTemplate the container configuration + * @throws IOException if an I/O exception occurs + */ + public void writeMetadata( + ImageReference imageReference, + BuildableManifestTemplate manifestTemplate, + ContainerConfigurationTemplate containerConfigurationTemplate) + throws IOException { + cacheStorage.writeMetadata(imageReference, manifestTemplate, containerConfigurationTemplate); + } + + /** + * Saves a V2.1 image manifest. + * + * @param imageReference the image reference to save the manifest and container configuration for + * @param manifestTemplate the V2.1 manifest + * @throws IOException if an I/O exception occurs + */ + public void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) + throws IOException { + cacheStorage.writeMetadata(imageReference, manifestTemplate); + } + /** * Saves a cache entry with a compressed layer {@link Blob}. Use {@link * #writeUncompressedLayer(Blob, ImmutableList)} to save a cache entry with an uncompressed layer diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java index 7956ebc9d3..eacdc83b95 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java @@ -18,6 +18,10 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -83,4 +87,28 @@ Optional retrieve(DescriptorDigest layerDigest) */ Optional select(DescriptorDigest selector) throws IOException, CacheCorruptedException; + + /** + * Saves the manifest and container configuration for a V2.2 or OCI image. + * + * @param imageReference the image reference to store the metadata for + * @param manifestTemplate the image's manifest + * @param containerConfigurationTemplate the image's container configuration + * @throws IOException if an I/O exception occurs + */ + void writeMetadata( + ImageReference imageReference, + BuildableManifestTemplate manifestTemplate, + ContainerConfigurationTemplate containerConfigurationTemplate) + throws IOException; + + /** + * Saves a V2.1 manifest. + * + * @param imageReference the image reference to store the metadata for + * @param manifestTemplate the image's manifest + * @throws IOException if an I/O exception occurs + */ + void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) + throws IOException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java index c63d1ce96e..08974abd98 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java @@ -18,6 +18,10 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -90,4 +94,20 @@ public Optional select(DescriptorDigest selector) throws IOException, CacheCorruptedException { return defaultCacheStorageReader.select(selector); } + + @Override + public void writeMetadata( + ImageReference imageReference, + BuildableManifestTemplate manifestTemplate, + ContainerConfigurationTemplate containerConfigurationTemplate) + throws IOException { + defaultCacheStorageWriter.writeMetadata( + imageReference, manifestTemplate, containerConfigurationTemplate); + } + + @Override + public void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) + throws IOException { + defaultCacheStorageWriter.writeMetadata(imageReference, manifestTemplate); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java index 5323bfbd13..47324e504d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.common.base.Splitter; import java.nio.file.Path; import java.security.DigestException; @@ -24,6 +26,7 @@ class DefaultCacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; + private static final String IMAGES_DIRECTORY = "images"; private static final String SELECTORS_DIRECTORY = "selectors"; private static final String TEMPORARY_DIRECTORY = "tmp"; private static final String TEMPORARY_LAYER_FILE_NAME = ".tmp.layer"; @@ -113,6 +116,35 @@ Path getLayerDirectory(DescriptorDigest layerDigest) { return getLayersDirectory().resolve(layerDigest.getHash()); } + /** + * Gets the directory to store the image manifest and configuration. + * + * @return the directory for the image manifest and configuration + */ + Path getImagesDirectory() { + return cacheDirectory.resolve(IMAGES_DIRECTORY); + } + + /** + * Gets the directory corresponding to the given image reference. + * + * @param imageReference the image reference + * @return a path in the form of {@code + * (jib-cache)/images/registry[!port]/repository!(tag|digest-type!digest)} + */ + Path getImageDirectory(ImageReference imageReference) { + // Replace ':' and '@' with '!' to avoid directory-naming restrictions + String replacedReference = imageReference.toStringWithTag().replace(':', '!').replace('@', '!'); + + // Split image reference on '/' to build directory structure + Iterable directories = Splitter.on('/').split(replacedReference); + Path destination = getImagesDirectory(); + for (String dir : directories) { + destination = destination.resolve(dir); + } + return destination; + } + /** * Gets the directory to store temporary files. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java index c826ce0465..0768103421 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java @@ -19,9 +19,17 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.LockFile; import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -101,6 +109,34 @@ private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile } } + /** + * Writes a json template to the destination path by writing to a temporary file then moving the + * file. + * + * @param jsonTemplate the json template + * @param destination the destination path + * @throws IOException if an I/O exception occurs + */ + private static void writeMetadata(JsonTemplate jsonTemplate, Path destination) + throws IOException { + Path temporaryFile = Files.createTempFile(destination.getParent(), null, null); + temporaryFile.toFile().deleteOnExit(); + Blobs.writeToFile(JsonTemplateMapper.toBlob(jsonTemplate), temporaryFile); + + // Attempts an atomic move first, and falls back to non-atomic if the file system does not + // support atomic moves. + try { + Files.move( + temporaryFile, + destination, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + + } catch (AtomicMoveNotSupportedException ignored2) { + Files.move(temporaryFile, destination, StandardCopyOption.REPLACE_EXISTING); + } + } + private final DefaultCacheStorageFiles defaultCacheStorageFiles; DefaultCacheStorageWriter(DefaultCacheStorageFiles defaultCacheStorageFiles) { @@ -204,6 +240,46 @@ CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOExcept } } + /** + * Saves the manifest and container configuration for a V2.2 or OCI image. + * + * @param imageReference the image reference to store the metadata for + * @param manifestTemplate the manifest + * @param containerConfiguration the container configuration + */ + void writeMetadata( + ImageReference imageReference, + BuildableManifestTemplate manifestTemplate, + ContainerConfigurationTemplate containerConfiguration) + throws IOException { + Preconditions.checkNotNull(manifestTemplate.getContainerConfiguration()); + Preconditions.checkNotNull(manifestTemplate.getContainerConfiguration().getDigest()); + + Path imageDirectory = defaultCacheStorageFiles.getImageDirectory(imageReference); + Files.createDirectories(imageDirectory); + + try (LockFile ignored1 = LockFile.lock(imageDirectory.resolve("lock"))) { + writeMetadata(manifestTemplate, imageDirectory.resolve("manifest.json")); + writeMetadata(containerConfiguration, imageDirectory.resolve("config.json")); + } + } + + /** + * Writes a V2.1 manifest for a given image reference. + * + * @param imageReference the image reference to store the metadata for + * @param manifestTemplate the manifest + */ + void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) + throws IOException { + Path imageDirectory = defaultCacheStorageFiles.getImageDirectory(imageReference); + Files.createDirectories(imageDirectory); + + try (LockFile ignored1 = LockFile.lock(imageDirectory.resolve("lock"))) { + writeMetadata(manifestTemplate, imageDirectory.resolve("manifest.json")); + } + } + /** * Writes a compressed {@code layerBlob} to the {@code layerDirectory}. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java new file mode 100644 index 0000000000..c1a565f7ef --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.base.Preconditions; +import java.io.Closeable; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** Creates and deletes lock files. */ +public class LockFile implements Closeable { + + private static final ConcurrentHashMap lockMap = new ConcurrentHashMap<>(); + + private final Path lockFile; + private final FileLock fileLock; + + private LockFile(Path lockFile, FileLock fileLock) { + this.lockFile = lockFile; + this.fileLock = fileLock; + } + + /** + * Creates a lock file. + * + * @param lockFile the path of the lock file + * @return a new {@link LockFile} that can be released later + * @throws IOException if creating the lock file fails + */ + public static LockFile lock(Path lockFile) throws IOException { + try { + // This first lock is to prevent multiple threads from calling FileChannel.lock(), which would + // otherwise throw OverlappingFileLockException + lockMap.computeIfAbsent(lockFile, key -> new ReentrantLock()).lockInterruptibly(); + + } catch (InterruptedException ex) { + throw new IOException("Interrupted while trying to acquire lock", ex); + } + + Files.createDirectories(lockFile.getParent()); + FileLock fileLock = new FileOutputStream(lockFile.toFile()).getChannel().lock(); + return new LockFile(lockFile, fileLock); + } + + /** Releases the lock file. */ + @Override + public void close() { + try { + fileLock.release(); + + } catch (IOException ex) { + throw new IllegalStateException("Unable to release lock", ex); + + } finally { + try { + Preconditions.checkNotNull(lockMap.get(lockFile)).unlock(); + Files.delete(lockFile); + + } catch (IllegalMonitorStateException | IOException ignored) { + } + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 6b3b0042d9..d544d01a78 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -80,9 +80,9 @@ public static Image toImage(V21ManifestTemplate manifestTemplate) imageBuilder.addLayer(new DigestOnlyLayer(digest)); } - if (manifestTemplate.getContainerConfiguration() != null) { + if (manifestTemplate.getContainerConfiguration().isPresent()) { configureBuilderWithContainerConfiguration( - imageBuilder, manifestTemplate.getContainerConfiguration()); + imageBuilder, manifestTemplate.getContainerConfiguration().get()); } return imageBuilder.build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java index 320b0def8c..74f12d72c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** @@ -118,21 +119,21 @@ public List getFsLayers() { * * @return container configuration if the first history string holds it; {@code null} otherwise */ - @Nullable - public ContainerConfigurationTemplate getContainerConfiguration() { + public Optional getContainerConfiguration() { try { if (history.isEmpty()) { - return null; + return Optional.empty(); } String v1Compatibility = history.get(0).v1Compatibility; if (v1Compatibility == null) { - return null; + return Optional.empty(); } - return JsonTemplateMapper.readJson(v1Compatibility, ContainerConfigurationTemplate.class); + return Optional.of( + JsonTemplateMapper.readJson(v1Compatibility, ContainerConfigurationTemplate.class)); } catch (IOException ex) { // not a container configuration; ignore and continue - return null; + return Optional.empty(); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java index 5fc21292a2..86da067be8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java @@ -17,6 +17,9 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; import org.hamcrest.CoreMatchers; @@ -156,4 +159,31 @@ public void testGetTemporaryDirectory() { Assert.assertEquals( Paths.get("cache/directory/tmp"), testDefaultCacheStorageFiles.getTemporaryDirectory()); } + + @Test + public void testGetImagesDirectory() { + Assert.assertEquals( + Paths.get("cache/directory/images"), testDefaultCacheStorageFiles.getImagesDirectory()); + } + + @Test + public void testGetImageDirectory() throws InvalidImageReferenceException { + Path imagesDirectory = Paths.get("cache", "directory", "images"); + Assert.assertEquals(imagesDirectory, testDefaultCacheStorageFiles.getImagesDirectory()); + + Assert.assertEquals( + imagesDirectory.resolve("reg.istry/repo/sitory!tag"), + testDefaultCacheStorageFiles.getImageDirectory( + ImageReference.parse("reg.istry/repo/sitory:tag"))); + Assert.assertEquals( + imagesDirectory.resolve( + "reg.istry/repo!sha256!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + testDefaultCacheStorageFiles.getImageDirectory( + ImageReference.parse( + "reg.istry/repo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + Assert.assertEquals( + imagesDirectory.resolve("reg.istry!5000/repo/sitory!tag"), + testDefaultCacheStorageFiles.getImageDirectory( + ImageReference.parse("reg.istry:5000/repo/sitory:tag"))); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java index 58b1c382b0..53e9c78612 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java @@ -21,11 +21,20 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.junit.Assert; @@ -64,10 +73,12 @@ private static Blob decompress(Blob blob) throws IOException { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private DefaultCacheStorageFiles defaultCacheStorageFiles; + private Path cacheRoot; @Before public void setUp() throws IOException { - defaultCacheStorageFiles = new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + cacheRoot = temporaryFolder.newFolder().toPath(); + defaultCacheStorageFiles = new DefaultCacheStorageFiles(cacheRoot); } @Test @@ -99,6 +110,63 @@ public void testWrite_uncompressed() throws IOException { Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); } + @Test + public void testWriteMetadata_v21() + throws IOException, URISyntaxException, InvalidImageReferenceException { + Path manifestJsonFile = + Paths.get(getClass().getClassLoader().getResource("core/json/v21manifest.json").toURI()); + V21ManifestTemplate manifestTemplate = + JsonTemplateMapper.readJsonFromFile(manifestJsonFile, V21ManifestTemplate.class); + ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); + + new DefaultCacheStorageWriter(defaultCacheStorageFiles) + .writeMetadata(imageReference, manifestTemplate); + + Path savedManifestPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/manifest.json"); + Assert.assertTrue(Files.exists(savedManifestPath)); + + V21ManifestTemplate savedManifest = + JsonTemplateMapper.readJsonFromFile(savedManifestPath, V21ManifestTemplate.class); + Assert.assertEquals("amd64", savedManifest.getContainerConfiguration().get().getArchitecture()); + } + + @Test + public void testWriteMetadata_v22() + throws IOException, URISyntaxException, InvalidImageReferenceException { + Path containerConfigurationJsonFile = + Paths.get( + getClass().getClassLoader().getResource("core/json/containerconfig.json").toURI()); + ContainerConfigurationTemplate containerConfigurationTemplate = + JsonTemplateMapper.readJsonFromFile( + containerConfigurationJsonFile, ContainerConfigurationTemplate.class); + Path manifestJsonFile = + Paths.get(getClass().getClassLoader().getResource("core/json/v22manifest.json").toURI()); + BuildableManifestTemplate manifestTemplate = + JsonTemplateMapper.readJsonFromFile(manifestJsonFile, V22ManifestTemplate.class); + ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); + + new DefaultCacheStorageWriter(defaultCacheStorageFiles) + .writeMetadata(imageReference, manifestTemplate, containerConfigurationTemplate); + + Path savedManifestPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/manifest.json"); + Path savedConfigPath = + cacheRoot.resolve("images/image.reference/project/thing!tag/config.json"); + Assert.assertTrue(Files.exists(savedManifestPath)); + Assert.assertTrue(Files.exists(savedConfigPath)); + + V22ManifestTemplate savedManifest = + JsonTemplateMapper.readJsonFromFile(savedManifestPath, V22ManifestTemplate.class); + Assert.assertEquals( + "8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad", + savedManifest.getContainerConfiguration().getDigest().getHash()); + + ContainerConfigurationTemplate savedContainerConfig = + JsonTemplateMapper.readJsonFromFile(savedConfigPath, ContainerConfigurationTemplate.class); + Assert.assertEquals("wasm", savedContainerConfig.getArchitecture()); + } + private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(uncompressedLayerBlob); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java new file mode 100644 index 0000000000..1181157cfe --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link LockFile}. */ +public class LockFileTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testLockAndRelease() throws InterruptedException { + AtomicInteger atomicInt = new AtomicInteger(0); + + // Runnable that would produce a race condition without a lock file + Runnable procedure = + () -> { + try (LockFile ignored = + LockFile.lock(temporaryFolder.getRoot().toPath().resolve("testLock"))) { + Assert.assertTrue(Files.exists(temporaryFolder.getRoot().toPath().resolve("testLock"))); + + int valueBeforeSleep = atomicInt.intValue(); + Thread.sleep(100); + atomicInt.set(valueBeforeSleep + 1); + + } catch (InterruptedException | IOException ex) { + throw new AssertionError(ex); + } + }; + + // Run the runnable once in this thread + once in the main thread + Thread thread = new Thread(procedure); + thread.start(); + procedure.run(); + thread.join(); + + // Assert no overlap while lock was in place + Assert.assertEquals(2, atomicInt.intValue()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index 1b2bca70f4..dd5b6c4947 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -46,7 +46,7 @@ public void testFromJson() throws URISyntaxException, IOException, DigestExcepti manifestJson.getFsLayers().get(0).getDigest()); ContainerConfigurationTemplate containerConfiguration = - manifestJson.getContainerConfiguration(); + manifestJson.getContainerConfiguration().orElse(null); Assert.assertEquals( Arrays.asList("JAVA_HOME=/opt/openjdk", "PATH=/opt/openjdk/bin"), containerConfiguration.getContainerEnvironment()); From abb0118c55393296209a7091e421b6d80d9ed0ee Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 29 Apr 2019 18:51:02 -0400 Subject: [PATCH 0562/2020] Refactor public API to hide internal methods (#1659) * Refactor public API * Delete BuildSteps * Rename BuildStepsRunner to JibBuildRunner --- ...java => ContainerizerIntegrationTest.java} | 265 +++++++++--------- .../cloud/tools/jib/api/Containerizer.java | 95 ++++++- .../tools/jib/api/DockerDaemonImage.java | 33 +-- .../com/google/cloud/tools/jib/api/Jib.java | 2 +- .../tools/jib/api/JibContainerBuilder.java | 65 +++-- .../cloud/tools/jib/api/RegistryImage.java | 18 +- .../cloud/tools/jib/api/SourceImage.java | 26 -- .../google/cloud/tools/jib/api/TarImage.java | 20 +- .../cloud/tools/jib/api/TargetImage.java | 41 --- .../cloud/tools/jib/builder/BuildSteps.java | 128 --------- .../cloud/tools/jib/image/ImageReference.java | 2 +- .../tools/jib/api/ContainerizerTest.java | 48 +++- .../tools/jib/api/DockerDaemonImageTest.java | 26 +- .../jib/api/JibContainerBuilderTest.java | 49 ++-- .../tools/jib/api/RegistryImageTest.java | 32 +-- .../cloud/tools/jib/api/TarImageTest.java | 10 +- .../tools/jib/gradle/BuildDockerTask.java | 5 +- .../tools/jib/gradle/BuildImageTask.java | 4 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 4 +- .../gradle/GradleProjectPropertiesTest.java | 13 +- .../tools/jib/maven/BuildDockerMojo.java | 5 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 4 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 4 +- .../jib/maven/MavenProjectProperties.java | 2 + .../jib/maven/MavenProjectPropertiesTest.java | 13 +- .../common/BuildStepsExecutionException.java | 4 +- ...ldStepsRunner.java => JibBuildRunner.java} | 31 +- .../JavaContainerBuilderHelperTest.java | 16 +- ...unnerTest.java => JibBuildRunnerTest.java} | 24 +- 29 files changed, 445 insertions(+), 544 deletions(-) rename jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/{BuildStepsIntegrationTest.java => ContainerizerIntegrationTest.java} (63%) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java rename jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/{BuildStepsRunner.java => JibBuildRunner.java} (92%) rename jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/{BuildStepsRunnerTest.java => JibBuildRunnerTest.java} (94%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java similarity index 63% rename from jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java rename to jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java index 942a2c92c8..14b349eb4b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java @@ -17,13 +17,15 @@ package com.google.cloud.tools.jib.builder; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.builder.steps.BuildResult; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.Jib; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.RegistryImage; +import com.google.cloud.tools.jib.api.TarImage; +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LayerCountEvent; @@ -34,9 +36,9 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -45,6 +47,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -55,7 +58,7 @@ import org.hamcrest.CoreMatchers; import org.junit.AfterClass; import org.junit.Assert; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -63,8 +66,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Integration tests for {@link BuildSteps}. */ -public class BuildStepsIntegrationTest { +// TODO: now it looks like we can move everything here into JibIntegrationTest. +/** Integration tests for {@link Containerizer}. */ +public class ContainerizerIntegrationTest { /** * Helper class to hold a {@link ProgressEventHandler} and verify that it handles a full progress. @@ -88,13 +92,24 @@ private void checkCompletion() { } @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); + private static final ExecutorService executorService = Executors.newCachedThreadPool(); - private static final Logger logger = LoggerFactory.getLogger(BuildStepsIntegrationTest.class); + private static final Logger logger = LoggerFactory.getLogger(ContainerizerIntegrationTest.class); private static final String DISTROLESS_DIGEST = "sha256:f488c213f278bc5f9ffe3ddf30c5dbb2303a15a74146b738d12453088e662880"; - private static final double DOUBLE_ERROR_MARGIN = 1e-10; + public static ImmutableList fakeLayerConfigurations; + + @BeforeClass + public static void setUp() throws URISyntaxException, IOException { + fakeLayerConfigurations = + ImmutableList.of( + makeLayerConfiguration("core/application/dependencies", "/app/libs/"), + makeLayerConfiguration("core/application/resources", "/app/resources/"), + makeLayerConfiguration("core/application/classes", "/app/classes/")); + } + @AfterClass public static void cleanUp() { executorService.shutdown(); @@ -105,15 +120,14 @@ public static void cleanUp() { * LayerConfiguration} from those files. */ private static LayerConfiguration makeLayerConfiguration( - String resourcePath, AbsoluteUnixPath pathInContainer) - throws URISyntaxException, IOException { + String resourcePath, String pathInContainer) throws URISyntaxException, IOException { try (Stream fileStream = Files.list(Paths.get(Resources.getResource(resourcePath).toURI()))) { LayerConfiguration.Builder layerConfigurationBuilder = LayerConfiguration.builder(); fileStream.forEach( sourceFile -> layerConfigurationBuilder.addEntry( - sourceFile, pathInContainer.resolve(sourceFile.getFileName()))); + sourceFile, AbsoluteUnixPath.get(pathInContainer + sourceFile.getFileName()))); return layerConfigurationBuilder.build(); } } @@ -148,8 +162,6 @@ private static void assertDockerInspect(String imageReference) @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private ImmutableList fakeLayerConfigurations; - private final Map layerCounts = new ConcurrentHashMap<>(); private final Consumer layerCountConsumer = layerCountEvent -> @@ -158,36 +170,23 @@ private static void assertDockerInspect(String imageReference) layerCountEvent.getCount(), (oldValue, count) -> oldValue + count); - @Before - public void setUp() throws IOException, URISyntaxException { - fakeLayerConfigurations = - ImmutableList.of( - makeLayerConfiguration( - "core/application/dependencies", AbsoluteUnixPath.get("/app/libs/")), - makeLayerConfiguration( - "core/application/resources", AbsoluteUnixPath.get("/app/resources/")), - makeLayerConfiguration( - "core/application/classes", AbsoluteUnixPath.get("/app/classes/"))); - } + private final ProgressChecker progressChecker = new ProgressChecker(); + private final EventHandlers eventHandlers = + new EventHandlers() + .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) + .add(JibEventType.LAYER_COUNT, layerCountConsumer); @Test public void testSteps_forBuildToDockerRegistry() - throws IOException, InterruptedException, ExecutionException { - ProgressChecker progressChecker = new ProgressChecker(); - + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { long lastTime = System.nanoTime(); - BuildResult image1 = - BuildSteps.forBuildToDockerRegistry( - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of("localhost:5000", "testimage", "testtag")) - .setEventDispatcher( - new DefaultEventDispatcher( - new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) - .add(JibEventType.LAYER_COUNT, layerCountConsumer))) - .build()) - .run(); + JibContainer image1 = + buildRegistryImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of("localhost:5000", "testimage", "testtag"), + Collections.emptyList()); + progressChecker.checkCompletion(); Assert.assertEquals( layerCounts, @@ -204,13 +203,12 @@ public void testSteps_forBuildToDockerRegistry() logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); lastTime = System.nanoTime(); - BuildResult image2 = - BuildSteps.forBuildToDockerRegistry( - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of("localhost:5000", "testimage", "testtag")) - .build()) - .run(); + JibContainer image2 = + buildRegistryImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of("localhost:5000", "testimage", "testtag"), + Collections.emptyList()); + logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); Assert.assertEquals(image1, image2); @@ -221,7 +219,7 @@ public void testSteps_forBuildToDockerRegistry() Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); - String imageReferenceByDigest = "localhost:5000/testimage@" + image1.getImageDigest(); + String imageReferenceByDigest = "localhost:5000/testimage@" + image1.getDigest(); localRegistry.pull(imageReferenceByDigest); assertDockerInspect(imageReferenceByDigest); Assert.assertEquals( @@ -231,21 +229,12 @@ public void testSteps_forBuildToDockerRegistry() @Test public void testSteps_forBuildToDockerRegistry_multipleTags() - throws IOException, InterruptedException, ExecutionException { - BuildSteps buildImageSteps = - BuildSteps.forBuildToDockerRegistry( - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of("localhost:5000", "testimage", "testtag")) - .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) - .build()); - - long lastTime = System.nanoTime(); - buildImageSteps.run(); - logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); - lastTime = System.nanoTime(); - buildImageSteps.run(); - logger.info("Secondary build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { + buildRegistryImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of("localhost:5000", "testimage", "testtag"), + Arrays.asList("testtag2", "testtag3")); String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); @@ -269,14 +258,13 @@ public void testSteps_forBuildToDockerRegistry_multipleTags() } @Test - public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() - throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException { - BuildSteps.forBuildToDockerRegistry( - getBuildConfigurationBuilder( - ImageReference.parse("openjdk:8-jre-alpine"), - ImageReference.of("localhost:5000", "testimage", "testtag")) - .build()) - .run(); + public void tesBuildToDockerRegistry_dockerHubBaseImage() + throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + buildRegistryImage( + ImageReference.parse("openjdk:8-jre-alpine"), + ImageReference.of("localhost:5000", "testimage", "testtag"), + Collections.emptyList()); String imageReference = "localhost:5000/testimage:testtag"; new Command("docker", "pull", imageReference).run(); @@ -285,30 +273,22 @@ public void testSteps_forBuildToDockerRegistry_dockerHubBaseImage() } @Test - public void testSteps_forBuildToDockerDaemon() - throws IOException, InterruptedException, ExecutionException { - ProgressChecker progressChecker = new ProgressChecker(); - - BuildConfiguration buildConfiguration = - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of(null, "testdocker", null)) - .setEventDispatcher( - new DefaultEventDispatcher( - new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) - .add(JibEventType.LAYER_COUNT, layerCountConsumer))) - .build(); - BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); + public void testBuildToDockerDaemon() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { + buildDockerDaemonImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of(null, "testdocker", null), + Collections.emptyList()); progressChecker.checkCompletion(); Assert.assertEquals( - layerCounts, ImmutableMap.of( BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, 4, BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - 3)); + 3), + layerCounts); assertDockerInspect("testdocker"); Assert.assertEquals( @@ -316,16 +296,14 @@ public void testSteps_forBuildToDockerDaemon() } @Test - public void testSteps_forBuildToDockerDaemon_multipleTags() - throws IOException, InterruptedException, ExecutionException { + public void testBuildToDockerDaemon_multipleTags() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { String imageReference = "testdocker"; - BuildConfiguration buildConfiguration = - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of(null, imageReference, null)) - .setAdditionalTargetImageTags(ImmutableSet.of("testtag2", "testtag3")) - .build(); - BuildSteps.forBuildToDockerDaemon(DockerClient.newDefaultClient(), buildConfiguration).run(); + buildDockerDaemonImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of(null, imageReference, null), + Arrays.asList("testtag2", "testtag3")); assertDockerInspect(imageReference); Assert.assertEquals( @@ -341,43 +319,65 @@ public void testSteps_forBuildToDockerDaemon_multipleTags() } @Test - public void testSteps_forBuildToTarball() - throws IOException, InterruptedException, ExecutionException { - ProgressChecker progressChecker = new ProgressChecker(); - - BuildConfiguration buildConfiguration = - getBuildConfigurationBuilder( - ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), - ImageReference.of(null, "testtar", null)) - .setEventDispatcher( - new DefaultEventDispatcher( - new EventHandlers() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) - .add(JibEventType.LAYER_COUNT, layerCountConsumer))) - .build(); + public void testBuildTarball() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException { Path outputPath = temporaryFolder.newFolder().toPath().resolve("test.tar"); - BuildSteps.forBuildToTar(outputPath, buildConfiguration).run(); + buildTarImage( + ImageReference.of("gcr.io", "distroless/java", DISTROLESS_DIGEST), + ImageReference.of(null, "testtar", null), + outputPath, + Collections.emptyList()); progressChecker.checkCompletion(); Assert.assertEquals( - layerCounts, ImmutableMap.of( BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, 4, BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - 3)); + 3), + layerCounts); new Command("docker", "load", "--input", outputPath.toString()).run(); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); } - private BuildConfiguration.Builder getBuildConfigurationBuilder( - ImageReference baseImage, ImageReference targetImage) throws IOException { - ImageConfiguration baseImageConfiguration = ImageConfiguration.builder(baseImage).build(); - ImageConfiguration targetImageConfiguration = ImageConfiguration.builder(targetImage).build(); - ContainerConfiguration containerConfiguration = - ContainerConfiguration.builder() + private JibContainer buildRegistryImage( + ImageReference baseImage, ImageReference targetImage, List additionalTags) + throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, + ExecutionException { + return buildImage( + baseImage, Containerizer.to(RegistryImage.named(targetImage)), additionalTags); + } + + private JibContainer buildDockerDaemonImage( + ImageReference baseImage, ImageReference targetImage, List additionalTags) + throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, + ExecutionException { + return buildImage( + baseImage, Containerizer.to(DockerDaemonImage.named(targetImage)), additionalTags); + } + + private JibContainer buildTarImage( + ImageReference baseImage, + ImageReference targetImage, + Path outputPath, + List additionalTags) + throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, + ExecutionException { + return buildImage( + baseImage, + Containerizer.to(TarImage.named(targetImage).saveTo(outputPath)), + additionalTags); + } + + private JibContainer buildImage( + ImageReference baseImage, Containerizer containerizer, List additionalTags) + throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, + ExecutionException { + JibContainerBuilder containerBuilder = + Jib.from(baseImage) .setEntrypoint( JavaEntrypointConstructor.makeEntrypoint( JavaEntrypointConstructor.defaultClasspath(AbsoluteUnixPath.get("/app")), @@ -388,17 +388,18 @@ private BuildConfiguration.Builder getBuildConfigurationBuilder( .setExposedPorts( ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) - .build(); + .setLayers(fakeLayerConfigurations); + Path cacheDirectory = temporaryFolder.newFolder().toPath(); - return BuildConfiguration.builder() - .setBaseImageConfiguration(baseImageConfiguration) - .setTargetImageConfiguration(targetImageConfiguration) - .setContainerConfiguration(containerConfiguration) - .setBaseImageLayersCacheDirectory(cacheDirectory) - .setApplicationLayersCacheDirectory(cacheDirectory) + containerizer + .setBaseImageLayersCache(cacheDirectory) + .setApplicationLayersCache(cacheDirectory) .setAllowInsecureRegistries(true) - .setLayerConfigurations(fakeLayerConfigurations) .setToolName("jib-integration-test") - .setExecutorService(executorService); + .setExecutorService(executorService) + .setEventHandlers(eventHandlers); + additionalTags.forEach(containerizer::withAdditionalTag); + + return containerBuilder.containerize(containerizer); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 4113ccd8c3..601e54f987 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -17,11 +17,16 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.steps.StepsRunner; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.UserCacheHome; import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +35,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import javax.annotation.Nullable; /** Configures how to containerize. */ @@ -45,6 +51,10 @@ public class Containerizer { private static final String DEFAULT_TOOL_NAME = "jib-core"; + private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; + private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; + private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; + /** * Gets a new {@link Containerizer} that containerizes to a container registry. * @@ -53,7 +63,27 @@ public class Containerizer { * @return a new {@link Containerizer} */ public static Containerizer to(RegistryImage registryImage) { - return new Containerizer(registryImage); + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(registryImage.getImageReference()) + .setCredentialRetrievers(registryImage.getCredentialRetrievers()) + .build(); + + Function stepsRunnerFactory = + buildConfiguration -> + StepsRunner.begin(buildConfiguration) + .retrieveTargetRegistryCredentials() + .authenticatePush() + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .pushBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .pushContainerConfiguration() + .pushApplicationLayers() + .pushImage(); + + return new Containerizer( + DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory); } /** @@ -63,7 +93,24 @@ public static Containerizer to(RegistryImage registryImage) { * @return a new {@link Containerizer} */ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { - return new Containerizer(dockerDaemonImage); + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(dockerDaemonImage.getImageReference()).build(); + + DockerClient.Builder dockerClientBuilder = DockerClient.builder(); + dockerDaemonImage.getDockerExecutable().ifPresent(dockerClientBuilder::setDockerExecutable); + dockerClientBuilder.setDockerEnvironment( + ImmutableMap.copyOf(dockerDaemonImage.getDockerEnvironment())); + + Function stepsRunnerFactory = + buildConfiguration -> + StepsRunner.begin(buildConfiguration) + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .loadDocker(dockerClientBuilder.build()); + + return new Containerizer(DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory); } /** @@ -73,10 +120,25 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { * @return a new {@link Containerizer} */ public static Containerizer to(TarImage tarImage) { - return new Containerizer(tarImage); + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(tarImage.getImageReference()).build(); + + Function stepsRunnerFactory = + buildConfiguration -> + StepsRunner.begin(buildConfiguration) + .pullBaseImage() + .pullAndCacheBaseImageLayers() + .buildAndCacheApplicationLayers() + .buildImage() + .writeTarFile(tarImage.getOutputFile()); + + return new Containerizer(DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory); } - private final TargetImage targetImage; + private final String description; + private final ImageConfiguration imageConfiguration; + private final Function stepsRunnerFactory; + private final Set additionalTags = new HashSet<>(); @Nullable private ExecutorService executorService; private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; @@ -86,8 +148,13 @@ public static Containerizer to(TarImage tarImage) { private String toolName = DEFAULT_TOOL_NAME; /** Instantiate with {@link #to}. */ - private Containerizer(TargetImage targetImage) { - this.targetImage = targetImage; + private Containerizer( + String description, + ImageConfiguration imageConfiguration, + Function stepsRunnerFactory) { + this.description = description; + this.imageConfiguration = imageConfiguration; + this.stepsRunnerFactory = stepsRunnerFactory; } /** @@ -181,10 +248,6 @@ public Containerizer setToolName(String toolName) { return this; } - TargetImage getTargetImage() { - return targetImage; - } - Set getAdditionalTags() { return additionalTags; } @@ -223,4 +286,16 @@ boolean getAllowInsecureRegistries() { String getToolName() { return toolName; } + + String getDescription() { + return description; + } + + ImageConfiguration getImageConfiguration() { + return imageConfiguration; + } + + StepsRunner createStepsRunner(BuildConfiguration buildConfiguration) { + return stepsRunnerFactory.apply(buildConfiguration); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index c2c8062f35..bd0b14d615 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -17,20 +17,16 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.common.collect.ImmutableMap; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; /** Builds to the Docker daemon. */ -// TODO: Add tests once JibContainerBuilder#containerize() is added. -public class DockerDaemonImage implements TargetImage { +public class DockerDaemonImage { /** * Instantiate with the image reference to tag the built image with. This is the name that shows @@ -58,7 +54,7 @@ public static DockerDaemonImage named(String imageReference) private final ImageReference imageReference; @Nullable private Path dockerExecutable; - @Nullable private Map dockerEnvironment; + private Map dockerEnvironment = Collections.emptyMap(); /** Instantiate with {@link #named}. */ private DockerDaemonImage(ImageReference imageReference) { @@ -87,20 +83,15 @@ public DockerDaemonImage setDockerEnvironment(Map dockerEnvironm return this; } - @Override - public ImageConfiguration toImageConfiguration() { - return ImageConfiguration.builder(imageReference).build(); + ImageReference getImageReference() { + return imageReference; } - @Override - public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { - DockerClient.Builder dockerClientBuilder = DockerClient.builder(); - if (dockerExecutable != null) { - dockerClientBuilder.setDockerExecutable(dockerExecutable); - } - if (dockerEnvironment != null) { - dockerClientBuilder.setDockerEnvironment(ImmutableMap.copyOf(dockerEnvironment)); - } - return BuildSteps.forBuildToDockerDaemon(dockerClientBuilder.build(), buildConfiguration); + Optional getDockerExecutable() { + return Optional.ofNullable(dockerExecutable); + } + + Map getDockerEnvironment() { + return dockerEnvironment; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index ac52bee3f4..06955fea71 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -46,7 +46,7 @@ public static JibContainerBuilder from(String baseImageReference) * @return a new {@link JibContainerBuilder} to continue building the container */ public static JibContainerBuilder from(ImageReference baseImageReference) { - return new JibContainerBuilder(RegistryImage.named(baseImageReference)); + return from(RegistryImage.named(baseImageReference)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index a678b093d0..9179b23b81 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -17,13 +17,16 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; @@ -83,14 +86,20 @@ private static String capitalizeFirstLetter(String string) { private List layerConfigurations = new ArrayList<>(); /** Instantiate with {@link Jib#from}. */ - JibContainerBuilder(SourceImage baseImage) { + JibContainerBuilder(RegistryImage baseImage) { this(baseImage, BuildConfiguration.builder()); } @VisibleForTesting - JibContainerBuilder(SourceImage baseImage, BuildConfiguration.Builder buildConfigurationBuilder) { + JibContainerBuilder( + RegistryImage baseImage, BuildConfiguration.Builder buildConfigurationBuilder) { this.buildConfigurationBuilder = buildConfigurationBuilder; - buildConfigurationBuilder.setBaseImageConfiguration(baseImage.toImageConfiguration()); + + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(baseImage.getImageReference()) + .setCredentialRetrievers(baseImage.getCredentialRetrievers()) + .build(); + buildConfigurationBuilder.setBaseImageConfiguration(imageConfiguration); } /** @@ -474,30 +483,12 @@ JibContainer containerize( BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer, executorService); - // Logs the different source files used. - buildConfiguration - .getEventDispatcher() - .dispatch(LogEvent.info("Containerizing application with the following files:")); - - for (LayerConfiguration layerConfiguration : layerConfigurations) { - if (layerConfiguration.getLayerEntries().isEmpty()) { - continue; - } + EventDispatcher eventDispatcher = buildConfiguration.getEventDispatcher(); + logSources(eventDispatcher); - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); - - for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - buildConfiguration - .getEventDispatcher() - .dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); - } - } - - try { - BuildResult result = containerizer.getTargetImage().toBuildSteps(buildConfiguration).run(); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(eventDispatcher, containerizer.getDescription())) { + BuildResult result = containerizer.createStepsRunner(buildConfiguration).run(); return new JibContainer(result.getImageDigest(), result.getImageId()); } catch (ExecutionException ex) { @@ -525,11 +516,11 @@ JibContainer containerize( * @throws IOException if an I/O exception occurs */ @VisibleForTesting - public BuildConfiguration toBuildConfiguration( + BuildConfiguration toBuildConfiguration( Containerizer containerizer, ExecutorService executorService) throws CacheDirectoryCreationException, IOException { buildConfigurationBuilder - .setTargetImageConfiguration(containerizer.getTargetImage().toImageConfiguration()) + .setTargetImageConfiguration(containerizer.getImageConfiguration()) .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) .setApplicationLayersCacheDirectory(containerizer.getApplicationLayersCacheDirectory()) @@ -548,4 +539,22 @@ public BuildConfiguration toBuildConfiguration( return buildConfigurationBuilder.build(); } + + private void logSources(EventDispatcher eventDispatcher) { + // Logs the different source files used. + eventDispatcher.dispatch(LogEvent.info("Containerizing application with the following files:")); + + for (LayerConfiguration layerConfiguration : layerConfigurations) { + if (layerConfiguration.getLayerEntries().isEmpty()) { + continue; + } + + eventDispatcher.dispatch( + LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); + + for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { + eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); + } + } + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index faa1e87585..7ced968c05 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -17,9 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; @@ -41,8 +38,7 @@ * credentials are valid push (for using this as a target image) or pull (for using this as a source * image) credentials for the repository specified via the image reference. */ -// TODO: Add tests once JibContainerBuilder#containerize() is added. -public class RegistryImage implements SourceImage, TargetImage { +public class RegistryImage { /** * Instantiate with the image reference to use. @@ -118,15 +114,11 @@ public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetrie return this; } - @Override - public ImageConfiguration toImageConfiguration() { - return ImageConfiguration.builder(imageReference) - .setCredentialRetrievers(credentialRetrievers) - .build(); + ImageReference getImageReference() { + return imageReference; } - @Override - public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { - return BuildSteps.forBuildToDockerRegistry(buildConfiguration); + List getCredentialRetrievers() { + return credentialRetrievers; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java deleted file mode 100644 index af8ec474b0..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/SourceImage.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. - -import com.google.cloud.tools.jib.configuration.ImageConfiguration; - -/** Represents a source image for Jib to use (for example, as a base image). */ -interface SourceImage { - - ImageConfiguration toImageConfiguration(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index 44f64a0fbf..73789ced70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -17,9 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; @@ -34,8 +31,7 @@ * .saveTo(Paths.get("image.tar")); * } */ -// TODO: Add tests once JibContainerBuilder#containerize() is added. -public class TarImage implements TargetImage { +public class TarImage { /** Finishes constructing a {@link TarImage}. */ public static class Builder { @@ -88,16 +84,6 @@ private TarImage(ImageReference imageReference, Path outputFile) { this.outputFile = outputFile; } - @Override - public ImageConfiguration toImageConfiguration() { - return ImageConfiguration.builder(imageReference).build(); - } - - @Override - public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { - return BuildSteps.forBuildToTar(outputFile, buildConfiguration); - } - /** * Gets the output file to save the tarball archive to. * @@ -106,4 +92,8 @@ public BuildSteps toBuildSteps(BuildConfiguration buildConfiguration) { Path getOutputFile() { return outputFile; } + + ImageReference getImageReference() { + return imageReference; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java deleted file mode 100644 index b0eebdffda..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TargetImage.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. - -import com.google.cloud.tools.jib.builder.BuildSteps; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; - -/** Represents a destination for the Jib-built image. */ -interface TargetImage { - - /** - * Converts into an {@link ImageConfiguration}. For internal use only. - * - * @return an {@link ImageConfiguration} - */ - ImageConfiguration toImageConfiguration(); - - /** - * Converts into {@link BuildSteps}. For internal use only. - * - * @param buildConfiguration the {@link BuildConfiguration} to use - * @return {@link BuildSteps} - */ - BuildSteps toBuildSteps(BuildConfiguration buildConfiguration); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java deleted file mode 100644 index 13d41cc5ee..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildSteps.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -import com.google.cloud.tools.jib.builder.steps.BuildResult; -import com.google.cloud.tools.jib.builder.steps.StepsRunner; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; -import java.nio.file.Path; -import java.util.concurrent.ExecutionException; - -/** Steps for building an image. */ -public class BuildSteps { - - private static final String DESCRIPTION_FOR_DOCKER_REGISTRY = "Building and pushing image"; - private static final String DESCRIPTION_FOR_DOCKER_DAEMON = "Building image to Docker daemon"; - private static final String DESCRIPTION_FOR_TARBALL = "Building image tarball"; - - /** - * All the steps to build an image to a Docker registry. - * - * @param buildConfiguration the configuration parameters for the build - * @return a new {@link BuildSteps} for building to a registry - */ - public static BuildSteps forBuildToDockerRegistry(BuildConfiguration buildConfiguration) { - return new BuildSteps( - DESCRIPTION_FOR_DOCKER_REGISTRY, - buildConfiguration, - StepsRunner.begin(buildConfiguration) - .retrieveTargetRegistryCredentials() - .authenticatePush() - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .pushBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .pushContainerConfiguration() - .pushApplicationLayers() - .pushImage()); - } - - /** - * All the steps to build to Docker daemon - * - * @param dockerClient the {@link DockerClient} for running {@code docker} commands - * @param buildConfiguration the configuration parameters for the build - * @return a new {@link BuildSteps} for building to a Docker daemon - */ - public static BuildSteps forBuildToDockerDaemon( - DockerClient dockerClient, BuildConfiguration buildConfiguration) { - return new BuildSteps( - DESCRIPTION_FOR_DOCKER_DAEMON, - buildConfiguration, - StepsRunner.begin(buildConfiguration) - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .loadDocker(dockerClient)); - } - - /** - * All the steps to build an image tarball. - * - * @param outputPath the path to output the tarball to - * @param buildConfiguration the configuration parameters for the build - * @return a new {@link BuildSteps} for building a tarball - */ - public static BuildSteps forBuildToTar(Path outputPath, BuildConfiguration buildConfiguration) { - return new BuildSteps( - DESCRIPTION_FOR_TARBALL, - buildConfiguration, - StepsRunner.begin(buildConfiguration) - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .writeTarFile(outputPath)); - } - - private final String description; - private final BuildConfiguration buildConfiguration; - private final StepsRunner stepsRunner; - - /** - * @param description a description of what the steps do - * @param buildConfiguration the configuration parameters for the build - * @param stepsRunner runs the necessary steps to build an image - */ - private BuildSteps( - String description, BuildConfiguration buildConfiguration, StepsRunner stepsRunner) { - this.description = description; - this.buildConfiguration = buildConfiguration; - this.stepsRunner = stepsRunner; - } - - public BuildConfiguration getBuildConfiguration() { - return buildConfiguration; - } - - /** - * Executes the build. - * - * @return the build result - * @throws InterruptedException if the execution is interrupted - * @throws ExecutionException if an exception occurs during execution - */ - public BuildResult run() throws InterruptedException, ExecutionException { - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { - return stepsRunner.run(); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java index 9aba1ef8d8..ee2956b407 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java @@ -341,6 +341,6 @@ public String toString() { * @return the image reference in Docker-readable format, without hiding the tag */ public String toStringWithTag() { - return this + (usesDefaultTag() ? ":" + DEFAULT_TAG : ""); + return toString() + (usesDefaultTag() ? ":" + DEFAULT_TAG : ""); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index 7734393fbc..358c6cae52 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -17,15 +17,20 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; +import java.util.Arrays; import java.util.concurrent.ExecutorService; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link Containerizer}. */ @@ -43,14 +48,12 @@ public void testTo() throws CacheDirectoryCreationException { TarImage tarImage = TarImage.named(ImageReference.of(null, "repository", null)).saveTo(Paths.get("ignored")); - verifyTo(Containerizer.to(registryImage), registryImage); - verifyTo(Containerizer.to(dockerDaemonImage), dockerDaemonImage); - verifyTo(Containerizer.to(tarImage), tarImage); + verifyTo(Containerizer.to(registryImage)); + verifyTo(Containerizer.to(dockerDaemonImage)); + verifyTo(Containerizer.to(tarImage)); } - private void verifyTo(Containerizer containerizer, TargetImage expectedTargetImage) - throws CacheDirectoryCreationException { - Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); + private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreationException { Assert.assertTrue(containerizer.getAdditionalTags().isEmpty()); Assert.assertFalse(containerizer.getExecutorService().isPresent()); Assert.assertFalse(containerizer.getEventHandlers().isPresent()); @@ -73,7 +76,6 @@ private void verifyTo(Containerizer containerizer, TargetImage expectedTargetIma .setAllowInsecureRegistries(true) .setToolName("tool"); - Assert.assertSame(expectedTargetImage, containerizer.getTargetImage()); Assert.assertEquals(ImmutableSet.of("tag1", "tag2"), containerizer.getAdditionalTags()); Assert.assertTrue(containerizer.getExecutorService().isPresent()); Assert.assertEquals(mockExecutorService, containerizer.getExecutorService().get()); @@ -101,4 +103,36 @@ public void testWithAdditionalTag() { Assert.assertEquals("invalid tag '+invalid+'", ex.getMessage()); } } + + @Test + public void testGetImageConfiguration_registryImage() throws InvalidImageReferenceException { + CredentialRetriever credentialRetriever = Mockito.mock(CredentialRetriever.class); + Containerizer containerizer = + Containerizer.to( + RegistryImage.named("registry/image").addCredentialRetriever(credentialRetriever)); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("registry/image", imageConfiguration.getImage().toString()); + Assert.assertEquals( + Arrays.asList(credentialRetriever), imageConfiguration.getCredentialRetrievers()); + } + + @Test + public void testGetImageConfiguration_dockerDaemonImage() throws InvalidImageReferenceException { + Containerizer containerizer = Containerizer.to(DockerDaemonImage.named("docker/deamon/image")); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("docker/deamon/image", imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + } + + @Test + public void testGetImageConfiguration_tarImage() throws InvalidImageReferenceException { + Containerizer containerizer = + Containerizer.to(TarImage.named("tar/image").saveTo(Paths.get("output/file"))); + + ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); + Assert.assertEquals("tar/image", imageConfiguration.getImage().toString()); + Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java index 9d8fb27723..ee03b00c49 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -16,9 +16,10 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.common.collect.ImmutableMap; +import java.nio.file.Paths; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -26,13 +27,22 @@ public class DockerDaemonImageTest { @Test - public void testToImageConfiguration() throws InvalidImageReferenceException { + public void testGetters_default() throws InvalidImageReferenceException { DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named("docker/daemon/image"); - ImageConfiguration imageConfiguration = dockerDaemonImage.toImageConfiguration(); - Assert.assertEquals( - ImageReference.parse("docker/daemon/image").toString(), - imageConfiguration.getImage().toString()); - Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + Assert.assertEquals("docker/daemon/image", dockerDaemonImage.getImageReference().toString()); + Assert.assertEquals(Optional.empty(), dockerDaemonImage.getDockerExecutable()); + Assert.assertEquals(0, dockerDaemonImage.getDockerEnvironment().size()); + } + + @Test + public void testGetters() throws InvalidImageReferenceException { + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named("docker/daemon/image") + .setDockerExecutable(Paths.get("docker/binary")) + .setDockerEnvironment(ImmutableMap.of("key", "value")); + + Assert.assertEquals(Paths.get("docker/binary"), dockerDaemonImage.getDockerExecutable().get()); + Assert.assertEquals(ImmutableMap.of("key", "value"), dockerDaemonImage.getDockerEnvironment()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 111d2466d7..d6384dcbc3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.builder.BuildSteps; import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; @@ -71,9 +71,8 @@ public class JibContainerBuilderTest { @Test public void testToBuildConfiguration_containerConfigurationSet() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { - RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -85,7 +84,8 @@ public void testToBuildConfiguration_containerConfigurationSet() BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration( - Containerizer.to(baseImage), MoreExecutors.newDirectExecutorService()); + Containerizer.to(RegistryImage.named("target/image")), + MoreExecutors.newDirectExecutorService()); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -104,9 +104,8 @@ public void testToBuildConfiguration_containerConfigurationSet() @Test public void testToBuildConfiguration_containerConfigurationAdd() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { - RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) .setEntrypoint("entry", "point") .setEnvironment(ImmutableMap.of("name", "value")) .addEnvironmentVariable("environment", "variable") @@ -118,7 +117,8 @@ public void testToBuildConfiguration_containerConfigurationAdd() BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration( - Containerizer.to(baseImage), MoreExecutors.newDirectExecutorService()); + Containerizer.to(RegistryImage.named("target/image")), + MoreExecutors.newDirectExecutorService()); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -138,8 +138,6 @@ public void testToBuildConfiguration_containerConfigurationAdd() public void testToBuildConfiguration() throws InvalidImageReferenceException, CredentialRetrievalException, IOException, CacheDirectoryCreationException { - RegistryImage baseImage = - RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); RegistryImage targetImage = RegistryImage.named(ImageReference.of("gcr.io", "my-project/my-app", null)) .addCredential("username", "password"); @@ -150,6 +148,8 @@ public void testToBuildConfiguration() .setExecutorService(mockExecutorService) .setEventHandlers(new EventHandlers().add(mockJibEventConsumer)); + RegistryImage baseImage = + RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); JibContainerBuilder jibContainerBuilder = new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); @@ -162,16 +162,13 @@ public void testToBuildConfiguration() buildConfiguration.getContainerConfiguration()); Assert.assertEquals( - baseImage.toImageConfiguration().getImage().toString(), - buildConfiguration.getBaseImageConfiguration().getImage().toString()); + "base/image", buildConfiguration.getBaseImageConfiguration().getImage().toString()); Assert.assertEquals( - 1, buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers().size()); - Assert.assertSame( - mockCredentialRetriever, - buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers().get(0)); + Arrays.asList(mockCredentialRetriever), + buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); Assert.assertEquals( - targetImage.toImageConfiguration().getImage().toString(), + "gcr.io/my-project/my-app", buildConfiguration.getTargetImageConfiguration().getImage().toString()); Assert.assertEquals( 1, buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers().size()); @@ -227,10 +224,8 @@ public void testToBuildConfiguration() /** Verify that an internally-created ExecutorService is shutdown. */ @Test public void testContainerize_executorCreated() throws Exception { - - RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -250,10 +245,8 @@ public void testContainerize_executorCreated() throws Exception { /** Verify that a provided ExecutorService is not shutdown. */ @Test public void testContainerize_configuredExecutor() throws Exception { - - RegistryImage baseImage = RegistryImage.named("test-image"); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -280,16 +273,15 @@ private Containerizer createMockContainerizer() ExecutionException, DigestException { ImageReference targetImage = ImageReference.parse("target-image"); - TargetImage mockTargetImage = Mockito.mock(TargetImage.class); Containerizer mockContainerizer = Mockito.mock(Containerizer.class); - BuildSteps mockBuildSteps = Mockito.mock(BuildSteps.class); + StepsRunner stepsRunner = Mockito.mock(StepsRunner.class); BuildResult mockBuildResult = Mockito.mock(BuildResult.class); - Mockito.when(mockTargetImage.toImageConfiguration()) + Mockito.when(mockContainerizer.getImageConfiguration()) .thenReturn(ImageConfiguration.builder(targetImage).build()); - Mockito.when(mockTargetImage.toBuildSteps(Mockito.any(BuildConfiguration.class))) - .thenReturn(mockBuildSteps); - Mockito.when(mockBuildSteps.run()).thenReturn(mockBuildResult); + Mockito.when(mockContainerizer.createStepsRunner(Mockito.any(BuildConfiguration.class))) + .thenReturn(stepsRunner); + Mockito.when(stepsRunner.run()).thenReturn(mockBuildResult); Mockito.when(mockBuildResult.getImageDigest()) .thenReturn( DescriptorDigest.fromHash( @@ -299,7 +291,6 @@ private Containerizer createMockContainerizer() DescriptorDigest.fromHash( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); - Mockito.when(mockContainerizer.getTargetImage()).thenReturn(mockTargetImage); Mockito.when(mockContainerizer.getAdditionalTags()).thenReturn(Collections.emptySet()); Mockito.when(mockContainerizer.getBaseImageLayersCacheDirectory()).thenReturn(Paths.get("/")); Mockito.when(mockContainerizer.getApplicationLayersCacheDirectory()).thenReturn(Paths.get("/")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java index 68f8f4125d..411486b26b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -16,10 +16,8 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import org.junit.Assert; @@ -35,25 +33,25 @@ public class RegistryImageTest { @Mock private CredentialRetriever mockCredentialRetriever; @Test - public void testToImageConfiguration() - throws InvalidImageReferenceException, CredentialRetrievalException { - ImageConfiguration imageConfiguration = + public void testGetters_default() throws InvalidImageReferenceException { + RegistryImage image = RegistryImage.named("registry/image"); + + Assert.assertEquals("registry/image", image.getImageReference().toString()); + Assert.assertEquals(0, image.getCredentialRetrievers().size()); + } + + @Test + public void testGetters() + throws InvalidImageReferenceException, AssertionError, CredentialRetrievalException { + RegistryImage image = RegistryImage.named("registry/image") .addCredentialRetriever(mockCredentialRetriever) - .addCredential("username", "password") - .toImageConfiguration(); + .addCredential("username", "password"); - Assert.assertEquals( - ImageReference.parse("registry/image").toString(), - imageConfiguration.getImage().toString()); - Assert.assertEquals(2, imageConfiguration.getCredentialRetrievers().size()); - Assert.assertSame(mockCredentialRetriever, imageConfiguration.getCredentialRetrievers().get(0)); + Assert.assertEquals(2, image.getCredentialRetrievers().size()); + Assert.assertSame(mockCredentialRetriever, image.getCredentialRetrievers().get(0)); Assert.assertEquals( Credential.from("username", "password"), - imageConfiguration - .getCredentialRetrievers() - .get(1) - .retrieve() - .orElseThrow(AssertionError::new)); + image.getCredentialRetrievers().get(1).retrieve().get()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java index adaa3cecb3..eef9cda552 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Paths; import org.junit.Assert; @@ -27,13 +25,9 @@ public class TarImageTest { @Test - public void testToImageConfiguration() throws InvalidImageReferenceException { + public void testGetters() throws InvalidImageReferenceException { TarImage tarImage = TarImage.named("tar/image").saveTo(Paths.get("output/file")); - ImageConfiguration imageConfiguration = tarImage.toImageConfiguration(); - - Assert.assertEquals( - ImageReference.parse("tar/image").toString(), imageConfiguration.getImage().toString()); - Assert.assertEquals(0, imageConfiguration.getCredentialRetrievers().size()); + Assert.assertEquals("tar/image", tarImage.getImageReference().toString()); Assert.assertEquals(Paths.get("output/file"), tarImage.getOutputFile()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2454ac5ee2..afc2fdb98b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -23,12 +23,12 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -135,8 +135,7 @@ public void buildDocker() Path buildOutput = getProject().getBuildDir().toPath(); try { - BuildStepsRunner.forBuildToDockerDaemon( - targetImageReference, jibExtension.getTo().getTags()) + JibBuildRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index b821d08462..15fe7da536 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -22,12 +22,12 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -115,7 +115,7 @@ public void buildImage() Path buildOutput = getProject().getBuildDir().toPath(); try { - BuildStepsRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) + JibBuildRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index c5c09a81cd..57de55d8f5 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -21,12 +21,12 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -137,7 +137,7 @@ public void buildTar() Path buildOutput = getProject().getBuildDir().toPath(); try { - BuildStepsRunner.forBuildTar(tarOutputPath) + JibBuildRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 4c3858ab99..c7184c1610 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -19,6 +19,8 @@ import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; @@ -548,10 +550,13 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() private BuildConfiguration setupBuildConfiguration(String appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - return new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get(appRoot)) - .createContainerBuilder(RegistryImage.named("base")) - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("to")), MoreExecutors.newDirectExecutorService()); + JibContainerBuilder jibContainerBuilder = + new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get(appRoot)) + .createContainerBuilder(RegistryImage.named("base")); + return JibContainerBuilderTestHelper.toBuildConfiguration( + jibContainerBuilder, + Containerizer.to(RegistryImage.named("to")) + .setExecutorService(MoreExecutors.newDirectExecutorService())); } private void setUpWarProject(Path webAppDirectory) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 78ff6cff07..67ed27c755 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -24,13 +24,13 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -128,8 +128,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); try { - BuildStepsRunner.forBuildToDockerDaemon( - targetImageReference, getTargetImageAdditionalTags()) + JibBuildRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 164b79cafb..40661148a9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -24,12 +24,12 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -121,7 +121,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); try { - BuildStepsRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) + JibBuildRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 8ede4498f8..6fb60a4126 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -22,12 +22,12 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.BuildStepsRunner; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; +import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; @@ -99,7 +99,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build(); try { - BuildStepsRunner.forBuildTar(tarOutputPath) + JibBuildRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) .writeImageId(buildOutput.resolve("jib-image.id")) .build( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index ad34a38c44..fbdcbf65e6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -82,6 +82,8 @@ static MavenProjectProperties getForProject( * checks for a property defined in the POM, then returns null if neither are defined. * * @param propertyName the name of the system property + * @param project the Maven project + * @param session the Maven session * @return the value of the system property, or null if not defined */ @Nullable diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 387c21c731..f728084fc6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; @@ -543,9 +545,12 @@ public void testIsWarProject_GwtLibPackagingIsNotWar() { private BuildConfiguration setupBuildConfiguration(AbsoluteUnixPath appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - return new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog, appRoot) - .createContainerBuilder(RegistryImage.named("base")) - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("to")), MoreExecutors.newDirectExecutorService()); + JibContainerBuilder JibContainerBuilder = + new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog, appRoot) + .createContainerBuilder(RegistryImage.named("base")); + return JibContainerBuilderTestHelper.toBuildConfiguration( + JibContainerBuilder, + Containerizer.to(RegistryImage.named("to")) + .setExecutorService(MoreExecutors.newDirectExecutorService())); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java index 68e010900d..3250823a14 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsExecutionException.java @@ -16,9 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.builder.BuildSteps; - -/** Wraps an exception that happens during {@link BuildSteps#run}. */ +/** Wraps an exception that happens during containerization. */ public class BuildStepsExecutionException extends Exception { BuildStepsExecutionException(String message, Throwable cause) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java similarity index 92% rename from jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java rename to jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index 40e3702a5f..d80dc184e0 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -21,7 +21,6 @@ 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.builder.BuildSteps; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; @@ -44,8 +43,8 @@ import javax.annotation.Nullable; import org.apache.http.conn.HttpHostConnectException; -/** Runs a {@link BuildSteps} and builds helpful error messages. */ -public class BuildStepsRunner { +/** Runs Jib and builds helpful error messages. */ +public class JibBuildRunner { private static final String STARTUP_MESSAGE_PREFIX_FOR_DOCKER_REGISTRY = "Containerizing application to "; @@ -84,11 +83,11 @@ private static String buildMessageWithTargetImageReferences( * * @param targetImageReference the target image reference * @param additionalTags additional tags to push to - * @return a {@link BuildStepsRunner} for building to a registry + * @return a {@link JibBuildRunner} for building to a registry */ - public static BuildStepsRunner forBuildImage( + public static JibBuildRunner forBuildImage( ImageReference targetImageReference, Set additionalTags) { - return new BuildStepsRunner( + return new JibBuildRunner( buildMessageWithTargetImageReferences( targetImageReference, additionalTags, @@ -103,11 +102,11 @@ public static BuildStepsRunner forBuildImage( * * @param targetImageReference the target image reference * @param additionalTags additional tags to push to - * @return a {@link BuildStepsRunner} for building to a Docker daemon + * @return a {@link JibBuildRunner} for building to a Docker daemon */ - public static BuildStepsRunner forBuildToDockerDaemon( + public static JibBuildRunner forBuildToDockerDaemon( ImageReference targetImageReference, Set additionalTags) { - return new BuildStepsRunner( + return new JibBuildRunner( buildMessageWithTargetImageReferences( targetImageReference, additionalTags, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), buildMessageWithTargetImageReferences( @@ -118,10 +117,10 @@ public static BuildStepsRunner forBuildToDockerDaemon( * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. * * @param outputPath the path to output the tarball to - * @return a {@link BuildStepsRunner} for building a tarball + * @return a {@link JibBuildRunner} for building a tarball */ - public static BuildStepsRunner forBuildTar(Path outputPath) { - return new BuildStepsRunner( + public static JibBuildRunner forBuildTar(Path outputPath) { + return new JibBuildRunner( String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); } @@ -153,13 +152,13 @@ private static void handleRegistryUnauthorizedException( @Nullable private Path imageIdOutputPath; @VisibleForTesting - BuildStepsRunner(String startupMessage, String successMessage) { + JibBuildRunner(String startupMessage, String successMessage) { this.startupMessage = startupMessage; this.successMessage = successMessage; } /** - * Runs the {@link BuildSteps}. + * Runs the build steps. * * @param jibContainerBuilder the {@link JibContainerBuilder} * @param containerizer the {@link Containerizer} @@ -247,7 +246,7 @@ public JibContainer build( * @param imageDigestOutputPath the location to write the image digest or {@code null} to skip * @return this */ - public BuildStepsRunner writeImageDigest(@Nullable Path imageDigestOutputPath) { + public JibBuildRunner writeImageDigest(@Nullable Path imageDigestOutputPath) { this.imageDigestOutputPath = imageDigestOutputPath; return this; } @@ -258,7 +257,7 @@ public BuildStepsRunner writeImageDigest(@Nullable Path imageDigestOutputPath) { * @param imageIdOutputPath the location to write the image id or {@code null} to skip * @return this */ - public BuildStepsRunner writeImageId(@Nullable Path imageIdOutputPath) { + public JibBuildRunner writeImageId(@Nullable Path imageIdOutputPath) { this.imageIdOutputPath = imageIdOutputPath; return this; } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 845b229581..342d37dfe9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; +import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; @@ -104,14 +106,16 @@ public void testFromExplodedWar() Path temporaryExplodedWar = temporaryFolder.getRoot().toPath().resolve("exploded-war"); Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); - Path extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); + Paths.get(Resources.getResource("core/layer").toURI()); - BuildConfiguration configuration = + JibContainerBuilder jibContainerBuilder = JavaContainerBuilderHelper.fromExplodedWar( - RegistryImage.named("base"), temporaryExplodedWar, AbsoluteUnixPath.get("/my/app")) - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("target")), - MoreExecutors.newDirectExecutorService()); + RegistryImage.named("base"), temporaryExplodedWar, AbsoluteUnixPath.get("/my/app")); + BuildConfiguration configuration = + JibContainerBuilderTestHelper.toBuildConfiguration( + jibContainerBuilder, + Containerizer.to(RegistryImage.named("target")) + .setExecutorService(MoreExecutors.newDirectExecutorService())); List resourcesLayerConfigurations = getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java similarity index 94% rename from jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java rename to jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 59a9bbd6db..5fa82f8e07 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/BuildStepsRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -41,9 +41,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link BuildStepsRunner}. */ +/** Tests for {@link JibBuildRunner}. */ @RunWith(MockitoJUnitRunner.class) -public class BuildStepsRunnerTest { +public class JibBuildRunnerTest { private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = new HelpfulSuggestions( @@ -70,17 +70,17 @@ public class BuildStepsRunnerTest { @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; - private BuildStepsRunner testBuildImageStepsRunner; + private JibBuildRunner testJibBuildRunner; @Before public void setUpMocks() { - testBuildImageStepsRunner = new BuildStepsRunner("ignored", "ignored"); + testJibBuildRunner = new JibBuildRunner("ignored", "ignored"); } @Test public void testBuildImage_pass() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, TEST_HELPFUL_SUGGESTIONS); } @@ -95,7 +95,7 @@ public void testBuildImage_httpHostConnectException() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -117,7 +117,7 @@ public void testBuildImage_unknownHostException() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -140,7 +140,7 @@ public void testBuildImage_insecureRegistryException() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -168,7 +168,7 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -197,7 +197,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -220,7 +220,7 @@ public void testBuildImage_registryCredentialsNotSentException() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, @@ -242,7 +242,7 @@ public void testBuildImage_other() .containerize(mockContainerizer); try { - testBuildImageStepsRunner.build( + testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, From 0a2a62875ed3ac4dc2c9de277b63c4d2dc3921b8 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 30 Apr 2019 08:39:00 -0400 Subject: [PATCH 0563/2020] Test that images are reproducible (#1669) --- .../tools/jib/api/ReproducibleImageTest.java | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java new file mode 100644 index 0000000000..9c96ad6732 --- /dev/null +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.configuration.LayerConfiguration; +import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.registry.RegistryException; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; +import java.util.zip.GZIPInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Verify that created image has explicit directory structures, default timestamps, permissions, and + * file orderings. + */ +public class ReproducibleImageTest { + + @ClassRule public static final TemporaryFolder imageLocation = new TemporaryFolder(); + + private static File imageTar; + + @BeforeClass + public static void createImage() + throws InvalidImageReferenceException, InterruptedException, CacheDirectoryCreationException, + IOException, RegistryException, ExecutionException { + + Path root = imageLocation.getRoot().toPath(); + Path fileA = Files.createFile(root.resolve("fileA.txt")); + Path fileB = Files.createFile(root.resolve("fileB.txt")); + Path fileC = Files.createFile(root.resolve("fileC.txt")); + Path subdir = Files.createDirectory(root.resolve("dir")); + Path subsubdir = Files.createDirectory(subdir.resolve("subdir")); + Files.createFile(subdir.resolve("fileD.txt")); + Files.createFile(subsubdir.resolve("fileE.txt")); + + imageTar = new File(imageLocation.getRoot(), "image.tar"); + Containerizer containerizer = + Containerizer.to(TarImage.named("jib-core/reproducible").saveTo(imageTar.toPath())); + + Jib.fromScratch() + .setEntrypoint("echo", "Hello World") + .addLayer(ImmutableList.of(fileA), AbsoluteUnixPath.get("/app")) + // layer with out-of-order files + .addLayer(ImmutableList.of(fileC, fileB), "/app") + .addLayer( + LayerConfiguration.builder() + .addEntryRecursive(subdir, AbsoluteUnixPath.get("/app")) + .build()) + .containerize(containerizer); + } + + @Test + public void testImageLayout() throws IOException { + Set paths = new HashSet<>(); + layerEntriesDo( + (layerName, layerEntry) -> { + if (layerEntry.isFile()) { + paths.add(layerEntry.getName()); + } + }); + Assert.assertEquals( + ImmutableSet.of( + "app/fileA.txt", + "app/fileB.txt", + "app/fileC.txt", + "app/fileD.txt", + "app/subdir/fileE.txt"), + paths); + } + + @Test + public void testAllFileAndDirectories() throws IOException { + layerEntriesDo( + (layerName, layerEntry) -> + Assert.assertTrue(layerEntry.isFile() || layerEntry.isDirectory())); + } + + @Test + public void testTimestampsEpochPlus1s() throws IOException { + layerEntriesDo( + (layerName, layerEntry) -> { + Instant modificationTime = layerEntry.getLastModifiedDate().toInstant(); + Assert.assertEquals( + layerName + ": " + layerEntry.getName(), Instant.ofEpochSecond(1), modificationTime); + }); + } + + @Test + public void testPermissions() throws IOException { + Assert.assertEquals(0644, FilePermissions.DEFAULT_FILE_PERMISSIONS.getPermissionBits()); + Assert.assertEquals(0755, FilePermissions.DEFAULT_FOLDER_PERMISSIONS.getPermissionBits()); + layerEntriesDo( + (layerName, layerEntry) -> { + if (layerEntry.isFile()) { + Assert.assertEquals( + layerName + ": " + layerEntry.getName(), 0644, layerEntry.getMode() & 0777); + } else if (layerEntry.isDirectory()) { + Assert.assertEquals( + layerName + ": " + layerEntry.getName(), 0755, layerEntry.getMode() & 0777); + } + }); + } + + @Test + public void testNoImplicitParentDirectories() throws IOException { + Set directories = new HashSet<>(); + layerEntriesDo( + (layerName, layerEntry) -> { + String entryPath = layerEntry.getName(); + if (layerEntry.isDirectory()) { + Assert.assertTrue("directories in tar end with /", entryPath.endsWith("/")); + entryPath = entryPath.substring(0, entryPath.length() - 1); + } + + int lastSlashPosition = entryPath.lastIndexOf('/'); + String parent = entryPath.substring(0, Math.max(0, lastSlashPosition)); + if (!parent.isEmpty()) { + Assert.assertTrue( + "layer has implicit parent directory: " + parent, directories.contains(parent)); + } + if (layerEntry.isDirectory()) { + directories.add(entryPath); + } + }); + } + + @Test + public void testFileOrdering() throws IOException { + Multimap layerPaths = ArrayListMultimap.create(); + layerEntriesDo((layerName, layerEntry) -> layerPaths.put(layerName, layerEntry.getName())); + for (Collection paths : layerPaths.asMap().values()) { + List sorted = new ArrayList<>(paths); + // ReproducibleLayerBuilder sorts by TarArchiveEntry::getName() + Collections.sort(sorted); + Assert.assertEquals("layer files are not consistently sorted", sorted, (List) paths); + } + } + + private void layerEntriesDo(BiConsumer layerConsumer) + throws IOException { + + try (TarArchiveInputStream input = new TarArchiveInputStream(new FileInputStream(imageTar))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextTarEntry()) != null) { + String imageEntryName = imageEntry.getName(); + // assume all .tar.gz files are layers + if (imageEntry.isFile() && imageEntryName.endsWith(".tar.gz")) { + @SuppressWarnings("resource") // must not close sub-streams + TarArchiveInputStream layer = new TarArchiveInputStream(new GZIPInputStream(input)); + TarArchiveEntry layerEntry; + while ((layerEntry = layer.getNextTarEntry()) != null) { + layerConsumer.accept(imageEntryName, layerEntry); + } + } + } + } + } +} From 91ee29b480667b6ec22ee8496594e39acf967158 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 30 Apr 2019 16:21:03 -0400 Subject: [PATCH 0564/2020] Remove unnecessary cache interfaces (#1670) --- .../google/cloud/tools/jib/cache/Cache.java | 31 +++-- .../cloud/tools/jib/cache/CacheStorage.java | 114 ----------------- ...orageFiles.java => CacheStorageFiles.java} | 4 +- ...ageReader.java => CacheStorageReader.java} | 22 ++-- ...ageWriter.java => CacheStorageWriter.java} | 81 ++++++------ .../cloud/tools/jib/cache/CachedLayer.java | 101 ++++++++++++--- .../tools/jib/cache/DefaultCacheStorage.java | 113 ----------------- .../tools/jib/cache/DefaultCachedLayer.java | 115 ------------------ .../jib/cache/UncompressedCacheWrite.java | 61 ---------- .../jib/builder/steps/BuildImageStepTest.java | 37 +----- ...esTest.java => CacheStorageFilesTest.java} | 45 ++++--- ...rTest.java => CacheStorageReaderTest.java} | 65 +++++----- ...rTest.java => CacheStorageWriterTest.java} | 25 ++-- ...hedLayerTest.java => CachedLayerTest.java} | 17 ++- 14 files changed, 228 insertions(+), 603 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java rename jib-core/src/main/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageFiles.java => CacheStorageFiles.java} (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageReader.java => CacheStorageReader.java} (84%) rename jib-core/src/main/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageWriter.java => CacheStorageWriter.java} (81%) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java rename jib-core/src/test/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageFilesTest.java => CacheStorageFilesTest.java} (79%) rename jib-core/src/test/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageReaderTest.java => CacheStorageReaderTest.java} (65%) rename jib-core/src/test/java/com/google/cloud/tools/jib/cache/{DefaultCacheStorageWriterTest.java => CacheStorageWriterTest.java} (89%) rename jib-core/src/test/java/com/google/cloud/tools/jib/cache/{DefaultCachedLayerTest.java => CachedLayerTest.java} (84%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 447f92b730..3fdb44a329 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -33,9 +33,6 @@ /** * Cache for storing data to be shared between Jib executions. * - *

Uses the default cache storage engine ({@link DefaultCacheStorage}) with layer entries as the - * selector ({@link LayerEntriesSelector}). - * *

This class is immutable and safe to use across threads. */ @Immutable @@ -50,13 +47,15 @@ public class Cache { */ public static Cache withDirectory(Path cacheDirectory) throws IOException { Files.createDirectories(cacheDirectory); - return new Cache(DefaultCacheStorage.withDirectory(cacheDirectory)); + return new Cache(new CacheStorageFiles(cacheDirectory)); } - private final CacheStorage cacheStorage; + private final CacheStorageWriter cacheStorageWriter; + private final CacheStorageReader cacheStorageReader; - private Cache(CacheStorage cacheStorage) { - this.cacheStorage = cacheStorage; + private Cache(CacheStorageFiles cacheStorageFiles) { + this.cacheStorageWriter = new CacheStorageWriter(cacheStorageFiles); + this.cacheStorageReader = new CacheStorageReader(cacheStorageFiles); } /** @@ -72,7 +71,8 @@ public void writeMetadata( BuildableManifestTemplate manifestTemplate, ContainerConfigurationTemplate containerConfigurationTemplate) throws IOException { - cacheStorage.writeMetadata(imageReference, manifestTemplate, containerConfigurationTemplate); + cacheStorageWriter.writeMetadata( + imageReference, manifestTemplate, containerConfigurationTemplate); } /** @@ -84,7 +84,7 @@ public void writeMetadata( */ public void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) throws IOException { - cacheStorage.writeMetadata(imageReference, manifestTemplate); + cacheStorageWriter.writeMetadata(imageReference, manifestTemplate); } /** @@ -97,7 +97,7 @@ public void writeMetadata(ImageReference imageReference, V21ManifestTemplate man * @throws IOException if an I/O exception occurs */ public CachedLayer writeCompressedLayer(Blob compressedLayerBlob) throws IOException { - return cacheStorage.write(compressedLayerBlob); + return cacheStorageWriter.writeCompressed(compressedLayerBlob); } /** @@ -111,9 +111,8 @@ public CachedLayer writeCompressedLayer(Blob compressedLayerBlob) throws IOExcep */ public CachedLayer writeUncompressedLayer( Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { - return cacheStorage.write( - new UncompressedCacheWrite( - uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries))); + return cacheStorageWriter.writeUncompressed( + uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries)); } /** @@ -127,12 +126,12 @@ public CachedLayer writeUncompressedLayer( public Optional retrieve(ImmutableList layerEntries) throws IOException, CacheCorruptedException { Optional optionalSelectedLayerDigest = - cacheStorage.select(LayerEntriesSelector.generateSelector(layerEntries)); + cacheStorageReader.select(LayerEntriesSelector.generateSelector(layerEntries)); if (!optionalSelectedLayerDigest.isPresent()) { return Optional.empty(); } - return cacheStorage.retrieve(optionalSelectedLayerDigest.get()); + return cacheStorageReader.retrieve(optionalSelectedLayerDigest.get()); } /** @@ -145,6 +144,6 @@ public Optional retrieve(ImmutableList layerEntries) */ public Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { - return cacheStorage.retrieve(layerDigest); + return cacheStorageReader.retrieve(layerDigest); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java deleted file mode 100644 index eacdc83b95..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorage.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import java.io.IOException; -import java.util.Optional; -import java.util.Set; - -/** - * Interface for queries to a cache storage engine. - * - *

The cache storage engine stores layer data in compressed form. These entries are read out as - * {@link CachedLayer}s. Cache entries can be retrieved by the layer digest. - * - *

The cache entries can also be queried by an arbitrarily-defined selector (in digest format). - * The selectors do not need to be unique. An example of a selector could be the digest of the list - * of source file paths that constructed the cached layer. - * - *

Implementations must be thread-safe and should be immutable. - */ -interface CacheStorage { - - /** - * Saves the {@link UncompressedCacheWrite}. - * - * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} - * @return the {@link CachedLayer} for the written {@link UncompressedCacheWrite} - * @throws IOException if an I/O exception occurs - */ - CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException; - - /** - * Saves a compressed layer {@link Blob}. - * - * @param compressedLayerBlob the compressed layer {@link Blob} - * @return the {@link CachedLayer} for the written layer - * @throws IOException if an I/O exception occurs - */ - CachedLayer write(Blob compressedLayerBlob) throws IOException; - - /** - * Fetches all the layer digests stored. - * - * @return the set of layer digests (that can be retrieved via {@link #retrieve}) - * @throws CacheCorruptedException if the cache was found to be corrupted - * @throws IOException if an I/O exception occurs - */ - Set fetchDigests() throws IOException, CacheCorruptedException; - - /** - * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. - * - * @param layerDigest the layer digest - * @return the {@link CachedLayer} referenced by the layer digest, if found - * @throws CacheCorruptedException if the cache was found to be corrupted - * @throws IOException if an I/O exception occurs - */ - Optional retrieve(DescriptorDigest layerDigest) - throws IOException, CacheCorruptedException; - - /** - * Queries for layer digests that can be selected with the {@code selector}. - * - * @param selector the selector to query with - * @return the layer digest selected, if found - * @throws IOException if an I/O exception occurs - */ - Optional select(DescriptorDigest selector) - throws IOException, CacheCorruptedException; - - /** - * Saves the manifest and container configuration for a V2.2 or OCI image. - * - * @param imageReference the image reference to store the metadata for - * @param manifestTemplate the image's manifest - * @param containerConfigurationTemplate the image's container configuration - * @throws IOException if an I/O exception occurs - */ - void writeMetadata( - ImageReference imageReference, - BuildableManifestTemplate manifestTemplate, - ContainerConfigurationTemplate containerConfigurationTemplate) - throws IOException; - - /** - * Saves a V2.1 manifest. - * - * @param imageReference the image reference to store the metadata for - * @param manifestTemplate the image's manifest - * @throws IOException if an I/O exception occurs - */ - void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) - throws IOException; -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java index 47324e504d..80c691d05f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -23,7 +23,7 @@ import java.security.DigestException; /** Resolves the files used in the default cache storage engine. */ -class DefaultCacheStorageFiles { +class CacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; private static final String IMAGES_DIRECTORY = "images"; @@ -61,7 +61,7 @@ static DescriptorDigest getDiffId(Path layerFile) throws CacheCorruptedException private final Path cacheDirectory; - DefaultCacheStorageFiles(Path cacheDirectory) { + CacheStorageFiles(Path cacheDirectory) { this.cacheDirectory = cacheDirectory; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java similarity index 84% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index c97ca0b235..157bc0b973 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -31,12 +31,12 @@ import java.util.stream.Stream; /** Reads from the default cache storage engine. */ -class DefaultCacheStorageReader { +class CacheStorageReader { - private final DefaultCacheStorageFiles defaultCacheStorageFiles; + private final CacheStorageFiles cacheStorageFiles; - DefaultCacheStorageReader(DefaultCacheStorageFiles defaultCacheStorageFiles) { - this.defaultCacheStorageFiles = defaultCacheStorageFiles; + CacheStorageReader(CacheStorageFiles cacheStorageFiles) { + this.cacheStorageFiles = cacheStorageFiles; } /** @@ -47,8 +47,7 @@ class DefaultCacheStorageReader { * @throws IOException if an I/O exception occurs */ Set fetchDigests() throws IOException, CacheCorruptedException { - try (Stream layerDirectories = - Files.list(defaultCacheStorageFiles.getLayersDirectory())) { + try (Stream layerDirectories = Files.list(cacheStorageFiles.getLayersDirectory())) { List layerDirectoriesList = layerDirectories.collect(Collectors.toList()); Set layerDigests = new HashSet<>(layerDirectoriesList.size()); for (Path layerDirectory : layerDirectoriesList) { @@ -73,17 +72,16 @@ Set fetchDigests() throws IOException, CacheCorruptedException */ Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { - Path layerDirectory = defaultCacheStorageFiles.getLayerDirectory(layerDigest); + Path layerDirectory = cacheStorageFiles.getLayerDirectory(layerDigest); if (!Files.exists(layerDirectory)) { return Optional.empty(); } - DefaultCachedLayer.Builder cachedLayerBuilder = - DefaultCachedLayer.builder().setLayerDigest(layerDigest); + CachedLayer.Builder cachedLayerBuilder = CachedLayer.builder().setLayerDigest(layerDigest); try (Stream filesInLayerDirectory = Files.list(layerDirectory)) { for (Path fileInLayerDirectory : filesInLayerDirectory.collect(Collectors.toList())) { - if (DefaultCacheStorageFiles.isLayerFile(fileInLayerDirectory)) { + if (CacheStorageFiles.isLayerFile(fileInLayerDirectory)) { if (cachedLayerBuilder.hasLayerBlob()) { throw new CacheCorruptedException( "Multiple layer files found for layer with digest " @@ -93,7 +91,7 @@ Optional retrieve(DescriptorDigest layerDigest) } cachedLayerBuilder .setLayerBlob(Blobs.from(fileInLayerDirectory)) - .setLayerDiffId(DefaultCacheStorageFiles.getDiffId(fileInLayerDirectory)) + .setLayerDiffId(CacheStorageFiles.getDiffId(fileInLayerDirectory)) .setLayerSize(Files.size(fileInLayerDirectory)); } } @@ -112,7 +110,7 @@ Optional retrieve(DescriptorDigest layerDigest) */ Optional select(DescriptorDigest selector) throws CacheCorruptedException, IOException { - Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); if (!Files.exists(selectorFile)) { return Optional.empty(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java similarity index 81% rename from jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index 0768103421..eb6d4b56af 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -43,9 +43,10 @@ import java.nio.file.StandardCopyOption; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import javax.annotation.Nullable; /** Writes to the default cache storage engine. */ -class DefaultCacheStorageWriter { +class CacheStorageWriter { /** Holds information about a layer that was written. */ private static class WrittenLayer { @@ -137,10 +138,10 @@ private static void writeMetadata(JsonTemplate jsonTemplate, Path destination) } } - private final DefaultCacheStorageFiles defaultCacheStorageFiles; + private final CacheStorageFiles cacheStorageFiles; - DefaultCacheStorageWriter(DefaultCacheStorageFiles defaultCacheStorageFiles) { - this.defaultCacheStorageFiles = defaultCacheStorageFiles; + CacheStorageWriter(CacheStorageFiles cacheStorageFiles) { + this.cacheStorageFiles = cacheStorageFiles; } /** @@ -153,14 +154,14 @@ private static void writeMetadata(JsonTemplate jsonTemplate, Path destination) * @return the {@link CachedLayer} representing the written entry * @throws IOException if an I/O exception occurs */ - CachedLayer write(Blob compressedLayerBlob) throws IOException { + CachedLayer writeCompressed(Blob compressedLayerBlob) throws IOException { // Creates the layers directory if it doesn't exist. - Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); + Files.createDirectories(cacheStorageFiles.getLayersDirectory()); // Creates the temporary directory. - Files.createDirectories(defaultCacheStorageFiles.getTemporaryDirectory()); + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(defaultCacheStorageFiles.getTemporaryDirectory())) { + new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); // Writes the layer file to the temporary directory. @@ -169,13 +170,12 @@ CachedLayer write(Blob compressedLayerBlob) throws IOException { // Moves the temporary directory to the final location. moveIfDoesNotExist( - temporaryLayerDirectory, - defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + temporaryLayerDirectory, cacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); // Updates cachedLayer with the blob information. Path layerFile = - defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); - return DefaultCachedLayer.builder() + cacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + return CachedLayer.builder() .setLayerDigest(writtenLayer.layerDigest) .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) @@ -185,55 +185,55 @@ CachedLayer write(Blob compressedLayerBlob) throws IOException { } /** - * Writes the {@link UncompressedCacheWrite}. - * - *

The {@link UncompressedCacheWrite} is written out to the cache directory in the form: + * Writes an uncompressed {@link Blob} out to the cache directory in the form: * *

    - *
  • The {@link UncompressedCacheWrite#getUncompressedLayerBlob} is written to the layer - * directory under the layers directory corresponding to the layer blob. - *
  • The {@link UncompressedCacheWrite#getSelector} is written to the selector file under the - * selectors directory. + *
  • The {@code uncompressedLayerBlob} is written to the layer directory under the layers + * directory corresponding to the layer blob. + *
  • The {@code selector} is written to the selector file under the selectors directory. *
* - * @param uncompressedCacheWrite the {@link UncompressedCacheWrite} to write out + * @param uncompressedLayerBlob the {@link Blob} containing the uncompressed layer contents to + * write out + * @param selector the optional selector digest to also reference this layer data. A selector + * digest may be a secondary identifier for a layer that is distinct from the default layer + * digest. * @return the {@link CachedLayer} representing the written entry * @throws IOException if an I/O exception occurs */ - CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { + CachedLayer writeUncompressed(Blob uncompressedLayerBlob, @Nullable DescriptorDigest selector) + throws IOException { // Creates the layers directory if it doesn't exist. - Files.createDirectories(defaultCacheStorageFiles.getLayersDirectory()); + Files.createDirectories(cacheStorageFiles.getLayersDirectory()); // Creates the temporary directory. The temporary directory must be in the same FileStore as the // final location for Files.move to work. - Files.createDirectories(defaultCacheStorageFiles.getTemporaryDirectory()); + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(defaultCacheStorageFiles.getTemporaryDirectory())) { + new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); // Writes the layer file to the temporary directory. WrittenLayer writtenLayer = - writeUncompressedLayerBlobToDirectory( - uncompressedCacheWrite.getUncompressedLayerBlob(), temporaryLayerDirectory); + writeUncompressedLayerBlobToDirectory(uncompressedLayerBlob, temporaryLayerDirectory); // Moves the temporary directory to the final location. moveIfDoesNotExist( - temporaryLayerDirectory, - defaultCacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); + temporaryLayerDirectory, cacheStorageFiles.getLayerDirectory(writtenLayer.layerDigest)); // Updates cachedLayer with the blob information. Path layerFile = - defaultCacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); - DefaultCachedLayer.Builder cachedLayerBuilder = - DefaultCachedLayer.builder() + cacheStorageFiles.getLayerFile(writtenLayer.layerDigest, writtenLayer.layerDiffId); + CachedLayer.Builder cachedLayerBuilder = + CachedLayer.builder() .setLayerDigest(writtenLayer.layerDigest) .setLayerDiffId(writtenLayer.layerDiffId) .setLayerSize(writtenLayer.layerSize) .setLayerBlob(Blobs.from(layerFile)); // Write the selector file. - if (uncompressedCacheWrite.getSelector().isPresent()) { - writeSelector(uncompressedCacheWrite.getSelector().get(), writtenLayer.layerDigest); + if (selector != null) { + writeSelector(selector, writtenLayer.layerDigest); } return cachedLayerBuilder.build(); @@ -255,7 +255,7 @@ void writeMetadata( Preconditions.checkNotNull(manifestTemplate.getContainerConfiguration()); Preconditions.checkNotNull(manifestTemplate.getContainerConfiguration().getDigest()); - Path imageDirectory = defaultCacheStorageFiles.getImageDirectory(imageReference); + Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); Files.createDirectories(imageDirectory); try (LockFile ignored1 = LockFile.lock(imageDirectory.resolve("lock"))) { @@ -272,7 +272,7 @@ void writeMetadata( */ void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) throws IOException { - Path imageDirectory = defaultCacheStorageFiles.getImageDirectory(imageReference); + Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); Files.createDirectories(imageDirectory); try (LockFile ignored1 = LockFile.lock(imageDirectory.resolve("lock"))) { @@ -291,7 +291,7 @@ void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTe private WrittenLayer writeCompressedLayerBlobToDirectory( Blob compressedLayerBlob, Path layerDirectory) throws IOException { // Writes the layer file to the temporary directory. - Path temporaryLayerFile = defaultCacheStorageFiles.getTemporaryLayerFile(layerDirectory); + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(layerDirectory); BlobDescriptor layerBlobDescriptor; try (OutputStream fileOutputStream = @@ -303,7 +303,7 @@ private WrittenLayer writeCompressedLayerBlobToDirectory( DescriptorDigest layerDiffId = getDiffIdByDecompressingFile(temporaryLayerFile); // Renames the temporary layer file to the correct filename. - Path layerFile = layerDirectory.resolve(defaultCacheStorageFiles.getLayerFilename(layerDiffId)); + Path layerFile = layerDirectory.resolve(cacheStorageFiles.getLayerFilename(layerDiffId)); moveIfDoesNotExist(temporaryLayerFile, layerFile); return new WrittenLayer( @@ -320,7 +320,7 @@ private WrittenLayer writeCompressedLayerBlobToDirectory( */ private WrittenLayer writeUncompressedLayerBlobToDirectory( Blob uncompressedLayerBlob, Path layerDirectory) throws IOException { - Path temporaryLayerFile = defaultCacheStorageFiles.getTemporaryLayerFile(layerDirectory); + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(layerDirectory); try (CountingDigestOutputStream compressedDigestOutputStream = new CountingDigestOutputStream( @@ -338,8 +338,7 @@ private WrittenLayer writeUncompressedLayerBlobToDirectory( long layerSize = compressedBlobDescriptor.getSize(); // Renames the temporary layer file to the correct filename. - Path layerFile = - layerDirectory.resolve(defaultCacheStorageFiles.getLayerFilename(layerDiffId)); + Path layerFile = layerDirectory.resolve(cacheStorageFiles.getLayerFilename(layerDiffId)); moveIfDoesNotExist(temporaryLayerFile, layerFile); return new WrittenLayer(layerDigest, layerDiffId, layerSize); @@ -356,7 +355,7 @@ private WrittenLayer writeUncompressedLayerBlobToDirectory( */ private void writeSelector(DescriptorDigest selector, DescriptorDigest layerDigest) throws IOException { - Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); // Creates the selectors directory if it doesn't exist. Files.createDirectories(selectorFile.getParent()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index 6b5c515624..b00d9706a6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -16,26 +16,99 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Layer; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; -/** - * Represents a cache entry for a layer stored in the cache. Implementations must be - * immutable. - */ -public interface CachedLayer extends Layer { +/** Default implementation of {@link CachedLayer}. */ +public class CachedLayer implements Layer { - /** - * Gets the digest of the layer. - * - * @return the layer digest - */ - DescriptorDigest getDigest(); + /** Builds a {@link CachedLayer}. */ + static class Builder { + + @Nullable private DescriptorDigest layerDigest; + @Nullable private DescriptorDigest layerDiffId; + private long layerSize = -1; + @Nullable private Blob layerBlob; + + private Builder() {} + + Builder setLayerDigest(DescriptorDigest layerDigest) { + this.layerDigest = layerDigest; + return this; + } + + Builder setLayerDiffId(DescriptorDigest layerDiffId) { + this.layerDiffId = layerDiffId; + return this; + } + + Builder setLayerSize(long layerSize) { + this.layerSize = layerSize; + return this; + } + + Builder setLayerBlob(Blob layerBlob) { + this.layerBlob = layerBlob; + return this; + } + + boolean hasLayerBlob() { + return layerBlob != null; + } + + CachedLayer build() { + return new CachedLayer( + Preconditions.checkNotNull(layerDigest, "layerDigest required"), + Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), + layerSize, + Preconditions.checkNotNull(layerBlob, "layerBlob required")); + } + } /** - * Gets the size of the layer, in bytes. + * Creates a new {@link Builder} for a {@link CachedLayer}. * - * @return the layer size + * @return the new {@link Builder} */ - long getSize(); + static Builder builder() { + return new Builder(); + } + + private final DescriptorDigest layerDiffId; + private final BlobDescriptor blobDescriptor; + private final Blob layerBlob; + + private CachedLayer( + DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, Blob layerBlob) { + this.layerDiffId = layerDiffId; + this.layerBlob = layerBlob; + this.blobDescriptor = new BlobDescriptor(layerSize, layerDigest); + } + + public DescriptorDigest getDigest() { + return blobDescriptor.getDigest(); + } + + public long getSize() { + return blobDescriptor.getSize(); + } + + @Override + public DescriptorDigest getDiffId() { + return layerDiffId; + } + + @Override + public Blob getBlob() { + return layerBlob; + } + + @Override + public BlobDescriptor getBlobDescriptor() { + return blobDescriptor; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java deleted file mode 100644 index 08974abd98..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCacheStorage.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2018 Google LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.Set; - -/** - * Default implementation of {@link CacheStorage}. This storage engine stores cache entries in a - * directory in the format: - * - *
{@code
- * layers/
- *   /
- *     
- *   ...
- * selectors/
- *   
- *   ...
- * }
- * - * Layers entries are stored in their own directories under the {@code layers/} directory. Each - * layer directory is named by the layer digest. Inside each layer directory, the layer contents - * file is named by the layer diff ID. - * - *

Selectors are stored in the {@code selectors/} directory. Each selector file is named by the - * selector digest. The contents of a selector file is the digest of the layer it selects. - */ -class DefaultCacheStorage implements CacheStorage { - - /** - * Instantiates a {@link CacheStorage} backed by this storage engine. - * - * @param cacheDirectory the directory for this cache - * @return a new {@link CacheStorage} - */ - static CacheStorage withDirectory(Path cacheDirectory) { - return new DefaultCacheStorage(new DefaultCacheStorageFiles(cacheDirectory)); - } - - private final DefaultCacheStorageWriter defaultCacheStorageWriter; - private final DefaultCacheStorageReader defaultCacheStorageReader; - - private DefaultCacheStorage(DefaultCacheStorageFiles defaultCacheStorageFiles) { - this.defaultCacheStorageWriter = new DefaultCacheStorageWriter(defaultCacheStorageFiles); - this.defaultCacheStorageReader = new DefaultCacheStorageReader(defaultCacheStorageFiles); - } - - @Override - public CachedLayer write(UncompressedCacheWrite uncompressedCacheWrite) throws IOException { - return defaultCacheStorageWriter.write(uncompressedCacheWrite); - } - - @Override - public CachedLayer write(Blob compressedLayerBlob) throws IOException { - return defaultCacheStorageWriter.write(compressedLayerBlob); - } - - @Override - public Set fetchDigests() throws IOException, CacheCorruptedException { - return defaultCacheStorageReader.fetchDigests(); - } - - @Override - public Optional retrieve(DescriptorDigest layerDigest) - throws IOException, CacheCorruptedException { - return defaultCacheStorageReader.retrieve(layerDigest); - } - - @Override - public Optional select(DescriptorDigest selector) - throws IOException, CacheCorruptedException { - return defaultCacheStorageReader.select(selector); - } - - @Override - public void writeMetadata( - ImageReference imageReference, - BuildableManifestTemplate manifestTemplate, - ContainerConfigurationTemplate containerConfigurationTemplate) - throws IOException { - defaultCacheStorageWriter.writeMetadata( - imageReference, manifestTemplate, containerConfigurationTemplate); - } - - @Override - public void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTemplate) - throws IOException { - defaultCacheStorageWriter.writeMetadata(imageReference, manifestTemplate); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java deleted file mode 100644 index 47bd107f0a..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/DefaultCachedLayer.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.base.Preconditions; -import javax.annotation.Nullable; - -/** Default implementation of {@link CachedLayer}. */ -class DefaultCachedLayer implements CachedLayer { - - /** Builds a {@link CachedLayer}. */ - static class Builder { - - @Nullable private DescriptorDigest layerDigest; - @Nullable private DescriptorDigest layerDiffId; - private long layerSize = -1; - @Nullable private Blob layerBlob; - - private Builder() {} - - Builder setLayerDigest(DescriptorDigest layerDigest) { - this.layerDigest = layerDigest; - return this; - } - - Builder setLayerDiffId(DescriptorDigest layerDiffId) { - this.layerDiffId = layerDiffId; - return this; - } - - Builder setLayerSize(long layerSize) { - this.layerSize = layerSize; - return this; - } - - Builder setLayerBlob(Blob layerBlob) { - this.layerBlob = layerBlob; - return this; - } - - boolean hasLayerBlob() { - return layerBlob != null; - } - - CachedLayer build() { - return new DefaultCachedLayer( - Preconditions.checkNotNull(layerDigest, "layerDigest required"), - Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), - layerSize, - Preconditions.checkNotNull(layerBlob, "layerBlob required")); - } - } - - /** - * Creates a new {@link Builder} for a {@link CachedLayer}. - * - * @return the new {@link Builder} - */ - static Builder builder() { - return new Builder(); - } - - private final DescriptorDigest layerDiffId; - private final BlobDescriptor blobDescriptor; - private final Blob layerBlob; - - private DefaultCachedLayer( - DescriptorDigest layerDigest, DescriptorDigest layerDiffId, long layerSize, Blob layerBlob) { - this.layerDiffId = layerDiffId; - this.layerBlob = layerBlob; - this.blobDescriptor = new BlobDescriptor(layerSize, layerDigest); - } - - @Override - public DescriptorDigest getDigest() { - return blobDescriptor.getDigest(); - } - - @Override - public DescriptorDigest getDiffId() { - return layerDiffId; - } - - @Override - public long getSize() { - return blobDescriptor.getSize(); - } - - @Override - public Blob getBlob() { - return layerBlob; - } - - @Override - public BlobDescriptor getBlobDescriptor() { - return blobDescriptor; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java deleted file mode 100644 index 1252e99e15..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/UncompressedCacheWrite.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.cache; - -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import java.util.Optional; -import javax.annotation.concurrent.Immutable; - -/** - * Represents uncompressed layer data to write to the cache with an additional selector digest. - * - *

Implementation is immutable and thread-safe. - */ -@Immutable -class UncompressedCacheWrite { - - private final Blob uncompressedLayerBlob; - private final DescriptorDigest selector; - - UncompressedCacheWrite(Blob uncompressedLayerBlob, DescriptorDigest selector) { - this.uncompressedLayerBlob = uncompressedLayerBlob; - this.selector = selector; - } - - /** - * Gets the {@link Blob} containing the uncompressed layer contents. - * - * @return the layer {@link Blob} - */ - Blob getUncompressedLayerBlob() { - return uncompressedLayerBlob; - } - - /** - * Gets the optional selector digest to also reference this layer data. A selector digest may be a - * secondary identifier for a layer that is distinct from the default layer digest. - * - *

For example, it is useful as an inexpensive alternative reference to a layer compared to - * calculating the primary layer digest (SHA256 of compressed tarball). - * - * @return the selector digest - */ - Optional getSelector() { - return Optional.ofNullable(selector); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index b1a0012005..11f33f81b2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,9 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -65,7 +63,7 @@ public class BuildImageStepTest { @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepResources; @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepClasses; @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepExtraFiles; - + @Mock private CachedLayer mockCachedLayer; private DescriptorDigest testDescriptorDigest; private HistoryEntry nonEmptyLayerHistory; private HistoryEntry emptyLayerHistory; @@ -75,33 +73,6 @@ public void setUp() throws DigestException { testDescriptorDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - CachedLayer testCachedLayer = - new CachedLayer() { - @Override - public DescriptorDigest getDigest() { - return testDescriptorDigest; - } - - @Override - public DescriptorDigest getDiffId() { - return testDescriptorDigest; - } - - @Override - public long getSize() { - return 0; - } - - @Override - public Blob getBlob() { - return Blobs.from("ignored"); - } - - @Override - public BlobDescriptor getBlobDescriptor() { - return new BlobDescriptor(0, testDescriptorDigest); - } - }; Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) @@ -113,6 +84,8 @@ public BlobDescriptor getBlobDescriptor() { Mockito.when(mockContainerConfiguration.getExposedPorts()).thenReturn(ImmutableSet.of()); Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(ImmutableList.of()); Mockito.when(mockContainerConfiguration.getUser()).thenReturn("root"); + Mockito.when(mockCachedLayer.getBlobDescriptor()) + .thenReturn(new BlobDescriptor(0, testDescriptorDigest)); nonEmptyLayerHistory = HistoryEntry.builder() @@ -154,7 +127,7 @@ public BlobDescriptor getBlobDescriptor() { .addHistory(emptyLayerHistory) .build(); Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer)); + .thenReturn(Futures.immediateFuture(mockCachedLayer)); Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) .thenReturn( Futures.immediateFuture( @@ -175,7 +148,7 @@ public BlobDescriptor getBlobDescriptor() { .forEach( layerStep -> Mockito.when(layerStep.getFuture()) - .thenReturn(Futures.immediateFuture(testCachedLayer))); + .thenReturn(Futures.immediateFuture(mockCachedLayer))); Mockito.when(mockBuildAndCacheApplicationLayerStepClasses.getLayerType()).thenReturn("classes"); Mockito.when(mockBuildAndCacheApplicationLayerStepDependencies.getLayerType()) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java similarity index 79% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java index 86da067be8..2a962466dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -26,21 +26,21 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link DefaultCacheStorageFiles}. */ -public class DefaultCacheStorageFilesTest { +/** Tests for {@link CacheStorageFiles}. */ +public class CacheStorageFilesTest { - private static final DefaultCacheStorageFiles testDefaultCacheStorageFiles = - new DefaultCacheStorageFiles(Paths.get("cache/directory")); + private static final CacheStorageFiles TEST_CACHE_STORAGE_FILES = + new CacheStorageFiles(Paths.get("cache/directory")); @Test public void testIsLayerFile() { Assert.assertTrue( - DefaultCacheStorageFiles.isLayerFile( + CacheStorageFiles.isLayerFile( Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); Assert.assertTrue( - DefaultCacheStorageFiles.isLayerFile( + CacheStorageFiles.isLayerFile( Paths.get("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); - Assert.assertFalse(DefaultCacheStorageFiles.isLayerFile(Paths.get("is.not.layer.file"))); + Assert.assertFalse(CacheStorageFiles.isLayerFile(Paths.get("is.not.layer.file"))); } @Test @@ -48,7 +48,7 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - DefaultCacheStorageFiles.getDiffId( + CacheStorageFiles.getDiffId( Paths.get( "layer", "file", @@ -56,14 +56,14 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - DefaultCacheStorageFiles.getDiffId( + CacheStorageFiles.getDiffId( Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); } @Test public void testGetDiffId_corrupted() { try { - DefaultCacheStorageFiles.getDiffId(Paths.get("not long enough")); + CacheStorageFiles.getDiffId(Paths.get("not long enough")); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { @@ -73,7 +73,7 @@ public void testGetDiffId_corrupted() { } try { - DefaultCacheStorageFiles.getDiffId( + CacheStorageFiles.getDiffId( Paths.get( "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); Assert.fail("Should have thrown CacheCorruptedException"); @@ -103,7 +103,7 @@ public void testGetLayerFile() throws DigestException { "layers", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - testDefaultCacheStorageFiles.getLayerFile(layerDigest, diffId)); + TEST_CACHE_STORAGE_FILES.getLayerFile(layerDigest, diffId)); } @Test @@ -114,7 +114,7 @@ public void testGetLayerFilename() throws DigestException { Assert.assertEquals( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - testDefaultCacheStorageFiles.getLayerFilename(diffId)); + TEST_CACHE_STORAGE_FILES.getLayerFilename(diffId)); } @Test @@ -129,14 +129,13 @@ public void testGetSelectorFile() throws DigestException { "directory", "selectors", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), - testDefaultCacheStorageFiles.getSelectorFile(selector)); + TEST_CACHE_STORAGE_FILES.getSelectorFile(selector)); } @Test public void testGetLayersDirectory() { Assert.assertEquals( - Paths.get("cache", "directory", "layers"), - testDefaultCacheStorageFiles.getLayersDirectory()); + Paths.get("cache", "directory", "layers"), TEST_CACHE_STORAGE_FILES.getLayersDirectory()); } @Test @@ -151,39 +150,39 @@ public void testGetLayerDirectory() throws DigestException { "directory", "layers", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - testDefaultCacheStorageFiles.getLayerDirectory(layerDigest)); + TEST_CACHE_STORAGE_FILES.getLayerDirectory(layerDigest)); } @Test public void testGetTemporaryDirectory() { Assert.assertEquals( - Paths.get("cache/directory/tmp"), testDefaultCacheStorageFiles.getTemporaryDirectory()); + Paths.get("cache/directory/tmp"), TEST_CACHE_STORAGE_FILES.getTemporaryDirectory()); } @Test public void testGetImagesDirectory() { Assert.assertEquals( - Paths.get("cache/directory/images"), testDefaultCacheStorageFiles.getImagesDirectory()); + Paths.get("cache/directory/images"), TEST_CACHE_STORAGE_FILES.getImagesDirectory()); } @Test public void testGetImageDirectory() throws InvalidImageReferenceException { Path imagesDirectory = Paths.get("cache", "directory", "images"); - Assert.assertEquals(imagesDirectory, testDefaultCacheStorageFiles.getImagesDirectory()); + Assert.assertEquals(imagesDirectory, TEST_CACHE_STORAGE_FILES.getImagesDirectory()); Assert.assertEquals( imagesDirectory.resolve("reg.istry/repo/sitory!tag"), - testDefaultCacheStorageFiles.getImageDirectory( + TEST_CACHE_STORAGE_FILES.getImageDirectory( ImageReference.parse("reg.istry/repo/sitory:tag"))); Assert.assertEquals( imagesDirectory.resolve( "reg.istry/repo!sha256!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - testDefaultCacheStorageFiles.getImageDirectory( + TEST_CACHE_STORAGE_FILES.getImageDirectory( ImageReference.parse( "reg.istry/repo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); Assert.assertEquals( imagesDirectory.resolve("reg.istry!5000/repo/sitory!tag"), - testDefaultCacheStorageFiles.getImageDirectory( + TEST_CACHE_STORAGE_FILES.getImageDirectory( ImageReference.parse("reg.istry:5000/repo/sitory:tag"))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java similarity index 65% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 6d852d50ae..f3205759ce 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -33,8 +33,8 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -/** Tests for {@link DefaultCacheStorageReader}. */ -public class DefaultCacheStorageReaderTest { +/** Tests for {@link CacheStorageReader}. */ +public class CacheStorageReaderTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -53,27 +53,24 @@ public void setUp() throws DigestException { @Test public void testListDigests() throws IOException, CacheCorruptedException { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + CacheStorageFiles cacheStorageFiles = + new CacheStorageFiles(temporaryFolder.newFolder().toPath()); - DefaultCacheStorageReader defaultCacheStorageReader = - new DefaultCacheStorageReader(defaultCacheStorageFiles); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); // Creates test layer directories. - Files.createDirectories( - defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest1.getHash())); - Files.createDirectories( - defaultCacheStorageFiles.getLayersDirectory().resolve(layerDigest2.getHash())); + Files.createDirectories(cacheStorageFiles.getLayersDirectory().resolve(layerDigest1.getHash())); + Files.createDirectories(cacheStorageFiles.getLayersDirectory().resolve(layerDigest2.getHash())); // Checks that layer directories created are all listed. Assert.assertEquals( new HashSet<>(Arrays.asList(layerDigest1, layerDigest2)), - defaultCacheStorageReader.fetchDigests()); + cacheStorageReader.fetchDigests()); // Checks that non-digest directories means the cache is corrupted. - Files.createDirectory(defaultCacheStorageFiles.getLayersDirectory().resolve("not a hash")); + Files.createDirectory(cacheStorageFiles.getLayersDirectory().resolve("not a hash")); try { - defaultCacheStorageReader.fetchDigests(); + cacheStorageReader.fetchDigests(); Assert.fail("Listing digests should have failed"); } catch (CacheCorruptedException ex) { @@ -84,22 +81,20 @@ public void testListDigests() throws IOException, CacheCorruptedException { @Test public void testRetrieve() throws IOException, CacheCorruptedException { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + CacheStorageFiles cacheStorageFiles = + new CacheStorageFiles(temporaryFolder.newFolder().toPath()); - DefaultCacheStorageReader defaultCacheStorageReader = - new DefaultCacheStorageReader(defaultCacheStorageFiles); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); // Creates the test layer directory. DescriptorDigest layerDigest = layerDigest1; DescriptorDigest layerDiffId = layerDigest2; Blob layerBlob = Blobs.from("layerBlob"); - Files.createDirectories(defaultCacheStorageFiles.getLayerDirectory(layerDigest)); - Blobs.writeToFileWithLock( - layerBlob, defaultCacheStorageFiles.getLayerFile(layerDigest, layerDiffId)); + Files.createDirectories(cacheStorageFiles.getLayerDirectory(layerDigest)); + Blobs.writeToFileWithLock(layerBlob, cacheStorageFiles.getLayerFile(layerDigest, layerDiffId)); // Checks that the CachedLayer is retrieved correctly. - Optional optionalCachedLayer = defaultCacheStorageReader.retrieve(layerDigest); + Optional optionalCachedLayer = cacheStorageReader.retrieve(layerDigest); Assert.assertTrue(optionalCachedLayer.isPresent()); Assert.assertEquals(layerDigest, optionalCachedLayer.get().getDigest()); Assert.assertEquals(layerDiffId, optionalCachedLayer.get().getDiffId()); @@ -107,9 +102,9 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCachedLayer.get().getBlob())); // Checks that multiple .layer files means the cache is corrupted. - Files.createFile(defaultCacheStorageFiles.getLayerFile(layerDigest, layerDigest)); + Files.createFile(cacheStorageFiles.getLayerFile(layerDigest, layerDigest)); try { - defaultCacheStorageReader.retrieve(layerDigest); + cacheStorageReader.retrieve(layerDigest); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { @@ -117,26 +112,25 @@ public void testRetrieve() throws IOException, CacheCorruptedException { "Multiple layer files found for layer with digest " + layerDigest.getHash() + " in directory: " - + defaultCacheStorageFiles.getLayerDirectory(layerDigest), + + cacheStorageFiles.getLayerDirectory(layerDigest), ex.getMessage()); } } @Test public void testSelect_invalidLayerDigest() throws IOException { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + CacheStorageFiles cacheStorageFiles = + new CacheStorageFiles(temporaryFolder.newFolder().toPath()); - DefaultCacheStorageReader defaultCacheStorageReader = - new DefaultCacheStorageReader(defaultCacheStorageFiles); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); DescriptorDigest selector = layerDigest1; - Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); Files.createDirectories(selectorFile.getParent()); Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from("not a valid layer digest"))); try { - defaultCacheStorageReader.select(selector); + cacheStorageReader.select(selector); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { @@ -150,18 +144,17 @@ public void testSelect_invalidLayerDigest() throws IOException { @Test public void testSelect() throws IOException, CacheCorruptedException { - DefaultCacheStorageFiles defaultCacheStorageFiles = - new DefaultCacheStorageFiles(temporaryFolder.newFolder().toPath()); + CacheStorageFiles cacheStorageFiles = + new CacheStorageFiles(temporaryFolder.newFolder().toPath()); - DefaultCacheStorageReader defaultCacheStorageReader = - new DefaultCacheStorageReader(defaultCacheStorageFiles); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); DescriptorDigest selector = layerDigest1; - Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); Files.createDirectories(selectorFile.getParent()); Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from(layerDigest2.getHash()))); - Optional selectedLayerDigest = defaultCacheStorageReader.select(selector); + Optional selectedLayerDigest = cacheStorageReader.select(selector); Assert.assertTrue(selectedLayerDigest.isPresent()); Assert.assertEquals(layerDigest2, selectedLayerDigest.get()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java similarity index 89% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index 53e9c78612..ea7de8d59c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -43,8 +43,8 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -/** Tests for {@link DefaultCacheStorageWriter}. */ -public class DefaultCacheStorageWriterTest { +/** Tests for {@link CacheStorageWriter}. */ +public class CacheStorageWriterTest { private static DescriptorDigest getDigest(Blob blob) throws IOException { return blob.writeTo(new CountingDigestOutputStream(ByteStreams.nullOutputStream())).getDigest(); @@ -72,13 +72,13 @@ private static Blob decompress(Blob blob) throws IOException { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private DefaultCacheStorageFiles defaultCacheStorageFiles; + private CacheStorageFiles cacheStorageFiles; private Path cacheRoot; @Before public void setUp() throws IOException { cacheRoot = temporaryFolder.newFolder().toPath(); - defaultCacheStorageFiles = new DefaultCacheStorageFiles(cacheRoot); + cacheStorageFiles = new CacheStorageFiles(cacheRoot); } @Test @@ -86,8 +86,7 @@ public void testWrite_compressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); CachedLayer cachedLayer = - new DefaultCacheStorageWriter(defaultCacheStorageFiles) - .write(compress(uncompressedLayerBlob)); + new CacheStorageWriter(cacheStorageFiles).writeCompressed(compress(uncompressedLayerBlob)); verifyCachedLayer(cachedLayer, uncompressedLayerBlob); } @@ -99,13 +98,13 @@ public void testWrite_uncompressed() throws IOException { DescriptorDigest selector = getDigest(Blobs.from("selector")); CachedLayer cachedLayer = - new DefaultCacheStorageWriter(defaultCacheStorageFiles) - .write(new UncompressedCacheWrite(uncompressedLayerBlob, selector)); + new CacheStorageWriter(cacheStorageFiles) + .writeUncompressed(uncompressedLayerBlob, selector); verifyCachedLayer(cachedLayer, uncompressedLayerBlob); // Verifies that the files are present. - Path selectorFile = defaultCacheStorageFiles.getSelectorFile(selector); + Path selectorFile = cacheStorageFiles.getSelectorFile(selector); Assert.assertTrue(Files.exists(selectorFile)); Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); } @@ -119,8 +118,7 @@ public void testWriteMetadata_v21() JsonTemplateMapper.readJsonFromFile(manifestJsonFile, V21ManifestTemplate.class); ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); - new DefaultCacheStorageWriter(defaultCacheStorageFiles) - .writeMetadata(imageReference, manifestTemplate); + new CacheStorageWriter(cacheStorageFiles).writeMetadata(imageReference, manifestTemplate); Path savedManifestPath = cacheRoot.resolve("images/image.reference/project/thing!tag/manifest.json"); @@ -146,7 +144,7 @@ public void testWriteMetadata_v22() JsonTemplateMapper.readJsonFromFile(manifestJsonFile, V22ManifestTemplate.class); ImageReference imageReference = ImageReference.parse("image.reference/project/thing:tag"); - new DefaultCacheStorageWriter(defaultCacheStorageFiles) + new CacheStorageWriter(cacheStorageFiles) .writeMetadata(imageReference, manifestTemplate, containerConfigurationTemplate); Path savedManifestPath = @@ -183,7 +181,6 @@ private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBl // Verifies that the files are present. Assert.assertTrue( Files.exists( - defaultCacheStorageFiles.getLayerFile( - cachedLayer.getDigest(), cachedLayer.getDiffId()))); + cacheStorageFiles.getLayerFile(cachedLayer.getDigest(), cachedLayer.getDiffId()))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java similarity index 84% rename from jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java index 1565b6cd1f..b6783668a7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/DefaultCachedLayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java @@ -26,9 +26,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link DefaultCachedLayer}. */ +/** Tests for {@link CachedLayer}. */ @RunWith(MockitoJUnitRunner.class) -public class DefaultCachedLayerTest { +public class CachedLayerTest { @Mock private DescriptorDigest mockLayerDigest; @Mock private DescriptorDigest mockLayerDiffId; @@ -36,7 +36,7 @@ public class DefaultCachedLayerTest { @Test public void testBuilder_fail() { try { - DefaultCachedLayer.builder().build(); + CachedLayer.builder().build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -44,7 +44,7 @@ public void testBuilder_fail() { } try { - DefaultCachedLayer.builder().setLayerDigest(mockLayerDigest).build(); + CachedLayer.builder().setLayerDigest(mockLayerDigest).build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -52,10 +52,7 @@ public void testBuilder_fail() { } try { - DefaultCachedLayer.builder() - .setLayerDigest(mockLayerDigest) - .setLayerDiffId(mockLayerDiffId) - .build(); + CachedLayer.builder().setLayerDigest(mockLayerDigest).setLayerDiffId(mockLayerDiffId).build(); Assert.fail("missing required"); } catch (NullPointerException ex) { @@ -65,8 +62,8 @@ public void testBuilder_fail() { @Test public void testBuilder_pass() throws IOException { - DefaultCachedLayer.Builder cachedLayerBuilder = - DefaultCachedLayer.builder() + CachedLayer.Builder cachedLayerBuilder = + CachedLayer.builder() .setLayerDigest(mockLayerDigest) .setLayerDiffId(mockLayerDiffId) .setLayerSize(1337); From 9d5ee513bdeea6009f62d665f98d24bbae235290 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 2 May 2019 10:32:56 -0400 Subject: [PATCH 0565/2020] Add cache methods for retrieving manifest and container config (#1676) --- .../google/cloud/tools/jib/cache/Cache.java | 14 +++ .../jib/cache/CacheCorruptedException.java | 24 +++- .../tools/jib/cache/CacheStorageFiles.java | 21 +++- .../tools/jib/cache/CacheStorageReader.java | 91 ++++++++++++++- .../jib/image/json/ManifestAndConfig.java | 51 ++++++++ .../jib/cache/CacheStorageFilesTest.java | 22 ++-- .../jib/cache/CacheStorageReaderTest.java | 110 ++++++++++++++++-- 7 files changed, 299 insertions(+), 34 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfig.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 3fdb44a329..de537a0955 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfig; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -115,6 +116,19 @@ public CachedLayer writeUncompressedLayer( uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries)); } + /** + * Retrieves the cached manifest and container configuration for an image reference. + * + * @param imageReference the image reference + * @return the manifest and container configuration for the image reference, if found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + public Optional retrieveMetadata(ImageReference imageReference) + throws IOException, CacheCorruptedException { + return cacheStorageReader.retrieveMetadata(imageReference); + } + /** * Retrieves the {@link CachedLayer} that was built from the {@code layerEntries}. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java index e3b250e0d5..b178523454 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java @@ -16,14 +16,30 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.ProjectInfo; +import java.nio.file.Path; + /** Thrown if the the cache was found to be corrupted. */ public class CacheCorruptedException extends Exception { - CacheCorruptedException(String message, Throwable cause) { - super(message, cause); + CacheCorruptedException(Path cacheDirectory, String message, Throwable cause) { + super( + message + + ". You may need to clear the cache by deleting the '" + + cacheDirectory + + "' directory (if this is a bug, please file an issue at " + + ProjectInfo.GITHUB_NEW_ISSUE_URL + + ")", + cause); } - CacheCorruptedException(String message) { - super(message); + CacheCorruptedException(Path cacheDirectory, String message) { + super( + message + + ". You may need to clear the cache by deleting the '" + + cacheDirectory + + "' directory (if this is a bug, please file an issue at " + + ProjectInfo.GITHUB_NEW_ISSUE_URL + + ")"); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java index 80c691d05f..cbc1767e54 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -41,6 +41,12 @@ static boolean isLayerFile(Path file) { return file.getFileName().toString().length() == DescriptorDigest.HASH_LENGTH; } + private final Path cacheDirectory; + + CacheStorageFiles(Path cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + /** * Gets the diff ID portion of the layer filename. * @@ -48,21 +54,24 @@ static boolean isLayerFile(Path file) { * @return the diff ID portion of the layer file filename * @throws CacheCorruptedException if no valid diff ID could be parsed */ - static DescriptorDigest getDiffId(Path layerFile) throws CacheCorruptedException { + DescriptorDigest getDiffId(Path layerFile) throws CacheCorruptedException { try { String diffId = layerFile.getFileName().toString(); return DescriptorDigest.fromHash(diffId); } catch (DigestException | IndexOutOfBoundsException ex) { throw new CacheCorruptedException( - "Layer file did not include valid diff ID: " + layerFile, ex); + cacheDirectory, "Layer file did not include valid diff ID: " + layerFile, ex); } } - private final Path cacheDirectory; - - CacheStorageFiles(Path cacheDirectory) { - this.cacheDirectory = cacheDirectory; + /** + * Gets the cache directory. + * + * @return the cache directory + */ + Path getCacheDirectory() { + return cacheDirectory; } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index 157bc0b973..b3bee7acd5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -16,8 +16,19 @@ package com.google.cloud.tools.jib.cache; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.filesystem.LockFile; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfig; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; +import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -55,13 +66,87 @@ Set fetchDigests() throws IOException, CacheCorruptedException layerDigests.add(DescriptorDigest.fromHash(layerDirectory.getFileName().toString())); } catch (DigestException ex) { - throw new CacheCorruptedException("Found non-digest file in layers directory", ex); + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "Found non-digest file in layers directory", + ex); } } return layerDigests; } } + /** + * Retrieves the cached manifest and container configuration for an image reference. + * + * @param imageReference the image reference + * @return the manifest and container configuration for the image reference, if found + * @throws IOException if an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + */ + Optional retrieveMetadata(ImageReference imageReference) + throws IOException, CacheCorruptedException { + Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); + Path manifestPath = imageDirectory.resolve("manifest.json"); + if (!Files.exists(manifestPath)) { + return Optional.empty(); + } + + try (LockFile ignored = LockFile.lock(imageDirectory.resolve("lock"))) { + // TODO: Consolidate with ManifestPuller + ObjectNode node = + new ObjectMapper().readValue(Files.newInputStream(manifestPath), ObjectNode.class); + if (!node.has("schemaVersion")) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), "Cannot find field 'schemaVersion' in manifest"); + } + + int schemaVersion = node.get("schemaVersion").asInt(-1); + if (schemaVersion == -1) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "`schemaVersion` field is not an integer in manifest"); + } + + if (schemaVersion == 1) { + return Optional.of( + new ManifestAndConfig( + JsonTemplateMapper.readJsonFromFile(manifestPath, V21ManifestTemplate.class), + null)); + } + if (schemaVersion == 2) { + // 'schemaVersion' of 2 can be either Docker V2.2 or OCI. + String mediaType = node.get("mediaType").asText(); + + ManifestTemplate manifestTemplate; + if (V22ManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + manifestTemplate = + JsonTemplateMapper.readJsonFromFile(manifestPath, V22ManifestTemplate.class); + } else if (OCIManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + manifestTemplate = + JsonTemplateMapper.readJsonFromFile(manifestPath, OCIManifestTemplate.class); + } else { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), "Unknown manifest mediaType: " + mediaType); + } + + Path configPath = imageDirectory.resolve("config.json"); + if (!Files.exists(configPath)) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "Manifest found, but missing container configuration"); + } + ContainerConfigurationTemplate config = + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + + return Optional.of(new ManifestAndConfig(manifestTemplate, config)); + } + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "Unknown schemaVersion in manifest: " + schemaVersion + " - only 1 and 2 are supported"); + } + } + /** * Retrieves the {@link CachedLayer} for the layer with digest {@code layerDigest}. * @@ -84,6 +169,7 @@ Optional retrieve(DescriptorDigest layerDigest) if (CacheStorageFiles.isLayerFile(fileInLayerDirectory)) { if (cachedLayerBuilder.hasLayerBlob()) { throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), "Multiple layer files found for layer with digest " + layerDigest.getHash() + " in directory: " @@ -91,7 +177,7 @@ Optional retrieve(DescriptorDigest layerDigest) } cachedLayerBuilder .setLayerBlob(Blobs.from(fileInLayerDirectory)) - .setLayerDiffId(CacheStorageFiles.getDiffId(fileInLayerDirectory)) + .setLayerDiffId(cacheStorageFiles.getDiffId(fileInLayerDirectory)) .setLayerSize(Files.size(fileInLayerDirectory)); } } @@ -122,6 +208,7 @@ Optional select(DescriptorDigest selector) } catch (DigestException ex) { throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), "Expected valid layer digest as contents of selector file `" + selectorFile + "` for selector `" diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfig.java new file mode 100644 index 0000000000..db30436e6a --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import java.util.Optional; +import javax.annotation.Nullable; + +/** Stores a manifest and container config. */ +public class ManifestAndConfig { + + private final ManifestTemplate manifest; + @Nullable private final ContainerConfigurationTemplate config; + + public ManifestAndConfig( + ManifestTemplate manifest, @Nullable ContainerConfigurationTemplate config) { + this.manifest = manifest; + this.config = config; + } + + /** + * Gets the manifest. + * + * @return the manifest + */ + public ManifestTemplate getManifest() { + return manifest; + } + + /** + * Gets the container configuration. + * + * @return the container configuration + */ + public Optional getConfig() { + return Optional.ofNullable(config); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java index 2a962466dc..64bb9efc92 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -48,7 +48,7 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - CacheStorageFiles.getDiffId( + TEST_CACHE_STORAGE_FILES.getDiffId( Paths.get( "layer", "file", @@ -56,33 +56,35 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - CacheStorageFiles.getDiffId( + TEST_CACHE_STORAGE_FILES.getDiffId( Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); } @Test public void testGetDiffId_corrupted() { try { - CacheStorageFiles.getDiffId(Paths.get("not long enough")); + TEST_CACHE_STORAGE_FILES.getDiffId(Paths.get("not long enough")); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { - Assert.assertEquals( - "Layer file did not include valid diff ID: not long enough", ex.getMessage()); + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith("Layer file did not include valid diff ID: not long enough")); Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } try { - CacheStorageFiles.getDiffId( + TEST_CACHE_STORAGE_FILES.getDiffId( Paths.get( "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { - Assert.assertEquals( - "Layer file did not include valid diff ID: " - + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - ex.getMessage()); + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "Layer file did not include valid diff ID: " + + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index f3205759ce..437f14aa0b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -19,9 +19,16 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.common.io.Resources; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.DigestException; import java.util.Arrays; import java.util.HashSet; @@ -36,6 +43,27 @@ /** Tests for {@link CacheStorageReader}. */ public class CacheStorageReaderTest { + private static void setupCachedMetadataV21(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + Files.copy( + Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()), + imageDirectory.resolve("manifest.json")); + } + + private static void setupCachedMetadataV22(Path cacheDirectory) + throws IOException, URISyntaxException { + Path imageDirectory = cacheDirectory.resolve("images/test/image!tag"); + Files.createDirectories(imageDirectory); + Files.copy( + Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()), + imageDirectory.resolve("manifest.json")); + Files.copy( + Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()), + imageDirectory.resolve("config.json")); + } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private DescriptorDigest layerDigest1; @@ -74,11 +102,67 @@ public void testListDigests() throws IOException, CacheCorruptedException { Assert.fail("Listing digests should have failed"); } catch (CacheCorruptedException ex) { - Assert.assertEquals("Found non-digest file in layers directory", ex.getMessage()); + Assert.assertThat( + ex.getMessage(), CoreMatchers.startsWith("Found non-digest file in layers directory")); Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } } + @Test + public void testRetrieveManifest_v21() + throws IOException, URISyntaxException, CacheCorruptedException { + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + setupCachedMetadataV21(cacheDirectory); + + CacheStorageFiles cacheStorageFiles = new CacheStorageFiles(cacheDirectory); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + + V21ManifestTemplate manifestTemplate = + (V21ManifestTemplate) + cacheStorageReader + .retrieveMetadata(ImageReference.of("test", "image", "tag")) + .get() + .getManifest(); + Assert.assertEquals(1, manifestTemplate.getSchemaVersion()); + } + + @Test + public void testRetrieveManifest_v22() + throws IOException, URISyntaxException, CacheCorruptedException { + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + setupCachedMetadataV22(cacheDirectory); + + CacheStorageFiles cacheStorageFiles = new CacheStorageFiles(cacheDirectory); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + + V22ManifestTemplate manifestTemplate = + (V22ManifestTemplate) + cacheStorageReader + .retrieveMetadata(ImageReference.of("test", "image", "tag")) + .get() + .getManifest(); + Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); + } + + @Test + public void testRetrieveContainerConfiguration() + throws IOException, URISyntaxException, CacheCorruptedException { + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + setupCachedMetadataV22(cacheDirectory); + + CacheStorageFiles cacheStorageFiles = new CacheStorageFiles(cacheDirectory); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + + ContainerConfigurationTemplate configurationTemplate = + cacheStorageReader + .retrieveMetadata(ImageReference.of("test", "image", "tag")) + .get() + .getConfig() + .get(); + Assert.assertEquals("wasm", configurationTemplate.getArchitecture()); + Assert.assertEquals("js", configurationTemplate.getOs()); + } + @Test public void testRetrieve() throws IOException, CacheCorruptedException { CacheStorageFiles cacheStorageFiles = @@ -108,12 +192,13 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { - Assert.assertEquals( - "Multiple layer files found for layer with digest " - + layerDigest.getHash() - + " in directory: " - + cacheStorageFiles.getLayerDirectory(layerDigest), - ex.getMessage()); + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "Multiple layer files found for layer with digest " + + layerDigest.getHash() + + " in directory: " + + cacheStorageFiles.getLayerDirectory(layerDigest))); } } @@ -134,11 +219,12 @@ public void testSelect_invalidLayerDigest() throws IOException { Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { - Assert.assertEquals( - "Expected valid layer digest as contents of selector file `" - + selectorFile - + "` for selector `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, but got: not a valid layer digest", - ex.getMessage()); + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "Expected valid layer digest as contents of selector file `" + + selectorFile + + "` for selector `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, but got: not a valid layer digest")); } } From 5cad2a62b2f7e0c8b86e427a7c397de804ec6f07 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 2 May 2019 11:20:34 -0400 Subject: [PATCH 0566/2020] Add content tests for image (#1680) --- .../tools/jib/api/ReproducibleImageTest.java | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 9c96ad6732..0bc365de85 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -26,9 +26,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; +import com.google.common.io.CharStreams; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -89,6 +92,53 @@ public static void createImage() .containerize(containerizer); } + @Test + public void testTarballStructure() throws IOException { + // known content should produce known results + List expected = + ImmutableList.of( + "c46572ef74f58d95e44dd36c1fbdfebd3752e8b56a794a13c11cfed35a1a6e1c.tar.gz", + "6d2763b0f3940d324ea6b55386429e5b173899608abf7d1bff62e25dd2e4dcea.tar.gz", + "530c1954a2b087d0b989895ea56435c9dc739a973f2d2b6cb9bb98e55bbea7ac.tar.gz", + "config.json", + "manifest.json"); + + List actual = new ArrayList<>(); + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(imageTar.toPath()))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextTarEntry()) != null) { + actual.add(imageEntry.getName()); + } + } + + Assert.assertEquals(expected, actual); + } + + @Test + public void testManifest() throws IOException { + try (InputStream input = Files.newInputStream(imageTar.toPath())) { + String exectedManifest = + "[{\"config\":\"config.json\",\"repoTags\":[\"jib-core/reproducible:latest\"]," + + "\"layers\":[\"c46572ef74f58d95e44dd36c1fbdfebd3752e8b56a794a13c11cfed35a1a6e1c.tar.gz\",\"6d2763b0f3940d324ea6b55386429e5b173899608abf7d1bff62e25dd2e4dcea.tar.gz\",\"530c1954a2b087d0b989895ea56435c9dc739a973f2d2b6cb9bb98e55bbea7ac.tar.gz\"]}]"; + String generatedManifest = extractFromTarFileAsString(imageTar, "manifest.json"); + Assert.assertEquals(exectedManifest, generatedManifest); + } + } + + @Test + public void testConfiguration() throws IOException { + try (InputStream input = Files.newInputStream(imageTar.toPath())) { + String exectedConfig = + "{\"created\":\"1970-01-01T00:00:00Z\",\"architecture\":\"amd64\",\"os\":\"linux\"," + + "\"config\":{\"Env\":[],\"Entrypoint\":[\"echo\",\"Hello World\"],\"ExposedPorts\":{},\"Labels\":{},\"Volumes\":{}}," + + "\"history\":[{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"}]," + + "\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:18e4f44e6d1835bd968339b166057bd17ab7d4cbb56dc7262a5cafea7cf8d405\",\"sha256:13369c34f073f2b9c1fa6431e23d925f1a8eac65b1726c8cc8fcc2596c69b414\",\"sha256:4f92c507112d7880ca0f504ef8272b7fdee107263270125036a260a741565923\"]}}"; + String generatedConfig = extractFromTarFileAsString(imageTar, "config.json"); + Assert.assertEquals(exectedConfig, generatedConfig); + } + } + @Test public void testImageLayout() throws IOException { Set paths = new HashSet<>(); @@ -179,7 +229,8 @@ public void testFileOrdering() throws IOException { private void layerEntriesDo(BiConsumer layerConsumer) throws IOException { - try (TarArchiveInputStream input = new TarArchiveInputStream(new FileInputStream(imageTar))) { + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(imageTar.toPath()))) { TarArchiveEntry imageEntry; while ((imageEntry = input.getNextTarEntry()) != null) { String imageEntryName = imageEntry.getName(); @@ -195,4 +246,18 @@ private void layerEntriesDo(BiConsumer layerConsumer) } } } + + private static String extractFromTarFileAsString(File tarFile, String filename) + throws IOException { + try (TarArchiveInputStream input = + new TarArchiveInputStream(Files.newInputStream(tarFile.toPath()))) { + TarArchiveEntry imageEntry; + while ((imageEntry = input.getNextTarEntry()) != null) { + if (filename.equals(imageEntry.getName())) { + return CharStreams.toString(new InputStreamReader(input, StandardCharsets.UTF_8)); + } + } + } + throw new AssertionError("file not found: " + filename); + } } From 940159deddfc6ebadcf0612ce2e89e88cdaca132 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 2 May 2019 18:41:41 -0400 Subject: [PATCH 0567/2020] No generics (#1690) --- .../jib/builder/steps/BuildImageStep.java | 23 ++++---- .../tools/jib/builder/steps/BuildResult.java | 4 +- .../jib/builder/steps/LoadDockerStep.java | 3 +- .../jib/builder/steps/PullBaseImageStep.java | 10 ++-- .../steps/PushContainerConfigurationStep.java | 3 +- .../jib/builder/steps/WriteTarFileStep.java | 3 +- .../cloud/tools/jib/docker/ImageTarball.java | 4 +- .../google/cloud/tools/jib/image/Image.java | 53 +++++++++---------- .../cloud/tools/jib/image/ImageLayers.java | 44 ++++++++------- .../jib/image/json/ImageToJsonTranslator.java | 4 +- .../jib/image/json/JsonToImageTranslator.java | 12 ++--- ...BuildAndCacheApplicationLayerStepTest.java | 9 ++-- .../jib/builder/steps/BuildImageStepTest.java | 17 +++--- .../jib/builder/steps/BuildResultTest.java | 7 ++- .../tools/jib/docker/ImageTarballTest.java | 2 +- .../tools/jib/image/ImageLayersTest.java | 6 +-- .../cloud/tools/jib/image/ImageTest.java | 6 +-- .../image/json/ImageToJsonTranslatorTest.java | 2 +- .../image/json/JsonToImageTranslatorTest.java | 5 +- 19 files changed, 99 insertions(+), 118 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 85d12e0b04..5edc8e35fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -27,7 +27,6 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.common.collect.ImmutableList; @@ -40,8 +39,7 @@ import javax.annotation.Nullable; /** Builds a model {@link Image}. */ -class BuildImageStep - implements AsyncStep>>, Callable>> { +class BuildImageStep implements AsyncStep>, Callable> { private static final String DESCRIPTION = "Building container configuration"; @@ -52,7 +50,7 @@ class BuildImageStep private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; private final ImmutableList buildAndCacheApplicationLayerSteps; - private final ListenableFuture>> listenableFuture; + private final ListenableFuture> listenableFuture; BuildImageStep( ListeningExecutorService listeningExecutorService, @@ -76,13 +74,13 @@ class BuildImageStep } @Override - public ListenableFuture>> getFuture() { + public ListenableFuture> getFuture() { return listenableFuture; } @Override - public AsyncStep> call() throws ExecutionException { - ListenableFuture> future = + public AsyncStep call() throws ExecutionException { + ListenableFuture future = AsyncDependencies.using(listeningExecutorService) .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) .addSteps(buildAndCacheApplicationLayerSteps) @@ -90,16 +88,15 @@ public AsyncStep> call() throws ExecutionException { return () -> future; } - private Image afterCachedLayerSteps() - throws ExecutionException, LayerPropertyNotFoundException { + private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( BuildStepType.BUILD_IMAGE, "building image format", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { // Constructs the image. - Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); - Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); + Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); + Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); @@ -185,7 +182,7 @@ private Image afterCachedLayerSteps() */ @Nullable private ImmutableList computeEntrypoint( - Image baseImage, ContainerConfiguration containerConfiguration) { + Image baseImage, ContainerConfiguration containerConfiguration) { boolean shouldInherit = baseImage.getEntrypoint() != null && containerConfiguration.getEntrypoint() == null; @@ -214,7 +211,7 @@ private ImmutableList computeEntrypoint( */ @Nullable private ImmutableList computeProgramArguments( - Image baseImage, ContainerConfiguration containerConfiguration) { + Image baseImage, ContainerConfiguration containerConfiguration) { boolean shouldInherit = baseImage.getProgramArguments() != null // Inherit CMD only when inheriting ENTRYPOINT. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index 383b055760..5ffa8fc01b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -38,8 +37,7 @@ public class BuildResult { * @return a new {@link BuildResult} with the image's digest and id * @throws IOException if writing the digest or container configuration fails */ - static BuildResult fromImage( - Image image, Class targetFormat) + static BuildResult fromImage(Image image, Class targetFormat) throws IOException { ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(image); BlobDescriptor containerConfigurationBlobDescriptor = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index dc15cddcfb..e45d0a140a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -27,7 +27,6 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.Layer; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -97,7 +96,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( BuildStepType.LOAD_DOCKER, "loading to Docker daemon", 1)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 1e96d4a310..07e16c67d9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -32,7 +32,6 @@ import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; @@ -65,17 +64,16 @@ class PullBaseImageStep /** Structure for the result returned by this step. */ static class BaseImageWithAuthorization { - private final Image baseImage; + private final Image baseImage; private final @Nullable Authorization baseImageAuthorization; @VisibleForTesting - BaseImageWithAuthorization( - Image baseImage, @Nullable Authorization baseImageAuthorization) { + BaseImageWithAuthorization(Image baseImage, @Nullable Authorization baseImageAuthorization) { this.baseImage = baseImage; this.baseImageAuthorization = baseImageAuthorization; } - Image getBaseImage() { + Image getBaseImage() { return baseImage; } @@ -211,7 +209,7 @@ public BaseImageWithAuthorization call() * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - private Image pullBaseImage( + private Image pullBaseImage( @Nullable Authorization registryAuthorization, ProgressEventDispatcher progressEventDispatcher) throws IOException, RegistryException, LayerPropertyNotFoundException, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 4d6835c59d..b3722bfbd3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ListenableFuture; @@ -90,7 +89,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() BuildStepType.PUSH_CONTAINER_CONFIGURATION, "pushing container configuration", 1); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); Blob containerConfigurationBlob = new ImageToJsonTranslator(image).getContainerConfigurationBlob(); BlobDescriptor blobDescriptor = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 5a19c07ce5..bd97b11520 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -97,7 +96,7 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( BuildStepType.WRITE_TAR_FILE, "writing to tar file", 1)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); + Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Builds the image to a tarball. Files.createDirectories(outputPath.getParent()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index f7ad06a162..2a595f923e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -41,7 +41,7 @@ public class ImageTarball { /** File name extension for the layer content files. */ private static final String LAYER_FILE_EXTENSION = ".tar.gz"; - private final Image image; + private final Image image; private final ImageReference imageReference; @@ -51,7 +51,7 @@ public class ImageTarball { * @param image the image to convert into a tarball * @param imageReference image reference to set in the manifest */ - public ImageTarball(Image image, ImageReference imageReference) { + public ImageTarball(Image image, ImageReference imageReference) { this.image = image; this.imageReference = imageReference; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index de83f08df9..10f0ede881 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -33,13 +33,13 @@ import javax.annotation.Nullable; /** Represents an image. */ -public class Image { +public class Image { /** Builds the immutable {@link Image}. */ - public static class Builder { + public static class Builder { private final Class imageFormat; - private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); + private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); // Don't use ImmutableMap.Builder because it does not allow for replacing existing keys with new @@ -68,7 +68,7 @@ private Builder(Class imageFormat) { * @param created the creation time * @return this */ - public Builder setCreated(Instant created) { + public Builder setCreated(Instant created) { this.created = created; return this; } @@ -79,7 +79,7 @@ public Builder setCreated(Instant created) { * @param architecture the architecture * @return this */ - public Builder setArchitecture(String architecture) { + public Builder setArchitecture(String architecture) { this.architecture = architecture; return this; } @@ -90,7 +90,7 @@ public Builder setArchitecture(String architecture) { * @param os the operating system * @return this */ - public Builder setOs(String os) { + public Builder setOs(String os) { this.os = os; return this; } @@ -101,7 +101,7 @@ public Builder setOs(String os) { * @param environment the map of environment variables * @return this */ - public Builder addEnvironment(@Nullable Map environment) { + public Builder addEnvironment(@Nullable Map environment) { if (environment != null) { this.environmentBuilder.putAll(environment); } @@ -115,7 +115,7 @@ public Builder addEnvironment(@Nullable Map environment) { * @param value the value to set it to * @return this */ - public Builder addEnvironmentVariable(String name, String value) { + public Builder addEnvironmentVariable(String name, String value) { environmentBuilder.put(name, value); return this; } @@ -126,7 +126,7 @@ public Builder addEnvironmentVariable(String name, String value) { * @param entrypoint the list of entrypoint tokens * @return this */ - public Builder setEntrypoint(@Nullable List entrypoint) { + public Builder setEntrypoint(@Nullable List entrypoint) { this.entrypoint = (entrypoint == null) ? null : ImmutableList.copyOf(entrypoint); return this; } @@ -137,7 +137,7 @@ public Builder setEntrypoint(@Nullable List entrypoint) { * @param user the username/UID and optionally the groupname/GID * @return this */ - public Builder setUser(@Nullable String user) { + public Builder setUser(@Nullable String user) { this.user = user; return this; } @@ -148,7 +148,7 @@ public Builder setUser(@Nullable String user) { * @param programArguments the list of arguments to append to the image entrypoint * @return this */ - public Builder setProgramArguments(@Nullable List programArguments) { + public Builder setProgramArguments(@Nullable List programArguments) { this.programArguments = (programArguments == null) ? null : ImmutableList.copyOf(programArguments); return this; @@ -160,7 +160,7 @@ public Builder setProgramArguments(@Nullable List programArguments) { * @param healthCheck the healthcheck configuration * @return this */ - public Builder setHealthCheck(@Nullable DockerHealthCheck healthCheck) { + public Builder setHealthCheck(@Nullable DockerHealthCheck healthCheck) { this.healthCheck = healthCheck; return this; } @@ -171,7 +171,7 @@ public Builder setHealthCheck(@Nullable DockerHealthCheck healthCheck) { * @param exposedPorts the exposed ports to add * @return this */ - public Builder addExposedPorts(@Nullable Set exposedPorts) { + public Builder addExposedPorts(@Nullable Set exposedPorts) { if (exposedPorts != null) { exposedPortsBuilder.addAll(exposedPorts); } @@ -184,7 +184,7 @@ public Builder addExposedPorts(@Nullable Set exposedPorts) { * @param volumes the directories to create volumes * @return this */ - public Builder addVolumes(@Nullable Set volumes) { + public Builder addVolumes(@Nullable Set volumes) { if (volumes != null) { volumesBuilder.addAll(ImmutableSet.copyOf(volumes)); } @@ -197,7 +197,7 @@ public Builder addVolumes(@Nullable Set volumes) { * @param labels the map of labels to add * @return this */ - public Builder addLabels(@Nullable Map labels) { + public Builder addLabels(@Nullable Map labels) { if (labels != null) { labelsBuilder.putAll(labels); } @@ -211,7 +211,7 @@ public Builder addLabels(@Nullable Map labels) { * @param value the value of the label * @return this */ - public Builder addLabel(String name, String value) { + public Builder addLabel(String name, String value) { labelsBuilder.put(name, value); return this; } @@ -222,7 +222,7 @@ public Builder addLabel(String name, String value) { * @param workingDirectory the working directory * @return this */ - public Builder setWorkingDirectory(@Nullable String workingDirectory) { + public Builder setWorkingDirectory(@Nullable String workingDirectory) { this.workingDirectory = workingDirectory; return this; } @@ -234,7 +234,7 @@ public Builder setWorkingDirectory(@Nullable String workingDirectory) { * @return this * @throws LayerPropertyNotFoundException if adding the layer fails */ - public Builder addLayer(T layer) throws LayerPropertyNotFoundException { + public Builder addLayer(Layer layer) throws LayerPropertyNotFoundException { imageLayersBuilder.add(layer); return this; } @@ -245,13 +245,13 @@ public Builder addLayer(T layer) throws LayerPropertyNotFoundException { * @param history the history object to add * @return this */ - public Builder addHistory(HistoryEntry history) { + public Builder addHistory(HistoryEntry history) { historyBuilder.add(history); return this; } - public Image build() { - return new Image<>( + public Image build() { + return new Image( imageFormat, created, architecture, @@ -270,9 +270,8 @@ public Image build() { } } - public static Builder builder( - Class imageFormat) { - return new Builder<>(imageFormat); + public static Builder builder(Class imageFormat) { + return new Builder(imageFormat); } /** The image format. */ @@ -288,7 +287,7 @@ public static Builder builder( private final String os; /** The layers of the image, in the order in which they are applied. */ - private final ImageLayers layers; + private final ImageLayers layers; /** The commands used to build each layer of the image */ private final ImmutableList history; @@ -325,7 +324,7 @@ private Image( @Nullable Instant created, String architecture, String os, - ImageLayers layers, + ImageLayers layers, ImmutableList history, @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @@ -415,7 +414,7 @@ public String getUser() { return user; } - public ImmutableList getLayers() { + public ImmutableList getLayers() { return layers.getLayers(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java index 18c165b9d2..3d1211fba6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java @@ -27,11 +27,11 @@ import javax.annotation.Nullable; /** Holds the layers for an image. */ -public class ImageLayers implements Iterable { +public class ImageLayers implements Iterable { - public static class Builder { + public static class Builder { - private final List layers = new ArrayList<>(); + private final List layers = new ArrayList<>(); private final ImmutableSet.Builder layerDigestsBuilder = ImmutableSet.builder(); private boolean removeDuplicates = false; @@ -46,7 +46,7 @@ public static class Builder { * @return this * @throws LayerPropertyNotFoundException if adding the layer fails */ - public Builder add(T layer) throws LayerPropertyNotFoundException { + public Builder add(Layer layer) throws LayerPropertyNotFoundException { layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); layers.add(layer); return this; @@ -55,14 +55,12 @@ public Builder add(T layer) throws LayerPropertyNotFoundException { /** * Adds all layers in {@code layers}. * - * @param child type of {@link Layer} * @param layers the layers to add * @return this * @throws LayerPropertyNotFoundException if adding a layer fails */ - public Builder addAll(ImageLayers layers) - throws LayerPropertyNotFoundException { - for (U layer : layers) { + public Builder addAll(ImageLayers layers) throws LayerPropertyNotFoundException { + for (Layer layer : layers) { add(layer); } return this; @@ -73,41 +71,41 @@ public Builder addAll(ImageLayers layers) * * @return this */ - public Builder removeDuplicates() { + public Builder removeDuplicates() { removeDuplicates = true; return this; } - public ImageLayers build() { + public ImageLayers build() { if (!removeDuplicates) { - return new ImageLayers<>(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); + return new ImageLayers(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); } // LinkedHashSet maintains the order but keeps the first occurrence. Keep last occurrence by // adding elements in reverse, and then reversing the result - Set dedupedButReversed = new LinkedHashSet(Lists.reverse(this.layers)); - ImmutableList deduped = ImmutableList.copyOf(dedupedButReversed).reverse(); - return new ImageLayers<>(deduped, layerDigestsBuilder.build()); + Set dedupedButReversed = new LinkedHashSet<>(Lists.reverse(this.layers)); + ImmutableList deduped = ImmutableList.copyOf(dedupedButReversed).reverse(); + return new ImageLayers(deduped, layerDigestsBuilder.build()); } } - public static Builder builder() { - return new Builder<>(); + public static Builder builder() { + return new Builder(); } /** The layers of the image, in the order in which they are applied. */ - private final ImmutableList layers; + private final ImmutableList layers; /** Keeps track of the layers already added. */ private final ImmutableSet layerDigests; - private ImageLayers(ImmutableList layers, ImmutableSet layerDigests) { + private ImageLayers(ImmutableList layers, ImmutableSet layerDigests) { this.layers = layers; this.layerDigests = layerDigests; } /** @return a read-only view of the image layers. */ - public ImmutableList getLayers() { + public ImmutableList getLayers() { return layers; } @@ -124,7 +122,7 @@ public boolean isEmpty() { * @param index the index of the layer to get * @return the layer at the specified index */ - public T get(int index) { + public Layer get(int index) { return layers.get(index); } @@ -134,11 +132,11 @@ public T get(int index) { * @throws LayerPropertyNotFoundException if getting the layer's blob descriptor fails */ @Nullable - public T get(DescriptorDigest digest) throws LayerPropertyNotFoundException { + public Layer get(DescriptorDigest digest) throws LayerPropertyNotFoundException { if (!has(digest)) { return null; } - for (T layer : layers) { + for (Layer layer : layers) { if (layer.getBlobDescriptor().getDigest().equals(digest)) { return layer; } @@ -155,7 +153,7 @@ public boolean has(DescriptorDigest digest) { } @Override - public Iterator iterator() { + public Iterator iterator() { return getLayers().iterator(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index bb504ffeac..18cd58e893 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -129,14 +129,14 @@ static ImmutableList environmentMapToList(@Nullable Map String::compareTo, keyMapper, ignored -> Collections.emptyMap())); } - private final Image image; + private final Image image; /** * Instantiate with an {@link Image}. * * @param image the image to translate */ - public ImageToJsonTranslator(Image image) { + public ImageToJsonTranslator(Image image) { this.image = image; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index d544d01a78..00fc5a8fd6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.DigestOnlyLayer; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.ReferenceLayer; @@ -71,9 +70,9 @@ public class JsonToImageTranslator { * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - public static Image toImage(V21ManifestTemplate manifestTemplate) + public static Image toImage(V21ManifestTemplate manifestTemplate) throws LayerPropertyNotFoundException, BadContainerConfigurationFormatException { - Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); + Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); // V21 layers are in reverse order of V22. (The first layer is the latest one.) for (DescriptorDigest digest : Lists.reverse(manifestTemplate.getLayerDigests())) { @@ -101,7 +100,7 @@ public static Image toImage(V21ManifestTemplate manifestTemplate) * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - public static Image toImage( + public static Image toImage( BuildableManifestTemplate manifestTemplate, ContainerConfigurationTemplate containerConfigurationTemplate) throws LayerCountMismatchException, LayerPropertyNotFoundException, @@ -125,7 +124,7 @@ public static Image toImage( "Mismatch between image manifest and container configuration"); } - Image.Builder imageBuilder = Image.builder(manifestTemplate.getClass()); + Image.Builder imageBuilder = Image.builder(manifestTemplate.getClass()); for (int layerIndex = 0; layerIndex < layers.size(); layerIndex++) { ReferenceNoDiffIdLayer noDiffIdLayer = layers.get(layerIndex); @@ -139,8 +138,7 @@ public static Image toImage( } private static void configureBuilderWithContainerConfiguration( - Image.Builder imageBuilder, - ContainerConfigurationTemplate containerConfigurationTemplate) + Image.Builder imageBuilder, ContainerConfigurationTemplate containerConfigurationTemplate) throws BadContainerConfigurationFormatException { containerConfigurationTemplate.getHistory().forEach(imageBuilder::addHistory); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 7f00635f89..cea49723b5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -127,8 +126,8 @@ public void setUp() throws IOException, URISyntaxException { Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); } - private ImageLayers buildFakeLayersToCache() throws ExecutionException { - ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); + private ImageLayers buildFakeLayersToCache() throws ExecutionException { + ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( @@ -159,7 +158,7 @@ public void testRun() .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(5, applicationLayers.size()); ImmutableList dependenciesLayerEntries = @@ -221,7 +220,7 @@ public void testRun_emptyLayersIgnored() .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + ImageLayers applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(3, applicationLayers.size()); ImmutableList dependenciesLayerEntries = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 11f33f81b2..65e53420d0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -27,7 +27,6 @@ import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableList; @@ -101,7 +100,7 @@ public void setUp() throws DigestException { .setEmptyLayer(true) .build(); - Image baseImage = + Image baseImage = Image.builder(V22ManifestTemplate.class) .setArchitecture("wasm") .setOs("js") @@ -172,7 +171,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); } @@ -201,7 +200,7 @@ public void test_propagateBaseImageConfiguration() mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals("wasm", image.getArchitecture()); Assert.assertEquals("js", image.getOs()); Assert.assertEquals( @@ -264,7 +263,7 @@ public void testOverrideWorkingDirectory() throws InterruptedException, Executio mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals("/my/directory", image.getWorkingDirectory()); } @@ -286,7 +285,7 @@ public void test_inheritedEntrypoint() throws ExecutionException, InterruptedExc mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); Assert.assertEquals(ImmutableList.of("test"), image.getProgramArguments()); @@ -309,7 +308,7 @@ public void test_inheritedEntrypointAndProgramArguments() mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); Assert.assertEquals(ImmutableList.of("catalina.sh", "run"), image.getProgramArguments()); @@ -332,7 +331,7 @@ public void test_notInheritedProgramArguments() throws ExecutionException, Inter mockBuildAndCacheApplicationLayerStepDependencies, mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); Assert.assertEquals(ImmutableList.of("myEntrypoint"), image.getEntrypoint()); Assert.assertNull(image.getProgramArguments()); @@ -352,7 +351,7 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted mockBuildAndCacheApplicationLayerStepResources, mockBuildAndCacheApplicationLayerStepClasses, mockBuildAndCacheApplicationLayerStepExtraFiles)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + Image image = buildImageStep.getFuture().get().getFuture().get(); // Make sure history is as expected HistoryEntry expectedAddedBaseLayerHistory = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java index 2357b9d62a..4d489c7e4a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import java.security.DigestException; @@ -66,9 +65,9 @@ public void testEquality() { @Test public void testFromImage() throws IOException { - Image image1 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); - Image image2 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); - Image image3 = Image.builder(V22ManifestTemplate.class).setUser("anotherUser").build(); + Image image1 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image2 = Image.builder(V22ManifestTemplate.class).setUser("user").build(); + Image image3 = Image.builder(V22ManifestTemplate.class).setUser("anotherUser").build(); Assert.assertEquals( BuildResult.fromImage(image1, V22ManifestTemplate.class), BuildResult.fromImage(image2, V22ManifestTemplate.class)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java index 146bd780c8..7c9addaefc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java @@ -80,7 +80,7 @@ public void testWriteTo() Mockito.when(mockLayer2.getBlobDescriptor()) .thenReturn(new BlobDescriptor(fileBSize, fakeDigestB)); Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); - Image testImage = + Image testImage = Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); ImageTarball imageToTarball = new ImageTarball(testImage, ImageReference.parse("my/image:tag")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index 2c9c6ae23c..bb8c2d691e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -61,7 +61,7 @@ public void setUpFakes() throws LayerPropertyNotFoundException { public void testAddLayer_success() throws LayerPropertyNotFoundException { List expectedLayers = Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer); - ImageLayers imageLayers = + ImageLayers imageLayers = ImageLayers.builder() .add(mockLayer) .add(mockReferenceLayer) @@ -77,7 +77,7 @@ public void testAddLayer_maintainDuplicates() throws LayerPropertyNotFoundExcept List expectedLayers = Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); - ImageLayers imageLayers = + ImageLayers imageLayers = ImageLayers.builder() .add(mockLayer) .add(mockReferenceLayer) @@ -95,7 +95,7 @@ public void testAddLayer_removeDuplicates() throws LayerPropertyNotFoundExceptio List expectedLayers = Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); - ImageLayers imageLayers = + ImageLayers imageLayers = ImageLayers.builder() .removeDuplicates() .add(mockLayer) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index 748eede33d..eeca85eeff 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -48,7 +48,7 @@ public void setUp() throws LayerPropertyNotFoundException { @Test public void test_smokeTest() throws LayerPropertyNotFoundException { - Image image = + Image image = Image.builder(V22ManifestTemplate.class) .setCreated(Instant.ofEpochSecond(10000)) .addEnvironmentVariable("crepecake", "is great") @@ -80,7 +80,7 @@ public void test_smokeTest() throws LayerPropertyNotFoundException { @Test public void testDefaults() { - Image image = Image.builder(V22ManifestTemplate.class).build(); + Image image = Image.builder(V22ManifestTemplate.class).build(); Assert.assertEquals("amd64", image.getArchitecture()); Assert.assertEquals("linux", image.getOs()); Assert.assertEquals(Collections.emptyList(), image.getLayers()); @@ -89,7 +89,7 @@ public void testDefaults() { @Test public void testOsArch() { - Image image = + Image image = Image.builder(V22ManifestTemplate.class).setArchitecture("wasm").setOs("js").build(); Assert.assertEquals("wasm", image.getArchitecture()); Assert.assertEquals("js", image.getOs()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index f7a5f68524..cd33ceed44 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -55,7 +55,7 @@ public class ImageToJsonTranslatorTest { private void setUp(Class imageFormat) throws DigestException, LayerPropertyNotFoundException { - Image.Builder testImageBuilder = + Image.Builder testImageBuilder = Image.builder(imageFormat) .setCreated(Instant.ofEpochSecond(20)) .setArchitecture("wasm") diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index a480f51bdb..2db5e4d4c8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -57,7 +57,7 @@ public void testToImage_v21() V21ManifestTemplate manifestTemplate = JsonTemplateMapper.readJsonFromFile(jsonFile, V21ManifestTemplate.class); - Image image = JsonToImageTranslator.toImage(manifestTemplate); + Image image = JsonToImageTranslator.toImage(manifestTemplate); List layers = image.getLayers(); Assert.assertEquals(2, layers.size()); @@ -181,8 +181,7 @@ private void testToImage_buildable( T manifestTemplate = JsonTemplateMapper.readJsonFromFile(manifestJsonFile, manifestTemplateClass); - Image image = - JsonToImageTranslator.toImage(manifestTemplate, containerConfigurationTemplate); + Image image = JsonToImageTranslator.toImage(manifestTemplate, containerConfigurationTemplate); List layers = image.getLayers(); Assert.assertEquals(1, layers.size()); From bd84f1cbe942c1c24b71c465c8ca16ddd464bdf2 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 2 May 2019 18:43:52 -0400 Subject: [PATCH 0568/2020] Return absolute paths to extra directories (#1686) * Resolve relative paths in FilesMojoV2 to module root. --- .../cloud/tools/jib/maven/skaffold/FilesMojoV2.java | 7 +++++++ .../cloud/tools/jib/maven/skaffold/FilesMojoTest.java | 4 +++- .../cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java | 8 +++++--- .../maven/projects/multi/complex-service/pom.xml | 8 +++++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index 656cf96551..e098846c7a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -169,6 +169,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { } private List resolveExtraDirectories(MavenProject project) throws MojoExecutionException { + return collectExtraDirectories(project) + .stream() + .map(path -> path.isAbsolute() ? path : project.getBasedir().toPath().resolve(path)) + .collect(Collectors.toList()); + } + + private List collectExtraDirectories(MavenProject project) throws MojoExecutionException { // Try getting extra directory from project/session properties String deprecatedProperty = MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index 348109e98b..f35601d6ee 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -136,7 +136,9 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti complexServiceRoot.resolve("src/main/java"), complexServiceRoot.resolve("src/main/resources1"), complexServiceRoot.resolve("src/main/resources2"), - complexServiceRoot.resolve("src/main/other-jib"), + complexServiceRoot.resolve("src/main/jib1"), + complexServiceRoot.resolve("src/main/jib2"), + Paths.get("/some/random/absolute/path/jib3"), // this test expects standard .m2 locations Paths.get( System.getProperty("user.home"), diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index 1941e3b31b..1fb8c6578c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -152,7 +152,9 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti complexServiceRoot.resolve("src/main/java").toString(), complexServiceRoot.resolve("src/main/resources1").toString(), complexServiceRoot.resolve("src/main/resources2").toString(), - complexServiceRoot.resolve("src/main/other-jib").toString(), + complexServiceRoot.resolve("src/main/jib1").toString(), + complexServiceRoot.resolve("src/main/jib2").toString(), + Paths.get("/").toAbsolutePath().resolve("some/random/absolute/path/jib3").toString(), // this test expects standard .m2 locations Paths.get( System.getProperty("user.home"), @@ -173,7 +175,7 @@ public void testFilesMojo_extraDirectoriesProperty() throws VerificationExceptio Arrays.asList( projectRoot.resolve("src/main/java").toString(), projectRoot.resolve("src/main/resources").toString(), - Paths.get("/some/extra/dir").toString(), - Paths.get("/another/extra/dir").toString())); + Paths.get("/").toAbsolutePath().resolve("some/extra/dir").toString(), + Paths.get("/").toAbsolutePath().resolve("another/extra/dir").toString())); } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml index 38100405f6..96458e4518 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml @@ -18,7 +18,13 @@ com.google.cloud.tools jib-maven-plugin - ${project.basedir}/src/main/other-jib + + + src/main/jib1 + ${project.basedir}/src/main/jib2 + /some/random/absolute/path/jib3 + + From 54403945611a55db1ee5dbb88f0c3e2751d9eb71 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 2 May 2019 18:47:58 -0400 Subject: [PATCH 0569/2020] Add FAQ when "401 Unauthorized" from registry (#1688) * FAQ when "401 Unauthorized" from registry --- docs/faq.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 3987bd37dd..0dc523c19e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,6 +20,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [How can I inspect the image Jib built?](#how-can-i-inspect-the-image-jib-built)\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ +[What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized)\ [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ [How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ @@ -343,6 +344,22 @@ See more at [Using Google Container Registry (GCR) with Minikube](https://ryanes Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). +### What should I do when the registry responds with UNAUTHORIZED? + +If the registry returns "401 Unauthorized" or `"code":"UNAUTHORIZED"`, it is often due to credential misconfiguration. Examples: + +* You did not configure auth information in the default places where Jib searches. + - `$HOME/.docker/config.json`, [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). + - Some common credential helpers on `$PATH` (for example, `docker-credential-osxkeychain`, `docker-credential-ecr-login`, etc.) for well-known registries. + - Jib configurations + - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) for Maven / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) for Gradle + - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) for Maven / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) for Gradle + - These parameters can also be set through properties: [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#system-properties) / [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#system-properties) +* You configured a credential helper, but the helper is not on `$PATH`. This is especially common when running Jib inside IDE where the IDE binary is launched directly from an OS menu and does not have access to your shell's environment. +* Typos in username, password, image names, or registry names. + +If you encounter issues interacting with a registry other than `UNAUTHORIZED`, check ["How can I diagnose problems pulling or pushing from remote registries?"](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). + ### How can I diagnose problems pulling or pushing from remote registries? There are a few reasons why Jib may be unable to connect to a remote registry, including: From 68e17bccc5b7c910704a70836e8db956ca23bafe Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 3 May 2019 10:20:05 -0400 Subject: [PATCH 0570/2020] Deprecate jib.extraDirectory in Gradle (#1671) --- .../cloud/tools/jib/gradle/JibRunHelper.java | 4 +- .../gradle/SingleProjectIntegrationTest.java | 34 +++++ .../simple/build-extra-dir-deprecated.gradle | 20 +++ .../simple/build-extra-dir-deprecated2.gradle | 18 +++ .../simple/build-extra-dir-deprecated3.gradle | 18 +++ .../projects/simple/build-extra-dirs.gradle | 2 +- .../projects/simple/build-extra-dirs2.gradle | 4 +- .../gradle/projects/simple/build.gradle | 2 +- .../projects/simple/complex-build.gradle | 4 +- .../tools/jib/gradle/BuildDockerTask.java | 1 + .../tools/jib/gradle/BuildImageTask.java | 1 + .../cloud/tools/jib/gradle/BuildTarTask.java | 3 +- ...s.java => ExtraDirectoriesParameters.java} | 32 +++-- .../cloud/tools/jib/gradle/FilesTask.java | 2 +- .../cloud/tools/jib/gradle/FilesTaskV2.java | 2 +- .../jib/gradle/GradleRawConfiguration.java | 4 +- .../cloud/tools/jib/gradle/JibExtension.java | 49 ++++--- .../cloud/tools/jib/gradle/TaskCommon.java | 23 +++- .../tools/jib/gradle/JibExtensionTest.java | 98 +++++++++++++- .../tools/jib/gradle/TaskCommonTest.java | 126 ++++++++++++++++++ .../complex-service/build.gradle | 6 +- .../gradle/projects/simple/build.gradle | 4 +- .../simple/pom-complex-properties.xml | 4 +- 23 files changed, 407 insertions(+), 54 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{ExtraDirectoryParameters.java => ExtraDirectoriesParameters.java} (71%) create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index b2cb9a63ef..fa06d3f1a4 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -89,7 +89,7 @@ static void buildAndRunAdditionalTag( assertCreationTimeEpoch(additionalImageReference); } - static void buildToDockerDaemon( + static BuildResult buildToDockerDaemon( TestProject testProject, String imageReference, String gradleBuildFile) throws IOException, InterruptedException, DigestException { BuildResult buildResult = @@ -106,6 +106,8 @@ static void buildToDockerDaemon( String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + return buildResult; } static String buildToDockerDaemonAndRun( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 2dcefd38ed..7af925c53f 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -129,6 +129,22 @@ private static void assertDockerInspect(String imageReference) + " }")); } + private static void assertExtraDirectoryDeprecationWarning(String pomXml) + throws DigestException, IOException, InterruptedException { + String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); + BuildResult buildResult = + JibRunHelper.buildToDockerDaemon(simpleTestProject, targetImage, pomXml); + Assert.assertEquals( + "Hello, world. \nrw-r--r--\nrw-r--r--\nfoo\ncat\n", + new Command("docker", "run", "--rm", targetImage).run()); + Assert.assertThat( + buildResult.getOutput(), + CoreMatchers.containsString( + "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " + + "are deprecated; use 'jib.extraDirectories.paths' and " + + "'jib.extraDirectories.permissions'")); + } + private static String buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) throws IOException, InterruptedException { @@ -232,6 +248,24 @@ public void testDockerDaemon_simpleWithIncompatibleJava11() } } + @Test + public void testDockerDaemon_simple_deprecatedExtraDirectory() + throws DigestException, IOException, InterruptedException { + assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated.gradle"); + } + + @Test + public void testDockerDaemon_simple_deprecatedExtraDirectory2() + throws DigestException, IOException, InterruptedException { + assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated2.gradle"); + } + + @Test + public void testDockerDaemon_simple_deprecatedExtraDirectory3() + throws DigestException, IOException, InterruptedException { + assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated3.gradle"); + } + @Test public void testDockerDaemon_simple_multipleExtraDirectories() throws DigestException, IOException, InterruptedException { diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle new file mode 100644 index 0000000000..f330a8f97a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") +jib.extraDirectory { + path = file('src/main/custom-extra-dir') +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle new file mode 100644 index 0000000000..4eeb3e5c56 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") +jib.extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle new file mode 100644 index 0000000000..3d0a53203f --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib.to.image = System.getProperty("_TARGET_IMAGE") +jib.extraDirectory.path = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle index d185ff2603..d3a4ff06c8 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle @@ -15,4 +15,4 @@ dependencies { } jib.to.image = System.getProperty("_TARGET_IMAGE") -jib.extraDirectory = ['src/main/custom-extra-dir', 'src/main/custom-extra-dir2'] +jib.extraDirectories.paths = ['src/main/custom-extra-dir', 'src/main/custom-extra-dir2'] diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle index 07ebab8972..362d5b54da 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle @@ -15,7 +15,7 @@ dependencies { } jib.to.image = System.getProperty("_TARGET_IMAGE") -jib.extraDirectory { - path = files('src/main/custom-extra-dir', 'src/main/custom-extra-dir2') +jib.extraDirectories { + paths = files('src/main/custom-extra-dir', 'src/main/custom-extra-dir2') permissions = ['/baz':'705'] } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle index b350cfeb4d..84b461ad2a 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle @@ -28,5 +28,5 @@ jib { workingDirectory = '/home' extraClasspath = ['/d1','/d2'] } - extraDirectory = file('src/main/custom-extra-dir') + extraDirectories.paths = file('src/main/custom-extra-dir') } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle index 9a9b9d1fbe..de303ff081 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle @@ -39,8 +39,8 @@ jib { labels = [key1:'value1', key2:'value2'] volumes = ['/var/log', '/var/log2'] } - extraDirectory { - path = file('src/main/custom-extra-dir') + extraDirectories { + paths = file('src/main/custom-extra-dir') permissions = ['/foo':'755', '/bar/cat':'777'] } allowInsecureRegistries = true diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index afc2fdb98b..9a10c72762 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -101,6 +101,7 @@ public void buildDocker() // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); + TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); try { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 15fe7da536..d887cfce38 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -78,6 +78,7 @@ public void buildImage() MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); + TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); try { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 57de55d8f5..5a5a55715d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -81,7 +81,7 @@ public void setTargetImage(String targetImage) { @InputFiles public FileCollection getInputFiles() { List extraDirectories = - Preconditions.checkNotNull(jibExtension).getExtraDirectory().getPaths(); + Preconditions.checkNotNull(jibExtension).getExtraDirectories().getPaths(); return extraDirectories .stream() .map(Path::toFile) @@ -105,6 +105,7 @@ public void buildTar() MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); + TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); try { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java similarity index 71% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java index 2d419becc5..219b52eb94 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java @@ -30,17 +30,19 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; -/** Object in {@link JibExtension} that configures the extra directory. */ -public class ExtraDirectoryParameters { +/** Object in {@link JibExtension} that configures the extra directories. */ +public class ExtraDirectoriesParameters { private final Project project; + @Deprecated private final JibExtension jibExtension; private List paths; private Map permissions = Collections.emptyMap(); @Inject - public ExtraDirectoryParameters(Project project) { + public ExtraDirectoriesParameters(Project project, JibExtension jibExtension) { this.project = project; + this.jibExtension = jibExtension; paths = Collections.singletonList( project.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib")); @@ -57,7 +59,9 @@ public List getPathStrings() { public List getPaths() { // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a // String to make them go away. - String property = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + String deprecatedProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); + String newProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); + String property = newProperty != null ? newProperty : deprecatedProperty; if (property != null) { List pathStrings = ConfigurationPropertyValidator.parseListProperty(property); return pathStrings.stream().map(Paths::get).collect(Collectors.toList()); @@ -67,16 +71,22 @@ public List getPaths() { /** * Sets paths. {@code paths} can be any suitable object describing file paths convertible by - * {@link Project#files} (such as {@code List}). + * {@link Project#files} (such as {@link File}, {@code List}, or {@code List}). * * @param paths paths to set. */ - // non-plural to retain backward-compatibility for the "jib.extraDirectory.path" config parameter - public void setPath(Object paths) { + public void setPaths(Object paths) { + jibExtension.extraDirectoriesConfigured = true; this.paths = project.files(paths).getFiles().stream().map(File::toPath).collect(Collectors.toList()); } + @Deprecated + public void setPath(File path) { + jibExtension.extraDirectoryConfigured = true; + this.paths = Collections.singletonList(path.toPath()); + } + /** * Gets the permissions for files in the extra layer on the container. Maps from absolute path on * the container to a 3-digit octal string representation of the file permission bits (e.g. {@code @@ -86,9 +96,11 @@ public void setPath(Object paths) { */ @Input public Map getPermissions() { - if (System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { - return ConfigurationPropertyValidator.parseMapProperty( - System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS)); + String deprecatedProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); + String newProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); + String property = newProperty != null ? newProperty : deprecatedProperty; + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property); } return permissions; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java index d59523fe4b..e32ad05486 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java @@ -150,7 +150,7 @@ public void listFiles() { printProjectFiles(project); // Print extra layer - List extraDirectories = jibExtension.getExtraDirectory().getPaths(); + List extraDirectories = jibExtension.getExtraDirectories().getPaths(); extraDirectories.stream().filter(Files::exists).forEach(System.out::println); // Find project dependencies diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java index 1d8e4a4a6a..6c5b2938a4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java @@ -69,7 +69,7 @@ public void listFiles() throws IOException { addProjectFiles(project); // Add extra layer - List extraDirectories = jibExtension.getExtraDirectory().getPaths(); + List extraDirectories = jibExtension.getExtraDirectories().getPaths(); extraDirectories.stream().filter(Files::exists).forEach(skaffoldFilesOutput::addInput); // Find project dependencies diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index de7ae2fd03..eed3b424a3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -152,11 +152,11 @@ public Optional getProperty(String propertyName) { @Override public List getExtraDirectories() { - return jibExtension.getExtraDirectory().getPaths(); + return jibExtension.getExtraDirectories().getPaths(); } @Override public Map getExtraDirectoryPermissions() { - return TaskCommon.convertPermissionsMap(jibExtension.getExtraDirectory().getPermissions()); + return TaskCommon.convertPermissionsMap(jibExtension.getExtraDirectories().getPermissions()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 70d35d5255..d7760e0004 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.io.File; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; @@ -48,8 +49,8 @@ * format = OCI * appRoot = '/app' * } - * extraDirectory { - * path = file('path/to/extra/dir') + * extraDirectories { + * paths = ['/path/to/extra/dir', 'can/be/relative/to/project/root'] * permissions = [ * '/path/on/container/file1': 744, * '/path/on/container/file2': 123 @@ -67,17 +68,19 @@ public class JibExtension { private final BaseImageParameters from; private final TargetImageParameters to; private final ContainerParameters container; - private final ExtraDirectoryParameters extraDirectory; - + private final ExtraDirectoriesParameters extraDirectories; private final Property allowInsecureRegistries; + @Deprecated boolean extraDirectoryConfigured; + @Deprecated boolean extraDirectoriesConfigured; + public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); from = objectFactory.newInstance(BaseImageParameters.class); to = objectFactory.newInstance(TargetImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); - extraDirectory = objectFactory.newInstance(ExtraDirectoryParameters.class, project); + extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project, this); allowInsecureRegistries = objectFactory.property(Boolean.class); @@ -97,19 +100,22 @@ public void container(Action action) { action.execute(container); } - public void extraDirectory(Action action) { - action.execute(extraDirectory); + @Deprecated + public void extraDirectory(Action action) { + extraDirectoryConfigured = true; + action.execute(extraDirectories); + } + + public void extraDirectories(Action action) { + extraDirectoriesConfigured = true; + action.execute(extraDirectories); } - /** - * Sets extra directory paths. {@code extraDirectories} can be any suitable object describing file - * paths convertible by {@link Project#files} (such as {@code List}). - * - * @param extraDirectories paths to set. - */ - // non-plural to retain backward-compatibility for the "jib.extraDirectory" config parameter - public void setExtraDirectory(Object extraDirectories) { - this.extraDirectory.setPath(extraDirectories); + @Deprecated + // for the deprecated "jib.extraDirectory" config parameter + public void setExtraDirectory(File extraDirectory) { + extraDirectoryConfigured = true; + this.extraDirectories.setPath(extraDirectory); } public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { @@ -134,10 +140,17 @@ public ContainerParameters getContainer() { return container; } + @Deprecated + @Nested + @Optional + public ExtraDirectoriesParameters getExtraDirectory() { + return extraDirectories; + } + @Nested @Optional - public ExtraDirectoryParameters getExtraDirectory() { - return extraDirectory; + public ExtraDirectoriesParameters getExtraDirectories() { + return extraDirectories; } @Input diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 3464a0d197..8b7ef5654d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -19,12 +19,13 @@ import com.google.api.client.http.HttpTransport; import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.common.annotations.VisibleForTesting; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import javax.annotation.Nullable; import org.gradle.api.Project; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.bundling.War; import org.gradle.internal.logging.events.LogEvent; @@ -67,6 +68,25 @@ static void disableHttpLogging() { java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); } + @Deprecated + static void checkDeprecatedUsage(JibExtension jibExtension, Logger logger) { + if (jibExtension.extraDirectoryConfigured + || System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null + || System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { + logger.warn( + "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " + + "are deprecated; use 'jib.extraDirectories.paths' and " + + "'jib.extraDirectories.permissions'"); + + if (jibExtension.extraDirectoriesConfigured + || System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS) != null + || System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS) != null) { + throw new IllegalArgumentException( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); + } + } + } + /** * Validates and converts a {@code String->String} file-path-to-file-permissions map to an * equivalent {@code AbsoluteUnixPath->FilePermission} map. @@ -74,7 +94,6 @@ static void disableHttpLogging() { * @param stringMap the map to convert (example entry: {@code "/path/on/container" -> "755"}) * @return the converted map */ - @VisibleForTesting static Map convertPermissionsMap( Map stringMap) { Map permissionsMap = new HashMap<>(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index c4ca5edbb9..40aab0338a 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -56,6 +56,8 @@ private static void clearProperties() { System.clearProperty("jib.container.user"); System.clearProperty("jib.extraDirectory.path"); System.clearProperty("jib.extraDirectory.permissions"); + System.clearProperty("jib.extraDirectories.paths"); + System.clearProperty("jib.extraDirectories.permissions"); } @Before @@ -151,24 +153,90 @@ public void testContainer() { } @Test - public void testExtraDirectory() { + public void testExtraDirectories_default() { Assert.assertEquals( Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "src", "main", "jib")), - testJibExtension.getExtraDirectory().getPaths()); + testJibExtension.getExtraDirectories().getPaths()); Assert.assertEquals( - Collections.emptyMap(), testJibExtension.getExtraDirectory().getPermissions()); + Collections.emptyMap(), testJibExtension.getExtraDirectories().getPermissions()); + } + @Test + public void testExtraDirectories_deprecatedConfig() { testJibExtension.extraDirectory( extraDirectory -> { extraDirectory.setPath(Paths.get("test", "path").toFile()); extraDirectory.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); }); + Assert.assertTrue(testJibExtension.extraDirectoryConfigured); + Assert.assertFalse(testJibExtension.extraDirectoriesConfigured); + + Assert.assertEquals( + Arrays.asList(Paths.get("test", "path")), + testJibExtension.getExtraDirectories().getPaths()); + Assert.assertEquals( + ImmutableMap.of("file1", "123", "file2", "456"), + testJibExtension.getExtraDirectories().getPermissions()); + } + + @Test + public void testExtraDirectories() { + testJibExtension.extraDirectories( + extraDirectories -> { + extraDirectories.setPaths("test/path"); + extraDirectories.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); + }); + Assert.assertFalse(testJibExtension.extraDirectoryConfigured); + Assert.assertTrue(testJibExtension.extraDirectoriesConfigured); + Assert.assertEquals( Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "test", "path")), - testJibExtension.getExtraDirectory().getPaths()); + testJibExtension.getExtraDirectories().getPaths()); Assert.assertEquals( ImmutableMap.of("file1", "123", "file2", "456"), - testJibExtension.getExtraDirectory().getPermissions()); + testJibExtension.getExtraDirectories().getPermissions()); + } + + @Test + public void testExtraDirectories_fileForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> { + extraDirectories.setPaths(Paths.get("test", "path").toFile()); + }); + Assert.assertEquals( + Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "test", "path")), + testJibExtension.getExtraDirectories().getPaths()); + } + + @Test + public void testExtraDirectories_stringListForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> { + extraDirectories.setPaths(Arrays.asList("test/path", "another/path")); + extraDirectories.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); + }); + + String projectRoot = fakeProject.getProjectDir().getPath(); + Assert.assertEquals( + Arrays.asList( + Paths.get(projectRoot, "test", "path"), Paths.get(projectRoot, "another", "path")), + testJibExtension.getExtraDirectories().getPaths()); + } + + @Test + public void testExtraDirectories_fileListForPaths() { + testJibExtension.extraDirectories( + extraDirectories -> { + extraDirectories.setPaths( + Arrays.asList( + Paths.get("test", "path").toFile(), Paths.get("another", "path").toFile())); + }); + + String projectRoot = fakeProject.getProjectDir().getPath(); + Assert.assertEquals( + Arrays.asList( + Paths.get(projectRoot, "test", "path"), Paths.get(projectRoot, "another", "path")), + testJibExtension.getExtraDirectories().getPaths()); } @Test @@ -220,5 +288,25 @@ public void testProperties() { Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); + System.setProperty("jib.extraDirectories.paths", "/foo,/bar/baz"); + Assert.assertEquals( + Arrays.asList(Paths.get("/foo"), Paths.get("/bar/baz")), + testJibExtension.getExtraDirectories().getPaths()); + System.setProperty("jib.extraDirectories.permissions", "/foo/bar=707,/baz=456"); + Assert.assertEquals( + ImmutableMap.of("/foo/bar", "707", "/baz", "456"), + testJibExtension.getExtraDirectories().getPermissions()); + } + + @Test + public void testDeprecatedProperties() { + System.setProperty("jib.extraDirectory.path", "/foo,/bar/baz"); + Assert.assertEquals( + Arrays.asList(Paths.get("/foo"), Paths.get("/bar/baz")), + testJibExtension.getExtraDirectories().getPaths()); + System.setProperty("jib.extraDirectory.permissions", "/foo/bar=707,/baz=456"); + Assert.assertEquals( + ImmutableMap.of("/foo/bar", "707", "/baz", "456"), + testJibExtension.getExtraDirectories().getPermissions()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java new file mode 100644 index 0000000000..882c213a75 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import org.gradle.api.logging.Logger; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link TaskCommon}. */ +@RunWith(MockitoJUnitRunner.class) +public class TaskCommonTest { + + @Mock private JibExtension jibExtension; + @Mock private Logger logger; + + @Before + public void setUp() { + Assert.assertNull(System.getProperty("jib.extraDirectory.path")); + Assert.assertNull(System.getProperty("jib.extraDirectory.permissions")); + Assert.assertNull(System.getProperty("jib.extraDirectories.paths")); + Assert.assertNull(System.getProperty("jib.extraDirectories.permissions")); + } + + @After + public void tearDown() { + System.clearProperty("jib.extraDirectory.path"); + System.clearProperty("jib.extraDirectory.permissions"); + System.clearProperty("jib.extraDirectories.paths"); + System.clearProperty("jib.extraDirectories.permissions"); + } + + @Test + public void testCheckDeprecatedUsage_default() { + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger, Mockito.never()).warn(Mockito.anyString()); + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoriesConfigured() { + jibExtension.extraDirectoriesConfigured = true; + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger, Mockito.never()).warn(Mockito.anyString()); + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoryPathPropertySet() { + System.setProperty("jib.extraDirectory.path", "something"); + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger, Mockito.times(1)) + .warn( + "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " + + "are deprecated; use 'jib.extraDirectories.paths' and " + + "'jib.extraDirectories.permissions'"); + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoryPermissionsPropertySet() { + System.setProperty("jib.extraDirectory.permissions", "something"); + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger, Mockito.times(1)) + .warn( + "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " + + "are deprecated; use 'jib.extraDirectories.paths' and " + + "'jib.extraDirectories.permissions'"); + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesPropertiesSet() { + System.setProperty("jib.extraDirectory.path", "something"); + System.setProperty("jib.extraDirectories.permissions", "something"); + + try { + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", + ex.getMessage()); + } + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoryConfigured() { + jibExtension.extraDirectoryConfigured = true; + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger, Mockito.times(1)) + .warn( + "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " + + "are deprecated; use 'jib.extraDirectories.paths' and " + + "'jib.extraDirectories.permissions'"); + } + + @Test + public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesConfigured() { + jibExtension.extraDirectoryConfigured = true; + jibExtension.extraDirectoriesConfigured = true; + try { + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", + ex.getMessage()); + } + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle index 92ca5a3e66..eacc676f1c 100644 --- a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle @@ -17,8 +17,8 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") } - extraDirectory { - path = file('src/main/other-jib') + extraDirectories { + paths = file('src/main/other-jib') } } @@ -34,4 +34,4 @@ dependencies { compile project(':lib') compile 'org.apache.commons:commons-io:1.3.2' compile 'com.google.guava:guava:HEAD-jre-SNAPSHOT' -} \ No newline at end of file +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle index 52d0f7cab5..6dab9e5207 100644 --- a/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle @@ -18,7 +18,7 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") } - extraDirectory { - path = file('src/main/custom-extra-dir') + extraDirectories { + paths = file('src/main/custom-extra-dir') } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml index 9e392e7e84..b8c1dc77e3 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml @@ -27,8 +27,8 @@ key1=value1,key2=value2 /var/log,/var/log2 Docker - ${project.basedir}/src/main/jib-custom - /foo=755,/bar/cat=777 + ${project.basedir}/src/main/jib-custom + /foo=755,/bar/cat=777 true From 0bda4259a599cb7d65967f605dbcda30065de9c4 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 3 May 2019 11:12:41 -0400 Subject: [PATCH 0571/2020] Add offline mode to jib-core (#1687) --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JibIntegrationTest.java | 63 ++++++++++++++++ .../tools/jib/registry/LocalRegistry.java | 14 +++- .../cloud/tools/jib/api/Containerizer.java | 35 +++++++-- .../tools/jib/api/JibContainerBuilder.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 5 ++ .../jib/builder/steps/PullBaseImageStep.java | 72 ++++++++++++++++--- .../jib/configuration/BuildConfiguration.java | 25 +++++++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 10 files changed, 202 insertions(+), 18 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index cc6b64b047..c19a94b034 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file. ### Added - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- `Containerizer#setOfflineMode` to retrieve the base image from Jib's cache rather than a container registry ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) ### Changed ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 0.9.1 diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 355b02a6db..baf4b8a71e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.cloud.tools.jib.registry.RegistryException; import java.io.IOException; +import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -32,7 +33,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Integration tests for {@link Jib}. */ public class JibIntegrationTest { @@ -40,6 +43,8 @@ public class JibIntegrationTest { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000, "username", "password"); + @Rule public final TemporaryFolder cacheFolder = new TemporaryFolder(); + /** * Pulls a built image and attempts to run it. * @@ -109,6 +114,64 @@ public void testScratch() inspectOutput.contains("\"Layers\": [")); } + @Test + public void testOffline() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + LocalRegistry tempRegistry = new LocalRegistry(5001); + tempRegistry.start(); + tempRegistry.pullAndPushToLocal("busybox", "busybox"); + Path cacheDirectory = cacheFolder.getRoot().toPath(); + + ImageReference targetImageReferenceOnline = + ImageReference.of("localhost:5001", "jib-core", "basic-online"); + ImageReference targetImageReferenceOffline = + ImageReference.of("localhost:5001", "jib-core", "basic-offline"); + + JibContainerBuilder jibContainerBuilder = + Jib.from("localhost:5001/busybox").setEntrypoint("echo", "Hello World"); + + // Should fail since Jib can't build to registry offline + try { + jibContainerBuilder.containerize( + Containerizer.to(RegistryImage.named(targetImageReferenceOffline)).setOfflineMode(true)); + Assert.fail(); + } catch (IllegalStateException ex) { + Assert.assertEquals("Cannot build to a container registry in offline mode", ex.getMessage()); + } + + // Should fail since Jib hasn't cached the base image yet + try { + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named(targetImageReferenceOffline)) + .setBaseImageLayersCache(cacheDirectory) + .setOfflineMode(true)); + Assert.fail(); + } catch (ExecutionException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; localhost:5001/busybox not found in local Jib cache", + ex.getCause().getMessage()); + } + + // Run online to cache the base image + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named(targetImageReferenceOnline)) + .setBaseImageLayersCache(cacheDirectory) + .setAllowInsecureRegistries(true)); + + // Run again in offline mode, should succeed this time + tempRegistry.stop(); + jibContainerBuilder.containerize( + Containerizer.to(DockerDaemonImage.named(targetImageReferenceOffline)) + .setBaseImageLayersCache(cacheDirectory) + .setOfflineMode(true)); + + // Verify output + Assert.assertEquals( + "Hello World\n", + new Command("docker", "run", "--rm", targetImageReferenceOffline.toString()).run()); + } + /** Ensure that a provided executor is not disposed. */ @Test public void testProvidedExecutorNotDisposed() diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index e5c0306603..67d453161c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -52,6 +52,16 @@ public LocalRegistry(int port, String username, String password) { /** Starts the local registry. */ @Override protected void before() throws IOException, InterruptedException { + start(); + } + + @Override + protected void after() { + stop(); + } + + /** Starts the registry */ + public void start() throws IOException, InterruptedException { // Runs the Docker registry. ArrayList dockerTokens = new ArrayList<>( @@ -96,8 +106,8 @@ protected void before() throws IOException, InterruptedException { waitUntilReady(); } - @Override - protected void after() { + /** Stops the registry. */ + public void stop() { try { logout(); new Command("docker", "stop", containerName).run(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 601e54f987..24d83ae4cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -39,7 +39,6 @@ import javax.annotation.Nullable; /** Configures how to containerize. */ -// TODO: Add tests once JibContainerBuilder#containerize() is added. public class Containerizer { /** @@ -83,7 +82,7 @@ public static Containerizer to(RegistryImage registryImage) { .pushImage(); return new Containerizer( - DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory); + DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory, true); } /** @@ -110,7 +109,8 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { .buildImage() .loadDocker(dockerClientBuilder.build()); - return new Containerizer(DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory); + return new Containerizer( + DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory, false); } /** @@ -132,12 +132,14 @@ public static Containerizer to(TarImage tarImage) { .buildImage() .writeTarFile(tarImage.getOutputFile()); - return new Containerizer(DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory); + return new Containerizer( + DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory, false); } private final String description; private final ImageConfiguration imageConfiguration; private final Function stepsRunnerFactory; + private final boolean mustBeOnline; private final Set additionalTags = new HashSet<>(); @Nullable private ExecutorService executorService; @@ -145,16 +147,19 @@ public static Containerizer to(TarImage tarImage) { @Nullable private Path applicationLayersCacheDirectory; @Nullable private EventHandlers eventHandlers; private boolean allowInsecureRegistries = false; + private boolean offline = false; private String toolName = DEFAULT_TOOL_NAME; /** Instantiate with {@link #to}. */ private Containerizer( String description, ImageConfiguration imageConfiguration, - Function stepsRunnerFactory) { + Function stepsRunnerFactory, + boolean mustBeOnline) { this.description = description; this.imageConfiguration = imageConfiguration; this.stepsRunnerFactory = stepsRunnerFactory; + this.mustBeOnline = mustBeOnline; } /** @@ -235,6 +240,22 @@ public Containerizer setAllowInsecureRegistries(boolean allowInsecureRegistries) return this; } + /** + * Sets whether or not to run the build in offline mode. In offline mode, the base image is + * retrieved from the cache instead of pulled from a registry, and the build will fail if the base + * image is not in the cache or if the target is an image registry. + * + * @param offline if {@code true}, the build will run in offline mode + * @return this + */ + public Containerizer setOfflineMode(boolean offline) { + if (mustBeOnline && offline) { + throw new IllegalStateException("Cannot build to a container registry in offline mode"); + } + this.offline = offline; + return this; + } + /** * Sets the name of the tool that is using Jib Core. The tool name is sent as part of the {@code * User-Agent} in registry requests and set as the {@code created_by} in the container layer @@ -283,6 +304,10 @@ boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } + boolean isOfflineMode() { + return offline; + } + String getToolName() { return toolName; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 9179b23b81..ea7b9e25c0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -476,7 +476,6 @@ JibContainer containerize( Containerizer containerizer, Supplier defaultExecutorServiceFactory) throws IOException, CacheDirectoryCreationException, InterruptedException, RegistryException, ExecutionException { - boolean shutdownExecutorService = !containerizer.getExecutorService().isPresent(); ExecutorService executorService = containerizer.getExecutorService().orElseGet(defaultExecutorServiceFactory); @@ -527,6 +526,7 @@ BuildConfiguration toBuildConfiguration( .setContainerConfiguration(containerConfigurationBuilder.build()) .setLayerConfigurations(layerConfigurations) .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) + .setOffline(containerizer.isOfflineMode()) .setToolName(containerizer.getToolName()) .setExecutorService(executorService); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 9c10f3060d..45a49378ae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -82,6 +82,11 @@ public CachedLayer call() throws IOException, CacheCorruptedException { Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { return optionalCachedLayer.get(); + } else if (buildConfiguration.isOffline()) { + throw new IOException( + "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + + layerDigest + + ". You may need to rerun Jib in online mode to re-download the base image layers."); } RegistryClient registryClient = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 07e16c67d9..18b137e0b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -32,15 +33,17 @@ import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; +import com.google.cloud.tools.jib.image.json.ManifestAndConfig; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -51,6 +54,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; @@ -106,8 +110,8 @@ public ListenableFuture getFuture() { @Override public BaseImageWithAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, - LayerCountMismatchException, ExecutionException, - BadContainerConfigurationFormatException { + LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, + CacheCorruptedException { // Skip this step if this is a scratch image ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); if (baseImageConfiguration.getImage().isScratch()) { @@ -126,6 +130,10 @@ public BaseImageWithAuthorization call() + buildConfiguration.getBaseImageConfiguration().getImage() + "...")); + if (buildConfiguration.isOffline()) { + return new BaseImageWithAuthorization(pullBaseImageOffline(), null); + } + try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( BuildStepType.PULL_BASE_IMAGE, "pulling base image manifest", 2); @@ -227,19 +235,24 @@ private Image pullBaseImage( switch (manifestTemplate.getSchemaVersion()) { case 1: V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; + buildConfiguration + .getBaseImageLayersCache() + .writeMetadata( + buildConfiguration.getBaseImageConfiguration().getImage(), v21ManifestTemplate); return JsonToImageTranslator.toImage(v21ManifestTemplate); case 2: - V22ManifestTemplate v22ManifestTemplate = (V22ManifestTemplate) manifestTemplate; - if (v22ManifestTemplate.getContainerConfiguration() == null - || v22ManifestTemplate.getContainerConfiguration().getDigest() == null) { + BuildableManifestTemplate buildableManifestTemplate = + (BuildableManifestTemplate) manifestTemplate; + if (buildableManifestTemplate.getContainerConfiguration() == null + || buildableManifestTemplate.getContainerConfiguration().getDigest() == null) { throw new UnknownManifestFormatException( - "Invalid container configuration in Docker V2.2 manifest: \n" - + Blobs.writeToString(JsonTemplateMapper.toBlob(v22ManifestTemplate))); + "Invalid container configuration in Docker V2.2/OCI manifest: \n" + + Blobs.writeToString(JsonTemplateMapper.toBlob(buildableManifestTemplate))); } DescriptorDigest containerConfigurationDigest = - v22ManifestTemplate.getContainerConfiguration().getDigest(); + buildableManifestTemplate.getContainerConfiguration().getDigest(); try (ProgressEventDispatcherContainer progressEventDispatcherContainer = new ProgressEventDispatcherContainer( @@ -256,10 +269,49 @@ private Image pullBaseImage( ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJson( containerConfigurationString, ContainerConfigurationTemplate.class); - return JsonToImageTranslator.toImage(v22ManifestTemplate, containerConfigurationTemplate); + buildConfiguration + .getBaseImageLayersCache() + .writeMetadata( + buildConfiguration.getBaseImageConfiguration().getImage(), + buildableManifestTemplate, + containerConfigurationTemplate); + return JsonToImageTranslator.toImage( + buildableManifestTemplate, containerConfigurationTemplate); } } throw new IllegalStateException("Unknown manifest schema version"); } + + /** + * Retrieves the cached base image. + * + * @return the cached image + * @throws IOException when an I/O exception occurs + * @throws CacheCorruptedException if the cache is corrupted + * @throws LayerPropertyNotFoundException if adding image layers fails + * @throws BadContainerConfigurationFormatException if the container configuration is in a bad + * format + */ + private Image pullBaseImageOffline() + throws IOException, CacheCorruptedException, BadContainerConfigurationFormatException, + LayerCountMismatchException { + ImageReference baseImage = buildConfiguration.getBaseImageConfiguration().getImage(); + Optional metadata = + buildConfiguration.getBaseImageLayersCache().retrieveMetadata(baseImage); + if (!metadata.isPresent()) { + throw new IOException( + "Cannot run Jib in offline mode; " + baseImage + " not found in local Jib cache"); + } + + ManifestTemplate manifestTemplate = metadata.get().getManifest(); + if (manifestTemplate instanceof V21ManifestTemplate) { + return JsonToImageTranslator.toImage((V21ManifestTemplate) manifestTemplate); + } + + ContainerConfigurationTemplate configurationTemplate = + metadata.get().getConfig().orElseThrow(IllegalStateException::new); + return JsonToImageTranslator.toImage( + (BuildableManifestTemplate) manifestTemplate, configurationTemplate); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 0917a8ec8b..a69f3be4d5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -57,6 +57,7 @@ public static class Builder { @Nullable private Path applicationLayersCacheDirectory; @Nullable private Path baseImageLayersCacheDirectory; private boolean allowInsecureRegistries = false; + private boolean offline = false; private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; private String toolName = DEFAULT_TOOL_NAME; @@ -157,6 +158,17 @@ public Builder setAllowInsecureRegistries(boolean allowInsecureRegistries) { return this; } + /** + * Sets whether or not to perform the build in offline mode. + * + * @param offline if {@code true}, the build will run in offline mode + * @return this + */ + public Builder setOffline(boolean offline) { + this.offline = offline; + return this; + } + /** * Sets the layers to build. * @@ -246,6 +258,7 @@ public BuildConfiguration build() throws IOException { Cache.withDirectory(Preconditions.checkNotNull(applicationLayersCacheDirectory)), targetFormat, allowInsecureRegistries, + offline, layerConfigurations, toolName, eventDispatcher, @@ -298,6 +311,7 @@ public static Builder builder() { private final Cache applicationLayersCache; private Class targetFormat; private final boolean allowInsecureRegistries; + private final boolean offline; private final ImmutableList layerConfigurations; private final String toolName; private final EventDispatcher eventDispatcher; @@ -313,6 +327,7 @@ private BuildConfiguration( Cache applicationLayersCache, Class targetFormat, boolean allowInsecureRegistries, + boolean offline, ImmutableList layerConfigurations, String toolName, EventDispatcher eventDispatcher, @@ -325,6 +340,7 @@ private BuildConfiguration( this.applicationLayersCache = applicationLayersCache; this.targetFormat = targetFormat; this.allowInsecureRegistries = allowInsecureRegistries; + this.offline = offline; this.layerConfigurations = layerConfigurations; this.toolName = toolName; this.eventDispatcher = eventDispatcher; @@ -395,6 +411,15 @@ public boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } + /** + * Gets whether or not to run the build in offline mode. + * + * @return {@code true} if the build will run in offline mode; {@code false} otherwise + */ + public boolean isOffline() { + return offline; + } + /** * Gets the configurations for building the layers. * diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index b65fe2d9e8..62c138de28 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 1.1.2 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a7d7a5f6ae..459993a8ff 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 1.1.2 From 61b0f182f862b4a2b257f67e365e4cb2cc755f69 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 3 May 2019 11:13:45 -0400 Subject: [PATCH 0572/2020] Update CHANGELOG for multiple extra directories (#1692) --- jib-gradle-plugin/CHANGELOG.md | 3 +++ jib-maven-plugin/CHANGELOG.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 62c138de28..7476c79bd1 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,9 +7,12 @@ All notable changes to this project will be documented in this file. - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Now supports multiple extra directories with `jib.extraDirectories.{paths|.permissions}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed +- `jib.extraDirectory({.path|.permissions})` are deprecated in favor of the new `jib.extraDirectories.{paths|.permissions}` configurations ([#1671](https://github.com/GoogleContainerTools/jib/pull/1671)) + ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 459993a8ff..7fc3e6428c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,9 +7,12 @@ All notable changes to this project will be documented in this file. - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Now supports multiple extra directories with `{|}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed +- `(|)` are deprecated in favor of the new `{|}` configurations ([#1626](https://github.com/GoogleContainerTools/jib/pull/1626)) + ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) From 5bc07f6e96a00a8bfd8e0b4157c11894e38bef34 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 3 May 2019 12:26:21 -0400 Subject: [PATCH 0573/2020] Add offline mode to plugins (#1689) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../gradle/SingleProjectIntegrationTest.java | 24 +++++++++++++++++++ .../jib/gradle/GradleProjectProperties.java | 5 ++++ jib-maven-plugin/CHANGELOG.md | 1 + .../jib/maven/MavenProjectProperties.java | 7 ++++++ .../maven/BuildImageMojoIntegrationTest.java | 20 ++++++++++++++++ .../common/PluginConfigurationProcessor.java | 1 + .../jib/plugins/common/ProjectProperties.java | 2 ++ .../PluginConfigurationProcessorTest.java | 2 ++ 9 files changed, 63 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7476c79bd1..c822a565d9 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) - Now supports multiple extra directories with `jib.extraDirectories.{paths|.permissions}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 7af925c53f..f52de2c413 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -215,6 +215,30 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertLayerSizer(8, targetImage); } + @Test + public void testBuild_failOffline() { + String targetImage = + "gcr.io/" + + IntegrationTestingConfiguration.getGCPProject() + + "/simpleimageoffline:gradle" + + System.nanoTime(); + + try { + simpleTestProject.build( + "--offline", + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + targetImage); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString("Cannot build to a container registry in offline mode")); + } + } + @Test public void testDockerDaemon_simpleOnJava11() throws DigestException, IOException, InterruptedException { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 141dba3839..8b159cd379 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -311,4 +311,9 @@ public int getMajorJavaVersion() { } return Integer.valueOf(version.getMajorVersion()); } + + @Override + public boolean isOffline() { + return project.getGradle().getStartParameter().isOffline(); + } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7fc3e6428c..f3ff2d7ccc 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) - Now supports multiple extra directories with `{|}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index fbdcbf65e6..7dc01a9b0f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -184,6 +184,7 @@ static int getVersionFromString(String versionString) { } private final MavenProject project; + private final MavenSession session; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; private final AbsoluteUnixPath appRoot; @@ -193,6 +194,7 @@ static int getVersionFromString(String versionString) { MavenProject project, MavenSession session, Log log, AbsoluteUnixPath appRoot) { this.project = project; this.appRoot = appRoot; + this.session = session; eventHandlers = makeEventHandlers(session, log, singleThreadedExecutor); } @@ -335,4 +337,9 @@ public int getMajorJavaVersion() { } return 6; // maven-compiler-plugin default is 1.6 } + + @Override + public boolean isOffline() { + return session.isOffline(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 9e720b6718..5c3134aaee 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -370,6 +370,26 @@ public void testExecute_simple() assertLayerSizer(8, targetImage); } + @Test + public void testExecute_failOffline() throws IOException { + String targetImage = getGcrImageReference("simpleimageoffline:maven"); + + // Test empty output error + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setAutoclean(false); + verifier.addCliOption("--offline"); + verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); + Assert.fail(); + + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString("Cannot build to a container registry in offline mode")); + } + } + @Test public void testExecute_simpleOnJava11() throws DigestException, VerificationException, IOException, InterruptedException { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 0d72227ffc..d3a03dda65 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -461,6 +461,7 @@ private static void configureContainerizer( RawConfiguration rawConfiguration, ProjectProperties projectProperties) { containerizer + .setOfflineMode(projectProperties.isOffline()) .setToolName(projectProperties.getToolName()) .setEventHandlers(projectProperties.getEventHandlers()) .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index e9716ddb6c..004af88d7d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -72,4 +72,6 @@ public interface ProjectProperties { String getVersion(); int getMajorJavaVersion(); + + boolean isOffline(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index a3bf273716..3ea50695f0 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -104,6 +104,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) .thenReturn(Jib.from("base")); + Mockito.when(projectProperties.isOffline()).thenReturn(false); Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) @@ -114,6 +115,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { .thenReturn(containerizer); Mockito.when(containerizer.setApplicationLayersCache(Mockito.any(Path.class))) .thenReturn(containerizer); + Mockito.when(containerizer.setOfflineMode(Mockito.anyBoolean())).thenReturn(containerizer); } @Test From fc9fce849a9fbedecd87c453134aba2c5438dc3e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 6 May 2019 16:17:02 -0400 Subject: [PATCH 0574/2020] Add FAQ for Forbidden (DENIED) / expand FAQ / update error message (#1697) --- docs/faq.md | 11 ++- .../GradleHelpfulSuggestionsBuilder.java | 4 -- .../maven/MavenHelpfulSuggestionsBuilder.java | 8 --- .../plugins/common/HelpfulSuggestions.java | 72 ++++++------------- .../ConfigurationPropertyValidatorTest.java | 4 -- .../common/HelpfulSuggestionsTest.java | 10 +-- .../plugins/common/JibBuildRunnerTest.java | 4 -- 7 files changed, 35 insertions(+), 78 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 0dc523c19e..3e12f1de37 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,6 +20,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [How can I inspect the image Jib built?](#how-can-i-inspect-the-image-jib-built)\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ +[What should I do when the registry responds with Forbidden or DENIED?](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied)\ [What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized)\ [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ @@ -344,9 +345,15 @@ See more at [Using Google Container Registry (GCR) with Minikube](https://ryanes Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). +### What should I do when the registry responds with Forbidden or DENIED? + +If the registry returns `403 Forbidden` or `"code":"DENIED"`, it often means Jib successfully authenticated using your credentials but the credentials do not have permissions to pull or push images. Make sure your account/role has the permissions to do the operation. + +Depending on registry implementations, it is also possible that the registry actually meant you are not authenticated. See [What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized) to ensure you have set up credentials correctly. + ### What should I do when the registry responds with UNAUTHORIZED? -If the registry returns "401 Unauthorized" or `"code":"UNAUTHORIZED"`, it is often due to credential misconfiguration. Examples: +If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is often due to credential misconfiguration. Examples: * You did not configure auth information in the default places where Jib searches. - `$HOME/.docker/config.json`, [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). @@ -355,8 +362,10 @@ If the registry returns "401 Unauthorized" or `"code":"UNAUTHORIZED"`, it is oft - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) for Maven / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) for Gradle - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) for Maven / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) for Gradle - These parameters can also be set through properties: [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#system-properties) / [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#system-properties) +* Different auth configurations exist in multiple places, and Jib is not picking up the auth information you are working on. * You configured a credential helper, but the helper is not on `$PATH`. This is especially common when running Jib inside IDE where the IDE binary is launched directly from an OS menu and does not have access to your shell's environment. * Typos in username, password, image names, or registry names. +* You are using a private registry without HTTPS. See [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). If you encounter issues interacting with a registry other than `UNAUTHORIZED`, check ["How can I diagnose problems pulling or pushing from remote registries?"](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java index d530000097..dfa3a6c98d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java @@ -66,12 +66,8 @@ HelpfulSuggestions build() { "gradle clean", baseImageReference, !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, - "from.credHelper", - ignored -> "from.auth", targetImageReference, !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, - "to.credHelper", - ignored -> "to.auth", "jib.to.image", "--image", "build.gradle"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java index 1efe58d463..2b8dd81be8 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java @@ -18,15 +18,11 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import java.util.function.Function; import javax.annotation.Nullable; /** Builder for Maven-specific {@link HelpfulSuggestions}. */ class MavenHelpfulSuggestionsBuilder { - private static final Function AUTH_CONFIGURATION_SUGGESTION = - registry -> "set credentials for '" + registry + "' in your Maven settings"; - private final String messagePrefix; private final JibPluginConfiguration jibPluginConfiguration; @@ -78,12 +74,8 @@ HelpfulSuggestions build() { "mvn clean", baseImageReference, !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, - "", - AUTH_CONFIGURATION_SUGGESTION, targetImageReference, !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, - "", - AUTH_CONFIGURATION_SUGGESTION, "", "-Dimage", "pom.xml"); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 4c97e99609..45c54c541d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.base.Preconditions; import java.nio.file.Path; -import java.util.function.Function; import javax.annotation.Nullable; /** Builds messages that provides suggestions on how to fix the error. */ @@ -110,12 +109,8 @@ private static String forIncompatibleBaseImageJavaVesion( private final String clearCacheCommand; @Nullable private final ImageReference baseImageReference; private final boolean noCredentialsDefinedForBaseImage; - private final String baseImageCredHelperConfiguration; - private final Function baseImageAuthConfiguration; @Nullable private final ImageReference targetImageReference; private final boolean noCredentialsDefinedForTargetImage; - private final String targetImageCredHelperConfiguration; - private final Function targetImageAuthConfiguration; private final String toImageConfiguration; private final String buildConfigurationFilename; private final String toImageFlag; @@ -128,17 +123,9 @@ private static String forIncompatibleBaseImageJavaVesion( * @param baseImageReference the base image reference * @param noCredentialsDefinedForBaseImage {@code true} if no credentials were defined for the * base image; {@code false} otherwise - * @param baseImageCredHelperConfiguration the configuration defining the credential helper name - * for the base image - * @param baseImageAuthConfiguration the way to define raw credentials for the base image - takes - * the base image registry as an argument * @param targetImageReference the target image reference * @param noCredentialsDefinedForTargetImage {@code true} if no credentials were defined for the - * base image; {@code false} otherwise - * @param targetImageCredHelperConfiguration the configuration defining the credential helper name - * for the target image - * @param targetImageAuthConfiguration the way to define raw credentials for the target image - - * takes the target image registry as an argument + * target image; {@code false} otherwise * @param toImageConfiguration the configuration defining the target image * @param toImageFlag the commandline flag used to set the target image * @param buildConfigurationFilename the filename of the build configuration @@ -148,12 +135,8 @@ public HelpfulSuggestions( String clearCacheCommand, @Nullable ImageReference baseImageReference, boolean noCredentialsDefinedForBaseImage, - String baseImageCredHelperConfiguration, - Function baseImageAuthConfiguration, @Nullable ImageReference targetImageReference, boolean noCredentialsDefinedForTargetImage, - String targetImageCredHelperConfiguration, - Function targetImageAuthConfiguration, String toImageConfiguration, String toImageFlag, String buildConfigurationFilename) { @@ -161,12 +144,8 @@ public HelpfulSuggestions( this.clearCacheCommand = clearCacheCommand; this.baseImageReference = baseImageReference; this.noCredentialsDefinedForBaseImage = noCredentialsDefinedForBaseImage; - this.baseImageCredHelperConfiguration = baseImageCredHelperConfiguration; - this.baseImageAuthConfiguration = baseImageAuthConfiguration; this.targetImageReference = targetImageReference; this.noCredentialsDefinedForTargetImage = noCredentialsDefinedForTargetImage; - this.targetImageCredHelperConfiguration = targetImageCredHelperConfiguration; - this.targetImageAuthConfiguration = targetImageAuthConfiguration; this.toImageConfiguration = toImageConfiguration; this.buildConfigurationFilename = buildConfigurationFilename; this.toImageFlag = toImageFlag; @@ -193,27 +172,33 @@ public String forCacheDirectoryNotOwned(Path cacheDirectory) { } public String forHttpStatusCodeForbidden(String imageReference) { - return suggest("make sure you have permissions for " + imageReference); + return suggest( + "make sure you have permissions for " + + imageReference + + " and set correct credentials. See " + + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied for help"); } public String forNoCredentialsDefined(String registry, String repository) { Preconditions.checkNotNull(baseImageReference); Preconditions.checkNotNull(targetImageReference); - if (noCredentialsDefinedForBaseImage - && registry.equals(baseImageReference.getRegistry()) - && repository.equals(baseImageReference.getRepository())) { - return forNoCredentialHelpersDefined( - baseImageCredHelperConfiguration, baseImageAuthConfiguration.apply(registry)); - } - if (noCredentialsDefinedForTargetImage - && registry.equals(targetImageReference.getRegistry()) - && repository.equals(targetImageReference.getRepository())) { - return forNoCredentialHelpersDefined( - targetImageCredHelperConfiguration, targetImageAuthConfiguration.apply(registry)); + + String unauthorizedEntity = registry; + if (registry.equals(baseImageReference.getRegistry()) + && repository.equals(baseImageReference.getRepository()) + && noCredentialsDefinedForBaseImage) { + unauthorizedEntity = baseImageReference.toString(); + } else if (registry.equals(targetImageReference.getRegistry()) + && repository.equals(targetImageReference.getRepository()) + && noCredentialsDefinedForTargetImage) { + unauthorizedEntity = targetImageReference.toString(); } - // Credential helper probably was not configured correctly or did not have the necessary - // credentials. - return forCredentialsNotCorrect(registry); + + return suggest( + "make sure your credentials for '" + + unauthorizedEntity + + "' are set up correctly. See " + + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help"); } public String forCredentialsNotSent() { @@ -251,17 +236,4 @@ public String none() { public String suggest(String suggestion) { return suggest(messagePrefix, suggestion); } - - private String forNoCredentialHelpersDefined( - String credHelperConfiguration, String authConfiguration) { - return suggest( - "set a credential helper name with the configuration '" - + credHelperConfiguration - + "' or " - + authConfiguration); - } - - private String forCredentialsNotCorrect(String registry) { - return suggest("make sure your credentials for '" + registry + "' are set up correctly"); - } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index a7c794dd30..517f0f82d3 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -129,12 +129,8 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept "", mockImageReference, false, - "", - unused -> "", mockImageReference, false, - "", - unused -> "", "to", "--to", "build.txt"); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index fbfd985e35..5634ac18a1 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -30,12 +30,8 @@ public class HelpfulSuggestionsTest { "clearCacheCommand", ImageReference.of("baseregistry", "baserepository", null), true, - "baseImageCredHelperConfiguration", - registry -> "baseImageAuthConfiguration " + registry, ImageReference.of("targetregistry", "targetrepository", null), false, - "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry, "toProperty", "toFlag", "buildFile"); @@ -55,13 +51,13 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should check that 'cacheDirectory' is not used by another application or set the `jib.useOnlyProjectCache` system property", TEST_HELPFUL_SUGGESTIONS.forCacheDirectoryNotOwned(Paths.get("cacheDirectory"))); Assert.assertEquals( - "messagePrefix, perhaps you should make sure you have permissions for imageReference", + "messagePrefix, perhaps you should make sure you have permissions for imageReference and set correct credentials. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied for help", TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("imageReference")); Assert.assertEquals( - "messagePrefix, perhaps you should set a credential helper name with the configuration 'baseImageCredHelperConfiguration' or baseImageAuthConfiguration baseregistry", + "messagePrefix, perhaps you should make sure your credentials for 'baseregistry/baserepository' are set up correctly. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help", TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("baseregistry", "baserepository")); Assert.assertEquals( - "messagePrefix, perhaps you should make sure your credentials for 'targetregistry' are set up correctly", + "messagePrefix, perhaps you should make sure your credentials for 'targetregistry' are set up correctly. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help", TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("targetregistry", "targetrepository")); Assert.assertEquals( "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 5fa82f8e07..68cbd16ea0 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -51,12 +51,8 @@ public class JibBuildRunnerTest { "clearCacheCommand", ImageReference.of("someregistry", "somerepository", null), false, - "baseImageCredHelperConfiguration", - registry -> "baseImageAuthConfiguration " + registry, ImageReference.of("toRegistry", "torepository", null), false, - "targetImageCredHelperConfiguration", - registry -> "targetImageAuthConfiguration " + registry, "toConfig", "toFlag", "buildFile"); From 62ab86bdc096c3f9745b8bf991851550a9a4e904 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 6 May 2019 19:13:54 -0400 Subject: [PATCH 0575/2020] Reorg and refactor code around Blob abuse (#1678) --- .../registry/BlobPullerIntegrationTest.java | 14 +-- .../ManifestPusherIntegrationTest.java | 11 +- .../cloud/tools/jib/blob/BlobDescriptor.java | 23 ---- .../google/cloud/tools/jib/blob/Blobs.java | 41 ++----- .../google/cloud/tools/jib/blob/FileBlob.java | 5 +- .../cloud/tools/jib/blob/InputStreamBlob.java | 3 +- .../google/cloud/tools/jib/blob/JsonBlob.java | 37 +++++++ .../cloud/tools/jib/blob/StringBlob.java | 5 +- ...terBlob.java => WritableContentsBlob.java} | 19 ++-- .../tools/jib/builder/steps/BuildResult.java | 7 +- .../jib/builder/steps/PullBaseImageStep.java | 2 +- .../jib/builder/steps/PushImageStep.java | 8 +- .../tools/jib/cache/CacheStorageWriter.java | 17 +-- .../tools/jib/cache/LayerEntriesSelector.java | 7 +- .../cloud/tools/jib/docker/ImageTarball.java | 8 +- .../jib/hash/CountingDigestOutputStream.java | 45 +++++--- .../google/cloud/tools/jib/hash/Digests.java | 102 ++++++++++++++++++ .../WritableContents.java} | 12 ++- .../google/cloud/tools/jib/http/Request.java | 13 +-- .../jib/image/json/ImageToJsonTranslator.java | 4 +- .../tools/jib/json/JsonTemplateMapper.java | 50 +++++---- .../tools/jib/registry/ManifestPusher.java | 11 +- .../google/cloud/tools/jib/blob/BlobTest.java | 46 ++------ .../jib/cache/CacheStorageReaderTest.java | 9 +- .../jib/cache/CacheStorageWriterTest.java | 20 ++-- .../jib/cache/LayerEntriesSelectorTest.java | 12 +-- .../json/DockerLoadManifestTemplateTest.java | 3 +- .../hash/CountingDigestOutputStreamTest.java | 8 +- .../jib/http/TestBlobProgressListener.java | 7 +- .../ContainerConfigurationTemplateTest.java | 6 +- .../image/json/ImageToJsonTranslatorTest.java | 5 +- .../image/json/OCIManifestTemplateTest.java | 6 +- .../image/json/V22ManifestTemplateTest.java | 6 +- .../jib/json/JsonTemplateMapperTest.java | 11 +- .../tools/jib/registry/BlobCheckerTest.java | 7 +- .../tools/jib/registry/BlobPullerTest.java | 2 +- .../jib/registry/ManifestPusherTest.java | 22 +--- 37 files changed, 315 insertions(+), 299 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java rename jib-core/src/main/java/com/google/cloud/tools/jib/blob/{WriterBlob.java => WritableContentsBlob.java} (59%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java rename jib-core/src/main/java/com/google/cloud/tools/jib/{blob/BlobWriter.java => hash/WritableContents.java} (63%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 1368525e41..4c46ceca3e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -54,23 +54,19 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti DescriptorDigest realDigest = manifestTemplate.getLayerDigests().get(0); // Pulls a layer BLOB of the busybox image. - CountingDigestOutputStream layerOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); LongAdder totalByteCount = new LongAdder(); LongAdder expectedSize = new LongAdder(); - registryClient - .pullBlob( + Blob pulledBlob = + registryClient.pullBlob( realDigest, size -> { Assert.assertEquals(0, expectedSize.sum()); expectedSize.add(size); }, - new TestBlobProgressListener(totalByteCount::add)) - .writeTo(layerOutputStream); + new TestBlobProgressListener(totalByteCount::add)); + Assert.assertEquals(realDigest, pulledBlob.writeTo(ByteStreams.nullOutputStream()).getDigest()); Assert.assertTrue(expectedSize.sum() > 0); Assert.assertEquals(expectedSize.sum(), totalByteCount.sum()); - - Assert.assertEquals(realDigest, layerOutputStream.toBlobDescriptor().getDigest()); } @Test diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 67a444f4c7..6be922efad 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -21,12 +21,11 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.ByteStreams; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -114,11 +113,7 @@ public void testPush() V22ManifestTemplate manifestTemplateByDigest = registryClient.pullManifest(imageDigest.toString(), V22ManifestTemplate.class); Assert.assertEquals( - JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(), - JsonTemplateMapper.toBlob(manifestTemplateByDigest) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest()); + Digests.computeJsonDigest(manifestTemplate), + Digests.computeJsonDigest(manifestTemplateByDigest)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java index 0b067cc912..ff59209998 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java @@ -16,12 +16,7 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.common.io.ByteStreams; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; /** Contains properties describing a BLOB, including its digest and possibly its size (in bytes). */ public class BlobDescriptor { @@ -31,24 +26,6 @@ public class BlobDescriptor { /** The size of the BLOB (in bytes). Negative if unknown. */ private final long size; - /** - * Creates a new {@link BlobDescriptor} from the contents of an {@link InputStream} while piping - * to an {@link OutputStream}. Does not close either streams. - * - * @param inputStream the stream to read the contents from - * @param outputStream the {@link OutputStream} to pipe to - * @return a {@link BlobDescriptor} of the piped contents - * @throws IOException if reading from or writing to the streams fails - */ - static BlobDescriptor fromPipe(InputStream inputStream, OutputStream outputStream) - throws IOException { - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(outputStream); - ByteStreams.copy(inputStream, countingDigestOutputStream); - countingDigestOutputStream.flush(); - return countingDigestOutputStream.toBlobDescriptor(); - } - public BlobDescriptor(long size, DescriptorDigest digest) { this.size = size; this.digest = digest; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 4cec2f9b7e..643572f529 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -16,13 +16,12 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.hash.WritableContents; +import com.google.cloud.tools.jib.json.JsonTemplate; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; /** Static methods for {@link Blob}. */ @@ -36,6 +35,10 @@ public static Blob from(Path file) { return new FileBlob(file); } + public static Blob from(JsonTemplate template) { + return new JsonBlob(template); + } + /** * Creates a {@link StringBlob} with UTF-8 encoding. * @@ -46,8 +49,8 @@ public static Blob from(String content) { return new StringBlob(content); } - public static Blob from(BlobWriter writer) { - return new WriterBlob(writer); + public static Blob from(WritableContents writable) { + return new WritableContentsBlob(writable); } /** @@ -74,33 +77,5 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { return byteArrayOutputStream.toByteArray(); } - /** - * Writes the BLOB to a file. - * - * @param blob the BLOB to to write - * @param blobFile the file to write to - * @return the {@link BlobDescriptor} of the written BLOB - * @throws IOException if writing the BLOB fails - */ - public static BlobDescriptor writeToFile(Blob blob, Path blobFile) throws IOException { - try (OutputStream outputStream = Files.newOutputStream(blobFile)) { - return blob.writeTo(outputStream); - } - } - - /** - * Writes the BLOB to a file with an exclusive lock. - * - * @param blob the BLOB to to write - * @param blobFile the file to write to - * @return the {@link BlobDescriptor} of the written BLOB - * @throws IOException if writing the BLOB fails - */ - public static BlobDescriptor writeToFileWithLock(Blob blob, Path blobFile) throws IOException { - try (OutputStream outputStream = FileOperations.newLockingOutputStream(blobFile)) { - return blob.writeTo(outputStream); - } - } - private Blobs() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java index 5c978c2dfb..7392f14116 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -34,8 +35,8 @@ class FileBlob implements Blob { @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - try (InputStream fileStream = new BufferedInputStream(Files.newInputStream(file))) { - return BlobDescriptor.fromPipe(fileStream, outputStream); + try (InputStream fileIn = new BufferedInputStream(Files.newInputStream(file))) { + return Digests.computeDigest(fileIn, outputStream); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java index 46273b691f..a0f709d2d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -39,7 +40,7 @@ public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { throw new IllegalStateException("Cannot rewrite Blob backed by an InputStream"); } try (InputStream inputStream = this.inputStream) { - return BlobDescriptor.fromPipe(inputStream, outputStream); + return Digests.computeDigest(inputStream, outputStream); } finally { isWritten = true; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java new file mode 100644 index 0000000000..5d463a5b27 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.blob; + +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.json.JsonTemplate; +import java.io.IOException; +import java.io.OutputStream; + +/** A {@link Blob} that holds {@link JsonTemplate}. */ +class JsonBlob implements Blob { + + private final JsonTemplate template; + + JsonBlob(JsonTemplate template) { + this.template = template; + } + + @Override + public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { + return Digests.computeDigest(template, outputStream); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java index 62e07e1297..5c5c29539f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.hash.Digests; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -33,9 +34,9 @@ class StringBlob implements Blob { @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - try (InputStream stringInputStream = + try (InputStream stringIn = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { - return BlobDescriptor.fromPipe(stringInputStream, outputStream); + return Digests.computeDigest(stringIn, outputStream); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java similarity index 59% rename from jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java index c47dc3e037..1cddad04b2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WriterBlob.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java @@ -16,25 +16,22 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.hash.WritableContents; import java.io.IOException; import java.io.OutputStream; -/** A {@link Blob} that writes with a {@link BlobWriter} function and hashes the bytes. */ -class WriterBlob implements Blob { +/** A {@link Blob} that holds {@link WritableContents}. */ +class WritableContentsBlob implements Blob { - private final BlobWriter writer; + private final WritableContents writableContents; - WriterBlob(BlobWriter writer) { - this.writer = writer; + WritableContentsBlob(WritableContents writableContents) { + this.writableContents = writableContents; } @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(outputStream); - writer.writeTo(countingDigestOutputStream); - countingDigestOutputStream.flush(); - return countingDigestOutputStream.toBlobDescriptor(); + return Digests.computeDigest(writableContents, outputStream); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index 5ffa8fc01b..836cbe1cc2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -17,11 +17,11 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.ByteStreams; import java.io.IOException; import java.util.Objects; @@ -47,10 +47,7 @@ static BuildResult fromImage(Image image, Class toSortedJsonTemplates(List layerEntr */ static DescriptorDigest generateSelector(ImmutableList layerEntries) throws IOException { - return JsonTemplateMapper.toBlob(toSortedJsonTemplates(layerEntries)) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + return Digests.computeJsonDigest(toSortedJsonTemplates(layerEntries)); } private LayerEntriesSelector() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 2a595f923e..62ae015650 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; @@ -77,10 +78,9 @@ public void writeTo(OutputStream out) throws IOException { // Adds the manifest to tarball. manifestTemplate.setRepoTags(imageReference.toStringWithTag()); - tarStreamBuilder.addByteEntry( - Blobs.writeToByteArray( - JsonTemplateMapper.toBlob(Collections.singletonList(manifestTemplate))), - MANIFEST_JSON_FILE_NAME); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + JsonTemplateMapper.writeTo(Collections.singletonList(manifestTemplate), outStream); + tarStreamBuilder.addByteEntry(outStream.toByteArray(), MANIFEST_JSON_FILE_NAME); tarStreamBuilder.writeAsTarArchiveTo(out); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java index b2e872c2a2..12cf3ebd65 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java @@ -16,22 +16,27 @@ package com.google.cloud.tools.jib.hash; -import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.common.base.Verify; import java.io.IOException; import java.io.OutputStream; import java.security.DigestException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import javax.annotation.Nullable; /** A {@link DigestOutputStream} that also keeps track of the total number of bytes written. */ public class CountingDigestOutputStream extends DigestOutputStream { private static final String SHA_256_ALGORITHM = "SHA-256"; - /** Keeps track of the total number of bytes appended. */ - private long totalBytes = 0; + private long bytesSoFar = 0; + + /** The total number of bytes used to compute a digest. Resets when {@link computeDigest) is called. */ + private long bytesHashed = 0; + + @Nullable private DescriptorDigest descriptorDigest; /** * Wraps the {@code outputStream}. @@ -49,12 +54,10 @@ public CountingDigestOutputStream(OutputStream outputStream) { } /** - * Builds a {@link BlobDescriptor} with the hash and size of the bytes written. The buffer resets - * after this method is called, so this method should only be called once per BlobDescriptor. - * - * @return the built {@link BlobDescriptor}. + * Computes the hash and remembers the size of the bytes written to compute the hash. The buffer + * resets after this method is called, so this method should only be called once per computation. */ - public BlobDescriptor toBlobDescriptor() { + public void computeDigest() { try { byte[] hashedBytes = digest.digest(); @@ -65,28 +68,40 @@ public BlobDescriptor toBlobDescriptor() { } String hash = stringBuilder.toString(); - DescriptorDigest digest = DescriptorDigest.fromHash(hash); - return new BlobDescriptor(totalBytes, digest); + bytesHashed = bytesSoFar; + descriptorDigest = DescriptorDigest.fromHash(hash); + bytesSoFar = 0; } catch (DigestException ex) { throw new RuntimeException("SHA-256 algorithm produced invalid hash: " + ex.getMessage(), ex); } } - /** @return the total number of bytes that were hashed */ - public long getTotalBytes() { - return totalBytes; + /** @return the number of bytes written and used to compute the most recent digest */ + public long getBytesHahsed() { + if (descriptorDigest == null) { + computeDigest(); + } + return bytesHashed; + } + + /** @return the most recently computed digest hash */ + public DescriptorDigest getDigest() { + if (descriptorDigest == null) { + computeDigest(); + } + return Verify.verifyNotNull(descriptorDigest); } @Override public void write(byte[] data, int offset, int length) throws IOException { super.write(data, offset, length); - totalBytes += length; + bytesSoFar += length; } @Override public void write(int singleByte) throws IOException { super.write(singleByte); - totalBytes++; + bytesSoFar++; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java new file mode 100644 index 0000000000..e5b0afca0d --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.hash; + +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * Utility class for computing a digest for various inputs while optionally writing to an output + * stream. + */ +// Note: intentionally this class does not depend on Blob, as Blob classes depend on this class. +// TODO: BlobDescriptor is merely a tuple of (size, digest). Rename BlobDescriptor to something +// more general. +public class Digests { + + public static DescriptorDigest computeJsonDigest(JsonTemplate template) throws IOException { + return computeDigest(template, ByteStreams.nullOutputStream()).getDigest(); + } + + public static DescriptorDigest computeJsonDigest(List templates) + throws IOException { + WritableContents contents = contentsOut -> JsonTemplateMapper.writeTo(templates, contentsOut); + return computeDigest(contents, ByteStreams.nullOutputStream()).getDigest(); + } + + public static BlobDescriptor computeDigest(JsonTemplate template, OutputStream optionalOutStream) + throws IOException { + WritableContents contents = contentsOut -> JsonTemplateMapper.writeTo(template, contentsOut); + return computeDigest(contents, optionalOutStream); + } + + public static BlobDescriptor computeDigest(InputStream inStream) throws IOException { + return computeDigest(inStream, ByteStreams.nullOutputStream()); + } + + /** + * Computes the digest by consuming the contents. + * + * @param contents the contents for which the digest is computed + * @return computed digest and bytes consumed + * @throws IOException if reading fails + */ + public static BlobDescriptor computeDigest(WritableContents contents) throws IOException { + return computeDigest(contents, ByteStreams.nullOutputStream()); + } + + /** + * Computes the digest by consuming the contents of an {@link InputStream} and optionally copying + * it to an {@link OutputStream}. Returns the computed digest along with the size of the bytes + * consumed to compute the digest. Does not close either stream. + * + * @param inStream the stream to read the contents from + * @param optionalOutStream the stream to which the contents are copied + * @return computed digest and bytes consumed + * @throws IOException if reading from or writing fails + */ + public static BlobDescriptor computeDigest(InputStream inStream, OutputStream optionalOutStream) + throws IOException { + WritableContents contents = contentsOut -> ByteStreams.copy(inStream, contentsOut); + return computeDigest(contents, optionalOutStream); + } + + /** + * Computes the digest by consuming the contents and optionally copying it to an {@link + * OutputStream}. Returns the computed digest along with the size of the bytes consumed to compute + * the digest. Does not close the stream. + * + * @param contents the contents to compute digest for + * @param optionalOutStream the stream to which the contents are copied + * @return computed digest and bytes consumed + * @throws IOException if reading from or writing fails + */ + public static BlobDescriptor computeDigest( + WritableContents contents, OutputStream optionalOutStream) throws IOException { + CountingDigestOutputStream digestOutStream = new CountingDigestOutputStream(optionalOutStream); + contents.writeTo(digestOutStream); + digestOutStream.flush(); + return new BlobDescriptor(digestOutStream.getBytesHahsed(), digestOutStream.getDigest()); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java similarity index 63% rename from jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java index acf393a524..5fc4fae624 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC. + * Copyright 2019 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -14,17 +14,19 @@ * the License. */ -package com.google.cloud.tools.jib.blob; +package com.google.cloud.tools.jib.hash; import java.io.IOException; import java.io.OutputStream; /** - * This function writes the contents of a BLOB. This function should write the same BLOB regardless - * of the number of times it is called. + * As a function, writes some contents to an output stream. As a class, represents contents that can + * be written to an output stream. This may be "unrealized-before-write" contents; for example, a + * file may be open and read for input contents only when this function is called to write to an + * output stream. */ @FunctionalInterface -public interface BlobWriter { +public interface WritableContents { void writeTo(OutputStream outputStream) throws IOException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java index e6ef54f7f0..88631a5546 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.http; +import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpHeaders; import java.util.List; import javax.annotation.Nullable; @@ -27,7 +28,7 @@ public class Request { private final HttpHeaders headers; /** The HTTP request body. */ - @Nullable private final BlobHttpContent body; + @Nullable private final HttpContent body; /** HTTP connection and read timeout. */ @Nullable private final Integer httpTimeout; @@ -35,7 +36,7 @@ public class Request { public static class Builder { private final HttpHeaders headers = new HttpHeaders().setAccept("*/*"); - @Nullable private BlobHttpContent body; + @Nullable private HttpContent body; @Nullable private Integer httpTimeout; public Request build() { @@ -92,11 +93,11 @@ public Builder setHttpTimeout(@Nullable Integer httpTimeout) { /** * Sets the body and its corresponding {@code Content-Type} header. * - * @param blobHttpContent the body content + * @param httpContent the body content * @return this */ - public Builder setBody(@Nullable BlobHttpContent blobHttpContent) { - this.body = blobHttpContent; + public Builder setBody(@Nullable HttpContent httpContent) { + this.body = httpContent; return this; } } @@ -116,7 +117,7 @@ HttpHeaders getHeaders() { } @Nullable - BlobHttpContent getHttpContent() { + HttpContent getHttpContent() { return body; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 18cd58e893..c0a07bd298 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -18,13 +18,13 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -189,7 +189,7 @@ public Blob getContainerConfigurationBlob() { } // Serializes into JSON. - return JsonTemplateMapper.toBlob(template); + return Blobs.from(template); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index 2ff9e4dbe5..c5f1c5f501 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -16,12 +16,14 @@ package com.google.cloud.tools.jib.json; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.CollectionType; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.Files; @@ -117,28 +119,36 @@ public static List readListOfJson( return objectMapper.readValue(jsonString, listType); } - /** - * Convert a {@link JsonTemplate} to a {@link Blob} of the JSON string. - * - * @param template the JSON template to convert - * @return a {@link Blob} of the JSON string - */ - public static Blob toBlob(JsonTemplate template) { - return toBlob((Object) template); + public static String toUtf8String(JsonTemplate template) + throws JsonGenerationException, JsonMappingException, IOException { + return toUtf8String((Object) template); } - /** - * Convert a list of {@link JsonTemplate} to a {@link Blob} of the JSON string. - * - * @param templates the list of JSON templates to convert - * @return a {@link Blob} of the JSON string - */ - public static Blob toBlob(List templates) { - return toBlob((Object) templates); + public static String toUtf8String(List templates) + throws JsonGenerationException, JsonMappingException, IOException { + return toUtf8String((Object) templates); + } + + public static void writeTo(JsonTemplate template, OutputStream out) + throws JsonGenerationException, JsonMappingException, IOException { + writeTo((Object) template, out); + } + + public static void writeTo(List templates, OutputStream out) + throws JsonGenerationException, JsonMappingException, IOException { + writeTo((Object) templates, out); + } + + private static String toUtf8String(Object template) + throws JsonGenerationException, JsonMappingException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeTo(template, out); + return out.toString("UTF-8"); } - private static Blob toBlob(Object template) { - return Blobs.from(outputStream -> objectMapper.writeValue(outputStream, template)); + private static void writeTo(Object template, OutputStream out) + throws JsonGenerationException, JsonMappingException, IOException { + objectMapper.writeValue(out, template); } private JsonTemplateMapper() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 87834575ba..eb5c9dc15f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -18,14 +18,14 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.ByteStreams; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -82,7 +82,7 @@ private static String makeUnexpectedImageDigestWarning( @Override public BlobHttpContent getContent() { return new BlobHttpContent( - JsonTemplateMapper.toBlob(manifestTemplate), + Blobs.from(manifestTemplate), manifestTemplate.getManifestMediaType(), // TODO: Consider giving progress on manifest push as well? null); @@ -125,10 +125,7 @@ public DescriptorDigest handleHttpResponseException(HttpResponseException httpRe @Override public DescriptorDigest handleResponse(Response response) throws IOException { // Checks if the image digest is as expected. - DescriptorDigest expectedDigest = - JsonTemplateMapper.toBlob(manifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedDigest = Digests.computeJsonDigest(manifestTemplate); List receivedDigests = response.getHeader(RESPONSE_DIGEST_HEADER); if (receivedDigests.size() == 1) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index 623068664d..e31f075dc5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.hash.WritableContents; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; @@ -29,10 +30,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; /** Tests for {@link Blob}. */ public class BlobTest { @@ -58,42 +57,13 @@ public void testFromString() throws IOException { } @Test - public void testFromBlobWriter() throws IOException { + public void testFromWritableContents() throws IOException { String expected = "crepecake"; - BlobWriter writer = + WritableContents writableContents = outputStream -> outputStream.write(expected.getBytes(StandardCharsets.UTF_8)); - verifyBlobWriteTo(expected, Blobs.from(writer)); - } - - @Test - public void testWriteToFileWithLock_newFile() throws IOException { - String expected = "crepecake"; - Path blobFile = Files.createTempFile("blob", "bin"); - Assert.assertTrue(Files.deleteIfExists(blobFile)); // ensure it doesn't exist - - Blobs.writeToFileWithLock(Blobs.from(expected), blobFile); - - Assert.assertTrue(Files.exists(blobFile)); - verifyBlobWriteTo(expected, Blobs.from(blobFile)); - } - - @Test - public void testWriteToFileWithLock_existingFile() throws IOException { - Path blobFile = Files.createTempFile("blob", "bin"); - // write out more bytes to ensure properly truncated - byte[] dataBytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - Files.write(blobFile, dataBytes, StandardOpenOption.WRITE); - Assert.assertTrue(Files.exists(blobFile)); - Assert.assertEquals(10, Files.size(blobFile)); - - String expected = "crepecake"; - Blobs.writeToFileWithLock(Blobs.from(expected), blobFile); - - Assert.assertTrue(Files.exists(blobFile)); - Assert.assertEquals(9, Files.size(blobFile)); - verifyBlobWriteTo(expected, Blobs.from(blobFile)); + verifyBlobWriteTo(expected, Blobs.from(writableContents)); } /** Checks that the {@link Blob} streams the expected string. */ @@ -107,10 +77,8 @@ private void verifyBlobWriteTo(String expected, Blob blob) throws IOException { byte[] expectedBytes = expected.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(expectedBytes.length, blobDescriptor.getSize()); - CountingDigestOutputStream countingDigestOutputStream = - new CountingDigestOutputStream(Mockito.mock(OutputStream.class)); - countingDigestOutputStream.write(expectedBytes); - DescriptorDigest expectedDigest = countingDigestOutputStream.toBlobDescriptor().getDigest(); + DescriptorDigest expectedDigest = + Digests.computeDigest(new ByteArrayInputStream(expectedBytes)).getDigest(); Assert.assertEquals(expectedDigest, blobDescriptor.getDigest()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 437f14aa0b..25b0345605 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; @@ -25,7 +24,9 @@ import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.io.Resources; import java.io.IOException; +import java.io.OutputStream; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -173,9 +174,11 @@ public void testRetrieve() throws IOException, CacheCorruptedException { // Creates the test layer directory. DescriptorDigest layerDigest = layerDigest1; DescriptorDigest layerDiffId = layerDigest2; - Blob layerBlob = Blobs.from("layerBlob"); Files.createDirectories(cacheStorageFiles.getLayerDirectory(layerDigest)); - Blobs.writeToFileWithLock(layerBlob, cacheStorageFiles.getLayerFile(layerDigest, layerDiffId)); + try (OutputStream out = + Files.newOutputStream(cacheStorageFiles.getLayerFile(layerDigest, layerDiffId))) { + out.write("layerBlob".getBytes(StandardCharsets.UTF_8)); + } // Checks that the CachedLayer is retrieved correctly. Optional optionalCachedLayer = cacheStorageReader.retrieve(layerDigest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index ea7de8d59c..c7a1f58d81 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.InvalidImageReferenceException; @@ -46,15 +45,8 @@ /** Tests for {@link CacheStorageWriter}. */ public class CacheStorageWriterTest { - private static DescriptorDigest getDigest(Blob blob) throws IOException { - return blob.writeTo(new CountingDigestOutputStream(ByteStreams.nullOutputStream())).getDigest(); - } - - private static BlobDescriptor getCompressedBlobDescriptor(Blob blob) throws IOException { - CountingDigestOutputStream compressedDigestOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream()); - compress(blob).writeTo(compressedDigestOutputStream); - return compressedDigestOutputStream.toBlobDescriptor(); + private static BlobDescriptor getDigest(Blob blob) throws IOException { + return blob.writeTo(ByteStreams.nullOutputStream()); } private static Blob compress(Blob blob) { @@ -94,8 +86,8 @@ public void testWrite_compressed() throws IOException { @Test public void testWrite_uncompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); - DescriptorDigest layerDigest = getCompressedBlobDescriptor(uncompressedLayerBlob).getDigest(); - DescriptorDigest selector = getDigest(Blobs.from("selector")); + DescriptorDigest layerDigest = getDigest(compress(uncompressedLayerBlob)).getDigest(); + DescriptorDigest selector = getDigest(Blobs.from("selector")).getDigest(); CachedLayer cachedLayer = new CacheStorageWriter(cacheStorageFiles) @@ -167,8 +159,8 @@ public void testWriteMetadata_v22() private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { - BlobDescriptor layerBlobDescriptor = getCompressedBlobDescriptor(uncompressedLayerBlob); - DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob); + BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); + DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob).getDigest(); // Verifies cachedLayer is correct. Assert.assertEquals(layerBlobDescriptor.getDigest(), cachedLayer.getDigest()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index b8181d2e55..3089ce42eb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -19,11 +19,10 @@ import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; -import com.google.common.io.ByteStreams; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -108,10 +107,7 @@ public void testToSortedJsonTemplates() throws IOException { @Test public void testGenerateSelector_empty() throws IOException { - DescriptorDigest expectedSelector = - JsonTemplateMapper.toBlob(ImmutableList.of()) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedSelector = Digests.computeJsonDigest(ImmutableList.of()); Assert.assertEquals( expectedSelector, LayerEntriesSelector.generateSelector(ImmutableList.of())); } @@ -119,9 +115,7 @@ public void testGenerateSelector_empty() throws IOException { @Test public void testGenerateSelector() throws IOException { DescriptorDigest expectedSelector = - JsonTemplateMapper.toBlob(toLayerEntryTemplates(inOrderLayerEntries)) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + Digests.computeJsonDigest(toLayerEntryTemplates(inOrderLayerEntries)); Assert.assertEquals( expectedSelector, LayerEntriesSelector.generateSelector(outOfOrderLayerEntries)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java index 8669f6467f..2f45ed5f53 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.docker.json; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; @@ -48,6 +47,6 @@ public void testToJson() throws URISyntaxException, IOException { template.addLayerFile("layer3.tar.gz"); List loadManifest = Collections.singletonList(template); - Assert.assertEquals(expectedJson, Blobs.writeToString(JsonTemplateMapper.toBlob(loadManifest))); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(loadManifest)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index a2b1b40f7e..b4901f2615 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.hash; -import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; @@ -57,10 +56,9 @@ public void test_smokeTest() throws IOException, DigestException { InputStream toHashInputStream = new ByteArrayInputStream(bytesToHash); ByteStreams.copy(toHashInputStream, countingDigestOutputStream); - BlobDescriptor expectedBlobDescriptor = - new BlobDescriptor(bytesToHash.length, DescriptorDigest.fromHash(expectedHash)); - Assert.assertEquals(expectedBlobDescriptor, countingDigestOutputStream.toBlobDescriptor()); - Assert.assertEquals(bytesToHash.length, countingDigestOutputStream.getTotalBytes()); + Assert.assertEquals( + DescriptorDigest.fromHash(expectedHash), countingDigestOutputStream.getDigest()); + Assert.assertEquals(bytesToHash.length, countingDigestOutputStream.getBytesHahsed()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java index e0e24d0874..3fd4a0adfe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java @@ -23,7 +23,7 @@ * Test implementation of {@link BlobProgressListener} that always forwards to a {@link Consumer * }. */ -public class TestBlobProgressListener implements BlobProgressListener, Consumer { +public class TestBlobProgressListener implements BlobProgressListener { private final Consumer byteCountConsumer; @@ -40,9 +40,4 @@ public void handleByteCount(long byteCount) { public Duration getDelayBetweenCallbacks() { return Duration.ofSeconds(-1); } - - @Override - public void accept(Long byteCount) { - handleByteCount(byteCount); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index 98366aacaa..e0e5423b16 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -91,10 +90,7 @@ public void testToJson() throws IOException, URISyntaxException, DigestException .build()); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(containerConfigJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfigJson)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index cd33ceed44..51045a449a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -190,9 +190,6 @@ private void testGetManifest( T manifestTemplate = imageToJsonTranslator.getManifestTemplate(manifestTemplateClass, blobDescriptor); - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestTemplate).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, new String(jsonStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestTemplate)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java index 40ff8e4fc1..7c7defd779 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -53,10 +52,7 @@ public void testToJson() throws DigestException, IOException, URISyntaxException "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index 58eb1a3486..c8d0ba8d55 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -53,10 +52,7 @@ public void testToJson() throws DigestException, IOException, URISyntaxException "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(manifestJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 1da54bad3d..4d2ea955c5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -85,10 +84,7 @@ public void testWriteJson() throws DigestException, IOException, URISyntaxExcept "sha256:d38f571aa1c11e3d516e0ef7e513e7308ccbeb869770cb8c4319d63b10a0075e")); testJson.list = Arrays.asList(innerObject1, innerObject2); - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(testJson).writeTo(jsonStream); - - Assert.assertEquals(expectedJson, jsonStream.toString()); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(testJson)); } @Test @@ -155,9 +151,6 @@ public void testToBlob_listOfJson() throws IOException, URISyntaxException { String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); List listOfJson = JsonTemplateMapper.readListOfJson(jsonString, TestJson.class); - ByteArrayOutputStream jsonStream = new ByteArrayOutputStream(); - JsonTemplateMapper.toBlob(listOfJson).writeTo(jsonStream); - - Assert.assertEquals(jsonString, jsonStream.toString()); + Assert.assertEquals(jsonString, JsonTemplateMapper.toUtf8String(listOfJson)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 2ec250c417..a45833830e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -19,7 +19,6 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -92,7 +91,7 @@ public void testHandleHttpResponseException() throws IOException, RegistryErrorE new ErrorResponseTemplate() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")); Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); BlobDescriptor blobDescriptor = testBlobChecker.handleHttpResponseException(mockHttpResponseException); @@ -112,7 +111,7 @@ public void testHandleHttpResponseException_hasOtherErrors() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")) .addError(new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "some message")); Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { testBlobChecker.handleHttpResponseException(mockHttpResponseException); @@ -132,7 +131,7 @@ public void testHandleHttpResponseException_notBlobUnknown() ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate(); Mockito.when(mockHttpResponseException.getContent()) - .thenReturn(Blobs.writeToString(JsonTemplateMapper.toBlob(emptyErrorResponseTemplate))); + .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { testBlobChecker.handleHttpResponseException(mockHttpResponseException); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index e0eb9311cd..a21f4bd725 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -87,7 +87,7 @@ public void testHandleResponse() throws IOException, UnexpectedBlobDigestExcepti Assert.assertEquals( "some BLOB content", new String(layerContentOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals(testBlobDigest, layerOutputStream.toBlobDescriptor().getDigest()); + Assert.assertEquals(testBlobDigest, layerOutputStream.getDigest()); Assert.assertEquals("some BLOB content".length(), byteCount.sum()); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 57b8fdb0f8..da0bae510f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -20,12 +20,12 @@ import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -90,10 +90,7 @@ public void testGetContent() throws IOException { @Test public void testHandleResponse_valid() throws IOException { - DescriptorDigest expectedDigest = - JsonTemplateMapper.toBlob(fakeManifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) .thenReturn(Collections.singletonList(expectedDigest.toString())); Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); @@ -101,10 +98,7 @@ public void testHandleResponse_valid() throws IOException { @Test public void testHandleResponse_noDigest() throws IOException { - DescriptorDigest expectedDigest = - JsonTemplateMapper.toBlob(fakeManifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) .thenReturn(Collections.emptyList()); @@ -115,10 +109,7 @@ public void testHandleResponse_noDigest() throws IOException { @Test public void testHandleResponse_multipleDigests() throws IOException { - DescriptorDigest expectedDigest = - JsonTemplateMapper.toBlob(fakeManifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) .thenReturn(Arrays.asList("too", "many")); @@ -130,10 +121,7 @@ public void testHandleResponse_multipleDigests() throws IOException { @Test public void testHandleResponse_invalidDigest() throws IOException { - DescriptorDigest expectedDigest = - JsonTemplateMapper.toBlob(fakeManifestTemplate) - .writeTo(ByteStreams.nullOutputStream()) - .getDigest(); + DescriptorDigest expectedDigest = Digests.computeJsonDigest(fakeManifestTemplate); Mockito.when(mockResponse.getHeader("Docker-Content-Digest")) .thenReturn(Collections.singletonList("not valid")); From af7fa3c3df18c95ef7036a507af395fd6180e98c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 7 May 2019 09:30:31 -0400 Subject: [PATCH 0576/2020] Fix lock file failures (#1696) --- .../cloud/tools/jib/filesystem/LockFile.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java index c1a565f7ef..08711ec27b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java @@ -20,6 +20,7 @@ import java.io.Closeable; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Path; @@ -34,10 +35,12 @@ public class LockFile implements Closeable { private final Path lockFile; private final FileLock fileLock; + private final OutputStream outputStream; - private LockFile(Path lockFile, FileLock fileLock) { + private LockFile(Path lockFile, FileLock fileLock, OutputStream outputStream) { this.lockFile = lockFile; this.fileLock = fileLock; + this.outputStream = outputStream; } /** @@ -58,8 +61,17 @@ public static LockFile lock(Path lockFile) throws IOException { } Files.createDirectories(lockFile.getParent()); - FileLock fileLock = new FileOutputStream(lockFile.toFile()).getChannel().lock(); - return new LockFile(lockFile, fileLock); + FileOutputStream outputStream = new FileOutputStream(lockFile.toFile()); + FileLock fileLock = null; + try { + fileLock = outputStream.getChannel().lock(); + return new LockFile(lockFile, fileLock, outputStream); + + } finally { + if (fileLock == null) { + outputStream.close(); + } + } } /** Releases the lock file. */ @@ -67,17 +79,13 @@ public static LockFile lock(Path lockFile) throws IOException { public void close() { try { fileLock.release(); + outputStream.close(); } catch (IOException ex) { throw new IllegalStateException("Unable to release lock", ex); } finally { - try { - Preconditions.checkNotNull(lockMap.get(lockFile)).unlock(); - Files.delete(lockFile); - - } catch (IllegalMonitorStateException | IOException ignored) { - } + Preconditions.checkNotNull(lockMap.get(lockFile)).unlock(); } } } From c49458a18b2cfe3dda55e0b4d59d462557ffb2ae Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 7 May 2019 13:02:20 -0400 Subject: [PATCH 0577/2020] Core release v0.9.2 (#1701) * [Gradle Release Plugin] - pre tag commit: 'v0.9.2-core'. * [Gradle Release Plugin] - new version commit: 'v0.9.3-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 36fff9cf0c..7972ba60d5 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.9.2-SNAPSHOT +version = 0.9.3-SNAPSHOT From 841f0d0074a6f7716b654aa62e7a6f83a1418cfc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 May 2019 14:24:44 -0400 Subject: [PATCH 0578/2020] Gradle release v1.2.0 (#1700) * [Gradle Release Plugin] - pre tag commit: 'v1.2.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.2.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index bc06e9292a..f0feddb6d2 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.1.3-SNAPSHOT +version = 1.2.1-SNAPSHOT From 264d3a098ee6cea1683e18e6d3618ad4561b2079 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 May 2019 14:24:57 -0400 Subject: [PATCH 0579/2020] Maven release v1.2.0 (#1698) * preparing release 1.2.0 * 1.2.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index c35dbc9c80..8c4e3ee73e 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.3-SNAPSHOT + 1.2.1-SNAPSHOT maven-plugin Jib From bc32d6e953babc996f7a4c2d7a02e9dab2be2a96 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 May 2019 14:25:39 -0400 Subject: [PATCH 0580/2020] Update extra directories usage, merge after 1.2.0 release (#1693) --- docs/index.md | 7 ------- jib-gradle-plugin/README.md | 24 ++++++++++++------------ jib-maven-plugin/README.md | 31 ++++++++++++++++++------------- 3 files changed, 30 insertions(+), 32 deletions(-) delete mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 4eb628bb0a..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Documentation for older versions: - -jib-maven-plugin -- [v0.1.7](https://github.com/GoogleContainerTools/jib/blob/03d42652e82c543d0a6c6b69bd8827ec8effd82d/docs/v0.1.7/jib-maven-plugin.md) - -jib-gradle-plugin -- [v0.1.1](https://github.com/GoogleContainerTools/jib/blob/03d42652e82c543d0a6c6b69bd8827ec8effd82d/docs/v0.1.1/jib-gradle-plugin.md) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index d5fea4589b..37711f451a 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -175,7 +175,7 @@ Field | Type | Default | Description `to` | [`to`](#to-closure) | *Required* | Configures the target image to build your application to. `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. -`extraDirectory` | [`extraDirectory`](#extradirectory-closure) / `File` | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. +`extraDirectories` | [`extraDirectories`](#extradirectories-closure) | See [`extraDirectories`](#extradirectories-closure) | Configures the directories used to add arbitrary files to the image. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `from` is a closure with the following properties: @@ -220,12 +220,12 @@ Property | Type | Default | Description `volumes` | `List` | *None* | Specifies a list of mount points on the container. `workingDirectory` | `String` | *None* | The working directory in the container. -`extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): +`extraDirectories` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): -Property | Type ---- | --- -`path` | `File` -`permissions` | `Map` +Property | Type | Default | Description +--- | --- | --- | --- +`paths` | `Object` | `(project-dir)/src/main/jib` | Extra directories acceptable by [`Project.files()`](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#files-java.lang.Object...-), such as `String`, `File`, `Path`, `List`, etc. Can be absolute or relative to the project root. +`permissions` | `Map` | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. **(`jibDockerBuild` only)** `dockerClient` is an object that can be configured directly on the `jibDockerBuild` task, and has the following properties: @@ -300,20 +300,20 @@ jib { You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). -You can configure a different directory by using the `jib.extraDirectory` parameter in your `build.gradle`: +You can configure different directories by using the `jib.extraDirectories.paths` parameter in your `build.gradle`: ```groovy jib { - // Copies files from 'src/main/custom-extra-dir' instead of 'src/main/jib' - extraDirectory = file('src/main/custom-extra-dir') + // Copies files from 'src/main/custom-extra-dir' and '/home/user/jib-extras' instead of 'src/main/jib' + extraDirectories.paths = ['src/main/custom-extra-dir', '/home/user/jib-extras'] } ``` -Alternatively, the `jib.extraDirectory` parameter can be used as a closure to set a custom extra directory, as well as the extra files' permissions on the container: +Alternatively, the `jib.extraDirectories` parameter can be used as a closure to set custom extra directories, as well as the extra files' permissions on the container: ```groovy jib { - extraDirectory { - path = file('src/main/custom-extra-dir') // Copies files from 'src/main/custom-extra-dir' + extraDirectories { + paths = 'src/main/custom-extra-dir' // Copies files from 'src/main/custom-extra-dir' permissions = [ '/path/on/container/to/fileA': '755', // Read/write/execute for owner, read/execute for group/other '/path/to/another/file': '644' // Read/write for owner, read-only for group/other diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 76bfd429fb..2c271e335b 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -220,7 +220,7 @@ Field | Type | Default | Description `to` | [`to`](#to-object) | *Required* | Configures the target image to build your application to. `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. -`extraDirectory` | [`extraDirectory`](#extradirectory-object) / string | `(project-dir)/src/main/jib` | Configures the directory used to add arbitrary files to the image. +`extraDirectories` | [`extraDirectories`](#extradirectories-object) | See [`extraDirectories`](#extradirectories-object) | Configures the directories used to add arbitrary files to the image. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. @@ -266,12 +266,12 @@ Property | Type | Default | Description `volumes` | list | *None* | Specifies a list of mount points on the container. `workingDirectory` | string | *None* | The working directory in the container. -`extraDirectory` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): +`extraDirectories` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): -Property | Type ---- | --- -`path` | string -`permissions` | list +Property | Type | Default | Description +--- | --- | --- | --- +`paths` | list | `[(project-dir)/src/main/jib]` | List of extra directories. Can be absolute or relative to the project root. +`permissions` | list | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. **(`jib:dockerBuild` only)** `dockerClient` is an object with the following properties: @@ -362,20 +362,25 @@ In this configuration, the image: You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). -You can configure a different directory by using the `` parameter in your `pom.xml`: +You can configure different directories by using the `` parameter in your `pom.xml`: ```xml - - ${project.basedir}/src/main/custom-extra-dir + + + + src/main/custom-extra-dir + /home/user/jib-extras + + ``` -Alternatively, the `` parameter can be used as an object to set a custom extra directory, as well as the extra files' permissions on the container: +Alternatively, the `` parameter can be used as an object to set custom extra directories, as well as the extra files' permissions on the container: ```xml - - ${project.basedir}/src/main/custom-extra-dir + + src/main/custom-extra-dir /path/on/container/to/fileA @@ -386,7 +391,7 @@ Alternatively, the `` parameter can be used as an object to set 644 - + ``` From 03e5e0186864107c36aba148a7113fda7f1fee30 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 7 May 2019 14:25:54 -0400 Subject: [PATCH 0581/2020] Reapply: Docs for extraClasspath (#1645) (#1675) --- jib-gradle-plugin/README.md | 1 + jib-maven-plugin/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 37711f451a..ce0bb1b98d 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -210,6 +210,7 @@ Property | Type | Default | Description `args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | `List` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. `labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 2c271e335b..886a7e8f65 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -256,6 +256,7 @@ Property | Type | Default | Description `args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. `entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `INHERIT` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). +`extraClasspath` | `list` | *None* | Additional paths in the container to prepend to the computed Java classpath. `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). From 68473ed70e9295167dbec738541763843158c926 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 May 2019 14:43:30 -0400 Subject: [PATCH 0582/2020] No maven debug output (#1699) * Don't run Maven with debug output * No Maven debug output * No Maven debug output * Show errors (-e) --- jib-maven-plugin/scripts/prepare_release.sh | 2 +- kokoro/continuous.bat | 2 +- kokoro/continuous.sh | 2 +- kokoro/presubmit.bat | 2 +- kokoro/presubmit.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index 9ac075555b..23f0133299 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -52,7 +52,7 @@ if [[ $(git status -uno --porcelain) ]]; then fi # Runs integration tests. -./mvnw -X -PintegrationTest verify +./mvnw -e -PintegrationTest verify # Checks out a new branch for this version release (eg. 1.5.7). BRANCH=maven_release_v${VERSION} diff --git a/kokoro/continuous.bat b/kokoro/continuous.bat index d0cb4542ee..949622bea1 100755 --- a/kokoro/continuous.bat +++ b/kokoro/continuous.bat @@ -15,7 +15,7 @@ set JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing REM TODO: Enable integration tests once docker works (b/73345382). cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -X && ^ +cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -e && ^ cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace exit /b %ERRORLEVEL% diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index dfd137a492..bc3b5e3f5d 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -32,5 +32,5 @@ fi (cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd github/jib/jib-plugins-common; ./gradlew clean build --info --stacktrace) -(cd github/jib/jib-maven-plugin; ./mvnw clean install -PintegrationTest -B -U -X) +(cd github/jib/jib-maven-plugin; ./mvnw clean install -PintegrationTest -B -U -e) (cd github/jib/jib-gradle-plugin; ./gradlew clean build integrationTest --info --stacktrace) diff --git a/kokoro/presubmit.bat b/kokoro/presubmit.bat index c66c6833ac..7dab7f73e5 100755 --- a/kokoro/presubmit.bat +++ b/kokoro/presubmit.bat @@ -11,7 +11,7 @@ REM FOR /f "tokens=*" %%i IN ('docker ps -aq') DO docker rm -vf %%i cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -X && ^ +cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -e && ^ cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace exit /b %ERRORLEVEL% diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index 3a4770dbbb..f84c885fbe 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -30,5 +30,5 @@ rm /usr/local/bin/docker-credential-osxkeychain || true (cd jib-core; ./gradlew clean build integrationTest --info --stacktrace) (cd jib-plugins-common; ./gradlew clean build --info --stacktrace) -(cd jib-maven-plugin; ./mvnw clean install -B -U -X) +(cd jib-maven-plugin; ./mvnw clean install -B -U -e) (cd jib-gradle-plugin; ./gradlew clean build --info --stacktrace) From 3af05cd960ec6ec2318b2b97a749c2237585b566 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 7 May 2019 15:14:50 -0400 Subject: [PATCH 0583/2020] Update core version strings for 0.9.2 (#1702) --- jib-core/CHANGELOG.md | 10 ++++++++-- jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index c19a94b034..6f6d6e36e4 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,11 +5,17 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.9.2 + +### Added + - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - `Containerizer#setOfflineMode` to retrieve the base image from Jib's cache rather than a container registry ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) -### Changed - ### Fixed - Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) diff --git a/jib-core/README.md b/jib-core/README.md index e7d3aee94d..b859373840 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.9.1 + 0.9.2 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.9.1' + compile 'com.google.cloud.tools:jib-core:0.9.2' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index 21b0936450..fac94cab5c 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.9.1' + classpath 'com.google.cloud.tools:jib-core:0.9.2' } } From 41cc0e73ac776498dc16825f391f683b45f89747 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 7 May 2019 15:14:58 -0400 Subject: [PATCH 0584/2020] Update plugin version strings for 1.2.0 (#1703) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 17 files changed, 34 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dff9036760..a9d112fd5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.1.3-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.2.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.1.2' + // id 'com.google.cloud.tools.jib' version '1.2.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 6d38d9c382..901dea4661 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.1.2 + 1.2.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 90a34dc7b5..9b00c2d1ed 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index badf7ad98d..1a3c6a2351 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.2 + 1.2.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 4016b26f65..40342a88be 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index f6c5a97b90..b9222c3937 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.1.2 + 1.2.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 03938293e0..19b02d7e3d 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.1.2" + id("com.google.cloud.tools.jib") version "1.2.0" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 9a4b1ec7f6..19ecef02e2 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 88fc77f441..f9e74d5596 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.1.2' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.2.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 7d41d34057..3642f06208 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.2 + 1.2.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 2f0513602a..c23ef37cbf 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 769a364d81..65370f8aef 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.1.2 + 1.2.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 81422794a5..d4ca651f9d 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c822a565d9..cf1eb1ab81 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.2.0 + +### Added + - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) - Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index ce0bb1b98d..fd22145f3e 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.1.2' + id 'com.google.cloud.tools.jib' version '1.2.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index f3ff2d7ccc..8414547e2f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.2.0 + +### Added + - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) - Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) - Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 886a7e8f65..50764174e0 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.2:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.2.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.1.2:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.2.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.1.2 + 1.2.0 myimage From 8f6af3b15894d6c67b17b055e3e2a248e54799d1 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 8 May 2019 10:47:32 -0400 Subject: [PATCH 0585/2020] Fix maven changelog (#1705) --- jib-maven-plugin/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 8414547e2f..9e221c3552 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -14,9 +14,9 @@ All notable changes to this project will be documented in this file. ### Added - Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) -- Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) - Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) -- Now supports multiple extra directories with `{|}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) +- Now supports multiple extra directories with `{|}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed From c5780f8c07921d2cbf3ed6ec55a6825729fb46d1 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 8 May 2019 11:06:54 -0400 Subject: [PATCH 0586/2020] Use Docker config.json credential retrievers before inferred retrievers (#1704) * Use Docker config.json credential providers before inferred providers * Add changelog --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ .../plugins/common/DefaultCredentialRetrievers.java | 2 +- .../common/DefaultCredentialRetrieversTest.java | 10 +++++----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index cf1eb1ab81..0680e97d5b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) + ### Fixed ## 1.2.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9e221c3552..459441d2c8 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) + ### Fixed ## 1.2.0 diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 7ac9a34ae7..2c58d8973f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -137,8 +137,8 @@ public List asList() throws FileNotFoundException { if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); } - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); return credentialRetrievers; } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 3f3ba8cb9f..534cfb396c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -72,7 +72,7 @@ public void testInitAsList() throws FileNotFoundException { DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); Assert.assertEquals( Arrays.asList( - mockInferCredentialHelperCredentialRetriever, mockDockerConfigCredentialRetriever), + mockDockerConfigCredentialRetriever, mockInferCredentialHelperCredentialRetriever), credentialRetrievers); } @@ -89,8 +89,8 @@ public void testInitAsList_all() throws FileNotFoundException { mockKnownCredentialRetriever, mockDockerCredentialHelperCredentialRetriever, mockInferredCredentialRetriever, - mockInferCredentialHelperCredentialRetriever, - mockDockerConfigCredentialRetriever), + mockDockerConfigCredentialRetriever, + mockInferCredentialHelperCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource"); @@ -111,8 +111,8 @@ public void testInitAsList_credentialHelperPath() throws IOException { Assert.assertEquals( Arrays.asList( mockDockerCredentialHelperCredentialRetriever, - mockInferCredentialHelperCredentialRetriever, - mockDockerConfigCredentialRetriever), + mockDockerConfigCredentialRetriever, + mockInferCredentialHelperCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory) .dockerCredentialHelper(fakeCredentialHelperPath.toString()); From 88a4f0bbb44249ae2c09992a03f0be44a4c4ea38 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 9 May 2019 16:33:08 -0400 Subject: [PATCH 0587/2020] Update error message (#1707) --- .../tools/jib/registry/ManifestPusher.java | 4 +++- .../jib/registry/ManifestPusherTest.java | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index eb5c9dc15f..be9ecab458 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -115,7 +115,9 @@ public DescriptorDigest handleHttpResponseException(HttpResponseException httpRe ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.TAG_INVALID) { throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) - .addReason("Registry may not support Image Manifest Version 2, Schema 2") + .addReason( + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2") .build(); } // rethrow: unhandled error response code. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index da0bae510f..6259ffbb7d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -163,7 +163,8 @@ public void testHandleHttpResponseException_dockerRegistry_tagInvalid() new HttpResponseException.Builder( HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) .setContent( - "{\"errors\":[{\"code\":\"TAG_INVALID\",\"message\":\"manifest tag did not match URI\"}]}") + "{\"errors\":[{\"code\":\"TAG_INVALID\"," + + "\"message\":\"manifest tag did not match URI\"}]}") .build(); try { testManifestPusher.handleHttpResponseException(exception); @@ -173,7 +174,8 @@ public void testHandleHttpResponseException_dockerRegistry_tagInvalid() Assert.assertThat( ex.getMessage(), CoreMatchers.containsString( - "Registry may not support Image Manifest Version 2, Schema 2")); + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); } } @@ -185,7 +187,8 @@ public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() new HttpResponseException.Builder( HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) .setContent( - "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}") + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"message\":\"manifest invalid\",\"detail\":{}}]}") .build(); try { testManifestPusher.handleHttpResponseException(exception); @@ -195,7 +198,8 @@ public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() Assert.assertThat( ex.getMessage(), CoreMatchers.containsString( - "Registry may not support Image Manifest Version 2, Schema 2")); + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); } } @@ -206,8 +210,9 @@ public void testHandleHttpResponseException_quayIo() throws HttpResponseExceptio new HttpResponseException.Builder( HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED MEDIA TYPE", new HttpHeaders()) .setContent( - "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"detail\":" - + "{\"message\":\"manifest schema version not supported\"},\"message\":\"manifest invalid\"}]}") + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"detail\":{\"message\":\"manifest schema version not supported\"}," + + "\"message\":\"manifest invalid\"}]}") .build(); try { testManifestPusher.handleHttpResponseException(exception); @@ -217,7 +222,8 @@ public void testHandleHttpResponseException_quayIo() throws HttpResponseExceptio Assert.assertThat( ex.getMessage(), CoreMatchers.containsString( - "Registry may not support Image Manifest Version 2, Schema 2")); + "Registry may not support pushing OCI Manifest or " + + "Docker Image Manifest Version 2, Schema 2")); } } From 3eed0b8a10c769dc386f68d85d09d55ebab01f55 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 10 May 2019 13:48:52 -0400 Subject: [PATCH 0588/2020] Move useful classes to api package (#1708) --- .../ContainerizerIntegrationTest.java | 15 ++------------- .../cloud/tools/jib/api/JibIntegrationTest.java | 4 ---- .../tools/jib/api/ReproducibleImageTest.java | 5 ----- ...henticationMethodRetrieverIntegrationTest.java | 4 ++-- .../jib/registry/BlobCheckerIntegrationTest.java | 3 ++- .../jib/registry/BlobPullerIntegrationTest.java | 3 ++- .../jib/registry/BlobPusherIntegrationTest.java | 3 ++- .../registry/ManifestPullerIntegrationTest.java | 1 + .../registry/ManifestPusherIntegrationTest.java | 3 ++- .../RegistryAuthenticatorIntegrationTest.java | 8 ++++---- .../jib/{filesystem => api}/AbsoluteUnixPath.java | 3 ++- .../CacheDirectoryCreationException.java | 2 +- .../google/cloud/tools/jib/api/Containerizer.java | 2 -- .../jib/{image => api}/DescriptorDigest.java | 2 +- .../cloud/tools/jib/api/DockerDaemonImage.java | 2 -- .../{configuration => api}/FilePermissions.java | 2 +- .../tools/jib/{image => api}/ImageReference.java | 2 +- .../InsecureRegistryException.java | 4 ++-- .../InvalidImageReferenceException.java | 2 +- .../cloud/tools/jib/api/JavaContainerBuilder.java | 4 ---- .../java/com/google/cloud/tools/jib/api/Jib.java | 3 --- .../google/cloud/tools/jib/api/JibContainer.java | 1 - .../cloud/tools/jib/api/JibContainerBuilder.java | 7 ------- .../tools/jib/{configuration => api}/Port.java | 2 +- .../RegistryAuthenticationFailedException.java | 7 ++++--- .../jib/{registry => api}/RegistryException.java | 2 +- .../google/cloud/tools/jib/api/RegistryImage.java | 2 -- .../RegistryUnauthorizedException.java | 2 +- .../jib/{filesystem => api}/RelativeUnixPath.java | 3 ++- .../com/google/cloud/tools/jib/api/TarImage.java | 2 -- .../cloud/tools/jib/blob/BlobDescriptor.java | 2 +- .../jib/builder/steps/AuthenticatePushStep.java | 4 ++-- .../tools/jib/builder/steps/BuildResult.java | 2 +- .../tools/jib/builder/steps/LoadDockerStep.java | 2 +- .../steps/PullAndCacheBaseImageLayerStep.java | 2 +- .../jib/builder/steps/PullBaseImageStep.java | 8 ++++---- .../tools/jib/builder/steps/PushBlobStep.java | 2 +- .../tools/jib/builder/steps/PushImageStep.java | 2 +- .../com/google/cloud/tools/jib/cache/Cache.java | 4 ++-- .../cloud/tools/jib/cache/CacheStorageFiles.java | 4 ++-- .../cloud/tools/jib/cache/CacheStorageReader.java | 4 ++-- .../cloud/tools/jib/cache/CacheStorageWriter.java | 4 ++-- .../google/cloud/tools/jib/cache/CachedLayer.java | 2 +- .../tools/jib/cache/LayerEntriesSelector.java | 2 +- .../jib/configuration/ContainerConfiguration.java | 3 ++- .../jib/configuration/ImageConfiguration.java | 2 +- .../jib/configuration/LayerConfiguration.java | 3 ++- .../cloud/tools/jib/docker/DockerClient.java | 2 +- .../cloud/tools/jib/docker/ImageTarball.java | 2 +- .../tools/jib/filesystem/UnixPathParser.java | 4 ++-- .../jib/frontend/CredentialRetrieverFactory.java | 2 +- .../tools/jib/frontend/ExposedPortsParser.java | 2 +- .../jib/frontend/JavaEntrypointConstructor.java | 4 ++-- .../jib/hash/CountingDigestOutputStream.java | 2 +- .../com/google/cloud/tools/jib/hash/Digests.java | 2 +- .../cloud/tools/jib/image/DigestOnlyLayer.java | 1 + .../com/google/cloud/tools/jib/image/Image.java | 4 ++-- .../google/cloud/tools/jib/image/ImageLayers.java | 1 + .../com/google/cloud/tools/jib/image/Layer.java | 1 + .../google/cloud/tools/jib/image/LayerEntry.java | 4 ++-- .../cloud/tools/jib/image/ReferenceLayer.java | 1 + .../tools/jib/image/ReferenceNoDiffIdLayer.java | 1 + .../jib/image/json/BuildableManifestTemplate.java | 2 +- .../json/ContainerConfigurationTemplate.java | 2 +- .../image/json/DescriptorDigestDeserializer.java | 2 +- .../image/json/DescriptorDigestSerializer.java | 2 +- .../jib/image/json/ImageToJsonTranslator.java | 6 +++--- .../jib/image/json/JsonToImageTranslator.java | 6 +++--- .../tools/jib/image/json/OCIManifestTemplate.java | 2 +- .../json/UnknownManifestFormatException.java | 2 +- .../tools/jib/image/json/V21ManifestTemplate.java | 2 +- .../tools/jib/image/json/V22ManifestTemplate.java | 2 +- .../registry/AuthenticationMethodRetriever.java | 1 + .../cloud/tools/jib/registry/BlobChecker.java | 2 +- .../cloud/tools/jib/registry/BlobPuller.java | 2 +- .../cloud/tools/jib/registry/BlobPusher.java | 3 ++- .../cloud/tools/jib/registry/ManifestPusher.java | 2 +- .../tools/jib/registry/RegistryAuthenticator.java | 3 +++ .../jib/registry/RegistryBrokenPipeException.java | 2 ++ .../cloud/tools/jib/registry/RegistryClient.java | 3 ++- .../RegistryCredentialsNotSentException.java | 2 ++ .../jib/registry/RegistryEndpointCaller.java | 3 +++ .../jib/registry/RegistryEndpointProvider.java | 1 + .../jib/registry/RegistryErrorException.java | 1 + .../jib/registry/RegistryNoResponseException.java | 2 ++ .../registry/UnexpectedBlobDigestException.java | 2 ++ .../{filesystem => api}/AbsoluteUnixPathTest.java | 2 +- .../cloud/tools/jib/api/ContainerizerTest.java | 3 --- .../jib/{image => api}/DescriptorDigestTest.java | 2 +- .../tools/jib/api/DockerDaemonImageTest.java | 1 - .../FilePermissionsTest.java | 2 +- .../jib/{image => api}/ImageReferenceTest.java | 2 +- .../tools/jib/api/JavaContainerBuilderTest.java | 4 ---- .../tools/jib/api/JibContainerBuilderTest.java | 6 ------ .../cloud/tools/jib/api/JibContainerTest.java | 1 - .../jib/{configuration => api}/PortTest.java | 2 +- .../cloud/tools/jib/api/RegistryImageTest.java | 1 - .../{filesystem => api}/RelativeUnixPathTest.java | 2 +- .../google/cloud/tools/jib/api/TarImageTest.java | 1 - .../com/google/cloud/tools/jib/blob/BlobTest.java | 2 +- .../BuildAndCacheApplicationLayerStepTest.java | 2 +- .../jib/builder/steps/BuildImageStepTest.java | 6 +++--- .../tools/jib/builder/steps/BuildResultTest.java | 2 +- .../RetrieveRegistryCredentialsStepTest.java | 2 +- .../tools/jib/cache/CacheStorageFilesTest.java | 6 +++--- .../tools/jib/cache/CacheStorageReaderTest.java | 4 ++-- .../tools/jib/cache/CacheStorageWriterTest.java | 6 +++--- .../google/cloud/tools/jib/cache/CacheTest.java | 4 ++-- .../cloud/tools/jib/cache/CachedLayerTest.java | 2 +- .../tools/jib/cache/LayerEntriesSelectorTest.java | 6 +++--- .../jib/configuration/BuildConfigurationTest.java | 5 +++-- .../configuration/ContainerConfigurationTest.java | 3 ++- .../jib/configuration/LayerConfigurationTest.java | 3 ++- .../cloud/tools/jib/docker/DockerClientTest.java | 4 ++-- .../cloud/tools/jib/docker/ImageTarballTest.java | 6 +++--- .../json/DockerLoadManifestTemplateTest.java | 2 +- .../frontend/CredentialRetrieverFactoryTest.java | 2 +- .../jib/frontend/ExposedPortsParserTest.java | 2 +- .../frontend/JavaEntrypointConstructorTest.java | 2 +- .../jib/hash/CountingDigestOutputStreamTest.java | 2 +- .../cloud/tools/jib/image/ImageLayersTest.java | 1 + .../google/cloud/tools/jib/image/ImageTest.java | 5 +++-- .../google/cloud/tools/jib/image/LayerTest.java | 1 + .../jib/image/ReproducibleLayerBuilderTest.java | 4 ++-- .../json/ContainerConfigurationTemplateTest.java | 2 +- .../jib/image/json/ImageToJsonTranslatorTest.java | 6 +++--- .../jib/image/json/JsonToImageTranslatorTest.java | 6 +++--- .../jib/image/json/OCIManifestTemplateTest.java | 2 +- .../jib/image/json/V21ManifestTemplateTest.java | 2 +- .../jib/image/json/V22ManifestTemplateTest.java | 2 +- .../tools/jib/json/JsonTemplateMapperTest.java | 2 +- .../cloud/tools/jib/registry/BlobCheckerTest.java | 2 +- .../cloud/tools/jib/registry/BlobPullerTest.java | 2 +- .../cloud/tools/jib/registry/BlobPusherTest.java | 3 ++- .../tools/jib/registry/ManifestPusherTest.java | 2 +- ...RegistryAuthenticationFailedExceptionTest.java | 1 + .../jib/registry/RegistryAuthenticatorTest.java | 1 + .../jib/registry/RegistryEndpointCallerTest.java | 3 +++ .../jib/gradle/EmptyProjectIntegrationTest.java | 2 +- .../cloud/tools/jib/gradle/JibRunHelper.java | 6 +++--- .../cloud/tools/jib/gradle/BuildDockerTask.java | 8 ++++---- .../cloud/tools/jib/gradle/BuildImageTask.java | 8 ++++---- .../cloud/tools/jib/gradle/BuildTarTask.java | 6 +++--- .../gradle/GradleHelpfulSuggestionsBuilder.java | 2 +- .../tools/jib/gradle/GradleProjectProperties.java | 2 +- .../tools/jib/gradle/GradleRawConfiguration.java | 4 ++-- .../google/cloud/tools/jib/gradle/TaskCommon.java | 4 ++-- .../jib/gradle/GradleProjectPropertiesTest.java | 8 ++++---- .../cloud/tools/jib/maven/BuildDockerMojo.java | 8 ++++---- .../cloud/tools/jib/maven/BuildImageMojo.java | 8 ++++---- .../cloud/tools/jib/maven/BuildTarMojo.java | 6 +++--- .../jib/maven/MavenHelpfulSuggestionsBuilder.java | 2 +- .../tools/jib/maven/MavenProjectProperties.java | 2 +- .../tools/jib/maven/MavenRawConfiguration.java | 4 ++-- .../google/cloud/tools/jib/maven/MojoCommon.java | 4 ++-- .../jib/maven/BuildImageMojoIntegrationTest.java | 6 +++--- .../jib/maven/MavenProjectPropertiesTest.java | 6 +++--- .../common/ConfigurationPropertyValidator.java | 4 ++-- .../jib/plugins/common/HelpfulSuggestions.java | 2 +- .../common/JavaContainerBuilderHelper.java | 6 +++--- .../tools/jib/plugins/common/JibBuildRunner.java | 12 ++++++------ .../common/PluginConfigurationProcessor.java | 6 +++--- .../jib/plugins/common/RawConfiguration.java | 4 ++-- .../jib/api/JibContainerBuilderTestHelper.java | 1 - .../ConfigurationPropertyValidatorTest.java | 4 ++-- .../plugins/common/HelpfulSuggestionsTest.java | 2 +- .../common/JavaContainerBuilderHelperTest.java | 6 +++--- .../jib/plugins/common/JibBuildRunnerTest.java | 10 +++++----- .../common/PluginConfigurationProcessorTest.java | 10 +++++----- .../jib/plugins/common/TimerEventHandlerTest.java | 2 +- .../common/logging/AnsiLoggerWithFooterTest.java | 2 +- .../logging/ProgressDisplayGeneratorTest.java | 2 +- 172 files changed, 275 insertions(+), 290 deletions(-) rename jib-core/src/integration-test/java/com/google/cloud/tools/jib/{builder => api}/ContainerizerIntegrationTest.java (95%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{filesystem => api}/AbsoluteUnixPath.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration => api}/CacheDirectoryCreationException.java (94%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{image => api}/DescriptorDigest.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration => api}/FilePermissions.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{image => api}/ImageReference.java (99%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{registry => api}/InsecureRegistryException.java (90%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{image => api}/InvalidImageReferenceException.java (95%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration => api}/Port.java (98%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{registry => api}/RegistryAuthenticationFailedException.java (85%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{registry => api}/RegistryException.java (95%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{registry => api}/RegistryUnauthorizedException.java (97%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{filesystem => api}/RelativeUnixPath.java (95%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{filesystem => api}/AbsoluteUnixPathTest.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{image => api}/DescriptorDigestTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{configuration => api}/FilePermissionsTest.java (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{image => api}/ImageReferenceTest.java (99%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{configuration => api}/PortTest.java (96%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{filesystem => api}/RelativeUnixPathTest.java (96%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java similarity index 95% rename from jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java rename to jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 14b349eb4b..592d696401 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -14,29 +14,18 @@ * the License. */ -package com.google.cloud.tools.jib.builder; +package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.DockerDaemonImage; -import com.google.cloud.tools.jib.api.Jib; -import com.google.cloud.tools.jib.api.JibContainer; -import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.api.TarImage; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index baf4b8a71e..9faa4099ba 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -17,12 +17,8 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.LocalRegistry; -import com.google.cloud.tools.jib.registry.RegistryException; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 0bc365de85..82356de44c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -16,12 +16,7 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index be34ccc9d2..cebdc0476d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; @@ -28,8 +29,7 @@ public class AuthenticationMethodRetrieverIntegrationTest { private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; @Test - public void testGetRegistryAuthenticator() - throws RegistryAuthenticationFailedException, IOException, RegistryException { + public void testGetRegistryAuthenticator() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index c53f8d4e26..cf8f3c4259 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import java.security.DigestException; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 4c46ceca3e..596a3acc5b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -16,10 +16,11 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.io.ByteStreams; import java.io.IOException; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 3d0ef643e0..9b421aec24 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -16,11 +16,12 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index c291a389d1..485e788bfe 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 6be922efad..88e3017e7a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -18,12 +18,13 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 5581ee7d95..2ff1e02939 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -16,10 +16,11 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.io.IOException; import org.junit.Assert; import org.junit.Test; @@ -31,8 +32,7 @@ public class RegistryAuthenticatorIntegrationTest { @Test public void testAuthenticate() - throws RegistryAuthenticationFailedException, InvalidImageReferenceException, IOException, - RegistryException { + throws IOException, RegistryException, InvalidImageReferenceException { ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.initializer( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/AbsoluteUnixPath.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/AbsoluteUnixPath.java index 7cba249d7f..2b8b6d93c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPath.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/AbsoluteUnixPath.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.filesystem; +package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.filesystem.UnixPathParser; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.nio.file.Path; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java similarity index 94% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java index cd103021b4..d049fe1a8a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/CacheDirectoryCreationException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; /** Thrown when a directory to be used as the cache could not be created. */ public class CacheDirectoryCreationException extends Exception { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 24d83ae4cf..d12c33832a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -19,12 +19,10 @@ import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.UserCacheHome; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import java.io.IOException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java index 4639cdd83e..4560f0b9a6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DescriptorDigest.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index bd0b14d615..d9b39ff429 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; import java.util.Collections; import java.util.Map; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java index c4a87f9717..c206666d5d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/FilePermissions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java similarity index 99% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java index ee2956b407..e9facae06f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.common.base.Preconditions; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java similarity index 90% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java index 14d100ef47..82722ff557 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/InsecureRegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import java.net.URL; @@ -23,7 +23,7 @@ */ public class InsecureRegistryException extends RegistryException { - InsecureRegistryException(URL insecureUrl) { + public InsecureRegistryException(URL insecureUrl) { super( "Failed to verify the server at " + insecureUrl diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java index 32636405df..e6ebc85c9e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/InvalidImageReferenceException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; /** Thrown when attempting to parse an invalid image reference. */ public class InvalidImageReferenceException extends Exception { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index bba22b798f..24dabb4c76 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -18,13 +18,9 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.frontend.MainClassFinder; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Files; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index 06955fea71..fb54c73f63 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -17,9 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; - /** Build containers with Jib. */ // TODO: Add tests once JibContainerBuilder#containerize() is added. public class Jib { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java index b445880f13..5818663a7f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.util.Objects; /** The container built by Jib. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index ea7b9e25c0..3d591e3357 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -20,21 +20,14 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.LayerEntry; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.UnknownHostException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Port.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/Port.java index f0ed4dbab0..e1b099438c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/Port.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Port.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; import java.util.Objects; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java similarity index 85% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java index 4e4926b7d9..6a13b3258c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import java.text.MessageFormat; @@ -25,13 +25,14 @@ public class RegistryAuthenticationFailedException extends RegistryException { private final String serverUrl; private final String imageName; - RegistryAuthenticationFailedException(String serverUrl, String imageName, Throwable cause) { + public RegistryAuthenticationFailedException( + String serverUrl, String imageName, Throwable cause) { super(MessageFormat.format(REASON, serverUrl, imageName, cause.getMessage()), cause); this.serverUrl = serverUrl; this.imageName = imageName; } - RegistryAuthenticationFailedException(String serverUrl, String imageName, String reason) { + public RegistryAuthenticationFailedException(String serverUrl, String imageName, String reason) { super(MessageFormat.format(REASON, serverUrl, imageName, reason)); this.serverUrl = serverUrl; this.imageName = imageName; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java index 8a4e307030..9dcfd0900b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import javax.annotation.Nullable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 7ced968c05..3a5683eb3f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -20,8 +20,6 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.util.ArrayList; import java.util.List; import java.util.Optional; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java similarity index 97% rename from jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java index 5897e4d2e9..692ff65d75 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.registry; +package com.google.cloud.tools.jib.api; import com.google.api.client.http.HttpResponseException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RelativeUnixPath.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/RelativeUnixPath.java index 0dfc23febb..6742b49e4b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPath.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RelativeUnixPath.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.filesystem; +package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.filesystem.UnixPathParser; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import javax.annotation.concurrent.Immutable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index 73789ced70..0744870c79 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Path; /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java index ff59209998..a5e6bbcb31 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; /** Contains properties describing a BLOB, including its digest and possibly its size (in bytes). */ public class BlobDescriptor { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 036e0fcd43..0992b4535d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -26,9 +28,7 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index 836cbe1cc2..e29cd7ce34 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index e45d0a140a..788056a119 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -26,7 +27,6 @@ import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 45a49378ae..519b373625 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 03349a6769..548d427c38 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -16,6 +16,10 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; @@ -31,9 +35,7 @@ import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; @@ -47,8 +49,6 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index cdc996549a..b5e48aff2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -28,7 +29,6 @@ import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.cloud.tools.jib.registry.RegistryException; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index f519ab61bb..0c8faf73a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -26,7 +27,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index de537a0955..3bd8afa294 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java index cbc1767e54..3296a71da3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.common.base.Splitter; import java.nio.file.Path; import java.security.DigestException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index b3bee7acd5..2c70a9b61d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -18,10 +18,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.filesystem.LockFile; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.ManifestAndConfig; import com.google.cloud.tools.jib.image.json.ManifestTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index 97848c00d1..2207dbb50a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; @@ -23,8 +25,6 @@ import com.google.cloud.tools.jib.filesystem.LockFile; import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index b00d9706a6..43a543693a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Layer; import com.google.common.base.Preconditions; import javax.annotation.Nullable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index c4115cc3b1..d684df76b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java index 2fe9e5bce1..c021937111 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Port; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index c1ddde05fc..832a335eaf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java index 7f5c181549..3bdc544426 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import java.io.IOException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index ccf8292b7b..0ddcb34614 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 62ae015650..5d8ef8d9d4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.docker; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplateMapper; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java index f4f9a98fda..d2e7de2787 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UnixPathParser.java @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList; /** Parses Unix-style paths. */ -class UnixPathParser { +public class UnixPathParser { /** * Parses a Unix-style path into a list of path components. @@ -28,7 +28,7 @@ class UnixPathParser { * @param unixPath the Unix-style path * @return a list of path components */ - static ImmutableList parse(String unixPath) { + public static ImmutableList parse(String unixPath) { ImmutableList.Builder pathComponents = ImmutableList.builder(); for (String pathComponent : Splitter.on('/').split(unixPath)) { if (pathComponent.isEmpty()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index e5553121b8..ff3a49d49d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java index 8193051ec3..17fe04bf3f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.api.Port; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java index 1a8be39d62..f60c4eee99 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.RelativeUnixPath; import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java index 12cf3ebd65..245e3f2891 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.hash; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.base.Verify; import java.io.IOException; import java.io.OutputStream; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java index e5b0afca0d..90d495914d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.hash; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.ByteStreams; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java index 0a88a112bc..fdeebc6074 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 10f0ede881..8e3dac8b6d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java index 3d1211fba6..fd37b6b29e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java index 05fccf9b89..88a7aec746 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java index d691fd4601..c9f024c431 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.image; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java index fb33a2fc06..27d599e8f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java index fc6df8b49d..db8aef33d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index 397512f3ff..7918ec8fe1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 5818edfdb5..895f59d8c0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.image.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java index 95e61b97ff..bca89c2a33 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; import java.security.DigestException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java index 83c28fd37f..8308159123 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; /** Serializes a {@link DescriptorDigest} into JSON element. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index c0a07bd298..f45a7416ba 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java index 00fc5a8fd6..35dfe38408 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.DigestOnlyLayer; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java index 9a27048c35..6f820a5a39 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java index 1651d343c4..860a232ffe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.registry.RegistryException; +import com.google.cloud.tools.jib.api.RegistryException; /** Exception thrown when trying to parse an unknown image manifest format. */ public class UnknownManifestFormatException extends RegistryException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java index 74f12d72c4..6e041ff35e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java index 7707b90eff..5d0de49ae5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index 4ddb1f8bee..ed28efee19 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -19,6 +19,7 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import java.net.MalformedURLException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index 427f47dfff..7c9176b6d7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -19,10 +19,10 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index b0534dab42..ebfe733673 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -17,12 +17,12 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.http.ListenableCountingOutputStream; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 8cb6fb1e23..2b79c783e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -18,11 +18,12 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.net.MediaType; import java.net.HttpURLConnection; import java.net.MalformedURLException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index be9ecab458..220ac1ff5c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -18,13 +18,13 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import java.io.IOException; import java.net.MalformedURLException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 0058a23062..2530c7c78b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,6 +17,9 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java index dd090e78da..76508c4184 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; + /** Thrown when the registry shut down the connection. */ class RegistryBrokenPipeException extends RegistryException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 7adcdacec9..81a782f0cc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; @@ -25,7 +27,6 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java index 4e95f9baf0..22fe535d35 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; + /** Thrown when registry request was unauthorized because credentials weren't sent. */ public class RegistryCredentialsNotSentException extends RegistryException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 9898a4cb96..241969bf21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -19,6 +19,9 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index c2dda8ecf9..f07dd476f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import java.io.IOException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java index 8886636800..f20209a581 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; import javax.annotation.Nullable; /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java index 068b180778..70513c21ee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; + /** Thrown when a registry did not respond. */ class RegistryNoResponseException extends RegistryException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java index d73a2243ac..ad90506c48 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryException; + /** Thrown when a pulled BLOB did not have the same digest as requested. */ class UnexpectedBlobDigestException extends RegistryException { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/AbsoluteUnixPathTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/AbsoluteUnixPathTest.java index 1392dae791..a23c8fcf18 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/AbsoluteUnixPathTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/AbsoluteUnixPathTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.filesystem; +package com.google.cloud.tools.jib.api; import java.nio.file.Paths; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index 358c6cae52..22e5a01003 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -16,12 +16,9 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; import java.util.Arrays; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java index 58461734f4..ac55ceccc3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/DescriptorDigestTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import java.security.DigestException; import java.util.HashMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java index ee03b00c49..82fa4ceda9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; import java.nio.file.Paths; import java.util.Optional; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/FilePermissionsTest.java similarity index 98% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/FilePermissionsTest.java index ab5efd09cc..cadba46916 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/FilePermissionsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/FilePermissionsTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java similarity index 99% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java index 65d91d827a..9519668f96 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; import com.google.common.base.Strings; import java.util.Arrays; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index be2c441f7c..e383f7851c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -17,11 +17,7 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index d6384dcbc3..19dac6ae83 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -19,20 +19,14 @@ import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java index 2d5950d00d..5c02e2e852 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.security.DigestException; import org.junit.Assert; import org.junit.Before; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortTest.java similarity index 96% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/PortTest.java index c70fe3f9a8..502c6cbadc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/PortTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java index 411486b26b..e740ae1ebb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RelativeUnixPathTest.java similarity index 96% rename from jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/RelativeUnixPathTest.java index 39dbdc2eef..079a22ce3b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/RelativeUnixPathTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RelativeUnixPathTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.filesystem; +package com.google.cloud.tools.jib.api; import com.google.common.collect.ImmutableList; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java index eef9cda552..321870b2d3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java index e31f075dc5..7a138c8344 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.blob; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.hash.WritableContents; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index cea49723b5..6d984851b4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; @@ -26,7 +27,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 65e53420d0..522c0ec51b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -16,16 +16,16 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.configuration.Port; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java index 4d489c7e4a..46aa04b3f9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 4536f547f6..037f5192cf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java index 64bb9efc92..bf08828f55 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.cache; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +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 java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 25b0345605..dc4acdbad2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index c7a1f58d81..ecf48e2f40 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.cache; +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.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 16d3c1f0ab..8a76dcad4c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java index b6783668a7..6b46022c3c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.IOException; import org.hamcrest.CoreMatchers; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 3089ce42eb..5e164abf59 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.cache; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 8d1063ab76..2f2491dc05 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -16,10 +16,11 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java index f2a0cd949d..7733c612b0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Port; import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.HashMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java index 0770917665..83b579c412 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java @@ -16,7 +16,8 @@ package com.google.cloud.tools.jib.configuration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 230c2a9a38..54da3c412f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.docker; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java index 7c9addaefc..c61a7342b8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.docker; +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.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java index 2f45ed5f53..f55cd546fa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.docker.json; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 8063515995..556d5d5e8f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; -import com.google.cloud.tools.jib.image.ImageReference; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java index de2e5748bb..76c19a34da 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.configuration.Port; +import com.google.cloud.tools.jib.api.Port; import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.Collections; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java index 81bab6cfb8..665fd126de 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.List; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index b4901f2615..ef88052426 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.hash; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java index bb8c2d691e..828150e22f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import java.util.Arrays; import java.util.List; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java index eeca85eeff..a961b5656a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java @@ -16,9 +16,10 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java index 0faa3bb480..5d062e08f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 5b6a70b652..c5613bc3cb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java index e0e5423b16..b5de33f79b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 51045a449a..01892bde82 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 2db5e4d4c8..7ab492f35c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.image.json; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.configuration.Port; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerCountMismatchException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java index 7c7defd779..f9a6027705 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java index dd5b6c4947..a2f33c1b5c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index c8d0ba8d55..6666009518 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.image.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 4d2ea955c5..87a586aa66 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.json; -import com.google.cloud.tools.jib.image.DescriptorDigest; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index a45833830e..0625d57cda 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -18,9 +18,9 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index a21f4bd725..6e202dd1a1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index 9066005762..a0a1b50106 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -17,12 +17,13 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.GenericUrl; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.cloud.tools.jib.image.DescriptorDigest; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.MalformedURLException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 6259ffbb7d..4f2738c8eb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -18,12 +18,12 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.image.DescriptorDigest; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java index 638b2e53cd..49e98abfe4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index c84fcdbf0f..c79c7e446b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.TestWebServer; import java.io.IOException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index a01711d047..fa61d44cb8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -20,6 +20,9 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index e55243608f..f382198c46 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import java.io.IOException; import java.security.DigestException; import org.hamcrest.CoreMatchers; diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index fa06d3f1a4..975483883a 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -17,9 +17,9 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +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 java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 9a10c72762..179847c83a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index d887cfce38..c8525982ea 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5a5a55715d..f66b412896 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,10 +16,10 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java index dfa3a6c98d..2694febc2a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import javax.annotation.Nullable; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 8b159cd379..1b03ab7bb7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; @@ -23,7 +24,6 @@ import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index eed3b424a3..37bed3d018 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 8b7ef5654d..edfd9983d4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.util.HashMap; import java.util.Map; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index c7184c1610..d9b06e751b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -16,18 +16,18 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 67ed27c755..484b5dec69 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 40661148a9..f233a56bfe 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.image.ImageFormat; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 6fb60a4126..20cbd7c5ee 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java index 2b8dd81be8..302434d843 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import javax.annotation.Nullable; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 7dc01a9b0f..5b352b736d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; @@ -23,7 +24,6 @@ import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 2778a3a3a7..d363824032 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index e963827a8c..d7738d2749 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 5c3134aaee..45a2bc1a44 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -18,9 +18,9 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.image.DescriptorDigest; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; +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.registry.LocalRegistry; import com.google.common.base.Splitter; import java.io.IOException; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index f728084fc6..6d08ae3a4f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -16,16 +16,16 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index c4ee6a7c5a..20b0b1c275 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.base.Strings; import java.util.ArrayList; import java.util.LinkedHashMap; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 45c54c541d..797ef6778c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.common.base.Preconditions; import java.nio.file.Path; import javax.annotation.Nullable; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 63503cc16c..8827629992 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -16,15 +16,15 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.FilePermissions; +import com.google.cloud.tools.jib.api.RelativeUnixPath; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.filesystem.RelativeUnixPath; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index d80dc184e0..aa860c44cb 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -18,18 +18,18 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; -import com.google.cloud.tools.jib.registry.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import java.io.IOException; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index d3a03dda65..6b31f982a0 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -16,8 +16,11 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerDaemonImage; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; @@ -26,13 +29,10 @@ import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 9ad339f04f..7a768308fb 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.configuration.FilePermissions; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.image.ImageFormat; import java.nio.file.Path; import java.util.List; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java index cac18e3820..9db8fdf1cb 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 517f0f82d3..9e2b008ed6 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.Optional; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 5634ac18a1..1f1185dd45 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.image.ImageReference; +import com.google.cloud.tools.jib.api.ImageReference; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 342d37dfe9..b2d54d5161 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -16,17 +16,17 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 68cbd16ea0..5b426198f0 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -18,15 +18,15 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.JibContainerBuilder; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.registry.InsecureRegistryException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; -import com.google.cloud.tools.jib.registry.RegistryException; -import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; import java.io.IOException; import java.net.UnknownHostException; import java.util.concurrent.ExecutionException; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 3ea50695f0..ad1b3912d7 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -16,20 +16,20 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.configuration.FilePermissions; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath; -import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.image.InvalidImageReferenceException; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java index a242252214..df216af6fe 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java @@ -25,7 +25,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link com.google.cloud.tools.jib.plugins.common.TimerEventHandler}. */ +/** Tests for {@link TimerEventHandler}. */ public class TimerEventHandlerTest { private final Deque logMessageQueue = new ArrayDeque<>(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index b01d4895ff..3c6094922d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -28,7 +28,7 @@ import org.junit.Before; import org.junit.Test; -/** Tests for {@link com.google.cloud.tools.jib.plugins.common.logging.AnsiLoggerWithFooter}. */ +/** Tests for {@link AnsiLoggerWithFooter}. */ public class AnsiLoggerWithFooterTest { private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java index cd57690e5e..a28c9e797b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java @@ -22,7 +22,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator}. */ +/** Tests for {@link ProgressDisplayGenerator}. */ public class ProgressDisplayGeneratorTest { private static String getBar(String bar, double value) { From 4b4fb47a382a15a8042ae15c5ee029ac82ae6411 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 13 May 2019 15:43:34 -0400 Subject: [PATCH 0589/2020] Add version to skaffold-files-v2 output (#1714) --- .../main/java/com/google/cloud/tools/jib/ProjectInfo.java | 5 ++++- .../cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java | 4 ++++ .../tools/jib/plugins/common/SkaffoldFilesOutputTest.java | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java index b032e218a5..9d2f67e5d7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib; +import javax.annotation.Nullable; + /** Constants relating to the Jib project. */ public class ProjectInfo { @@ -25,7 +27,8 @@ public class ProjectInfo { /** Link to file an issue against the GitHub repository. */ public static final String GITHUB_NEW_ISSUE_URL = GITHUB_URL + "/issues/new"; - /** The project version. */ + /** The project version. May be {@code null} if the version cannot be determined. */ + @Nullable public static final String VERSION = ProjectInfo.class.getPackage().getImplementationVersion(); private ProjectInfo() {} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java index 33594d3648..4fd4ccdf0d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -25,6 +26,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; /** * Builds a JSON string containing files and directories that build = new ArrayList<>(); private final List inputs = new ArrayList<>(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java index e25d2070f2..a8de049be9 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java @@ -26,7 +26,7 @@ public class SkaffoldFilesOutputTest { private static final String TEST_JSON = - "{\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}"; + "{\"version\":null,\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}"; @Test public void testGetJsonString() throws IOException { @@ -44,7 +44,8 @@ public void testGetJsonString() throws IOException { public void testGetJsonString_empty() throws IOException { SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); Assert.assertEquals( - "{\"build\":[],\"inputs\":[],\"ignore\":[]}", skaffoldFilesOutput.getJsonString()); + "{\"version\":null,\"build\":[],\"inputs\":[],\"ignore\":[]}", + skaffoldFilesOutput.getJsonString()); } @Test From f20312e466a704b2bfd3097655c0ef8cb164fb1d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Tue, 14 May 2019 12:25:57 -0400 Subject: [PATCH 0590/2020] Use `jib.containerize` property to only containerize specific module/project (#1715) --- .../tools/jib/maven/BuildDockerMojo.java | 7 +++ .../cloud/tools/jib/maven/BuildImageMojo.java | 8 +++ .../cloud/tools/jib/maven/BuildTarMojo.java | 8 +++ .../jib/maven/JibPluginConfiguration.java | 34 ++++++++++++ .../maven/BuildDockerMojoIntegrationTest.java | 9 +++- .../maven/BuildImageMojoIntegrationTest.java | 9 +++- .../maven/BuildTarMojoIntegrationTest.java | 9 +++- .../jib/maven/JibPluginConfigurationTest.java | 54 +++++++++++++++++++ .../tools/jib/maven/SkippedGoalVerifier.java | 25 +++++++-- .../jib/plugins/common/PropertyNames.java | 1 + 10 files changed, 155 insertions(+), 9 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 484b5dec69..80fb2b0e9a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -74,6 +74,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; + } else if (!isContainerizable()) { + getLog() + .info( + "Skipping containerization of this module (not specified in " + + PropertyNames.CONTAINERIZE + + ")"); + return; } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index f233a56bfe..df14c141bf 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -32,6 +32,7 @@ import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; @@ -59,6 +60,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; + } else if (!isContainerizable()) { + getLog() + .info( + "Skipping containerization of this module (not specified in " + + PropertyNames.CONTAINERIZE + + ")"); + return; } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 20cbd7c5ee..ecb2f1b487 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -57,6 +58,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; + } else if (!isContainerizable()) { + getLog() + .info( + "Skipping containerization of this module (not specified in " + + PropertyNames.CONTAINERIZE + + ")"); + return; } if ("pom".equals(getProject().getPackaging())) { getLog().info("Skipping containerization because packaging is 'pom'..."); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 25cca343b0..bbe8db895e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -23,6 +23,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.File; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; @@ -626,6 +627,39 @@ boolean isSkipped() { return skip; } + /** + * Return false if the `jib.containerize` property is specified and does not match this + * module/project. Used by the Skaffold-Jib binding. + * + * @return true if this module should be containerized + */ + boolean isContainerizable() { + String moduleSpecification = getProperty(PropertyNames.CONTAINERIZE); + if (project == null || Strings.isNullOrEmpty(moduleSpecification)) { + return true; + } + // modules can be specified in one of three ways: + // 1) a `groupId:artifactId` + // 2) an `:artifactId` + // 3) relative path within the repository + if (moduleSpecification.equals(project.getGroupId() + ":" + project.getArtifactId()) + || moduleSpecification.equals(":" + project.getArtifactId())) { + return true; + } + // Relative paths never have a colon on *nix nor Windows. This moduleSpecification could be an + // :artifactId or groupId:artifactId for a different artifact. + if (moduleSpecification.contains(":")) { + return false; + } + try { + Path projectBase = project.getBasedir().toPath(); + return projectBase.endsWith(moduleSpecification); + } catch (InvalidPathException ex) { + // ignore since moduleSpecification may not actually be a path + return false; + } + } + SettingsDecrypter getSettingsDecrypter() { return settingsDecrypter; } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index e6725e2d0c..a401cd4b6a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -155,8 +155,13 @@ public void testExecute_defaultTarget() } @Test - public void testExecute_skipJibGoal() throws VerificationException, IOException { - SkippedGoalVerifier.verifyGoalIsSkipped(emptyTestProject, BuildDockerMojo.GOAL_NAME); + public void testExecute_jibSkip() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibSkip(emptyTestProject, BuildDockerMojo.GOAL_NAME); + } + + @Test + public void testExecute_jibContainerizeSkips() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibContainerizeSkips(emptyTestProject, BuildDockerMojo.GOAL_NAME); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 45a2bc1a44..a7edc2d724 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -535,8 +535,13 @@ public void testExecute_complexProperties() } @Test - public void testExecute_skipJibGoal() throws VerificationException, IOException { - SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildImageMojo.GOAL_NAME); + public void testExecute_jibSkip() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibSkip(skippedTestProject, BuildImageMojo.GOAL_NAME); + } + + @Test + public void testExecute_jibContainerizeSkips() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibContainerizeSkips(simpleTestProject, BuildDockerMojo.GOAL_NAME); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 4f1d9f454b..615ea32fc9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -80,7 +80,12 @@ public void testExecute_simple() } @Test - public void testExecute_skipJibGoal() throws VerificationException, IOException { - SkippedGoalVerifier.verifyGoalIsSkipped(skippedTestProject, BuildTarMojo.GOAL_NAME); + public void testExecute_jibSkip() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibSkip(skippedTestProject, BuildTarMojo.GOAL_NAME); + } + + @Test + public void testExecute_jibContainerizeSkips() throws VerificationException, IOException { + SkippedGoalVerifier.verifyJibContainerizeSkips(simpleTestProject, BuildDockerMojo.GOAL_NAME); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 1437e665f3..b6a0f663b2 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.io.File; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; @@ -320,4 +321,57 @@ public void testGetExtraDirectoryPermissions_bothPropertiesUsed() { ex.getMessage()); } } + + @Test + public void testIsContainerizable_noProperty() { + Properties projectProperties = project.getProperties(); + + projectProperties.remove("jib.containerize"); + Assert.assertTrue(testPluginConfiguration.isContainerizable()); + + projectProperties.setProperty("jib.containerize", ""); + Assert.assertTrue(testPluginConfiguration.isContainerizable()); + } + + @Test + public void testIsContainerizable_artifactId() { + project.setGroupId("group"); + project.setArtifactId("artifact"); + project.setFile(new File("/repository/project/pom.xml")); // sets baseDir + + Properties projectProperties = project.getProperties(); + projectProperties.setProperty("jib.containerize", ":artifact"); + Assert.assertTrue(testPluginConfiguration.isContainerizable()); + + projectProperties.setProperty("jib.containerize", ":artifact2"); + Assert.assertFalse(testPluginConfiguration.isContainerizable()); + } + + @Test + public void testIsContainerizable_groupAndArtifactId() { + project.setGroupId("group"); + project.setArtifactId("artifact"); + project.setFile(new File("/repository/project/pom.xml")); // sets baseDir + + Properties projectProperties = project.getProperties(); + projectProperties.setProperty("jib.containerize", "group:artifact"); + Assert.assertTrue(testPluginConfiguration.isContainerizable()); + + projectProperties.setProperty("jib.containerize", "group:artifact2"); + Assert.assertFalse(testPluginConfiguration.isContainerizable()); + } + + @Test + public void testIsContainerizable_directory() { + project.setGroupId("group"); + project.setArtifactId("artifact"); + project.setFile(new File("/repository/project/pom.xml")); // sets baseDir + + Properties projectProperties = project.getProperties(); + projectProperties.setProperty("jib.containerize", "project"); + Assert.assertTrue(testPluginConfiguration.isContainerizable()); + + projectProperties.setProperty("jib.containerize", "project2"); + Assert.assertFalse(testPluginConfiguration.isContainerizable()); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java index f98b7f99ec..0796ef82c6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SkippedGoalVerifier.java @@ -26,11 +26,11 @@ import org.hamcrest.CoreMatchers; import org.junit.Assert; -/** A simple verifier utility to test goal skipping accross all our jib goals. */ +/** A simple verifier utility to test goal skipping across all our jib goals. */ class SkippedGoalVerifier { - /** Verifies that a Jib goal is skipped. */ - static void verifyGoalIsSkipped(TestProject testProject, String goal) + /** Verifies that a Jib goal is skipped with {@code jib.skip=true}. */ + static void verifyJibSkip(TestProject testProject, String goal) throws VerificationException, IOException { Verifier verifier = new Verifier(testProject.getProjectRoot().toString()); verifier.setAutoclean(false); @@ -47,5 +47,24 @@ static void verifyGoalIsSkipped(TestProject testProject, String goal) + "[INFO] BUILD SUCCESS")); } + /** Verifies that a Jib goal is skipped with {@code jib.containerize=noGroup:noArtifact}. */ + static void verifyJibContainerizeSkips(TestProject testProject, String goal) + throws VerificationException, IOException { + Verifier verifier = new Verifier(testProject.getProjectRoot().toString()); + verifier.setAutoclean(false); + // noGroup:noArtifact should never match + verifier.setSystemProperty("jib.containerize", "noGroup:noArtifact"); + + verifier.executeGoal("jib:" + goal); + + Path logFile = Paths.get(verifier.getBasedir(), verifier.getLogFileName()); + Assert.assertThat( + new String(Files.readAllBytes(logFile), StandardCharsets.UTF_8), + CoreMatchers.containsString( + "[INFO] Skipping containerization of this module (not specified in jib.containerize)\n" + + "[INFO] ------------------------------------------------------------------------\n" + + "[INFO] BUILD SUCCESS")); + } + private SkippedGoalVerifier() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index ad2a6d07b2..d7cc87ea97 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -58,6 +58,7 @@ public class PropertyNames { public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; public static final String SKIP = "jib.skip"; public static final String CONSOLE = "jib.console"; + public static final String CONTAINERIZE = "jib.containerize"; private PropertyNames() {} } From d3b1e8c19b36427c84c5a7c6cc034e3e6d87a2d6 Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 15 May 2019 10:41:45 -0400 Subject: [PATCH 0591/2020] Evaluate settings.xml later, and per id (#1712) * Evaluate settings.xml later, and per id - Also adds in SettingsFixture for tests that want to use an actual instance of settings.xml and optionally can create a SettingsDecrypter from a settings-security.xml --- .../tools/jib/maven/BuildDockerMojo.java | 9 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 9 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 9 +- .../jib/maven/DecryptedMavenSettings.java | 67 ---- ...r.java => MavenSettingsProxyProvider.java} | 60 +++- .../maven/MavenSettingsServerCredentials.java | 43 ++- .../jib/maven/DecryptedMavenSettingsTest.java | 104 ------ .../maven/MavenSettingsProxyProviderTest.java | 301 ++++++++++++++++++ .../MavenSettingsServerCredentialsTest.java | 76 +++-- .../tools/jib/maven/ProxyProviderTest.java | 268 ---------------- .../tools/jib/maven/SettingsFixture.java | 89 ++++++ .../settings/bad-encrypted-proxy-settings.xml | 13 + .../settings/encrypted-proxy-settings.xml | 22 ++ .../settings/http-only-proxy-settings.xml | 25 ++ .../settings/https-only-proxy-settings.xml | 25 ++ .../settings/no-active-proxy-settings.xml | 22 ++ .../src/test/resources/maven/settings/readme | 13 + .../settings/settings-security.empty.xml | 0 .../maven/settings/settings-security.xml | 3 + .../resources/maven/settings/settings.xml | 19 ++ .../plugins/common/InferredAuthException.java | 27 ++ .../plugins/common/InferredAuthProvider.java | 32 ++ .../common/PluginConfigurationProcessor.java | 33 +- 23 files changed, 750 insertions(+), 519 deletions(-) delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java rename jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/{ProxyProvider.java => MavenSettingsProxyProvider.java} (56%) delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/bad-encrypted-proxy-settings.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/encrypted-proxy-settings.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/http-only-proxy-settings.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/https-only-proxy-settings.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/no-active-proxy-settings.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/readme create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/settings-security.empty.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/settings-security.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/settings/settings.xml create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthException.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 80fb2b0e9a..a1d319c22f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -110,18 +110,17 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - DecryptedMavenSettings decryptedSettings = - DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); - PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( mavenRawConfiguration, - new MavenSettingsServerCredentials(decryptedSettings), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter()), projectProperties, dockerExecutable, getDockerClientEnvironment(), mavenHelpfulSuggestionsBuilder.build()); - ProxyProvider.init(decryptedSettings); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index df14c141bf..a45294b7b1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -105,15 +105,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { EventDispatcher eventDispatcher = new DefaultEventDispatcher(projectProperties.getEventHandlers()); - DecryptedMavenSettings decryptedSettings = - DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); - PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( mavenRawConfiguration, - new MavenSettingsServerCredentials(decryptedSettings), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter()), projectProperties); - ProxyProvider.init(decryptedSettings); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); HelpfulSuggestions helpfulSuggestions = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index ecb2f1b487..811d18df00 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -84,19 +84,18 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - DecryptedMavenSettings decryptedSettings = - DecryptedMavenSettings.from(getSession().getSettings(), getSettingsDecrypter()); - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); Path tarOutputPath = buildOutput.resolve("jib-image.tar"); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForTarImage( mavenRawConfiguration, - new MavenSettingsServerCredentials(decryptedSettings), + new MavenSettingsServerCredentials( + getSession().getSettings(), getSettingsDecrypter()), projectProperties, tarOutputPath, mavenHelpfulSuggestionsBuilder.build()); - ProxyProvider.init(decryptedSettings); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); HelpfulSuggestions helpfulSuggestions = mavenHelpfulSuggestionsBuilder diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java deleted file mode 100644 index 23b19185f4..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettings.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import java.util.List; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.settings.Proxy; -import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.building.SettingsProblem; -import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; - -/** Provides decrypted Maven settings information. */ -class DecryptedMavenSettings { - - static DecryptedMavenSettings from(Settings settings, SettingsDecrypter decryptor) - throws MojoExecutionException { - SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(settings); - SettingsDecryptionResult result = decryptor.decrypt(request); - // Un-encrypted passwords are passed through, so a problem indicates a real issue. - // If there are any ERROR or FATAL problems reported, then decryption failed. - for (SettingsProblem problem : result.getProblems()) { - if (problem.getSeverity() == SettingsProblem.Severity.ERROR - || problem.getSeverity() == SettingsProblem.Severity.FATAL) { - throw new MojoExecutionException("Unable to decrypt settings.xml: " + problem); - } - } - return new DecryptedMavenSettings(result, settings); - } - - private final SettingsDecryptionResult result; - private final Settings settings; - - private DecryptedMavenSettings(SettingsDecryptionResult result, Settings settings) { - this.result = result; - this.settings = settings; - } - - List getServers() { - // SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return - // results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers - // can be empty. If the decrypted result returns an empty list, we fall back to the original - // settings. - return result.getServers().isEmpty() ? settings.getServers() : result.getServers(); - } - - List getProxies() { - return result.getProxies().isEmpty() ? settings.getProxies() : result.getProxies(); - } -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProvider.java similarity index 56% rename from jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java rename to jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProvider.java index 157ac8146c..00152be27c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProxyProvider.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProvider.java @@ -18,33 +18,62 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; import javax.annotation.Nullable; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; /** Propagates proxy configuration from Maven settings to system properties. */ -class ProxyProvider { +class MavenSettingsProxyProvider { private static final ImmutableList PROXY_PROPERTIES = ImmutableList.of("proxyHost", "proxyPort", "proxyUser", "proxyPassword"); /** - * Initializes proxy settings based on Maven settings. + * Initializes proxy settings based on Maven settings if they are not already set by the user + * directly. * * @param settings Maven settings */ - static void init(DecryptedMavenSettings settings) { - configureProxy(settings, "https"); - configureProxy(settings, "http"); - } + static void activateHttpAndHttpsProxies(Settings settings, SettingsDecrypter decrypter) + throws MojoExecutionException { + List proxies = new ArrayList<>(2); + for (String protocol : ImmutableList.of("http", "https")) { + if (areProxyPropertiesSet(protocol)) { + continue; + } + settings + .getProxies() + .stream() + .filter(Proxy::isActive) + .filter(proxy -> protocol.equals(proxy.getProtocol())) + .findFirst() + .ifPresent(proxies::add); + } - private static void configureProxy(DecryptedMavenSettings settings, String protocol) { - settings - .getProxies() - .stream() - .filter(Proxy::isActive) - .filter(proxy -> protocol.equals(proxy.getProtocol())) - .findFirst() - .ifPresent(ProxyProvider::setProxyProperties); + if (proxies.size() == 0) { + return; + } + + SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest().setProxies(proxies); + SettingsDecryptionResult result = decrypter.decrypt(request); + + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new MojoExecutionException( + "Unable to decrypt proxy info from settings.xml: " + problem); + } + } + + result.getProxies().forEach(MavenSettingsProxyProvider::setProxyProperties); } /** @@ -55,9 +84,6 @@ private static void configureProxy(DecryptedMavenSettings settings, String proto @VisibleForTesting static void setProxyProperties(Proxy proxy) { String protocol = proxy.getProtocol(); - if (areProxyPropertiesSet(protocol)) { - return; - } setPropertySafe(protocol + ".proxyHost", proxy.getHost()); setPropertySafe(protocol + ".proxyPort", String.valueOf(proxy.getPort())); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 322a563c5f..a42d4a6f6e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -17,28 +17,36 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.plugins.common.AuthProperty; +import com.google.cloud.tools.jib.plugins.common.InferredAuthException; +import com.google.cloud.tools.jib.plugins.common.InferredAuthProvider; import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.SettingsProblem; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; /** * Retrieves credentials for servers defined in Maven settings. */ -class MavenSettingsServerCredentials implements Function> { +class MavenSettingsServerCredentials implements InferredAuthProvider { static final String CREDENTIAL_SOURCE = "Maven settings"; - private final DecryptedMavenSettings settings; + private final Settings settings; + private final SettingsDecrypter decrypter; /** * Create new instance. * * @param settings decrypted Maven settings */ - MavenSettingsServerCredentials(DecryptedMavenSettings settings) { + MavenSettingsServerCredentials(Settings settings, SettingsDecrypter decrypter) { this.settings = settings; + this.decrypter = decrypter; } /** @@ -48,15 +56,28 @@ class MavenSettingsServerCredentials implements Function apply(String registry) { - Predicate idMatches = server -> registry.equals(server.getId()); - Optional server = settings.getServers().stream().filter(idMatches).findFirst(); - if (!server.isPresent()) { + public Optional inferAuth(String registry) throws InferredAuthException { + + Server server = settings.getServer(registry); + if (server == null) { return Optional.empty(); } - String username = server.get().getUsername(); - String password = server.get().getPassword(); + SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(server); + SettingsDecryptionResult result = decrypter.decrypt(request); + // Un-encrypted passwords are passed through, so a problem indicates a real issue. + // If there are any ERROR or FATAL problems reported, then decryption failed. + for (SettingsProblem problem : result.getProblems()) { + if (problem.getSeverity() == SettingsProblem.Severity.ERROR + || problem.getSeverity() == SettingsProblem.Severity.FATAL) { + throw new InferredAuthException( + "Unable to decrypt server(" + registry + ") info from settings.xml: " + problem); + } + } + Server resultServer = result.getServer(); + + String username = resultServer.getUsername(); + String password = resultServer.getPassword(); return Optional.of( new AuthProperty() { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java deleted file mode 100644 index 934c11cf9a..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/DecryptedMavenSettingsTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.settings.Proxy; -import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.building.SettingsProblem; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link DecryptedMavenSettings}. */ -@RunWith(MockitoJUnitRunner.class) -public class DecryptedMavenSettingsTest { - - @Mock private Server server1; - @Mock private Server server2; - @Mock private Proxy proxy; - @Mock private Settings settings; - @Mock private SettingsDecrypter settingsDecrypter; - @Mock private SettingsDecryptionResult decryptionResult; - - private final List servers = Arrays.asList(server1, server2); - private final List proxies = Arrays.asList(proxy); - - private DecryptedMavenSettings decryptedSettings; - - @Before - public void setUp() throws MojoExecutionException { - Mockito.when(settingsDecrypter.decrypt(Mockito.any())).thenReturn(decryptionResult); - Mockito.when(decryptionResult.getProblems()).thenReturn(Collections.emptyList()); - - decryptedSettings = DecryptedMavenSettings.from(settings, settingsDecrypter); - } - - @Test - public void testFrom_decrypterFailure() { - SettingsProblem problem = Mockito.mock(SettingsProblem.class); - Mockito.when(problem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR); - // Maven's SettingsProblem has a more structured toString, but irrelevant here - Mockito.when(problem.toString()).thenReturn("MockProblemText"); - Mockito.when(decryptionResult.getProblems()).thenReturn(Arrays.asList(problem)); - - try { - DecryptedMavenSettings.from(settings, settingsDecrypter); - Assert.fail(); - } catch (MojoExecutionException ex) { - Assert.assertEquals("Unable to decrypt settings.xml: MockProblemText", ex.getMessage()); - } - } - - @Test - public void testGetServers() { - Mockito.when(decryptionResult.getServers()).thenReturn(servers); - Assert.assertEquals(servers, decryptedSettings.getServers()); - } - - @Test - public void testGetProxies() { - Mockito.when(decryptionResult.getProxies()).thenReturn(proxies); - Assert.assertEquals(proxies, decryptedSettings.getProxies()); - } - - @Test - public void testGetServers_emptyListFromDecryption() { - Mockito.when(decryptionResult.getServers()).thenReturn(Collections.emptyList()); - Mockito.when(settings.getServers()).thenReturn(servers); - - Assert.assertEquals(servers, decryptedSettings.getServers()); - } - - @Test - public void testGetProxies_emptyListFromDecryption() { - Mockito.when(decryptionResult.getProxies()).thenReturn(Collections.emptyList()); - Mockito.when(settings.getProxies()).thenReturn(proxies); - - Assert.assertEquals(proxies, decryptedSettings.getProxies()); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java new file mode 100644 index 0000000000..f0d1c08a5c --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.common.collect.ImmutableList; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.settings.Proxy; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.hamcrest.CoreMatchers; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +/** Test for {@link MavenSettingsProxyProvider}. */ +@RunWith(MockitoJUnitRunner.class) +public class MavenSettingsProxyProviderTest { + + private static Settings noActiveProxiesSettings; + private static Settings httpOnlyProxySettings; + private static Settings httpsOnlyProxySettings; + private static Settings mixedProxyEncryptedSettings; + private static Settings badProxyEncryptedSettings; + private static SettingsDecrypter settingsDecrypter; + private static SettingsDecrypter emptySettingsDescypter; + + private static final ImmutableList proxyProperties = + ImmutableList.of( + "http.proxyHost", + "http.proxyPort", + "http.proxyUser", + "http.proxyPassword", + "https.proxyHost", + "https.proxyPort", + "https.proxyUser", + "https.proxyPassword", + "http.nonProxyHosts"); + + // HashMap to allow saving null values. + private final HashMap savedProperties = new HashMap<>(); + + @BeforeClass + public static void setUpTestFixtures() { + noActiveProxiesSettings = + SettingsFixture.newSettings( + Paths.get("src/test/resources/maven/settings/no-active-proxy-settings.xml")); + httpOnlyProxySettings = + SettingsFixture.newSettings( + Paths.get("src/test/resources/maven/settings/http-only-proxy-settings.xml")); + httpsOnlyProxySettings = + SettingsFixture.newSettings( + Paths.get("src/test/resources/maven/settings/https-only-proxy-settings.xml")); + mixedProxyEncryptedSettings = + SettingsFixture.newSettings( + Paths.get("src/test/resources/maven/settings/encrypted-proxy-settings.xml")); + badProxyEncryptedSettings = + SettingsFixture.newSettings( + Paths.get("src/test/resources/maven/settings/bad-encrypted-proxy-settings.xml")); + settingsDecrypter = + SettingsFixture.newSettingsDecrypter( + Paths.get("src/test/resources/maven/settings/settings-security.xml")); + emptySettingsDescypter = + SettingsFixture.newSettingsDecrypter( + Paths.get("src/test/resources/maven/settings/settings-security.empty.xml")); + } + + @Before + public void setUp() { + proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); + proxyProperties.stream().forEach(key -> System.clearProperty(key)); + } + + @After + public void tearDown() { + Consumer> restoreProperty = + entry -> { + if (entry.getValue() == null) { + System.clearProperty(entry.getKey()); + } else { + System.setProperty(entry.getKey(), entry.getValue()); + } + }; + savedProperties.entrySet().stream().forEach(restoreProperty); + } + + @Test + public void testAreProxyPropertiesSet() { + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpHostSet() { + System.setProperty("http.proxyHost", "host"); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsHostSet() { + System.setProperty("https.proxyHost", "host"); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpPortSet() { + System.setProperty("http.proxyPort", "port"); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsPortSet() { + System.setProperty("https.proxyPort", "port"); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpUserSet() { + System.setProperty("http.proxyUser", "user"); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsUserSet() { + System.setProperty("https.proxyUser", "user"); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpPasswordSet() { + System.setProperty("http.proxyPassword", "password"); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_httpsPasswordSet() { + System.setProperty("https.proxyPassword", "password"); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertTrue(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testAreProxyPropertiesSet_ignoresHttpNonProxyHosts() { + System.setProperty("http.nonProxyHosts", "non proxy hosts"); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("http")); + Assert.assertFalse(MavenSettingsProxyProvider.areProxyPropertiesSet("https")); + } + + @Test + public void testSetProxyProperties() { + Proxy httpProxy = new Proxy(); + httpProxy.setProtocol("http"); + httpProxy.setHost("host"); + httpProxy.setPort(1080); + httpProxy.setUsername("user"); + httpProxy.setPassword("pass"); + httpProxy.setNonProxyHosts("non proxy hosts"); + + MavenSettingsProxyProvider.setProxyProperties(httpProxy); + Assert.assertEquals("host", System.getProperty("http.proxyHost")); + Assert.assertEquals("1080", System.getProperty("http.proxyPort")); + Assert.assertEquals("user", System.getProperty("http.proxyUser")); + Assert.assertEquals("pass", System.getProperty("http.proxyPassword")); + Assert.assertEquals("non proxy hosts", System.getProperty("http.nonProxyHosts")); + + Proxy httpsProxy = new Proxy(); + httpsProxy.setProtocol("https"); + httpsProxy.setHost("https host"); + httpsProxy.setPort(1443); + httpsProxy.setUsername("https user"); + httpsProxy.setPassword("https pass"); + MavenSettingsProxyProvider.setProxyProperties(httpsProxy); + Assert.assertEquals("https host", System.getProperty("https.proxyHost")); + Assert.assertEquals("1443", System.getProperty("https.proxyPort")); + Assert.assertEquals("https user", System.getProperty("https.proxyUser")); + Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); + } + + @Test + public void testSetProxyProperties_someValuesUndefined() { + Proxy httpProxy = new Proxy(); + httpProxy.setProtocol("http"); + httpProxy.setHost("http://host"); + + MavenSettingsProxyProvider.setProxyProperties(httpProxy); + Assert.assertEquals("http://host", System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("http.proxyUser")); + Assert.assertNull(System.getProperty("http.proxyPassword")); + Assert.assertNull(System.getProperty("http.nonProxyHosts")); + + Proxy httpsProxy = new Proxy(); + httpsProxy.setProtocol("https"); + httpsProxy.setUsername("https user"); + httpsProxy.setPassword("https pass"); + MavenSettingsProxyProvider.setProxyProperties(httpsProxy); + Assert.assertNull(System.getProperty("https.proxyHost")); + Assert.assertEquals("https user", System.getProperty("https.proxyUser")); + Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); + } + + @Test + public void testActivateHttpAndHttpsProxies_noActiveProxy() throws MojoExecutionException { + + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + noActiveProxiesSettings, settingsDecrypter); + + Assert.assertNull(System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("https.proxyHost")); + } + + @Test + public void testActivateHttpAndHttpsProxies_firstActiveHttpProxy() throws MojoExecutionException { + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + httpOnlyProxySettings, settingsDecrypter); + + Assert.assertEquals("proxy2.example.com", System.getProperty("http.proxyHost")); + Assert.assertNull(System.getProperty("https.proxyHost")); + } + + @Test + public void testActivateHttpAndHttpsProxies_firstActiveHttpsProxy() + throws MojoExecutionException { + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + httpsOnlyProxySettings, settingsDecrypter); + + Assert.assertEquals("proxy2.example.com", System.getProperty("https.proxyHost")); + Assert.assertNull(System.getProperty("http.proxyHost")); + } + + @Test + public void testActivateHttpAndHttpsProxies_encryptedProxy() throws MojoExecutionException { + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + mixedProxyEncryptedSettings, settingsDecrypter); + + Assert.assertEquals("password1", System.getProperty("http.proxyPassword")); + Assert.assertEquals("password2", System.getProperty("https.proxyPassword")); + } + + @Test + public void testActivateHttpAndHttpsProxies_dontOverwriteUserHttp() + throws MojoExecutionException { + System.setProperty("http.proxyHost", "host"); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + mixedProxyEncryptedSettings, settingsDecrypter); + + Assert.assertNull(System.getProperty("http.proxyPassword")); + Assert.assertEquals("password2", System.getProperty("https.proxyPassword")); + } + + @Test + public void testActivateHttpAndHttpsProxies_dontOverwriteUserHttps() + throws MojoExecutionException { + System.setProperty("https.proxyHost", "host"); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + mixedProxyEncryptedSettings, settingsDecrypter); + + Assert.assertEquals("password1", System.getProperty("http.proxyPassword")); + Assert.assertNull(System.getProperty("https.proxyPassword")); + } + + @Test + public void testActivateHttpAndHttpsProxies_decryptionFailure() { + try { + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + badProxyEncryptedSettings, settingsDecrypter); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith("Unable to decrypt proxy info from settings.xml:")); + } + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index f1c8663c78..45e6c04682 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -17,45 +17,77 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.plugins.common.AuthProperty; -import java.util.Arrays; +import com.google.cloud.tools.jib.plugins.common.InferredAuthException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; -import org.apache.maven.settings.Server; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link MavenSettingsServerCredentials}. */ -@RunWith(MockitoJUnitRunner.class) public class MavenSettingsServerCredentialsTest { - @Mock private DecryptedMavenSettings mockSettings; - @Mock private Server mockServer1; - - private MavenSettingsServerCredentials testMavenSettingsServerCredentials; + private MavenSettingsServerCredentials mavenSettingsServerCredentialsNoMasterPassword; + private MavenSettingsServerCredentials mavenSettingsServerCredentials; + private Path testSettings = Paths.get("src/test/resources/maven/settings/settings.xml"); + private Path testSettingsSecurity = + Paths.get("src/test/resources/maven/settings/settings-security.xml"); + private Path testSettingsSecurityEmpty = + Paths.get("src/test/resources/maven/settings/settings-security.empty.xml"); @Before public void setUp() { - Mockito.when(mockSettings.getServers()).thenReturn(Arrays.asList(mockServer1)); - Mockito.when(mockServer1.getId()).thenReturn("server1"); - Mockito.when(mockServer1.getUsername()).thenReturn("server1 username"); - Mockito.when(mockServer1.getPassword()).thenReturn("server1 password"); - testMavenSettingsServerCredentials = new MavenSettingsServerCredentials(mockSettings); + mavenSettingsServerCredentials = + new MavenSettingsServerCredentials( + SettingsFixture.newSettings(testSettings), + SettingsFixture.newSettingsDecrypter(testSettingsSecurity)); + mavenSettingsServerCredentialsNoMasterPassword = + new MavenSettingsServerCredentials( + SettingsFixture.newSettings(testSettings), + SettingsFixture.newSettingsDecrypter(testSettingsSecurityEmpty)); + } + + @Test + public void testInferredAuth_decrypterFailure() { + try { + mavenSettingsServerCredentials.inferAuth("badServer"); + Assert.fail(); + } catch (InferredAuthException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith("Unable to decrypt server(badServer) info from settings.xml:")); + } + } + + @Test + public void testInferredAuth_successEncrypted() throws InferredAuthException { + Optional auth = mavenSettingsServerCredentials.inferAuth("encryptedServer"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("encryptedUser", auth.get().getUsername()); + Assert.assertEquals("password1", auth.get().getPassword()); + } + + @Test + public void testInferredAuth_successUnencrypted() throws InferredAuthException { + Optional auth = mavenSettingsServerCredentials.inferAuth("simpleServer"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("simpleUser", auth.get().getUsername()); + Assert.assertEquals("password2", auth.get().getPassword()); } @Test - public void testRetrieve_found() { - Optional auth = testMavenSettingsServerCredentials.apply("server1"); + public void testInferredAuth_successNoPasswordDoesNotBlowUp() throws InferredAuthException { + Optional auth = + mavenSettingsServerCredentialsNoMasterPassword.inferAuth("simpleServer"); Assert.assertTrue(auth.isPresent()); - Assert.assertEquals("server1 username", auth.get().getUsername()); - Assert.assertEquals("server1 password", auth.get().getPassword()); + Assert.assertEquals("simpleUser", auth.get().getUsername()); + Assert.assertEquals("password2", auth.get().getPassword()); } @Test - public void testRetrieve_notFound() { - Assert.assertFalse(testMavenSettingsServerCredentials.apply("serverUnknown").isPresent()); + public void testInferredAuth_notFound() throws InferredAuthException { + Assert.assertFalse(mavenSettingsServerCredentials.inferAuth("serverUnknown").isPresent()); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java deleted file mode 100644 index caf5e158a6..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProxyProviderTest.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.common.collect.ImmutableList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; -import org.apache.maven.settings.Proxy; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; - -/** Test for {@link ProxyProvider}. */ -public class ProxyProviderTest { - - private static final ImmutableList proxyProperties = - ImmutableList.of( - "http.proxyHost", - "http.proxyPort", - "http.proxyUser", - "http.proxyPassword", - "https.proxyHost", - "https.proxyPort", - "https.proxyUser", - "https.proxyPassword", - "http.nonProxyHosts"); - - // HashMap to allow saving null values. - private final HashMap savedProperties = new HashMap<>(); - - @Before - public void setUp() { - proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); - proxyProperties.stream().forEach(key -> System.clearProperty(key)); - } - - @After - public void tearDown() { - Consumer> restoreProperty = - entry -> { - if (entry.getValue() == null) { - System.clearProperty(entry.getKey()); - } else { - System.setProperty(entry.getKey(), entry.getValue()); - } - }; - savedProperties.entrySet().stream().forEach(restoreProperty); - } - - @Test - public void testAreProxyPropertiesSet() { - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpHostSet() { - System.setProperty("http.proxyHost", "host"); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpsHostSet() { - System.setProperty("https.proxyHost", "host"); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpPortSet() { - System.setProperty("http.proxyPort", "port"); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpsPortSet() { - System.setProperty("https.proxyPort", "port"); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpUserSet() { - System.setProperty("http.proxyUser", "user"); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpsUserSet() { - System.setProperty("https.proxyUser", "user"); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpPasswordSet() { - System.setProperty("http.proxyPassword", "password"); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_httpsPasswordSet() { - System.setProperty("https.proxyPassword", "password"); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertTrue(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testAreProxyPropertiesSet_ignoresHttpNonProxyHosts() { - System.setProperty("http.nonProxyHosts", "non proxy hosts"); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("http")); - Assert.assertFalse(ProxyProvider.areProxyPropertiesSet("https")); - } - - @Test - public void testSetProxyProperties() { - Proxy httpProxy = new Proxy(); - httpProxy.setProtocol("http"); - httpProxy.setHost("host"); - httpProxy.setPort(1080); - httpProxy.setUsername("user"); - httpProxy.setPassword("pass"); - httpProxy.setNonProxyHosts("non proxy hosts"); - - ProxyProvider.setProxyProperties(httpProxy); - Assert.assertEquals("host", System.getProperty("http.proxyHost")); - Assert.assertEquals("1080", System.getProperty("http.proxyPort")); - Assert.assertEquals("user", System.getProperty("http.proxyUser")); - Assert.assertEquals("pass", System.getProperty("http.proxyPassword")); - Assert.assertEquals("non proxy hosts", System.getProperty("http.nonProxyHosts")); - - Proxy httpsProxy = new Proxy(); - httpsProxy.setProtocol("https"); - httpsProxy.setHost("https host"); - httpsProxy.setPort(1443); - httpsProxy.setUsername("https user"); - httpsProxy.setPassword("https pass"); - ProxyProvider.setProxyProperties(httpsProxy); - Assert.assertEquals("https host", System.getProperty("https.proxyHost")); - Assert.assertEquals("1443", System.getProperty("https.proxyPort")); - Assert.assertEquals("https user", System.getProperty("https.proxyUser")); - Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); - } - - @Test - public void testSetProxyProperties_someValuesUndefined() { - Proxy httpProxy = new Proxy(); - httpProxy.setProtocol("http"); - httpProxy.setHost("http://host"); - - ProxyProvider.setProxyProperties(httpProxy); - Assert.assertEquals("http://host", System.getProperty("http.proxyHost")); - Assert.assertNull(System.getProperty("http.proxyUser")); - Assert.assertNull(System.getProperty("http.proxyPassword")); - Assert.assertNull(System.getProperty("http.nonProxyHosts")); - - Proxy httpsProxy = new Proxy(); - httpsProxy.setProtocol("https"); - httpsProxy.setUsername("https user"); - httpsProxy.setPassword("https pass"); - ProxyProvider.setProxyProperties(httpsProxy); - Assert.assertNull(System.getProperty("https.proxyHost")); - Assert.assertEquals("https user", System.getProperty("https.proxyUser")); - Assert.assertEquals("https pass", System.getProperty("https.proxyPassword")); - } - - @Test - public void testInit_noActiveProxy() { - Proxy httpProxy = new Proxy(); - httpProxy.setProtocol("http"); - httpProxy.setHost("proxy1 host"); - httpProxy.setActive(false); - - Proxy httpsProxy = new Proxy(); - httpsProxy.setProtocol("https"); - httpsProxy.setHost("proxy2 host"); - httpsProxy.setActive(false); - - DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); - Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(httpProxy, httpsProxy)); - ProxyProvider.init(settings); - - Assert.assertNull(System.getProperty("http.proxyHost")); - Assert.assertNull(System.getProperty("https.proxyHost")); - } - - @Test - public void testInit_firstActiveHttpProxy() { - Proxy proxy1 = new Proxy(); - proxy1.setProtocol("http"); - proxy1.setHost("proxy1 host"); - proxy1.setActive(false); - - Proxy proxy2 = new Proxy(); - proxy2.setProtocol("http"); - proxy2.setHost("proxy2 host"); - proxy2.setActive(true); - - Proxy proxy3 = new Proxy(); - proxy3.setProtocol("http"); - proxy3.setHost("proxy3 host"); - proxy3.setActive(false); - - Proxy proxy4 = new Proxy(); - proxy4.setProtocol("http"); - proxy4.setHost("proxy4 host"); - proxy4.setActive(true); - - DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); - Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); - ProxyProvider.init(settings); - - Assert.assertEquals("proxy2 host", System.getProperty("http.proxyHost")); - Assert.assertNull(System.getProperty("https.proxyHost")); - } - - @Test - public void testInit_firstActiveHttpsProxy() { - Proxy proxy1 = new Proxy(); - proxy1.setProtocol("https"); - proxy1.setHost("proxy1 host"); - proxy1.setActive(false); - - Proxy proxy2 = new Proxy(); - proxy2.setProtocol("https"); - proxy2.setHost("proxy2 host"); - proxy2.setActive(true); - - Proxy proxy3 = new Proxy(); - proxy3.setProtocol("https"); - proxy3.setHost("proxy3 host"); - proxy3.setActive(false); - - Proxy proxy4 = new Proxy(); - proxy4.setProtocol("https"); - proxy4.setHost("proxy4 host"); - proxy4.setActive(true); - - DecryptedMavenSettings settings = Mockito.mock(DecryptedMavenSettings.class); - Mockito.when(settings.getProxies()).thenReturn(Arrays.asList(proxy1, proxy2, proxy3, proxy4)); - ProxyProvider.init(settings); - - Assert.assertNull(System.getProperty("http.proxyHost")); - Assert.assertEquals("proxy2 host", System.getProperty("https.proxyHost")); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java new file mode 100644 index 0000000000..8fe60a6dbe --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.common.base.Preconditions; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.building.DefaultSettingsBuilderFactory; +import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; +import org.apache.maven.settings.building.SettingsBuilder; +import org.apache.maven.settings.building.SettingsBuildingException; +import org.apache.maven.settings.building.SettingsBuildingRequest; +import org.apache.maven.settings.crypto.DefaultSettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.sonatype.plexus.components.cipher.DefaultPlexusCipher; +import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher; + +public class SettingsFixture { + + /** + * Create a new {@link Settings} for testing purposes. + * + * @param settingsFile absolute path to settings.xml + * @return {@link Settings} built from settingsFile + */ + public static Settings newSettings(Path settingsFile) { + Preconditions.checkArgument(Files.isRegularFile(settingsFile)); + try { + SettingsBuilder settingsBuilder = new DefaultSettingsBuilderFactory().newInstance(); + SettingsBuildingRequest settingsRequest = new DefaultSettingsBuildingRequest(); + settingsRequest.setUserSettingsFile(settingsFile.toFile()); + return settingsBuilder.build(settingsRequest).getEffectiveSettings(); + } catch (SettingsBuildingException ex) { + throw new IllegalStateException("Tests need to be rewritten: " + ex.getMessage(), ex); + } + } + + /** + * Create a new {@link SettingsDecrypter} for testing purposes. + * + * @param settingsSecurityFile absolute path to security-settings.xml + * @return {@link SettingsDecrypter} built from settingsSecurityFile + */ + public static SettingsDecrypter newSettingsDecrypter(Path settingsSecurityFile) { + Preconditions.checkArgument(Files.isRegularFile(settingsSecurityFile)); + try { + + DefaultPlexusCipher injectCypher = new DefaultPlexusCipher(); + + DefaultSecDispatcher injectedDispatcher = new DefaultSecDispatcher(); + injectedDispatcher.setConfigurationFile(settingsSecurityFile.toAbsolutePath().toString()); + setField(DefaultSecDispatcher.class, injectedDispatcher, "_cipher", injectCypher); + + DefaultSettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter(); + setField( + DefaultSettingsDecrypter.class, + settingsDecrypter, + "securityDispatcher", + injectedDispatcher); + return settingsDecrypter; + } catch (Exception ex) { + throw new IllegalStateException("Tests need to be rewritten: " + ex.getMessage(), ex); + } + } + + private static void setField( + Class clazz, T instance, String fieldName, Object injectedField) + throws NoSuchFieldException, IllegalAccessException { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, injectedField); + } +} diff --git a/jib-maven-plugin/src/test/resources/maven/settings/bad-encrypted-proxy-settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/bad-encrypted-proxy-settings.xml new file mode 100644 index 0000000000..d78c6d62f5 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/bad-encrypted-proxy-settings.xml @@ -0,0 +1,13 @@ + + + + http + true + http + proxy.example.com + 8080 + proxyuser + {i-made-this-up=} + + + \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/maven/settings/encrypted-proxy-settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/encrypted-proxy-settings.xml new file mode 100644 index 0000000000..4edc1f040c --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/encrypted-proxy-settings.xml @@ -0,0 +1,22 @@ + + + + http + true + http + proxy1.example.com + 8080 + proxyuser + {AmwLphSL7qQHMPGxPn6NHQFNkU+ME6BU550hxoQrKHk=} + + + https + true + https + proxy2.example.com + 8443 + proxyuser + password2 + + + \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/maven/settings/http-only-proxy-settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/http-only-proxy-settings.xml new file mode 100644 index 0000000000..02ef8934de --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/http-only-proxy-settings.xml @@ -0,0 +1,25 @@ + + + + http1 + false + http + proxy1.example.com + 8080 + + + http2 + true + http + proxy2.example.com + 8080 + + + http3 + true + http + proxy3.example.com + 8080 + + + \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/maven/settings/https-only-proxy-settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/https-only-proxy-settings.xml new file mode 100644 index 0000000000..22e3c62eb9 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/https-only-proxy-settings.xml @@ -0,0 +1,25 @@ + + + + https1 + false + https + proxy1.example.com + 8443 + + + https2 + true + https + proxy2.example.com + 8443 + + + https3 + true + https + proxy3.example.com + 8443 + + + \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/maven/settings/no-active-proxy-settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/no-active-proxy-settings.xml new file mode 100644 index 0000000000..45702fdec9 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/no-active-proxy-settings.xml @@ -0,0 +1,22 @@ + + + + http + false + http + proxy.example.com + 8080 + proxyuser + somepassword + + + https + false + https + proxy.example.com + 8443 + proxyuser + somepassword + + + \ No newline at end of file diff --git a/jib-maven-plugin/src/test/resources/maven/settings/readme b/jib-maven-plugin/src/test/resources/maven/settings/readme new file mode 100644 index 0000000000..0dd5e2c689 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/readme @@ -0,0 +1,13 @@ +Generate settings-security.xml with, if you change this, you have to change all the files in settings.xml, so just leave it as is: + +$ mvn --encrypt-master-password +Master password: +{generatedValue=} +^^^ copy this password into security-settings.xml + +Create settings.xml with mixed encrypted and unencrpted values, to encrypt a password value use: + +$ mvn --encrypt-password -Dsettings.security= +Password: +{generatedValue=} +^^^ copy this password into the right spot in settings.xml diff --git a/jib-maven-plugin/src/test/resources/maven/settings/settings-security.empty.xml b/jib-maven-plugin/src/test/resources/maven/settings/settings-security.empty.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-maven-plugin/src/test/resources/maven/settings/settings-security.xml b/jib-maven-plugin/src/test/resources/maven/settings/settings-security.xml new file mode 100644 index 0000000000..c136e67634 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/settings-security.xml @@ -0,0 +1,3 @@ + + {}X6FMi5KzfGgHeRrZdTiwUdXfVSSqcNdXvVjt3tw7oxs= + diff --git a/jib-maven-plugin/src/test/resources/maven/settings/settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/settings.xml new file mode 100644 index 0000000000..f5fc0a4dcb --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/settings/settings.xml @@ -0,0 +1,19 @@ + + + + encryptedServer + encryptedUser + {AmwLphSL7qQHMPGxPn6NHQFNkU+ME6BU550hxoQrKHk=} + + + simpleServer + simpleUser + password2 + + + badServer + badUser + {i-made-this-up=} + + + diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthException.java new file mode 100644 index 0000000000..d53b1d96fc --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Indicates that the {@link InferredAuthProvider} encountered a failure while trying to determine + * auth credentials (not thrown for missing). + */ +public class InferredAuthException extends Exception { + public InferredAuthException(String message) { + super(message); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java new file mode 100644 index 0000000000..6b4e03ce5a --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InferredAuthProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.util.Optional; + +/** An auth provider specific to the client's architecture. */ +public interface InferredAuthProvider { + + /** + * Find auth credentials for a specific registry. + * + * @param registry we want credential for + * @return auth information for the registry (can be empty) + * @throws InferredAuthException if the auth discovery process resulted in a fatal error + */ + Optional inferAuth(String registry) throws InferredAuthException; +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 6b31f982a0..493a18f7b9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -48,7 +48,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import javax.annotation.Nullable; /** @@ -59,7 +58,7 @@ public class PluginConfigurationProcessor { public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( RawConfiguration rawConfiguration, - Function> inferredAuthProvider, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, @Nullable Path dockerExecutable, @Nullable Map dockerEnvironment, @@ -89,7 +88,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa public static PluginConfigurationProcessor processCommonConfigurationForTarImage( RawConfiguration rawConfiguration, - Function> inferredAuthProvider, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, Path tarImagePath, HelpfulSuggestions helpfulSuggestions) @@ -112,7 +111,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( RawConfiguration rawConfiguration, - Function> inferredAuthProvider, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, @@ -151,7 +150,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry @VisibleForTesting static PluginConfigurationProcessor processCommonConfiguration( RawConfiguration rawConfiguration, - Function> inferredAuthProvider, + InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, Containerizer containerizer, ImageReference targetImageReference, @@ -398,7 +397,7 @@ private static boolean configureCredentialRetrievers( String usernamePropertyName, String passwordPropertyName, AuthProperty knownAuth, - Function> inferredAuthProvider, + InferredAuthProvider inferredAuthProvider, @Nullable String credHelper) throws FileNotFoundException { DefaultCredentialRetrievers defaultCredentialRetrievers = @@ -416,15 +415,19 @@ private static boolean configureCredentialRetrievers( defaultCredentialRetrievers.setKnownCredential( optionalCredential.get(), knownAuth.getAuthDescriptor()); } else { - Optional optionalInferredAuth = - inferredAuthProvider.apply(imageReference.getRegistry()); - credentialPresent = optionalInferredAuth.isPresent(); - if (optionalInferredAuth.isPresent()) { - AuthProperty auth = optionalInferredAuth.get(); - String username = Verify.verifyNotNull(auth.getUsername()); - String password = Verify.verifyNotNull(auth.getPassword()); - Credential credential = Credential.from(username, password); - defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); + try { + Optional optionalInferredAuth = + inferredAuthProvider.inferAuth(imageReference.getRegistry()); + credentialPresent = optionalInferredAuth.isPresent(); + if (optionalInferredAuth.isPresent()) { + AuthProperty auth = optionalInferredAuth.get(); + String username = Verify.verifyNotNull(auth.getUsername()); + String password = Verify.verifyNotNull(auth.getPassword()); + Credential credential = Credential.from(username, password); + defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); + } + } catch (InferredAuthException ex) { + eventDispatcher.dispatch(LogEvent.warn("InferredAuthException: " + ex.getMessage())); } } defaultCredentialRetrievers.setCredentialHelper(credHelper); From 93525fa3bbf8c1ca6fa61b37e780def759de7b2e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 17 May 2019 10:38:38 -0400 Subject: [PATCH 0592/2020] Remove RegistryAuthenticator.Initializer (#1717) * Remove RegistryAuthenticator.Initializer * Clean up code * Make Authenticator immutable --- ...icationMethodRetrieverIntegrationTest.java | 2 +- .../RegistryAuthenticatorIntegrationTest.java | 12 +- .../builder/steps/AuthenticatePushStep.java | 31 ++--- .../jib/builder/steps/PullBaseImageStep.java | 71 +++++----- .../jib/registry/RegistryAuthenticator.java | 129 ++++-------------- .../AuthenticationMethodRetrieverTest.java | 2 +- .../registry/RegistryAuthenticatorTest.java | 31 +++-- 7 files changed, 96 insertions(+), 182 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index cebdc0476d..fa621cca24 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -35,7 +35,7 @@ public void testGetRegistryAuthenticator() throws IOException, RegistryException .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(); + Authorization authorization = registryAuthenticator.authenticatePull(null); RegistryClient authorizedRegistryClient = RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 2ff1e02939..3095a104b2 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -28,20 +27,19 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; - @Test public void testAuthenticate() throws IOException, RegistryException, InvalidImageReferenceException { ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - EVENT_DISPATCHER, + RegistryClient.factory( + ignored -> {}, dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) - .initialize(); + .newRegistryClient() + .getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(); + Authorization authorization = registryAuthenticator.authenticatePull(null); // Checks that some token was received. Assert.assertTrue(0 < authorization.getToken().length()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 0992b4535d..88aed8438f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; @@ -78,32 +79,30 @@ public ListenableFuture getFuture() { public Authorization call() throws ExecutionException, RegistryAuthenticationFailedException, IOException, RegistryException { - String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); + String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( BuildStepType.AUTHENTICATE_PUSH, "authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { - Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - buildConfiguration.getEventDispatcher(), - registry, - buildConfiguration.getTargetImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) - .setUserAgentSuffix(buildConfiguration.getToolName()) - .initialize(); + buildConfiguration + .newTargetImageRegistryClientFactory() + .newRegistryClient() + .getRegistryAuthenticator(); if (registryAuthenticator != null) { - return registryAuthenticator.setCredential(registryCredential).authenticatePush(); + return registryAuthenticator.authenticatePush(registryCredential); } - - return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) - ? null - : Authorizations.withBasicCredentials( - registryCredential.getUsername(), registryCredential.getPassword()); + } catch (InsecureRegistryException ex) { + // Cannot skip certificate validation or use HTTP; fall through. } + + return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) + ? null + : Authorizations.withBasicCredentials( + registryCredential.getUsername(), registryCredential.getPassword()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 548d427c38..df23fefc5d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.async.AsyncStep; @@ -31,6 +32,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; @@ -112,23 +114,20 @@ public BaseImageWithAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, CacheCorruptedException { + EventDispatcher eventDispatcher = buildConfiguration.getEventDispatcher(); // Skip this step if this is a scratch image ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); if (baseImageConfiguration.getImage().isScratch()) { - buildConfiguration - .getEventDispatcher() - .dispatch(LogEvent.progress("Getting scratch base image...")); + eventDispatcher.dispatch(LogEvent.progress("Getting scratch base image...")); return new BaseImageWithAuthorization( Image.builder(buildConfiguration.getTargetFormat()).build(), null); } - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.progress( - "Getting base image " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "...")); + eventDispatcher.dispatch( + LogEvent.progress( + "Getting base image " + + buildConfiguration.getBaseImageConfiguration().getImage() + + "...")); if (buildConfiguration.isOffline()) { return new BaseImageWithAuthorization(pullBaseImageOffline(), null); @@ -144,13 +143,11 @@ public BaseImageWithAuthorization call() return new BaseImageWithAuthorization(pullBaseImage(null, progressEventDispatcher), null); } catch (RegistryUnauthorizedException ex) { - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.lifecycle( - "The base image requires auth. Trying again for " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "...")); + eventDispatcher.dispatch( + LogEvent.lifecycle( + "The base image requires auth. Trying again for " + + buildConfiguration.getBaseImageConfiguration().getImage() + + "...")); // If failed, then, retrieve base registry credentials and try with retrieved credentials. // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to @@ -176,27 +173,27 @@ public BaseImageWithAuthorization call() } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. // See https://docs.docker.com/registry/spec/auth/token - RegistryAuthenticator registryAuthenticator = - RegistryAuthenticator.initializer( - buildConfiguration.getEventDispatcher(), - buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getImageRepository()) - .setAllowInsecureRegistries(buildConfiguration.getAllowInsecureRegistries()) - .setUserAgentSuffix(buildConfiguration.getToolName()) - .initialize(); - if (registryAuthenticator == null) { - buildConfiguration - .getEventDispatcher() - .dispatch( - LogEvent.error( - "Failed to retrieve authentication challenge for registry that required token authentication")); - throw registryUnauthorizedException; + try { + RegistryAuthenticator registryAuthenticator = + buildConfiguration + .newBaseImageRegistryClientFactory() + .newRegistryClient() + .getRegistryAuthenticator(); + if (registryAuthenticator != null) { + Authorization pullAuthorization = + registryAuthenticator.authenticatePull(registryCredential); + + return new BaseImageWithAuthorization( + pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); + } + + } catch (InsecureRegistryException insecureRegistryException) { + // Cannot skip certificate validation or use HTTP; fall through. } - registryAuthorization = - registryAuthenticator.setCredential(registryCredential).authenticatePull(); - - return new BaseImageWithAuthorization( - pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); + eventDispatcher.dispatch( + LogEvent.error( + "Failed to retrieve authentication challenge for registry that required token authentication")); + throw registryUnauthorizedException; } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 2530c7c78b..00da28f2ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,12 +17,9 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Authorizations; @@ -50,91 +47,6 @@ */ public class RegistryAuthenticator { - /** Initializer for {@link RegistryAuthenticator}. */ - public static class Initializer { - - private final EventDispatcher eventDispatcher; - private final String serverUrl; - private final String repository; - private boolean allowInsecureRegistries = false; - @Nullable private String userAgentSuffix; - - /** - * Instantiates a new initializer for {@link RegistryAuthenticator}. - * - * @param eventDispatcher the event dispatcher used for dispatching log events - * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) - * @param repository the image/repository name (also known as, namespace) - */ - private Initializer(EventDispatcher eventDispatcher, String serverUrl, String repository) { - this.eventDispatcher = eventDispatcher; - this.serverUrl = serverUrl; - this.repository = repository; - } - - public Initializer setAllowInsecureRegistries(boolean allowInsecureRegistries) { - this.allowInsecureRegistries = allowInsecureRegistries; - return this; - } - - public Initializer setUserAgentSuffix(@Nullable String userAgentSuffix) { - this.userAgentSuffix = userAgentSuffix; - return this; - } - - /** - * Gets a {@link RegistryAuthenticator} for a custom registry server and repository. - * - * @return the {@link RegistryAuthenticator} to authenticate pulls/pushes with the registry, or - * {@code null} if no token authentication is necessary - * @throws RegistryAuthenticationFailedException if failed to create the registry authenticator - * @throws IOException if communicating with the endpoint fails - * @throws RegistryException if communicating with the endpoint fails - */ - @Nullable - public RegistryAuthenticator initialize() - throws RegistryAuthenticationFailedException, IOException, RegistryException { - try { - return RegistryClient.factory(eventDispatcher, serverUrl, repository) - .setAllowInsecureRegistries(allowInsecureRegistries) - .setUserAgentSuffix(userAgentSuffix) - .newRegistryClient() - .getRegistryAuthenticator(); - - } catch (MalformedURLException ex) { - throw new RegistryAuthenticationFailedException(serverUrl, repository, ex); - - } catch (InsecureRegistryException ex) { - // Cannot skip certificate validation or use HTTP, so just return null. - return null; - } - } - } - - /** - * Sets a {@code Credential} to help the authentication. - * - * @param credential the credential used to authenticate. - * @return this - */ - public RegistryAuthenticator setCredential(@Nullable Credential credential) { - this.credential = credential; - return this; - } - - /** - * Gets a new initializer for {@link RegistryAuthenticator}. - * - * @param eventDispatcher the event dispatcher used for dispatching log events - * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) - * @param repository the image/repository name (also known as, namespace) - * @return the new {@link Initializer} - */ - public static Initializer initializer( - EventDispatcher eventDispatcher, String serverUrl, String repository) { - return new Initializer(eventDispatcher, serverUrl, repository); - } - // TODO: Replace with a WWW-Authenticate header parser. /** * Instantiates from parsing a {@code WWW-Authenticate} header. @@ -228,7 +140,6 @@ private String getToken() { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String realm; private final String service; - @Nullable private Credential credential; private final String userAgent; RegistryAuthenticator( @@ -245,21 +156,25 @@ private String getToken() { /** * Authenticates permissions to pull. * + * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the pull * @throws RegistryAuthenticationFailedException if authentication fails */ - public Authorization authenticatePull() throws RegistryAuthenticationFailedException { - return authenticate("pull"); + public Authorization authenticatePull(@Nullable Credential credential) + throws RegistryAuthenticationFailedException { + return authenticate(credential, "pull"); } /** * Authenticates permission to pull and push. * + * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the push * @throws RegistryAuthenticationFailedException if authentication fails */ - public Authorization authenticatePush() throws RegistryAuthenticationFailedException { - return authenticate("pull,push"); + public Authorization authenticatePush(@Nullable Credential credential) + throws RegistryAuthenticationFailedException { + return authenticate(credential, "pull,push"); } @VisibleForTesting @@ -273,16 +188,17 @@ String getServiceScopeRequestParameters(String scope) { } @VisibleForTesting - URL getAuthenticationUrl(String scope) throws MalformedURLException { - return isOAuth2Auth() + URL getAuthenticationUrl(@Nullable Credential credential, String scope) + throws MalformedURLException { + return isOAuth2Auth(credential) ? new URL(realm) // Required parameters will be sent via POST . : new URL(realm + "?" + getServiceScopeRequestParameters(scope)); } @VisibleForTesting - String getAuthRequestParameters(String scope) { + String getAuthRequestParameters(@Nullable Credential credential, String scope) { String serviceScope = getServiceScopeRequestParameters(scope); - return isOAuth2Auth() + return isOAuth2Auth(credential) ? serviceScope // https://github.com/GoogleContainerTools/jib/pull/1545 + "&client_id=jib.da031fe481a93ac107a95a96462358f9" @@ -293,29 +209,31 @@ String getAuthRequestParameters(String scope) { } @VisibleForTesting - boolean isOAuth2Auth() { + boolean isOAuth2Auth(@Nullable Credential credential) { return credential != null && credential.isOAuth2RefreshToken(); } /** * Sends the authentication request and retrieves the Bearer authorization token. * + * @param credential the credential used to authenticate * @param scope the scope of permissions to authenticate for * @return the {@link Authorization} response * @throws RegistryAuthenticationFailedException if authentication fails * @see https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ - private Authorization authenticate(String scope) throws RegistryAuthenticationFailedException { + private Authorization authenticate(@Nullable Credential credential, String scope) + throws RegistryAuthenticationFailedException { try (Connection connection = - Connection.getConnectionFactory().apply(getAuthenticationUrl(scope))) { + Connection.getConnectionFactory().apply(getAuthenticationUrl(credential, scope))) { Request.Builder requestBuilder = Request.builder() .setHttpTimeout(JibSystemProperties.getHttpTimeout()) .setUserAgent(userAgent); - if (isOAuth2Auth()) { - String parameters = getAuthRequestParameters(scope); + if (isOAuth2Auth(credential)) { + String parameters = getAuthRequestParameters(credential, scope); requestBuilder.setBody( new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString(), null)); } else if (credential != null) { @@ -325,7 +243,8 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa } Request request = requestBuilder.build(); - Response response = isOAuth2Auth() ? connection.post(request) : connection.get(request); + Response response = + isOAuth2Auth(credential) ? connection.post(request) : connection.get(request); String responseString = Blobs.writeToString(response.getBody()); AuthenticationResponseTemplate responseJson = @@ -336,9 +255,9 @@ private Authorization authenticate(String scope) throws RegistryAuthenticationFa registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), "Did not get token in authentication response from " - + getAuthenticationUrl(scope) + + getAuthenticationUrl(credential, scope) + "; parameters: " - + getAuthRequestParameters(scope)); + + getAuthRequestParameters(credential, scope)); } return Authorizations.withBearerToken(responseJson.getToken()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index ff5754f90b..7eae71f001 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -149,6 +149,6 @@ public void testHandleHttpResponseException_pass() Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), - registryAuthenticator.getAuthenticationUrl("someScope")); + registryAuthenticator.getAuthenticationUrl(null, "someScope")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index c79c7e446b..39bd57c139 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -55,7 +55,7 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + registryAuthenticator.getAuthenticationUrl(null, "scope")); registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( @@ -64,55 +64,56 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + registryAuthenticator.getAuthenticationUrl(null, "scope")); } @Test public void testAuthRequestParameters_basicAuth() { Assert.assertEquals( "service=someservice&scope=repository:someimage:scope", - registryAuthenticator.getAuthRequestParameters("scope")); + registryAuthenticator.getAuthRequestParameters(null, "scope")); } @Test public void testAuthRequestParameters_oauth2() { - registryAuthenticator.setCredential(Credential.from("", "oauth2_access_token")); + Credential credential = Credential.from("", "oauth2_access_token"); Assert.assertEquals( "service=someservice&scope=repository:someimage:scope" + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + "&grant_type=refresh_token&refresh_token=oauth2_access_token", - registryAuthenticator.getAuthRequestParameters("scope")); + registryAuthenticator.getAuthRequestParameters(credential, "scope")); } @Test public void isOAuth2Auth_nullCredential() { - Assert.assertFalse(registryAuthenticator.isOAuth2Auth()); + Assert.assertFalse(registryAuthenticator.isOAuth2Auth(null)); } @Test public void isOAuth2Auth_basicAuth() { - registryAuthenticator.setCredential(Credential.from("name", "password")); - Assert.assertFalse(registryAuthenticator.isOAuth2Auth()); + Credential credential = Credential.from("name", "password"); + Assert.assertFalse(registryAuthenticator.isOAuth2Auth(credential)); } @Test public void isOAuth2Auth_oauth2() { - registryAuthenticator.setCredential(Credential.from("", "oauth2_token")); - Assert.assertTrue(registryAuthenticator.isOAuth2Auth()); + Credential credential = Credential.from("", "oauth2_token"); + Assert.assertTrue(registryAuthenticator.isOAuth2Auth(credential)); } @Test public void getAuthenticationUrl_basicAuth() throws MalformedURLException { Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + registryAuthenticator.getAuthenticationUrl(null, "scope")); } @Test public void istAuthenticationUrl_oauth2() throws MalformedURLException { - registryAuthenticator.setCredential(Credential.from("", "oauth2_token")); + Credential credential = Credential.from("", "oauth2_token"); Assert.assertEquals( - new URL("https://somerealm"), registryAuthenticator.getAuthenticationUrl("scope")); + new URL("https://somerealm"), + registryAuthenticator.getAuthenticationUrl(credential, "scope")); } @Test @@ -175,7 +176,7 @@ public void testFromAuthenticationMethod_noService() Assert.assertEquals( new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl("scope")); + registryAuthenticator.getAuthenticationUrl(null, "scope")); } @Test @@ -188,7 +189,7 @@ public void testUserAgent() "Bearer realm=\"" + server.getEndpoint() + "\"", registryEndpointRequestProperties, "Competent-Agent"); - authenticator.authenticatePush(); + authenticator.authenticatePush(null); } catch (RegistryAuthenticationFailedException ex) { // Doesn't matter if auth fails. We only examine what we sent. } From ccfa35e83198ed5b0f956067976186724252ca4a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 17 May 2019 15:12:17 -0400 Subject: [PATCH 0593/2020] Reorg and refactor repo - continued (#1684) * Decouple Blob * Rename ImageToTarTranslator * Decouple Blob from TarStreamBuilder * Remove Blob dep from DigestUtil * Move WritableContents to hash package * Decouple Blob from http Response * Decouple Blob from http Request * Remove more Blob indirection * Push up throttling decision * Rename "with..." to "from..." * Specialize ThrottledConsumer * ListenableCountingOutputStream -> NotifyingOutputStream * optionalOutStream -> outStream * ThrottledLongConsumer -> ThrottledAccumulatingConsumer --- .../registry/BlobPullerIntegrationTest.java | 5 +- .../registry/BlobPusherIntegrationTest.java | 5 +- .../ManifestPusherIntegrationTest.java | 6 +- .../builder/steps/AuthenticatePushStep.java | 3 +- .../tools/jib/builder/steps/BuildResult.java | 5 +- .../steps/PullAndCacheBaseImageLayerStep.java | 8 +- .../jib/builder/steps/PullBaseImageStep.java | 11 +- .../tools/jib/builder/steps/PushBlobStep.java | 32 +---- .../steps/PushContainerConfigurationStep.java | 14 +- ...ottledProgressEventDispatcherWrapper.java} | 34 ++--- .../tools/jib/cache/CacheStorageWriter.java | 7 +- .../cloud/tools/jib/docker/ImageTarball.java | 17 ++- .../ThrottledAccumulatingConsumer.java | 82 +++++++++++ .../jib/hash/CountingDigestOutputStream.java | 38 ++---- .../google/cloud/tools/jib/hash/Digests.java | 36 ++--- .../cloud/tools/jib/http/Authorization.java | 31 ++++- .../cloud/tools/jib/http/Authorizations.java | 53 -------- .../cloud/tools/jib/http/BlobHttpContent.java | 22 ++- .../tools/jib/http/BlobProgressListener.java | 37 ----- .../http/ListenableCountingOutputStream.java | 127 ------------------ .../tools/jib/http/NotifyingOutputStream.java | 86 ++++++++++++ .../google/cloud/tools/jib/http/Response.java | 9 +- .../jib/image/json/ImageToJsonTranslator.java | 11 +- .../tools/jib/json/JsonTemplateMapper.java | 18 ++- .../cloud/tools/jib/registry/BlobPuller.java | 26 ++-- .../cloud/tools/jib/registry/BlobPusher.java | 14 +- .../tools/jib/registry/ManifestPuller.java | 8 +- .../tools/jib/registry/ManifestPusher.java | 6 +- .../jib/registry/RegistryAuthenticator.java | 14 +- .../tools/jib/registry/RegistryClient.java | 20 +-- .../jib/cache/CacheStorageReaderTest.java | 4 +- .../cloud/tools/jib/cache/CacheTest.java | 12 +- .../hash/CountingDigestOutputStreamTest.java | 7 +- .../cloud/tools/jib/http/ConnectionTest.java | 13 +- ...st.java => NotifyingOutputStreamTest.java} | 38 +++--- .../cloud/tools/jib/http/ResponseTest.java | 15 +-- .../jib/http/TestBlobProgressListener.java | 43 ------ .../jib/http/WithServerConnectionTest.java | 11 +- .../image/json/ImageToJsonTranslatorTest.java | 17 +-- .../tools/jib/registry/BlobPullerTest.java | 29 ++-- .../tools/jib/registry/BlobPusherTest.java | 31 +---- .../jib/registry/ManifestPullerTest.java | 22 ++- .../registry/RegistryEndpointCallerTest.java | 15 ++- 43 files changed, 465 insertions(+), 577 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/{ProgressEventDispatcherContainer.java => ThrottledProgressEventDispatcherWrapper.java} (64%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java rename jib-core/src/test/java/com/google/cloud/tools/jib/http/{ListenableCountingOutputStreamTest.java => NotifyingOutputStreamTest.java} (64%) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 596a3acc5b..9d4c26e63f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.io.ByteStreams; import java.io.IOException; @@ -64,7 +63,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti Assert.assertEquals(0, expectedSize.sum()); expectedSize.add(size); }, - new TestBlobProgressListener(totalByteCount::add)); + totalByteCount::add); Assert.assertEquals(realDigest, pulledBlob.writeTo(ByteStreams.nullOutputStream()).getDigest()); Assert.assertTrue(expectedSize.sum() > 0); Assert.assertEquals(expectedSize.sum(), totalByteCount.sum()); @@ -84,7 +83,7 @@ public void testPull_unknownBlob() throws IOException, DigestException, Interrup try { registryClient - .pullBlob(nonexistentDigest, ignored -> {}, new TestBlobProgressListener(ignored -> {})) + .pullBlob(nonexistentDigest, ignored -> {}, ignored -> {}) .writeTo(ByteStreams.nullOutputStream()); Assert.fail("Trying to pull nonexistent blob should have errored"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 9b421aec24..eb6e5f34df 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.http.TestBlobProgressListener; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -48,8 +47,6 @@ public void testPush() RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); - Assert.assertFalse( - registryClient.pushBlob( - testBlobDigest, testBlob, null, new TestBlobProgressListener(ignored -> {}))); + Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 88e3017e7a..3523145d8d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.http.TestBlobProgressListener; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -88,14 +87,13 @@ public void testPush() .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse( - registryClient.pushBlob( - testLayerBlobDigest, testLayerBlob, null, new TestBlobProgressListener(ignored -> {}))); + registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); Assert.assertFalse( registryClient.pushBlob( testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null, - new TestBlobProgressListener(ignored -> {}))); + ignored -> {})); // Pushes the manifest. DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 88aed8438f..b46980a19b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -28,7 +28,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -102,7 +101,7 @@ public Authorization call() return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) ? null - : Authorizations.withBasicCredentials( + : Authorization.fromBasicCredentials( registryCredential.getUsername(), registryCredential.getPassword()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index e29cd7ce34..29f2669fa0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.common.io.ByteStreams; import java.io.IOException; import java.util.Objects; @@ -41,9 +40,7 @@ static BuildResult fromImage(Image image, Class, Callable { - private static class ForwardingProgressListener implements BlobProgressListener { - - private final ProgressEventDispatcher progressEventDispatcher; - - private ForwardingProgressListener(ProgressEventDispatcher progressEventDispatcher) { - this.progressEventDispatcher = progressEventDispatcher; - } - - @Override - public void handleByteCount(long byteCount) { - progressEventDispatcher.dispatchProgress(byteCount); - } - - @Override - public Duration getDelayBetweenCallbacks() { - return Duration.ofMillis(100); - } - } - private static final String DESCRIPTION = "Pushing BLOB "; private final BuildConfiguration buildConfiguration; @@ -105,7 +85,9 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc blobDescriptor.getSize()); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor)) { + buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() @@ -121,11 +103,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc } // todo: leverage cross-repository mounts - registryClient.pushBlob( - blobDescriptor.getDigest(), - blob, - null, - new ForwardingProgressListener(progressEventDispatcher)); + registryClient.pushBlob(blobDescriptor.getDigest(), blob, null, throttledProgressReporter); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index b3722bfbd3..2cd50c5d92 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -19,15 +19,16 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; -import com.google.common.io.ByteStreams; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; @@ -90,10 +91,9 @@ private PushBlobStep afterBuildConfigurationFutureFuture() TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - Blob containerConfigurationBlob = - new ImageToJsonTranslator(image).getContainerConfigurationBlob(); - BlobDescriptor blobDescriptor = - containerConfigurationBlob.writeTo(ByteStreams.nullOutputStream()); + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(image).getContainerConfiguration(); + BlobDescriptor blobDescriptor = Digests.computeDigest(containerConfiguration); return new PushBlobStep( listeningExecutorService, @@ -101,7 +101,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() progressEventDispatcher.newChildProducer(), authenticatePushStep, blobDescriptor, - containerConfigurationBlob, + Blobs.from(containerConfiguration), BuildStepType.PUSH_CONTAINER_CONFIGURATION); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java similarity index 64% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java index d1ff257010..4d518f7788 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ProgressEventDispatcherContainer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java @@ -18,28 +18,29 @@ import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.http.BlobProgressListener; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.common.base.Preconditions; import java.io.Closeable; -import java.time.Duration; import javax.annotation.Nullable; /** - * Contains a {@link ProgressEventDispatcher}. This class is mutable and should only be used within - * a local context. + * Contains a {@link ProgressEventDispatcher} and throttles dispatching progress events with the + * default delay used by {@link ThrottledConsumer}. This class is mutable and should only be used + * within a local context. * *

This class is necessary because the total BLOb size (allocation units) is not known until the * response headers are received, only after which can the {@link ProgressEventDispatcher} be * created. */ -class ProgressEventDispatcherContainer implements BlobProgressListener, Closeable { +class ThrottledProgressEventDispatcherWrapper implements Closeable { private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String description; private final BuildStepType type; @Nullable private ProgressEventDispatcher progressEventDispatcher; + @Nullable private ThrottledAccumulatingConsumer throttledDispatcher; - ProgressEventDispatcherContainer( + ThrottledProgressEventDispatcherWrapper( ProgressEventDispatcher.Factory progressEventDispatcherFactory, String description, BuildStepType type) { @@ -48,25 +49,24 @@ class ProgressEventDispatcherContainer implements BlobProgressListener, Closeabl this.type = type; } - @Override - public void handleByteCount(long byteCount) { - Preconditions.checkNotNull(progressEventDispatcher); - progressEventDispatcher.dispatchProgress(byteCount); - } - - @Override - public Duration getDelayBetweenCallbacks() { - return Duration.ofMillis(100); + public void dispatchProgress(Long progressUnits) { + Preconditions.checkNotNull(throttledDispatcher); + throttledDispatcher.accept(progressUnits); } @Override public void close() { Preconditions.checkNotNull(progressEventDispatcher); + Preconditions.checkNotNull(throttledDispatcher); + throttledDispatcher.close(); progressEventDispatcher.close(); } - void initializeWithBlobSize(long blobSize) { + void setProgressTarget(long allocationUnits) { Preconditions.checkState(progressEventDispatcher == null); - progressEventDispatcher = progressEventDispatcherFactory.create(type, description, blobSize); + progressEventDispatcher = + progressEventDispatcherFactory.create(type, description, allocationUnits); + throttledDispatcher = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index 2207dbb50a..a63a3572f3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -108,7 +108,7 @@ private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile GZIPInputStream decompressorStream = new GZIPInputStream(fileInputStream)) { ByteStreams.copy(decompressorStream, diffIdCaptureOutputStream); } - return diffIdCaptureOutputStream.getDigest(); + return diffIdCaptureOutputStream.computeDigest().getDigest(); } } @@ -337,8 +337,9 @@ private WrittenLayer writeUncompressedLayerBlobToDirectory( // The GZIPOutputStream must be closed in order to write out the remaining compressed data. compressorStream.close(); - DescriptorDigest layerDigest = compressedDigestOutputStream.getDigest(); - long layerSize = compressedDigestOutputStream.getBytesHahsed(); + BlobDescriptor blobDescriptor = compressedDigestOutputStream.computeDigest(); + DescriptorDigest layerDigest = blobDescriptor.getDigest(); + long layerSize = blobDescriptor.getSize(); // Renames the temporary layer file to the correct filename. Path layerFile = layerDirectory.resolve(cacheStorageFiles.getLayerFilename(layerDiffId)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 5d8ef8d9d4..5dbed705df 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -17,15 +17,13 @@ package com.google.cloud.tools.jib.docker; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; @@ -71,16 +69,17 @@ public void writeTo(OutputStream out) throws IOException { } // Adds the container configuration to the tarball. - Blob containerConfigurationBlob = - new ImageToJsonTranslator(image).getContainerConfigurationBlob(); + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(image).getContainerConfiguration(); tarStreamBuilder.addByteEntry( - Blobs.writeToByteArray(containerConfigurationBlob), CONTAINER_CONFIGURATION_JSON_FILE_NAME); + JsonTemplateMapper.toByteArray(containerConfiguration), + CONTAINER_CONFIGURATION_JSON_FILE_NAME); // Adds the manifest to tarball. manifestTemplate.setRepoTags(imageReference.toStringWithTag()); - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - JsonTemplateMapper.writeTo(Collections.singletonList(manifestTemplate), outStream); - tarStreamBuilder.addByteEntry(outStream.toByteArray(), MANIFEST_JSON_FILE_NAME); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(Collections.singletonList(manifestTemplate)), + MANIFEST_JSON_FILE_NAME); tarStreamBuilder.writeAsTarArchiveTo(out); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java new file mode 100644 index 0000000000..65140b2ddc --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.event.progress; + +import java.io.Closeable; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Wraps a {@code Consumer} so that multiple consume calls ({@link #accept}) within a short + * period of time are merged into a single later call with the value accumulated up to that point. + */ +public class ThrottledAccumulatingConsumer implements Consumer, Closeable { + + private final Consumer consumer; + + /** Delay between each call to the underlying {@link #accept}. */ + private final Duration delayBetweenCallbacks; + + /** Last time the underlying {@link #accept} was called. */ + private Instant previousCallback; + + /** "Clock" that returns the current {@link Instant}. */ + private final Supplier getNow; + + @Nullable private Long valueSoFar; + + /** + * Wraps a consumer with the delay of 100 ms. + * + * @param callback {@link Consumer} callback to wrap + */ + public ThrottledAccumulatingConsumer(Consumer callback) { + this(callback, Duration.ofMillis(100), Instant::now); + } + + public ThrottledAccumulatingConsumer( + Consumer consumer, Duration delayBetweenCallbacks, Supplier getNow) { + this.consumer = consumer; + this.delayBetweenCallbacks = delayBetweenCallbacks; + this.getNow = getNow; + + previousCallback = getNow.get(); + } + + @Override + public void accept(Long value) { + valueSoFar = valueSoFar == null ? value : valueSoFar + value; + + Instant now = getNow.get(); + Instant nextFireTime = previousCallback.plus(delayBetweenCallbacks); + if (now.isAfter(nextFireTime)) { + consumer.accept(valueSoFar); + previousCallback = now; + valueSoFar = null; + } + } + + @Override + public void close() { + if (valueSoFar != null) { + consumer.accept(valueSoFar); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java index 245e3f2891..ad91e0b07d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java @@ -17,14 +17,13 @@ package com.google.cloud.tools.jib.hash; import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.common.base.Verify; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import java.io.IOException; import java.io.OutputStream; import java.security.DigestException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import javax.annotation.Nullable; /** A {@link DigestOutputStream} that also keeps track of the total number of bytes written. */ public class CountingDigestOutputStream extends DigestOutputStream { @@ -33,11 +32,6 @@ public class CountingDigestOutputStream extends DigestOutputStream { private long bytesSoFar = 0; - /** The total number of bytes used to compute a digest. Resets when {@link computeDigest) is called. */ - private long bytesHashed = 0; - - @Nullable private DescriptorDigest descriptorDigest; - /** * Wraps the {@code outputStream}. * @@ -54,10 +48,13 @@ public CountingDigestOutputStream(OutputStream outputStream) { } /** - * Computes the hash and remembers the size of the bytes written to compute the hash. The buffer - * resets after this method is called, so this method should only be called once per computation. + * Computes the hash and returns it along with the size of the bytes written to compute the hash. + * The buffer resets after this method is called, so this method should only be called once per + * computation. + * + * @return the computed hash and the size of the bytes consumed */ - public void computeDigest() { + public BlobDescriptor computeDigest() { try { byte[] hashedBytes = digest.digest(); @@ -68,31 +65,16 @@ public void computeDigest() { } String hash = stringBuilder.toString(); - bytesHashed = bytesSoFar; - descriptorDigest = DescriptorDigest.fromHash(hash); + BlobDescriptor blobDescriptor = + new BlobDescriptor(bytesSoFar, DescriptorDigest.fromHash(hash)); bytesSoFar = 0; + return blobDescriptor; } catch (DigestException ex) { throw new RuntimeException("SHA-256 algorithm produced invalid hash: " + ex.getMessage(), ex); } } - /** @return the number of bytes written and used to compute the most recent digest */ - public long getBytesHahsed() { - if (descriptorDigest == null) { - computeDigest(); - } - return bytesHashed; - } - - /** @return the most recently computed digest hash */ - public DescriptorDigest getDigest() { - if (descriptorDigest == null) { - computeDigest(); - } - return Verify.verifyNotNull(descriptorDigest); - } - @Override public void write(byte[] data, int offset, int length) throws IOException { super.write(data, offset, length); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java index 90d495914d..606282e8b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java @@ -45,10 +45,14 @@ public static DescriptorDigest computeJsonDigest(List te return computeDigest(contents, ByteStreams.nullOutputStream()).getDigest(); } - public static BlobDescriptor computeDigest(JsonTemplate template, OutputStream optionalOutStream) + public static BlobDescriptor computeDigest(JsonTemplate template) throws IOException { + return computeDigest(template, ByteStreams.nullOutputStream()); + } + + public static BlobDescriptor computeDigest(JsonTemplate template, OutputStream outStream) throws IOException { WritableContents contents = contentsOut -> JsonTemplateMapper.writeTo(template, contentsOut); - return computeDigest(contents, optionalOutStream); + return computeDigest(contents, outStream); } public static BlobDescriptor computeDigest(InputStream inStream) throws IOException { @@ -67,36 +71,36 @@ public static BlobDescriptor computeDigest(WritableContents contents) throws IOE } /** - * Computes the digest by consuming the contents of an {@link InputStream} and optionally copying - * it to an {@link OutputStream}. Returns the computed digest along with the size of the bytes - * consumed to compute the digest. Does not close either stream. + * Computes the digest by consuming the contents of an {@link InputStream} while copying it to an + * {@link OutputStream}. Returns the computed digest along with the size of the bytes consumed to + * compute the digest. Does not close either stream. * * @param inStream the stream to read the contents from - * @param optionalOutStream the stream to which the contents are copied + * @param outStream the stream to which the contents are copied * @return computed digest and bytes consumed * @throws IOException if reading from or writing fails */ - public static BlobDescriptor computeDigest(InputStream inStream, OutputStream optionalOutStream) + public static BlobDescriptor computeDigest(InputStream inStream, OutputStream outStream) throws IOException { WritableContents contents = contentsOut -> ByteStreams.copy(inStream, contentsOut); - return computeDigest(contents, optionalOutStream); + return computeDigest(contents, outStream); } /** - * Computes the digest by consuming the contents and optionally copying it to an {@link - * OutputStream}. Returns the computed digest along with the size of the bytes consumed to compute - * the digest. Does not close the stream. + * Computes the digest by consuming the contents while copying it to an {@link OutputStream}. + * Returns the computed digest along with the size of the bytes consumed to compute the digest. + * Does not close the stream. * * @param contents the contents to compute digest for - * @param optionalOutStream the stream to which the contents are copied + * @param outStream the stream to which the contents are copied * @return computed digest and bytes consumed * @throws IOException if reading from or writing fails */ - public static BlobDescriptor computeDigest( - WritableContents contents, OutputStream optionalOutStream) throws IOException { - CountingDigestOutputStream digestOutStream = new CountingDigestOutputStream(optionalOutStream); + public static BlobDescriptor computeDigest(WritableContents contents, OutputStream outStream) + throws IOException { + CountingDigestOutputStream digestOutStream = new CountingDigestOutputStream(outStream); contents.writeTo(digestOutStream); digestOutStream.flush(); - return new BlobDescriptor(digestOutStream.getBytesHahsed(), digestOutStream.getDigest()); + return digestOutStream.computeDigest(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index 0bb13e407d..7c764f4f2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.http; +import com.google.api.client.util.Base64; +import java.nio.charset.StandardCharsets; import java.util.Objects; /** @@ -27,10 +29,37 @@ */ public class Authorization { + /** + * @param token the token + * @return an {@link Authorization} with a {@code Bearer} token + */ + public static Authorization fromBearerToken(String token) { + return new Authorization("Bearer", token); + } + + /** + * @param username the username + * @param secret the secret + * @return an {@link Authorization} with a {@code Basic} credentials + */ + public static Authorization fromBasicCredentials(String username, String secret) { + String credentials = username + ":" + secret; + String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); + return new Authorization("Basic", token); + } + + /** + * @param token the token + * @return an {@link Authorization} with a base64-encoded {@code username:password} string + */ + public static Authorization fromBasicToken(String token) { + return new Authorization("Basic", token); + } + private final String scheme; private final String token; - Authorization(String scheme, String token) { + private Authorization(String scheme, String token) { this.scheme = scheme; this.token = token; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java deleted file mode 100644 index e8ae835a23..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorizations.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.util.Base64; -import java.nio.charset.StandardCharsets; - -/** Static initializers for {@link Authorization}. */ -public class Authorizations { - - /** - * @param token the token - * @return an {@link Authorization} with a {@code Bearer} token - */ - public static Authorization withBearerToken(String token) { - return new Authorization("Bearer", token); - } - - /** - * @param username the username - * @param secret the secret - * @return an {@link Authorization} with a {@code Basic} credentials - */ - public static Authorization withBasicCredentials(String username, String secret) { - String credentials = username + ":" + secret; - String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); - return new Authorization("Basic", token); - } - - /** - * @param token the token - * @return an {@link Authorization} with a base64-encoded {@code username:password} string - */ - public static Authorization withBasicToken(String token) { - return new Authorization("Basic", token); - } - - private Authorizations() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java index ad1fe85e73..4c185cc30e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java @@ -20,20 +20,23 @@ import com.google.cloud.tools.jib.blob.Blob; import java.io.IOException; import java.io.OutputStream; -import javax.annotation.Nullable; +import java.util.function.Consumer; /** {@link Blob}-backed {@link HttpContent}. */ public class BlobHttpContent implements HttpContent { private final Blob blob; private final String contentType; - @Nullable private final BlobProgressListener blobProgressListener; + private final Consumer writtenByteCountListener; - public BlobHttpContent( - Blob blob, String contentType, @Nullable BlobProgressListener blobProgressListener) { + public BlobHttpContent(Blob blob, String contentType) { + this(blob, contentType, ignored -> {}); + } + + public BlobHttpContent(Blob blob, String contentType, Consumer writtenByteCountListener) { this.blob = blob; this.contentType = contentType; - this.blobProgressListener = blobProgressListener; + this.writtenByteCountListener = writtenByteCountListener; } @Override @@ -54,14 +57,7 @@ public boolean retrySupported() { @Override public void writeTo(OutputStream outputStream) throws IOException { - outputStream = - blobProgressListener == null - ? outputStream - : new ListenableCountingOutputStream( - outputStream, - blobProgressListener::handleByteCount, - blobProgressListener.getDelayBetweenCallbacks()); - blob.writeTo(outputStream); + blob.writeTo(new NotifyingOutputStream(outputStream, writtenByteCountListener)); outputStream.flush(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java deleted file mode 100644 index 499e36a920..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobProgressListener.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import java.time.Duration; - -/** Listens to BLOb send/receive progress. */ -public interface BlobProgressListener { - - /** - * Callback to call with count of newly-processed bytes. - * - * @param byteCount sent/received byte count - */ - void handleByteCount(long byteCount); - - /** - * Gets the minimum delay between calls to {@link #handleByteCount}. - * - * @return the delay between callbacks - */ - Duration getDelayBetweenCallbacks(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java deleted file mode 100644 index fcc77470a4..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStream.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.io.OutputStream; -import java.time.Duration; -import java.time.Instant; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Counts the number of bytes written and reports the count to a callback. The count is reported - * with certain time delays to avoid calling the callback too often. - */ -public class ListenableCountingOutputStream extends OutputStream { - - /** The underlying {@link OutputStream} to wrap and forward bytes to. */ - private final OutputStream underlyingOutputStream; - - /** Receives a count of bytes written since the last call. */ - private final Consumer byteCountConsumer; - - /** Delay between each call to {@link #byteCountConsumer}. */ - private final Duration delayBetweenCallbacks; - - /** Supplies the current {@link Instant}. */ - private final Supplier instantSupplier; - - /** Number of bytes to provide to {@link #byteCountConsumer}. */ - private long byteCount = 0; - - /** Last time {@link #byteCountConsumer} was called. */ - private Instant previousCallback; - - /** - * Wraps the {@code underlyingOutputStream} to count the bytes written. - * - * @param underlyingOutputStream the wrapped {@link OutputStream} - * @param byteCountConsumer the byte count {@link Consumer} - * @param delayBetweenCallbacks the minimum delay between each call to {@link #byteCountConsumer} - */ - public ListenableCountingOutputStream( - OutputStream underlyingOutputStream, - Consumer byteCountConsumer, - Duration delayBetweenCallbacks) { - this(underlyingOutputStream, byteCountConsumer, delayBetweenCallbacks, Instant::now); - } - - @VisibleForTesting - ListenableCountingOutputStream( - OutputStream underlyingOutputStream, - Consumer byteCountConsumer, - Duration delayBetweenCallbacks, - Supplier instantSupplier) { - this.underlyingOutputStream = underlyingOutputStream; - this.byteCountConsumer = byteCountConsumer; - this.delayBetweenCallbacks = delayBetweenCallbacks; - this.instantSupplier = instantSupplier; - - previousCallback = instantSupplier.get(); - } - - @Override - public void write(int singleByte) throws IOException { - underlyingOutputStream.write(singleByte); - countBytesWritten(1); - } - - @Override - public void write(byte[] byteArray) throws IOException { - underlyingOutputStream.write(byteArray); - countBytesWritten(byteArray.length); - } - - @Override - public void write(byte byteArray[], int offset, int length) throws IOException { - underlyingOutputStream.write(byteArray, offset, length); - countBytesWritten(length); - } - - @Override - public void flush() throws IOException { - underlyingOutputStream.flush(); - callByteCountConsumer(instantSupplier.get()); - } - - @Override - public void close() throws IOException { - underlyingOutputStream.close(); - callByteCountConsumer(instantSupplier.get()); - } - - private void countBytesWritten(int byteCount) { - this.byteCount += byteCount; - - Instant now = instantSupplier.get(); - if (previousCallback.plus(delayBetweenCallbacks).isBefore(now)) { - callByteCountConsumer(now); - } - } - - private void callByteCountConsumer(Instant now) { - if (byteCount == 0) { - return; - } - - byteCountConsumer.accept(byteCount); - byteCount = 0; - previousCallback = now; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java new file mode 100644 index 0000000000..8b7bf1f4be --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.function.Consumer; + +/** Counts the number of bytes written and reports the count to a callback. */ +public class NotifyingOutputStream extends OutputStream { + + /** The underlying {@link OutputStream} to wrap and forward bytes to. */ + private final OutputStream underlyingOutputStream; + + /** Receives a count of bytes written since the last call. */ + private final Consumer byteCountListener; + + /** Number of bytes to provide to {@link #byteCountListener}. */ + private long byteCount = 0; + + /** + * Wraps the {@code underlyingOutputStream} to count the bytes written. + * + * @param underlyingOutputStream the wrapped {@link OutputStream} + * @param byteCountListener the byte count {@link Consumer} + */ + public NotifyingOutputStream( + OutputStream underlyingOutputStream, Consumer byteCountListener) { + this.underlyingOutputStream = underlyingOutputStream; + this.byteCountListener = byteCountListener; + } + + @Override + public void write(int singleByte) throws IOException { + underlyingOutputStream.write(singleByte); + countAndCallListener(1); + } + + @Override + public void write(byte[] byteArray) throws IOException { + underlyingOutputStream.write(byteArray); + countAndCallListener(byteArray.length); + } + + @Override + public void write(byte byteArray[], int offset, int length) throws IOException { + underlyingOutputStream.write(byteArray, offset, length); + countAndCallListener(length); + } + + @Override + public void flush() throws IOException { + underlyingOutputStream.flush(); + countAndCallListener(0); + } + + @Override + public void close() throws IOException { + underlyingOutputStream.close(); + countAndCallListener(0); + } + + private void countAndCallListener(int written) { + this.byteCount += written; + if (byteCount == 0) { + return; + } + + byteCountListener.accept(byteCount); + byteCount = 0; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java index a9f66d235b..3c7f895586 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java @@ -18,10 +18,9 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponse; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.common.net.HttpHeaders; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** Holds an HTTP response. */ @@ -65,11 +64,11 @@ public long getContentLength() throws NumberFormatException { } /** - * @return the HTTP response body as a {@link Blob}. + * @return the HTTP response body as an {@link InputStream}. * @throws IOException if getting the HTTP response content fails. */ - public Blob getBody() throws IOException { - return Blobs.from(httpResponse.getContent()); + public InputStream getBody() throws IOException { + return httpResponse.getContent(); } /** @return the original request URL */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index f45a7416ba..b6309455a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -21,10 +21,10 @@ import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -145,7 +145,7 @@ public ImageToJsonTranslator(Image image) { * * @return the container configuration {@link Blob} */ - public Blob getContainerConfigurationBlob() { + public JsonTemplate getContainerConfiguration() { // Set up the JSON template. ContainerConfigurationTemplate template = new ContainerConfigurationTemplate(); @@ -188,14 +188,13 @@ public Blob getContainerConfigurationBlob() { template.setContainerHealthCheckRetries(healthCheck.getRetries().orElse(null)); } - // Serializes into JSON. - return Blobs.from(template); + return template; } /** * Gets the manifest as a JSON template. The {@code containerConfigurationBlobDescriptor} must be - * the [@link BlobDescriptor} obtained by writing out the container configuration {@link Blob} - * returned from {@link #getContainerConfigurationBlob()}. + * the {@link BlobDescriptor} obtained by writing out the container configuration JSON returned + * from {@link #getContainerConfiguration()}. * * @param child type of {@link BuildableManifestTemplate}. * @param manifestTemplateClass the JSON template to translate the image to. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index c5f1c5f501..fe462059f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -129,6 +130,16 @@ public static String toUtf8String(List templates) return toUtf8String((Object) templates); } + public static byte[] toByteArray(JsonTemplate template) + throws JsonGenerationException, JsonMappingException, IOException { + return toByteArray((Object) template); + } + + public static byte[] toByteArray(List templates) + throws JsonGenerationException, JsonMappingException, IOException { + return toByteArray((Object) templates); + } + public static void writeTo(JsonTemplate template, OutputStream out) throws JsonGenerationException, JsonMappingException, IOException { writeTo((Object) template, out); @@ -141,9 +152,14 @@ public static void writeTo(List templates, OutputStream private static String toUtf8String(Object template) throws JsonGenerationException, JsonMappingException, IOException { + return new String(toByteArray(template), StandardCharsets.UTF_8); + } + + private static byte[] toByteArray(Object template) + throws JsonGenerationException, JsonMappingException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); writeTo(template, out); - return out.toString("UTF-8"); + return out.toByteArray(); } private static void writeTo(Object template, OutputStream out) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index ebfe733673..04dd6b4bc3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -19,9 +19,9 @@ import com.google.api.client.http.HttpMethods; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.BlobProgressListener; -import com.google.cloud.tools.jib.http.ListenableCountingOutputStream; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.http.Response; import java.io.IOException; import java.io.OutputStream; @@ -45,32 +45,30 @@ class BlobPuller implements RegistryEndpointProvider { */ private final OutputStream destinationOutputStream; - private final Consumer blobSizeConsumer; - private final BlobProgressListener blobProgressListener; + private final Consumer blobSizeListener; + private final Consumer writtenByteCountListener; BlobPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, OutputStream destinationOutputStream, - Consumer blobSizeConsumer, - BlobProgressListener blobProgressListener) { + Consumer blobSizeListener, + Consumer writtenByteCountListener) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; this.destinationOutputStream = destinationOutputStream; - this.blobSizeConsumer = blobSizeConsumer; - this.blobProgressListener = blobProgressListener; + this.blobSizeListener = blobSizeListener; + this.writtenByteCountListener = writtenByteCountListener; } @Override public Void handleResponse(Response response) throws IOException, UnexpectedBlobDigestException { - blobSizeConsumer.accept(response.getContentLength()); + blobSizeListener.accept(response.getContentLength()); try (OutputStream outputStream = - new ListenableCountingOutputStream( - destinationOutputStream, - blobProgressListener::handleByteCount, - blobProgressListener.getDelayBetweenCallbacks())) { - BlobDescriptor receivedBlobDescriptor = response.getBody().writeTo(outputStream); + new NotifyingOutputStream(destinationOutputStream, writtenByteCountListener)) { + BlobDescriptor receivedBlobDescriptor = + Digests.computeDigest(response.getBody(), outputStream); if (!blobDigest.equals(receivedBlobDescriptor.getDigest())) { throw new UnexpectedBlobDigestException( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 2b79c783e9..d0ebfcafef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.http.Response; import com.google.common.net.MediaType; import java.net.HttpURLConnection; @@ -30,6 +29,7 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; /** @@ -113,12 +113,12 @@ public String getActionDescription() { private class Writer implements RegistryEndpointProvider { private final URL location; - private final BlobProgressListener blobProgressListener; + private final Consumer writtenByteCountListener; @Nullable @Override public BlobHttpContent getContent() { - return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), blobProgressListener); + return new BlobHttpContent(blob, MediaType.OCTET_STREAM.toString(), writtenByteCountListener); } @Override @@ -148,9 +148,9 @@ public String getActionDescription() { return BlobPusher.this.getActionDescription(); } - private Writer(URL location, BlobProgressListener blobProgressListener) { + private Writer(URL location, Consumer writtenByteCountListener) { this.location = location; - this.blobProgressListener = blobProgressListener; + this.writtenByteCountListener = writtenByteCountListener; } } @@ -220,8 +220,8 @@ RegistryEndpointProvider initializer() { * @param blobProgressListener the listener for {@link Blob} push progress * @return a {@link RegistryEndpointProvider} for writing the BLOB to an upload location */ - RegistryEndpointProvider writer(URL location, BlobProgressListener blobProgressListener) { - return new Writer(location, blobProgressListener); + RegistryEndpointProvider writer(URL location, Consumer writtenByteCountListener) { + return new Writer(location, writtenByteCountListener); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index a5ba631c57..615a4a9500 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.api.client.http.HttpMethods; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; @@ -28,9 +27,12 @@ import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.CharStreams; import java.io.IOException; +import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -79,7 +81,9 @@ public List getAccept() { /** Parses the response body into a {@link ManifestTemplate}. */ @Override public T handleResponse(Response response) throws IOException, UnknownManifestFormatException { - return getManifestTemplateFromJson(Blobs.writeToString(response.getBody())); + String jsonString = + CharStreams.toString(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); + return getManifestTemplateFromJson(jsonString); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 220ac1ff5c..7a5f162eda 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -81,11 +81,9 @@ private static String makeUnexpectedImageDigestWarning( @Override public BlobHttpContent getContent() { + // TODO: Consider giving progress on manifest push as well? return new BlobHttpContent( - Blobs.from(manifestTemplate), - manifestTemplate.getManifestMediaType(), - // TODO: Consider giving progress on manifest push as well? - null); + Blobs.from(manifestTemplate), manifestTemplate.getManifestMediaType()); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 00da28f2ab..2b71668368 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Authorizations; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; @@ -31,10 +30,13 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; +import com.google.common.io.CharStreams; import com.google.common.net.MediaType; import java.io.IOException; +import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -235,17 +237,17 @@ private Authorization authenticate(@Nullable Credential credential, String scope if (isOAuth2Auth(credential)) { String parameters = getAuthRequestParameters(credential, scope); requestBuilder.setBody( - new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString(), null)); + new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString())); } else if (credential != null) { requestBuilder.setAuthorization( - Authorizations.withBasicCredentials( - credential.getUsername(), credential.getPassword())); + Authorization.fromBasicCredentials(credential.getUsername(), credential.getPassword())); } Request request = requestBuilder.build(); Response response = isOAuth2Auth(credential) ? connection.post(request) : connection.get(request); - String responseString = Blobs.writeToString(response.getBody()); + String responseString = + CharStreams.toString(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); AuthenticationResponseTemplate responseJson = JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); @@ -259,7 +261,7 @@ private Authorization authenticate(@Nullable Credential credential, String scope + "; parameters: " + getAuthRequestParameters(credential, scope)); } - return Authorizations.withBearerToken(responseJson.getToken()); + return Authorization.fromBearerToken(responseJson.getToken()); } catch (IOException ex) { throw new RegistryAuthenticationFailedException( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 81a782f0cc..1737d52956 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.BlobProgressListener; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -247,14 +246,15 @@ public BlobDescriptor checkBlob(DescriptorDigest blobDigest) * written out. * * @param blobDigest the digest of the BLOB to download - * @param blobSizeConsumer callback to receive the total size of the BLOb to pull - * @param blobProgressListener listener for progress of the pull + * @param blobSizeListener callback to receive the total size of the BLOb to pull + * @param writtenByteCountListener listens on byte count written to an output stream during the + * pull * @return a {@link Blob} */ public Blob pullBlob( DescriptorDigest blobDigest, - Consumer blobSizeConsumer, - BlobProgressListener blobProgressListener) { + Consumer blobSizeListener, + Consumer writtenByteCountListener) { return Blobs.from( outputStream -> { try { @@ -263,8 +263,8 @@ public Blob pullBlob( registryEndpointRequestProperties, blobDigest, outputStream, - blobSizeConsumer, - blobProgressListener)); + blobSizeListener, + writtenByteCountListener)); } catch (RegistryException ex) { throw new IOException(ex); @@ -280,7 +280,7 @@ public Blob pullBlob( * @param blob the BLOB to push * @param sourceRepository if pushing to the same registry then the source image, or {@code null} * otherwise; used to optimize the BLOB push - * @param blobProgressListener listener for BLOb push progress + * @param writtenByteCountListener listens on byte count written to the registry during the push * @return {@code true} if the BLOB already exists on the registry and pushing was skipped; false * if the BLOB was pushed * @throws IOException if communicating with the endpoint fails @@ -290,7 +290,7 @@ public boolean pushBlob( DescriptorDigest blobDigest, Blob blob, @Nullable String sourceRepository, - BlobProgressListener blobProgressListener) + Consumer writtenByteCountListener) throws IOException, RegistryException { BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); @@ -312,7 +312,7 @@ public boolean pushBlob( // PATCH with BLOB URL putLocation = - callRegistryEndpoint(blobPusher.writer(patchLocation, blobProgressListener)); + callRegistryEndpoint(blobPusher.writer(patchLocation, writtenByteCountListener)); Preconditions.checkNotNull(putLocation); timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index dc4acdbad2..0e52bcbec3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -215,7 +215,7 @@ public void testSelect_invalidLayerDigest() throws IOException { DescriptorDigest selector = layerDigest1; Path selectorFile = cacheStorageFiles.getSelectorFile(selector); Files.createDirectories(selectorFile.getParent()); - Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from("not a valid layer digest"))); + Files.write(selectorFile, "not a valid layer digest".getBytes(StandardCharsets.UTF_8)); try { cacheStorageReader.select(selector); @@ -241,7 +241,7 @@ public void testSelect() throws IOException, CacheCorruptedException { DescriptorDigest selector = layerDigest1; Path selectorFile = cacheStorageFiles.getSelectorFile(selector); Files.createDirectories(selectorFile.getParent()); - Files.write(selectorFile, Blobs.writeToByteArray(Blobs.from(layerDigest2.getHash()))); + Files.write(selectorFile, layerDigest2.getHash().getBytes(StandardCharsets.UTF_8)); Optional selectedLayerDigest = cacheStorageReader.select(selector); Assert.assertTrue(selectedLayerDigest.isPresent()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 8a76dcad4c..5bf145d5fd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -62,16 +62,10 @@ private static Blob compress(Blob blob) { * * @param blob the {@link Blob} to decompress * @return the decompressed {@link Blob} + * @throws IOException if an I/O exception occurs */ - private static Blob decompress(Blob blob) { - return Blobs.from( - outputStream -> { - ByteArrayInputStream compressedInputStream = - new ByteArrayInputStream(Blobs.writeToByteArray(blob)); - try (GZIPInputStream decompressorStream = new GZIPInputStream(compressedInputStream)) { - ByteStreams.copy(decompressorStream, outputStream); - } - }); + private static Blob decompress(Blob blob) throws IOException { + return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index ef88052426..b0e4594a7a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.hash; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; @@ -56,9 +57,9 @@ public void test_smokeTest() throws IOException, DigestException { InputStream toHashInputStream = new ByteArrayInputStream(bytesToHash); ByteStreams.copy(toHashInputStream, countingDigestOutputStream); - Assert.assertEquals( - DescriptorDigest.fromHash(expectedHash), countingDigestOutputStream.getDigest()); - Assert.assertEquals(bytesToHash.length, countingDigestOutputStream.getBytesHahsed()); + BlobDescriptor blobDescriptor = countingDigestOutputStream.computeDigest(); + Assert.assertEquals(DescriptorDigest.fromHash(expectedHash), blobDescriptor.getDigest()); + Assert.assertEquals(bytesToHash.length, blobDescriptor.getSize()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index 3732146904..a7e26c2eb2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.concurrent.atomic.LongAdder; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,11 +56,11 @@ private interface SendFunction { ArgumentCaptor.forClass(BlobHttpContent.class); private final GenericUrl fakeUrl = new GenericUrl("http://crepecake/fake/url"); + private final LongAdder totalByteCount = new LongAdder(); + private Request fakeRequest; private HttpResponse mockHttpResponse; - private long totalByteCount = 0; - @InjectMocks private final Connection testConnection = Connection.getConnectionFactory().apply(fakeUrl.toURL()); @@ -111,10 +112,8 @@ private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { .setUserAgent("fake user agent") .setBody( new BlobHttpContent( - Blobs.from("crepecake"), - "fake.content.type", - new TestBlobProgressListener(byteCount -> totalByteCount += byteCount))) - .setAuthorization(Authorizations.withBasicCredentials("fake-username", "fake-secret")) + Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) + .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) .setHttpTimeout(httpTimeout) .build(); @@ -159,6 +158,6 @@ private void testSend(String httpMethod, SendFunction sendFunction) throws IOExc Assert.assertEquals( "crepecake", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals("crepecake".length(), totalByteCount); + Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java similarity index 64% rename from jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java index 6f3f126897..facf1cdd76 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ListenableCountingOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.http; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Duration; @@ -28,8 +29,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link ListenableCountingOutputStream}. */ -public class ListenableCountingOutputStreamTest { +/** Tests for {@link NotifyingOutputStream}. */ +public class NotifyingOutputStreamTest { @Test public void testCallback_correctSequence() throws IOException { @@ -37,12 +38,11 @@ public void testCallback_correctSequence() throws IOException { List byteCounts = new ArrayList<>(); - try (ListenableCountingOutputStream listenableCountingOutputStream = - new ListenableCountingOutputStream( - byteArrayOutputStream, byteCounts::add, Duration.ofSeconds(-1))) { - listenableCountingOutputStream.write(0); - listenableCountingOutputStream.write(new byte[] {1, 2, 3}); - listenableCountingOutputStream.write(new byte[] {1, 2, 3, 4, 5}, 3, 2); + try (NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(byteArrayOutputStream, byteCounts::add)) { + notifyingOutputStream.write(0); + notifyingOutputStream.write(new byte[] {1, 2, 3}); + notifyingOutputStream.write(new byte[] {1, 2, 3, 4, 5}, 3, 2); } Assert.assertEquals(Arrays.asList(1L, 3L, 2L), byteCounts); @@ -58,25 +58,25 @@ public void testDelay() throws IOException { Queue instantQueue = new ArrayDeque<>(); instantQueue.add(Instant.EPOCH); - try (ListenableCountingOutputStream listenableCountingOutputStream = - new ListenableCountingOutputStream( - byteArrayOutputStream, byteCounts::add, Duration.ofSeconds(3), instantQueue::remove)) { + try (ThrottledAccumulatingConsumer byteCounter = + new ThrottledAccumulatingConsumer( + byteCounts::add, Duration.ofSeconds(3), instantQueue::remove); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(byteArrayOutputStream, byteCounter)) { instantQueue.add(Instant.EPOCH); - listenableCountingOutputStream.write(100); + notifyingOutputStream.write(100); instantQueue.add(Instant.EPOCH); - listenableCountingOutputStream.write(new byte[] {101, 102, 103}); + notifyingOutputStream.write(new byte[] {101, 102, 103}); instantQueue.add(Instant.EPOCH.plusSeconds(4)); - listenableCountingOutputStream.write(new byte[] {104, 105, 106}); + notifyingOutputStream.write(new byte[] {104, 105, 106}); instantQueue.add(Instant.EPOCH.plusSeconds(10)); - listenableCountingOutputStream.write(new byte[] {107, 108}); + notifyingOutputStream.write(new byte[] {107, 108}); instantQueue.add(Instant.EPOCH.plusSeconds(10)); - listenableCountingOutputStream.write(new byte[] {109}); + notifyingOutputStream.write(new byte[] {109}); instantQueue.add(Instant.EPOCH.plusSeconds(13)); - listenableCountingOutputStream.write(new byte[] {0, 110}, 1, 1); - - instantQueue.add(Instant.EPOCH.plusSeconds(14)); + notifyingOutputStream.write(new byte[] {0, 110}, 1, 1); } Assert.assertEquals(Arrays.asList(7L, 2L, 2L), byteCounts); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java index bde361410e..0cba195cd0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java @@ -17,9 +17,8 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.HttpResponse; -import com.google.cloud.tools.jib.blob.Blob; +import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.junit.Assert; @@ -37,19 +36,13 @@ public class ResponseTest { @Test public void testGetContent() throws IOException { - String expectedResponse = "crepecake\nis\ngood!"; - ByteArrayInputStream responseInputStream = - new ByteArrayInputStream(expectedResponse.getBytes(StandardCharsets.UTF_8)); + byte[] expectedResponse = "crepecake\nis\ngood!".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream responseInputStream = new ByteArrayInputStream(expectedResponse); Mockito.when(httpResponseMock.getContent()).thenReturn(responseInputStream); Response response = new Response(httpResponseMock); - Blob responseStream = response.getBody(); - ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream(); - responseStream.writeTo(responseOutputStream); - - Assert.assertEquals( - expectedResponse, new String(responseOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertArrayEquals(expectedResponse, ByteStreams.toByteArray(response.getBody())); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java deleted file mode 100644 index 3fd4a0adfe..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestBlobProgressListener.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import java.time.Duration; -import java.util.function.Consumer; - -/** - * Test implementation of {@link BlobProgressListener} that always forwards to a {@link Consumer - * }. - */ -public class TestBlobProgressListener implements BlobProgressListener { - - private final Consumer byteCountConsumer; - - public TestBlobProgressListener(Consumer byteCountConsumer) { - this.byteCountConsumer = byteCountConsumer; - } - - @Override - public void handleByteCount(long byteCount) { - byteCountConsumer.accept(byteCount); - } - - @Override - public Duration getDelayBetweenCallbacks() { - return Duration.ofSeconds(-1); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index f870911d26..52dd89eb78 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -16,10 +16,11 @@ package com.google.cloud.tools.jib.http; -import com.google.cloud.tools.jib.blob.Blobs; +import com.google.common.io.ByteStreams; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import javax.net.ssl.SSLException; import org.junit.Assert; @@ -37,7 +38,9 @@ public void testGet() Response response = connection.send("GET", new Request.Builder().build()); Assert.assertEquals(200, response.getStatusCode()); - Assert.assertEquals("Hello World!", Blobs.writeToString(response.getBody())); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); } } @@ -81,7 +84,9 @@ public void testInsecureConnection() Response response = connection.send("GET", new Request.Builder().build()); Assert.assertEquals(200, response.getStatusCode()); - Assert.assertEquals("Hello World!", Blobs.writeToString(response.getBody())); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index 01892bde82..cd99363669 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -23,17 +23,17 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; -import com.google.common.io.ByteStreams; import com.google.common.io.Resources; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -127,13 +127,9 @@ public void testGetContainerConfiguration() String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the container configuration and writes the JSON string. - Blob containerConfigurationBlob = imageToJsonTranslator.getContainerConfigurationBlob(); + JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - containerConfigurationBlob.writeTo(byteArrayOutputStream); - - Assert.assertEquals( - expectedJson, new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfiguration)); } @Test @@ -184,9 +180,8 @@ private void testGetManifest( String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the manifest and writes the JSON string. - Blob containerConfigurationBlob = imageToJsonTranslator.getContainerConfigurationBlob(); - BlobDescriptor blobDescriptor = - containerConfigurationBlob.writeTo(ByteStreams.nullOutputStream()); + JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); + BlobDescriptor blobDescriptor = Digests.computeDigest(containerConfiguration); T manifestTemplate = imageToJsonTranslator.getManifestTemplate(manifestTemplateClass, blobDescriptor); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java index 6e202dd1a1..09d9f09e39 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java @@ -17,14 +17,13 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.http.TestBlobProgressListener; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -63,17 +62,19 @@ public void setUpFakes() throws DigestException { fakeDigest, layerOutputStream, ignored -> {}, - new TestBlobProgressListener(ignored -> {})); + ignored -> {}); } @Test public void testHandleResponse() throws IOException, UnexpectedBlobDigestException { - Blob testBlob = Blobs.from("some BLOB content"); - DescriptorDigest testBlobDigest = testBlob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + InputStream blobContent = + new ByteArrayInputStream("some BLOB content".getBytes(StandardCharsets.UTF_8)); + DescriptorDigest testBlobDigest = Digests.computeDigest(blobContent).getDigest(); + blobContent.reset(); Response mockResponse = Mockito.mock(Response.class); Mockito.when(mockResponse.getContentLength()).thenReturn((long) "some BLOB content".length()); - Mockito.when(mockResponse.getBody()).thenReturn(testBlob); + Mockito.when(mockResponse.getBody()).thenReturn(blobContent); LongAdder byteCount = new LongAdder(); BlobPuller blobPuller = @@ -82,22 +83,24 @@ public void testHandleResponse() throws IOException, UnexpectedBlobDigestExcepti testBlobDigest, layerOutputStream, size -> Assert.assertEquals("some BLOB content".length(), size.longValue()), - new TestBlobProgressListener(byteCount::add)); + byteCount::add); blobPuller.handleResponse(mockResponse); Assert.assertEquals( "some BLOB content", new String(layerContentOutputStream.toByteArray(), StandardCharsets.UTF_8)); - Assert.assertEquals(testBlobDigest, layerOutputStream.getDigest()); + Assert.assertEquals(testBlobDigest, layerOutputStream.computeDigest().getDigest()); Assert.assertEquals("some BLOB content".length(), byteCount.sum()); } @Test public void testHandleResponse_unexpectedDigest() throws IOException { - Blob testBlob = Blobs.from("some BLOB content"); - DescriptorDigest testBlobDigest = testBlob.writeTo(ByteStreams.nullOutputStream()).getDigest(); + InputStream blobContent = + new ByteArrayInputStream("some BLOB content".getBytes(StandardCharsets.UTF_8)); + DescriptorDigest testBlobDigest = Digests.computeDigest(blobContent).getDigest(); + blobContent.reset(); Response mockResponse = Mockito.mock(Response.class); - Mockito.when(mockResponse.getBody()).thenReturn(testBlob); + Mockito.when(mockResponse.getBody()).thenReturn(blobContent); try { testBlobPuller.handleResponse(mockResponse); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index a0a1b50106..44bf89b649 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; -import com.google.cloud.tools.jib.http.TestBlobProgressListener; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.MalformedURLException; @@ -165,8 +164,7 @@ public void testInitializer_getActionDescription() { @Test public void testWriter_getContent() throws IOException { LongAdder byteCount = new LongAdder(); - BlobHttpContent body = - testBlobPusher.writer(mockURL, new TestBlobProgressListener(byteCount::add)).getContent(); + BlobHttpContent body = testBlobPusher.writer(mockURL, byteCount::add).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); @@ -181,12 +179,7 @@ public void testWriter_getContent() throws IOException { @Test public void testWriter_GetAccept() { - Assert.assertEquals( - 0, - testBlobPusher - .writer(mockURL, new TestBlobProgressListener(ignored -> {})) - .getAccept() - .size()); + Assert.assertEquals(0, testBlobPusher.writer(mockURL, ignored -> {}).getAccept().size()); } @Test @@ -197,37 +190,25 @@ public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), - testBlobPusher - .writer(mockURL, new TestBlobProgressListener(ignored -> {})) - .handleResponse(mockResponse)); + testBlobPusher.writer(mockURL, ignored -> {}).handleResponse(mockResponse)); } @Test public void testWriter_getApiRoute() throws MalformedURLException { URL fakeUrl = new URL("http://someurl"); - Assert.assertEquals( - fakeUrl, - testBlobPusher - .writer(fakeUrl, new TestBlobProgressListener(ignored -> {})) - .getApiRoute("")); + Assert.assertEquals(fakeUrl, testBlobPusher.writer(fakeUrl, ignored -> {}).getApiRoute("")); } @Test public void testWriter_getHttpMethod() { - Assert.assertEquals( - "PATCH", - testBlobPusher - .writer(mockURL, new TestBlobProgressListener(ignored -> {})) - .getHttpMethod()); + Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL, ignored -> {}).getHttpMethod()); } @Test public void testWriter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher - .writer(mockURL, new TestBlobProgressListener(ignored -> {})) - .getActionDescription()); + testBlobPusher.writer(mockURL, ignored -> {}).getActionDescription()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index 05747193e3..0c5a37fdd4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -24,10 +23,14 @@ import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -44,6 +47,10 @@ @RunWith(MockitoJUnitRunner.class) public class ManifestPullerTest { + private static InputStream stringToInputStreamUtf8(String string) { + return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); + } + @Mock private Response mockResponse; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = @@ -56,8 +63,9 @@ public class ManifestPullerTest { public void testHandleResponse_v21() throws URISyntaxException, IOException, UnknownManifestFormatException { Path v21ManifestFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); + InputStream v21Manifest = new ByteArrayInputStream(Files.readAllBytes(v21ManifestFile)); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v21ManifestFile)); + Mockito.when(mockResponse.getBody()).thenReturn(v21Manifest); ManifestTemplate manifestTemplate = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) @@ -70,8 +78,9 @@ public void testHandleResponse_v21() public void testHandleResponse_v22() throws URISyntaxException, IOException, UnknownManifestFormatException { Path v22ManifestFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); + InputStream v22Manifest = new ByteArrayInputStream(Files.readAllBytes(v22ManifestFile)); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from(v22ManifestFile)); + Mockito.when(mockResponse.getBody()).thenReturn(v22Manifest); ManifestTemplate manifestTemplate = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) @@ -82,7 +91,7 @@ public void testHandleResponse_v22() @Test public void testHandleResponse_noSchemaVersion() throws IOException { - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("{}")); + Mockito.when(mockResponse.getBody()).thenReturn(stringToInputStreamUtf8("{}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An empty manifest should throw an error"); @@ -95,7 +104,7 @@ public void testHandleResponse_noSchemaVersion() throws IOException { @Test public void testHandleResponse_invalidSchemaVersion() throws IOException { Mockito.when(mockResponse.getBody()) - .thenReturn(Blobs.from("{\"schemaVersion\":\"not valid\"}")); + .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\":\"not valid\"}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("A non-integer schemaVersion should throw an error"); @@ -107,7 +116,8 @@ public void testHandleResponse_invalidSchemaVersion() throws IOException { @Test public void testHandleResponse_unknownSchemaVersion() throws IOException { - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("{\"schemaVersion\":0}")); + Mockito.when(mockResponse.getBody()) + .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\":0}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An unknown manifest schemaVersion should throw an error"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index fa61d44cb8..642949eb3f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -23,20 +23,23 @@ import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.http.Authorizations; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.MockConnection; import com.google.cloud.tools.jib.http.Response; +import com.google.common.io.CharStreams; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.net.ConnectException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -86,7 +89,8 @@ public List getAccept() { @Nullable @Override public String handleResponse(Response response) throws IOException { - return Blobs.writeToString(response.getBody()); + return CharStreams.toString( + new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); } @Override @@ -125,7 +129,8 @@ public void setUp() throws IOException { Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); Mockito.when(mockInsecureConnectionFactory.apply(Mockito.any())) .thenReturn(mockInsecureConnection); - Mockito.when(mockResponse.getBody()).thenReturn(Blobs.from("body")); + Mockito.when(mockResponse.getBody()) + .thenReturn(new ByteArrayInputStream("body".getBytes(StandardCharsets.UTF_8))); } @After @@ -638,7 +643,7 @@ private RegistryEndpointCaller createRegistryEndpointCaller( "userAgent", (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), - Authorizations.withBasicToken("token"), + Authorization.fromBasicToken("token"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), allowInsecure, mockConnectionFactory, From 38d79184e553e5b3964113ccb3aa1477a657c6d7 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 17 May 2019 16:25:36 -0400 Subject: [PATCH 0594/2020] Fix groupId in docs. (#1723) --- jib-maven-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 50764174e0..ad3d5c49ea 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -186,7 +186,7 @@ You can also bind `jib:build` to a Maven lifecycle, such as `package`, by adding ```xml - com.google.com.tools + com.google.cloud.tools jib-maven-plugin ... From 7e77bb400b318cac87860d423ed966323e89cff9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 17 May 2019 17:21:09 -0400 Subject: [PATCH 0595/2020] Move EventDispatcher logic to EventHandlers (#1725) --- jib-core/CHANGELOG.md | 3 + .../jib/api/ContainerizerIntegrationTest.java | 5 +- ...icationMethodRetrieverIntegrationTest.java | 8 +- .../registry/BlobCheckerIntegrationTest.java | 8 +- .../registry/BlobPullerIntegrationTest.java | 8 +- .../registry/BlobPusherIntegrationTest.java | 6 +- .../ManifestPullerIntegrationTest.java | 10 +-- .../ManifestPusherIntegrationTest.java | 10 +-- .../RegistryAuthenticatorIntegrationTest.java | 3 +- .../tools/jib/api/JibContainerBuilder.java | 32 +++---- .../jib/builder/ProgressEventDispatcher.java | 24 ++--- .../jib/builder/TimerEventDispatcher.java | 21 ++--- .../builder/steps/AuthenticatePushStep.java | 7 +- .../BuildAndCacheApplicationLayerStep.java | 10 +-- .../jib/builder/steps/BuildImageStep.java | 8 +- .../jib/builder/steps/LoadDockerStep.java | 4 +- .../steps/PullAndCacheBaseImageLayerStep.java | 2 +- .../PullAndCacheBaseImageLayersStep.java | 4 +- .../jib/builder/steps/PullBaseImageStep.java | 14 +-- .../tools/jib/builder/steps/PushBlobStep.java | 4 +- .../steps/PushContainerConfigurationStep.java | 2 +- .../jib/builder/steps/PushImageStep.java | 4 +- .../jib/builder/steps/PushLayersStep.java | 4 +- .../RetrieveRegistryCredentialsStep.java | 6 +- .../tools/jib/builder/steps/StepsRunner.java | 4 +- .../jib/builder/steps/WriteTarFileStep.java | 2 +- .../jib/configuration/BuildConfiguration.java | 31 +++---- .../jib/event/DefaultEventDispatcher.java | 46 ---------- .../tools/jib/event/EventDispatcher.java | 23 ----- .../cloud/tools/jib/event/EventHandlers.java | 88 ++++++++++++++----- .../frontend/CredentialRetrieverFactory.java | 40 ++++----- .../tools/jib/frontend/MainClassFinder.java | 14 +-- .../tools/jib/registry/ManifestPusher.java | 10 +-- .../tools/jib/registry/RegistryClient.java | 32 ++++--- .../jib/registry/RegistryEndpointCaller.java | 18 ++-- .../DockerConfigCredentialRetriever.java | 21 ++--- .../jib/api/JibContainerBuilderTest.java | 4 +- .../builder/ProgressEventDispatcherTest.java | 16 ++-- .../jib/builder/TimerEventDispatcherTest.java | 9 +- ...BuildAndCacheApplicationLayerStepTest.java | 8 +- .../jib/builder/steps/BuildImageStepTest.java | 20 ++--- .../RetrieveRegistryCredentialsStepTest.java | 26 +++--- .../jib/event/DefaultEventDispatcherTest.java | 67 -------------- .../tools/jib/event/EventHandlersTest.java | 45 +++++++++- .../tools/jib/event/events/LogEventTest.java | 19 ++-- .../jib/event/events/ProgressEventTest.java | 31 +++---- .../progress/ProgressEventHandlerTest.java | 19 ++-- .../CredentialRetrieverFactoryTest.java | 24 ++--- .../jib/frontend/MainClassFinderTest.java | 22 ++--- .../jib/registry/ManifestPusherTest.java | 12 +-- .../jib/registry/RegistryClientTest.java | 6 +- .../registry/RegistryEndpointCallerTest.java | 18 ++-- .../DockerConfigCredentialRetrieverTest.java | 31 +++---- .../tools/jib/gradle/BuildDockerTask.java | 3 +- .../tools/jib/gradle/BuildImageTask.java | 3 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 3 +- .../jib/gradle/GradleProjectProperties.java | 5 +- .../tools/jib/maven/BuildDockerMojo.java | 6 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 6 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 6 +- .../jib/maven/MavenProjectProperties.java | 5 +- .../jib/maven/MavenRawConfigurationTest.java | 6 +- .../ConfigurationPropertyValidator.java | 20 ++--- .../jib/plugins/common/JibBuildRunner.java | 14 +-- .../jib/plugins/common/MainClassResolver.java | 14 +-- .../common/PluginConfigurationProcessor.java | 33 +++---- .../ConfigurationPropertyValidatorTest.java | 31 ++++--- .../plugins/common/JibBuildRunnerTest.java | 41 +++------ .../plugins/common/MainClassResolverTest.java | 2 +- .../PluginConfigurationProcessorTest.java | 2 +- 70 files changed, 487 insertions(+), 626 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 6f6d6e36e4..88db723247 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. ### Changed +- Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package +- Removed `EventDispatcher` and `DefaultEventDispatcher`; events are now dispatched directly from `EventHandlers` + ### Fixed ## 0.9.2 diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 592d696401..61d5e34d66 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -161,9 +161,10 @@ private static void assertDockerInspect(String imageReference) private final ProgressChecker progressChecker = new ProgressChecker(); private final EventHandlers eventHandlers = - new EventHandlers() + EventHandlers.builder() .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) - .add(JibEventType.LAYER_COUNT, layerCountConsumer); + .add(JibEventType.LAYER_COUNT, layerCountConsumer) + .build(); @Test public void testSteps_forBuildToDockerRegistry() diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index fa621cca24..28465fed7e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -26,19 +26,19 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testGetRegistryAuthenticator() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_HANDLERS, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(null); RegistryClient authorizedRegistryClient = - RegistryClient.factory(EVENT_DISPATCHER, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EVENT_HANDLERS, "registry.hub.docker.com", "library/busybox") .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index cf8f3c4259..a054c68fab 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import java.security.DigestException; @@ -30,13 +30,13 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testCheck_exists() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = @@ -51,7 +51,7 @@ public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); DescriptorDigest fakeBlobDigest = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 9d4c26e63f..ec22cb167a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.io.ByteStreams; import java.io.IOException; @@ -36,7 +36,7 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -45,7 +45,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls the busybox image. localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -77,7 +77,7 @@ public void testPull_unknownBlob() throws IOException, DigestException, Interrup "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index eb6e5f34df..6126aaef0f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -31,7 +31,7 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testPush() @@ -44,7 +44,7 @@ public void testPush() "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 485e788bfe..f99656473d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -31,13 +31,13 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testPull_v21() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -51,7 +51,7 @@ public void testPull_v21() throws IOException, RegistryException, InterruptedExc public void testPull_v22() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_HANDLERS, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -65,7 +65,7 @@ public void testPull_unknownManifest() localRegistry.pullAndPushToLocal("busybox", "busybox"); try { RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 3523145d8d..01bbdc1022 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -36,18 +36,18 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventDispatcher EVENT_DISPATCHER = jibEvent -> {}; + private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EVENT_HANDLERS, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "busybox") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); try { @@ -83,7 +83,7 @@ public void testPush() // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory(EVENT_DISPATCHER, "localhost:5000", "testimage") + RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse( diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index 3095a104b2..b4807fad1f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; import org.junit.Assert; @@ -33,7 +34,7 @@ public void testAuthenticate() ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryClient.factory( - ignored -> {}, + EventHandlers.none(), dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) .newRegistryClient() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 3d591e3357..36e971fc68 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -23,8 +23,7 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.LayerEntry; @@ -475,11 +474,11 @@ JibContainer containerize( BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer, executorService); - EventDispatcher eventDispatcher = buildConfiguration.getEventDispatcher(); - logSources(eventDispatcher); + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + logSources(eventHandlers); try (TimerEventDispatcher ignored = - new TimerEventDispatcher(eventDispatcher, containerizer.getDescription())) { + new TimerEventDispatcher(eventHandlers, containerizer.getDescription())) { BuildResult result = containerizer.createStepsRunner(buildConfiguration).run(); return new JibContainer(result.getImageDigest(), result.getImageId()); @@ -511,7 +510,7 @@ JibContainer containerize( BuildConfiguration toBuildConfiguration( Containerizer containerizer, ExecutorService executorService) throws CacheDirectoryCreationException, IOException { - buildConfigurationBuilder + return buildConfigurationBuilder .setTargetImageConfiguration(containerizer.getImageConfiguration()) .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) @@ -521,32 +520,25 @@ BuildConfiguration toBuildConfiguration( .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) .setOffline(containerizer.isOfflineMode()) .setToolName(containerizer.getToolName()) - .setExecutorService(executorService); - - containerizer - .getEventHandlers() - .ifPresent( - eventHandlers -> - buildConfigurationBuilder.setEventDispatcher( - new DefaultEventDispatcher(eventHandlers))); - - return buildConfigurationBuilder.build(); + .setExecutorService(executorService) + .setEventHandlers(containerizer.getEventHandlers().orElse(EventHandlers.none())) + .build(); } - private void logSources(EventDispatcher eventDispatcher) { + private void logSources(EventHandlers eventHandlers) { // Logs the different source files used. - eventDispatcher.dispatch(LogEvent.info("Containerizing application with the following files:")); + eventHandlers.dispatch(LogEvent.info("Containerizing application with the following files:")); for (LayerConfiguration layerConfiguration : layerConfigurations) { if (layerConfiguration.getLayerEntries().isEmpty()) { continue; } - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.info("\t" + capitalizeFirstLetter(layerConfiguration.getName()) + ":")); for (LayerEntry layerEntry : layerConfiguration.getLayerEntries()) { - eventDispatcher.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); + eventHandlers.dispatch(LogEvent.info("\t\t" + layerEntry.getSourceFile())); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index 414d928ccb..fa693b465a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.Allocation; import com.google.common.base.Preconditions; @@ -54,34 +54,34 @@ ProgressEventDispatcher create( /** * Creates a new {@link ProgressEventDispatcher} with a root {@link Allocation}. * - * @param eventDispatcher the {@link EventDispatcher} + * @param eventHandlers the {@link EventHandlers} * @param description user-facing description of what the allocation represents * @param allocationUnits number of allocation units * @return a new {@link ProgressEventDispatcher} */ public static ProgressEventDispatcher newRoot( - EventDispatcher eventDispatcher, String description, long allocationUnits) { + EventHandlers eventHandlers, String description, long allocationUnits) { return newProgressEventDispatcher( - eventDispatcher, Allocation.newRoot(description, allocationUnits), BuildStepType.ALL); + eventHandlers, Allocation.newRoot(description, allocationUnits), BuildStepType.ALL); } /** * Creates a new {@link ProgressEventDispatcher} and dispatches a new {@link ProgressEvent} with * progress 0 for {@code allocation}. * - * @param eventDispatcher the {@link EventDispatcher} + * @param eventHandlers the {@link EventHandlers} * @param allocation the {@link Allocation} to manage * @return a new {@link ProgressEventDispatcher} */ private static ProgressEventDispatcher newProgressEventDispatcher( - EventDispatcher eventDispatcher, Allocation allocation, BuildStepType buildStepType) { + EventHandlers eventHandlers, Allocation allocation, BuildStepType buildStepType) { ProgressEventDispatcher progressEventDispatcher = - new ProgressEventDispatcher(eventDispatcher, allocation, buildStepType); + new ProgressEventDispatcher(eventHandlers, allocation, buildStepType); progressEventDispatcher.dispatchProgress(0); return progressEventDispatcher; } - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final Allocation allocation; private final BuildStepType buildStepType; @@ -89,8 +89,8 @@ private static ProgressEventDispatcher newProgressEventDispatcher( private boolean closed = false; private ProgressEventDispatcher( - EventDispatcher eventDispatcher, Allocation allocation, BuildStepType buildStepType) { - this.eventDispatcher = eventDispatcher; + EventHandlers eventHandlers, Allocation allocation, BuildStepType buildStepType) { + this.eventHandlers = eventHandlers; this.allocation = allocation; this.buildStepType = buildStepType; @@ -117,7 +117,7 @@ public ProgressEventDispatcher create( Preconditions.checkState(!used); used = true; return newProgressEventDispatcher( - eventDispatcher, allocation.newChild(description, allocationUnits), buildStepType); + eventHandlers, allocation.newChild(description, allocationUnits), buildStepType); } }; } @@ -139,7 +139,7 @@ public void close() { */ public void dispatchProgress(long progressUnits) { long unitsDecremented = decrementRemainingAllocationUnits(progressUnits); - eventDispatcher.dispatch(new ProgressEvent(allocation, unitsDecremented, buildStepType)); + eventHandlers.dispatch(new ProgressEvent(allocation, unitsDecremented, buildStepType)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java index e08eb0703a..3c136327a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.events.TimerEvent.State; import com.google.common.annotations.VisibleForTesting; @@ -30,7 +30,7 @@ public class TimerEventDispatcher implements Closeable { private static final Clock DEFAULT_CLOCK = Clock.systemUTC(); - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final String description; private final Clock clock; @@ -39,20 +39,17 @@ public class TimerEventDispatcher implements Closeable { /** * Creates a new {@link TimerEventDispatcher}. * - * @param eventDispatcher the {@link EventDispatcher} used to dispatch the {@link TimerEvent}s + * @param eventHandlers the {@link EventHandlers} used to dispatch the {@link TimerEvent}s * @param description the default description for the {@link TimerEvent}s */ - public TimerEventDispatcher(EventDispatcher eventDispatcher, String description) { - this(eventDispatcher, description, DEFAULT_CLOCK, null); + public TimerEventDispatcher(EventHandlers eventHandlers, String description) { + this(eventHandlers, description, DEFAULT_CLOCK, null); } @VisibleForTesting TimerEventDispatcher( - EventDispatcher eventDispatcher, - String description, - Clock clock, - @Nullable Timer parentTimer) { - this.eventDispatcher = eventDispatcher; + EventHandlers eventHandlers, String description, Clock clock, @Nullable Timer parentTimer) { + this.eventHandlers = eventHandlers; this.description = description; this.clock = clock; this.timer = new Timer(clock, parentTimer); @@ -67,7 +64,7 @@ public TimerEventDispatcher(EventDispatcher eventDispatcher, String description) * @return the new {@link TimerEventDispatcher} */ public TimerEventDispatcher subTimer(String description) { - return new TimerEventDispatcher(eventDispatcher, description, clock, timer); + return new TimerEventDispatcher(eventHandlers, description, clock, timer); } /** @@ -98,7 +95,7 @@ public void close() { } private void dispatchTimerEvent(State state, Duration duration, String eventDescription) { - eventDispatcher.dispatch( + eventHandlers.dispatch( new TimerEvent(state, timer, duration, timer.getElapsedTime(), eventDescription)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index b46980a19b..29a06755bd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.InsecureRegistryException; -import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; @@ -75,9 +74,7 @@ public ListenableFuture getFuture() { @Override @Nullable - public Authorization call() - throws ExecutionException, RegistryAuthenticationFailedException, IOException, - RegistryException { + public Authorization call() throws ExecutionException, IOException, RegistryException { Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); @@ -86,7 +83,7 @@ public Authorization call() BuildStepType.AUTHENTICATE_PUSH, "authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, registry))) { + buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, registry))) { RegistryAuthenticator registryAuthenticator = buildConfiguration .newTargetImageRegistryClientFactory() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index dc33132d21..6d2557a2c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -57,7 +57,7 @@ static ImmutableList makeList( "setting up to build application layers", layerCount); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = ImmutableList.builderWithExpectedSize(layerCount); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { @@ -77,7 +77,7 @@ static ImmutableList makeList( ImmutableList steps = buildAndCacheApplicationLayerSteps.build(); buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch( new LayerCountEvent(BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, steps.size())); return steps; @@ -115,7 +115,7 @@ public ListenableFuture getFuture() { public CachedLayer call() throws IOException, CacheCorruptedException { String description = "Building " + layerType + " layer"; - buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); + buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( @@ -123,7 +123,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { "building " + layerType + " layer", 1); TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); // Don't build the layer if it exists already. @@ -138,7 +138,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { cache.writeUncompressedLayer(layerBlob, layerConfiguration.getLayerEntries()); buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); return cachedLayer; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 5edc8e35fb..8bb5dff335 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -93,7 +93,7 @@ private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNo progressEventDispatcherFactory.create( BuildStepType.BUILD_IMAGE, "building image format", 1); TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); @@ -192,8 +192,8 @@ private ImmutableList computeEntrypoint( if (entrypointToUse != null) { String logSuffix = shouldInherit ? " (inherited from base image)" : ""; String message = "Container entrypoint set to " + entrypointToUse + logSuffix; - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle("")); - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(message)); + buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle("")); + buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle(message)); } return entrypointToUse; @@ -226,7 +226,7 @@ private ImmutableList computeProgramArguments( if (programArgumentsToUse != null) { String logSuffix = shouldInherit ? " (inherited from base image)" : ""; String message = "Container program arguments set to " + programArgumentsToUse + logSuffix; - buildConfiguration.getEventDispatcher().dispatch(LogEvent.lifecycle(message)); + buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle(message)); } return programArgumentsToUse; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 788056a119..be45345226 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -90,7 +90,7 @@ public BuildResult call() throws ExecutionException, InterruptedException { private BuildResult afterPushBaseImageLayerFuturesFuture() throws ExecutionException, InterruptedException, IOException { buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = @@ -102,7 +102,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() // Load the image to docker daemon. buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch( LogEvent.debug(dockerClient.load(new ImageTarball(image, targetImageReference)))); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index eb1df521b2..4d7988e0c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -75,7 +75,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { 1); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), String.format(DESCRIPTION, layerDigest))) { + buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, layerDigest))) { Cache cache = buildConfiguration.getBaseImageLayersCache(); // Checks if the layer already exists in the cache. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index db47168c88..431bada590 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -81,11 +81,11 @@ public ImmutableList call() "checking base image layers", baseImageLayers.size()); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = ImmutableList.builderWithExpectedSize(baseImageLayers.size()); buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch( new LayerCountEvent( BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, baseImageLayers.size())); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 0867d6bbd3..c0808e6afe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -32,7 +32,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; @@ -113,16 +113,16 @@ public BaseImageWithAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, CacheCorruptedException { - EventDispatcher eventDispatcher = buildConfiguration.getEventDispatcher(); + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); // Skip this step if this is a scratch image ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); if (baseImageConfiguration.getImage().isScratch()) { - eventDispatcher.dispatch(LogEvent.progress("Getting scratch base image...")); + eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); return new BaseImageWithAuthorization( Image.builder(buildConfiguration.getTargetFormat()).build(), null); } - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.progress( "Getting base image " + buildConfiguration.getBaseImageConfiguration().getImage() @@ -136,13 +136,13 @@ public BaseImageWithAuthorization call() progressEventDispatcherFactory.create( BuildStepType.PULL_BASE_IMAGE, "pulling base image manifest", 2); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // First, try with no credentials. try { return new BaseImageWithAuthorization(pullBaseImage(null, progressEventDispatcher), null); } catch (RegistryUnauthorizedException ex) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.lifecycle( "The base image requires auth. Trying again for " + buildConfiguration.getBaseImageConfiguration().getImage() @@ -189,7 +189,7 @@ public BaseImageWithAuthorization call() } catch (InsecureRegistryException insecureRegistryException) { // Cannot skip certificate validation or use HTTP; fall through. } - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.error( "Failed to retrieve authentication challenge for registry that required token authentication")); throw registryUnauthorizedException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 0528127c16..515cb5ed0f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -85,7 +85,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc blobDescriptor.getSize()); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventDispatcher(), DESCRIPTION + blobDescriptor); + buildConfiguration.getEventHandlers(), DESCRIPTION + blobDescriptor); ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { RegistryClient registryClient = @@ -97,7 +97,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc // check if the BLOB is available if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 2cd50c5d92..cde36688e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -89,7 +89,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() progressEventDispatcherFactory.create( BuildStepType.PUSH_CONTAINER_CONFIGURATION, "pushing container configuration", 1); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); JsonTemplate containerConfiguration = new ImageToJsonTranslator(image).getContainerConfiguration(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 0c8faf73a9..430ec83992 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -125,7 +125,7 @@ private BuildResult afterAllPushed() BuildStepType.PUSH_IMAGE, "pushing image manifest", targetImageTags.size()); try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() @@ -156,7 +156,7 @@ private BuildResult afterAllPushed() progressEventDispatcherFactory.create( BuildStepType.PUSH_IMAGE, "tagging with " + tag, 1)) { buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.info("Tagging with " + tag + "...")); registryClient.pushManifest(manifestTemplate, tag); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 6cb22307b7..6caac34272 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -79,7 +79,7 @@ public ListenableFuture>> getFuture() { @Override public ImmutableList> call() throws ExecutionException { try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), DESCRIPTION)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList> cachedLayers = NonBlockingSteps.get(cachedLayerStep); @@ -90,7 +90,7 @@ public ImmutableList> call() throws ExecutionException { ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(new LayerCountEvent(buildStepType, cachedLayers.size())); for (AsyncStep cachedLayerStep : cachedLayers) { ProgressEventDispatcher.Factory childProgressEventDispatcherFactory = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index fe2a82403e..47d3cfaeb8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -104,13 +104,13 @@ public ListenableFuture getFuture() { public Credential call() throws CredentialRetrievalException { String description = makeDescription(registry); - buildConfiguration.getEventDispatcher().dispatch(LogEvent.progress(description + "...")); + buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create( buildStepType, "retrieving credentials for " + registry, 1); TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventDispatcher(), description)) { + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { @@ -121,7 +121,7 @@ public Credential call() throws CredentialRetrievalException { // If no credentials found, give an info (not warning because in most cases, the base image is // public and does not need extra credentials) and return null. buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.info("No credentials could be retrieved for registry " + registry)); return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 7dcd359d0b..887ef0c93f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -253,9 +253,7 @@ public BuildResult run() throws ExecutionException, InterruptedException { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot( - buildConfiguration.getEventDispatcher(), - rootProgressAllocationDescription, - stepsCount)) { + buildConfiguration.getEventHandlers(), rootProgressAllocationDescription, stepsCount)) { rootProgressEventDispatcher = progressEventDispatcher; stepsRunnable.run(); return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index bd97b11520..51c7e64455 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -90,7 +90,7 @@ public BuildResult call() throws ExecutionException, InterruptedException { private BuildResult writeTarFile() throws ExecutionException, IOException { buildConfiguration - .getEventDispatcher() + .getEventHandlers() .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index a69f3be4d5..bc472a433e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -61,10 +61,7 @@ public static class Builder { private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; private String toolName = DEFAULT_TOOL_NAME; - private EventDispatcher eventDispatcher = - jibEvent -> { - /* No-op EventDispatcher. */ - }; + private EventHandlers eventHandlers = EventHandlers.none(); @Nullable private ExecutorService executorService; private Builder() {} @@ -192,13 +189,13 @@ public Builder setToolName(String toolName) { } /** - * Sets the {@link EventDispatcher} to dispatch events with. + * Sets the {@link EventHandlers} to dispatch events with. * - * @param eventDispatcher the {@link EventDispatcher} + * @param eventHandlers the {@link EventHandlers} * @return this */ - public Builder setEventDispatcher(EventDispatcher eventDispatcher) { - this.eventDispatcher = eventDispatcher; + public Builder setEventHandlers(EventHandlers eventHandlers) { + this.eventHandlers = eventHandlers; return this; } @@ -242,7 +239,7 @@ public BuildConfiguration build() throws IOException { switch (missingFields.size()) { case 0: // No errors if (Preconditions.checkNotNull(baseImageConfiguration).getImage().usesDefaultTag()) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( "Base image '" + baseImageConfiguration.getImage() @@ -261,7 +258,7 @@ public BuildConfiguration build() throws IOException { offline, layerConfigurations, toolName, - eventDispatcher, + eventHandlers, Preconditions.checkNotNull(executorService)); case 1: @@ -314,7 +311,7 @@ public static Builder builder() { private final boolean offline; private final ImmutableList layerConfigurations; private final String toolName; - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final ExecutorService executorService; /** Instantiate with {@link #builder}. */ @@ -330,7 +327,7 @@ private BuildConfiguration( boolean offline, ImmutableList layerConfigurations, String toolName, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, ExecutorService executorService) { this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; @@ -343,7 +340,7 @@ private BuildConfiguration( this.offline = offline; this.layerConfigurations = layerConfigurations; this.toolName = toolName; - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.executorService = executorService; } @@ -376,8 +373,8 @@ public String getToolName() { return toolName; } - public EventDispatcher getEventDispatcher() { - return eventDispatcher; + public EventHandlers getEventHandlers() { + return eventHandlers; } public ExecutorService getExecutorService() { @@ -451,7 +448,7 @@ public RegistryClient.Factory newTargetImageRegistryClientFactory() { private RegistryClient.Factory newRegistryClientFactory(ImageConfiguration imageConfiguration) { return RegistryClient.factory( - getEventDispatcher(), + getEventHandlers(), imageConfiguration.getImageRegistry(), imageConfiguration.getImageRepository()) .setAllowInsecureRegistries(getAllowInsecureRegistries()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java deleted file mode 100644 index b49aa021d9..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/DefaultEventDispatcher.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.event; - -import com.google.common.collect.ImmutableMultimap; - -/** Dispatches {@link JibEvent}s to event handlers. */ -public class DefaultEventDispatcher implements EventDispatcher { - - /** Maps from {@link JibEvent} class to handlers for that event type. */ - private final ImmutableMultimap, Handler> handlers; - - /** - * Creates an instance from {@link Handler}s in an {@link EventHandlers}. - * - * @param eventHandlers the {@link EventHandlers} to get the {@link Handler}s from - */ - public DefaultEventDispatcher(EventHandlers eventHandlers) { - handlers = eventHandlers.getHandlers(); - } - - /** - * Dispatches {@code jibEvent} to all the handlers that can handle it. - * - * @param jibEvent the {@link JibEvent} to dispatch - */ - @Override - public void dispatch(JibEvent jibEvent) { - handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); - handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java deleted file mode 100644 index 3c617d63a9..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventDispatcher.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.event; - -/** Dispatches {@link JibEvent}s. */ -public interface EventDispatcher { - - void dispatch(JibEvent jibEvent); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index 469b7baec8..f85e0ba82c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; @@ -24,38 +25,82 @@ /** Builds a set of event handlers to handle {@link JibEvent}s. */ public class EventHandlers { + /** Builder for {@link EventHandlers}. */ + public static class Builder { + + private final Multimap, Handler> handlers = + ArrayListMultimap.create(); + + /** + * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. + * The order in which handlers are added is the order in which they are called when the event is + * dispatched. + * + *

Note: Implementations of {@code eventConsumer} must be thread-safe. + * + * @param eventType the event type that {@code eventConsumer} should handle + * @param eventConsumer the event handler + * @param the type of {@code eventClass} + * @return this + */ + public Builder add(JibEventType eventType, Consumer eventConsumer) { + Class eventClass = eventType.getEventClass(); + handlers.put(eventClass, new Handler<>(eventClass, eventConsumer)); + return this; + } + + /** + * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link + * #add(JibEventType, Consumer)} for more details. + * + * @param eventConsumer the event handler + * @return this + */ + public Builder add(Consumer eventConsumer) { + return add(JibEventType.ALL, eventConsumer); + } + + public EventHandlers build() { + return new EventHandlers(handlers); + } + } + /** Maps from {@link JibEvent} class to handlers for that event type. */ - private final Multimap, Handler> handlers = - ArrayListMultimap.create(); + private final ImmutableMultimap, Handler> handlers; + + private EventHandlers(Multimap, Handler> handlers) { + this.handlers = ImmutableMultimap.copyOf(handlers); + } /** - * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventClass}. - * The order in which handlers are added is the order in which they are called when the event is - * dispatched. + * Returns an empty {@link EventHandlers}. * - *

Note: Implementations of {@code eventConsumer} must be thread-safe. + * @return the {@link EventHandlers} + */ + public static EventHandlers none() { + return new Builder().build(); + } + + /** + * Creates a new {@link EventHandlers.Builder}. * - * @param eventType the event type that {@code eventConsumer} should handle - * @param eventConsumer the event handler - * @param the type of {@code eventClass} - * @return this + * @return the builder */ - public EventHandlers add( - JibEventType eventType, Consumer eventConsumer) { - Class eventClass = eventType.getEventClass(); - handlers.put(eventClass, new Handler<>(eventClass, eventConsumer)); - return this; + public static Builder builder() { + return new Builder(); } /** - * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link - * #add(JibEventType, Consumer)} for more details. + * Dispatches {@code jibEvent} to all the handlers that can handle it. * - * @param eventConsumer the event handler - * @return this + * @param jibEvent the {@link JibEvent} to dispatch */ - public EventHandlers add(Consumer eventConsumer) { - return add(JibEventType.ALL, eventConsumer); + public void dispatch(JibEvent jibEvent) { + if (handlers.isEmpty()) { + return; + } + handlers.get(JibEvent.class).forEach(handler -> handler.handle(jibEvent)); + handlers.get(jibEvent.getClass()).forEach(handler -> handler.handle(jibEvent)); } /** @@ -63,6 +108,7 @@ public EventHandlers add(Consumer eventConsumer) { * * @return the map from {@link JibEvent} type to a list of {@link Handler}s */ + @VisibleForTesting ImmutableMultimap, Handler> getHandlers() { return ImmutableMultimap.copyOf(handlers); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index ff3a49d49d..8780464dad 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -19,8 +19,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventDispatcher; -import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; @@ -57,13 +56,13 @@ interface DockerCredentialHelperFactory { * Creates a new {@link CredentialRetrieverFactory} for an image. * * @param imageReference the image the credential are for - * @param eventDispatcher an event dispatcher for dispatching log events + * @param eventHandlers an event handlers for dispatching log events * @return a new {@link CredentialRetrieverFactory} */ public static CredentialRetrieverFactory forImage( - ImageReference imageReference, EventDispatcher eventDispatcher) { + ImageReference imageReference, EventHandlers eventHandlers) { return new CredentialRetrieverFactory( - imageReference, eventDispatcher, DockerCredentialHelper::new); + imageReference, eventHandlers, DockerCredentialHelper::new); } /** @@ -74,20 +73,20 @@ public static CredentialRetrieverFactory forImage( */ public static CredentialRetrieverFactory forImage(ImageReference imageReference) { return new CredentialRetrieverFactory( - imageReference, ignored -> {}, DockerCredentialHelper::new); + imageReference, EventHandlers.none(), DockerCredentialHelper::new); } - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final ImageReference imageReference; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, DockerCredentialHelperFactory dockerCredentialHelperFactory) { this.imageReference = imageReference; - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } @@ -127,13 +126,13 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - dispatchEvent(LogEvent.info("Checking credentials from " + credentialHelper)); + eventHandlers.dispatch(LogEvent.info("Checking credentials from " + credentialHelper)); try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { - dispatchEvent( + eventHandlers.dispatch( LogEvent.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); return Optional.empty(); @@ -177,9 +176,9 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - dispatchEvent(LogEvent.info(ex.getMessage())); + eventHandlers.dispatch(LogEvent.info(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - dispatchEvent(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); + eventHandlers.dispatch(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } } @@ -221,16 +220,16 @@ CredentialRetriever dockerConfig( return () -> { try { Optional dockerConfigCredentials = - dockerConfigCredentialRetriever.retrieve(eventDispatcher); + dockerConfigCredentialRetriever.retrieve(eventHandlers); if (dockerConfigCredentials.isPresent()) { - dispatchEvent( + eventHandlers.dispatch( LogEvent.info( "Using credentials from Docker config for " + imageReference.getRegistry())); return dockerConfigCredentials; } } catch (IOException ex) { - dispatchEvent(LogEvent.info("Unable to parse Docker config")); + eventHandlers.dispatch(LogEvent.info("Unable to parse Docker config")); } return Optional.empty(); }; @@ -248,14 +247,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) } private void logGotCredentialsFrom(String credentialSource) { - dispatchEvent( + eventHandlers.dispatch( LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); } - - private void dispatchEvent(JibEvent jibEvent) { - if (eventDispatcher == null) { - return; - } - eventDispatcher.dispatch(jibEvent); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java index 55313079ed..40148bb50b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -138,17 +138,17 @@ public MethodVisitor visitMethod( } private final ImmutableList files; - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; /** * Finds a class with {@code psvm} (see class javadoc) in {@code files}. * * @param files the files to check - * @param eventDispatcher used for dispatching log events. + * @param eventHandlers used for dispatching log events. */ - public MainClassFinder(List files, EventDispatcher eventDispatcher) { + public MainClassFinder(List files, EventHandlers eventHandlers) { this.files = ImmutableList.copyOf(files); - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; } /** @@ -198,11 +198,11 @@ private Optional findMainClass(Path file) { } catch (ArrayIndexOutOfBoundsException ignored) { // Not a valid class file (thrown by ClassReader if it reads an invalid format) - eventDispatcher.dispatch(LogEvent.warn("Invalid class file found: " + file)); + eventHandlers.dispatch(LogEvent.warn("Invalid class file found: " + file)); } catch (IOException ignored) { // Could not read class file. - eventDispatcher.dispatch(LogEvent.warn("Could not read file: " + file)); + eventHandlers.dispatch(LogEvent.warn("Could not read file: " + file)); } return Optional.empty(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 7a5f162eda..0e3a49ccf7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -20,7 +20,7 @@ import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -66,17 +66,17 @@ private static String makeUnexpectedImageDigestWarning( private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final BuildableManifestTemplate manifestTemplate; private final String imageTag; - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; ManifestPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, BuildableManifestTemplate manifestTemplate, String imageTag, - EventDispatcher eventDispatcher) { + EventHandlers eventHandlers) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.manifestTemplate = manifestTemplate; this.imageTag = imageTag; - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; } @Override @@ -141,7 +141,7 @@ public DescriptorDigest handleResponse(Response response) throws IOException { } // The received digest is not as expected. Warns about this. - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn(makeUnexpectedImageDigestWarning(expectedDigest, receivedDigests))); return expectedDigest; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 1737d52956..b1b7e9e2c9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -44,7 +44,7 @@ public class RegistryClient { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private boolean allowInsecureRegistries = false; @@ -52,9 +52,9 @@ public static class Factory { @Nullable private Authorization authorization; private Factory( - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, RegistryEndpointRequestProperties registryEndpointRequestProperties) { - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.registryEndpointRequestProperties = registryEndpointRequestProperties; } @@ -99,7 +99,7 @@ public Factory setUserAgentSuffix(@Nullable String userAgentSuffix) { */ public RegistryClient newRegistryClient() { return new RegistryClient( - eventDispatcher, + eventHandlers, authorization, registryEndpointRequestProperties, allowInsecureRegistries, @@ -132,18 +132,16 @@ private String makeUserAgent() { /** * Creates a new {@link Factory} for building a {@link RegistryClient}. * - * @param eventDispatcher the event dispatcher used for dispatching log events + * @param eventHandlers the event handlers used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) * @return the new {@link Factory} */ - public static Factory factory( - EventDispatcher eventDispatcher, String serverUrl, String imageName) { - return new Factory( - eventDispatcher, new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory(EventHandlers eventHandlers, String serverUrl, String imageName) { + return new Factory(eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName)); } - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final boolean allowInsecureRegistries; @@ -152,18 +150,18 @@ public static Factory factory( /** * Instantiate with {@link #factory}. * - * @param eventDispatcher the event dispatcher used for dispatching log events + * @param eventHandlers the event handlers used for dispatching log events * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed */ private RegistryClient( - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, String userAgent) { - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.allowInsecureRegistries = allowInsecureRegistries; @@ -224,7 +222,7 @@ public DescriptorDigest pushManifest(BuildableManifestTemplate manifestTemplate, return Verify.verifyNotNull( callRegistryEndpoint( new ManifestPusher( - registryEndpointRequestProperties, manifestTemplate, imageTag, eventDispatcher))); + registryEndpointRequestProperties, manifestTemplate, imageTag, eventHandlers))); } /** @@ -296,7 +294,7 @@ public boolean pushBlob( new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); try (TimerEventDispatcher timerEventDispatcher = - new TimerEventDispatcher(eventDispatcher, "pushBlob")) { + new TimerEventDispatcher(eventHandlers, "pushBlob")) { try (TimerEventDispatcher timerEventDispatcher2 = timerEventDispatcher.subTimer("pushBlob POST " + blobDigest)) { @@ -347,7 +345,7 @@ String getUserAgent() { private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { return new RegistryEndpointCaller<>( - eventDispatcher, + eventHandlers, userAgent, getApiRouteBase(), registryEndpointProvider, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 241969bf21..150071b5f6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; @@ -81,7 +81,7 @@ static boolean isBrokenPipe(IOException original) { return false; } - private final EventDispatcher eventDispatcher; + private final EventHandlers eventHandlers; private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @@ -98,7 +98,7 @@ static boolean isBrokenPipe(IOException original) { /** * Constructs with parameters for making the request. * - * @param eventDispatcher the event dispatcher used for dispatching log events + * @param eventHandlers the event dispatcher used for dispatching log events * @param userAgent {@code User-Agent} header to send with the request * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint @@ -108,7 +108,7 @@ static boolean isBrokenPipe(IOException original) { * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -117,7 +117,7 @@ static boolean isBrokenPipe(IOException original) { boolean allowInsecureRegistries) throws MalformedURLException { this( - eventDispatcher, + eventHandlers, userAgent, apiRouteBase, registryEndpointProvider, @@ -130,7 +130,7 @@ static boolean isBrokenPipe(IOException original) { @VisibleForTesting RegistryEndpointCaller( - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, String userAgent, String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @@ -140,7 +140,7 @@ static boolean isBrokenPipe(IOException original) { Function connectionFactory, @Nullable Function insecureConnectionFactory) throws MalformedURLException { - this.eventDispatcher = eventDispatcher; + this.eventHandlers = eventHandlers; this.initialRequestUrl = registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; @@ -193,7 +193,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } try { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.info( "Cannot verify server at " + url + ". Attempting again with no TLS verification.")); return call(url, getInsecureConnectionFactory()); @@ -207,7 +207,7 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr private T fallBackToHttp(URL url) throws IOException, RegistryException { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.info( "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl)); return call(httpUrl.toURL(), connectionFactory); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 6a5d8ff9d4..96803751e3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -18,7 +18,7 @@ import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; @@ -30,7 +30,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; -import javax.annotation.Nullable; /** * Retrieves registry credentials from the Docker config. @@ -71,31 +70,29 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { /** * Retrieves credentials for a registry. Tries all possible known aliases. * - * @param eventDispatcher an event dispatcher for dispatching log events + * @param eventHandlers event handlers for dispatching log events * @return {@link Credential} found for {@code registry}, or {@link Optional#empty} if not found * @throws IOException if failed to parse the config JSON */ - public Optional retrieve(@Nullable EventDispatcher eventDispatcher) - throws IOException { + public Optional retrieve(EventHandlers eventHandlers) throws IOException { if (!Files.exists(dockerConfigFile)) { return Optional.empty(); } DockerConfig dockerConfig = new DockerConfig( JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class)); - return retrieve(dockerConfig, eventDispatcher); + return retrieve(dockerConfig, eventHandlers); } /** * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @param dockerConfig the {@link DockerConfig} to retrieve from - * @param eventDispatcher an event dispatcher for dispatching log events + * @param eventHandlers event handlers for dispatching log events * @return the retrieved credentials, or {@code Optional#empty} if none are found */ @VisibleForTesting - Optional retrieve( - DockerConfig dockerConfig, @Nullable EventDispatcher eventDispatcher) { + Optional retrieve(DockerConfig dockerConfig, EventHandlers eventHandlers) { for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { // First, tries to find defined auth. String auth = dockerConfig.getAuthFor(registryAlias); @@ -120,10 +117,10 @@ Optional retrieve( | CredentialHelperUnhandledServerUrlException | CredentialHelperNotFoundException ex) { // Warns the user that the specified credential helper cannot be used. - if (eventDispatcher != null && ex.getMessage() != null) { - eventDispatcher.dispatch(LogEvent.warn(ex.getMessage())); + if (ex.getMessage() != null) { + eventHandlers.dispatch(LogEvent.warn(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - eventDispatcher.dispatch(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); + eventHandlers.dispatch(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 19dac6ae83..50c0b75b60 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -140,7 +140,7 @@ public void testToBuildConfiguration() .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) .setExecutorService(mockExecutorService) - .setEventHandlers(new EventHandlers().add(mockJibEventConsumer)); + .setEventHandlers(EventHandlers.builder().add(mockJibEventConsumer).build()); RegistryImage baseImage = RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); @@ -188,7 +188,7 @@ public void testToBuildConfiguration() Assert.assertEquals(mockExecutorService, buildConfiguration.getExecutorService()); - buildConfiguration.getEventDispatcher().dispatch(mockJibEvent); + buildConfiguration.getEventHandlers().dispatch(mockJibEvent); Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); Assert.assertEquals("jib-core", buildConfiguration.getToolName()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java index f114da01c1..881b48fe21 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import java.util.List; import org.junit.Assert; @@ -31,12 +31,12 @@ @RunWith(MockitoJUnitRunner.class) public class ProgressEventDispatcherTest { - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Test public void testDispatch() { try (ProgressEventDispatcher progressEventDispatcher = - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 10); + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 10); ProgressEventDispatcher ignored = progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 20)) { // empty @@ -44,7 +44,7 @@ public void testDispatch() { ArgumentCaptor progressEventArgumentCaptor = ArgumentCaptor.forClass(ProgressEvent.class); - Mockito.verify(mockEventDispatcher, Mockito.times(4)) + Mockito.verify(mockEventHandlers, Mockito.times(4)) .dispatch(progressEventArgumentCaptor.capture()); List progressEvents = progressEventArgumentCaptor.getAllValues(); @@ -60,14 +60,14 @@ public void testDispatch() { @Test public void testDispatch_safeWithtooMuchProgress() { try (ProgressEventDispatcher progressEventDispatcher = - ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 10)) { + ProgressEventDispatcher.newRoot(mockEventHandlers, "allocation description", 10)) { progressEventDispatcher.dispatchProgress(6); progressEventDispatcher.dispatchProgress(8); progressEventDispatcher.dispatchProgress(1); } ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); - Mockito.verify(mockEventDispatcher, Mockito.times(4)).dispatch(eventsCaptor.capture()); + Mockito.verify(mockEventHandlers, Mockito.times(4)).dispatch(eventsCaptor.capture()); List progressEvents = eventsCaptor.getAllValues(); Assert.assertSame(progressEvents.get(0).getAllocation(), progressEvents.get(1).getAllocation()); @@ -85,7 +85,7 @@ public void testDispatch_safeWithtooMuchProgress() { @Test public void testDispatch_safeWithTooManyChildren() { try (ProgressEventDispatcher progressEventDispatcher = - ProgressEventDispatcher.newRoot(mockEventDispatcher, "allocation description", 1); + ProgressEventDispatcher.newRoot(mockEventHandlers, "allocation description", 1); ProgressEventDispatcher ignored1 = progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 5); ProgressEventDispatcher ignored2 = @@ -94,7 +94,7 @@ public void testDispatch_safeWithTooManyChildren() { } ArgumentCaptor eventsCaptor = ArgumentCaptor.forClass(ProgressEvent.class); - Mockito.verify(mockEventDispatcher, Mockito.times(5)).dispatch(eventsCaptor.capture()); + Mockito.verify(mockEventHandlers, Mockito.times(5)).dispatch(eventsCaptor.capture()); List progressEvents = eventsCaptor.getAllValues(); Assert.assertEquals(1, progressEvents.get(0).getAllocation().getAllocationUnits()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java index 1c0948edb5..4ffaaaeffe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.builder; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.TimerEvent; @@ -44,13 +42,12 @@ public class TimerEventDispatcherTest { @Test public void testLogging() { - EventDispatcher eventDispatcher = - new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.TIMING, timerEventQueue::add)); + EventHandlers eventHandlers = + EventHandlers.builder().add(JibEventType.TIMING, timerEventQueue::add).build(); Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); try (TimerEventDispatcher parentTimerEventDispatcher = - new TimerEventDispatcher(eventDispatcher, "description", mockClock, null)) { + new TimerEventDispatcher(eventHandlers, "description", mockClock, null)) { Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1)); parentTimerEventDispatcher.lap(); Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH.plusMillis(1).plusNanos(1)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 6d984851b4..59369ae842 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -26,7 +26,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ImageLayers; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -87,7 +87,7 @@ private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOExce @Mock private BuildConfiguration mockBuildConfiguration; private Cache cache; - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; private LayerConfiguration fakeDependenciesLayerConfiguration; private LayerConfiguration fakeSnapshotDependenciesLayerConfiguration; @@ -122,7 +122,7 @@ public void setUp() throws IOException, URISyntaxException { cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); + Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(mockEventHandlers); Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); } @@ -133,7 +133,7 @@ private ImageLayers buildFakeLayersToCache() throws ExecutionException { BuildAndCacheApplicationLayerStep.makeList( MoreExecutors.newDirectExecutorService(), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer()); + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 522c0ec51b..3da452d9c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.HistoryEntry; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -52,7 +52,7 @@ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; @Mock private PullBaseImageStep mockPullBaseImageStep; @@ -73,7 +73,7 @@ public void setUp() throws DigestException { DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Mockito.when(mockBuildConfiguration.getEventDispatcher()).thenReturn(mockEventDispatcher); + Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(mockEventHandlers); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); @@ -164,7 +164,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -193,7 +193,7 @@ public void test_propagateBaseImageConfiguration() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -256,7 +256,7 @@ public void testOverrideWorkingDirectory() throws InterruptedException, Executio new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -278,7 +278,7 @@ public void test_inheritedEntrypoint() throws ExecutionException, InterruptedExc new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -301,7 +301,7 @@ public void test_inheritedEntrypointAndProgramArguments() new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -324,7 +324,7 @@ public void test_notInheritedProgramArguments() throws ExecutionException, Inter new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( @@ -343,7 +343,7 @@ public void test_generateHistoryObjects() throws ExecutionException, Interrupted new BuildImageStep( MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer(), + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), mockPullBaseImageStep, mockPullAndCacheBaseImageLayersStep, ImmutableList.of( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 037f5192cf..551f5369bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -45,7 +45,7 @@ @RunWith(MockitoJUnitRunner.class) public class RetrieveRegistryCredentialsStepTest { - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private ListeningExecutorService mockListeningExecutorService; @Test @@ -64,16 +64,14 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) - .newChildProducer()) + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); Assert.assertEquals( Credential.from("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) - .newChildProducer()) + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); } @@ -86,24 +84,22 @@ public void testCall_none() throws CredentialRetrievalException, IOException { RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) - .newChildProducer()) + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); - Mockito.verify(mockEventDispatcher, Mockito.atLeastOnce()) + Mockito.verify(mockEventHandlers, Mockito.atLeastOnce()) .dispatch(Mockito.any(ProgressEvent.class)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( mockListeningExecutorService, buildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1) - .newChildProducer()) + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); } @@ -122,7 +118,7 @@ public void testCall_exception() throws IOException { RetrieveRegistryCredentialsStep.forBaseImage( mockListeningExecutorService, buildConfiguration, - ProgressEventDispatcher.newRoot(mockEventDispatcher, "ignored", 1).newChildProducer()) + ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call(); Assert.fail("Should have thrown exception"); @@ -138,7 +134,7 @@ private BuildConfiguration makeFakeBuildConfiguration( ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); return BuildConfiguration.builder() - .setEventDispatcher(mockEventDispatcher) + .setEventHandlers(mockEventHandlers) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) .setCredentialRetrievers(baseCredentialRetrievers) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java deleted file mode 100644 index 02b084e281..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/DefaultEventDispatcherTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.event; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link DefaultEventDispatcher}. */ -public class DefaultEventDispatcherTest { - - /** Test {@link JibEvent}. */ - private static class TestJibEvent1 implements JibEvent {} - - /** Test {@link JibEvent}. */ - private static class TestJibEvent2 implements JibEvent {} - - @Test - public void testDispatch() { - List emissions = new ArrayList<>(); - - EventHandlers eventHandlers = - new EventHandlers() - .add( - new JibEventType<>(TestJibEvent1.class), - testJibEvent1 -> emissions.add("handled 1 first")) - .add( - new JibEventType<>(TestJibEvent1.class), - testJibEvent1 -> emissions.add("handled 1 second")) - .add( - new JibEventType<>(TestJibEvent2.class), - testJibEvent2 -> emissions.add("handled 2")) - .add(jibEvent -> emissions.add("handled generic")); - - TestJibEvent1 testJibEvent1 = new TestJibEvent1(); - TestJibEvent2 testJibEvent2 = new TestJibEvent2(); - - EventDispatcher eventDispatcher = new DefaultEventDispatcher(eventHandlers); - eventDispatcher.dispatch(testJibEvent1); - eventDispatcher.dispatch(testJibEvent2); - - Assert.assertEquals( - Arrays.asList( - "handled generic", - "handled 1 first", - "handled 1 second", - "handled generic", - "handled 2"), - emissions); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java index c0754c7ed3..7ef350e8de 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -16,6 +16,9 @@ package com.google.cloud.tools.jib.event; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import javax.annotation.Nullable; import org.junit.Assert; import org.junit.Test; @@ -45,18 +48,22 @@ private void sayHello(String name) { } } + /** Test {@link JibEvent}. */ + private static class TestJibEvent3 implements JibEvent {} + @Test public void testAdd() { int[] counter = new int[1]; EventHandlers eventHandlers = - new EventHandlers() + EventHandlers.builder() .add( new JibEventType<>(TestJibEvent1.class), testJibEvent1 -> Assert.assertEquals("payload", testJibEvent1.getPayload())) .add( new JibEventType<>(TestJibEvent2.class), testJibEvent2 -> testJibEvent2.sayHello("Jib")) - .add(jibEvent -> counter[0]++); + .add(jibEvent -> counter[0]++) + .build(); Assert.assertTrue(eventHandlers.getHandlers().containsKey(JibEvent.class)); Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent1.class)); Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent2.class)); @@ -79,4 +86,38 @@ public void testAdd() { Mockito.verifyNoMoreInteractions(mockTestJibEvent1); testJibEvent2.assertMessageCorrect("Jib"); } + + @Test + public void testDispatch() { + List emissions = new ArrayList<>(); + + EventHandlers eventHandlers = + EventHandlers.builder() + .add( + new JibEventType<>(TestJibEvent2.class), + testJibEvent2 -> emissions.add("handled 2 first")) + .add( + new JibEventType<>(TestJibEvent2.class), + testJibEvent2 -> emissions.add("handled 2 second")) + .add( + new JibEventType<>(TestJibEvent3.class), + testJibEvent3 -> emissions.add("handled 3")) + .add(jibEvent -> emissions.add("handled generic")) + .build(); + + TestJibEvent2 testJibEvent2 = new TestJibEvent2(); + TestJibEvent3 testJibEvent3 = new TestJibEvent3(); + + eventHandlers.dispatch(testJibEvent2); + eventHandlers.dispatch(testJibEvent3); + + Assert.assertEquals( + Arrays.asList( + "handled generic", + "handled 2 first", + "handled 2 second", + "handled generic", + "handled 3"), + emissions); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java index 796689acb6..1cd8d33907 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent.Level; @@ -33,18 +31,17 @@ public class LogEventTest { // Note that in actual code, the event handler should NOT perform thread unsafe operations like // here. - private final EventDispatcher eventDispatcher = - new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.LOGGING, receivedLogEvents::offer)); + private final EventHandlers eventHandlers = + EventHandlers.builder().add(JibEventType.LOGGING, receivedLogEvents::offer).build(); @Test public void testFactories() { - eventDispatcher.dispatch(LogEvent.error("error")); - eventDispatcher.dispatch(LogEvent.lifecycle("lifecycle")); - eventDispatcher.dispatch(LogEvent.progress("progress")); - eventDispatcher.dispatch(LogEvent.warn("warn")); - eventDispatcher.dispatch(LogEvent.info("info")); - eventDispatcher.dispatch(LogEvent.debug("debug")); + eventHandlers.dispatch(LogEvent.error("error")); + eventHandlers.dispatch(LogEvent.lifecycle("lifecycle")); + eventHandlers.dispatch(LogEvent.progress("progress")); + eventHandlers.dispatch(LogEvent.warn("warn")); + eventHandlers.dispatch(LogEvent.info("info")); + eventHandlers.dispatch(LogEvent.debug("debug")); verifyNextLogEvent(Level.ERROR, "error"); verifyNextLogEvent(Level.LIFECYCLE, "lifecycle"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java index 705b56bc21..a766e29ab6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.event.events; import com.google.cloud.tools.jib.builder.BuildStepType; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.progress.Allocation; @@ -48,10 +46,8 @@ private static class AllocationTree { private AllocationTree() {} } - private static EventDispatcher makeEventDispatcher( - Consumer progressEventConsumer) { - return new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.PROGRESS, progressEventConsumer)); + private static EventHandlers makeEventHandlers(Consumer progressEventConsumer) { + return EventHandlers.builder().add(JibEventType.PROGRESS, progressEventConsumer).build(); } private static final double DOUBLE_ERROR_MARGIN = 1e-10; @@ -70,18 +66,18 @@ public void testAccumulateProgress() { progress += units * fractionOfRoot; }; - EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); + EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2 / 100 * 50, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 10, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 10, BuildStepType.ALL)); Assert.assertEquals(1.0 / 2 + 1.0 / 2 / 200 * 10, progress, DOUBLE_ERROR_MARGIN); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 190, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 190, BuildStepType.ALL)); Assert.assertEquals(1.0, progress, DOUBLE_ERROR_MARGIN); } @@ -95,21 +91,21 @@ public void testSmoke() { updateCompletionMap(allocation, units); }; - EventDispatcher eventDispatcher = makeEventDispatcher(progressEventConsumer); + EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(1, allocationCompletionMap.size()); Assert.assertEquals(50, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); Assert.assertEquals(3, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.root).longValue()); - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 200, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 200, BuildStepType.ALL)); Assert.assertEquals(4, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); @@ -128,9 +124,8 @@ public void testType() { called[0] = true; }; - EventDispatcher buildImageDispatcher = makeEventDispatcher(buildImageConsumer); - buildImageDispatcher.dispatch( - new ProgressEvent(AllocationTree.child1, 50, BuildStepType.BUILD_IMAGE)); + EventHandlers eventHandlers = makeEventHandlers(buildImageConsumer); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 50, BuildStepType.BUILD_IMAGE)); Assert.assertTrue(called[0]); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 2632906694..20fcbd6b90 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.event.progress; import com.google.cloud.tools.jib.MultithreadedExecutor; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.ProgressEvent; @@ -61,24 +59,23 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce ProgressEventHandler progressEventHandler = new ProgressEventHandler(update -> maxProgress.accumulate(update.getProgress())); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher( - new EventHandlers().add(JibEventType.PROGRESS, progressEventHandler)); + EventHandlers eventHandlers = + EventHandlers.builder().add(JibEventType.PROGRESS, progressEventHandler).build(); // Adds root, child1, and child1Child. multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.root, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.root, 0L, null)); return null; }); multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); return null; }); multithreadedExecutor.invoke( () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L, null)); return null; }); Assert.assertEquals(0.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); @@ -89,14 +86,14 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 50, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L, null)); return null; })); callables.addAll( Collections.nCopies( 100, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child2, 1L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 1L, null)); return null; })); @@ -110,7 +107,7 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 100, () -> { - eventDispatcher.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); return null; })); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 556d5d5e8f..111927947a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; @@ -64,7 +64,7 @@ private static DockerCredentialHelperFactory getTestFactory( }; } - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; @@ -87,7 +87,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("registry", "repository", null), - mockEventDispatcher, + mockEventHandlers, getTestFactory( "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); @@ -97,7 +97,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { .dockerCredentialHelper(Paths.get("docker-credential-helper")) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("Using docker-credential-helper for registry")); } @@ -106,7 +106,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.gcr.io", "repository", null), - mockEventDispatcher, + mockEventHandlers, getTestFactory( "something.gcr.io", Paths.get("docker-credential-gcr"), @@ -118,7 +118,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { .inferCredentialHelper() .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); } @@ -127,7 +127,7 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.amazonaws.com", "repository", null), - mockEventDispatcher, + mockEventHandlers, getTestFactory( "something.amazonaws.com", Paths.get("docker-credential-ecr-login"), @@ -137,17 +137,17 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); - Mockito.verify(mockEventDispatcher).dispatch(LogEvent.info("warning")); - Mockito.verify(mockEventDispatcher).dispatch(LogEvent.info(" Caused by: the root cause")); + Mockito.verify(mockEventHandlers).dispatch(LogEvent.info("warning")); + Mockito.verify(mockEventHandlers).dispatch(LogEvent.info(" Caused by: the root cause")); } @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( - ImageReference.of("registry", "repository", null), mockEventDispatcher); + ImageReference.of("registry", "repository", null), mockEventHandlers); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockEventDispatcher)) + Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockEventHandlers)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( @@ -156,7 +156,7 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException .dockerConfig(mockDockerConfigCredentialRetriever) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("Using credentials from Docker config for registry")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java index e0f379cf48..056cb6d6ea 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.frontend; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.MainClassFinder.Result.Type; import com.google.common.io.Resources; @@ -35,13 +35,13 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); @@ -52,7 +52,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -64,7 +64,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @@ -73,7 +73,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertEquals( MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); @@ -87,7 +87,7 @@ public void testFindMainClass_extension() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -98,7 +98,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -109,7 +109,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -120,7 +120,7 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -132,7 +132,7 @@ public void testMainClass_varargs() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/varargs").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventDispatcher).find(); + new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 4f2738c8eb..e78a15c48f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -19,7 +19,7 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; @@ -53,7 +53,7 @@ public class ManifestPusherTest { @Mock private Response mockResponse; - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; private Path v22manifestJsonFile; private V22ManifestTemplate fakeManifestTemplate; @@ -70,7 +70,7 @@ public void setUp() throws URISyntaxException, IOException { new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeManifestTemplate, "test-image-tag", - mockEventDispatcher); + mockEventHandlers); } @Test @@ -103,7 +103,7 @@ public void testHandleResponse_noDigest() throws IOException { .thenReturn(Collections.emptyList()); Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.warn("Expected image digest " + expectedDigest + ", but received none")); } @@ -114,7 +114,7 @@ public void testHandleResponse_multipleDigests() throws IOException { .thenReturn(Arrays.asList("too", "many")); Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.warn("Expected image digest " + expectedDigest + ", but received: too, many")); } @@ -126,7 +126,7 @@ public void testHandleResponse_invalidDigest() throws IOException { .thenReturn(Collections.singletonList("not valid")); Assert.assertEquals(expectedDigest, testManifestPusher.handleResponse(mockResponse)); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.warn("Expected image digest " + expectedDigest + ", but received: not valid")); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index d95ce2ce39..6ca25274dc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; @@ -31,7 +31,7 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { - @Mock private EventDispatcher eventDispatcher; + @Mock private EventHandlers eventHandlers; @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; @@ -39,7 +39,7 @@ public class RegistryClientTest { @Before public void setUp() { testRegistryClientFactory = - RegistryClient.factory(eventDispatcher, "some.server.url", "some image name"); + RegistryClient.factory(eventHandlers, "some.server.url", "some image name"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 642949eb3f..833a5c1b97 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; @@ -113,7 +113,7 @@ private static HttpResponse mockRedirectHttpResponse(String redirectLocation) th return mockHttpResponse(code307, new HttpHeaders().setLocation(redirectLocation)); } - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private Connection mockConnection; @Mock private Connection mockInsecureConnection; @Mock private Response mockResponse; @@ -174,7 +174,7 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); @@ -202,11 +202,11 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); @@ -230,7 +230,7 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); @@ -353,11 +353,11 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); @@ -639,7 +639,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) private RegistryEndpointCaller createRegistryEndpointCaller( boolean allowInsecure, int port) throws MalformedURLException { return new RegistryEndpointCaller<>( - mockEventDispatcher, + mockEventHandlers, "userAgent", (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 0620dbdd4c..1de405ba87 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.io.Resources; import java.io.IOException; @@ -41,7 +41,7 @@ public class DockerConfigCredentialRetrieverTest { @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; private Path dockerConfigFile; @@ -58,7 +58,7 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventDispatcher).isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventHandlers).isPresent()); } @Test @@ -66,8 +66,7 @@ public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - Optional credentials = - dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("some", credentials.get().getUsername()); Assert.assertEquals("auth", credentials.get().getPassword()); @@ -83,7 +82,7 @@ public void testRetrieve_useCredsStore() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventDispatcher) + .retrieve(mockDockerConfig, mockEventHandlers) .orElseThrow(AssertionError::new)); } @@ -97,7 +96,7 @@ public void testRetrieve_useCredsStore_withProtocol() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventDispatcher) + .retrieve(mockDockerConfig, mockEventHandlers) .orElseThrow(AssertionError::new)); } @@ -111,7 +110,7 @@ public void testRetrieve_useCredHelper() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventDispatcher) + .retrieve(mockDockerConfig, mockEventHandlers) .orElseThrow(AssertionError::new)); } @@ -127,11 +126,11 @@ public void testRetrieve_useCredHelper_warn() Paths.get("docker-credential-path"), new Throwable("cause"))); new DockerConfigCredentialRetriever("another registry", dockerConfigFile) - .retrieve(mockDockerConfig, mockEventDispatcher); + .retrieve(mockDockerConfig, mockEventHandlers); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch(LogEvent.warn("The system does not have docker-credential-path CLI")); - Mockito.verify(mockEventDispatcher).dispatch(LogEvent.warn(" Caused by: cause")); + Mockito.verify(mockEventHandlers).dispatch(LogEvent.warn(" Caused by: cause")); } @Test @@ -139,7 +138,7 @@ public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventDispatcher).isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventHandlers).isPresent()); } @Test @@ -152,7 +151,7 @@ public void testRetrieve_credentialFromAlias() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventDispatcher) + .retrieve(mockDockerConfig, mockEventHandlers) .orElseThrow(AssertionError::new)); } @@ -164,8 +163,7 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); - Optional credentials = - dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); @@ -179,8 +177,7 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); - Optional credentials = - dockerConfigCredentialRetriever.retrieve(mockEventDispatcher); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 179847c83a..0c5dda3a2e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -142,7 +141,7 @@ public void buildDocker() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index c8525982ea..0147261fb2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -122,7 +121,7 @@ public void buildImage() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index f66b412896..de75b3ce45 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -144,7 +143,7 @@ public void buildTar() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 1b03ab7bb7..e9ad77c5f4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -100,7 +100,7 @@ private static EventHandlers makeEventHandlers( } ConsoleLogger consoleLogger = consoleLoggerBuilder.build(); - return new EventHandlers() + return EventHandlers.builder() .add( JibEventType.LOGGING, logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) @@ -116,7 +116,8 @@ private static EventHandlers makeEventHandlers( update.getProgress(), update.getUnfinishedAllocations()); footer.add(""); consoleLogger.setFooter(footer); - })); + })) + .build(); } private static boolean isProgressFooterEnabled(Project project) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index a1d319c22f..1fc9dcf172 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -21,8 +21,6 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; @@ -104,8 +102,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); @@ -140,7 +136,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index a45294b7b1..3411d273fa 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -20,8 +20,6 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; @@ -102,8 +100,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( @@ -134,7 +130,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 811d18df00..7c965e1362 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -19,8 +19,6 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -78,8 +76,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); @@ -112,7 +108,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - eventDispatcher, + projectProperties.getEventHandlers(), helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 5b352b736d..302ef88703 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -119,7 +119,7 @@ private static EventHandlers makeEventHandlers( } ConsoleLogger consoleLogger = logEventHandlerBuilder.build(); - return new EventHandlers() + return EventHandlers.builder() .add( JibEventType.LOGGING, logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) @@ -132,7 +132,8 @@ private static EventHandlers makeEventHandlers( update -> consoleLogger.setFooter( ProgressDisplayGenerator.generateProgressDisplay( - update.getProgress(), update.getUnfinishedAllocations())))); + update.getProgress(), update.getUnfinishedAllocations())))) + .build(); } @VisibleForTesting diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index a66f71a4a2..0ec69cc561 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.FromAuthConfiguration; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.collect.ImmutableMap; @@ -36,7 +36,7 @@ public class MavenRawConfigurationTest { @Test public void testGetters() { JibPluginConfiguration jibPluginConfiguration = Mockito.mock(JibPluginConfiguration.class); - EventDispatcher eventDispatcher = Mockito.mock(EventDispatcher.class); + EventHandlers eventHandlers = Mockito.mock(EventHandlers.class); Server server = Mockito.mock(Server.class); Mockito.when(server.getUsername()).thenReturn("maven settings user"); @@ -103,6 +103,6 @@ public void testGetters() { Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); - Mockito.verifyNoMoreInteractions(eventDispatcher); + Mockito.verifyNoMoreInteractions(eventHandlers); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 20b0b1c275..8ebb779f26 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.common.base.Strings; @@ -42,7 +42,7 @@ public class ConfigurationPropertyValidator { * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. * - * @param eventDispatcher the {@link EventDispatcher} used to dispatch log events + * @param eventHandlers the {@link EventHandlers} used to dispatch log events * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials @@ -51,7 +51,7 @@ public class ConfigurationPropertyValidator { * {@link Optional#empty} if neither is configured. */ public static Optional getImageCredential( - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, String usernameProperty, String passwordProperty, AuthProperty auth, @@ -66,7 +66,7 @@ public static Optional getImageCredential( // Warn if a system property is missing if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( passwordProperty + " system property is set, but " @@ -74,7 +74,7 @@ public static Optional getImageCredential( + " is not; attempting other authentication methods.")); } if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( usernameProperty + " system property is set, but " @@ -87,14 +87,14 @@ public static Optional getImageCredential( return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getUsername())) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( auth.getUsernameDescriptor() + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( auth.getPasswordDescriptor() + " is missing from build configuration; ignoring auth section.")); @@ -109,7 +109,7 @@ public static Optional getImageCredential( * {@code project-name:project-version} if target image is not configured * * @param targetImage the configured target image reference - * @param eventDispatcher the {@link EventDispatcher} used to dispatch log events + * @param eventHandlers the {@link EventHandlers} used to dispatch log events * @param generatedName the image name to use if {@code targetImage} is {@code null} * @param generatedTag the tag to use if {@code targetImage} is {@code null} * @param helpfulSuggestions used for generating the message notifying the user of the generated @@ -121,13 +121,13 @@ public static Optional getImageCredential( */ public static ImageReference getGeneratedTargetDockerTag( @Nullable String targetImage, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, String generatedName, String generatedTag, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { if (Strings.isNullOrEmpty(targetImage)) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(generatedName, generatedTag))); // Try to parse generated tag to verify that project name and version are valid (throws an diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index aa860c44cb..e39bd541e1 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -27,7 +27,7 @@ import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.common.annotations.VisibleForTesting; @@ -162,7 +162,7 @@ private static void handleRegistryUnauthorizedException( * * @param jibContainerBuilder the {@link JibContainerBuilder} * @param containerizer the {@link Containerizer} - * @param eventDispatcher the {@link EventDispatcher} + * @param eventHandlers the {@link EventHandlers} * @param helpfulSuggestions suggestions to use in help messages for exceptions * @return the built {@link JibContainer} * @throws BuildStepsExecutionException if another exception is thrown during the build @@ -172,17 +172,17 @@ private static void handleRegistryUnauthorizedException( public JibContainer build( JibContainerBuilder jibContainerBuilder, Containerizer containerizer, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { try { - eventDispatcher.dispatch(LogEvent.lifecycle("")); - eventDispatcher.dispatch(LogEvent.lifecycle(startupMessage)); + eventHandlers.dispatch(LogEvent.lifecycle("")); + eventHandlers.dispatch(LogEvent.lifecycle(startupMessage)); JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); - eventDispatcher.dispatch(LogEvent.lifecycle("")); - eventDispatcher.dispatch(LogEvent.lifecycle(successMessage)); + eventHandlers.dispatch(LogEvent.lifecycle("")); + eventHandlers.dispatch(LogEvent.lifecycle(successMessage)); // when an image is built, write out the digest and id if (imageDigestOutputPath != null) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 990ea36c99..7ae71df030 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.common.annotations.VisibleForTesting; @@ -72,7 +71,8 @@ public static String resolveMainClass( Preconditions.checkNotNull(mainClass); if (!isValidJavaClass(mainClass)) { - new DefaultEventDispatcher(projectProperties.getEventHandlers()) + projectProperties + .getEventHandlers() .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); } @@ -95,7 +95,8 @@ static boolean isValidJavaClass(String className) { @Nullable private static String getMainClassFromJar(ProjectProperties projectProperties) { - new DefaultEventDispatcher(projectProperties.getEventHandlers()) + projectProperties + .getEventHandlers() .dispatch( LogEvent.info( "Searching for main class... Add a 'mainClass' configuration to '" @@ -106,7 +107,8 @@ private static String getMainClassFromJar(ProjectProperties projectProperties) { private static String findMainClassInClassFiles(ProjectProperties projectProperties) throws MainClassInferenceException, IOException { - new DefaultEventDispatcher(projectProperties.getEventHandlers()) + projectProperties + .getEventHandlers() .dispatch( LogEvent.debug( "Could not find a valid main class specified in " @@ -114,9 +116,7 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + "; attempting to infer main class.")); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder( - projectProperties.getClassFiles(), - new DefaultEventDispatcher(projectProperties.getEventHandlers())) + new MainClassFinder(projectProperties.getClassFiles(), projectProperties.getEventHandlers()) .find(); switch (mainClassFinderResult.getType()) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 493a18f7b9..77c3f965ac 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -26,8 +26,7 @@ import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.DefaultEventDispatcher; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; @@ -121,12 +120,10 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); RegistryImage targetImage = RegistryImage.named(targetImageReference); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); boolean isTargetImageCredentialPresent = configureCredentialRetrievers( rawConfiguration, - eventDispatcher, + projectProperties.getEventHandlers(), targetImage, targetImageReference, PropertyNames.TO_AUTH_USERNAME, @@ -164,10 +161,9 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageReference baseImageReference = ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); - EventDispatcher eventDispatcher = - new DefaultEventDispatcher(projectProperties.getEventHandlers()); + EventHandlers eventHandlers = projectProperties.getEventHandlers(); if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not " + "enable this on a public network!")); @@ -177,7 +173,7 @@ static PluginConfigurationProcessor processCommonConfiguration( boolean isBaseImageCredentialPresent = configureCredentialRetrievers( rawConfiguration, - eventDispatcher, + eventHandlers, baseImage, baseImageReference, PropertyNames.FROM_AUTH_USERNAME, @@ -199,7 +195,7 @@ static PluginConfigurationProcessor processCommonConfiguration( getWorkingDirectoryChecked(rawConfiguration) .ifPresent(jibContainerBuilder::setWorkingDirectory); if (rawConfiguration.getUseCurrentTimestamp()) { - eventDispatcher.dispatch( + eventHandlers.dispatch( LogEvent.warn( "Setting image creation time to current time; your image may not be reproducible.")); jibContainerBuilder.setCreationTime(Instant.now()); @@ -256,7 +252,8 @@ static List computeEntrypoint( if (rawConfiguration.getMainClass().isPresent() || !rawConfiguration.getJvmFlags().isEmpty() || !rawExtraClasspath.isEmpty()) { - new DefaultEventDispatcher(projectProperties.getEventHandlers()) + projectProperties + .getEventHandlers() .dispatch( LogEvent.warn( "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); @@ -391,7 +388,7 @@ static Optional getWorkingDirectoryChecked(RawConfiguration ra // TODO: find a way to reduce the number of arguments. private static boolean configureCredentialRetrievers( RawConfiguration rawConfiguration, - EventDispatcher eventDispatcher, + EventHandlers eventHandlers, RegistryImage registryImage, ImageReference imageReference, String usernamePropertyName, @@ -402,14 +399,10 @@ private static boolean configureCredentialRetrievers( throws FileNotFoundException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(imageReference, eventDispatcher)); + CredentialRetrieverFactory.forImage(imageReference, eventHandlers)); Optional optionalCredential = ConfigurationPropertyValidator.getImageCredential( - eventDispatcher, - usernamePropertyName, - passwordPropertyName, - knownAuth, - rawConfiguration); + eventHandlers, usernamePropertyName, passwordPropertyName, knownAuth, rawConfiguration); boolean credentialPresent = optionalCredential.isPresent(); if (optionalCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( @@ -427,7 +420,7 @@ private static boolean configureCredentialRetrievers( defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); } } catch (InferredAuthException ex) { - eventDispatcher.dispatch(LogEvent.warn("InferredAuthException: " + ex.getMessage())); + eventHandlers.dispatch(LogEvent.warn("InferredAuthException: " + ex.getMessage())); } } defaultCredentialRetrievers.setCredentialHelper(credHelper); @@ -443,7 +436,7 @@ private static ImageReference getGeneratedTargetDockerTag( throws InvalidImageReferenceException { return ConfigurationPropertyValidator.getGeneratedTargetDockerTag( rawConfiguration.getToImage().orElse(null), - new DefaultEventDispatcher(projectProperties.getEventHandlers()), + projectProperties.getEventHandlers(), projectProperties.getName(), projectProperties.getVersion().equals("unspecified") ? "latest" diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 9e2b008ed6..57b92766eb 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -35,7 +35,7 @@ @RunWith(MockitoJUnitRunner.class) public class ConfigurationPropertyValidatorTest { - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; @Mock private RawConfiguration mockConfiguration; @@ -55,7 +55,7 @@ public void testGetImageAuth() { Credential expected = Credential.from("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, + mockEventHandlers, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, @@ -69,21 +69,21 @@ public void testGetImageAuth() { expected = Credential.from("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, + mockEventHandlers, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); - Mockito.verify(mockEventDispatcher, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); + Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); // Auth completely missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, + mockEventHandlers, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, @@ -95,13 +95,13 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, + mockEventHandlers, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); @@ -110,13 +110,13 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventDispatcher, + mockEventHandlers, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.warn("user is missing from build configuration; ignoring auth section.")); } @@ -138,19 +138,18 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept // Target configured ImageReference result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - "a/b:c", mockEventDispatcher, "project-name", "project-version", helpfulSuggestions); + "a/b:c", mockEventHandlers, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockEventDispatcher, Mockito.never()) - .dispatch(LogEvent.lifecycle(Mockito.any())); + Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.lifecycle(Mockito.any())); // Target not configured result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventDispatcher, "project-name", "project-version", helpfulSuggestions); + null, mockEventHandlers, "project-name", "project-version", helpfulSuggestions); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockEventDispatcher) + Mockito.verify(mockEventHandlers) .dispatch( LogEvent.lifecycle( "Tagging image with generated image reference project-name:project-version. If you'd " @@ -160,7 +159,7 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept // Generated tag invalid try { ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventDispatcher, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); + null, mockEventHandlers, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); Assert.fail(); } catch (InvalidImageReferenceException ignored) { } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 5b426198f0..8ed671ceec 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventDispatcher; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import java.io.IOException; import java.net.UnknownHostException; @@ -61,7 +61,7 @@ public class JibBuildRunnerTest { @Mock private JibContainerBuilder mockJibContainerBuilder; @Mock private Containerizer mockContainerizer; - @Mock private EventDispatcher mockEventDispatcher; + @Mock private EventHandlers mockEventHandlers; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @@ -77,7 +77,7 @@ public void setUpMocks() { public void testBuildImage_pass() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventDispatcher, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); } @Test @@ -92,10 +92,7 @@ public void testBuildImage_httpHostConnectException() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -114,10 +111,7 @@ public void testBuildImage_unknownHostException() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -137,10 +131,7 @@ public void testBuildImage_insecureRegistryException() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -165,10 +156,7 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -194,10 +182,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -217,10 +202,7 @@ public void testBuildImage_registryCredentialsNotSentException() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -239,10 +221,7 @@ public void testBuildImage_other() try { testJibBuildRunner.build( - mockJibContainerBuilder, - mockContainerizer, - mockEventDispatcher, - TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index 95c5d44fcb..e32f19d91a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -48,7 +48,7 @@ public class MainClassResolverTest { @Before public void setup() { Mockito.when(mockProjectProperties.getEventHandlers()) - .thenReturn(new EventHandlers().add(mockJibEventConsumer)); + .thenReturn(EventHandlers.builder().add(mockJibEventConsumer).build()); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index ad1b3912d7..c355bb441c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -100,7 +100,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getEventHandlers()) - .thenReturn(new EventHandlers().add(JibEventType.LOGGING, logger)); + .thenReturn(EventHandlers.builder().add(JibEventType.LOGGING, logger).build()); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) .thenReturn(Jib.from("base")); From 6e35bb264433130ff1a35a277eeca8684375e885 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 20 May 2019 10:33:40 -0400 Subject: [PATCH 0596/2020] Make Containerizer EventHandlers non-optional (#1726) --- ...AuthenticationMethodRetrieverIntegrationTest.java | 6 ++---- .../jib/registry/BlobCheckerIntegrationTest.java | 5 ++--- .../jib/registry/BlobPullerIntegrationTest.java | 5 ++--- .../jib/registry/BlobPusherIntegrationTest.java | 3 +-- .../jib/registry/ManifestPullerIntegrationTest.java | 7 +++---- .../jib/registry/ManifestPusherIntegrationTest.java | 2 +- .../RegistryAuthenticatorIntegrationTest.java | 2 +- .../google/cloud/tools/jib/api/Containerizer.java | 6 +++--- .../cloud/tools/jib/api/JibContainerBuilder.java | 2 +- .../tools/jib/configuration/BuildConfiguration.java | 2 +- .../google/cloud/tools/jib/event/EventHandlers.java | 12 +++--------- .../jib/frontend/CredentialRetrieverFactory.java | 2 +- .../cloud/tools/jib/api/ContainerizerTest.java | 5 ++--- .../cloud/tools/jib/api/JibContainerBuilderTest.java | 2 +- 14 files changed, 24 insertions(+), 37 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 28465fed7e..39f4f655b3 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -26,19 +26,17 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); - @Test public void testGetRegistryAuthenticator() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); Assert.assertNotNull(registryAuthenticator); Authorization authorization = registryAuthenticator.authenticatePull(null); RegistryClient authorizedRegistryClient = - RegistryClient.factory(EVENT_HANDLERS, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index a054c68fab..b89082fd10 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -30,13 +30,12 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testCheck_exists() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = @@ -51,7 +50,7 @@ public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); DescriptorDigest fakeBlobDigest = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index ec22cb167a..be33010b24 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -36,7 +36,6 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -45,7 +44,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti // Pulls the busybox image. localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -77,7 +76,7 @@ public void testPull_unknownBlob() throws IOException, DigestException, Interrup "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 6126aaef0f..6700045517 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -31,7 +31,6 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testPush() @@ -44,7 +43,7 @@ public void testPush() "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "testimage") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index f99656473d..8d5539dd63 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -31,13 +31,12 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); @Test public void testPull_v21() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = @@ -51,7 +50,7 @@ public void testPull_v21() throws IOException, RegistryException, InterruptedExc public void testPull_v22() throws IOException, RegistryException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -65,7 +64,7 @@ public void testPull_unknownManifest() localRegistry.pullAndPushToLocal("busybox", "busybox"); try { RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 01bbdc1022..f3aa9138bb 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -36,7 +36,7 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.none(); + private static final EventHandlers EVENT_HANDLERS = EventHandlers.NONE; @Test public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index b4807fad1f..ccb7ed873e 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -34,7 +34,7 @@ public void testAuthenticate() ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); RegistryAuthenticator registryAuthenticator = RegistryClient.factory( - EventHandlers.none(), + EventHandlers.NONE, dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) .newRegistryClient() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index d12c33832a..96641f4de3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -143,7 +143,7 @@ public static Containerizer to(TarImage tarImage) { @Nullable private ExecutorService executorService; private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; @Nullable private Path applicationLayersCacheDirectory; - @Nullable private EventHandlers eventHandlers; + private EventHandlers eventHandlers = EventHandlers.NONE; private boolean allowInsecureRegistries = false; private boolean offline = false; private String toolName = DEFAULT_TOOL_NAME; @@ -294,8 +294,8 @@ Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException return applicationLayersCacheDirectory; } - Optional getEventHandlers() { - return Optional.ofNullable(eventHandlers); + EventHandlers getEventHandlers() { + return eventHandlers; } boolean getAllowInsecureRegistries() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 36e971fc68..fcc9816fc2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -521,7 +521,7 @@ BuildConfiguration toBuildConfiguration( .setOffline(containerizer.isOfflineMode()) .setToolName(containerizer.getToolName()) .setExecutorService(executorService) - .setEventHandlers(containerizer.getEventHandlers().orElse(EventHandlers.none())) + .setEventHandlers(containerizer.getEventHandlers()) .build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index bc472a433e..302cfc7f8e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -61,7 +61,7 @@ public static class Builder { private ImmutableList layerConfigurations = ImmutableList.of(); private Class targetFormat = DEFAULT_TARGET_FORMAT; private String toolName = DEFAULT_TOOL_NAME; - private EventHandlers eventHandlers = EventHandlers.none(); + private EventHandlers eventHandlers = EventHandlers.NONE; @Nullable private ExecutorService executorService; private Builder() {} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index f85e0ba82c..aa1f2b79dc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -65,6 +65,9 @@ public EventHandlers build() { } } + /** An empty {@link EventHandlers}. */ + public static final EventHandlers NONE = new Builder().build(); + /** Maps from {@link JibEvent} class to handlers for that event type. */ private final ImmutableMultimap, Handler> handlers; @@ -72,15 +75,6 @@ private EventHandlers(Multimap, Handler Date: Wed, 22 May 2019 13:29:10 +0200 Subject: [PATCH 0597/2020] Refactor progress completion reporting (#1720) * wip * Rename methods * Add tests --- .../jib/api/ContainerizerIntegrationTest.java | 2 +- .../progress/AllocationCompletionTracker.java | 138 ++++++++++-------- .../event/progress/ProgressEventHandler.java | 24 +-- .../AllocationCompletionTrackerTest.java | 58 +++++++- .../jib/gradle/GradleProjectProperties.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../logging/ProgressDisplayGenerator.java | 57 +------- .../logging/ProgressDisplayGeneratorTest.java | 13 +- 8 files changed, 158 insertions(+), 138 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 61d5e34d66..91ccbbd729 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -68,7 +68,7 @@ private static class ProgressChecker { new ProgressEventHandler( update -> { lastProgress = update.getProgress(); - areTasksFinished = update.getUnfinishedAllocations().isEmpty(); + areTasksFinished = update.getUnfinishedLeafTasks().isEmpty(); }); private volatile double lastProgress = 0.0; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java index 9b79138c47..947dde4152 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java @@ -16,13 +16,18 @@ package com.google.cloud.tools.jib.event.progress; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import java.util.PriorityQueue; -import java.util.Queue; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; /** * Keeps track of the progress for {@link Allocation}s as well as their order in which they appear. @@ -32,10 +37,10 @@ class AllocationCompletionTracker { /** - * Holds the progress units completed along with a creation order. This is used as the value of - * the {@link #completionMap}. + * Holds the progress units remaining along with a creation order (index starting from 0). This is + * used as the value of the {@link #completionMap}. */ - private static class InsertionOrderUnits implements Comparable { + private static class IndexedRemainingUnits implements Comparable { /** Monotonically-increasing source for {@link #index}. */ private static final AtomicInteger currentIndex = new AtomicInteger(); @@ -44,30 +49,33 @@ private static class InsertionOrderUnits implements Comparable completionMap = + private final ConcurrentHashMap completionMap = new ConcurrentHashMap<>(); /** @@ -78,30 +86,25 @@ public int compareTo(InsertionOrderUnits otherInsertionOrderUnits) { * * @param allocation the {@link Allocation} to update progress for * @param units the units of progress - * @return {@code true} if the map was updated; {@code false} if {@code allocation} was already - * present. Note that this may return {@code true} even if the map was not updated if called - * concurrently, but never {@code false} if the map was updated. + * @return {@code true} if the map was updated */ boolean updateProgress(Allocation allocation, long units) { - if (units == 0L) { - // Puts the allocation in the map if not present, with progress initialized to 0. - boolean alreadyPresent = completionMap.containsKey(allocation); - completionMap.computeIfAbsent(allocation, InsertionOrderUnits::new); - return !alreadyPresent; - } + AtomicBoolean mapUpdated = new AtomicBoolean(units != 0); completionMap.compute( allocation, - (ignored, insertionOrderUnits) -> { - if (insertionOrderUnits == null) { - insertionOrderUnits = new InsertionOrderUnits(allocation); + (ignored, indexedRemainingUnits) -> { + if (indexedRemainingUnits == null) { + indexedRemainingUnits = new IndexedRemainingUnits(allocation); + mapUpdated.set(true); } - updateInsertionOrderUnits(insertionOrderUnits, units); - - return insertionOrderUnits; + if (units != 0) { + updateIndexedRemainingUnits(indexedRemainingUnits, units); + } + return indexedRemainingUnits; }); - return true; + return mapUpdated.get(); } /** @@ -112,56 +115,67 @@ boolean updateProgress(Allocation allocation, long units) { * * @return a list of unfinished {@link Allocation}s */ - ImmutableList getUnfinishedAllocations() { - Queue unfinishedInsertionOrderUnits = new PriorityQueue<>(); - - for (InsertionOrderUnits insertionOrderUnits : completionMap.values()) { - if (insertionOrderUnits.units.get() < insertionOrderUnits.allocation.getAllocationUnits()) { - unfinishedInsertionOrderUnits.add(insertionOrderUnits); - } - } - - ImmutableList.Builder unfinishedAllocations = - ImmutableList.builderWithExpectedSize(unfinishedInsertionOrderUnits.size()); - while (!unfinishedInsertionOrderUnits.isEmpty()) { - unfinishedAllocations.add(unfinishedInsertionOrderUnits.remove().allocation); - } - return unfinishedAllocations.build(); + @VisibleForTesting + List getUnfinishedAllocations() { + return completionMap + .values() + .stream() + .filter(IndexedRemainingUnits::isUnfinished) + .sorted() + .map(remainingUnits -> remainingUnits.allocation) + .collect(Collectors.toList()); } /** - * Helper method for {@link #updateProgress(Allocation, long)}. Adds units to {@code - * insertionOrderUnits}. Updates {@link InsertionOrderUnits} for parent {@link Allocation}s if - * {@code insertionOrderUnits.units} reached completion (equals {@code - * allocation.getAllocationUnits()}. This method is not thread-safe for the {@code - * insertionOrderUnits} and should be called atomically relative to the {@code - * insertionOrderUnits}. + * Helper method for {@link #updateProgress(Allocation, long)}. Subtract {@code units} from {@code + * indexedRemainingUnits}. Updates {@link IndexedRemainingUnits} for parent {@link Allocation}s if + * remaining units becomes 0. This method is not thread-safe for the {@code + * indexedRemainingUnits} and should be called atomically relative to the {@code + * indexedRemainingUnits}. * - * @param insertionOrderUnits the {@link InsertionOrderUnits} to update progress for + * @param indexedRemainingUnits the {@link IndexedRemainingUnits} to update progress for * @param units the units of progress */ - private void updateInsertionOrderUnits(InsertionOrderUnits insertionOrderUnits, long units) { - Allocation allocation = insertionOrderUnits.allocation; + private void updateIndexedRemainingUnits( + IndexedRemainingUnits indexedRemainingUnits, long units) { + Allocation allocation = indexedRemainingUnits.allocation; - long newUnits = insertionOrderUnits.units.addAndGet(units); - if (newUnits > allocation.getAllocationUnits()) { + long newUnits = indexedRemainingUnits.remainingUnits.addAndGet(-units); + if (newUnits < 0L) { throw new IllegalStateException( "Progress exceeds max for '" + allocation.getDescription() + "': " - + newUnits - + " > " + + -newUnits + + " more beyond " + allocation.getAllocationUnits()); } // Updates the parent allocations if this allocation completed. - if (newUnits == allocation.getAllocationUnits()) { + if (newUnits == 0L) { allocation .getParent() .ifPresent( parentAllocation -> - updateInsertionOrderUnits( + updateIndexedRemainingUnits( Preconditions.checkNotNull(completionMap.get(parentAllocation)), 1L)); } } + + ImmutableList getUnfinishedLeafTasks() { + List allUnfinished = getUnfinishedAllocations(); + Set unfinishedLeaves = new LinkedHashSet<>(allUnfinished); // preserves order + + for (Allocation allocation : allUnfinished) { + Optional parent = allocation.getParent(); + + while (parent.isPresent()) { + unfinishedLeaves.remove(parent.get()); + parent = parent.get().getParent(); + } + } + + return ImmutableList.copyOf( + unfinishedLeaves.stream().map(Allocation::getDescription).collect(Collectors.toList())); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java index 2ccba88de5..915790e7ed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java @@ -29,15 +29,18 @@ */ public class ProgressEventHandler implements Consumer { - /** Contains the accumulated progress and which {@link Allocation}s are not yet complete. */ + /** + * Contains the accumulated progress and which "leaf" tasks are not yet complete. Leaf tasks are + * those that do not have sub-tasks. + */ public static class Update { private final double progress; - private final ImmutableList unfinishedAllocations; + private final ImmutableList unfinishedLeafTasks; - private Update(double progress, ImmutableList unfinishedAllocations) { + private Update(double progress, ImmutableList unfinishedLeafTasks) { this.progress = progress; - this.unfinishedAllocations = unfinishedAllocations; + this.unfinishedLeafTasks = unfinishedLeafTasks; } /** @@ -50,13 +53,13 @@ public double getProgress() { } /** - * Gets a list of the unfinished {@link Allocation}s in the order in which those {@link - * Allocation}s were encountered. + * Gets a list of the unfinished "leaf" tasks in the order in which those tasks were + * encountered. * - * @return a list of unfinished {@link Allocation}s + * @return a list of unfinished "leaf" tasks */ - public ImmutableList getUnfinishedAllocations() { - return unfinishedAllocations; + public ImmutableList getUnfinishedLeafTasks() { + return unfinishedLeafTasks; } } @@ -89,8 +92,7 @@ public void accept(ProgressEvent progressEvent) { if (completionTracker.updateProgress(allocation, progressUnits)) { // Note: Could produce false positives. - updateNotifier.accept( - new Update(progress.sum(), completionTracker.getUnfinishedAllocations())); + updateNotifier.accept(new Update(progress.sum(), completionTracker.getUnfinishedLeafTasks())); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java index 0fcc7a1c8d..0f13673469 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java @@ -94,7 +94,7 @@ public void testGetUnfinishedAllocations_singleThread() { Assert.fail(); } catch (IllegalStateException ex) { - Assert.assertEquals("Progress exceeds max for 'child1': 2 > 1", ex.getMessage()); + Assert.assertEquals("Progress exceeds max for 'child1': 1 more beyond 1", ex.getMessage()); } } @@ -163,4 +163,60 @@ public void testGetUnfinishedAllocations_multipleThreads() Collections.emptyList(), allocationCompletionTracker.getUnfinishedAllocations()); } } + + @Test + public void testGetUnfinishedLeafTasks() { + AllocationCompletionTracker tracker = new AllocationCompletionTracker(); + tracker.updateProgress(AllocationTree.root, 0); + Assert.assertEquals(Arrays.asList("root"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1, 0); + Assert.assertEquals(Arrays.asList("child1"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 0); + Assert.assertEquals(Arrays.asList("child1", "child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 0); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Collections.emptyList(), tracker.getUnfinishedLeafTasks()); + } + + @Test + public void testGetUnfinishedLeafTasks_differentUpdateOrder() { + AllocationCompletionTracker tracker = new AllocationCompletionTracker(); + tracker.updateProgress(AllocationTree.root, 0); + Assert.assertEquals(Arrays.asList("root"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 0); + Assert.assertEquals(Arrays.asList("child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1, 0); + Assert.assertEquals(Arrays.asList("child2", "child1"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 0); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Arrays.asList("child2", "child1Child"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child1Child, 50); + Assert.assertEquals(Arrays.asList("child2"), tracker.getUnfinishedLeafTasks()); + + tracker.updateProgress(AllocationTree.child2, 100); + Assert.assertEquals(Collections.emptyList(), tracker.getUnfinishedLeafTasks()); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index e9ad77c5f4..33dbb711e7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -113,7 +113,7 @@ private static EventHandlers makeEventHandlers( update -> { List footer = ProgressDisplayGenerator.generateProgressDisplay( - update.getProgress(), update.getUnfinishedAllocations()); + update.getProgress(), update.getUnfinishedLeafTasks()); footer.add(""); consoleLogger.setFooter(footer); })) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 302ef88703..385ff47e55 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -132,7 +132,7 @@ private static EventHandlers makeEventHandlers( update -> consoleLogger.setFooter( ProgressDisplayGenerator.generateProgressDisplay( - update.getProgress(), update.getUnfinishedAllocations())))) + update.getProgress(), update.getUnfinishedLeafTasks())))) .build(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java index 39b6b82cb6..1b447a5b13 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGenerator.java @@ -16,12 +16,8 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Optional; -import java.util.Set; /** * Generates a display of progress and unfinished tasks. @@ -45,16 +41,18 @@ public class ProgressDisplayGenerator { * Generates a progress display. * * @param progress the overall progress, with {@code 1.0} meaning fully complete - * @param unfinishedAllocations the unfinished {@link Allocation}s + * @param unfinishedLeafTasks the unfinished leaf tasks * @return the progress display as a list of lines */ public static List generateProgressDisplay( - double progress, List unfinishedAllocations) { + double progress, List unfinishedLeafTasks) { List lines = new ArrayList<>(); lines.add(HEADER); lines.add(generateProgressBar(progress)); - lines.addAll(generateUnfinishedTasks(unfinishedAllocations)); + for (String task : unfinishedLeafTasks) { + lines.add("> " + task); + } return lines; } @@ -81,50 +79,5 @@ private static String generateProgressBar(double progress) { .toString(); } - /** - * Generates the display of the unfinished tasks from a list of unfinished {@link Allocation}s - * - * @param unfinishedAllocations the list of unfinished {@link Allocation}s - * @return the display of the unfinished {@link Allocation}s - */ - private static List generateUnfinishedTasks(List unfinishedAllocations) { - List lines = new ArrayList<>(); - for (Allocation unfinishedAllocation : getLeafAllocations(unfinishedAllocations)) { - lines.add("> " + unfinishedAllocation.getDescription()); - } - return lines; - } - - /** - * Gets a list of just the leaf {@link Allocation}s in {@code unfinishedAllocations} in the same - * order as they appear in {@code unfinishedAllocations}. - * - * @param unfinishedAllocations the list of unfinished {@link Allocation}s - * @return the list of unfinished {@link Allocation}s - */ - // TODO: Optimization: Change AllocationCompletionTracker#getUnfinishedAllocations to only return - // leaf Allocations so that this computation is unnecessary. - private static List getLeafAllocations(List unfinishedAllocations) { - // Prunes the set of all unfinished Allocations to leave just the leaves. - Set leafAllocationSet = new HashSet<>(unfinishedAllocations); - for (Allocation allocation : unfinishedAllocations) { - Optional parent = allocation.getParent(); - - while (parent.isPresent()) { - leafAllocationSet.remove(parent.get()); - parent = parent.get().getParent(); - } - } - - // Makes a list of leaf allocations in the same order as the unfinishedAllocations. - List leafAllocations = new ArrayList<>(); - for (Allocation allocation : unfinishedAllocations) { - if (leafAllocationSet.contains(allocation)) { - leafAllocations.add(allocation); - } - } - return leafAllocations; - } - private ProgressDisplayGenerator() {} } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java index a28c9e797b..8a09dd6850 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ProgressDisplayGeneratorTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.Arrays; import java.util.Collections; import org.junit.Assert; @@ -52,18 +51,14 @@ public void testGenerateProgressDisplay_progressBar_100() { @Test public void testGenerateProgressDisplay_unfinishedTasks() { - Allocation root = Allocation.newRoot("does not display", 2); - Allocation childLeft = root.newChild("does not display", 2); - Allocation childLeftDown = childLeft.newChild("childLeftDown", 2); - Allocation childRight = root.newChild("childRight", 2); - Assert.assertEquals( Arrays.asList( "Executing tasks:", getBar("[=============== ]", 50.0), - "> childLeftDown", - "> childRight"), + "> unfinished task", + "> another task in progress", + "> stalled"), ProgressDisplayGenerator.generateProgressDisplay( - 0.5, Arrays.asList(root, childLeft, childLeftDown, childRight))); + 0.5, Arrays.asList("unfinished task", "another task in progress", "stalled"))); } } From 7537b50b94d3ced82f51cbe093fa9e08a4af4d9c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 22 May 2019 12:17:18 -0400 Subject: [PATCH 0598/2020] Adjust MainClassFinder api (#1729) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JavaContainerBuilder.java | 1 - .../{frontend => api}/MainClassFinder.java | 94 ++++++++----------- .../MainClassFinderTest.java | 22 ++--- .../jib/plugins/common/MainClassResolver.java | 6 +- 5 files changed, 54 insertions(+), 70 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend => api}/MainClassFinder.java (69%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend => api}/MainClassFinderTest.java (84%) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 88db723247..b7bbfe2b6c 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package - Removed `EventDispatcher` and `DefaultEventDispatcher`; events are now dispatched directly from `EventHandlers` +- MainClassFinder now uses a static method instead of requiring instantiation ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 24dabb4c76..f70ac84e06 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; -import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Files; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java similarity index 69% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java index 40148bb50b..f3d699cddb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java @@ -14,12 +14,11 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -27,7 +26,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -94,7 +92,7 @@ public String getFoundMainClass() { * @return the type of the result */ public Type getType() { - return Preconditions.checkNotNull(type); + return type; } /** @@ -137,29 +135,49 @@ public MethodVisitor visitMethod( } } - private final ImmutableList files; - private final EventHandlers eventHandlers; - - /** - * Finds a class with {@code psvm} (see class javadoc) in {@code files}. - * - * @param files the files to check - * @param eventHandlers used for dispatching log events. - */ - public MainClassFinder(List files, EventHandlers eventHandlers) { - this.files = ImmutableList.copyOf(files); - this.eventHandlers = eventHandlers; - } - /** - * Tries to find classes with {@code psvm} (see class javadoc) in {@link #files}. + * Tries to find classes with {@code psvm} (see class javadoc) in {@code files}. * + * @param files the files to search + * @param eventHandlers the {@link EventHandlers} used for handling log messages * @return the {@link Result} of the main class finding attempt */ - public Result find() { + public static Result find(List files, EventHandlers eventHandlers) { List mainClasses = new ArrayList<>(); for (Path file : files) { - findMainClass(file).ifPresent(mainClasses::add); + // Makes sure classFile is valid. + if (!Files.exists(file)) { + eventHandlers.dispatch( + LogEvent.debug("MainClassFinder: " + file + " does not exist; ignoring")); + continue; + } + if (!Files.isRegularFile(file)) { + eventHandlers.dispatch( + LogEvent.debug("MainClassFinder: " + file + " is not a regular file; skipping")); + continue; + } + if (!file.toString().endsWith(".class")) { + eventHandlers.dispatch( + LogEvent.debug("MainClassFinder: " + file + " is not a class file; skipping")); + continue; + } + + MainClassVisitor mainClassVisitor = new MainClassVisitor(); + try (InputStream classFileInputStream = Files.newInputStream(file)) { + ClassReader reader = new ClassReader(classFileInputStream); + reader.accept(mainClassVisitor, 0); + if (mainClassVisitor.visitedMainClass) { + mainClasses.add(reader.getClassName().replace('/', '.')); + } + + } catch (ArrayIndexOutOfBoundsException ignored) { + // Not a valid class file (thrown by ClassReader if it reads an invalid format) + eventHandlers.dispatch(LogEvent.warn("Invalid class file found: " + file)); + + } catch (IOException ignored) { + // Could not read class file. + eventHandlers.dispatch(LogEvent.warn("Could not read file: " + file)); + } } if (mainClasses.size() == 1) { @@ -173,38 +191,4 @@ public Result find() { // More than one main class found. return Result.multipleMainClasses(mainClasses); } - - /** - * Checks the {@code file} for being a {@code .class} file with {@code public static void - * main(String[] args)}. - * - * @param file the file - * @return name of the class containing a main method, or {@link Optional#empty} if {@code - * classFile} is not a class - */ - private Optional findMainClass(Path file) { - // Makes sure classFile is valid. - if (!Files.exists(file) || !Files.isRegularFile(file) || !file.toString().endsWith(".class")) { - return Optional.empty(); - } - - MainClassVisitor mainClassVisitor = new MainClassVisitor(); - try (InputStream classFileInputStream = Files.newInputStream(file)) { - ClassReader reader = new ClassReader(classFileInputStream); - reader.accept(mainClassVisitor, 0); - if (mainClassVisitor.visitedMainClass) { - return Optional.of(reader.getClassName().replace('/', '.')); - } - - } catch (ArrayIndexOutOfBoundsException ignored) { - // Not a valid class file (thrown by ClassReader if it reads an invalid format) - eventHandlers.dispatch(LogEvent.warn("Invalid class file found: " + file)); - - } catch (IOException ignored) { - // Could not read class file. - eventHandlers.dispatch(LogEvent.warn("Could not read file: " + file)); - } - - return Optional.empty(); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java similarity index 84% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java index 056cb6d6ea..082aa605be 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java @@ -14,11 +14,11 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.api.MainClassFinder.Result.Type; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.frontend.MainClassFinder.Result.Type; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -41,7 +41,7 @@ public class MainClassFinderTest { public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); @@ -52,7 +52,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -64,7 +64,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @@ -73,7 +73,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertEquals( MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); @@ -87,7 +87,7 @@ public void testFindMainClass_extension() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -98,7 +98,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -109,7 +109,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -120,7 +120,7 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -132,7 +132,7 @@ public void testMainClass_varargs() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/varargs").toURI()); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers).find(); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 7ae71df030..5eb09c348c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.MainClassFinder; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.frontend.MainClassFinder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; @@ -116,8 +116,8 @@ private static String findMainClassInClassFiles(ProjectProperties projectPropert + "; attempting to infer main class.")); MainClassFinder.Result mainClassFinderResult = - new MainClassFinder(projectProperties.getClassFiles(), projectProperties.getEventHandlers()) - .find(); + MainClassFinder.find( + projectProperties.getClassFiles(), projectProperties.getEventHandlers()); switch (mainClassFinderResult.getType()) { case MAIN_CLASS_FOUND: From 5748225c36b4c7fdcef765ce1ebcb3cf41cf53a6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 23 May 2019 10:48:23 -0400 Subject: [PATCH 0599/2020] Update jackson-databind to 2.9.9 (#1735) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 0b63a07c0c..a7808dd240 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'com.google.http-client:google-http-client:1.27.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:27.0.1-jre' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' implementation 'org.ow2.asm:asm:7.0' testImplementation 'junit:junit:4.12' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 41c6ef9a0b..64fda3e50d 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -64,7 +64,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 8c4e3ee73e..0c906b6cc6 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -71,7 +71,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.8 + 2.9.9 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 8d22d24b4c..5aa426dcd5 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -32,7 +32,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' From ea0bf52a00fbe62208981652ed5f42064592dc91 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 24 May 2019 12:27:03 -0400 Subject: [PATCH 0600/2020] Document symlink behaviour for extras (#1738) --- jib-gradle-plugin/README.md | 2 ++ jib-maven-plugin/README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index fd22145f3e..375936d8f4 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -323,6 +323,8 @@ jib { } ``` +Note that Jib does not follow symbolic links. If a symbolic link is present, it will be removed prior to placing the files and directories. + ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index ad3d5c49ea..1faec2cebe 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -396,6 +396,8 @@ Alternatively, the `` parameter can be used as an object to se ``` +Note that Jib does not follow symbolic links. If a symbolic link is present, it will be removed prior to placing the files and directories. + ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers) or [defined in your Maven settings](#using-maven-settings). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. From d069b915ce9fc866e90c0d4a61bc5d39a53f4fcd Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 24 May 2019 15:43:56 -0400 Subject: [PATCH 0601/2020] Move ImageFormat to api package (#1733) --- .../tools/jib/{image => api}/ImageFormat.java | 22 ++++--------------- .../tools/jib/api/JibContainerBuilder.java | 3 +-- .../jib/configuration/BuildConfiguration.java | 9 ++++++-- .../jib/api/JibContainerBuilderTest.java | 9 ++++---- .../configuration/BuildConfigurationTest.java | 3 ++- .../tools/jib/gradle/ContainerParameters.java | 2 +- .../jib/gradle/GradleRawConfiguration.java | 2 +- .../tools/jib/gradle/JibExtensionTest.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../jib/maven/MavenRawConfiguration.java | 2 +- .../jib/plugins/common/RawConfiguration.java | 2 +- 11 files changed, 24 insertions(+), 34 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{image => api}/ImageFormat.java (51%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java similarity index 51% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java index 129d25a3e1..8e680f32ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageFormat.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java @@ -14,28 +14,14 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; - -/** Enumeration of {@link BuildableManifestTemplate}s that indicates the format of the image. */ +/** Indicates the format of the image. */ public enum ImageFormat { /** @see Docker V2.2 */ - Docker(V22ManifestTemplate.class), + Docker, /** @see OCI */ - OCI(OCIManifestTemplate.class); - - private final Class manifestTemplateClass; - - ImageFormat(Class manifestTemplateClass) { - this.manifestTemplateClass = manifestTemplateClass; - } - - public Class getManifestTemplateClass() { - return manifestTemplateClass; - } + OCI } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index fcc9816fc2..c85ead0524 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -389,7 +388,7 @@ public JibContainerBuilder addLabel(String key, String value) { * @return this */ public JibContainerBuilder setFormat(ImageFormat imageFormat) { - buildConfigurationBuilder.setTargetFormat(imageFormat.getManifestTemplateClass()); + buildConfigurationBuilder.setTargetFormat(imageFormat); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 302cfc7f8e..c8bcd4117f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -16,10 +16,12 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; +import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.annotations.VisibleForTesting; @@ -139,8 +141,11 @@ public Builder setBaseImageLayersCacheDirectory(Path baseImageLayersCacheDirecto * @param targetFormat the target format * @return this */ - public Builder setTargetFormat(Class targetFormat) { - this.targetFormat = targetFormat; + public Builder setTargetFormat(ImageFormat targetFormat) { + this.targetFormat = + targetFormat == ImageFormat.Docker + ? V22ManifestTemplate.class + : OCIManifestTemplate.class; return this; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index d298525a06..c871c6d757 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -26,7 +26,8 @@ import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; @@ -193,8 +194,7 @@ public void testToBuildConfiguration() Assert.assertEquals("jib-core", buildConfiguration.getToolName()); - Assert.assertSame( - ImageFormat.Docker.getManifestTemplateClass(), buildConfiguration.getTargetFormat()); + Assert.assertSame(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertEquals("jib-core", buildConfiguration.getToolName()); @@ -208,8 +208,7 @@ public void testToBuildConfiguration() .withAdditionalTag("tag2") .setToolName("toolName"), MoreExecutors.newDirectExecutorService()); - Assert.assertSame( - ImageFormat.OCI.getManifestTemplateClass(), buildConfiguration.getTargetFormat()); + Assert.assertSame(OCIManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertEquals( ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals("toolName", buildConfiguration.getToolName()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 2f2491dc05..37dfcfd44f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; @@ -100,7 +101,7 @@ public void testBuilder() throws Exception { .setContainerConfiguration(containerConfiguration) .setApplicationLayersCacheDirectory(expectedApplicationLayersCacheDirectory) .setBaseImageLayersCacheDirectory(expectedBaseImageLayersCacheDirectory) - .setTargetFormat(OCIManifestTemplate.class) + .setTargetFormat(ImageFormat.OCI) .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) .setToolName(expectedCreatedBy) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 92eecea59e..f84d308649 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.base.Preconditions; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 37bed3d018..9b80a3c7b0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import java.nio.file.Path; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 40aab0338a..25313dcc25 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 3411d273fa..04ece9b957 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -18,9 +18,9 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.image.ImageFormat; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index d363824032..b096d12037 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import java.nio.file.Path; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 7a768308fb..f27f7bd93c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.image.ImageFormat; +import com.google.cloud.tools.jib.api.ImageFormat; import java.nio.file.Path; import java.util.List; import java.util.Map; From 983efb7c9306f1aef84c5c7e8d95adee0310d839 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 27 May 2019 16:07:41 -0400 Subject: [PATCH 0602/2020] Include pointers to FAQ in new-issue template (#1741) --- .github/ISSUE_TEMPLATE.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 79001c0a57..385f28d889 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,10 +1,31 @@ - + **Description of the issue**: + + **Expected behavior**: -**Steps to reproduce**: +**Steps to reproduce**: + **Environment**: From 5abf3bf202ec6baf2608f6798ac3661dfe4bd562 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 28 May 2019 10:36:31 -0400 Subject: [PATCH 0603/2020] Move LayerConfiguration and LayerEntry to api package (#1736) --- .../jib/api/ContainerizerIntegrationTest.java | 1 - .../tools/jib/api/ReproducibleImageTest.java | 1 - .../tools/jib/api/JavaContainerBuilder.java | 1 - .../tools/jib/api/JibContainerBuilder.java | 2 - .../LayerConfiguration.java | 46 +++++++++++++------ .../tools/jib/{image => api}/LayerEntry.java | 23 ++-------- .../BuildAndCacheApplicationLayerStep.java | 2 +- .../google/cloud/tools/jib/cache/Cache.java | 2 +- .../tools/jib/cache/LayerEntriesSelector.java | 2 +- .../jib/configuration/BuildConfiguration.java | 1 + .../jib/image/ReproducibleLayerBuilder.java | 8 ++-- .../jib/api/JavaContainerBuilderTest.java | 1 - .../jib/api/JibContainerBuilderTest.java | 1 - .../LayerConfigurationTest.java | 7 +-- ...BuildAndCacheApplicationLayerStepTest.java | 4 +- .../cloud/tools/jib/cache/CacheTest.java | 2 +- .../jib/cache/LayerEntriesSelectorTest.java | 2 +- .../configuration/BuildConfigurationTest.java | 1 + .../image/ReproducibleLayerBuilderTest.java | 3 +- .../gradle/GradleProjectPropertiesTest.java | 4 +- .../jib/maven/MavenProjectPropertiesTest.java | 4 +- .../common/JavaContainerBuilderHelper.java | 2 +- .../JavaContainerBuilderHelperTest.java | 4 +- .../PluginConfigurationProcessorTest.java | 2 +- 24 files changed, 61 insertions(+), 65 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration => api}/LayerConfiguration.java (85%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{image => api}/LayerEntry.java (82%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{configuration => api}/LayerConfigurationTest.java (95%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 91ccbbd729..3798d29034 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.builder.BuildStepType; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LayerCountEvent; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 82356de44c..689a222408 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index f70ac84e06..073bb97f17 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.ProjectInfo; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index c85ead0524..228d4456ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -22,10 +22,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.UnknownHostException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java similarity index 85% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java index 3bdc544426..55da591bf1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java @@ -14,11 +14,8 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; -import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Files; @@ -50,6 +47,17 @@ public Builder setName(String name) { return this; } + /** + * Adds an entry to the layer. + * + * @param entry the layer entry to add + * @return this + */ + public Builder addEntry(LayerEntry entry) { + layerEntries.add(entry); + return this; + } + /** * Adds an entry to the layer. Only adds the single source file to the exact path in the * container file system. @@ -72,7 +80,7 @@ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { return addEntry( sourceFile, pathInContainer, - LayerEntry.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, pathInContainer)); + DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, pathInContainer)); } /** @@ -95,7 +103,7 @@ public Builder addEntry( sourceFile, pathInContainer, permissions, - LayerEntry.DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, pathInContainer)); + DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, pathInContainer)); } /** @@ -118,8 +126,7 @@ public Builder addEntry( AbsoluteUnixPath pathInContainer, FilePermissions permissions, Instant lastModifiedTime) { - layerEntries.add(new LayerEntry(sourceFile, pathInContainer, permissions, lastModifiedTime)); - return this; + return addEntry(new LayerEntry(sourceFile, pathInContainer, permissions, lastModifiedTime)); } /** @@ -139,8 +146,7 @@ public Builder addEntry( */ public Builder addEntryRecursive(Path sourceFile, AbsoluteUnixPath pathInContainer) throws IOException { - return addEntryRecursive( - sourceFile, pathInContainer, LayerEntry.DEFAULT_FILE_PERMISSIONS_PROVIDER); + return addEntryRecursive(sourceFile, pathInContainer, DEFAULT_FILE_PERMISSIONS_PROVIDER); } /** @@ -161,10 +167,7 @@ public Builder addEntryRecursive( BiFunction filePermissionProvider) throws IOException { return addEntryRecursive( - sourceFile, - pathInContainer, - filePermissionProvider, - LayerEntry.DEFAULT_MODIFIED_TIME_PROVIDER); + sourceFile, pathInContainer, filePermissionProvider, DEFAULT_MODIFIED_TIME_PROVIDER); } /** @@ -215,6 +218,21 @@ public LayerConfiguration build() { } } + /** Provider that returns default file permissions (644 for files, 755 for directories). */ + public static final BiFunction + DEFAULT_FILE_PERMISSIONS_PROVIDER = + (sourcePath, destinationPath) -> + Files.isDirectory(sourcePath) + ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS + : FilePermissions.DEFAULT_FILE_PERMISSIONS; + + /** Default file modification time (EPOCH + 1 second). */ + public static final Instant DEFAULT_MODIFIED_TIME = Instant.ofEpochSecond(1); + + /** Provider that returns default file modification time (EPOCH + 1 second). */ + public static final BiFunction DEFAULT_MODIFIED_TIME_PROVIDER = + (sourcePath, destinationPath) -> DEFAULT_MODIFIED_TIME; + /** * Gets a new {@link Builder} for {@link LayerConfiguration}. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java similarity index 82% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java index c9f024c431..2de8b92ec4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java @@ -14,15 +14,11 @@ * the License. */ -package com.google.cloud.tools.jib.image; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; -import com.google.cloud.tools.jib.api.FilePermissions; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.Objects; -import java.util.function.BiFunction; /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar @@ -38,19 +34,6 @@ */ public class LayerEntry { - /** Provider that returns default file permissions (644 for files, 755 for directories). */ - public static final BiFunction - DEFAULT_FILE_PERMISSIONS_PROVIDER = - (sourcePath, destinationPath) -> - Files.isDirectory(sourcePath) - ? FilePermissions.DEFAULT_FOLDER_PERMISSIONS - : FilePermissions.DEFAULT_FILE_PERMISSIONS; - - public static final Instant DEFAULT_MODIFIED_TIME = Instant.ofEpochSecond(1); - /** Provider that returns default file modification time (EPOCH + 1 second). */ - public static final BiFunction DEFAULT_MODIFIED_TIME_PROVIDER = - (sourcePath, destinationPath) -> DEFAULT_MODIFIED_TIME; - private final Path sourceFile; private final AbsoluteUnixPath extractionPath; private final FilePermissions permissions; @@ -76,7 +59,7 @@ public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath) { this( sourceFile, extractionPath, - DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, extractionPath)); + LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, extractionPath)); } /** @@ -94,7 +77,7 @@ public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath, FilePermissi sourceFile, extractionPath, permissions, - DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, extractionPath)); + LayerConfiguration.DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, extractionPath)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 6d2557a2c8..e6896c855d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.builder.BuildStepType; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 3bd8afa294..6a5c946eed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -18,8 +18,8 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.ManifestAndConfig; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index d684df76b8..d43a93cd58 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.cache; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index c8bcd4117f..5d77b87fae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.api.ImageFormat; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index d68d0d7e71..644041701f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.tar.TarStreamBuilder; @@ -57,7 +59,7 @@ private static class UniqueTarArchiveEntries { /** * Adds a {@link TarArchiveEntry} if its extraction path does not exist yet. Also adds all of * the parent directories on the extraction path, if the parent does not exist. Parent will have - * modified time to set to {@link LayerEntry#DEFAULT_MODIFIED_TIME}. + * modified time to set to {@link LayerConfiguration#DEFAULT_MODIFIED_TIME}. * * @param tarArchiveEntry the {@link TarArchiveEntry} */ @@ -71,7 +73,7 @@ private void add(TarArchiveEntry tarArchiveEntry) { Path namePath = Paths.get(tarArchiveEntry.getName()); if (namePath.getParent() != namePath.getRoot()) { TarArchiveEntry dir = new TarArchiveEntry(DIRECTORY_FILE, namePath.getParent().toString()); - dir.setModTime(LayerEntry.DEFAULT_MODIFIED_TIME.toEpochMilli()); + dir.setModTime(LayerConfiguration.DEFAULT_MODIFIED_TIME.toEpochMilli()); add(dir); } @@ -137,6 +139,6 @@ public Blob build() { tarStreamBuilder.addTarArchiveEntry(entry); } - return Blobs.from(outputStream -> tarStreamBuilder.writeAsTarArchiveTo(outputStream)); + return Blobs.from(tarStreamBuilder::writeAsTarArchiveTo); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index e383f7851c..c37346738e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index c871c6d757..a8fd47ad5d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java similarity index 95% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java index 83b579c412..8d22dbdf6e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java @@ -14,11 +14,8 @@ * the License. */ -package com.google.cloud.tools.jib.configuration; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; -import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import java.io.IOException; @@ -30,7 +27,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link LayerConfiguration}. */ +/** Tests for {@link com.google.cloud.tools.jib.api.LayerConfiguration}. */ public class LayerConfigurationTest { @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 59369ae842..f3d56e4725 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; @@ -25,10 +27,8 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ImageLayers; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 5bf145d5fd..f37740a2dd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -18,9 +18,9 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.common.io.CountingOutputStream; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index 5e164abf59..e1a551cba3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -19,9 +19,9 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 37dfcfd44f..d87f0e58a8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index c5613bc3cb..df9752618e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -18,9 +18,10 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; import com.google.common.io.Resources; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index d9b06e751b..70cbabf6cf 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -25,10 +25,10 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 6d08ae3a4f..69ca83a2d1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -23,10 +23,10 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 8827629992..ed87744e59 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -21,9 +21,9 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.RelativeUnixPath; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import java.io.IOException; import java.nio.file.Files; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index b2d54d5161..85f694379a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -23,11 +23,11 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.LayerConfiguration; import com.google.cloud.tools.jib.filesystem.FileOperations; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index c355bb441c..8b4e7613be 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -25,12 +25,12 @@ import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; +import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.image.LayerEntry; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; From 967b524a344914803615aa64c2020fc9d49848df Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 30 May 2019 16:01:43 -0400 Subject: [PATCH 0604/2020] Remove JibEventType (#1749) --- .../jib/api/ContainerizerIntegrationTest.java | 6 +- .../cloud/tools/jib/event/EventHandlers.java | 11 ++-- .../cloud/tools/jib/event/JibEventType.java | 60 ------------------- .../jib/builder/TimerEventDispatcherTest.java | 3 +- .../tools/jib/event/EventHandlersTest.java | 18 ++---- .../tools/jib/event/events/LogEventTest.java | 3 +- .../jib/event/events/ProgressEventTest.java | 3 +- .../progress/ProgressEventHandlerTest.java | 3 +- .../jib/gradle/GradleProjectProperties.java | 9 +-- .../jib/maven/MavenProjectProperties.java | 9 +-- .../PluginConfigurationProcessorTest.java | 3 +- 11 files changed, 28 insertions(+), 100 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 3798d29034..7f08559ba4 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -19,8 +19,8 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LayerCountEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; @@ -161,8 +161,8 @@ private static void assertDockerInspect(String imageReference) private final ProgressChecker progressChecker = new ProgressChecker(); private final EventHandlers eventHandlers = EventHandlers.builder() - .add(JibEventType.PROGRESS, progressChecker.progressEventHandler) - .add(JibEventType.LAYER_COUNT, layerCountConsumer) + .add(ProgressEvent.class, progressChecker.progressEventHandler) + .add(LayerCountEvent.class, layerCountConsumer) .build(); @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index aa1f2b79dc..1d88baadd4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -43,21 +43,20 @@ public static class Builder { * @param the type of {@code eventClass} * @return this */ - public Builder add(JibEventType eventType, Consumer eventConsumer) { - Class eventClass = eventType.getEventClass(); - handlers.put(eventClass, new Handler<>(eventClass, eventConsumer)); + public Builder add(Class eventType, Consumer eventConsumer) { + handlers.put(eventType, new Handler<>(eventType, eventConsumer)); return this; } /** - * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link - * #add(JibEventType, Consumer)} for more details. + * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link #add(Class, + * Consumer)} for more details. * * @param eventConsumer the event handler * @return this */ public Builder add(Consumer eventConsumer) { - return add(JibEventType.ALL, eventConsumer); + return add(JibEvent.class, eventConsumer); } public EventHandlers build() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java deleted file mode 100644 index 70ccaed091..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEventType.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.event; - -import com.google.cloud.tools.jib.event.events.LayerCountEvent; -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.event.events.TimerEvent; -import com.google.common.annotations.VisibleForTesting; - -/** Holds references to all {@link JibEvent} types. */ -public class JibEventType { - - /** All event types. Handlers for this will always be called first. */ - public static final JibEventType ALL = new JibEventType<>(JibEvent.class); - - /** - * Event with a message to use for logging purposes. Do not attempt to parse for information as - * the format can change. - */ - public static final JibEventType LOGGING = new JibEventType<>(LogEvent.class); - - /** Timer event for timing various part of Jib's execution. */ - public static final JibEventType TIMING = new JibEventType<>(TimerEvent.class); - - /** Event indicating progress for tasks in Jib's execution. */ - public static final JibEventType PROGRESS = - new JibEventType<>(ProgressEvent.class); - - /** Event used for counting layers processed during a build step. */ - public static final JibEventType LAYER_COUNT = - new JibEventType<>(LayerCountEvent.class); - - // TODO: Add entries for all JibEvent types. - - private final Class eventClass; - - @VisibleForTesting - JibEventType(Class eventClass) { - this.eventClass = eventClass; - } - - Class getEventClass() { - return eventClass; - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java index 4ffaaaeffe..b19ec8eca3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.events.TimerEvent.State; import java.time.Clock; @@ -43,7 +42,7 @@ public class TimerEventDispatcherTest { @Test public void testLogging() { EventHandlers eventHandlers = - EventHandlers.builder().add(JibEventType.TIMING, timerEventQueue::add).build(); + EventHandlers.builder().add(TimerEvent.class, timerEventQueue::add).build(); Mockito.when(mockClock.instant()).thenReturn(Instant.EPOCH); try (TimerEventDispatcher parentTimerEventDispatcher = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java index 7ef350e8de..4a18eb8c83 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -57,11 +57,9 @@ public void testAdd() { EventHandlers eventHandlers = EventHandlers.builder() .add( - new JibEventType<>(TestJibEvent1.class), + TestJibEvent1.class, testJibEvent1 -> Assert.assertEquals("payload", testJibEvent1.getPayload())) - .add( - new JibEventType<>(TestJibEvent2.class), - testJibEvent2 -> testJibEvent2.sayHello("Jib")) + .add(TestJibEvent2.class, testJibEvent2 -> testJibEvent2.sayHello("Jib")) .add(jibEvent -> counter[0]++) .build(); Assert.assertTrue(eventHandlers.getHandlers().containsKey(JibEvent.class)); @@ -93,15 +91,9 @@ public void testDispatch() { EventHandlers eventHandlers = EventHandlers.builder() - .add( - new JibEventType<>(TestJibEvent2.class), - testJibEvent2 -> emissions.add("handled 2 first")) - .add( - new JibEventType<>(TestJibEvent2.class), - testJibEvent2 -> emissions.add("handled 2 second")) - .add( - new JibEventType<>(TestJibEvent3.class), - testJibEvent3 -> emissions.add("handled 3")) + .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 first")) + .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 second")) + .add(TestJibEvent3.class, testJibEvent3 -> emissions.add("handled 3")) .add(jibEvent -> emissions.add("handled generic")) .build(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java index 1cd8d33907..fee62d8691 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.event.events; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent.Level; import java.util.ArrayDeque; import java.util.Deque; @@ -32,7 +31,7 @@ public class LogEventTest { // Note that in actual code, the event handler should NOT perform thread unsafe operations like // here. private final EventHandlers eventHandlers = - EventHandlers.builder().add(JibEventType.LOGGING, receivedLogEvents::offer).build(); + EventHandlers.builder().add(LogEvent.class, receivedLogEvents::offer).build(); @Test public void testFactories() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java index a766e29ab6..75874040bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.HashMap; import java.util.Map; @@ -47,7 +46,7 @@ private AllocationTree() {} } private static EventHandlers makeEventHandlers(Consumer progressEventConsumer) { - return EventHandlers.builder().add(JibEventType.PROGRESS, progressEventConsumer).build(); + return EventHandlers.builder().add(ProgressEvent.class, progressEventConsumer).build(); } private static final double DOUBLE_ERROR_MARGIN = 1e-10; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 20fcbd6b90..2e08f7943f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.MultithreadedExecutor; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.ProgressEvent; import java.io.IOException; import java.util.ArrayList; @@ -60,7 +59,7 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce ProgressEventHandler progressEventHandler = new ProgressEventHandler(update -> maxProgress.accumulate(update.getProgress())); EventHandlers eventHandlers = - EventHandlers.builder().add(JibEventType.PROGRESS, progressEventHandler).build(); + EventHandlers.builder().add(ProgressEvent.class, progressEventHandler).build(); // Adds root, child1, and child1Child. multithreadedExecutor.invoke( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 33dbb711e7..f744ab50ff 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -21,8 +21,9 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; @@ -102,13 +103,13 @@ private static EventHandlers makeEventHandlers( return EventHandlers.builder() .add( - JibEventType.LOGGING, + LogEvent.class, logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) .add( - JibEventType.TIMING, + TimerEvent.class, new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) .add( - JibEventType.PROGRESS, + ProgressEvent.class, new ProgressEventHandler( update -> { List footer = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 385ff47e55..dc0e2b3417 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -21,8 +21,9 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.event.events.ProgressEvent; +import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; @@ -121,13 +122,13 @@ private static EventHandlers makeEventHandlers( return EventHandlers.builder() .add( - JibEventType.LOGGING, + LogEvent.class, logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) .add( - JibEventType.TIMING, + TimerEvent.class, new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) .add( - JibEventType.PROGRESS, + ProgressEvent.class, new ProgressEventHandler( update -> consoleLogger.setFooter( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 8b4e7613be..66c316a31a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -29,7 +29,6 @@ import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEventType; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -100,7 +99,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getEventHandlers()) - .thenReturn(EventHandlers.builder().add(JibEventType.LOGGING, logger).build()); + .thenReturn(EventHandlers.builder().add(LogEvent.class, logger).build()); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) .thenReturn(Jib.from("base")); From cd252d054e3b3fb640724da0b34144bcbf3b72e0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 30 May 2019 16:08:45 -0400 Subject: [PATCH 0605/2020] Rename ExposedPortsParser to Ports (#1750) --- .../jib/api/ContainerizerIntegrationTest.java | 4 +--- .../ExposedPortsParser.java => api/Ports.java} | 12 ++++++------ .../PortsTest.java} | 15 +++++++-------- .../common/PluginConfigurationProcessor.java | 4 ++-- 4 files changed, 16 insertions(+), 19 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{frontend/ExposedPortsParser.java => api/Ports.java} (91%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{frontend/ExposedPortsParserTest.java => api/PortsTest.java} (85%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 7f08559ba4..be37cb05e5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.common.collect.ImmutableList; @@ -374,8 +373,7 @@ private JibContainer buildImage( "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) - .setExposedPorts( - ExposedPortsParser.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) + .setExposedPorts(Ports.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) .setLabels(ImmutableMap.of("key1", "value1", "key2", "value2")) .setLayers(fakeLayerConfigurations); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java similarity index 91% rename from jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java index 17fe04bf3f..de4e967707 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/ExposedPortsParser.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java @@ -14,17 +14,16 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.api.Port; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Utility for parsing exposed ports from text representations. */ -public class ExposedPortsParser { +/** Utility for parsing Docker/OCI ports from text representations. */ +public class Ports { /** * Pattern used for parsing information out of exposed port configurations. @@ -40,7 +39,8 @@ public class ExposedPortsParser { *

For example: ["1000", "2000-2002"] will expand to a list of {@link Port}s with the port * numbers [1000, 2000, 2001, 2002] * - * @param ports the list of port numbers/ranges + * @param ports the list of port numbers/ranges, with an optional protocol separated by a '/' + * (defaults to TCP if missing). * @return the ports as a list of {@link Port} * @throws NumberFormatException if any of the ports are in an invalid format or out of range */ @@ -87,5 +87,5 @@ public static ImmutableSet parse(List ports) throws NumberFormatEx return result.build(); } - private ExposedPortsParser() {} + private Ports() {} } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java similarity index 85% rename from jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java index 76c19a34da..85aaf55eef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/ExposedPortsParserTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java @@ -14,9 +14,8 @@ * the License. */ -package com.google.cloud.tools.jib.frontend; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.api.Port; import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.Collections; @@ -24,8 +23,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link ExposedPortsParser}. */ -public class ExposedPortsParserTest { +/** Tests for {@link Ports}. */ +public class PortsTest { @Test public void testParse() { @@ -46,13 +45,13 @@ public void testParse() { Port.udp(6001), Port.udp(6002)) .build(); - ImmutableSet result = ExposedPortsParser.parse(goodInputs); + ImmutableSet result = Ports.parse(goodInputs); Assert.assertEquals(expected, result); List badInputs = Arrays.asList("abc", "/udp", "1000/abc", "a100/tcp", "20/udpabc"); for (String input : badInputs) { try { - ExposedPortsParser.parse(Collections.singletonList(input)); + Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( @@ -66,7 +65,7 @@ public void testParse() { } try { - ExposedPortsParser.parse(Collections.singletonList("4002-4000")); + Ports.parse(Collections.singletonList("4002-4000")); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( @@ -76,7 +75,7 @@ public void testParse() { badInputs = Arrays.asList("0", "70000", "0-400", "1-70000"); for (String input : badInputs) { try { - ExposedPortsParser.parse(Collections.singletonList(input)); + Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 77c3f965ac..26b14efe1b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -23,13 +23,13 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.Ports; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.frontend.ExposedPortsParser; import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.annotations.VisibleForTesting; @@ -188,7 +188,7 @@ static PluginConfigurationProcessor processCommonConfiguration( .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) - .setExposedPorts(ExposedPortsParser.parse(rawConfiguration.getPorts())) + .setExposedPorts(Ports.parse(rawConfiguration.getPorts())) .setVolumes(getVolumesSet(rawConfiguration)) .setLabels(rawConfiguration.getLabels()) .setUser(rawConfiguration.getUser().orElse(null)); From e4eef0bfa458185677f599138bc82882d790d3d0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 30 May 2019 16:29:08 -0400 Subject: [PATCH 0606/2020] Remove JavaEntrypointConstructor (#1751) --- .../jib/api/ContainerizerIntegrationTest.java | 7 +- .../tools/jib/api/JavaContainerBuilder.java | 20 +++--- .../frontend/JavaEntrypointConstructor.java | 66 ------------------- .../JavaEntrypointConstructorTest.java | 49 -------------- .../common/PluginConfigurationProcessor.java | 19 ++++-- 5 files changed, 27 insertions(+), 134 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index be37cb05e5..bcbacee28f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -367,10 +366,8 @@ private JibContainer buildImage( JibContainerBuilder containerBuilder = Jib.from(baseImage) .setEntrypoint( - JavaEntrypointConstructor.makeEntrypoint( - JavaEntrypointConstructor.defaultClasspath(AbsoluteUnixPath.get("/app")), - Collections.emptyList(), - "HelloWorld")) + Arrays.asList( + "java", "-cp", "/app/resources:/app/classes:/app/libs/*", "HelloWorld")) .setProgramArguments(Collections.singletonList("An argument.")) .setEnvironment(ImmutableMap.of("env1", "envvalue1", "env2", "envvalue2")) .setExposedPorts(Ports.parse(Arrays.asList("1000", "2000-2002/tcp", "3000/udp"))) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 073bb97f17..12d7f7fbff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Files; @@ -165,12 +164,9 @@ public static JavaContainerBuilder from(RegistryImage registryImage) { private final List addedOthers = new ArrayList<>(); private AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); - private RelativeUnixPath classesDestination = - JavaEntrypointConstructor.DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE; - private RelativeUnixPath resourcesDestination = - JavaEntrypointConstructor.DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE; - private RelativeUnixPath dependenciesDestination = - JavaEntrypointConstructor.DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE; + private RelativeUnixPath classesDestination = RelativeUnixPath.get("classes"); + private RelativeUnixPath resourcesDestination = RelativeUnixPath.get("resources"); + private RelativeUnixPath dependenciesDestination = RelativeUnixPath.get("libs"); private RelativeUnixPath othersDestination = RelativeUnixPath.get("classpath"); @Nullable private String mainClass; @@ -534,8 +530,14 @@ public JibContainerBuilder toContainerBuilder() throws IOException { "Bug in jib-core; please report the bug at " + ProjectInfo.GITHUB_NEW_ISSUE_URL); } } - jibContainerBuilder.setEntrypoint( - JavaEntrypointConstructor.makeEntrypoint(classpathElements, jvmFlags, mainClass)); + String classpathString = String.join(":", classpathElements); + List entrypoint = new ArrayList<>(4 + jvmFlags.size()); + entrypoint.add("java"); + entrypoint.addAll(jvmFlags); + entrypoint.add("-cp"); + entrypoint.add(classpathString); + entrypoint.add(mainClass); + jibContainerBuilder.setEntrypoint(entrypoint); } return jibContainerBuilder; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java deleted file mode 100644 index f60c4eee99..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructor.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; -import com.google.cloud.tools.jib.api.RelativeUnixPath; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Constructs an image entrypoint for the Java application. */ -public class JavaEntrypointConstructor { - - public static final RelativeUnixPath DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE = - RelativeUnixPath.get("resources"); - public static final RelativeUnixPath DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE = - RelativeUnixPath.get("classes"); - public static final RelativeUnixPath DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE = - RelativeUnixPath.get("libs"); - - public static List defaultClasspath(AbsoluteUnixPath appRoot) { - return Arrays.asList( - appRoot.resolve(DEFAULT_RELATIVE_RESOURCES_PATH_ON_IMAGE).toString(), - appRoot.resolve(DEFAULT_RELATIVE_CLASSES_PATH_ON_IMAGE).toString(), - appRoot.resolve(DEFAULT_RELATIVE_DEPENDENCIES_PATH_ON_IMAGE).resolve("*").toString()); - } - - /** - * Constructs the container entrypoint. - * - *

The entrypoint is {@code java [jvm flags] -cp [classpaths] [main class]}. - * - * @param classpathElements paths to add to the classpath (will be separated by {@code :} - * @param jvmFlags the JVM flags to start the container with - * @param mainClass the name of the main class to run on startup - * @return a list of the entrypoint tokens - */ - public static List makeEntrypoint( - List classpathElements, List jvmFlags, String mainClass) { - String classpathString = String.join(":", classpathElements); - - List entrypoint = new ArrayList<>(4 + jvmFlags.size()); - entrypoint.add("java"); - entrypoint.addAll(jvmFlags); - entrypoint.add("-cp"); - entrypoint.add(classpathString); - entrypoint.add(mainClass); - return entrypoint; - } - - private JavaEntrypointConstructor() {} -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java deleted file mode 100644 index 665fd126de..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/JavaEntrypointConstructorTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.frontend; - -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; -import com.google.common.collect.ImmutableList; -import java.util.Arrays; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; - -/** Tests for {@link JavaEntrypointConstructor}. */ -public class JavaEntrypointConstructorTest { - - @Test - public void testDefaultClasspath() { - List classpath = - JavaEntrypointConstructor.defaultClasspath(AbsoluteUnixPath.get("/dir")); - Assert.assertEquals( - ImmutableList.of("/dir/resources", "/dir/classes", "/dir/libs/*"), classpath); - } - - @Test - public void testMakeEntrypoint() { - List expectedJvmFlags = Arrays.asList("-flag", "anotherFlag"); - String expectedMainClass = "SomeMainClass"; - - List entrypoint = - JavaEntrypointConstructor.makeEntrypoint( - Arrays.asList("/d1", "/d2", "/d3"), expectedJvmFlags, expectedMainClass); - Assert.assertEquals( - Arrays.asList("java", "-flag", "anotherFlag", "-cp", "/d1:/d2:/d3", "SomeMainClass"), - entrypoint); - } -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 26b14efe1b..f68198b416 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -30,7 +30,6 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.frontend.JavaEntrypointConstructor; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -269,13 +268,23 @@ static List computeEntrypoint( return null; } - List classpath = new ArrayList<>(rawExtraClasspath); - classpath.addAll(JavaEntrypointConstructor.defaultClasspath(appRoot)); String mainClass = MainClassResolver.resolveMainClass( rawConfiguration.getMainClass().orElse(null), projectProperties); - return JavaEntrypointConstructor.makeEntrypoint( - classpath, rawConfiguration.getJvmFlags(), mainClass); + + List classpath = new ArrayList<>(rawExtraClasspath); + classpath.add(appRoot.resolve("resources").toString()); + classpath.add(appRoot.resolve("classes").toString()); + classpath.add(appRoot.resolve("libs/*").toString()); + String classpathString = String.join(":", classpath); + + List entrypoint = new ArrayList<>(4 + rawConfiguration.getJvmFlags().size()); + entrypoint.add("java"); + entrypoint.addAll(rawConfiguration.getJvmFlags()); + entrypoint.add("-cp"); + entrypoint.add(classpathString); + entrypoint.add(mainClass); + return entrypoint; } /** From 3beed174b9874b93c6f56e5c6c4f0d1468d367a1 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 31 May 2019 11:51:44 -0400 Subject: [PATCH 0607/2020] No need to pass appRoot when instantiate ProjectProperties (#1752) --- .../tools/jib/gradle/BuildDockerTask.java | 6 +--- .../tools/jib/gradle/BuildImageTask.java | 6 +--- .../cloud/tools/jib/gradle/BuildTarTask.java | 6 +--- .../jib/gradle/GradleProjectProperties.java | 14 ++++----- .../cloud/tools/jib/gradle/TaskCommon.java | 4 --- .../gradle/GradleProjectPropertiesTest.java | 10 +++--- .../tools/jib/maven/BuildDockerMojo.java | 6 +--- .../cloud/tools/jib/maven/BuildImageMojo.java | 6 +--- .../cloud/tools/jib/maven/BuildTarMojo.java | 6 +--- .../jib/maven/MavenProjectProperties.java | 23 ++++++++------ .../cloud/tools/jib/maven/MojoCommon.java | 12 ------- .../jib/maven/MavenProjectPropertiesTest.java | 31 +++++++++---------- .../common/PluginConfigurationProcessor.java | 15 ++++----- .../jib/plugins/common/ProjectProperties.java | 5 ++- .../PluginConfigurationProcessorTest.java | 18 ++++++----- 15 files changed, 68 insertions(+), 100 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 0c5dda3a2e..3cbb58c0c3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; @@ -105,11 +104,8 @@ public void buildDocker() try { RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); + GradleProjectProperties.getForProject(getProject(), getLogger()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 0147261fb2..35439981f9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; @@ -82,11 +81,8 @@ public void buildImage() try { RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); + GradleProjectProperties.getForProject(getProject(), getLogger()); if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index de75b3ce45..a2461bb04f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -109,11 +108,8 @@ public void buildTar() try { RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - gradleRawConfiguration, TaskCommon.isWarProject(getProject())); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger(), appRoot); + GradleProjectProperties.getForProject(getProject(), getLogger()); GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index f744ab50ff..3af542c52c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -71,9 +71,8 @@ class GradleProjectProperties implements ProjectProperties { private static final String MAIN_SOURCE_SET_NAME = "main"; /** @return a GradleProjectProperties from the given project and logger. */ - static GradleProjectProperties getForProject( - Project project, Logger logger, AbsoluteUnixPath appRoot) { - return new GradleProjectProperties(project, logger, appRoot); + static GradleProjectProperties getForProject(Project project, Logger logger) { + return new GradleProjectProperties(project, logger); } static Path getExplodedWarDirectory(Project project) { @@ -143,19 +142,18 @@ private static boolean isProgressFooterEnabled(Project project) { private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; private final Logger logger; - private final AbsoluteUnixPath appRoot; @VisibleForTesting - GradleProjectProperties(Project project, Logger logger, AbsoluteUnixPath appRoot) { + GradleProjectProperties(Project project, Logger logger) { this.project = project; this.logger = logger; - this.appRoot = appRoot; eventHandlers = makeEventHandlers(project, logger, singleThreadedExecutor); } @Override - public JibContainerBuilder createContainerBuilder(RegistryImage baseImage) { + public JibContainerBuilder createContainerBuilder( + RegistryImage baseImage, AbsoluteUnixPath appRoot) { try { if (isWarProject()) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); @@ -266,7 +264,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - return TaskCommon.isWarProject(project); + return TaskCommon.getWarTask(project) != null; } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index edfd9983d4..b98ce0befe 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -36,10 +36,6 @@ /** Collection of common methods to share between Gradle tasks. */ class TaskCommon { - static boolean isWarProject(Project project) { - return getWarTask(project) != null; - } - @Nullable static War getWarTask(Project project) { WarPluginConvention warPluginConvention = diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 70cbabf6cf..d6b50fbf3b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -221,8 +221,7 @@ public void setup() throws URISyntaxException, IOException { getResource("gradle/webapp").resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); - gradleProjectProperties = - new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get("/app")); + gradleProjectProperties = new GradleProjectProperties(mockProject, mockLogger); } @Test @@ -361,7 +360,8 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer Path nonexistentFile = Paths.get("/nonexistent/file"); Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - gradleProjectProperties.createContainerBuilder(RegistryImage.named("base")); + gradleProjectProperties.createContainerBuilder( + RegistryImage.named("base"), AbsoluteUnixPath.get("/anything")); Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } @@ -551,8 +551,8 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() private BuildConfiguration setupBuildConfiguration(String appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JibContainerBuilder jibContainerBuilder = - new GradleProjectProperties(mockProject, mockLogger, AbsoluteUnixPath.get(appRoot)) - .createContainerBuilder(RegistryImage.named("base")); + new GradleProjectProperties(mockProject, mockLogger) + .createContainerBuilder(RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot)); return JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, Containerizer.to(RegistryImage.named("to")) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 1fc9dcf172..1796a3cdc1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; @@ -97,11 +96,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 04ece9b957..2d22e1beac 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; @@ -95,11 +94,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 7c965e1362..a763f802b7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -71,11 +70,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - AbsoluteUnixPath appRoot = - PluginConfigurationProcessor.getAppRootChecked( - mavenRawConfiguration, MojoCommon.isWarProject(getProject())); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog(), appRoot); + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index dc0e2b3417..c9f23af10f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -70,12 +70,10 @@ public class MavenProjectProperties implements ProjectProperties { * @param project the {@link MavenProject} for the plugin. * @param session the {@link MavenSession} for the plugin. * @param log the Maven {@link Log} to log messages during Jib execution - * @param appRoot root directory in the image where the app will be placed * @return a MavenProjectProperties from the given project and logger. */ - static MavenProjectProperties getForProject( - MavenProject project, MavenSession session, Log log, AbsoluteUnixPath appRoot) { - return new MavenProjectProperties(project, session, log, appRoot); + static MavenProjectProperties getForProject(MavenProject project, MavenSession session, Log log) { + return new MavenProjectProperties(project, session, log); } /** @@ -189,19 +187,17 @@ static int getVersionFromString(String versionString) { private final MavenSession session; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final EventHandlers eventHandlers; - private final AbsoluteUnixPath appRoot; @VisibleForTesting - MavenProjectProperties( - MavenProject project, MavenSession session, Log log, AbsoluteUnixPath appRoot) { + MavenProjectProperties(MavenProject project, MavenSession session, Log log) { this.project = project; - this.appRoot = appRoot; this.session = session; eventHandlers = makeEventHandlers(session, log, singleThreadedExecutor); } @Override - public JibContainerBuilder createContainerBuilder(RegistryImage baseImage) throws IOException { + public JibContainerBuilder createContainerBuilder( + RegistryImage baseImage, AbsoluteUnixPath appRoot) throws IOException { try { if (isWarProject()) { Path explodedWarPath = @@ -296,9 +292,16 @@ public String getJarPluginName() { return JAR_PLUGIN_NAME; } + /** + * Gets whether or not the given project is a war project. This is the case for projects with + * packaging {@code war} and {@code gwt-app}. + * + * @return {@code true} if the project is a war project, {@code false} if not + */ @Override public boolean isWarProject() { - return MojoCommon.isWarProject(project); + String packaging = project.getPackaging(); + return "war".equals(packaging) || "gwt-app".equals(packaging); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index d7738d2749..36cce87772 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -31,18 +31,6 @@ /** Collection of common methods to share between Maven goals. */ class MojoCommon { - /** - * Gets whether or not the given project is a war project. This is the case for projects with - * packaging {@code war} and {@code gwt-app}. - * - * @param project the Maven project - * @return {@code true} if the project is a war project, {@code false} if not - */ - static boolean isWarProject(MavenProject project) { - String packaging = project.getPackaging(); - return "war".equals(packaging) || "gwt-app".equals(packaging); - } - /** * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code * (project dir)/src/main/jib} by default if not configured. diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 69ca83a2d1..62b78bcad3 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -176,8 +176,7 @@ private static Path getResource(String path) throws URISyntaxException { public void setup() throws IOException, URISyntaxException { Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = - new MavenProjectProperties( - mockMavenProject, mockMavenSession, mockLog, AbsoluteUnixPath.get("/app")); + new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); @@ -363,7 +362,7 @@ public void isProgressFooterEnabled() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/app")); + BuildConfiguration configuration = setupBuildConfiguration("/app"); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); Path dependenciesPath = getResource("maven/application/dependencies"); @@ -403,7 +402,7 @@ public void testCreateContainerBuilder_correctFiles() @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + BuildConfiguration configuration = setupBuildConfiguration("/my/app"); assertNonDefaultAppRoot(configuration); } @@ -416,7 +415,7 @@ public void testCreateContainerBuilder_warNonDefaultAppRoot() Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + BuildConfiguration configuration = setupBuildConfiguration("/my/app"); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertSourcePathsUnordered( ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")), @@ -479,7 +478,7 @@ public void testCreateContainerBuilder_jarNonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { // Test when the default packaging is set Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - BuildConfiguration configuration = setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); + BuildConfiguration configuration = setupBuildConfiguration("/my/app"); assertNonDefaultAppRoot(configuration); } @@ -492,7 +491,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() .thenReturn(temporaryFolder.getRoot().toPath().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + setupBuildConfiguration("/my/app"); // should pass } @Test @@ -504,7 +503,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() .thenReturn(temporaryFolder.getRoot().toPath().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + setupBuildConfiguration("/my/app"); // should pass } @Test @@ -516,38 +515,38 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() .thenReturn(temporaryFolder.getRoot().toPath().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration(AbsoluteUnixPath.get("/my/app")); // should pass + setupBuildConfiguration("/my/app"); // should pass } @Test public void testIsWarProject_WarPackagingIsWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + Assert.assertTrue(mavenProjectProperties.isWarProject()); } @Test public void testIsWarProject_GwtAppPackagingIsWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-app"); - Assert.assertTrue(MojoCommon.isWarProject(mockMavenProject)); + Assert.assertTrue(mavenProjectProperties.isWarProject()); } @Test public void testIsWarProject_JarPackagingIsNotWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + Assert.assertFalse(mavenProjectProperties.isWarProject()); } @Test public void testIsWarProject_GwtLibPackagingIsNotWar() { Mockito.when(mockMavenProject.getPackaging()).thenReturn("gwt-lib"); - Assert.assertFalse(MojoCommon.isWarProject(mockMavenProject)); + Assert.assertFalse(mavenProjectProperties.isWarProject()); } - private BuildConfiguration setupBuildConfiguration(AbsoluteUnixPath appRoot) + private BuildConfiguration setupBuildConfiguration(String appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JibContainerBuilder JibContainerBuilder = - new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog, appRoot) - .createContainerBuilder(RegistryImage.named("base")); + new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) + .createContainerBuilder(RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot)); return JibContainerBuilderTestHelper.toBuildConfiguration( JibContainerBuilder, Containerizer.to(RegistryImage.named("to")) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index f68198b416..54212bbbb9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -183,7 +183,8 @@ static PluginConfigurationProcessor processCommonConfiguration( JibContainerBuilder jibContainerBuilder = projectProperties - .createContainerBuilder(baseImage) + .createContainerBuilder( + baseImage, getAppRootChecked(rawConfiguration, projectProperties)) .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) @@ -242,8 +243,7 @@ static PluginConfigurationProcessor processCommonConfiguration( static List computeEntrypoint( RawConfiguration rawConfiguration, ProjectProperties projectProperties) throws MainClassInferenceException, InvalidAppRootException, IOException { - AbsoluteUnixPath appRoot = - getAppRootChecked(rawConfiguration, projectProperties.isWarProject()); + AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); List rawExtraClasspath = rawConfiguration.getExtraClasspath(); @@ -358,17 +358,18 @@ static Set getVolumesSet(RawConfiguration rawConfiguration) * JavaContainerBuilder#DEFAULT_APP_ROOT} for other projects. * * @param rawConfiguration raw configuration data - * @param isWarProject whether or not the project is a WAR project + * @param projectProperties the project properties * @return the app root value * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path */ @VisibleForTesting - public static AbsoluteUnixPath getAppRootChecked( - RawConfiguration rawConfiguration, boolean isWarProject) throws InvalidAppRootException { + static AbsoluteUnixPath getAppRootChecked( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws InvalidAppRootException { String appRoot = rawConfiguration.getAppRoot(); if (appRoot.isEmpty()) { appRoot = - isWarProject + projectProperties.isWarProject() ? JavaContainerBuilder.DEFAULT_WEB_APP_ROOT : JavaContainerBuilder.DEFAULT_APP_ROOT; } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 004af88d7d..e03b376a51 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.EventHandlers; @@ -50,10 +51,12 @@ public interface ProjectProperties { * Starts the containerization process. * * @param baseImage the base image + * @param appRoot root directory in the image where the app will be placed * @return a {@link JibContainerBuilder} with classes, resources, and dependencies added to it * @throws IOException if there is a problem walking the project files */ - JibContainerBuilder createContainerBuilder(RegistryImage baseImage) throws IOException; + JibContainerBuilder createContainerBuilder(RegistryImage baseImage, AbsoluteUnixPath appRoot) + throws IOException; List getClassFiles() throws IOException; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 66c316a31a..6a393fd3eb 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -101,7 +101,9 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getEventHandlers()) .thenReturn(EventHandlers.builder().add(LogEvent.class, logger).build()); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); - Mockito.when(projectProperties.createContainerBuilder(Mockito.any())) + Mockito.when( + projectProperties.createContainerBuilder( + Mockito.any(RegistryImage.class), Mockito.any(AbsoluteUnixPath.class))) .thenReturn(Jib.from("base")); Mockito.when(projectProperties.isOffline()).thenReturn(false); @@ -435,7 +437,7 @@ public void testGetAppRootChecked() throws InvalidAppRootException { Assert.assertEquals( AbsoluteUnixPath.get("/some/root"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } @Test @@ -443,7 +445,7 @@ public void testGetAppRootChecked_errorOnNonAbsolutePath() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("relative/path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("relative/path", ex.getMessage()); @@ -455,7 +457,7 @@ public void testGetAppRootChecked_errorOnWindowsPath() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("\\windows\\path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("\\windows\\path", ex.getMessage()); @@ -467,7 +469,7 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("C:\\windows\\path"); try { - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties); Assert.fail(); } catch (InvalidAppRootException ex) { Assert.assertEquals("C:\\windows\\path", ex.getMessage()); @@ -477,19 +479,21 @@ public void testGetAppRootChecked_errorOnWindowsPathWithDriveLetter() { @Test public void testGetAppRootChecked_defaultNonWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(projectProperties.isWarProject()).thenReturn(false); Assert.assertEquals( AbsoluteUnixPath.get("/app"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, false)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } @Test public void testGetAppRootChecked_defaultWarProject() throws InvalidAppRootException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn(""); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( AbsoluteUnixPath.get("/jetty/webapps/ROOT"), - PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, true)); + PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } @Test From 9231d43ec580416bd01a59b16170e28463563213 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 31 May 2019 12:27:04 -0400 Subject: [PATCH 0608/2020] Prevent LayerEntry backreach into LayerConfiguration (#1753) --- .../cloud/tools/jib/api/LayerEntry.java | 45 +++---------------- .../tools/jib/api/LayerConfigurationTest.java | 25 +++++++---- .../cloud/tools/jib/cache/CacheTest.java | 13 +++++- .../jib/cache/LayerEntriesSelectorTest.java | 32 +++++++++---- .../image/ReproducibleLayerBuilderTest.java | 26 +++++++---- 5 files changed, 75 insertions(+), 66 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java index 2de8b92ec4..b17ebda94f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java @@ -23,14 +23,6 @@ /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar * archive entries. - * - *

Note that: - * - *

    - *
  • Entry source files can be either files or directories. - *
  • Adding a directory does not include the contents of the directory. Each file under a - * directory must be added as a separate {@link LayerEntry}. - *
*/ public class LayerEntry { @@ -51,38 +43,13 @@ public class LayerEntry { * AbsoluteUnixPath.get("/app/classes/com"))} adds a directory to the container file system at * {@code /app/classes/com}. This does not add the contents of {@code com/}. * - * @param sourceFile the source file to add to the layer - * @param extractionPath the path in the container file system corresponding to the {@code - * sourceFile} - */ - public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath) { - this( - sourceFile, - extractionPath, - LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, extractionPath)); - } - - /** - * Instantiates with a source file and the path to place the source file in the container file - * system. + *

Note that: * - * @param sourceFile the source file to add to the layer - * @param extractionPath the path in the container file system corresponding to the {@code - * sourceFile} - * @param permissions the file permissions on the container. Use {@code null} to use defaults (644 - * for files, 755 for directories) - */ - public LayerEntry(Path sourceFile, AbsoluteUnixPath extractionPath, FilePermissions permissions) { - this( - sourceFile, - extractionPath, - permissions, - LayerConfiguration.DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, extractionPath)); - } - - /** - * Instantiates with a source file and the path to place the source file in the container file - * system. + *

    + *
  • Entry source files can be either files or directories. + *
  • Adding a directory does not include the contents of the directory. Each file under a + * directory must be added as a separate {@link LayerEntry}. + *
* * @param sourceFile the source file to add to the layer * @param extractionPath the path in the container file system corresponding to the {@code diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java index 8d22dbdf6e..a9745a8812 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java @@ -30,6 +30,14 @@ /** Tests for {@link com.google.cloud.tools.jib.api.LayerConfiguration}. */ public class LayerConfigurationTest { + private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new LayerEntry( + source, + destination, + LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(source, destination), + LayerConfiguration.DEFAULT_MODIFIED_TIME); + } + @Test public void testAddEntryRecursive_defaults() throws IOException, URISyntaxException { Path testDirectory = Paths.get(Resources.getResource("core/layer").toURI()).toAbsolutePath(); @@ -43,16 +51,17 @@ public void testAddEntryRecursive_defaults() throws IOException, URISyntaxExcept ImmutableSet expectedLayerEntries = ImmutableSet.of( - new LayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), - new LayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), - new LayerEntry(testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), - new LayerEntry( + defaultLayerEntry(testDirectory, AbsoluteUnixPath.get("/app/layer/")), + defaultLayerEntry(testDirectory.resolve("a"), AbsoluteUnixPath.get("/app/layer/a/")), + defaultLayerEntry( + testDirectory.resolve("a/b"), AbsoluteUnixPath.get("/app/layer/a/b/")), + defaultLayerEntry( testDirectory.resolve("a/b/bar"), AbsoluteUnixPath.get("/app/layer/a/b/bar/")), - new LayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), - new LayerEntry( + defaultLayerEntry(testDirectory.resolve("c/"), AbsoluteUnixPath.get("/app/layer/c")), + defaultLayerEntry( testDirectory.resolve("c/cat/"), AbsoluteUnixPath.get("/app/layer/c/cat")), - new LayerEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), - new LayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); + defaultLayerEntry(testDirectory.resolve("foo"), AbsoluteUnixPath.get("/app/layer/foo")), + defaultLayerEntry(testFile, AbsoluteUnixPath.get("/app/fileA"))); Assert.assertEquals( expectedLayerEntries, ImmutableSet.copyOf(layerConfiguration.getLayerEntries())); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index f37740a2dd..ade0700b65 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; @@ -93,6 +94,14 @@ private static long sizeOf(Blob blob) throws IOException { return countingOutputStream.getCount(); } + private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new LayerEntry( + source, + destination, + LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(source, destination), + LayerConfiguration.DEFAULT_MODIFIED_TIME); + } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private Blob layerBlob1; @@ -121,9 +130,9 @@ public void setUp() throws IOException { layerSize1 = sizeOf(compress(layerBlob1)); layerEntries1 = ImmutableList.of( - new LayerEntry( + defaultLayerEntry( directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")), - new LayerEntry( + defaultLayerEntry( directory.resolve("another/source/file"), AbsoluteUnixPath.get("/another/extraction/path"))); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java index e1a551cba3..d20e10f40a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.cache.LayerEntriesSelector.LayerEntryTemplate; import com.google.cloud.tools.jib.hash.Digests; @@ -38,6 +39,14 @@ /** Tests for {@link LayerEntriesSelector}. */ public class LayerEntriesSelectorTest { + private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new LayerEntry( + source, + destination, + LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(source, destination), + LayerConfiguration.DEFAULT_MODIFIED_TIME); + } + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private ImmutableList outOfOrderLayerEntries; private ImmutableList inOrderLayerEntries; @@ -58,20 +67,23 @@ public void setUp() throws IOException { Path file2 = Files.createFile(folder.resolve("files").resolve("two")); Path file3 = Files.createFile(folder.resolve("gile")); - LayerEntry testLayerEntry1 = new LayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); - LayerEntry testLayerEntry2 = new LayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); - LayerEntry testLayerEntry3 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry1 = defaultLayerEntry(file1, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry2 = defaultLayerEntry(file2, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry testLayerEntry3 = defaultLayerEntry(file3, AbsoluteUnixPath.get("/extraction/path")); LayerEntry testLayerEntry4 = new LayerEntry( file3, AbsoluteUnixPath.get("/extraction/path"), - FilePermissions.fromOctalString("755")); - LayerEntry testLayerEntry5 = new LayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); + FilePermissions.fromOctalString("755"), + LayerConfiguration.DEFAULT_MODIFIED_TIME); + LayerEntry testLayerEntry5 = + defaultLayerEntry(file3, AbsoluteUnixPath.get("/extraction/patha")); LayerEntry testLayerEntry6 = new LayerEntry( file3, AbsoluteUnixPath.get("/extraction/patha"), - FilePermissions.fromOctalString("755")); + FilePermissions.fromOctalString("755"), + LayerConfiguration.DEFAULT_MODIFIED_TIME); outOfOrderLayerEntries = ImmutableList.of( @@ -125,7 +137,7 @@ public void testGenerateSelector_fileModified() throws IOException { Path layerFile = temporaryFolder.newFolder("testFolder").toPath().resolve("file"); Files.write(layerFile, "hello".getBytes(StandardCharsets.UTF_8)); Files.setLastModifiedTime(layerFile, FileTime.from(Instant.EPOCH)); - LayerEntry layerEntry = new LayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); + LayerEntry layerEntry = defaultLayerEntry(layerFile, AbsoluteUnixPath.get("/extraction/path")); DescriptorDigest expectedSelector = LayerEntriesSelector.generateSelector(ImmutableList.of(layerEntry)); @@ -148,12 +160,14 @@ public void testGenerateSelector_permissionsModified() throws IOException { new LayerEntry( layerFile, AbsoluteUnixPath.get("/extraction/path"), - FilePermissions.fromOctalString("111")); + FilePermissions.fromOctalString("111"), + LayerConfiguration.DEFAULT_MODIFIED_TIME); LayerEntry layerEntry222 = new LayerEntry( layerFile, AbsoluteUnixPath.get("/extraction/path"), - FilePermissions.fromOctalString("222")); + FilePermissions.fromOctalString("222"), + LayerConfiguration.DEFAULT_MODIFIED_TIME); // Verify that changing permissions generates a different selector Assert.assertNotEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index df9752618e..202f348170 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -92,6 +92,14 @@ private static void verifyNextTarArchiveEntryIsDirectory( Assert.assertEquals(TarArchiveEntry.DEFAULT_DIR_MODE, extractionPathEntry.getMode()); } + private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { + return new LayerEntry( + source, + destination, + LayerConfiguration.DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(source, destination), + LayerConfiguration.DEFAULT_MODIFIED_TIME); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test @@ -168,14 +176,14 @@ public void testToBlob_reproducibility() throws IOException { Blob layer = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), - new LayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) + defaultLayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), + defaultLayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) .build(); Blob reproduced = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), - new LayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) + defaultLayerEntry(fileB2, AbsoluteUnixPath.get("/somewhere/fileB")), + defaultLayerEntry(fileA2, AbsoluteUnixPath.get("/somewhere/fileA")))) .build(); byte[] layerContent = Blobs.writeToByteArray(layer); @@ -270,7 +278,7 @@ public void testBuild_timestampDefault() throws IOException { Blob blob = new ReproducibleLayerBuilder( - ImmutableList.of(new LayerEntry(file, AbsoluteUnixPath.get("/fileA")))) + ImmutableList.of(defaultLayerEntry(file, AbsoluteUnixPath.get("/fileA")))) .build(); Path tarFile = temporaryFolder.newFile().toPath(); @@ -321,15 +329,17 @@ public void testBuild_permissions() throws IOException { Blob blob = new ReproducibleLayerBuilder( ImmutableList.of( - new LayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA")), + defaultLayerEntry(fileA, AbsoluteUnixPath.get("/somewhere/fileA")), new LayerEntry( fileB, AbsoluteUnixPath.get("/somewhere/fileB"), - FilePermissions.fromOctalString("123")), + FilePermissions.fromOctalString("123"), + LayerConfiguration.DEFAULT_MODIFIED_TIME), new LayerEntry( folder, AbsoluteUnixPath.get("/somewhere/folder"), - FilePermissions.fromOctalString("456")))) + FilePermissions.fromOctalString("456"), + LayerConfiguration.DEFAULT_MODIFIED_TIME))) .build(); Path tarFile = temporaryFolder.newFile().toPath(); From d39211059015fd564b68a603bc9906ec8ef05127 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 31 May 2019 13:51:36 -0400 Subject: [PATCH 0609/2020] Remove informative BuildStepType from events (#1744) * Remove BuildStepType * Remove LayerCountEvent --- jib-core/README.md | 3 +- .../jib/api/ContainerizerIntegrationTest.java | 51 ++++----------- .../tools/jib/builder/BuildStepType.java | 63 ------------------- .../jib/builder/ProgressEventDispatcher.java | 22 +++---- .../builder/steps/AuthenticatePushStep.java | 4 +- .../BuildAndCacheApplicationLayerStep.java | 15 +---- .../jib/builder/steps/BuildImageStep.java | 4 +- .../jib/builder/steps/LoadDockerStep.java | 4 +- .../steps/PullAndCacheBaseImageLayerStep.java | 9 +-- .../PullAndCacheBaseImageLayersStep.java | 11 +--- .../jib/builder/steps/PullBaseImageStep.java | 7 +-- .../tools/jib/builder/steps/PushBlobStep.java | 10 +-- .../steps/PushContainerConfigurationStep.java | 7 +-- .../jib/builder/steps/PushImageStep.java | 7 +-- .../jib/builder/steps/PushLayersStep.java | 16 +---- .../RetrieveRegistryCredentialsStep.java | 15 ++--- .../tools/jib/builder/steps/StepsRunner.java | 7 +-- ...rottledProgressEventDispatcherWrapper.java | 10 +-- .../jib/builder/steps/WriteTarFileStep.java | 4 +- .../jib/event/events/LayerCountEvent.java | 56 ----------------- .../tools/jib/event/events/ProgressEvent.java | 16 +---- .../tools/jib/api/LayerConfigurationTest.java | 2 +- .../builder/ProgressEventDispatcherTest.java | 6 +- .../jib/event/events/ProgressEventTest.java | 18 +++--- .../progress/ProgressEventHandlerTest.java | 12 ++-- 25 files changed, 68 insertions(+), 311 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java diff --git a/jib-core/README.md b/jib-core/README.md index b859373840..5f1b72fbd0 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -124,8 +124,7 @@ When Jib dispatches events, the event handlers you defined for that event type w - [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. - [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. -- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Since Jib consists of a hierarchy of build steps, each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. Each progress event also carries the type of its corresponding build step, which can be retrieved using `getBuildStepType()`. -- [`LayerCountEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LayerCountEvent.html) - Indicates the number of layers in pull/build/push steps in the build process. Use `getBuildStepType()` to retrieve the step that the event is part of, and use `getCount()` to get the number of layers being processed. +- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. ## Frequently Asked Questions (FAQ) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index bcbacee28f..ddf9bd1138 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -17,12 +17,11 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; @@ -34,12 +33,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Consumer; import java.util.stream.Stream; import org.hamcrest.CoreMatchers; import org.junit.AfterClass; @@ -146,21 +142,20 @@ private static void assertDockerInspect(String imageReference) Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); } - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private static void assertLayerSizer(int expected, String imageReference) + throws IOException, InterruptedException { + Command command = + new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); + String layers = command.run().trim(); + Assert.assertEquals(expected, Splitter.on(",").splitToList(layers).size()); + } - private final Map layerCounts = new ConcurrentHashMap<>(); - private final Consumer layerCountConsumer = - layerCountEvent -> - layerCounts.merge( - layerCountEvent.getBuildStepType(), - layerCountEvent.getCount(), - (oldValue, count) -> oldValue + count); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private final ProgressChecker progressChecker = new ProgressChecker(); private final EventHandlers eventHandlers = EventHandlers.builder() .add(ProgressEvent.class, progressChecker.progressEventHandler) - .add(LayerCountEvent.class, layerCountConsumer) .build(); @Test @@ -175,17 +170,6 @@ public void testSteps_forBuildToDockerRegistry() Collections.emptyList()); progressChecker.checkCompletion(); - Assert.assertEquals( - layerCounts, - ImmutableMap.of( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, - 4, - BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - 3, - BuildStepType.PUSH_BASE_LAYERS, - 4, - BuildStepType.PUSH_APPLICATION_LAYERS, - 3)); logger.info("Initial build time: " + ((System.nanoTime() - lastTime) / 1_000_000)); @@ -203,6 +187,7 @@ public void testSteps_forBuildToDockerRegistry() String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); assertDockerInspect(imageReference); + assertLayerSizer(7, imageReference); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); @@ -269,15 +254,9 @@ public void testBuildToDockerDaemon() Collections.emptyList()); progressChecker.checkCompletion(); - Assert.assertEquals( - ImmutableMap.of( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, - 4, - BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - 3), - layerCounts); assertDockerInspect("testdocker"); + assertLayerSizer(7, "testdocker"); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testdocker").run()); } @@ -317,15 +296,9 @@ public void testBuildTarball() Collections.emptyList()); progressChecker.checkCompletion(); - Assert.assertEquals( - ImmutableMap.of( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, - 4, - BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - 3), - layerCounts); new Command("docker", "load", "--input", outputPath.toString()).run(); + assertLayerSizer(7, "testtar"); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java deleted file mode 100644 index bd38e04913..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildStepType.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder; - -/** Types corresponding to steps in the containerization process. */ -public enum BuildStepType { - - /** Root type that corresponds to build startup. */ - ALL, - - /** Authentication step for pushing to target registry. */ - AUTHENTICATE_PUSH, - - /** Step for building/caching an application layer. */ - BUILD_AND_CACHE_APPLICATION_LAYER, - - /** Step for building image layers/configuration. */ - BUILD_IMAGE, - - /** Step for loading the image into the Docker daemon. */ - LOAD_DOCKER, - - /** Step for pulling/caching a base image layer. */ - PULL_AND_CACHE_BASE_IMAGE_LAYER, - - /** Step for pulling the base image manifest. */ - PULL_BASE_IMAGE, - - /** Step for pushing the container configuration to the target registry. */ - PUSH_CONTAINER_CONFIGURATION, - - /** Step for pushing the image manifest to the target registry. */ - PUSH_IMAGE, - - /** Step for pushing the application image layers to the target registry. */ - PUSH_APPLICATION_LAYERS, - - /** Step for pushing the base image layers to the target registry. */ - PUSH_BASE_LAYERS, - - /** Step for retrieving credentials for the base image registry. */ - RETRIEVE_REGISTRY_CREDENTIALS_BASE, - - /** Step for retrieving credentials for the target image registry. */ - RETRIEVE_REGISTRY_CREDENTIALS_TARGET, - - /** Step for writing the image tarball to disk. */ - WRITE_TAR_FILE -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index fa693b465a..e7af9a2a20 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -42,13 +42,11 @@ public interface Factory { /** * Creates the {@link ProgressEventDispatcher} with an associated {@link Allocation}. * - * @param buildStepType the build step that the progress events correspond to * @param description user-facing description of what the allocation represents * @param allocationUnits number of allocation units * @return the new {@link ProgressEventDispatcher} */ - ProgressEventDispatcher create( - BuildStepType buildStepType, String description, long allocationUnits); + ProgressEventDispatcher create(String description, long allocationUnits); } /** @@ -62,7 +60,7 @@ ProgressEventDispatcher create( public static ProgressEventDispatcher newRoot( EventHandlers eventHandlers, String description, long allocationUnits) { return newProgressEventDispatcher( - eventHandlers, Allocation.newRoot(description, allocationUnits), BuildStepType.ALL); + eventHandlers, Allocation.newRoot(description, allocationUnits)); } /** @@ -74,25 +72,22 @@ public static ProgressEventDispatcher newRoot( * @return a new {@link ProgressEventDispatcher} */ private static ProgressEventDispatcher newProgressEventDispatcher( - EventHandlers eventHandlers, Allocation allocation, BuildStepType buildStepType) { + EventHandlers eventHandlers, Allocation allocation) { ProgressEventDispatcher progressEventDispatcher = - new ProgressEventDispatcher(eventHandlers, allocation, buildStepType); + new ProgressEventDispatcher(eventHandlers, allocation); progressEventDispatcher.dispatchProgress(0); return progressEventDispatcher; } private final EventHandlers eventHandlers; private final Allocation allocation; - private final BuildStepType buildStepType; private long remainingAllocationUnits; private boolean closed = false; - private ProgressEventDispatcher( - EventHandlers eventHandlers, Allocation allocation, BuildStepType buildStepType) { + private ProgressEventDispatcher(EventHandlers eventHandlers, Allocation allocation) { this.eventHandlers = eventHandlers; this.allocation = allocation; - this.buildStepType = buildStepType; remainingAllocationUnits = allocation.getAllocationUnits(); } @@ -112,12 +107,11 @@ public Factory newChildProducer() { private boolean used = false; @Override - public ProgressEventDispatcher create( - BuildStepType buildStepType, String description, long allocationUnits) { + public ProgressEventDispatcher create(String description, long allocationUnits) { Preconditions.checkState(!used); used = true; return newProgressEventDispatcher( - eventHandlers, allocation.newChild(description, allocationUnits), buildStepType); + eventHandlers, allocation.newChild(description, allocationUnits)); } }; } @@ -139,7 +133,7 @@ public void close() { */ public void dispatchProgress(long progressUnits) { long unitsDecremented = decrementRemainingAllocationUnits(progressUnits); - eventHandlers.dispatch(new ProgressEvent(allocation, unitsDecremented, buildStepType)); + eventHandlers.dispatch(new ProgressEvent(allocation, unitsDecremented)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 29a06755bd..76d2bf5909 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -79,8 +78,7 @@ public Authorization call() throws ExecutionException, IOException, RegistryExce String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - BuildStepType.AUTHENTICATE_PUSH, "authenticating push to " + registry, 1); + progressEventDispatcherFactory.create("authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, registry))) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index e6896c855d..e3fdea7b04 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -19,14 +19,12 @@ import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; @@ -53,9 +51,7 @@ static ImmutableList makeList( try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - "setting up to build application layers", - layerCount); + "setting up to build application layers", layerCount); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = @@ -76,10 +72,6 @@ static ImmutableList makeList( } ImmutableList steps = buildAndCacheApplicationLayerSteps.build(); - buildConfiguration - .getEventHandlers() - .dispatch( - new LayerCountEvent(BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, steps.size())); return steps; } } @@ -118,10 +110,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - BuildStepType.BUILD_AND_CACHE_APPLICATION_LAYER, - "building " + layerType + " layer", - 1); + progressEventDispatcherFactory.create("building " + layerType + " layer", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 8bb5dff335..3a5b88c2ec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -90,8 +89,7 @@ public AsyncStep call() throws ExecutionException { private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - BuildStepType.BUILD_IMAGE, "building image format", 1); + progressEventDispatcherFactory.create("building image format", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // Constructs the image. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index be45345226..86bbc6227e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -94,8 +93,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() .dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - BuildStepType.LOAD_DOCKER, "loading to Docker daemon", 1)) { + progressEventDispatcherFactory.create("loading to Docker daemon", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 4d7988e0c8..9f5913cecd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; @@ -69,10 +68,7 @@ public ListenableFuture getFuture() { @Override public CachedLayer call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, - "checking base image layer " + layerDigest, - 1); + progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, layerDigest))) { @@ -98,8 +94,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { try (ThrottledProgressEventDispatcherWrapper progressEventDispatcherWrapper = new ThrottledProgressEventDispatcherWrapper( progressEventDispatcher.newChildProducer(), - "pulling base image layer " + layerDigest, - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER)) { + "pulling base image layer " + layerDigest)) { return cache.writeCompressedLayer( registryClient.pullBlob( layerDigest, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 431bada590..18a499e10a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -19,12 +19,10 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; @@ -77,18 +75,11 @@ public ImmutableList call() try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, - "checking base image layers", - baseImageLayers.size()); + "checking base image layers", baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = ImmutableList.builderWithExpectedSize(baseImageLayers.size()); - buildConfiguration - .getEventHandlers() - .dispatch( - new LayerCountEvent( - BuildStepType.PULL_AND_CACHE_BASE_IMAGE_LAYER, baseImageLayers.size())); for (Layer layer : baseImageLayers) { pullAndCacheBaseImageLayerStepsBuilder.add( new PullAndCacheBaseImageLayerStep( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index c0808e6afe..1dc151a436 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; @@ -133,8 +132,7 @@ public BaseImageWithAuthorization call() } try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - BuildStepType.PULL_BASE_IMAGE, "pulling base image manifest", 2); + progressEventDispatcherFactory.create("pulling base image manifest", 2); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // First, try with no credentials. @@ -253,8 +251,7 @@ private Image pullBaseImage( try (ThrottledProgressEventDispatcherWrapper progressEventDispatcherWrapper = new ThrottledProgressEventDispatcherWrapper( progressEventDispatcher.newChildProducer(), - "pull container configuration " + containerConfigurationDigest, - BuildStepType.PULL_BASE_IMAGE)) { + "pull container configuration " + containerConfigurationDigest)) { String containerConfigurationString = Blobs.writeToString( registryClient.pullBlob( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 515cb5ed0f..cd41886673 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -46,7 +45,6 @@ class PushBlobStep implements AsyncStep, Callable listenableFuture; @@ -56,14 +54,12 @@ class PushBlobStep implements AsyncStep, Callable getFuture() { public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDipatcherFactory.create( - buildStepType, - "pushing blob " + blobDescriptor.getDigest(), - blobDescriptor.getSize()); + "pushing blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), DESCRIPTION + blobDescriptor); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index cde36688e2..8ffbe1345a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -86,8 +85,7 @@ public AsyncStep call() throws ExecutionException { private PushBlobStep afterBuildConfigurationFutureFuture() throws ExecutionException, IOException { try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - BuildStepType.PUSH_CONTAINER_CONFIGURATION, "pushing container configuration", 1); + progressEventDispatcherFactory.create("pushing container configuration", 1); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); @@ -101,8 +99,7 @@ private PushBlobStep afterBuildConfigurationFutureFuture() progressEventDispatcher.newChildProducer(), authenticatePushStep, blobDescriptor, - Blobs.from(containerConfiguration), - BuildStepType.PUSH_CONTAINER_CONFIGURATION); + Blobs.from(containerConfiguration)); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 430ec83992..bdca799169 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -121,8 +120,7 @@ private BuildResult afterAllPushed() throws ExecutionException, IOException, InterruptedException { ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - BuildStepType.PUSH_IMAGE, "pushing image manifest", targetImageTags.size()); + progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { @@ -153,8 +151,7 @@ private BuildResult afterAllPushed() listeningExecutorService.submit( () -> { try (ProgressEventDispatcher ignored2 = - progressEventDispatcherFactory.create( - BuildStepType.PUSH_IMAGE, "tagging with " + tag, 1)) { + progressEventDispatcherFactory.create("tagging with " + tag, 1)) { buildConfiguration .getEventHandlers() .dispatch(LogEvent.info("Tagging with " + tag + "...")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java index 6caac34272..299b6c0bd9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java @@ -20,12 +20,10 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LayerCountEvent; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -46,7 +44,6 @@ class PushLayersStep private final AuthenticatePushStep authenticatePushStep; private final AsyncStep>> cachedLayerStep; - private final BuildStepType buildStepType; private final ListenableFuture>> listenableFuture; @@ -56,14 +53,12 @@ class PushLayersStep ProgressEventDispatcher.Factory progressEventDispatcherFactory, AuthenticatePushStep authenticatePushStep, AsyncStep>> - cachedLayerStep, - BuildStepType buildStepType) { + cachedLayerStep) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authenticatePushStep = authenticatePushStep; this.cachedLayerStep = cachedLayerStep; - this.buildStepType = buildStepType; listenableFuture = AsyncDependencies.using(listeningExecutorService) @@ -84,14 +79,10 @@ public ImmutableList> call() throws ExecutionException { NonBlockingSteps.get(cachedLayerStep); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - buildStepType, "setting up to push layers", cachedLayers.size())) { + progressEventDispatcherFactory.create("setting up to push layers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. ImmutableList.Builder> pushBlobStepsBuilder = ImmutableList.builder(); - buildConfiguration - .getEventHandlers() - .dispatch(new LayerCountEvent(buildStepType, cachedLayers.size())); for (AsyncStep cachedLayerStep : cachedLayers) { ProgressEventDispatcher.Factory childProgressEventDispatcherFactory = progressEventDispatcher.newChildProducer(); @@ -120,7 +111,6 @@ private PushBlobStep makePushBlobStep( progressEventDispatcherFactory, authenticatePushStep, new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), - cachedLayer.getBlob(), - buildStepType); + cachedLayer.getBlob()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 47d3cfaeb8..052abe5c20 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -50,8 +49,7 @@ static RetrieveRegistryCredentialsStep forBaseImage( buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers(), - BuildStepType.RETRIEVE_REGISTRY_CREDENTIALS_BASE); + buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); } /** Retrieves credentials for the target image. */ @@ -64,8 +62,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers(), - BuildStepType.RETRIEVE_REGISTRY_CREDENTIALS_TARGET); + buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); } private final BuildConfiguration buildConfiguration; @@ -73,7 +70,6 @@ static RetrieveRegistryCredentialsStep forTargetImage( private final String registry; private final ImmutableList credentialRetrievers; - private final BuildStepType buildStepType; private final ListenableFuture listenableFuture; @@ -83,13 +79,11 @@ static RetrieveRegistryCredentialsStep forTargetImage( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String registry, - ImmutableList credentialRetrievers, - BuildStepType buildStepType) { + ImmutableList credentialRetrievers) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registry = registry; this.credentialRetrievers = credentialRetrievers; - this.buildStepType = buildStepType; listenableFuture = listeningExecutorService.submit(this); } @@ -107,8 +101,7 @@ public Credential call() throws CredentialRetrievalException { buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - buildStepType, "retrieving credentials for " + registry, 1); + progressEventDispatcherFactory.create("retrieving credentials for " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 887ef0c93f..fff5d935eb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.AsyncSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -146,8 +145,7 @@ public StepsRunner pushBaseImageLayers() { buildConfiguration, Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), - BuildStepType.PUSH_BASE_LAYERS)); + Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); } public StepsRunner buildAndCacheApplicationLayers() { @@ -195,8 +193,7 @@ public StepsRunner pushApplicationLayers() { Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), Preconditions.checkNotNull(steps.authenticatePushStep), AsyncSteps.immediate( - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)), - BuildStepType.PUSH_APPLICATION_LAYERS)); + Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); } public StepsRunner pushImage() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java index 4d518f7788..59d24fb063 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.common.base.Preconditions; @@ -36,17 +35,13 @@ class ThrottledProgressEventDispatcherWrapper implements Closeable { private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String description; - private final BuildStepType type; @Nullable private ProgressEventDispatcher progressEventDispatcher; @Nullable private ThrottledAccumulatingConsumer throttledDispatcher; ThrottledProgressEventDispatcherWrapper( - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - String description, - BuildStepType type) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory, String description) { this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.description = description; - this.type = type; } public void dispatchProgress(Long progressUnits) { @@ -64,8 +59,7 @@ public void close() { void setProgressTarget(long allocationUnits) { Preconditions.checkState(progressEventDispatcher == null); - progressEventDispatcher = - progressEventDispatcherFactory.create(type, description, allocationUnits); + progressEventDispatcher = progressEventDispatcherFactory.create(description, allocationUnits); throttledDispatcher = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 51c7e64455..da6e8e5028 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageTarball; @@ -94,8 +93,7 @@ private BuildResult writeTarFile() throws ExecutionException, IOException { .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create( - BuildStepType.WRITE_TAR_FILE, "writing to tar file", 1)) { + progressEventDispatcherFactory.create("writing to tar file", 1)) { Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); // Builds the image to a tarball. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java deleted file mode 100644 index b3ee4e11f5..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LayerCountEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.event.events; - -import com.google.cloud.tools.jib.builder.BuildStepType; -import com.google.cloud.tools.jib.event.JibEvent; - -/** Event used for counting layers processed during a build step. */ -public class LayerCountEvent implements JibEvent { - - private final BuildStepType buildStepType; - private final int count; - - /** - * Creates a new {@link LayerCountEvent}. For internal use only. - * - * @param buildStepType the build step associated with the event - * @param count the number of layers counted - */ - public LayerCountEvent(BuildStepType buildStepType, int count) { - this.buildStepType = buildStepType; - this.count = count; - } - - /** - * Gets the type of build step that fired the event. - * - * @return the type of build step that fired the event - */ - public BuildStepType getBuildStepType() { - return buildStepType; - } - - /** - * Gets the number of layers. - * - * @return the number of layers - */ - public int getCount() { - return count; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java index e8da3fa584..53665ea250 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.event.progress.Allocation; @@ -37,13 +36,9 @@ public class ProgressEvent implements JibEvent { /** Units of progress. */ private final long progressUnits; - /** The step in the build process that the progress event corresponds to. */ - private final BuildStepType buildStepType; - - public ProgressEvent(Allocation allocation, long progressUnits, BuildStepType buildStepType) { + public ProgressEvent(Allocation allocation, long progressUnits) { this.allocation = allocation; this.progressUnits = progressUnits; - this.buildStepType = buildStepType; } /** @@ -64,13 +59,4 @@ public Allocation getAllocation() { public long getUnits() { return progressUnits; } - - /** - * Gets the build step that the progress event corresponds to. - * - * @return the build step - */ - public BuildStepType getBuildStepType() { - return buildStepType; - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java index a9745a8812..d926e5f8be 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/LayerConfigurationTest.java @@ -27,7 +27,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link com.google.cloud.tools.jib.api.LayerConfiguration}. */ +/** Tests for {@link LayerConfiguration}. */ public class LayerConfigurationTest { private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java index 881b48fe21..18faf049d4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java @@ -38,7 +38,7 @@ public void testDispatch() { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 10); ProgressEventDispatcher ignored = - progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 20)) { + progressEventDispatcher.newChildProducer().create("ignored", 20)) { // empty } @@ -87,9 +87,9 @@ public void testDispatch_safeWithTooManyChildren() { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot(mockEventHandlers, "allocation description", 1); ProgressEventDispatcher ignored1 = - progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 5); + progressEventDispatcher.newChildProducer().create("ignored", 5); ProgressEventDispatcher ignored2 = - progressEventDispatcher.newChildProducer().create(BuildStepType.ALL, "ignored", 4)) { + progressEventDispatcher.newChildProducer().create("ignored", 4)) { // empty } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java index 75874040bc..b80b55e5dd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.builder.BuildStepType; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.progress.Allocation; import java.util.HashMap; @@ -67,16 +66,16 @@ public void testAccumulateProgress() { EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); Assert.assertEquals(1.0 / 2 / 100 * 50, progress, DOUBLE_ERROR_MARGIN); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); Assert.assertEquals(1.0 / 2, progress, DOUBLE_ERROR_MARGIN); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 10, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 10)); Assert.assertEquals(1.0 / 2 + 1.0 / 2 / 200 * 10, progress, DOUBLE_ERROR_MARGIN); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 190, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 190)); Assert.assertEquals(1.0, progress, DOUBLE_ERROR_MARGIN); } @@ -92,19 +91,19 @@ public void testSmoke() { EventHandlers eventHandlers = makeEventHandlers(progressEventConsumer); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); Assert.assertEquals(1, allocationCompletionMap.size()); Assert.assertEquals(50, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 50)); Assert.assertEquals(3, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.child1).longValue()); Assert.assertEquals(1, allocationCompletionMap.get(AllocationTree.root).longValue()); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 200, BuildStepType.ALL)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 200)); Assert.assertEquals(4, allocationCompletionMap.size()); Assert.assertEquals(100, allocationCompletionMap.get(AllocationTree.child1Child).longValue()); @@ -119,12 +118,11 @@ public void testType() { boolean[] called = new boolean[] {false}; Consumer buildImageConsumer = progressEvent -> { - Assert.assertEquals(BuildStepType.BUILD_IMAGE, progressEvent.getBuildStepType()); called[0] = true; }; EventHandlers eventHandlers = makeEventHandlers(buildImageConsumer); - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 50, BuildStepType.BUILD_IMAGE)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 50)); Assert.assertTrue(called[0]); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java index 2e08f7943f..484620dff5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java @@ -64,17 +64,17 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce // Adds root, child1, and child1Child. multithreadedExecutor.invoke( () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.root, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.root, 0L)); return null; }); multithreadedExecutor.invoke( () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); return null; }); multithreadedExecutor.invoke( () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 0L)); return null; }); Assert.assertEquals(0.0, maxProgress.get(), DOUBLE_ERROR_MARGIN); @@ -85,14 +85,14 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 50, () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1Child, 1L)); return null; })); callables.addAll( Collections.nCopies( 100, () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 1L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child2, 1L)); return null; })); @@ -106,7 +106,7 @@ public void testAccept() throws ExecutionException, InterruptedException, IOExce Collections.nCopies( 100, () -> { - eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L, null)); + eventHandlers.dispatch(new ProgressEvent(AllocationTree.child1, 0L)); return null; })); Assert.assertEquals( From 5dba0af06ea8fe0a536d9e943507cf741d5aac44 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 3 Jun 2019 11:02:19 -0400 Subject: [PATCH 0610/2020] Resolve input files more comprehensively on jibBuildTar (#1754) * Resolve input files more comprehensively on jibBuildTar (fixes UP-TO-DATE check) --- .../cloud/tools/jib/gradle/BuildTarTask.java | 10 ++---- .../jib/gradle/GradleProjectProperties.java | 36 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index a2461bb04f..6adacc033f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -73,18 +73,14 @@ public void setTargetImage(String targetImage) { } /** - * @return the input files to this task are all the output files for all the dependencies of the - * {@code classes} task. + * @return a collection of all the files that jib includes in the image. Only used to calculate + * UP-TO-DATE. */ @InputFiles public FileCollection getInputFiles() { List extraDirectories = Preconditions.checkNotNull(jibExtension).getExtraDirectories().getPaths(); - return extraDirectories - .stream() - .map(Path::toFile) - .map(directory -> GradleProjectProperties.getInputFiles(directory, getProject())) - .reduce(getProject().files(), getProject()::files); + return GradleProjectProperties.getInputFiles(getProject(), extraDirectories); } /** diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 3af542c52c..1b7b013480 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -41,7 +41,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.tools.ant.taskdefs.condition.Os; @@ -268,27 +267,28 @@ public boolean isWarProject() { } /** - * Returns the input files for a task. + * Returns the input files for a task. These files include the runtimeClasspath of the application + * and any extraDirectories defined by the user to include in the container. * - * @param extraDirectory the image's configured extra directory * @param project the gradle project - * @return the input files to the task are all the output files for all the dependencies of the - * {@code classes} task + * @param extraDirectories the image's configured extra directories + * @return the input files */ - static FileCollection getInputFiles(File extraDirectory, Project project) { - Task classesTask = project.getTasks().getByPath("classes"); - Set classesDependencies = - classesTask.getTaskDependencies().getDependencies(classesTask); - + static FileCollection getInputFiles(Project project, List extraDirectories) { + JavaPluginConvention javaPluginConvention = + project.getConvention().getPlugin(JavaPluginConvention.class); + SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); List dependencyFileCollections = new ArrayList<>(); - for (Task task : classesDependencies) { - dependencyFileCollections.add(task.getOutputs().getFiles()); - } - if (Files.exists(extraDirectory.toPath())) { - return project.files(dependencyFileCollections, extraDirectory); - } else { - return project.files(dependencyFileCollections); - } + dependencyFileCollections.add(mainSourceSet.getRuntimeClasspath()); + + extraDirectories + .stream() + .filter(Files::exists) + .map(Path::toFile) + .map(project::files) + .forEach(dependencyFileCollections::add); + + return project.files(dependencyFileCollections); } @Override From 432da0ec9b1bc7049b39be6b8a30d1e449bb7a27 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 5 Jun 2019 13:44:15 -0400 Subject: [PATCH 0611/2020] Move Credential and CredentialRetriever to api package (#1762) --- .../com/google/cloud/tools/jib/api/JibIntegrationTest.java | 1 - .../credentials/DockerCredentialHelperIntegrationTest.java | 2 +- .../jib/{configuration/credentials => api}/Credential.java | 2 +- .../credentials => api}/CredentialRetriever.java | 2 +- .../java/com/google/cloud/tools/jib/api/RegistryImage.java | 7 +------ .../tools/jib/builder/steps/AuthenticatePushStep.java | 2 +- .../cloud/tools/jib/builder/steps/PullBaseImageStep.java | 2 +- .../jib/builder/steps/RetrieveRegistryCredentialsStep.java | 4 ++-- .../cloud/tools/jib/configuration/ImageConfiguration.java | 2 +- .../tools/jib/frontend/CredentialRetrieverFactory.java | 4 ++-- .../cloud/tools/jib/registry/RegistryAuthenticator.java | 2 +- .../registry/credentials/CredentialRetrievalException.java | 2 +- .../credentials/DockerConfigCredentialRetriever.java | 2 +- .../jib/registry/credentials/DockerCredentialHelper.java | 2 +- .../com/google/cloud/tools/jib/api/ContainerizerTest.java | 1 - .../{configuration/credentials => api}/CredentialTest.java | 4 ++-- .../cloud/tools/jib/api/JibContainerBuilderTest.java | 2 -- .../com/google/cloud/tools/jib/api/RegistryImageTest.java | 2 -- .../builder/steps/RetrieveRegistryCredentialsStepTest.java | 4 ++-- .../tools/jib/configuration/BuildConfigurationTest.java | 4 ++-- .../tools/jib/frontend/CredentialRetrieverFactoryTest.java | 2 +- .../tools/jib/registry/RegistryAuthenticatorTest.java | 2 +- .../credentials/DockerConfigCredentialRetrieverTest.java | 2 +- .../jib/plugins/common/ConfigurationPropertyValidator.java | 2 +- .../jib/plugins/common/DefaultCredentialRetrievers.java | 4 ++-- .../jib/plugins/common/PluginConfigurationProcessor.java | 2 +- .../plugins/common/ConfigurationPropertyValidatorTest.java | 2 +- .../plugins/common/DefaultCredentialRetrieversTest.java | 4 ++-- 28 files changed, 31 insertions(+), 42 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration/credentials => api}/Credential.java (97%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{configuration/credentials => api}/CredentialRetriever.java (95%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{configuration/credentials => api}/CredentialTest.java (94%) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 9faa4099ba..7124df8d56 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.registry.LocalRegistry; import java.io.IOException; import java.nio.file.Path; diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 581469be27..1f4bf6970b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.api.Credential; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java similarity index 97% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java index cefc9b1484..23d5475bae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/Credential.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration.credentials; +package com.google.cloud.tools.jib.api; import java.util.Objects; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java similarity index 95% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java index 20def4297b..4e357ff8a5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration.credentials; +package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import java.util.Optional; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 3a5683eb3f..43242e49e0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -17,9 +17,6 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; -import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -83,8 +80,7 @@ public RegistryImage addCredential(String username, String password) { /** * Adds {@link CredentialRetriever} to fetch push/pull credentials for the image. Credential * retrievers are attempted in the order in which they are specified until credentials are - * successfully retrieved. See {@link CredentialRetrieverFactory} for useful pre-defined - * CredentialRetrievers. + * successfully retrieved. * *

Example usage: * @@ -105,7 +101,6 @@ public RegistryImage addCredential(String username, String password) { * * @param credentialRetriever the {@link CredentialRetriever} to add * @return this - * @see CredentialRetrieverFactory */ public RegistryImage addCredentialRetriever(CredentialRetriever credentialRetriever) { credentialRetrievers.add(credentialRetriever); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 76d2bf5909..196da2b4d4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 1dc151a436..12451f984d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InsecureRegistryException; @@ -30,7 +31,6 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 052abe5c20..4219bf553a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index 832a335eaf..d472331244 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.configuration; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index f74c4e47f7..152f250a9d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 2b71668368..f67f08644a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,9 +17,9 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java index 14089fe1b7..ee482a7853 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry.credentials; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.api.CredentialRetriever; /** Thrown if something went wrong during {@link CredentialRetriever#retrieve}. */ public class CredentialRetrievalException extends Exception { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 96803751e3..5580705a62 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.api.client.util.Base64; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.json.JsonTemplateMapper; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 91ec0aca34..b07cfc78ec 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index e9ea4d5e6b..38ede2cdc8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java similarity index 94% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java index d5731c5f7b..a5444441f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/credentials/CredentialTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.configuration.credentials; +package com.google.cloud.tools.jib.api; import java.util.Arrays; import java.util.HashSet; @@ -22,7 +22,7 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link Credential}. */ +/** Tests for {@link com.google.cloud.tools.jib.api.Credential}. */ public class CredentialTest { @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index a8fd47ad5d..a1071851c1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -21,8 +21,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java index e740ae1ebb..98c894c2d2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import org.junit.Assert; import org.junit.Test; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 551f5369bc..a3d8f6e40d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -16,12 +16,12 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index d87f0e58a8..0703844de3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -17,12 +17,12 @@ package com.google.cloud.tools.jib.configuration; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.Port; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 111927947a..4054e6ae01 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.frontend; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 39bd57c139..d6481b8271 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.http.TestWebServer; import java.io.IOException; import java.net.MalformedURLException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 1de405ba87..09cc13214a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry.credentials; -import com.google.cloud.tools.jib.configuration.credentials.Credential; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.io.Resources; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 8ebb779f26..2b15ae887f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.http.Authorization; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 2c58d8973f..399d3cc2d6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import java.io.FileNotFoundException; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 54212bbbb9..f5dfee731a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.DockerDaemonImage; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; @@ -26,7 +27,6 @@ import com.google.cloud.tools.jib.api.Ports; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 57b92766eb..b93d1df34c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.configuration.credentials.Credential; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableList; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 534cfb396c..2d2ac99e3c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.configuration.credentials.Credential; -import com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import java.io.FileNotFoundException; import java.io.IOException; From 044f8cfb61ec1c1a230f1199d5c5b84f143b59fb Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 6 Jun 2019 14:51:24 -0400 Subject: [PATCH 0612/2020] Avoid exposing EventHandlers in public API (#1759) --- jib-core/CHANGELOG.md | 5 +- .../jib/api/ContainerizerIntegrationTest.java | 7 +- .../cloud/tools/jib/api/Containerizer.java | 36 ++++++-- .../tools/jib/api/JibContainerBuilder.java | 3 +- .../tools/jib/{event => api}/JibEvent.java | 2 +- .../jib/{event/events => api}/LogEvent.java | 3 +- .../cloud/tools/jib/api/MainClassFinder.java | 18 ++-- .../BuildAndCacheApplicationLayerStep.java | 2 +- .../jib/builder/steps/BuildImageStep.java | 2 +- .../jib/builder/steps/LoadDockerStep.java | 2 +- .../jib/builder/steps/PullBaseImageStep.java | 2 +- .../tools/jib/builder/steps/PushBlobStep.java | 2 +- .../jib/builder/steps/PushImageStep.java | 2 +- .../RetrieveRegistryCredentialsStep.java | 2 +- .../jib/builder/steps/WriteTarFileStep.java | 2 +- .../jib/configuration/BuildConfiguration.java | 2 +- .../cloud/tools/jib/event/EventHandlers.java | 14 +-- .../google/cloud/tools/jib/event/Handler.java | 5 +- .../tools/jib/event/events/ProgressEvent.java | 2 +- .../tools/jib/event/events/TimerEvent.java | 2 +- .../frontend/CredentialRetrieverFactory.java | 35 ++++---- .../tools/jib/registry/ManifestPusher.java | 2 +- .../jib/registry/RegistryEndpointCaller.java | 2 +- .../DockerConfigCredentialRetriever.java | 18 ++-- .../tools/jib/api/ContainerizerTest.java | 5 -- .../jib/api/JibContainerBuilderTest.java | 5 +- .../tools/jib/api/MainClassFinderTest.java | 22 ++--- .../RetrieveRegistryCredentialsStepTest.java | 2 +- .../tools/jib/event/EventHandlersTest.java | 5 +- .../tools/jib/event/events/LogEventTest.java | 3 +- .../CredentialRetrieverFactoryTest.java | 31 ++++--- .../jib/registry/ManifestPusherTest.java | 2 +- .../registry/RegistryEndpointCallerTest.java | 2 +- .../DockerConfigCredentialRetrieverTest.java | 32 +++---- .../tools/jib/gradle/BuildDockerTask.java | 2 +- .../tools/jib/gradle/BuildImageTask.java | 2 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../jib/gradle/GradleProjectProperties.java | 90 +++++++++---------- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../jib/maven/MavenProjectProperties.java | 83 ++++++++--------- .../ConfigurationPropertyValidator.java | 32 +++---- .../jib/plugins/common/JibBuildRunner.java | 16 ++-- .../jib/plugins/common/MainClassResolver.java | 33 +++---- .../common/PluginConfigurationProcessor.java | 42 ++++----- .../jib/plugins/common/ProjectProperties.java | 12 ++- .../common/logging/AnsiLoggerWithFooter.java | 2 +- .../plugins/common/logging/ConsoleLogger.java | 4 +- .../common/logging/ConsoleLoggerBuilder.java | 2 +- .../common/logging/PlainConsoleLogger.java | 2 +- .../ConfigurationPropertyValidatorTest.java | 66 ++++++-------- .../plugins/common/JibBuildRunnerTest.java | 18 ++-- .../plugins/common/MainClassResolverTest.java | 20 ++--- .../PluginConfigurationProcessorTest.java | 15 ++-- .../logging/AnsiLoggerWithFooterTest.java | 2 +- .../logging/ConsoleLoggerBuilderTest.java | 2 +- .../logging/PlainConsoleLoggerTest.java | 2 +- 58 files changed, 346 insertions(+), 390 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{event => api}/JibEvent.java (94%) rename jib-core/src/main/java/com/google/cloud/tools/jib/{event/events => api}/LogEvent.java (96%) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index b7bbfe2b6c..fa06b5cb7b 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,10 +5,13 @@ All notable changes to this project will be documented in this file. ### Added +- `Containerizer#addEventHandler` for adding event handlers + ### Changed - Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package -- Removed `EventDispatcher` and `DefaultEventDispatcher`; events are now dispatched directly from `EventHandlers` +- Event handlers are now added directly to the `Containerizer` rather than adding them to an `EventHandlers` object first +- Removed multiple classes to simplify the event system (`JibEventType`, `BuildStepType`, `EventDispatcher`, `DefaultEventDispatcher`, `LayerCountEvent`) - MainClassFinder now uses a static method instead of requiring instantiation ### Fixed diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index ddf9bd1138..cc92f0600f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.Command; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.registry.LocalRegistry; @@ -153,10 +152,6 @@ private static void assertLayerSizer(int expected, String imageReference) @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private final ProgressChecker progressChecker = new ProgressChecker(); - private final EventHandlers eventHandlers = - EventHandlers.builder() - .add(ProgressEvent.class, progressChecker.progressEventHandler) - .build(); @Test public void testSteps_forBuildToDockerRegistry() @@ -354,7 +349,7 @@ private JibContainer buildImage( .setAllowInsecureRegistries(true) .setToolName("jib-integration-test") .setExecutorService(executorService) - .setEventHandlers(eventHandlers); + .addEventHandler(ProgressEvent.class, progressChecker.progressEventHandler); additionalTags.forEach(containerizer::withAdditionalTag); return containerBuilder.containerize(containerizer); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 96641f4de3..4f999aa979 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -33,6 +33,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nullable; @@ -138,12 +139,12 @@ public static Containerizer to(TarImage tarImage) { private final ImageConfiguration imageConfiguration; private final Function stepsRunnerFactory; private final boolean mustBeOnline; - private final Set additionalTags = new HashSet<>(); + private final EventHandlers.Builder eventHandlersBuilder = EventHandlers.builder(); + @Nullable private ExecutorService executorService; private Path baseImageLayersCacheDirectory = DEFAULT_BASE_CACHE_DIRECTORY; @Nullable private Path applicationLayersCacheDirectory; - private EventHandlers eventHandlers = EventHandlers.NONE; private boolean allowInsecureRegistries = false; private boolean offline = false; private String toolName = DEFAULT_TOOL_NAME; @@ -217,13 +218,32 @@ public Containerizer setApplicationLayersCache(Path cacheDirectory) { } /** - * Sets the {@link EventHandlers} to handle events dispatched during Jib's execution. + * Adds the {@code eventConsumer} to handle the {@link JibEvent} with class {@code eventType}. The + * order in which handlers are added is the order in which they are called when the event is + * dispatched. + * + *

Note: Implementations of {@code eventConsumer} must be thread-safe. + * + * @param eventType the event type that {@code eventConsumer} should handle + * @param eventConsumer the event handler + * @param the type of {@code eventType} + * @return this + */ + public Containerizer addEventHandler( + Class eventType, Consumer eventConsumer) { + eventHandlersBuilder.add(eventType, eventConsumer); + return this; + } + + /** + * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link + * #addEventHandler(Class, Consumer)} for more details. * - * @param eventHandlers the {@link EventHandlers} + * @param eventConsumer the event handler * @return this */ - public Containerizer setEventHandlers(EventHandlers eventHandlers) { - this.eventHandlers = eventHandlers; + public Containerizer addEventHandler(Consumer eventConsumer) { + eventHandlersBuilder.add(JibEvent.class, eventConsumer); return this; } @@ -294,8 +314,8 @@ Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException return applicationLayersCacheDirectory; } - EventHandlers getEventHandlers() { - return eventHandlers; + EventHandlers buildEventHandlers() { + return eventHandlersBuilder.build(); } boolean getAllowInsecureRegistries() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 228d4456ce..70506c17c3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -23,7 +23,6 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.UnknownHostException; @@ -518,7 +517,7 @@ BuildConfiguration toBuildConfiguration( .setOffline(containerizer.isOfflineMode()) .setToolName(containerizer.getToolName()) .setExecutorService(executorService) - .setEventHandlers(containerizer.getEventHandlers()) + .setEventHandlers(containerizer.buildEventHandlers()) .build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java similarity index 94% rename from jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java index 607c006fdc..9adc4192c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/JibEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.event; +package com.google.cloud.tools.jib.api; /** * Type for events dispatched by Jib Core. Implementation classes should not inherit from diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java similarity index 96% rename from jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java index 01c86ae184..ddcaef22f7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/LogEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java @@ -14,9 +14,8 @@ * the License. */ -package com.google.cloud.tools.jib.event.events; +package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.event.JibEvent; import com.google.common.annotations.VisibleForTesting; import java.util.Objects; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java index f3d699cddb..66bc222471 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.base.Preconditions; import java.io.IOException; import java.io.InputStream; @@ -26,6 +24,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -139,25 +138,24 @@ public MethodVisitor visitMethod( * Tries to find classes with {@code psvm} (see class javadoc) in {@code files}. * * @param files the files to search - * @param eventHandlers the {@link EventHandlers} used for handling log messages + * @param logger a {@link Consumer} used to handle log events * @return the {@link Result} of the main class finding attempt */ - public static Result find(List files, EventHandlers eventHandlers) { + public static Result find(List files, Consumer logger) { List mainClasses = new ArrayList<>(); for (Path file : files) { // Makes sure classFile is valid. if (!Files.exists(file)) { - eventHandlers.dispatch( - LogEvent.debug("MainClassFinder: " + file + " does not exist; ignoring")); + logger.accept(LogEvent.debug("MainClassFinder: " + file + " does not exist; ignoring")); continue; } if (!Files.isRegularFile(file)) { - eventHandlers.dispatch( + logger.accept( LogEvent.debug("MainClassFinder: " + file + " is not a regular file; skipping")); continue; } if (!file.toString().endsWith(".class")) { - eventHandlers.dispatch( + logger.accept( LogEvent.debug("MainClassFinder: " + file + " is not a class file; skipping")); continue; } @@ -172,11 +170,11 @@ public static Result find(List files, EventHandlers eventHandlers) { } catch (ArrayIndexOutOfBoundsException ignored) { // Not a valid class file (thrown by ClassReader if it reads an invalid format) - eventHandlers.dispatch(LogEvent.warn("Invalid class file found: " + file)); + logger.accept(LogEvent.warn("Invalid class file found: " + file)); } catch (IOException ignored) { // Could not read class file. - eventHandlers.dispatch(LogEvent.warn("Could not read file: " + file)); + logger.accept(LogEvent.warn("Could not read file: " + file)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index e3fdea7b04..6a2187cee8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 3a5b88c2ec..644eb5f3d4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 86bbc6227e..b8f71bd167 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageTarball; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 12451f984d..3c45b54816 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.async.AsyncStep; @@ -32,7 +33,6 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index cd41886673..662b37a3cd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; @@ -25,7 +26,6 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.util.concurrent.ListenableFuture; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index bdca799169..f967286715 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; @@ -24,7 +25,6 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 4219bf553a..0cf0d1c4c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -18,11 +18,11 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index da6e8e5028..f57625d92a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.async.AsyncDependencies; import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageTarball; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; import com.google.common.collect.ImmutableList; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 5d77b87fae..1c9ca20b53 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -18,9 +18,9 @@ import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index 1d88baadd4..e9c736990d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event; +import com.google.cloud.tools.jib.api.JibEvent; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMultimap; @@ -43,22 +44,11 @@ public static class Builder { * @param the type of {@code eventClass} * @return this */ - public Builder add(Class eventType, Consumer eventConsumer) { + public Builder add(Class eventType, Consumer eventConsumer) { handlers.put(eventType, new Handler<>(eventType, eventConsumer)); return this; } - /** - * Adds the {@code eventConsumer} to handle all {@link JibEvent} types. See {@link #add(Class, - * Consumer)} for more details. - * - * @param eventConsumer the event handler - * @return this - */ - public Builder add(Consumer eventConsumer) { - return add(JibEvent.class, eventConsumer); - } - public EventHandlers build() { return new EventHandlers(handlers); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java index 5661b371a1..c3d3002dcc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event; +import com.google.cloud.tools.jib.api.JibEvent; import com.google.common.base.Preconditions; import java.util.function.Consumer; @@ -23,9 +24,9 @@ class Handler { private final Class eventClass; - private final Consumer eventConsumer; + private final Consumer eventConsumer; - Handler(Class eventClass, Consumer eventConsumer) { + Handler(Class eventClass, Consumer eventConsumer) { this.eventClass = eventClass; this.eventConsumer = eventConsumer; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java index 53665ea250..613a9fcc47 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.api.JibEvent; import com.google.cloud.tools.jib.event.progress.Allocation; /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java index a7aaba0e66..52fe116a6f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.event.events; -import com.google.cloud.tools.jib.event.JibEvent; +import com.google.cloud.tools.jib.api.JibEvent; import java.time.Duration; import java.util.Optional; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 152f250a9d..14ba14124b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -19,8 +19,7 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -34,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** Static factories for various {@link CredentialRetriever}s. */ public class CredentialRetrieverFactory { @@ -56,13 +56,12 @@ interface DockerCredentialHelperFactory { * Creates a new {@link CredentialRetrieverFactory} for an image. * * @param imageReference the image the credential are for - * @param eventHandlers an event handlers for dispatching log events + * @param logger a consumer for handling log events * @return a new {@link CredentialRetrieverFactory} */ public static CredentialRetrieverFactory forImage( - ImageReference imageReference, EventHandlers eventHandlers) { - return new CredentialRetrieverFactory( - imageReference, eventHandlers, DockerCredentialHelper::new); + ImageReference imageReference, Consumer logger) { + return new CredentialRetrieverFactory(imageReference, logger, DockerCredentialHelper::new); } /** @@ -73,20 +72,20 @@ public static CredentialRetrieverFactory forImage( */ public static CredentialRetrieverFactory forImage(ImageReference imageReference) { return new CredentialRetrieverFactory( - imageReference, EventHandlers.NONE, DockerCredentialHelper::new); + imageReference, logEvent -> {}, DockerCredentialHelper::new); } - private final EventHandlers eventHandlers; private final ImageReference imageReference; + private final Consumer logger; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, - EventHandlers eventHandlers, + Consumer logger, DockerCredentialHelperFactory dockerCredentialHelperFactory) { this.imageReference = imageReference; - this.eventHandlers = eventHandlers; + this.logger = logger; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; } @@ -126,13 +125,13 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - eventHandlers.dispatch(LogEvent.info("Checking credentials from " + credentialHelper)); + logger.accept(LogEvent.info("Checking credentials from " + credentialHelper)); try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); } catch (CredentialHelperUnhandledServerUrlException ex) { - eventHandlers.dispatch( + logger.accept( LogEvent.info( "No credentials for " + imageReference.getRegistry() + " in " + credentialHelper)); return Optional.empty(); @@ -176,9 +175,9 @@ public CredentialRetriever inferCredentialHelper() { | CredentialHelperUnhandledServerUrlException ex) { if (ex.getMessage() != null) { // Warns the user that the specified (or inferred) credential helper cannot be used. - eventHandlers.dispatch(LogEvent.info(ex.getMessage())); + logger.accept(LogEvent.info(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - eventHandlers.dispatch(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); + logger.accept(LogEvent.info(" Caused by: " + ex.getCause().getMessage())); } } @@ -220,16 +219,16 @@ CredentialRetriever dockerConfig( return () -> { try { Optional dockerConfigCredentials = - dockerConfigCredentialRetriever.retrieve(eventHandlers); + dockerConfigCredentialRetriever.retrieve(logger); if (dockerConfigCredentials.isPresent()) { - eventHandlers.dispatch( + logger.accept( LogEvent.info( "Using credentials from Docker config for " + imageReference.getRegistry())); return dockerConfigCredentials; } } catch (IOException ex) { - eventHandlers.dispatch(LogEvent.info("Unable to parse Docker config")); + logger.accept(LogEvent.info("Unable to parse Docker config")); } return Optional.empty(); }; @@ -247,7 +246,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) } private void logGotCredentialsFrom(String credentialSource) { - eventHandlers.dispatch( + logger.accept( LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 0e3a49ccf7..4928408b9e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -19,9 +19,9 @@ import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 150071b5f6..c0e10f7eea 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -20,10 +20,10 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.Connection; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 5580705a62..0e13727cde 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -18,8 +18,7 @@ import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.api.Credential; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; @@ -30,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; +import java.util.function.Consumer; /** * Retrieves registry credentials from the Docker config. @@ -70,29 +70,29 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { /** * Retrieves credentials for a registry. Tries all possible known aliases. * - * @param eventHandlers event handlers for dispatching log events + * @param logger a consumer for handling log events * @return {@link Credential} found for {@code registry}, or {@link Optional#empty} if not found * @throws IOException if failed to parse the config JSON */ - public Optional retrieve(EventHandlers eventHandlers) throws IOException { + public Optional retrieve(Consumer logger) throws IOException { if (!Files.exists(dockerConfigFile)) { return Optional.empty(); } DockerConfig dockerConfig = new DockerConfig( JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class)); - return retrieve(dockerConfig, eventHandlers); + return retrieve(dockerConfig, logger); } /** * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @param dockerConfig the {@link DockerConfig} to retrieve from - * @param eventHandlers event handlers for dispatching log events + * @param logger a consumer for handling log events * @return the retrieved credentials, or {@code Optional#empty} if none are found */ @VisibleForTesting - Optional retrieve(DockerConfig dockerConfig, EventHandlers eventHandlers) { + Optional retrieve(DockerConfig dockerConfig, Consumer logger) { for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { // First, tries to find defined auth. String auth = dockerConfig.getAuthFor(registryAlias); @@ -118,9 +118,9 @@ Optional retrieve(DockerConfig dockerConfig, EventHandlers eventHand | CredentialHelperNotFoundException ex) { // Warns the user that the specified credential helper cannot be used. if (ex.getMessage() != null) { - eventHandlers.dispatch(LogEvent.warn(ex.getMessage())); + logger.accept(LogEvent.warn(ex.getMessage())); if (ex.getCause() != null && ex.getCause().getMessage() != null) { - eventHandlers.dispatch(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); + logger.accept(LogEvent.warn(" Caused by: " + ex.getCause().getMessage())); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index 38ede2cdc8..a2a8c22090 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.common.collect.ImmutableSet; import java.nio.file.Paths; import java.util.Arrays; @@ -34,7 +33,6 @@ public class ContainerizerTest { @Mock private ExecutorService mockExecutorService; - @Mock private EventHandlers mockEventHandlers; @Test public void testTo() throws CacheDirectoryCreationException { @@ -52,7 +50,6 @@ public void testTo() throws CacheDirectoryCreationException { private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreationException { Assert.assertTrue(containerizer.getAdditionalTags().isEmpty()); Assert.assertFalse(containerizer.getExecutorService().isPresent()); - Assert.assertEquals(EventHandlers.NONE, containerizer.getEventHandlers()); Assert.assertEquals( Containerizer.DEFAULT_BASE_CACHE_DIRECTORY, containerizer.getBaseImageLayersCacheDirectory()); @@ -66,7 +63,6 @@ private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreation .withAdditionalTag("tag1") .withAdditionalTag("tag2") .setExecutorService(mockExecutorService) - .setEventHandlers(mockEventHandlers) .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) .setAllowInsecureRegistries(true) @@ -75,7 +71,6 @@ private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreation Assert.assertEquals(ImmutableSet.of("tag1", "tag2"), containerizer.getAdditionalTags()); Assert.assertTrue(containerizer.getExecutorService().isPresent()); Assert.assertEquals(mockExecutorService, containerizer.getExecutorService().get()); - Assert.assertEquals(mockEventHandlers, containerizer.getEventHandlers()); Assert.assertEquals( Paths.get("base/image/layers"), containerizer.getBaseImageLayersCacheDirectory()); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index a1071851c1..b1b7359774 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEvent; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -138,7 +137,7 @@ public void testToBuildConfiguration() .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) .setExecutorService(mockExecutorService) - .setEventHandlers(EventHandlers.builder().add(mockJibEventConsumer).build()); + .addEventHandler(mockJibEventConsumer); RegistryImage baseImage = RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); @@ -287,7 +286,7 @@ private Containerizer createMockContainerizer() Mockito.when(mockContainerizer.getAllowInsecureRegistries()).thenReturn(false); Mockito.when(mockContainerizer.getToolName()).thenReturn("mocktool"); Mockito.when(mockContainerizer.getExecutorService()).thenReturn(Optional.empty()); - Mockito.when(mockContainerizer.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(mockContainerizer.buildEventHandlers()).thenReturn(EventHandlers.NONE); return mockContainerizer; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java index 082aa605be..ee5584089a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java @@ -17,13 +17,13 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.api.MainClassFinder.Result.Type; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.function.Consumer; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -35,13 +35,13 @@ @RunWith(MockitoJUnitRunner.class) public class MainClassFinderTest { - @Mock private EventHandlers mockEventHandlers; + @Mock private Consumer logEventConsumer; @Test public void testFindMainClass_simple() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/simple").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); @@ -52,7 +52,7 @@ public void testFindMainClass_subdirectories() throws URISyntaxException, IOExce Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/subdirectories").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -64,7 +64,7 @@ public void testFindMainClass_noClass() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/no-main").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertEquals(Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType()); } @@ -73,7 +73,7 @@ public void testFindMainClass_multiple() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/multiple").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertEquals( MainClassFinder.Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType()); Assert.assertEquals(2, mainClassFinderResult.getFoundMainClasses().size()); @@ -87,7 +87,7 @@ public void testFindMainClass_extension() throws URISyntaxException, IOException Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/extension").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -98,7 +98,7 @@ public void testFindMainClass_importedMethods() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/imported-methods").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -109,7 +109,7 @@ public void testFindMainClass_externalClasses() throws URISyntaxException, IOExc Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/external-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("main.MainClass")); @@ -120,7 +120,7 @@ public void testFindMainClass_innerClasses() throws URISyntaxException, IOExcept Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/inner-classes").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), @@ -132,7 +132,7 @@ public void testMainClass_varargs() throws URISyntaxException, IOException { Path rootDirectory = Paths.get(Resources.getResource("core/class-finder-tests/varargs").toURI()); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), mockEventHandlers); + MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer); Assert.assertSame(Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType()); Assert.assertThat( mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorld")); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index a3d8f6e40d..ad4ce0a2b4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.ListeningExecutorService; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java index 4a18eb8c83..686e8e26d1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.event; +import com.google.cloud.tools.jib.api.JibEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -60,7 +61,7 @@ public void testAdd() { TestJibEvent1.class, testJibEvent1 -> Assert.assertEquals("payload", testJibEvent1.getPayload())) .add(TestJibEvent2.class, testJibEvent2 -> testJibEvent2.sayHello("Jib")) - .add(jibEvent -> counter[0]++) + .add(JibEvent.class, jibEvent -> counter[0]++) .build(); Assert.assertTrue(eventHandlers.getHandlers().containsKey(JibEvent.class)); Assert.assertTrue(eventHandlers.getHandlers().containsKey(TestJibEvent1.class)); @@ -94,7 +95,7 @@ public void testDispatch() { .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 first")) .add(TestJibEvent2.class, testJibEvent2 -> emissions.add("handled 2 second")) .add(TestJibEvent3.class, testJibEvent3 -> emissions.add("handled 3")) - .add(jibEvent -> emissions.add("handled generic")) + .add(JibEvent.class, jibEvent -> emissions.add("handled generic")) .build(); TestJibEvent2 testJibEvent2 = new TestJibEvent2(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java index fee62d8691..aa3b8737ea 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java @@ -16,8 +16,9 @@ package com.google.cloud.tools.jib.event.events; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; import java.util.ArrayDeque; import java.util.Deque; import org.junit.Assert; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 4054e6ae01..16537a52da 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -18,8 +18,7 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory.DockerCredentialHelperFactory; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperNotFoundException; import com.google.cloud.tools.jib.registry.credentials.CredentialHelperUnhandledServerUrlException; @@ -30,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; +import java.util.function.Consumer; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -64,7 +64,7 @@ private static DockerCredentialHelperFactory getTestFactory( }; } - @Mock private EventHandlers mockEventHandlers; + @Mock private Consumer mockLogger; @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; @@ -87,7 +87,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("registry", "repository", null), - mockEventHandlers, + mockLogger, getTestFactory( "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); @@ -97,8 +97,7 @@ public void testDockerCredentialHelper() throws CredentialRetrievalException { .dockerCredentialHelper(Paths.get("docker-credential-helper")) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.info("Using docker-credential-helper for registry")); + Mockito.verify(mockLogger).accept(LogEvent.info("Using docker-credential-helper for registry")); } @Test @@ -106,7 +105,7 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.gcr.io", "repository", null), - mockEventHandlers, + mockLogger, getTestFactory( "something.gcr.io", Paths.get("docker-credential-gcr"), @@ -118,8 +117,8 @@ public void testInferCredentialHelper() throws CredentialRetrievalException { .inferCredentialHelper() .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); } @Test @@ -127,7 +126,7 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException CredentialRetrieverFactory credentialRetrieverFactory = new CredentialRetrieverFactory( ImageReference.of("something.amazonaws.com", "repository", null), - mockEventHandlers, + mockLogger, getTestFactory( "something.amazonaws.com", Paths.get("docker-credential-ecr-login"), @@ -137,17 +136,17 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException Mockito.when(mockCredentialHelperNotFoundException.getCause()) .thenReturn(new IOException("the root cause")); Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); - Mockito.verify(mockEventHandlers).dispatch(LogEvent.info("warning")); - Mockito.verify(mockEventHandlers).dispatch(LogEvent.info(" Caused by: the root cause")); + Mockito.verify(mockLogger).accept(LogEvent.info("warning")); + Mockito.verify(mockLogger).accept(LogEvent.info(" Caused by: the root cause")); } @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage( - ImageReference.of("registry", "repository", null), mockEventHandlers); + ImageReference.of("registry", "repository", null), mockLogger); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockEventHandlers)) + Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockLogger)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( @@ -156,7 +155,7 @@ public void testDockerConfig() throws IOException, CredentialRetrievalException .dockerConfig(mockDockerConfigCredentialRetriever) .retrieve() .orElseThrow(AssertionError::new)); - Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.info("Using credentials from Docker config for registry")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using credentials from Docker config for registry")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index e78a15c48f..fba3529cac 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -19,8 +19,8 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 833a5c1b97..26a5a3e30b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -21,10 +21,10 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 09cc13214a..5c4d830ba1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -17,14 +17,14 @@ package com.google.cloud.tools.jib.registry.credentials; import com.google.cloud.tools.jib.api.Credential; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; +import java.util.function.Consumer; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -41,7 +41,7 @@ public class DockerConfigCredentialRetrieverTest { @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; - @Mock private EventHandlers mockEventHandlers; + @Mock private Consumer mockLogger; private Path dockerConfigFile; @@ -58,7 +58,7 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventHandlers).isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test @@ -66,7 +66,7 @@ public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("some", credentials.get().getUsername()); Assert.assertEquals("auth", credentials.get().getPassword()); @@ -82,7 +82,7 @@ public void testRetrieve_useCredsStore() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventHandlers) + .retrieve(mockDockerConfig, mockLogger) .orElseThrow(AssertionError::new)); } @@ -96,7 +96,7 @@ public void testRetrieve_useCredsStore_withProtocol() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventHandlers) + .retrieve(mockDockerConfig, mockLogger) .orElseThrow(AssertionError::new)); } @@ -110,7 +110,7 @@ public void testRetrieve_useCredHelper() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventHandlers) + .retrieve(mockDockerConfig, mockLogger) .orElseThrow(AssertionError::new)); } @@ -126,11 +126,11 @@ public void testRetrieve_useCredHelper_warn() Paths.get("docker-credential-path"), new Throwable("cause"))); new DockerConfigCredentialRetriever("another registry", dockerConfigFile) - .retrieve(mockDockerConfig, mockEventHandlers); + .retrieve(mockDockerConfig, mockLogger); - Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.warn("The system does not have docker-credential-path CLI")); - Mockito.verify(mockEventHandlers).dispatch(LogEvent.warn(" Caused by: cause")); + Mockito.verify(mockLogger) + .accept(LogEvent.warn("The system does not have docker-credential-path CLI")); + Mockito.verify(mockLogger).accept(LogEvent.warn(" Caused by: cause")); } @Test @@ -138,7 +138,7 @@ public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); - Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockEventHandlers).isPresent()); + Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test @@ -151,7 +151,7 @@ public void testRetrieve_credentialFromAlias() { Assert.assertEquals( FAKE_CREDENTIAL, dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockEventHandlers) + .retrieve(mockDockerConfig, mockLogger) .orElseThrow(AssertionError::new)); } @@ -163,7 +163,7 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); @@ -177,7 +177,7 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); - Optional credentials = dockerConfigCredentialRetriever.retrieve(mockEventHandlers); + Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("token for", credentials.get().getUsername()); Assert.assertEquals(" index.docker.io/v1/", credentials.get().getPassword()); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 3cbb58c0c3..f3ada00b5e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -137,7 +137,7 @@ public void buildDocker() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 35439981f9..b1e5213997 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -117,7 +117,7 @@ public void buildImage() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 6adacc033f..ef5f7c9015 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -135,7 +135,7 @@ public void buildTar() .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 1b7b013480..8ad2984979 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -17,11 +17,11 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -78,47 +78,6 @@ static Path getExplodedWarDirectory(Project project) { return project.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME); } - private static EventHandlers makeEventHandlers( - Project project, Logger logger, SingleThreadedExecutor singleThreadedExecutor) { - ConsoleLoggerBuilder consoleLoggerBuilder = - (isProgressFooterEnabled(project) - ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) - : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) - .lifecycle(logger::lifecycle); - if (logger.isDebugEnabled()) { - consoleLoggerBuilder.debug(logger::debug); - } - if (logger.isInfoEnabled()) { - consoleLoggerBuilder.info(logger::info); - } - if (logger.isWarnEnabled()) { - consoleLoggerBuilder.warn(logger::warn); - } - if (logger.isErrorEnabled()) { - consoleLoggerBuilder.error(logger::error); - } - ConsoleLogger consoleLogger = consoleLoggerBuilder.build(); - - return EventHandlers.builder() - .add( - LogEvent.class, - logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) - .add( - TimerEvent.class, - new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) - .add( - ProgressEvent.class, - new ProgressEventHandler( - update -> { - List footer = - ProgressDisplayGenerator.generateProgressDisplay( - update.getProgress(), update.getUnfinishedLeafTasks()); - footer.add(""); - consoleLogger.setFooter(footer); - })) - .build(); - } - private static boolean isProgressFooterEnabled(Project project) { if ("plain".equals(System.getProperty(PropertyNames.CONSOLE))) { return false; @@ -139,15 +98,31 @@ private static boolean isProgressFooterEnabled(Project project) { private final Project project; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); - private final EventHandlers eventHandlers; private final Logger logger; + private final ConsoleLogger consoleLogger; @VisibleForTesting GradleProjectProperties(Project project, Logger logger) { this.project = project; this.logger = logger; - - eventHandlers = makeEventHandlers(project, logger, singleThreadedExecutor); + ConsoleLoggerBuilder consoleLoggerBuilder = + (isProgressFooterEnabled(project) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) + .lifecycle(logger::lifecycle); + if (logger.isDebugEnabled()) { + consoleLoggerBuilder.debug(logger::debug); + } + if (logger.isInfoEnabled()) { + consoleLoggerBuilder.info(logger::info); + } + if (logger.isWarnEnabled()) { + consoleLoggerBuilder.warn(logger::warn); + } + if (logger.isErrorEnabled()) { + consoleLoggerBuilder.error(logger::error); + } + this.consoleLogger = consoleLoggerBuilder.build(); } @Override @@ -227,8 +202,27 @@ public void waitForLoggingThread() { } @Override - public EventHandlers getEventHandlers() { - return eventHandlers; + public void configureEventHandlers(Containerizer containerizer) { + containerizer + .addEventHandler(LogEvent.class, this::log) + .addEventHandler( + TimerEvent.class, + new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + .addEventHandler( + ProgressEvent.class, + new ProgressEventHandler( + update -> { + List footer = + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedLeafTasks()); + footer.add(""); + consoleLogger.setFooter(footer); + })); + } + + @Override + public void log(LogEvent logEvent) { + consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 1796a3cdc1..8c4c0fe95d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -132,7 +132,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 2d22e1beac..99877554e6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -126,7 +126,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a763f802b7..ac6b4d5455 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -104,7 +104,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { .build( pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), - projectProperties.getEventHandlers(), + projectProperties::log, helpfulSuggestions); } finally { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index c9f23af10f..41e3798ce1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -17,11 +17,11 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -97,44 +97,6 @@ public static String getProperty( return null; } - private static EventHandlers makeEventHandlers( - MavenSession session, Log log, SingleThreadedExecutor singleThreadedExecutor) { - ConsoleLoggerBuilder logEventHandlerBuilder = - (isProgressFooterEnabled(session) - ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) - : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) - .lifecycle(log::info); - if (log.isDebugEnabled()) { - logEventHandlerBuilder - .debug(log::debug) - // INFO messages also go to Log#debug since Log#info is used for LIFECYCLE. - .info(log::debug); - } - if (log.isWarnEnabled()) { - logEventHandlerBuilder.warn(log::warn); - } - if (log.isErrorEnabled()) { - logEventHandlerBuilder.error(log::error); - } - ConsoleLogger consoleLogger = logEventHandlerBuilder.build(); - - return EventHandlers.builder() - .add( - LogEvent.class, - logEvent -> consoleLogger.log(logEvent.getLevel(), logEvent.getMessage())) - .add( - TimerEvent.class, - new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) - .add( - ProgressEvent.class, - new ProgressEventHandler( - update -> - consoleLogger.setFooter( - ProgressDisplayGenerator.generateProgressDisplay( - update.getProgress(), update.getUnfinishedLeafTasks())))) - .build(); - } - @VisibleForTesting static boolean isProgressFooterEnabled(MavenSession session) { if (!session.getRequest().isInteractiveMode()) { @@ -186,13 +148,30 @@ static int getVersionFromString(String versionString) { private final MavenProject project; private final MavenSession session; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); - private final EventHandlers eventHandlers; + private final ConsoleLogger consoleLogger; @VisibleForTesting MavenProjectProperties(MavenProject project, MavenSession session, Log log) { this.project = project; this.session = session; - eventHandlers = makeEventHandlers(session, log, singleThreadedExecutor); + ConsoleLoggerBuilder logEventHandlerBuilder = + (isProgressFooterEnabled(session) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) + .lifecycle(log::info); + if (log.isDebugEnabled()) { + logEventHandlerBuilder + .debug(log::debug) + // INFO messages also go to Log#debug since Log#info is used for LIFECYCLE. + .info(log::debug); + } + if (log.isWarnEnabled()) { + logEventHandlerBuilder.warn(log::warn); + } + if (log.isErrorEnabled()) { + logEventHandlerBuilder.error(log::error); + } + this.consoleLogger = logEventHandlerBuilder.build(); } @Override @@ -242,8 +221,24 @@ public void waitForLoggingThread() { } @Override - public EventHandlers getEventHandlers() { - return eventHandlers; + public void configureEventHandlers(Containerizer containerizer) { + containerizer + .addEventHandler(LogEvent.class, this::log) + .addEventHandler( + TimerEvent.class, + new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + .addEventHandler( + ProgressEvent.class, + new ProgressEventHandler( + update -> + consoleLogger.setFooter( + ProgressDisplayGenerator.generateProgressDisplay( + update.getProgress(), update.getUnfinishedLeafTasks())))); + } + + @Override + public void log(LogEvent logEvent) { + consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); } @Override diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 2b15ae887f..4cd2e7e4a4 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -19,8 +19,7 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.http.Authorization; import com.google.common.base.Strings; import java.util.ArrayList; @@ -28,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -42,7 +42,7 @@ public class ConfigurationPropertyValidator { * Gets a {@link Credential} from a username and password. First tries system properties, then * tries build configuration, otherwise returns null. * - * @param eventHandlers the {@link EventHandlers} used to dispatch log events + * @param logger a consumer for handling log events * @param usernameProperty the name of the username system property * @param passwordProperty the name of the password system property * @param auth the configured credentials @@ -51,7 +51,7 @@ public class ConfigurationPropertyValidator { * {@link Optional#empty} if neither is configured. */ public static Optional getImageCredential( - EventHandlers eventHandlers, + Consumer logger, String usernameProperty, String passwordProperty, AuthProperty auth, @@ -66,7 +66,7 @@ public static Optional getImageCredential( // Warn if a system property is missing if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { - eventHandlers.dispatch( + logger.accept( LogEvent.warn( passwordProperty + " system property is set, but " @@ -74,7 +74,7 @@ public static Optional getImageCredential( + " is not; attempting other authentication methods.")); } if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { - eventHandlers.dispatch( + logger.accept( LogEvent.warn( usernameProperty + " system property is set, but " @@ -87,14 +87,14 @@ public static Optional getImageCredential( return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getUsername())) { - eventHandlers.dispatch( + logger.accept( LogEvent.warn( auth.getUsernameDescriptor() + " is missing from build configuration; ignoring auth section.")); return Optional.empty(); } if (Strings.isNullOrEmpty(auth.getPassword())) { - eventHandlers.dispatch( + logger.accept( LogEvent.warn( auth.getPasswordDescriptor() + " is missing from build configuration; ignoring auth section.")); @@ -109,9 +109,8 @@ public static Optional getImageCredential( * {@code project-name:project-version} if target image is not configured * * @param targetImage the configured target image reference - * @param eventHandlers the {@link EventHandlers} used to dispatch log events - * @param generatedName the image name to use if {@code targetImage} is {@code null} - * @param generatedTag the tag to use if {@code targetImage} is {@code null} + * @param projectProperties the {@link ProjectProperties} providing the project name, version, and + * log event handler * @param helpfulSuggestions used for generating the message notifying the user of the generated * tag * @return an {@link ImageReference} parsed from the configured target image, or one of the form @@ -121,13 +120,16 @@ public static Optional getImageCredential( */ public static ImageReference getGeneratedTargetDockerTag( @Nullable String targetImage, - EventHandlers eventHandlers, - String generatedName, - String generatedTag, + ProjectProperties projectProperties, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { + String generatedName = projectProperties.getName(); + String generatedTag = + projectProperties.getVersion().equals("unspecified") + ? "latest" + : projectProperties.getVersion(); if (Strings.isNullOrEmpty(targetImage)) { - eventHandlers.dispatch( + projectProperties.log( LogEvent.lifecycle(helpfulSuggestions.forGeneratedTag(generatedName, generatedTag))); // Try to parse generated tag to verify that project name and version are valid (throws an diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index e39bd541e1..e55926a19c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -24,11 +24,10 @@ import com.google.cloud.tools.jib.api.InsecureRegistryException; 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.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; @@ -40,6 +39,7 @@ import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import javax.annotation.Nullable; import org.apache.http.conn.HttpHostConnectException; @@ -162,7 +162,7 @@ private static void handleRegistryUnauthorizedException( * * @param jibContainerBuilder the {@link JibContainerBuilder} * @param containerizer the {@link Containerizer} - * @param eventHandlers the {@link EventHandlers} + * @param logger consumer for handling log events * @param helpfulSuggestions suggestions to use in help messages for exceptions * @return the built {@link JibContainer} * @throws BuildStepsExecutionException if another exception is thrown during the build @@ -172,17 +172,17 @@ private static void handleRegistryUnauthorizedException( public JibContainer build( JibContainerBuilder jibContainerBuilder, Containerizer containerizer, - EventHandlers eventHandlers, + Consumer logger, HelpfulSuggestions helpfulSuggestions) throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { try { - eventHandlers.dispatch(LogEvent.lifecycle("")); - eventHandlers.dispatch(LogEvent.lifecycle(startupMessage)); + logger.accept(LogEvent.lifecycle("")); + logger.accept(LogEvent.lifecycle(startupMessage)); JibContainer jibContainer = jibContainerBuilder.containerize(containerizer); - eventHandlers.dispatch(LogEvent.lifecycle("")); - eventHandlers.dispatch(LogEvent.lifecycle(successMessage)); + logger.accept(LogEvent.lifecycle("")); + logger.accept(LogEvent.lifecycle(successMessage)); // when an image is built, write out the digest and id if (imageDigestOutputPath != null) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java index 5eb09c348c..b6a234de2e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/MainClassResolver.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.MainClassFinder; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; @@ -71,9 +71,7 @@ public static String resolveMainClass( Preconditions.checkNotNull(mainClass); if (!isValidJavaClass(mainClass)) { - projectProperties - .getEventHandlers() - .dispatch(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); + projectProperties.log(LogEvent.warn("'mainClass' is not a valid Java class : " + mainClass)); } return mainClass; @@ -95,29 +93,24 @@ static boolean isValidJavaClass(String className) { @Nullable private static String getMainClassFromJar(ProjectProperties projectProperties) { - projectProperties - .getEventHandlers() - .dispatch( - LogEvent.info( - "Searching for main class... Add a 'mainClass' configuration to '" - + projectProperties.getPluginName() - + "' to improve build speed.")); + projectProperties.log( + LogEvent.info( + "Searching for main class... Add a 'mainClass' configuration to '" + + projectProperties.getPluginName() + + "' to improve build speed.")); return projectProperties.getMainClassFromJar(); } private static String findMainClassInClassFiles(ProjectProperties projectProperties) throws MainClassInferenceException, IOException { - projectProperties - .getEventHandlers() - .dispatch( - LogEvent.debug( - "Could not find a valid main class specified in " - + projectProperties.getJarPluginName() - + "; attempting to infer main class.")); + projectProperties.log( + LogEvent.debug( + "Could not find a valid main class specified in " + + projectProperties.getJarPluginName() + + "; attempting to infer main class.")); MainClassFinder.Result mainClassFinderResult = - MainClassFinder.find( - projectProperties.getClassFiles(), projectProperties.getEventHandlers()); + MainClassFinder.find(projectProperties.getClassFiles(), projectProperties::log); switch (mainClassFinderResult.getType()) { case MAIN_CLASS_FOUND: diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index f5dfee731a..48d1e465dc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -24,11 +24,10 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.Ports; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.TarImage; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.common.annotations.VisibleForTesting; @@ -122,7 +121,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry boolean isTargetImageCredentialPresent = configureCredentialRetrievers( rawConfiguration, - projectProperties.getEventHandlers(), + projectProperties, targetImage, targetImageReference, PropertyNames.TO_AUTH_USERNAME, @@ -160,9 +159,8 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageReference baseImageReference = ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); - EventHandlers eventHandlers = projectProperties.getEventHandlers(); if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { - eventHandlers.dispatch( + projectProperties.log( LogEvent.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not " + "enable this on a public network!")); @@ -172,7 +170,7 @@ static PluginConfigurationProcessor processCommonConfiguration( boolean isBaseImageCredentialPresent = configureCredentialRetrievers( rawConfiguration, - eventHandlers, + projectProperties, baseImage, baseImageReference, PropertyNames.FROM_AUTH_USERNAME, @@ -195,7 +193,7 @@ baseImage, getAppRootChecked(rawConfiguration, projectProperties)) getWorkingDirectoryChecked(rawConfiguration) .ifPresent(jibContainerBuilder::setWorkingDirectory); if (rawConfiguration.getUseCurrentTimestamp()) { - eventHandlers.dispatch( + projectProperties.log( LogEvent.warn( "Setting image creation time to current time; your image may not be reproducible.")); jibContainerBuilder.setCreationTime(Instant.now()); @@ -251,11 +249,9 @@ static List computeEntrypoint( if (rawConfiguration.getMainClass().isPresent() || !rawConfiguration.getJvmFlags().isEmpty() || !rawExtraClasspath.isEmpty()) { - projectProperties - .getEventHandlers() - .dispatch( - LogEvent.warn( - "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); + projectProperties.log( + LogEvent.warn( + "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } if (rawEntrypoint.get().size() == 1 && "INHERIT".equals(rawEntrypoint.get().get(0))) { @@ -398,7 +394,7 @@ static Optional getWorkingDirectoryChecked(RawConfiguration ra // TODO: find a way to reduce the number of arguments. private static boolean configureCredentialRetrievers( RawConfiguration rawConfiguration, - EventHandlers eventHandlers, + ProjectProperties projectProperties, RegistryImage registryImage, ImageReference imageReference, String usernamePropertyName, @@ -409,10 +405,14 @@ private static boolean configureCredentialRetrievers( throws FileNotFoundException { DefaultCredentialRetrievers defaultCredentialRetrievers = DefaultCredentialRetrievers.init( - CredentialRetrieverFactory.forImage(imageReference, eventHandlers)); + CredentialRetrieverFactory.forImage(imageReference, projectProperties::log)); Optional optionalCredential = ConfigurationPropertyValidator.getImageCredential( - eventHandlers, usernamePropertyName, passwordPropertyName, knownAuth, rawConfiguration); + projectProperties::log, + usernamePropertyName, + passwordPropertyName, + knownAuth, + rawConfiguration); boolean credentialPresent = optionalCredential.isPresent(); if (optionalCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( @@ -430,7 +430,7 @@ private static boolean configureCredentialRetrievers( defaultCredentialRetrievers.setInferredCredential(credential, auth.getAuthDescriptor()); } } catch (InferredAuthException ex) { - eventHandlers.dispatch(LogEvent.warn("InferredAuthException: " + ex.getMessage())); + projectProperties.log(LogEvent.warn("InferredAuthException: " + ex.getMessage())); } } defaultCredentialRetrievers.setCredentialHelper(credHelper); @@ -445,13 +445,7 @@ private static ImageReference getGeneratedTargetDockerTag( HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException { return ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - rawConfiguration.getToImage().orElse(null), - projectProperties.getEventHandlers(), - projectProperties.getName(), - projectProperties.getVersion().equals("unspecified") - ? "latest" - : projectProperties.getVersion(), - helpfulSuggestions); + rawConfiguration.getToImage().orElse(null), projectProperties, helpfulSuggestions); } /** @@ -466,10 +460,10 @@ private static void configureContainerizer( Containerizer containerizer, RawConfiguration rawConfiguration, ProjectProperties projectProperties) { + projectProperties.configureEventHandlers(containerizer); containerizer .setOfflineMode(projectProperties.isOffline()) .setToolName(projectProperties.getToolName()) - .setEventHandlers(projectProperties.getEventHandlers()) .setAllowInsecureRegistries(rawConfiguration.getAllowInsecureRegistries()) .setBaseImageLayersCache( getCheckedCacheDirectory( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index e03b376a51..e4ba372261 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -17,9 +17,10 @@ package com.google.cloud.tools.jib.plugins.common; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.event.EventHandlers; import java.io.IOException; import java.nio.file.Path; import java.util.List; @@ -40,8 +41,15 @@ public interface ProjectProperties { // TODO: Move out of ProjectProperties. void waitForLoggingThread(); + /** + * Adds the plugin's event handlers to a containerizer. + * + * @param containerizer the containerizer to add event handlers to + */ // TODO: Move out of ProjectProperties. - EventHandlers getEventHandlers(); + void configureEventHandlers(Containerizer containerizer); + + void log(LogEvent logEvent); String getToolName(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index dc4e201b5d..07e264e6e3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java index a21e61e6de..bd06700b9d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLogger.java @@ -16,8 +16,8 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent.Level; import java.util.List; /** Logs messages to the console. Implementations must be thread-safe. */ diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java index 49852f5cca..820844f462 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import java.util.function.Consumer; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java index d244513b93..73a3e74557 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.function.Consumer; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index b93d1df34c..0ae2515420 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -19,11 +19,11 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.Optional; +import java.util.function.Consumer; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +35,7 @@ @RunWith(MockitoJUnitRunner.class) public class ConfigurationPropertyValidatorTest { - @Mock private EventHandlers mockEventHandlers; + @Mock private Consumer mockLogger; @Mock private AuthProperty mockAuth; @Mock private ImageReference mockImageReference; @Mock private RawConfiguration mockConfiguration; @@ -55,11 +55,7 @@ public void testGetImageAuth() { Credential expected = Credential.from("abcde", "12345"); Optional actual = ConfigurationPropertyValidator.getImageCredential( - mockEventHandlers, - "jib.test.auth.user", - "jib.test.auth.pass", - mockAuth, - mockConfiguration); + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); @@ -69,25 +65,17 @@ public void testGetImageAuth() { expected = Credential.from("vwxyz", "98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventHandlers, - "jib.test.auth.user", - "jib.test.auth.pass", - mockAuth, - mockConfiguration); + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertTrue(actual.isPresent()); Assert.assertEquals(expected.toString(), actual.get().toString()); - Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.warn(Mockito.any())); + Mockito.verify(mockLogger, Mockito.never()).accept(LogEvent.warn(Mockito.any())); // Auth completely missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventHandlers, - "jib.test.auth.user", - "jib.test.auth.pass", - mockAuth, - mockConfiguration); + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertFalse(actual.isPresent()); // Password missing @@ -95,30 +83,20 @@ public void testGetImageAuth() { Mockito.when(mockAuth.getPassword()).thenReturn(null); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventHandlers, - "jib.test.auth.user", - "jib.test.auth.pass", - mockAuth, - mockConfiguration); + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); + Mockito.verify(mockLogger) + .accept(LogEvent.warn("pass is missing from build configuration; ignoring auth section.")); // Username missing Mockito.when(mockAuth.getUsername()).thenReturn(null); Mockito.when(mockAuth.getPassword()).thenReturn("98765"); actual = ConfigurationPropertyValidator.getImageCredential( - mockEventHandlers, - "jib.test.auth.user", - "jib.test.auth.pass", - mockAuth, - mockConfiguration); + mockLogger, "jib.test.auth.user", "jib.test.auth.pass", mockAuth, mockConfiguration); Assert.assertFalse(actual.isPresent()); - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.warn("user is missing from build configuration; ignoring auth section.")); + Mockito.verify(mockLogger) + .accept(LogEvent.warn("user is missing from build configuration; ignoring auth section.")); } @Test @@ -136,30 +114,36 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept "build.txt"); // Target configured + ProjectProperties mockProjectProperties = Mockito.mock(ProjectProperties.class); + Mockito.when(mockProjectProperties.getName()).thenReturn("project-name"); + Mockito.when(mockProjectProperties.getVersion()).thenReturn("project-version"); + ImageReference result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - "a/b:c", mockEventHandlers, "project-name", "project-version", helpfulSuggestions); + "a/b:c", mockProjectProperties, helpfulSuggestions); Assert.assertEquals("a/b", result.getRepository()); Assert.assertEquals("c", result.getTag()); - Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.lifecycle(Mockito.any())); + Mockito.verify(mockLogger, Mockito.never()).accept(LogEvent.lifecycle(Mockito.any())); // Target not configured result = ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventHandlers, "project-name", "project-version", helpfulSuggestions); + null, mockProjectProperties, helpfulSuggestions); Assert.assertEquals("project-name", result.getRepository()); Assert.assertEquals("project-version", result.getTag()); - Mockito.verify(mockEventHandlers) - .dispatch( + Mockito.verify(mockProjectProperties) + .log( LogEvent.lifecycle( "Tagging image with generated image reference project-name:project-version. If you'd " + "like to specify a different tag, you can set the to parameter in your " + "build.txt, or use the --to= commandline flag.")); // Generated tag invalid + Mockito.when(mockProjectProperties.getName()).thenReturn("%#&///*@("); + Mockito.when(mockProjectProperties.getVersion()).thenReturn("%$#//&*@($"); try { ConfigurationPropertyValidator.getGeneratedTargetDockerTag( - null, mockEventHandlers, "%#&///*@(", "%$#//&*@($", helpfulSuggestions); + null, mockProjectProperties, helpfulSuggestions); Assert.fail(); } catch (InvalidImageReferenceException ignored) { } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 8ed671ceec..4f1135bbf3 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import java.io.IOException; import java.net.UnknownHostException; @@ -61,7 +60,6 @@ public class JibBuildRunnerTest { @Mock private JibContainerBuilder mockJibContainerBuilder; @Mock private Containerizer mockContainerizer; - @Mock private EventHandlers mockEventHandlers; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @Mock private HttpResponseException mockHttpResponseException; @@ -77,7 +75,7 @@ public void setUpMocks() { public void testBuildImage_pass() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); } @Test @@ -92,7 +90,7 @@ public void testBuildImage_httpHostConnectException() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -111,7 +109,7 @@ public void testBuildImage_unknownHostException() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -131,7 +129,7 @@ public void testBuildImage_insecureRegistryException() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -156,7 +154,7 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -182,7 +180,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -202,7 +200,7 @@ public void testBuildImage_registryCredentialsNotSentException() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { @@ -221,7 +219,7 @@ public void testBuildImage_other() try { testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, mockEventHandlers, TEST_HELPFUL_SUGGESTIONS); + mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); Assert.fail("buildImage should have thrown an exception"); } catch (BuildStepsExecutionException ex) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java index e32f19d91a..d640269e9a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/MainClassResolverTest.java @@ -16,9 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.JibEvent; -import com.google.cloud.tools.jib.event.events.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; @@ -26,7 +24,6 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.function.Consumer; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -42,13 +39,10 @@ public class MainClassResolverTest { private static final Path FAKE_CLASSES_PATH = Paths.get("a/b/c"); - @Mock private Consumer mockJibEventConsumer; @Mock private ProjectProperties mockProjectProperties; @Before public void setup() { - Mockito.when(mockProjectProperties.getEventHandlers()) - .thenReturn(EventHandlers.builder().add(mockJibEventConsumer).build()); Mockito.when(mockProjectProperties.getPluginName()).thenReturn("plugin"); Mockito.when(mockProjectProperties.getJarPluginName()).thenReturn("jar-plugin"); } @@ -69,8 +63,8 @@ public void testResolveMainClass_notValid() throws MainClassInferenceException, .thenReturn(ImmutableList.of(FAKE_CLASSES_PATH)); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockJibEventConsumer) - .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockProjectProperties) + .log(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -85,8 +79,8 @@ public void testResolveMainClass_multipleInferredWithBackup() .asList()); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockJibEventConsumer) - .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockProjectProperties) + .log(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test @@ -119,8 +113,8 @@ public void testResolveMainClass_noneInferredWithBackup() .thenReturn(ImmutableList.of(Paths.get("ignored"))); Assert.assertEquals( "${start-class}", MainClassResolver.resolveMainClass(null, mockProjectProperties)); - Mockito.verify(mockJibEventConsumer) - .accept(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); + Mockito.verify(mockProjectProperties) + .log(LogEvent.warn("'mainClass' is not a valid Java class : ${start-class}")); } @Test diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 6a393fd3eb..8b4cd832f5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -26,10 +26,9 @@ import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; import com.google.cloud.tools.jib.api.LayerEntry; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.LogEvent; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -98,8 +97,6 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); - Mockito.when(projectProperties.getEventHandlers()) - .thenReturn(EventHandlers.builder().add(LogEvent.class, logger).build()); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when( projectProperties.createContainerBuilder( @@ -108,8 +105,6 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.isOffline()).thenReturn(false); Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); - Mockito.when(containerizer.setEventHandlers(Mockito.any(EventHandlers.class))) - .thenReturn(containerizer); Mockito.when(containerizer.setAllowInsecureRegistries(Mockito.anyBoolean())) .thenReturn(containerizer); Mockito.when(containerizer.setBaseImageLayersCache(Mockito.any(Path.class))) @@ -365,8 +360,8 @@ public void testEntrypoint_warningOnJvmFlags() Assert.assertEquals( Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(logger) - .accept( + Mockito.verify(projectProperties) + .log( LogEvent.warn( "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } @@ -388,8 +383,8 @@ public void testEntrypoint_warningOnMainclass() Assert.assertEquals( Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verify(logger) - .accept( + Mockito.verify(projectProperties) + .log( LogEvent.warn( "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index 3c6094922d..a0acdf8a78 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.Arrays; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java index 577f9615e9..fda6efaa61 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder.ConsoleLoggerFactory; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java index 7836d39b8e..c21603f8c7 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.plugins.common.logging; -import com.google.cloud.tools.jib.event.events.LogEvent.Level; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.Arrays; From 75b5dc81f959e9fee0b8424e6fee233c2c930646 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 6 Jun 2019 16:43:45 -0400 Subject: [PATCH 0613/2020] Add internal Skaffold-init task/goal (#1758) --- .../cloud/tools/jib/gradle/JibPlugin.java | 2 + .../tools/jib/gradle/SkaffoldInitTask.java | 50 ++++++++ .../jib/gradle/SkaffoldInitTaskTest.java | 94 +++++++++++++++ .../jib/maven/JibPluginConfiguration.java | 4 +- .../jib/maven/skaffold/SkaffoldInitMojo.java | 52 +++++++++ .../maven/skaffold/SkaffoldInitMojoTest.java | 110 ++++++++++++++++++ .../plugins/common/SkaffoldInitOutput.java | 96 +++++++++++++++ 7 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldInitOutput.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 9f1ec9e1cc..3e62a87ec0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -41,6 +41,7 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; @VisibleForTesting static final String FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; + @VisibleForTesting static final String INIT_TASK_NAME = "_jibSkaffoldInit"; @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; /** @@ -120,6 +121,7 @@ public void apply(Project project) { .setJibExtension(jibExtension); project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); project.getTasks().create(FILES_TASK_V2_NAME, FilesTaskV2.class).setJibExtension(jibExtension); + project.getTasks().create(INIT_TASK_NAME, SkaffoldInitTask.class).setJibExtension(jibExtension); project.afterEvaluate( projectAfterEvaluation -> { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java new file mode 100644 index 0000000000..34c6c0e255 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import com.google.common.base.Preconditions; +import java.io.IOException; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out to.image configuration and project name, used for Jib project detection in Skaffold. + * + *

Expected use: {@code ./gradlew _jibSkaffoldInit -q} + */ +public class SkaffoldInitTask extends DefaultTask { + + @Nullable private JibExtension jibExtension; + + public SkaffoldInitTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + @TaskAction + public void listModulesAndTargets() throws IOException { + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); + skaffoldInitOutput.setImage(Preconditions.checkNotNull(jibExtension).getTo().getImage()); + if (!getProject().equals(getProject().getRootProject())) { + skaffoldInitOutput.setProject(getProject().getName()); + } + System.out.println("\nBEGIN JIB JSON"); + System.out.println(skaffoldInitOutput.getJsonString()); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java new file mode 100644 index 0000000000..3109137fb2 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link SkaffoldInitTask}. */ +public class SkaffoldInitTaskTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + + /** + * Verifies that the files task succeeded and returns the list of JSON strings printed by the + * task. + * + * @param project the project to run the task on + * @return the JSON strings printed by the task + */ + private static List getJsons(TestProject project) { + BuildResult buildResult = + project.build(JibPlugin.INIT_TASK_NAME, "-q", "-D_TARGET_IMAGE=testimage"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.INIT_TASK_NAME); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + String output = buildResult.getOutput().trim(); + Assert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); + + Pattern pattern = Pattern.compile("BEGIN JIB JSON\r?\n(\\{.*})"); + Matcher matcher = pattern.matcher(output); + List jsons = new ArrayList<>(); + while (matcher.find()) { + jsons.add(matcher.group(1)); + } + + // Return task output with header removed + return jsons; + } + + @Test + public void testFilesTask_singleProject() throws IOException { + List outputs = getJsons(simpleTestProject); + Assert.assertEquals(1, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertNull(skaffoldInitOutput.getProject()); + } + + @Test + public void testFilesTask_multiProject() throws IOException { + List outputs = getJsons(multiTestProject); + Assert.assertEquals(3, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertNull(skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("complex-service", skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("simple-service", skaffoldInitOutput.getProject()); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index bbe8db895e..b7986049ae 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -259,7 +259,7 @@ MavenSession getSession() { return Preconditions.checkNotNull(session); } - MavenProject getProject() { + protected MavenProject getProject() { return Preconditions.checkNotNull(project); } @@ -302,7 +302,7 @@ AuthConfiguration getBaseImageAuth() { * @return the configured target image reference */ @Nullable - String getTargetImage() { + protected String getTargetImage() { String propertyAlternate = getProperty(PropertyNames.TO_IMAGE_ALTERNATE); if (propertyAlternate != null) { return propertyAlternate; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java new file mode 100644 index 0000000000..510ca46d20 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.JibPluginConfiguration; +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * Prints out to.image configuration and project name, used for Jib project detection in Skaffold. + * + *

Expected use: {@code ./mvnw jib:_skaffold-init -q} + */ +@Mojo(name = SkaffoldInitMojo.GOAL_NAME, requiresDependencyCollection = ResolutionScope.NONE) +public class SkaffoldInitMojo extends JibPluginConfiguration { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-init"; + + @Override + public void execute() throws MojoExecutionException { + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); + skaffoldInitOutput.setImage(getTargetImage()); + if (getProject().getParent() != null) { + skaffoldInitOutput.setProject(getProject().getName()); + } + + System.out.println("\nBEGIN JIB JSON"); + try { + System.out.println(skaffoldInitOutput.getJsonString()); + } catch (IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java new file mode 100644 index 0000000000..51e050bbcf --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.TestPlugin; +import com.google.cloud.tools.jib.maven.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link SkaffoldInitMojo}. */ +public class SkaffoldInitMojoTest { + + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + + @ClassRule + public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + + /** + * Verifies that the files task succeeded and returns the list of JSON strings printed by the + * task. + * + * @param project the project to run the task on + * @return the JSON strings printed by the task + */ + private static List getJsons(TestProject project) + throws VerificationException, IOException { + Verifier verifier = new Verifier(project.getProjectRoot().toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-q"); + verifier.addCliOption("-Dimage=testimage"); + verifier.executeGoal("jib:" + SkaffoldInitMojo.GOAL_NAME); + + verifier.verifyErrorFreeLog(); + Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); + String output = String.join("\n", Files.readAllLines(logFile, StandardCharsets.UTF_8)).trim(); + Assert.assertThat(output, CoreMatchers.startsWith("BEGIN JIB JSON")); + + Pattern pattern = Pattern.compile("BEGIN JIB JSON\r?\n(\\{.*})"); + Matcher matcher = pattern.matcher(output); + List jsons = new ArrayList<>(); + while (matcher.find()) { + jsons.add(matcher.group(1)); + } + + return jsons; + } + + @Test + public void testFilesMojo_singleModule() throws IOException, VerificationException { + List outputs = getJsons(simpleTestProject); + Assert.assertEquals(1, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertNull(skaffoldInitOutput.getProject()); + } + + @Test + public void testFilesMojo_multiModule() throws IOException, VerificationException { + List outputs = getJsons(multiTestProject); + Assert.assertEquals(4, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertNull(skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("name-service", skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("lib", skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(3)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("service", skaffoldInitOutput.getProject()); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldInitOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldInitOutput.java new file mode 100644 index 0000000000..34ba12320b --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldInitOutput.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import javax.annotation.Nullable; + +/** + * Builds a JSON string containing the configured target image and sub-project name, to be consumed + * by Skaffold. + * + *

Example: + * + *

{@code
+ * {
+ *   "image":"gcr.io/project/test",
+ *   "project":"project-name"
+ * }
+ * }
+ */ +@JsonInclude(Include.NON_NULL) +@JsonAutoDetect( + fieldVisibility = Visibility.ANY, + setterVisibility = Visibility.NONE, + getterVisibility = Visibility.NONE) +public class SkaffoldInitOutput { + + @Nullable private String image; + + @Nullable private String project; + + public SkaffoldInitOutput() {} + + @VisibleForTesting + public SkaffoldInitOutput(String json) throws IOException { + SkaffoldInitOutput skaffoldInitOutput = + new ObjectMapper().readValue(json, SkaffoldInitOutput.class); + this.image = skaffoldInitOutput.image; + this.project = skaffoldInitOutput.project; + } + + public void setImage(@Nullable String image) { + this.image = image; + } + + public void setProject(@Nullable String project) { + this.project = project; + } + + /** + * Gets the added files in JSON format. + * + * @return the files in a JSON string + * @throws IOException if writing out the JSON fails + */ + public String getJsonString() throws IOException { + try (OutputStream outputStream = new ByteArrayOutputStream()) { + new ObjectMapper().writeValue(outputStream, this); + return outputStream.toString(); + } + } + + @VisibleForTesting + @Nullable + public String getImage() { + return image; + } + + @VisibleForTesting + @Nullable + public String getProject() { + return project; + } +} From 2094f80059bfb350b32a89e4fbeaa9a535d9b0e6 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 7 Jun 2019 14:53:48 -0400 Subject: [PATCH 0614/2020] Update CHANGELOG.md (#1764) * Update CHANGELOG.md --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 0680e97d5b..7c9615e920 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue with `jibBuildTar` where `UP-TO-DATE` checks were incorrect. ([#1757](https://github.com/GoogleContainerTools/jib/issues/1757)) + ## 1.2.0 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 459441d2c8..4d94dd1bca 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue where decyrpting Maven settings `settings.xml` wholesale caused the build to fail. We now decrypt only the parts that are required. ([#1709](https://github.com/GoogleContainerTools/jib/issues/1709)) + ## 1.2.0 ### Added From eaada62b27d3f57c9f57440a9a2884b328665c2c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 7 Jun 2019 15:57:10 -0400 Subject: [PATCH 0615/2020] Core release v0.10.0 (#1767) * [Gradle Release Plugin] - pre tag commit: 'v0.10.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.10.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 7972ba60d5..0340ae781e 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.9.3-SNAPSHOT +version = 0.10.1-SNAPSHOT From 9a39a0f6b6387a7b1b18621ac6a6faea16650e73 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 7 Jun 2019 15:57:23 -0400 Subject: [PATCH 0616/2020] Maven release v1.3.0 (#1768) * preparing release 1.3.0 * 1.3.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 0c906b6cc6..01134b0348 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.2.1-SNAPSHOT + 1.3.1-SNAPSHOT maven-plugin Jib From 1206188c845a4d66797dea18ce3a3a1b45dd0eb8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 7 Jun 2019 15:57:47 -0400 Subject: [PATCH 0617/2020] Gradle release v1.3.0 (#1769) * [Gradle Release Plugin] - pre tag commit: 'v1.3.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.3.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index f0feddb6d2..cfe19d8482 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.2.1-SNAPSHOT +version = 1.3.1-SNAPSHOT From 816626e946fc8cdad6e651fb2e55056f058b6a6a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Sun, 9 Jun 2019 21:05:29 -0400 Subject: [PATCH 0618/2020] Minor fixes (#1765) --- .../cloud/tools/jib/cache/CacheStorageWriter.java | 13 ++++--------- .../tools/jib/maven/MavenProjectProperties.java | 10 +++++----- .../common/logging/ConsoleLoggerBuilder.java | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index a63a3572f3..a427db0c70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -25,13 +25,13 @@ import com.google.cloud.tools.jib.filesystem.LockFile; import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.base.Preconditions; -import com.google.common.io.ByteStreams; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; @@ -101,14 +101,9 @@ private static void moveIfDoesNotExist(Path source, Path destination) throws IOE */ private static DescriptorDigest getDiffIdByDecompressingFile(Path compressedFile) throws IOException { - try (CountingDigestOutputStream diffIdCaptureOutputStream = - new CountingDigestOutputStream(ByteStreams.nullOutputStream())) { - try (InputStream fileInputStream = - new BufferedInputStream(Files.newInputStream(compressedFile)); - GZIPInputStream decompressorStream = new GZIPInputStream(fileInputStream)) { - ByteStreams.copy(decompressorStream, diffIdCaptureOutputStream); - } - return diffIdCaptureOutputStream.computeDigest().getDigest(); + try (InputStream fileInputStream = + new BufferedInputStream(new GZIPInputStream(Files.newInputStream(compressedFile)))) { + return Digests.computeDigest(fileInputStream).getDigest(); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 41e3798ce1..9369a8b244 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -154,24 +154,24 @@ static int getVersionFromString(String versionString) { MavenProjectProperties(MavenProject project, MavenSession session, Log log) { this.project = project; this.session = session; - ConsoleLoggerBuilder logEventHandlerBuilder = + ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(session) ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) .lifecycle(log::info); if (log.isDebugEnabled()) { - logEventHandlerBuilder + consoleLoggerBuilder .debug(log::debug) // INFO messages also go to Log#debug since Log#info is used for LIFECYCLE. .info(log::debug); } if (log.isWarnEnabled()) { - logEventHandlerBuilder.warn(log::warn); + consoleLoggerBuilder.warn(log::warn); } if (log.isErrorEnabled()) { - logEventHandlerBuilder.error(log::error); + consoleLoggerBuilder.error(log::error); } - this.consoleLogger = logEventHandlerBuilder.build(); + this.consoleLogger = consoleLoggerBuilder.build(); } @Override diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java index 820844f462..4254806dd3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java @@ -22,7 +22,7 @@ import java.util.function.Consumer; import java.util.function.Function; -/** Builds a handler for {@link ConsoleLogger}. */ +/** Builds a {@link ConsoleLogger}. */ public class ConsoleLoggerBuilder { /** From d8dc5e2dca3dafdede1a4db812a11292bb701c43 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 10 Jun 2019 10:19:48 -0400 Subject: [PATCH 0619/2020] Update changelogs and version strings for 1.3.0 (#1770) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/ktor/build.gradle.kts | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 6 ++++++ jib-maven-plugin/README.md | 6 +++--- 18 files changed, 38 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9d112fd5b..2302c6f889 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.2.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.3.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.2.0' + // id 'com.google.cloud.tools.jib' version '1.3.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 901dea4661..e3ee297adf 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.2.0 + 1.3.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 9b00c2d1ed..2c16724822 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 1a3c6a2351..8ce33408bf 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.2.0 + 1.3.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 40342a88be..9b195ab9d7 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index b9222c3937..7b18a5b327 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.2.0 + 1.3.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 19b02d7e3d..6e2ac6da23 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "1.2.0" + id("com.google.cloud.tools.jib") version "1.3.0" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 19ecef02e2..c392f4a262 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index f9e74d5596..8d66114303 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.2.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.3.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 3642f06208..03b9023b1f 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.2.0 + 1.3.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index c23ef37cbf..c5fa4d45cd 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 65370f8aef..1ea97766d1 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.2.0 + 1.3.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index d4ca651f9d..3ddad16d1d 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index fa06b5cb7b..ff11aa694a 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.0 + +### Added + - `Containerizer#addEventHandler` for adding event handlers ### Changed @@ -14,8 +22,6 @@ All notable changes to this project will be documented in this file. - Removed multiple classes to simplify the event system (`JibEventType`, `BuildStepType`, `EventDispatcher`, `DefaultEventDispatcher`, `LayerCountEvent`) - MainClassFinder now uses a static method instead of requiring instantiation -### Fixed - ## 0.9.2 ### Added diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7c9615e920..193e7ae8b1 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file. ### Changed +### Fixed + +## 1.3.0 + +### Changed + - Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) ### Fixed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 375936d8f4..e6796c29cd 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.2.0' + id 'com.google.cloud.tools.jib' version '1.3.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4d94dd1bca..c2ae7f727e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file. ### Changed +### Fixed + +## 1.3.0 + +### Changed + - Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) ### Fixed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 1faec2cebe..f81d92aedc 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.2.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.3.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.2.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.3.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.2.0 + 1.3.0 myimage From 46ea7ea7d16f178ca8dbb092405c0d51781c6b7b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 10 Jun 2019 10:31:21 -0400 Subject: [PATCH 0620/2020] Update jib-core readme for 0.10.0 (#1766) --- jib-core/README.md | 46 ++++++++++-------------- jib-core/examples/build.gradle/README.md | 4 +-- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/jib-core/README.md b/jib-core/README.md index 5f1b72fbd0..273eb9e47f 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -1,4 +1,4 @@ -![experimental](https://img.shields.io/badge/stability-experimental-red.svg) +![experimental](https://img.shields.io/badge/stability-beta-orange.svg) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.cloud.tools/jib-core) [![Gitter version](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/google/jib) @@ -6,7 +6,7 @@ Jib Core is a Java library for building Docker and [OCI](https://github.com/opencontainers/image-spec) container images. It implements a general-purpose container builder that can be used to build containers without a Docker daemon, for any application. The implementation is pure Java. -*The API is currently in alpha and may change substantially.* +*The API is currently in beta and may change substantially.* Jib Core powers the popular Jib plugins for Maven and Gradle. The plugins build containers specifically for JVM languages and separate the application into multiple layers to optimize for fast rebuilds.\ For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin).\ @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.9.2 + 0.10.0 ``` @@ -30,11 +30,11 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.9.2' + compile 'com.google.cloud.tools:jib-core:0.10.0' } ``` -## Simple example +## Examples ```java Jib.from("busybox") @@ -46,16 +46,14 @@ Jib.from("busybox") ``` 1. [`Jib.from("busybox")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Jib.html#from-java.lang.String-) creates a new [`JibContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/0.1.0/com/google/cloud/tools/jib/api/JibContainerBuilder.html) configured with [`busybox`](https://hub.docker.com/_/busybox/) as the base image. -1. [`.addLayer(...)`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.filesystem.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. +1. [`.addLayer(...)`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#addLayer-java.util.List-com.google.cloud.tools.jib.api.AbsoluteUnixPath-) configures the `JibContainerBuilder` with a new layer with `helloworld.sh` (local file) to be placed into the container at `/helloworld.sh`. 1. [`.setEntrypoint("sh", "/helloworld.sh")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#setEntrypoint-java.lang.String...-) sets the entrypoint of the container to run `/helloworld.sh`. 1. [`RegistryImage.named("gcr.io/my-project/hello-from-jib")`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#named-java.lang.String-) creates a new [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) configured with `gcr.io/my-project/hello-from-jib` as the target image to push to. -1. [`.addCredential`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.configuration.credentials.CredentialRetriever-). +1. [`.addCredential`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredential-java.lang.String-java.lang.String-) adds the username/password credentials to authenticate the push to `gcr.io/my-project/hello-from-jib`. See [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) for common credential retrievers (to retrieve credentials from Docker config or credential helpers, for example). These credential retrievers an be used with [`.addCredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html#addCredentialRetriever-com.google.cloud.tools.jib.api.CredentialRetriever-). 1. [`Containerizer.to`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#to-com.google.cloud.tools.jib.api.RegistryImage-) creates a new [`Containerizer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html) configured to push to the `RegistryImage`. 1. [`.containerize`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-) executes the containerization. If successful, the container image will be available at `gcr.io/my-project/hello-from-jib`. -## Tutorials - -*None yet available. We welcome contributions for examples and tutorials!* +See [examples](examples/README.md) for links to more jib-core samples. We welcome contributions for additional examples and tutorials! ## API overview @@ -73,15 +71,14 @@ Three `TargetImage` types define the 3 different targets Jib can build to: - [`TarImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive Other useful classes: -- [`ImageReference`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/image/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references -- [`LayerConfiguration`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/configuration/LayerConfiguration.html) - configures a container layer to build -- [`CredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/configuration/credentials/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry +- [`ImageReference`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references +- [`LayerConfiguration`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/LayerConfiguration.html) - configures a container layer to build +- [`CredentialRetriever`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/CredentialRetriever.html) - implement with custom credential retrieval methods for authenticating against a container registry - [`CredentialRetrieverFactory`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.html) - provides useful `CredentialRetriever`s to retrieve credentials from Docker config and credential helpers -- [`EventHandlers`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/EventHandlers.html) - attach event handlers to handle events dispatched during the container build execution Java-specific API: - [`JavaContainerBuilder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JavaContainerBuilder.html) - configures a `JibContainerBuilder` for Java-specific applications -- [`MainClassFinder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/frontend/MainClassFinder.html) - find the main Java class in a given list of class files +- [`MainClassFinder`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/MainClassFinder.html) - find the main Java class in a given list of class files ## API reference @@ -98,33 +95,28 @@ The Jib Core system consists 3 main parts: Some other parts of Jib Core internals include: - a caching mechanism to speed up builds (configurable with [`Containerizer.setApplicationLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setApplicationLayersCache-java.nio.file.Path-) and [`Containerizer.setBaseImageLayersCache`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setBaseImageLayersCache-java.nio.file.Path-)) -- an [eventing system](#events) to react to events from Jib Core during its execution (add handlers with [`Containerizer.setEventHandlers`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#setEventHandlers-com.google.cloud.tools.jib.event.EventHandlers-)) +- an [eventing system](#events) to react to events from Jib Core during its execution (add handlers with [`Containerizer.addEventHandler`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/Containerizer.html#addEventHandler-java.lang.Class-java.util.function.Consumer-)) - support for fully-concurrent multi-threaded executions ## Events -Throughout the build process, Jib Core dispatches events that provide useful information. These events implement the type [`JibEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/JibEvent.html), and can be handled by registering event handlers with the event dispatcher that is passed to Jib. +Throughout the build process, Jib Core dispatches events that provide useful information. These events implement the type [`JibEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibEvent.html), and can be handled by registering event handlers with the containerizer. ```java -// Setup event handlers -EventHandlers eventHandlers = new EventHandlers() - .add(JibEventType.LOGGING, logEvent -> System.out.println(logEvent.getLevel() + ": " + logEvent.getMessage())) - .add(JibEventType.TIMING, timeEvent -> ...); - -// Register with Jib Jib.from(...) ... .containerize( Containerizer.to(...) ... - .setEventHandlers(eventHandlers)); + .addEventHandler(LogEvent.class, logEvent -> System.out.println(logEvent.getLevel() + ": " + logEvent.getMessage()) + .addEventHandler(TimerEvent.class, timeEvent -> ...)); ``` When Jib dispatches events, the event handlers you defined for that event type will be called. The following are the types of events you can listen for in Jib core (see [API reference](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/package-summary.html) for more information): -- [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. -- [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. -- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) - Indicates the amount of progress build steps have made. Each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. +- [`LogEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/LogEvent.html) - Log message events. The message and verbosity can be retrieved using `getMessage()` and `getLevel()`, respectively. +- [`TimerEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/TimerEvent.html) (*Incubating*) - Events used for measuring how long different build steps take. You can retrieve the duration since the timer's creation and the duration since the same timer's previous event using `getElapsed()` and `getDuration()`, respectively. +- [`ProgressEvent`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/event/events/ProgressEvent.html) (*Incubating*) - Indicates the amount of progress build steps have made. Each progress event consists of an allocation (containing a fraction representing how much of the root allocation this allocation accounts for) and a number of progress units that indicates the amount of work completed since the previous progress event. In other words, the amount of work a single progress event has completed (out of 1.0) can be calculated using `getAllocation().getFractionOfRoot() * getUnits()`. ## Frequently Asked Questions (FAQ) diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index fac94cab5c..b881eff264 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,15 +13,15 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.9.2' + classpath 'com.google.cloud.tools:jib-core:0.10.0' } } import com.google.cloud.tools.jib.api.Jib import com.google.cloud.tools.jib.api.Containerizer +import com.google.cloud.tools.jib.api.ImageReference import com.google.cloud.tools.jib.api.RegistryImage import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory -import com.google.cloud.tools.jib.image.ImageReference // Creates a task called 'dojib'. task('dojib') { From fd7d4b275bf2cb2ac856634b0b2865d3d6d62002 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 14 Jun 2019 11:23:31 -0400 Subject: [PATCH 0621/2020] Remove Kokoro publish and sign scripts (#1775) --- jib-core/kokoro/release_publish.sh | 67 ---------------------- jib-core/kokoro/release_sign.sh | 35 ----------- jib-maven-plugin/kokoro/release_publish.sh | 65 --------------------- jib-maven-plugin/kokoro/release_sign.sh | 35 ----------- 4 files changed, 202 deletions(-) delete mode 100644 jib-core/kokoro/release_publish.sh delete mode 100644 jib-core/kokoro/release_sign.sh delete mode 100644 jib-maven-plugin/kokoro/release_publish.sh delete mode 100644 jib-maven-plugin/kokoro/release_sign.sh diff --git a/jib-core/kokoro/release_publish.sh b/jib-core/kokoro/release_publish.sh deleted file mode 100644 index 48f0ed6017..0000000000 --- a/jib-core/kokoro/release_publish.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e - -SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/72743_sonatype_username) -SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/72743_sonatype_password) -CREDENTIALS="$SONATYPE_USERNAME:$SONATYPE_PASSWORD" - -# Display commands to stderr. -set -x - -# Goes to the GCS directory. -cd ${KOKORO_GFILE_DIR} - -# Finds the latest ubuntu/release-sign build directory. -LAST_BUILD=$(ls prod/jib/core/ubuntu/release-sign/ | sort -rV | head -1) - -# Finds the bundled jar file in the latest signed artifact directory. -BUNDLED_JAR_FILE=$(find `pwd`/prod/jib/core/ubuntu/release-sign/${LAST_BUILD}/* -type f \( -iname \*-bundle.jar \)) - -# Usage: GetSessionID -# Uses the environment variable CREDENTIALS. -# Stores the Nexus session ID in the given variable. -GetSessionID() { - local __resultvar=$1 - - # Makes a temporary file to store the login cookies. - local cookies_temp=$(mktemp /tmp/sonatype_cookies.XXXXXXX) - - # Sends a login request. - { local login_response=$(curl 'https://oss.sonatype.org/service/local/authentication/login' -X 'GET' -u "$CREDENTIALS" -c ${cookies_temp} 2> /dev/null); } 2> /dev/null - - # Checks if login was successful. - echo ${login_response} | grep -q 'true' - - local login_check=$(echo -n $?) - - if [ "$login_check" -eq "1" ]; then - return 1 - fi - - # Extracts the session ID from the cookies. - local nxsessionid - local nxsessionid_line=$(cat ${cookies_temp} | grep 'NXSESSIONID') - nxsessionid=$(echo -n ${nxsessionid_line} | awk '{print $NF}') - - eval ${__resultvar}="'$nxsessionid'" -} - -# Usage: UploadJAR -# Uploads the bundled JAR file to the Nexus Staging Repository. -UploadJAR() { - curl 'https://oss.sonatype.org/service/local/staging/bundle_upload' -H "Cookie: NXSESSIONID=$1" -H 'Content-Type: multipart/form-data' --compressed -F "file=@$2" -} - -# Gets the session ID. -GetSessionID NXSESSIONID -if [ $? -eq 1 ]; then - echo 'Login failed!' - exit 1 -fi -echo 'Login successful.' - -# Uploads the bundled JAR file. -echo 'Uploading artifact...' -UploadJAR ${NXSESSIONID} ${BUNDLED_JAR_FILE} diff --git a/jib-core/kokoro/release_sign.sh b/jib-core/kokoro/release_sign.sh deleted file mode 100644 index f858f04331..0000000000 --- a/jib-core/kokoro/release_sign.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e -# Display commands being run. -set -x - -cd ${KOKORO_GFILE_DIR} -mkdir signed && chmod 777 signed - -# find the latest directory under prod/jib/core/gcp_ubuntu/release-build/ -LAST_BUILD=$(ls prod/jib/core/gcp_ubuntu/release-build/ | sort -rV | head -1) - -# find the jars and the pom in the latest build artifact directory -FILES=$(find `pwd`/prod/jib/core/gcp_ubuntu/release-build/${LAST_BUILD}/* -type f \( -iname \*.jar -o -iname \*.pom \)) - -for f in ${FILES} -do - echo "Processing $f file..." - filename=$(basename "$f") - mv ${f} signed/$filename - if /escalated_sign/escalated_sign.py -j /escalated_sign_jobs -t linux_gpg_sign \ - `pwd`/signed/${filename} - then echo "Signed $filename" - else - echo "Could not sign $filename" - exit 1 - fi -done - -# bundle the artifacts for manual deploy to the Maven staging repository -cd signed -POM_NAME=$(ls *.pom) -BUNDLE_NAME=${POM_NAME%.pom}-bundle.jar -jar -cvf ${BUNDLE_NAME} * diff --git a/jib-maven-plugin/kokoro/release_publish.sh b/jib-maven-plugin/kokoro/release_publish.sh deleted file mode 100644 index 1243c4b313..0000000000 --- a/jib-maven-plugin/kokoro/release_publish.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e - -CREDENTIALS="$SONATYPE_USERNAME:$SONATYPE_PASSWORD" - -# Display commands to stderr. -set -x - -# Goes to the GCS directory. -cd $KOKORO_GFILE_DIR - -# Finds the latest ubuntu/release-sign build directory. -LAST_BUILD=$(ls prod/jib/maven/ubuntu/release-sign/ | sort -rV | head -1) - -# Finds the bundled jar file in the latest signed artifact directory. -BUNDLED_JAR_FILE=$(find `pwd`/prod/jib/maven/ubuntu/release-sign/${LAST_BUILD}/* -type f \( -iname \*-bundle.jar \)) - -# Usage: GetSessionID -# Uses the environment variable CREDENTIALS. -# Stores the Nexus session ID in the given variable. -GetSessionID() { - local __resultvar=$1 - - # Makes a temporary file to store the login cookies. - local cookies_temp=$(mktemp /tmp/sonatype_cookies.XXXXXXX) - - # Sends a login request. - { local login_response=$(curl 'https://oss.sonatype.org/service/local/authentication/login' -X 'GET' -u "$CREDENTIALS" -c $cookies_temp 2> /dev/null); } 2> /dev/null - - # Checks if login was successful. - echo $login_response | grep -q 'true' - - local login_check=$(echo -n $?) - - if [ "$login_check" -eq "1" ]; then - return 1 - fi - - # Extracts the session ID from the cookies. - local nxsessionid - local nxsessionid_line=$(cat $cookies_temp | grep 'NXSESSIONID') - nxsessionid=$(echo -n $nxsessionid_line | awk '{print $NF}') - - eval $__resultvar="'$nxsessionid'" -} - -# Usage: UploadJAR -# Uploads the bundled JAR file to the Nexus Staging Repository. -UploadJAR() { - curl 'https://oss.sonatype.org/service/local/staging/bundle_upload' -H "Cookie: NXSESSIONID=$1" -H 'Content-Type: multipart/form-data' --compressed -F "file=@$2" -} - -# Gets the session ID. -GetSessionID NXSESSIONID -if [ $? -eq 1 ]; then - echo 'Login failed!' - exit 1 -fi -echo 'Login successful.' - -# Uploads the bundled JAR file. -echo 'Uploading artifact...' -UploadJAR $NXSESSIONID $BUNDLED_JAR_FILE diff --git a/jib-maven-plugin/kokoro/release_sign.sh b/jib-maven-plugin/kokoro/release_sign.sh deleted file mode 100644 index 093305378d..0000000000 --- a/jib-maven-plugin/kokoro/release_sign.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# Fail on any error. -set -e -# Display commands being run. -set -x - -cd $KOKORO_GFILE_DIR -mkdir signed && chmod 777 signed - -# find the latest directory under prod/jib/maven/gcp_ubuntu/release-build/ -LAST_BUILD=$(ls prod/jib/maven/gcp_ubuntu/release-build/ | sort -rV | head -1) - -# find the jars and the pom in the latest build artifact directory -FILES=$(find `pwd`/prod/jib/maven/gcp_ubuntu/release-build/${LAST_BUILD}/* -type f \( -iname \*.jar -o -iname \*.pom \)) - -for f in $FILES -do - echo "Processing $f file..." - filename=$(basename "$f") - mv $f signed/$filename - if /escalated_sign/escalated_sign.py -j /escalated_sign_jobs -t linux_gpg_sign \ - `pwd`/signed/$filename - then echo "Signed $filename" - else - echo "Could not sign $filename" - exit 1 - fi -done - -# bundle the artifacts for manual deploy to the Maven staging repository -cd signed -POM_NAME=$(ls *.pom) -BUNDLE_NAME=${POM_NAME%.pom}-bundle.jar -jar -cvf ${BUNDLE_NAME} * From 1267fee3edc72db2c5e984ddc5cad7a52a5a9797 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 14 Jun 2019 16:12:02 -0400 Subject: [PATCH 0622/2020] Update JavaContainerBuilder for multiple dep layers (#1773) * Update JavaContainerBuilder for multiple dep layers * Update plugins to match previous behavior --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JavaContainerBuilder.java | 127 ++++++++++++++---- .../jib/api/JavaContainerBuilderTest.java | 22 ++- .../jib/gradle/GradleProjectProperties.java | 15 ++- .../jib/maven/MavenProjectProperties.java | 9 ++ .../common/JavaContainerBuilderHelper.java | 11 +- 6 files changed, 153 insertions(+), 33 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index ff11aa694a..9e336804b2 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- `JibContainerBuilder#addDependencies` is now split into three methods: `addDependencies`, `addSnapshotDependencies`, `addProjectDependencies` ([#1773](https://github.com/GoogleContainerTools/jib/pull/1773)) + ### Fixed ## 0.10.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 12d7f7fbff..e161b30378 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -19,6 +19,9 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Streams; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -55,6 +58,7 @@ private PathPredicatePair(Path path, Predicate predicate) { public enum LayerType { DEPENDENCIES("dependencies"), SNAPSHOT_DEPENDENCIES("snapshot dependencies"), + PROJECT_DEPENDENCIES("project dependencies"), RESOURCES("resources"), CLASSES("classes"), EXTRA_FILES("extra files"); @@ -161,6 +165,8 @@ public static JavaContainerBuilder from(RegistryImage registryImage) { private final List addedResources = new ArrayList<>(); private final List addedClasses = new ArrayList<>(); private final List addedDependencies = new ArrayList<>(); + private final List addedSnapshotDependencies = new ArrayList<>(); + private final List addedProjectDependencies = new ArrayList<>(); private final List addedOthers = new ArrayList<>(); private AbsoluteUnixPath appRoot = AbsoluteUnixPath.get("/app"); @@ -244,8 +250,8 @@ public JavaContainerBuilder setOthersDestination(RelativeUnixPath othersDestinat } /** - * Adds dependency JARs to the image. Duplicate JAR filenames are renamed with the filesize in - * order to avoid collisions. + * Adds dependency JARs to the image. Duplicate JAR filenames across all dependencies are renamed + * with the filesize in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -264,8 +270,8 @@ public JavaContainerBuilder addDependencies(List dependencyFiles) throws I } /** - * Adds dependency JARs to the image. Duplicate JAR filenames are renamed with the filesize in - * order to avoid collisions. + * Adds dependency JARs to the image. Duplicate JAR filenames across all dependencies are renamed + * with the filesize in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -275,6 +281,74 @@ public JavaContainerBuilder addDependencies(Path... dependencyFiles) throws IOEx return addDependencies(Arrays.asList(dependencyFiles)); } + /** + * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are + * renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addSnapshotDependencies(List dependencyFiles) + throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + addedSnapshotDependencies.addAll(dependencyFiles); + classpathOrder.add(LayerType.DEPENDENCIES); // this is a single classpath entry with all deps + return this; + } + + /** + * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are + * renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addSnapshotDependencies(Path... dependencyFiles) throws IOException { + return addSnapshotDependencies(Arrays.asList(dependencyFiles)); + } + + /** + * Adds project dependency JARs to the image. Generally, project dependency are jars produced from + * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across + * all dependencies are renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addProjectDependencies(List dependencyFiles) + throws IOException { + // Make sure all files exist before adding any + for (Path file : dependencyFiles) { + if (!Files.exists(file)) { + throw new NoSuchFileException(file.toString()); + } + } + addedProjectDependencies.addAll(dependencyFiles); + classpathOrder.add(LayerType.DEPENDENCIES); // this is a single classpath entry with all deps + return this; + } + + /** + * Adds project dependency JARs to the image. Generally, project dependency are jars produced from + * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across + * all dependencies are renamed with the filesize in order to avoid collisions. + * + * @param dependencyFiles the list of dependency JARs to add to the image + * @return this + * @throws IOException if adding the layer fails + */ + public JavaContainerBuilder addProjectDependencies(Path... dependencyFiles) throws IOException { + return addProjectDependencies(Arrays.asList(dependencyFiles)); + } + /** * Adds the contents of a resources directory to the image. * @@ -453,10 +527,12 @@ public JibContainerBuilder toContainerBuilder() throws IOException { appRoot.resolve(resourcesDestination)); } - // Detect duplicate filenames and rename with filesize to avoid collisions + // Detect duplicate filenames across all layer types List duplicates = - addedDependencies - .stream() + Streams.concat( + addedDependencies.stream(), + addedSnapshotDependencies.stream(), + addedProjectDependencies.stream()) .map(Path::getFileName) .map(Path::toString) .collect(Collectors.groupingBy(filename -> filename, Collectors.counting())) @@ -465,23 +541,26 @@ public JibContainerBuilder toContainerBuilder() throws IOException { .filter(entry -> entry.getValue() > 1) .map(Entry::getKey) .collect(Collectors.toList()); - for (Path file : addedDependencies) { - // Add dependencies to layer configuration - addFileToLayer( - layerBuilders, - file.getFileName().toString().contains("SNAPSHOT") - ? LayerType.SNAPSHOT_DEPENDENCIES - : LayerType.DEPENDENCIES, - file, - appRoot - .resolve(dependenciesDestination) - .resolve( - duplicates.contains(file.getFileName().toString()) - ? file.getFileName() - .toString() - .replaceFirst("\\.jar$", "-" + Files.size(file)) - + ".jar" - : file.getFileName().toString())); + + ImmutableMap> layerMap = + ImmutableMap.of( + LayerType.DEPENDENCIES, addedDependencies, + LayerType.SNAPSHOT_DEPENDENCIES, addedSnapshotDependencies, + LayerType.PROJECT_DEPENDENCIES, addedProjectDependencies); + for (LayerType layerType : layerMap.keySet()) { + for (Path file : Preconditions.checkNotNull(layerMap.get(layerType))) { + // handle duplicates by appending filesize to the end of the file + String jarName = file.getFileName().toString(); + if (duplicates.contains(jarName)) { + jarName = jarName.replaceFirst("\\.jar$", "-" + Files.size(file)) + ".jar"; + } + // Add dependencies to layer configuration + addFileToLayer( + layerBuilders, + layerType, + file, + appRoot.resolve(dependenciesDestination).resolve(jarName)); + } } // Add others to layer configuration diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index c37346738e..dcd54dd85b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -67,10 +67,12 @@ public void testToJibContainerBuilder_all() .addClasses(getResource("core/application/classes")) .addDependencies( getResource("core/application/dependencies/dependency-1.0.0.jar"), - getResource("core/application/dependencies/more/dependency-1.0.0.jar"), - getResource("core/application/dependencies/libraryA.jar"), - getResource("core/application/dependencies/libraryB.jar"), + getResource("core/application/dependencies/more/dependency-1.0.0.jar")) + .addSnapshotDependencies( getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) + .addProjectDependencies( + getResource("core/application/dependencies/libraryA.jar"), + getResource("core/application/dependencies/libraryB.jar")) .addToClasspath(getResource("core/fileA"), getResource("core/fileB")) .setClassesDestination(RelativeUnixPath.get("different-classes")) .setResourcesDestination(RelativeUnixPath.get("different-resources")) @@ -100,9 +102,7 @@ public void testToJibContainerBuilder_all() List expectedDependencies = ImmutableList.of( AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-770.jar"), - AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar"), - AbsoluteUnixPath.get("/hello/different-libs/libraryA.jar"), - AbsoluteUnixPath.get("/hello/different-libs/libraryB.jar")); + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar")); Assert.assertEquals( expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); @@ -114,6 +114,14 @@ public void testToJibContainerBuilder_all() expectedSnapshotDependencies, getExtractionPaths(buildConfiguration, "snapshot dependencies")); + List expectedProjectDependencies = + ImmutableList.of( + AbsoluteUnixPath.get("/hello/different-libs/libraryA.jar"), + AbsoluteUnixPath.get("/hello/different-libs/libraryB.jar")); + Assert.assertEquals( + expectedProjectDependencies, + getExtractionPaths(buildConfiguration, "project dependencies")); + // Check resources List expectedResources = ImmutableList.of( @@ -145,7 +153,7 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() JavaContainerBuilder.fromDistroless() .addDependencies(getResource("core/application/dependencies/libraryA.jar")) .addDependencies(getResource("core/application/dependencies/libraryB.jar")) - .addDependencies( + .addSnapshotDependencies( getResource("core/application/snapshot-dependencies/dependency-1.0.0-SNAPSHOT.jar")) .addClasses(getResource("core/application/classes/")) .addClasses(getResource("core/class-finder-tests/extension")) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 8ad2984979..7a48ffd400 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -144,11 +144,16 @@ public JibContainerBuilder createContainerBuilder( mainSourceSet.getOutput().getClassesDirs().filter(File::exists); Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); - FileCollection dependencyFiles = + + FileCollection allDependencyFiles = allFiles .minus(classesOutputDirectories) .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); + FileCollection snapshotDependencyFiles = + allDependencyFiles.filter(file -> file.getName().contains("SNAPSHOT")); + FileCollection dependencyFiles = allDependencyFiles.minus(snapshotDependencyFiles); + JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); @@ -174,6 +179,14 @@ public JibContainerBuilder createContainerBuilder( .map(File::toPath) .collect(Collectors.toList())); + javaContainerBuilder.addSnapshotDependencies( + snapshotDependencyFiles + .getFiles() + .stream() + .filter(File::exists) + .map(File::toPath) + .collect(Collectors.toList())); + return javaContainerBuilder.toContainerBuilder(); } catch (IOException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 9369a8b244..8c6b94654e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -196,6 +196,15 @@ public JibContainerBuilder createContainerBuilder( project .getArtifacts() .stream() + .filter(artifact -> !artifact.isSnapshot()) + .map(Artifact::getFile) + .map(File::toPath) + .collect(Collectors.toList())) + .addSnapshotDependencies( + project + .getArtifacts() + .stream() + .filter(Artifact::isSnapshot) .map(Artifact::getFile) .map(File::toPath) .collect(Collectors.toList())) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index ed87744e59..08e2a1afe9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -95,7 +95,16 @@ public static JibContainerBuilder fromExplodedWar( javaContainerBuilder.addClasses(webInfClasses, isClassFile); } if (Files.exists(webInfLib)) { - javaContainerBuilder.addDependencies(new DirectoryWalker(webInfLib).filterRoot().walk()); + javaContainerBuilder.addDependencies( + new DirectoryWalker(webInfLib) + .filterRoot() + .filter(path -> !path.getFileName().toString().contains("SNAPSHOT")) + .walk()); + javaContainerBuilder.addSnapshotDependencies( + new DirectoryWalker(webInfLib) + .filterRoot() + .filter(path -> path.getFileName().toString().contains("SNAPSHOT")) + .walk()); } return javaContainerBuilder.toContainerBuilder(); } From efbe9c3dae06c3f4df1df1264d5715a3b667f8b3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Sun, 16 Jun 2019 23:02:22 -0400 Subject: [PATCH 0623/2020] Add Maven support for containerizing packaged JAR (#1746) * Add containerizingMode config * Specialize error message on mode / refactor code * Target maven-jar-plugin's JAR path * Error on "packaged" mode with WAR * Do not use context classloader --- .../src/main/java/example/HelloWorld.java | 16 +- .../tools/jib/gradle/BuildDockerTask.java | 5 + .../tools/jib/gradle/BuildImageTask.java | 5 + .../cloud/tools/jib/gradle/BuildTarTask.java | 5 + .../jib/gradle/GradleProjectProperties.java | 40 ++--- .../jib/gradle/GradleRawConfiguration.java | 5 + .../cloud/tools/jib/gradle/JibExtension.java | 13 +- .../gradle/GradleProjectPropertiesTest.java | 12 +- .../tools/jib/gradle/JibExtensionTest.java | 8 + .../tools/jib/maven/BuildDockerMojo.java | 5 + .../cloud/tools/jib/maven/BuildImageMojo.java | 5 + .../cloud/tools/jib/maven/BuildTarMojo.java | 5 + .../jib/maven/JibPluginConfiguration.java | 21 ++- .../jib/maven/MavenProjectProperties.java | 78 ++++++--- .../jib/maven/MavenRawConfiguration.java | 5 + .../maven/BuildDockerMojoIntegrationTest.java | 33 ++++ .../jib/maven/JibPluginConfigurationTest.java | 5 + .../jib/maven/MavenProjectPropertiesTest.java | 161 +++++++++++++----- .../simple/pom-jar-containerization.xml | 63 +++++++ .../src/main/java/com/test/HelloWorld.java | 68 +++++--- .../plugins/common/ContainerizingMode.java | 30 ++++ .../InvalidContainerizingModeException.java | 32 ++++ .../common/JavaContainerBuilderHelper.java | 17 +- .../common/PluginConfigurationProcessor.java | 61 +++++-- .../jib/plugins/common/ProjectProperties.java | 4 +- .../jib/plugins/common/PropertyNames.java | 1 + .../jib/plugins/common/RawConfiguration.java | 2 + .../JavaContainerBuilderHelperTest.java | 7 +- .../PluginConfigurationProcessorTest.java | 127 ++++++++++++-- 29 files changed, 659 insertions(+), 180 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-jar-containerization.xml create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerizingModeException.java diff --git a/examples/helloworld/src/main/java/example/HelloWorld.java b/examples/helloworld/src/main/java/example/HelloWorld.java index e4185f27f7..89bd1f67d5 100644 --- a/examples/helloworld/src/main/java/example/HelloWorld.java +++ b/examples/helloworld/src/main/java/example/HelloWorld.java @@ -16,21 +16,21 @@ package example; -import com.google.common.io.Resources; +import com.google.common.io.CharStreams; +import java.io.InputStreamReader; import java.io.IOException; +import java.io.Reader; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; public class HelloWorld { public static void main(String[] args) throws URISyntaxException, IOException { - Path worldFile = Paths.get(Resources.getResource("world").toURI()); - String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); - - System.out.println("Hello " + world); + try (Reader reader = new InputStreamReader( + HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8)) { + String world = CharStreams.toString(reader); + System.out.println("Hello " + world); + } } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index f3ada00b5e..bc636f872c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -149,6 +150,10 @@ public void buildDocker() throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index b1e5213997..0a31bd08f7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -129,6 +130,10 @@ public void buildImage() throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index ef5f7c9015..5cfeaeac80 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -147,6 +148,10 @@ public void buildTar() throw new GradleException( "container.appRoot is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new GradleException( + "invalid value for containerizingMode: " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new GradleException( "container.workingDirectory is not an absolute Unix-style path: " diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 7a48ffd400..275f159ad4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -127,12 +128,15 @@ private static boolean isProgressFooterEnabled(Project project) { @Override public JibContainerBuilder createContainerBuilder( - RegistryImage baseImage, AbsoluteUnixPath appRoot) { + RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); + try { if (isWarProject()) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); - return JavaContainerBuilderHelper.fromExplodedWar(baseImage, explodedWarPath, appRoot); + return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); } JavaPluginConvention javaPluginConvention = @@ -143,7 +147,7 @@ public JibContainerBuilder createContainerBuilder( FileCollection classesOutputDirectories = mainSourceSet.getOutput().getClassesDirs().filter(File::exists); Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); - FileCollection allFiles = mainSourceSet.getRuntimeClasspath(); + FileCollection allFiles = mainSourceSet.getRuntimeClasspath().filter(File::exists); FileCollection allDependencyFiles = allFiles @@ -154,9 +158,6 @@ public JibContainerBuilder createContainerBuilder( allDependencyFiles.filter(file -> file.getName().contains("SNAPSHOT")); FileCollection dependencyFiles = allDependencyFiles.minus(snapshotDependencyFiles); - JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); - // Adds resource files if (Files.exists(resourcesOutputDirectory)) { javaContainerBuilder.addResources(resourcesOutputDirectory); @@ -171,23 +172,16 @@ public JibContainerBuilder createContainerBuilder( } // Adds dependency files - javaContainerBuilder.addDependencies( - dependencyFiles - .getFiles() - .stream() - .filter(File::exists) - .map(File::toPath) - .collect(Collectors.toList())); - - javaContainerBuilder.addSnapshotDependencies( - snapshotDependencyFiles - .getFiles() - .stream() - .filter(File::exists) - .map(File::toPath) - .collect(Collectors.toList())); - - return javaContainerBuilder.toContainerBuilder(); + return javaContainerBuilder + .addDependencies( + dependencyFiles.getFiles().stream().map(File::toPath).collect(Collectors.toList())) + .addSnapshotDependencies( + snapshotDependencyFiles + .getFiles() + .stream() + .map(File::toPath) + .collect(Collectors.toList())) + .toContainerBuilder(); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 9b80a3c7b0..4f7900eb24 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -159,4 +159,9 @@ public List getExtraDirectories() { public Map getExtraDirectoryPermissions() { return TaskCommon.convertPermissionsMap(jibExtension.getExtraDirectories().getPermissions()); } + + @Override + public String getContainerizingMode() { + return jibExtension.getContainerizingMode(); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index d7760e0004..119d4dc73d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -64,12 +64,14 @@ public class JibExtension { // Defines default configuration values. private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; + private static final String DEFAULT_CONTAINERIZING_MODE = "exploded"; private final BaseImageParameters from; private final TargetImageParameters to; private final ContainerParameters container; private final ExtraDirectoriesParameters extraDirectories; private final Property allowInsecureRegistries; + private final Property containerizingMode; @Deprecated boolean extraDirectoryConfigured; @Deprecated boolean extraDirectoriesConfigured; @@ -83,9 +85,11 @@ public JibExtension(Project project) { extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project, this); allowInsecureRegistries = objectFactory.property(Boolean.class); + containerizingMode = objectFactory.property(String.class); // Sets defaults. allowInsecureRegistries.set(DEFAULT_ALLOW_INSECURE_REGISTIRIES); + containerizingMode.set(DEFAULT_CONTAINERIZING_MODE); } public void from(Action action) { @@ -115,7 +119,7 @@ public void extraDirectories(Action action) // for the deprecated "jib.extraDirectory" config parameter public void setExtraDirectory(File extraDirectory) { extraDirectoryConfigured = true; - this.extraDirectories.setPath(extraDirectory); + extraDirectories.setPath(extraDirectory); } public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { @@ -161,4 +165,11 @@ boolean getAllowInsecureRegistries() { } return allowInsecureRegistries.get(); } + + @Input + @Optional + public String getContainerizingMode() { + String property = System.getProperty(PropertyNames.CONTAINERIZING_MODE); + return property != null ? property : containerizingMode.get(); + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index d6b50fbf3b..b9aedffaf3 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -84,6 +85,8 @@ @RunWith(MockitoJUnitRunner.class) public class GradleProjectPropertiesTest { + private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; + /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ private static class TestFileCollection extends AbstractFileCollection { @@ -361,7 +364,9 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); gradleProjectProperties.createContainerBuilder( - RegistryImage.named("base"), AbsoluteUnixPath.get("/anything")); + RegistryImage.named("base"), + AbsoluteUnixPath.get("/anything"), + DEFAULT_CONTAINERIZING_MODE); Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } @@ -552,7 +557,10 @@ private BuildConfiguration setupBuildConfiguration(String appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JibContainerBuilder jibContainerBuilder = new GradleProjectProperties(mockProject, mockLogger) - .createContainerBuilder(RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot)); + .createContainerBuilder( + RegistryImage.named("base"), + AbsoluteUnixPath.get(appRoot), + DEFAULT_CONTAINERIZING_MODE); return JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, Containerizer.to(RegistryImage.named("to")) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 25313dcc25..2a12108c88 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -54,6 +54,7 @@ private static void clearProperties() { System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); System.clearProperty("jib.container.user"); + System.clearProperty("jib.containerizingMode"); System.clearProperty("jib.extraDirectory.path"); System.clearProperty("jib.extraDirectory.permissions"); System.clearProperty("jib.extraDirectories.paths"); @@ -152,6 +153,11 @@ public void testContainer() { Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); } + @Test + public void testContainerizingMode() { + Assert.assertEquals("exploded", testJibExtension.getContainerizingMode()); + } + @Test public void testExtraDirectories_default() { Assert.assertEquals( @@ -288,6 +294,8 @@ public void testProperties() { Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); + System.setProperty("jib.containerizingMode", "packaged"); + Assert.assertEquals("packaged", testJibExtension.getContainerizingMode()); System.setProperty("jib.extraDirectories.paths", "/foo,/bar/baz"); Assert.assertEquals( Arrays.asList(Paths.get("/foo"), Paths.get("/bar/baz")), diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 8c4c0fe95d..69dcc1be1f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -146,6 +147,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new MojoExecutionException( + "invalid value for : " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 99877554e6..e2b19894b1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -140,6 +141,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new MojoExecutionException( + "invalid value for : " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index ac6b4d5455..cf08df5f43 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -118,6 +119,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { " is not an absolute Unix-style path: " + ex.getInvalidPathValue(), ex); + } catch (InvalidContainerizingModeException ex) { + throw new MojoExecutionException( + "invalid value for : " + ex.getInvalidContainerizingMode(), ex); + } catch (InvalidWorkingDirectoryException ex) { throw new MojoExecutionException( " is not an absolute Unix-style path: " diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index b7986049ae..4572eb7c5a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -171,9 +171,7 @@ public static class ContainerParameters { @Nullable @Parameter private List args; - @Nullable - @Parameter(required = true) - private String format = "Docker"; + @Parameter private String format = "Docker"; @Parameter private List ports = Collections.emptyList(); @@ -244,13 +242,13 @@ public List getPaths() { // this parameter is cloned in FilesMojo @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); - @Parameter( - defaultValue = "false", - required = true, - property = PropertyNames.ALLOW_INSECURE_REGISTRIES) + @Parameter(property = PropertyNames.ALLOW_INSECURE_REGISTRIES) private boolean allowInsecureRegistries; - @Parameter(defaultValue = "false", property = PropertyNames.SKIP) + @Parameter(property = PropertyNames.CONTAINERIZING_MODE) + private String containerizingMode = "exploded"; + + @Parameter(property = PropertyNames.SKIP) private boolean skip; @Component protected SettingsDecrypter settingsDecrypter; @@ -533,7 +531,7 @@ String getFormat() { if (property != null) { return property; } - return Preconditions.checkNotNull(container.format); + return container.format; } /** @@ -623,6 +621,11 @@ boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } + public String getContainerizingMode() { + String property = getProperty(PropertyNames.CONTAINERIZING_MODE); + return property != null ? property : containerizingMode; + } + boolean isSkipped() { return skip; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 8c6b94654e..866ea4b3e2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -40,6 +41,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -176,45 +178,67 @@ static int getVersionFromString(String versionString) { @Override public JibContainerBuilder createContainerBuilder( - RegistryImage baseImage, AbsoluteUnixPath appRoot) throws IOException { + RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) + throws IOException { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); + try { if (isWarProject()) { Path explodedWarPath = - Paths.get(project.getBuild().getDirectory()).resolve(project.getBuild().getFinalName()); - return JavaContainerBuilderHelper.fromExplodedWar(baseImage, explodedWarPath, appRoot); + Paths.get(project.getBuild().getDirectory(), project.getBuild().getFinalName()); + return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); } - Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - - // Add dependencies, resources, and classes - return JavaContainerBuilder.from(baseImage) - .setAppRoot(appRoot) - .addResources(classesOutputDirectory, isClassFile.negate()) - .addClasses(classesOutputDirectory, isClassFile) + // Add dependencies + Set allDependencies = project.getArtifacts(); + javaContainerBuilder .addDependencies( - project - .getArtifacts() + allDependencies .stream() .filter(artifact -> !artifact.isSnapshot()) .map(Artifact::getFile) .map(File::toPath) .collect(Collectors.toList())) .addSnapshotDependencies( - project - .getArtifacts() + allDependencies .stream() .filter(Artifact::isSnapshot) .map(Artifact::getFile) .map(File::toPath) - .collect(Collectors.toList())) - .toContainerBuilder(); + .collect(Collectors.toList())); + + switch (containerizingMode) { + case EXPLODED: + // Add resources, and classes + Path classesOutputDirectory = Paths.get(project.getBuild().getOutputDirectory()); + // Don't use Path.endsWith(), since Path works on path elements. + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + javaContainerBuilder + .addResources(classesOutputDirectory, isClassFile.negate()) + .addClasses(classesOutputDirectory, isClassFile); + break; + + case PACKAGED: + // Add a JAR + javaContainerBuilder.addToClasspath(getJarArtifact()); + break; + + default: + throw new IllegalStateException("unknown containerizing mode: " + containerizingMode); + } + + return javaContainerBuilder.toContainerBuilder(); } catch (IOException ex) { throw new IOException( - "Obtaining project build output files failed; make sure you have compiled your project " + "Obtaining project build output files failed; make sure you have " + + (containerizingMode == ContainerizingMode.PACKAGED ? "packaged" : "compiled") + + " your project " + "before trying to build the image. (Did you accidentally run \"mvn clean " - + "jib:build\" instead of \"mvn clean compile jib:build\"?)", + + "jib:build\" instead of \"mvn clean " + + (containerizingMode == ContainerizingMode.PACKAGED ? "package" : "compile") + + " jib:build\"?)", ex); } } @@ -351,4 +375,20 @@ public int getMajorJavaVersion() { public boolean isOffline() { return session.isOffline(); } + + /** + * Gets the path of the JAR that the Maven JAR Plugin would generate. + * + *

https://maven.apache.org/plugins/maven-jar-plugin/jar-mojo.html + * https://github.com/apache/maven-jar-plugin/blob/80f58a84aacff6e671f5a601d62a3a3800b507dc/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java#L177 + * + * @return the path of the JAR + */ + @VisibleForTesting + Path getJarArtifact() { + // TODO: use maven-jar-plugin's and (i.e., + // "/-.jar"). + String jarName = project.getBuild().getFinalName() + ".jar"; + return Paths.get(project.getBuild().getDirectory(), jarName); + } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index b096d12037..ecb17b52c7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -164,4 +164,9 @@ public List getExtraDirectories() { public Map getExtraDirectoryPermissions() { return MojoCommon.convertPermissionsList(jibPluginConfiguration.getExtraDirectoryPermissions()); } + + @Override + public String getContainerizingMode() { + return jibPluginConfiguration.getContainerizingMode(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index a401cd4b6a..8402c88a64 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.security.DigestException; import java.time.Instant; +import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.hamcrest.CoreMatchers; @@ -192,4 +193,36 @@ public void testExecute_noToImageAndInvalidProjectName() Assert.assertEquals( "Hello, world. \n", new Command("docker", "run", "--rm", "my-artifact-id:1").run()); } + + @Test + public void testExecute_jarContainerization() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "jarcontainerizationimage:maven" + System.nanoTime(); + buildToDockerDaemon( + simpleTestProject.getProjectRoot(), targetImage, "pom-jar-containerization.xml"); + Assert.assertEquals( + "Hello, world. \nImplementation-Title: hello-world\nImplementation-Version: 1\n", + new Command("docker", "run", "--rm", targetImage).run()); + } + + @Test + public void testExecute_jarContainerizationOnMissingJar() + throws VerificationException, IOException, InterruptedException, DigestException { + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("_TARGET_IMAGE", "erroronmissingjar"); + verifier.setAutoclean(false); + verifier.addCliOption("--file=pom-jar-containerization.xml"); + verifier.executeGoals(Arrays.asList("clean", "jib:dockerBuild")); + Assert.fail(); + + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "Obtaining project build output files failed; make sure you have packaged your " + + "project before trying to build the image. (Did you accidentally run \"mvn " + + "clean jib:build\" instead of \"mvn clean package jib:build\"?)")); + } + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index b6a0f663b2..4ac459b4be 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -68,6 +68,7 @@ public void testDefaults() { Assert.assertEquals("", testPluginConfiguration.getAppRoot()); Assert.assertNull(testPluginConfiguration.getWorkingDirectory()); Assert.assertTrue(testPluginConfiguration.getExtraClasspath().isEmpty()); + Assert.assertEquals("exploded", testPluginConfiguration.getContainerizingMode()); } @Test @@ -122,6 +123,8 @@ public void testSystemProperties() { sessionProperties.put("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); + sessionProperties.put("jib.containerizingMode", "packaged"); + Assert.assertEquals("packaged", testPluginConfiguration.getContainerizingMode()); sessionProperties.put("jib.extraDirectories.paths", "custom-jib"); Assert.assertEquals( @@ -187,6 +190,8 @@ public void testPomProperties() { project.getProperties().setProperty("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); + project.getProperties().setProperty("jib.containerizingMode", "packaged"); + Assert.assertEquals("packaged", testPluginConfiguration.getContainerizingMode()); project.getProperties().setProperty("jib.extraDirectories.paths", "custom-jib"); Assert.assertEquals( diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 62b78bcad3..81238fdf60 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; @@ -65,23 +66,26 @@ @RunWith(MockitoJUnitRunner.class) public class MavenProjectPropertiesTest { + private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; + /** Helper for reading back layers in a {@code BuildConfiguration}. */ private static class ContainerBuilderLayers { - private final List resourcesLayerEntries; - private final List classesLayerEntries; - private final List dependenciesLayerEntries; - private final List snapshotsLayerEntries; + private final List resourcesLayers; + private final List classesLayers; + private final List dependenciesLayers; + private final List snapshotsLayers; + private final List extraFilesLayers; private ContainerBuilderLayers(BuildConfiguration configuration) { - resourcesLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); - classesLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); - dependenciesLayerEntries = + resourcesLayers = getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); + classesLayers = getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + dependenciesLayers = getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); - snapshotsLayerEntries = + snapshotsLayers = getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + extraFilesLayers = + getLayerConfigurationsByName(configuration, LayerType.EXTRA_FILES.getName()); } } @@ -122,10 +126,10 @@ private static void assertNonDefaultAppRoot(BuildConfiguration configuration) { "/my/app/libs/libraryA.jar", "/my/app/libs/libraryB.jar", "/my/app/libs/library.jarC.jar"), - layers.dependenciesLayerEntries.get(0).getLayerEntries()); + layers.dependenciesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Collections.singletonList("/my/app/libs/dependencyX-1.0.0-SNAPSHOT.jar"), - layers.snapshotsLayerEntries.get(0).getLayerEntries()); + layers.snapshotsLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/resources/directory", @@ -134,7 +138,7 @@ private static void assertNonDefaultAppRoot(BuildConfiguration configuration) { "/my/app/resources/resourceA", "/my/app/resources/resourceB", "/my/app/resources/world"), - layers.resourcesLayerEntries.get(0).getLayerEntries()); + layers.resourcesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/classes/HelloWorld.class", @@ -142,7 +146,7 @@ private static void assertNonDefaultAppRoot(BuildConfiguration configuration) { "/my/app/classes/package", "/my/app/classes/package/some.class", "/my/app/classes/some.class"), - layers.classesLayerEntries.get(0).getLayerEntries()); + layers.classesLayers.get(0).getLayerEntries()); } private static Path getResource(String path) throws URISyntaxException { @@ -173,7 +177,7 @@ private static Path getResource(String path) throws URISyntaxException { private MavenProjectProperties mavenProjectProperties; @Before - public void setup() throws IOException, URISyntaxException { + public void setUp() throws IOException, URISyntaxException { Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog); @@ -193,9 +197,8 @@ public void setup() throws IOException, URISyntaxException { makeArtifact(dependenciesPath.resolve("library.jarC.jar")), makeArtifact(dependenciesPath.resolve("libraryB.jar")), makeArtifact(dependenciesPath.resolve("libraryA.jar")), - makeArtifact(dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar")), - makeArtifact( - dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar")), + makeArtifact(dependenciesPath.resolve("more/dependency-1.0.0.jar")), + makeArtifact(dependenciesPath.resolve("another/one/dependency-1.0.0.jar")), // Maven reads and populates "Artifacts" with its own processing, so read some from a // repository testRepository.findArtifact("com.test", "dependency", "1.0.0"), @@ -362,7 +365,7 @@ public void isProgressFooterEnabled() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration("/app"); + BuildConfiguration configuration = setupBuildConfiguration("/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); Path dependenciesPath = getResource("maven/application/dependencies"); @@ -370,16 +373,16 @@ public void testCreateContainerBuilder_correctFiles() assertSourcePathsUnordered( ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), - dependenciesPath.resolve("more").resolve("dependency-1.0.0.jar"), - dependenciesPath.resolve("another").resolve("one").resolve("dependency-1.0.0.jar"), + dependenciesPath.resolve("more/dependency-1.0.0.jar"), + dependenciesPath.resolve("another/one/dependency-1.0.0.jar"), dependenciesPath.resolve("libraryA.jar"), dependenciesPath.resolve("libraryB.jar"), dependenciesPath.resolve("library.jarC.jar")), - layers.dependenciesLayerEntries.get(0).getLayerEntries()); + layers.dependenciesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")), - layers.snapshotsLayerEntries.get(0).getLayerEntries()); + layers.snapshotsLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( applicationDirectory.resolve("output/directory"), @@ -388,7 +391,7 @@ public void testCreateContainerBuilder_correctFiles() applicationDirectory.resolve("output/resourceA"), applicationDirectory.resolve("output/resourceB"), applicationDirectory.resolve("output/world")), - layers.resourcesLayerEntries.get(0).getLayerEntries()); + layers.resourcesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( applicationDirectory.resolve("output/HelloWorld.class"), @@ -396,16 +399,69 @@ public void testCreateContainerBuilder_correctFiles() applicationDirectory.resolve("output/package"), applicationDirectory.resolve("output/package/some.class"), applicationDirectory.resolve("output/some.class")), - layers.classesLayerEntries.get(0).getLayerEntries()); + layers.classesLayers.get(0).getLayerEntries()); } @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + BuildConfiguration configuration = + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); assertNonDefaultAppRoot(configuration); } + @Test + public void testCreateContainerBuilder_packagedMode() + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, + URISyntaxException { + Path jar = temporaryFolder.newFile("final-name.jar").toPath(); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + + BuildConfiguration configuration = + setupBuildConfiguration("/app-root", ContainerizingMode.PACKAGED); + + ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + Assert.assertEquals(1, layers.dependenciesLayers.size()); + Assert.assertEquals(1, layers.snapshotsLayers.size()); + Assert.assertEquals(0, layers.resourcesLayers.size()); + Assert.assertEquals(0, layers.classesLayers.size()); + Assert.assertEquals(1, layers.extraFilesLayers.size()); + + Path dependenciesPath = getResource("maven/application/dependencies"); + assertSourcePathsUnordered( + Arrays.asList( + testRepository.artifactPathOnDisk("com.test", "dependency", "1.0.0"), + dependenciesPath.resolve("more/dependency-1.0.0.jar"), + dependenciesPath.resolve("another/one/dependency-1.0.0.jar"), + dependenciesPath.resolve("libraryA.jar"), + dependenciesPath.resolve("libraryB.jar"), + dependenciesPath.resolve("library.jarC.jar")), + layers.dependenciesLayers.get(0).getLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList( + testRepository.artifactPathOnDisk("com.test", "dependencyX", "1.0.0-SNAPSHOT")), + layers.snapshotsLayers.get(0).getLayerEntries()); + assertSourcePathsUnordered( + Arrays.asList(jar), layers.extraFilesLayers.get(0).getLayerEntries()); + + assertExtractionPathsUnordered( + Arrays.asList( + "/app-root/libs/dependency-1.0.0-200.jar", + "/app-root/libs/dependency-1.0.0-480.jar", + "/app-root/libs/dependency-1.0.0-770.jar", + "/app-root/libs/library.jarC.jar", + "/app-root/libs/libraryA.jar", + "/app-root/libs/libraryB.jar"), + layers.dependenciesLayers.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/app-root/libs/dependencyX-1.0.0-SNAPSHOT.jar"), + layers.snapshotsLayers.get(0).getLayerEntries()); + assertExtractionPathsUnordered( + Arrays.asList("/app-root/classpath/final-name.jar"), + layers.extraFilesLayers.get(0).getLayerEntries()); + } + @Test public void testCreateContainerBuilder_warNonDefaultAppRoot() throws URISyntaxException, IOException, InvalidImageReferenceException, @@ -415,15 +471,16 @@ public void testCreateContainerBuilder_warNonDefaultAppRoot() Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + BuildConfiguration configuration = + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertSourcePathsUnordered( ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")), - layers.dependenciesLayerEntries.get(0).getLayerEntries()); + layers.dependenciesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), - layers.snapshotsLayerEntries.get(0).getLayerEntries()); + layers.snapshotsLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( outputPath.resolve("final-name/META-INF"), @@ -436,21 +493,21 @@ public void testCreateContainerBuilder_warNonDefaultAppRoot() outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), outputPath.resolve("final-name/WEB-INF/lib"), outputPath.resolve("final-name/WEB-INF/web.xml")), - layers.resourcesLayerEntries.get(0).getLayerEntries()); + layers.resourcesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), outputPath.resolve("final-name/WEB-INF/classes/package"), outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")), - layers.classesLayerEntries.get(0).getLayerEntries()); + layers.classesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Collections.singletonList("/my/app/WEB-INF/lib/dependency-1.0.0.jar"), - layers.dependenciesLayerEntries.get(0).getLayerEntries()); + layers.dependenciesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Collections.singletonList("/my/app/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), - layers.snapshotsLayerEntries.get(0).getLayerEntries()); + layers.snapshotsLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/META-INF", @@ -463,14 +520,14 @@ public void testCreateContainerBuilder_warNonDefaultAppRoot() "/my/app/WEB-INF/classes/package/test.properties", "/my/app/WEB-INF/lib", "/my/app/WEB-INF/web.xml"), - layers.resourcesLayerEntries.get(0).getLayerEntries()); + layers.resourcesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( Arrays.asList( "/my/app/WEB-INF/classes/HelloWorld.class", "/my/app/WEB-INF/classes/empty_dir", "/my/app/WEB-INF/classes/package", "/my/app/WEB-INF/classes/package/Other.class"), - layers.classesLayerEntries.get(0).getLayerEntries()); + layers.classesLayers.get(0).getLayerEntries()); } @Test @@ -478,7 +535,8 @@ public void testCreateContainerBuilder_jarNonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { // Test when the default packaging is set Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + BuildConfiguration configuration = + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); assertNonDefaultAppRoot(configuration); } @@ -487,11 +545,10 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("final-name"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app"); // should pass + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -499,11 +556,10 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app"); // should pass + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -511,11 +567,20 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()) - .thenReturn(temporaryFolder.getRoot().toPath().toString()); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app"); // should pass + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass + } + + @Test + public void testGetJarArtifact() { + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Assert.assertEquals( + temporaryFolder.getRoot().toPath().resolve("helloworld-1.jar"), + mavenProjectProperties.getJarArtifact()); } @Test @@ -542,11 +607,13 @@ public void testIsWarProject_GwtLibPackagingIsNotWar() { Assert.assertFalse(mavenProjectProperties.isWarProject()); } - private BuildConfiguration setupBuildConfiguration(String appRoot) + private BuildConfiguration setupBuildConfiguration( + String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JibContainerBuilder JibContainerBuilder = new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) - .createContainerBuilder(RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot)); + .createContainerBuilder( + RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot), containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( JibContainerBuilder, Containerizer.to(RegistryImage.named("to")) diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-jar-containerization.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-jar-containerization.xml new file mode 100644 index 0000000000..eda2034cbd --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-jar-containerization.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.1 + + + + true + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + packaged + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java index 4080b0cfca..8b5614e186 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java @@ -17,12 +17,13 @@ package com.test; import dependency.Greeting; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; @@ -34,37 +35,48 @@ public static void main(String[] args) throws IOException, URISyntaxException { String greeting = Greeting.getGreeting(); // Gets the contents of the resource file 'world'. - ClassLoader classLoader = HelloWorld.class.getClassLoader(); - Path worldFile = Paths.get(classLoader.getResource("world").toURI()); - String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader( + HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8))) { + String world = reader.readLine(); + System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); - System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); + // Prints the contents of the extra files. + if (Files.exists(Paths.get("/foo"))) { + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); + System.out.println( + new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); + System.out.println( + new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); + } + // Prints the contents of the files in the second extra directory. + if (Files.exists(Paths.get("/baz"))) { + System.out.println( + new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + } - // Prints the contents of the extra files. - if (Files.exists(Paths.get("/foo"))) { - System.out.println( - PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); - System.out.println( - PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); - System.out.println(new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); - System.out.println( - new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); - } - // Prints the contents of the files in the second extra directory. - if (Files.exists(Paths.get("/baz"))) { - System.out.println(new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); - } + // Prints jvm flags + for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + System.out.println(jvmFlag); + } - // Prints jvm flags - for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { - System.out.println(jvmFlag); - } + if (System.getenv("env1") != null) { + System.out.println(System.getenv("env1")); + } + if (System.getenv("env2") != null) { + System.out.println(System.getenv("env2")); + } - if (System.getenv("env1") != null) { - System.out.println(System.getenv("env1")); - } - if (System.getenv("env2") != null) { - System.out.println(System.getenv("env2")); + Package pack = HelloWorld.class.getPackage(); + if (pack.getImplementationTitle() != null) { + System.out.println("Implementation-Title: " + pack.getImplementationTitle()); + System.out.println("Implementation-Version: " + pack.getImplementationVersion()); + } } } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java new file mode 100644 index 0000000000..f50654eb32 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** + * Containerizing mode. + * + *

    + *
  • {@code EXPLODED} puts individual application files without packaging. + *
  • {@code PACKAGED} puts a single packaged artifact for an application. + *
+ */ +public enum ContainerizingMode { + EXPLODED, + PACKAGED +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerizingModeException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerizingModeException.java new file mode 100644 index 0000000000..2271ece771 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidContainerizingModeException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +/** Indicates that the {@code containerizingMode} config value is invalid. */ +public class InvalidContainerizingModeException extends Exception { + + private final String invalidContainerizingMode; + + public InvalidContainerizingModeException(String message, String invalidContainerizingMode) { + super(message); + this.invalidContainerizingMode = invalidContainerizingMode; + } + + public String getInvalidContainerizingMode() { + return invalidContainerizingMode; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 08e2a1afe9..38958cfc33 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LayerConfiguration; -import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.api.RelativeUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import java.io.IOException; @@ -66,27 +65,25 @@ public static LayerConfiguration extraDirectoryLayerConfiguration( /** * Constructs a new {@link JibContainerBuilder} for a WAR project. * - * @param baseImage the base image of the container + * @param javaContainerBuilder Java container builder to start with * @param explodedWar the exploded WAR directory - * @param appRoot root directory in the image where the app will be placed * @return {@link JibContainerBuilder} containing the layers for the exploded WAR * @throws IOException if adding layer contents fails */ public static JibContainerBuilder fromExplodedWar( - RegistryImage baseImage, Path explodedWar, AbsoluteUnixPath appRoot) throws IOException { + JavaContainerBuilder javaContainerBuilder, Path explodedWar) throws IOException { Path webInfLib = explodedWar.resolve("WEB-INF/lib"); Path webInfClasses = explodedWar.resolve("WEB-INF/classes"); Predicate isDependency = path -> path.startsWith(webInfLib); Predicate isClassFile = + // Don't use Path.endsWith(), since Path works on path elements. path -> path.startsWith(webInfClasses) && path.getFileName().toString().endsWith(".class"); Predicate isResource = isDependency.or(isClassFile).negate(); - JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.from(baseImage) - .setAppRoot(appRoot) - .setResourcesDestination(RelativeUnixPath.get("")) - .setClassesDestination(RelativeUnixPath.get("WEB-INF/classes")) - .setDependenciesDestination(RelativeUnixPath.get("WEB-INF/lib")); + javaContainerBuilder + .setResourcesDestination(RelativeUnixPath.get("")) + .setClassesDestination(RelativeUnixPath.get("WEB-INF/classes")) + .setDependenciesDestination(RelativeUnixPath.get("WEB-INF/lib")); if (Files.exists(explodedWar)) { javaContainerBuilder.addResources(explodedWar, isResource); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 48d1e465dc..71e3fe7728 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -62,7 +63,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -91,7 +93,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -112,7 +115,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry ProjectProperties projectProperties) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -152,7 +156,8 @@ static PluginConfigurationProcessor processCommonConfiguration( boolean isTargetImageCredentialPresent) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); @@ -182,7 +187,9 @@ static PluginConfigurationProcessor processCommonConfiguration( JibContainerBuilder jibContainerBuilder = projectProperties .createContainerBuilder( - baseImage, getAppRootChecked(rawConfiguration, projectProperties)) + baseImage, + getAppRootChecked(rawConfiguration, projectProperties), + getContainerizingModeChecked(rawConfiguration, projectProperties)) .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) .setEnvironment(rawConfiguration.getEnvironment()) @@ -235,12 +242,14 @@ baseImage, getAppRootChecked(rawConfiguration, projectProperties)) * @return the entrypoint * @throws MainClassInferenceException if no valid main class is configured or discovered * @throws InvalidAppRootException if {@code appRoot} value is not an absolute Unix path + * @throws InvalidContainerizingModeException if {@code containerizingMode} value is invalid */ @Nullable @VisibleForTesting static List computeEntrypoint( RawConfiguration rawConfiguration, ProjectProperties projectProperties) - throws MainClassInferenceException, InvalidAppRootException, IOException { + throws MainClassInferenceException, InvalidAppRootException, IOException, + InvalidContainerizingModeException { AbsoluteUnixPath appRoot = getAppRootChecked(rawConfiguration, projectProperties); Optional> rawEntrypoint = rawConfiguration.getEntrypoint(); @@ -264,16 +273,27 @@ static List computeEntrypoint( return null; } + List classpath = new ArrayList<>(rawExtraClasspath); + ContainerizingMode mode = getContainerizingModeChecked(rawConfiguration, projectProperties); + switch (mode) { + case EXPLODED: + classpath.add(appRoot.resolve("resources").toString()); + classpath.add(appRoot.resolve("classes").toString()); + classpath.add(appRoot.resolve("libs/*").toString()); + break; + case PACKAGED: + classpath.add(appRoot.resolve("classpath/*").toString()); + classpath.add(appRoot.resolve("libs/*").toString()); + break; + default: + throw new IllegalStateException("unknown containerizing mode: " + mode); + } + + String classpathString = String.join(":", classpath); String mainClass = MainClassResolver.resolveMainClass( rawConfiguration.getMainClass().orElse(null), projectProperties); - List classpath = new ArrayList<>(rawExtraClasspath); - classpath.add(appRoot.resolve("resources").toString()); - classpath.add(appRoot.resolve("classes").toString()); - classpath.add(appRoot.resolve("libs/*").toString()); - String classpathString = String.join(":", classpath); - List entrypoint = new ArrayList<>(4 + rawConfiguration.getJvmFlags().size()); entrypoint.add("java"); entrypoint.addAll(rawConfiguration.getJvmFlags()); @@ -376,6 +396,23 @@ static AbsoluteUnixPath getAppRootChecked( } } + @VisibleForTesting + static ContainerizingMode getContainerizingModeChecked( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws InvalidContainerizingModeException { + String rawMode = rawConfiguration.getContainerizingMode(); + try { + ContainerizingMode mode = ContainerizingMode.valueOf(rawMode.toUpperCase(Locale.US)); + if (mode == ContainerizingMode.PACKAGED && projectProperties.isWarProject()) { + throw new UnsupportedOperationException( + "packaged containerizing mode for WAR is not yet supported"); + } + return mode; + } catch (IllegalArgumentException ex) { + throw new InvalidContainerizingModeException(rawMode, rawMode); + } + } + @VisibleForTesting static Optional getWorkingDirectoryChecked(RawConfiguration rawConfiguration) throws InvalidWorkingDirectoryException { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index e4ba372261..3b716a59be 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -60,10 +60,12 @@ public interface ProjectProperties { * * @param baseImage the base image * @param appRoot root directory in the image where the app will be placed + * @param containerizingMode mode to containerize the app * @return a {@link JibContainerBuilder} with classes, resources, and dependencies added to it * @throws IOException if there is a problem walking the project files */ - JibContainerBuilder createContainerBuilder(RegistryImage baseImage, AbsoluteUnixPath appRoot) + JibContainerBuilder createContainerBuilder( + RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) throws IOException; List getClassFiles() throws IOException; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index d7cc87ea97..33ebab924a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -56,6 +56,7 @@ public class PropertyNames { public static final String EXTRA_DIRECTORIES_PERMISSIONS = "jib.extraDirectories.permissions"; public static final String DOCKER_CLIENT_EXECUTABLE = "jib.dockerClient.executable"; public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; + public static final String CONTAINERIZING_MODE = "jib.containerizingMode"; public static final String SKIP = "jib.skip"; public static final String CONSOLE = "jib.console"; public static final String CONTAINERIZE = "jib.containerize"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index f27f7bd93c..93cbf06e52 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -79,4 +79,6 @@ public interface RawConfiguration { List getExtraDirectories(); Map getExtraDirectoryPermissions(); + + String getContainerizingMode(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 85f694379a..516a14bc3b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; @@ -108,9 +109,11 @@ public void testFromExplodedWar() Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); Paths.get(Resources.getResource("core/layer").toURI()); + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(RegistryImage.named("base")) + .setAppRoot(AbsoluteUnixPath.get("/my/app")); JibContainerBuilder jibContainerBuilder = - JavaContainerBuilderHelper.fromExplodedWar( - RegistryImage.named("base"), temporaryExplodedWar, AbsoluteUnixPath.get("/my/app")); + JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, temporaryExplodedWar); BuildConfiguration configuration = JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 8b4cd832f5..3d2560ca3d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -94,13 +94,16 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); Mockito.when(rawConfiguration.getExtraDirectories()) .thenReturn(Arrays.asList(Paths.get("nonexistent/path"))); + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("exploded"); Mockito.when(projectProperties.getToolName()).thenReturn("tool"); Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when( projectProperties.createContainerBuilder( - Mockito.any(RegistryImage.class), Mockito.any(AbsoluteUnixPath.class))) + Mockito.any(RegistryImage.class), + Mockito.any(AbsoluteUnixPath.class), + Mockito.any(ContainerizingMode.class))) .thenReturn(Jib.from("base")); Mockito.when(projectProperties.isOffline()).thenReturn(false); @@ -118,7 +121,8 @@ public void setUp() throws IOException, InvalidImageReferenceException { public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -141,7 +145,8 @@ public void testPluginConfigurationProcessor_extraDirectory() throws URISyntaxException, InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, IOException, IncompatibleBaseImageJavaVersionException, InvalidWorkingDirectoryException, InvalidImageReferenceException, - CacheDirectoryCreationException { + CacheDirectoryCreationException, NumberFormatException, + InvalidContainerizingModeException { Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Mockito.when(rawConfiguration.getExtraDirectories()).thenReturn(Arrays.asList(extraDirectory)); Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) @@ -186,7 +191,8 @@ public void testPluginConfigurationProcessor_extraDirectory() public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() throws InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidImageReferenceException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); @@ -203,7 +209,8 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -218,7 +225,8 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -235,7 +243,8 @@ public void testEntrypoint() @Test public void testComputeEntrypoint_inheritKeyword() - throws MainClassInferenceException, InvalidAppRootException, IOException { + throws MainClassInferenceException, InvalidAppRootException, IOException, + InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Collections.singletonList("INHERIT"))); @@ -245,7 +254,8 @@ public void testComputeEntrypoint_inheritKeyword() @Test public void testComputeEntrypoint_inheritKeywordInNonSingletonList() - throws MainClassInferenceException, InvalidAppRootException, IOException { + throws MainClassInferenceException, InvalidAppRootException, IOException, + InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("INHERIT", ""))); @@ -253,11 +263,31 @@ public void testComputeEntrypoint_inheritKeywordInNonSingletonList() PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties)); } + @Test + public void testComputeEntrypoint_default() + throws MainClassInferenceException, InvalidAppRootException, IOException, + InvalidContainerizingModeException { + Assert.assertEquals( + Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties)); + } + + @Test + public void testComputeEntrypoint_exploded() + throws MainClassInferenceException, InvalidAppRootException, IOException, + InvalidContainerizingModeException { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("packaged"); + Assert.assertEquals( + Arrays.asList("java", "-cp", "/app/classpath/*:/app/libs/*", "java.lang.Object"), + PluginConfigurationProcessor.computeEntrypoint(rawConfiguration, projectProperties)); + } + @Test public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -274,7 +304,8 @@ public void testEntrypoint_defaultWarPackaging() public void testEntrypoint_defaultNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -295,7 +326,8 @@ public void testEntrypoint_defaultNonWarPackaging() public void testEntrypoint_extraClasspathNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getExtraClasspath()) .thenReturn(Collections.singletonList("/foo")); @@ -319,7 +351,8 @@ public void testEntrypoint_extraClasspathNonWarPackaging() public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -334,7 +367,8 @@ public void testUser() public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -347,7 +381,8 @@ public void testUser_null() public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -370,7 +405,8 @@ public void testEntrypoint_warningOnJvmFlags() public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -393,7 +429,8 @@ public void testEntrypoint_warningOnMainclass() public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -415,7 +452,8 @@ public void testEntrypointClasspath_nonDefaultAppRoot() public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, - InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException { + InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, + NumberFormatException, InvalidContainerizingModeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -491,6 +529,58 @@ public void testGetAppRootChecked_defaultWarProject() throws InvalidAppRootExcep PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } + @Test + public void testGetContainerizingModeChecked_exploded() + throws InvalidContainerizingModeException { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("exploded"); + + Assert.assertEquals( + ContainerizingMode.EXPLODED, + PluginConfigurationProcessor.getContainerizingModeChecked( + rawConfiguration, projectProperties)); + } + + @Test + public void testGetContainerizingModeChecked_packaged() + throws InvalidContainerizingModeException { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("packaged"); + + Assert.assertEquals( + ContainerizingMode.PACKAGED, + PluginConfigurationProcessor.getContainerizingModeChecked( + rawConfiguration, projectProperties)); + } + + @Test + public void testGetContainerizingModeChecked_invalidValue() { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("this is wrong"); + + try { + PluginConfigurationProcessor.getContainerizingModeChecked( + rawConfiguration, projectProperties); + Assert.fail(); + } catch (InvalidContainerizingModeException ex) { + Assert.assertEquals("this is wrong", ex.getInvalidContainerizingMode()); + Assert.assertEquals("this is wrong", ex.getMessage()); + } + } + + @Test + public void testGetContainerizingModeChecked_packagedWithWar() + throws InvalidContainerizingModeException { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("packaged"); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + try { + PluginConfigurationProcessor.getContainerizingModeChecked( + rawConfiguration, projectProperties); + Assert.fail(); + } catch (UnsupportedOperationException ex) { + Assert.assertEquals( + "packaged containerizing mode for WAR is not yet supported", ex.getMessage()); + } + } + @Test public void testGetWorkingDirectoryChecked() throws InvalidWorkingDirectoryException { Mockito.when(rawConfiguration.getWorkingDirectory()).thenReturn(Optional.of("/valid/path")); @@ -736,7 +826,8 @@ public void testGetInvalidVolumesList() { private PluginConfigurationProcessor createPluginConfigurationProcessor() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException { + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException { return PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, ignored -> Optional.empty(), From cca4a6b76ca837f4b11668ab67bbcf033516c975 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 17 Jun 2019 13:01:13 -0400 Subject: [PATCH 0624/2020] Fix broken javadoc link (#1777) --- .../steps/ThrottledProgressEventDispatcherWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java index 59d24fb063..26a8de1612 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java @@ -24,8 +24,8 @@ /** * Contains a {@link ProgressEventDispatcher} and throttles dispatching progress events with the - * default delay used by {@link ThrottledConsumer}. This class is mutable and should only be used - * within a local context. + * default delay used by {@link ThrottledAccumulatingConsumer}. This class is mutable and should + * only be used within a local context. * *

This class is necessary because the total BLOb size (allocation units) is not known until the * response headers are received, only after which can the {@link ProgressEventDispatcher} be From 273cdc9e835345897c24e0ac2b2de8510f682df5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 19 Jun 2019 12:09:20 -0400 Subject: [PATCH 0625/2020] Add Gradle support for containerizing packaged JAR (#1760) --- .../jib/api/ContainerizerIntegrationTest.java | 8 +-- jib-gradle-plugin/CHANGELOG.md | 2 + .../gradle/SingleProjectIntegrationTest.java | 20 ++++-- .../simple/build-jar-containerization.gradle | 31 +++++++++ .../src/main/java/com/test/HelloWorld.java | 67 +++++++++++-------- .../jib/gradle/GradleProjectProperties.java | 56 ++++++++++------ .../cloud/tools/jib/gradle/JibExtension.java | 5 ++ .../cloud/tools/jib/gradle/JibPlugin.java | 10 +-- .../gradle/GradleProjectPropertiesTest.java | 25 ++----- jib-maven-plugin/CHANGELOG.md | 2 + .../jib/maven/MavenProjectProperties.java | 2 +- .../maven/BuildDockerMojoIntegrationTest.java | 3 +- .../maven/BuildImageMojoIntegrationTest.java | 6 +- .../src/main/java/com/test/HelloWorld.java | 1 - .../common/PluginConfigurationProcessor.java | 7 +- .../PluginConfigurationProcessorTest.java | 14 ++++ 16 files changed, 169 insertions(+), 90 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index cc92f0600f..46f7de15d9 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -141,7 +141,7 @@ private static void assertDockerInspect(String imageReference) Assert.assertThat(history, CoreMatchers.containsString("bazel build ...")); } - private static void assertLayerSizer(int expected, String imageReference) + private static void assertLayerSize(int expected, String imageReference) throws IOException, InterruptedException { Command command = new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); @@ -182,7 +182,7 @@ public void testSteps_forBuildToDockerRegistry() String imageReference = "localhost:5000/testimage:testtag"; localRegistry.pull(imageReference); assertDockerInspect(imageReference); - assertLayerSizer(7, imageReference); + assertLayerSize(7, imageReference); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", imageReference).run()); @@ -251,7 +251,7 @@ public void testBuildToDockerDaemon() progressChecker.checkCompletion(); assertDockerInspect("testdocker"); - assertLayerSizer(7, "testdocker"); + assertLayerSize(7, "testdocker"); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testdocker").run()); } @@ -293,7 +293,7 @@ public void testBuildTarball() progressChecker.checkCompletion(); new Command("docker", "load", "--input", outputPath.toString()).run(); - assertLayerSizer(7, "testtar"); + assertLayerSize(7, "testtar"); Assert.assertEquals( "Hello, world. An argument.\n", new Command("docker", "run", "--rm", "testtar").run()); } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 193e7ae8b1..11e4788230 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'` ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index f52de2c413..4598457ae0 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -86,7 +86,7 @@ private static void assertEntrypoint(String expected, String imageReference) .trim()); } - private static void assertLayerSizer(int expected, String imageReference) + private static void assertLayerSize(int expected, String imageReference) throws IOException, InterruptedException { Command command = new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); @@ -212,7 +212,7 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertEntrypoint( "[java -cp /d1:/d2:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]", targetImage); - assertLayerSizer(8, targetImage); + assertLayerSize(8, targetImage); } @Test @@ -298,7 +298,7 @@ public void testDockerDaemon_simple_multipleExtraDirectories() "Hello, world. \nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", JibRunHelper.buildToDockerDaemonAndRun( simpleTestProject, targetImage, "build-extra-dirs.gradle")); - assertLayerSizer(9, targetImage); // one more than usual + assertLayerSize(9, targetImage); // one more than usual } @Test @@ -309,7 +309,7 @@ public void testDockerDaemon_simple_multipleExtraDirectoriesWithAlternativeConfi "Hello, world. \nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", JibRunHelper.buildToDockerDaemonAndRun( simpleTestProject, targetImage, "build-extra-dirs2.gradle")); - assertLayerSizer(9, targetImage); // one more than usual + assertLayerSize(9, targetImage); // one more than usual } @Test @@ -347,7 +347,17 @@ public void testDockerDaemon_simple() throws IOException, InterruptedException, } @Test - public void testExecute_dockerClient() throws IOException, InterruptedException, DigestException { + public void testDockerDaemon_jarContainerization() + throws DigestException, IOException, InterruptedException { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + Assert.assertEquals( + "Hello, world. \nImplementation-Title: helloworld\nImplementation-Version: 1\n", + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-jar-containerization.gradle")); + } + + @Test + public void testBuild_dockerClient() throws IOException, InterruptedException, DigestException { Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); new Command( "chmod", "+x", simpleTestProject.getProjectRoot().resolve("mock-docker.sh").toString()) diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle new file mode 100644 index 0000000000..d18d6dff02 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jar { + manifest { + attributes( + 'Implementation-Title': 'helloworld', + 'Implementation-Version': '1' + ) + } +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } + containerizingMode = 'packaged' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java index 4080b0cfca..bc41a9f3f2 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java @@ -17,12 +17,13 @@ package com.test; import dependency.Greeting; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; @@ -34,37 +35,47 @@ public static void main(String[] args) throws IOException, URISyntaxException { String greeting = Greeting.getGreeting(); // Gets the contents of the resource file 'world'. - ClassLoader classLoader = HelloWorld.class.getClassLoader(); - Path worldFile = Paths.get(classLoader.getResource("world").toURI()); - String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader( + HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8))) { + String world = reader.readLine(); + System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); - System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); + // Prints the contents of the extra files. + if (Files.exists(Paths.get("/foo"))) { + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); + System.out.println( + PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); + System.out.println( + new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); + System.out.println( + new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); + } + // Prints the contents of the files in the second extra directory. + if (Files.exists(Paths.get("/baz"))) { + System.out.println( + new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + } - // Prints the contents of the extra files. - if (Files.exists(Paths.get("/foo"))) { - System.out.println( - PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/foo")))); - System.out.println( - PosixFilePermissions.toString(Files.getPosixFilePermissions(Paths.get("/bar/cat")))); - System.out.println(new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); - System.out.println( - new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); - } - // Prints the contents of the files in the second extra directory. - if (Files.exists(Paths.get("/baz"))) { - System.out.println(new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); - } + // Prints jvm flags + for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + System.out.println(jvmFlag); + } - // Prints jvm flags - for (String jvmFlag : ManagementFactory.getRuntimeMXBean().getInputArguments()) { - System.out.println(jvmFlag); - } + if (System.getenv("env1") != null) { + System.out.println(System.getenv("env1")); + } + if (System.getenv("env2") != null) { + System.out.println(System.getenv("env2")); + } - if (System.getenv("env1") != null) { - System.out.println(System.getenv("env1")); - } - if (System.getenv("env2") != null) { - System.out.println(System.getenv("env2")); + Package pack = HelloWorld.class.getPackage(); + if (pack.getImplementationTitle() != null) { + System.out.println("Implementation-Title: " + pack.getImplementationTitle()); + System.out.println("Implementation-Version: " + pack.getImplementationVersion()); + } } } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 275f159ad4..3a333fe2aa 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -48,7 +48,6 @@ import org.gradle.api.GradleException; import org.gradle.api.JavaVersion; import org.gradle.api.Project; -import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPluginConvention; @@ -123,7 +122,7 @@ private static boolean isProgressFooterEnabled(Project project) { if (logger.isErrorEnabled()) { consoleLoggerBuilder.error(logger::error); } - this.consoleLogger = consoleLoggerBuilder.build(); + consoleLogger = consoleLoggerBuilder.build(); } @Override @@ -158,21 +157,8 @@ public JibContainerBuilder createContainerBuilder( allDependencyFiles.filter(file -> file.getName().contains("SNAPSHOT")); FileCollection dependencyFiles = allDependencyFiles.minus(snapshotDependencyFiles); - // Adds resource files - if (Files.exists(resourcesOutputDirectory)) { - javaContainerBuilder.addResources(resourcesOutputDirectory); - } - - // Adds class files - for (File classesOutputDirectory : classesOutputDirectories) { - javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); - } - if (classesOutputDirectories.isEmpty()) { - logger.warn("No classes files were found - did you compile your project?"); - } - // Adds dependency files - return javaContainerBuilder + javaContainerBuilder .addDependencies( dependencyFiles.getFiles().stream().map(File::toPath).collect(Collectors.toList())) .addSnapshotDependencies( @@ -180,8 +166,36 @@ public JibContainerBuilder createContainerBuilder( .getFiles() .stream() .map(File::toPath) - .collect(Collectors.toList())) - .toContainerBuilder(); + .collect(Collectors.toList())); + + switch (containerizingMode) { + case EXPLODED: + // Adds resource files + if (Files.exists(resourcesOutputDirectory)) { + javaContainerBuilder.addResources(resourcesOutputDirectory); + } + + // Adds class files + for (File classesOutputDirectory : classesOutputDirectories) { + javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); + } + if (classesOutputDirectories.isEmpty()) { + logger.warn("No classes files were found - did you compile your project?"); + } + break; + + case PACKAGED: + // Add a JAR + Jar jarTask = (Jar) project.getTasks().findByName("jar"); + javaContainerBuilder.addToClasspath( + jarTask.getDestinationDir().toPath().resolve(jarTask.getArchiveName())); + break; + + default: + throw new IllegalStateException("unknown containerizing mode: " + containerizingMode); + } + + return javaContainerBuilder.toContainerBuilder(); } catch (IOException ex) { throw new GradleException("Obtaining project build output files failed", ex); @@ -245,11 +259,11 @@ public String getPluginName() { @Nullable @Override public String getMainClassFromJar() { - List jarTasks = new ArrayList<>(project.getTasksByName("jar", false)); - if (jarTasks.size() != 1) { + Jar jarTask = (Jar) project.getTasks().findByName("jar"); + if (jarTask == null) { return null; } - return (String) ((Jar) jarTasks.get(0)).getManifest().getAttributes().get("Main-Class"); + return (String) jarTask.getManifest().getAttributes().get("Main-Class"); } @Override diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 119d4dc73d..5ce850c3dd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -57,6 +57,7 @@ * ] * } * allowInsecureRegistries = false + * containerizingMode = 'exploded' * } * } */ @@ -126,6 +127,10 @@ public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } + public void setContainerizingMode(String containerizingMode) { + this.containerizingMode.set(containerizingMode); + } + @Nested @Optional public BaseImageParameters getFrom() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 3e62a87ec0..25de43cce8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -130,16 +130,16 @@ public void apply(Project project) { Task dependsOnTask; if (warTask != null) { ExplodedWarTask explodedWarTask = - (ExplodedWarTask) - project - .getTasks() - .create(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class) - .dependsOn(warTask); + project.getTasks().create(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); + explodedWarTask.dependsOn(warTask); explodedWarTask.setWarFile(warTask.getArchivePath().toPath()); explodedWarTask.setExplodedWarDirectory( GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); // Have all tasks depend on the 'jibExplodedWar' task. dependsOnTask = explodedWarTask; + } else if ("packaged".equals(jibExtension.getContainerizingMode())) { + // Have all tasks depend on the 'jar' task. + dependsOnTask = projectAfterEvaluation.getTasks().getByPath("jar"); } else { // Have all tasks depend on the 'classes' task. dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index b9aedffaf3..1de514e77e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -167,10 +167,8 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private FileResolver mockFileResolver; @Mock private Jar mockJar; - @Mock private Jar mockJar2; @Mock private Project mockProject; @Mock private Convention mockConvention; - @Mock private WarPluginConvention mockWarPluginConvention; @Mock private TaskContainer mockTaskContainer; @Mock private Logger mockLogger; @Mock private Gradle mockGradle; @@ -179,7 +177,6 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private TaskContainer taskContainer; @Mock private War war; private Manifest manifest; @@ -230,21 +227,13 @@ public void setup() throws URISyntaxException, IOException { @Test public void testGetMainClassFromJar_success() { manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); - Mockito.when(mockProject.getTasksByName("jar", false)).thenReturn(ImmutableSet.of(mockJar)); + Mockito.when(mockTaskContainer.findByName("jar")).thenReturn(mockJar); Assert.assertEquals("some.main.class", gradleProjectProperties.getMainClassFromJar()); } @Test public void testGetMainClassFromJar_missing() { - Mockito.when(mockProject.getTasksByName("jar", false)).thenReturn(Collections.emptySet()); - Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); - } - - @Test - public void testGetMainClassFromJar_multiple() { - manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); - Mockito.when(mockProject.getTasksByName("jar", false)) - .thenReturn(ImmutableSet.of(mockJar, mockJar2)); + Mockito.when(mockTaskContainer.findByName("jar")).thenReturn(null); Assert.assertNull(gradleProjectProperties.getMainClassFromJar()); } @@ -569,11 +558,11 @@ private BuildConfiguration setupBuildConfiguration(String appRoot) private void setUpWarProject(Path webAppDirectory) { Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); - Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); + Mockito.when(mockTaskContainer.findByName("war")).thenReturn(war); + + WarPluginConvention warPluginConvention = Mockito.mock(WarPluginConvention.class); + Mockito.when(warPluginConvention.getProject()).thenReturn(mockProject); Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(mockWarPluginConvention); - Mockito.when(mockWarPluginConvention.getProject()).thenReturn(mockProject); - Mockito.when(mockProject.getTasks()).thenReturn(taskContainer); - Mockito.when(taskContainer.findByName("war")).thenReturn(war); + .thenReturn(warPluginConvention); } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index c2ae7f727e..72c2f7badc 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged` ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 866ea4b3e2..a073d0239c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -173,7 +173,7 @@ static int getVersionFromString(String versionString) { if (log.isErrorEnabled()) { consoleLoggerBuilder.error(log::error); } - this.consoleLogger = consoleLoggerBuilder.build(); + consoleLogger = consoleLoggerBuilder.build(); } @Override diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 8402c88a64..418f209480 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -206,8 +206,7 @@ public void testExecute_jarContainerization() } @Test - public void testExecute_jarContainerizationOnMissingJar() - throws VerificationException, IOException, InterruptedException, DigestException { + public void testExecute_jarContainerizationOnMissingJar() throws IOException { try { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("_TARGET_IMAGE", "erroronmissingjar"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index a7edc2d724..8f28be4fc9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -298,7 +298,7 @@ private static void assertEntrypoint(String expected, String imageReference) .trim()); } - private static void assertLayerSizer(int expected, String imageReference) + private static void assertLayerSize(int expected, String imageReference) throws IOException, InterruptedException { Command command = new Command("docker", "inspect", "-f", "{{join .RootFS.Layers \",\"}}", imageReference); @@ -367,7 +367,7 @@ public void testExecute_simple() new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); assertWorkingDirectory("/home", targetImage); - assertLayerSizer(8, targetImage); + assertLayerSize(8, targetImage); } @Test @@ -449,7 +449,7 @@ public void testExecute_multipleExtraDirectories() Assert.assertEquals( "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-extra-dirs.xml", false)); - assertLayerSizer(9, targetImage); // one more than usual + assertLayerSize(9, targetImage); // one more than usual } @Test diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java index 8b5614e186..bc41a9f3f2 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java @@ -35,7 +35,6 @@ public static void main(String[] args) throws IOException, URISyntaxException { String greeting = Greeting.getGreeting(); // Gets the contents of the resource file 'world'. - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try (BufferedReader reader = new BufferedReader( new InputStreamReader( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 71e3fe7728..593f70f94d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -215,8 +215,7 @@ static PluginConfigurationProcessor processCommonConfiguration( } } - PluginConfigurationProcessor.configureContainerizer( - containerizer, rawConfiguration, projectProperties); + configureContainerizer(containerizer, rawConfiguration, projectProperties); return new PluginConfigurationProcessor( jibContainerBuilder, @@ -402,6 +401,10 @@ static ContainerizingMode getContainerizingModeChecked( throws InvalidContainerizingModeException { String rawMode = rawConfiguration.getContainerizingMode(); try { + if (!rawMode.toLowerCase(Locale.US).equals(rawMode)) { + throw new InvalidContainerizingModeException(rawMode, rawMode); + } + ContainerizingMode mode = ContainerizingMode.valueOf(rawMode.toUpperCase(Locale.US)); if (mode == ContainerizingMode.PACKAGED && projectProperties.isWarProject()) { throw new UnsupportedOperationException( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 3d2560ca3d..85e087e2eb 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -551,6 +551,20 @@ public void testGetContainerizingModeChecked_packaged() rawConfiguration, projectProperties)); } + @Test + public void testGetContainerizingModeChecked_caseSensitive() { + Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("PACKAGED"); + + try { + PluginConfigurationProcessor.getContainerizingModeChecked( + rawConfiguration, projectProperties); + Assert.fail(); + } catch (InvalidContainerizingModeException ex) { + Assert.assertEquals("PACKAGED", ex.getInvalidContainerizingMode()); + Assert.assertEquals("PACKAGED", ex.getMessage()); + } + } + @Test public void testGetContainerizingModeChecked_invalidValue() { Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("this is wrong"); From 652bf4501bb1d2373d7cffa864b1b791fad54e91 Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 19 Jun 2019 14:07:53 -0400 Subject: [PATCH 0626/2020] Maven: Split project dependencies out into their own layer (#1780) * Split project dependencies out into their own layer --- .../jib/maven/MavenProjectProperties.java | 67 +++++++++++++------ .../jib/maven/MavenProjectPropertiesTest.java | 51 ++++++++++++++ 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index a073d0239c..dcf284564f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; @@ -36,11 +37,14 @@ import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; -import java.io.File; +import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -190,24 +194,6 @@ public JibContainerBuilder createContainerBuilder( return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); } - // Add dependencies - Set allDependencies = project.getArtifacts(); - javaContainerBuilder - .addDependencies( - allDependencies - .stream() - .filter(artifact -> !artifact.isSnapshot()) - .map(Artifact::getFile) - .map(File::toPath) - .collect(Collectors.toList())) - .addSnapshotDependencies( - allDependencies - .stream() - .filter(Artifact::isSnapshot) - .map(Artifact::getFile) - .map(File::toPath) - .collect(Collectors.toList())); - switch (containerizingMode) { case EXPLODED: // Add resources, and classes @@ -228,7 +214,26 @@ public JibContainerBuilder createContainerBuilder( throw new IllegalStateException("unknown containerizing mode: " + containerizingMode); } - return javaContainerBuilder.toContainerBuilder(); + // Classify and add dependencies + Map> classifiedDependencies = + classifyDependencies( + project.getArtifacts(), + session + .getProjects() + .stream() + .map(MavenProject::getArtifact) + .collect(Collectors.toSet())); + + return javaContainerBuilder + .addDependencies( + Preconditions.checkNotNull(classifiedDependencies.get(LayerType.DEPENDENCIES))) + .addSnapshotDependencies( + Preconditions.checkNotNull( + classifiedDependencies.get(LayerType.SNAPSHOT_DEPENDENCIES))) + .addProjectDependencies( + Preconditions.checkNotNull( + classifiedDependencies.get(LayerType.PROJECT_DEPENDENCIES))) + .toContainerBuilder(); } catch (IOException ex) { throw new IOException( @@ -243,6 +248,28 @@ public JibContainerBuilder createContainerBuilder( } } + @VisibleForTesting + Map> classifyDependencies( + Set dependencies, Set projectArtifacts) { + Map> classifiedDependencies = new HashMap<>(); + classifiedDependencies.put(LayerType.DEPENDENCIES, new ArrayList<>()); + classifiedDependencies.put(LayerType.SNAPSHOT_DEPENDENCIES, new ArrayList<>()); + classifiedDependencies.put(LayerType.PROJECT_DEPENDENCIES, new ArrayList<>()); + + for (Artifact artifact : dependencies) { + if (projectArtifacts.contains(artifact)) { + classifiedDependencies.get(LayerType.PROJECT_DEPENDENCIES).add(artifact.getFile().toPath()); + } else if (artifact.isSnapshot()) { + classifiedDependencies + .get(LayerType.SNAPSHOT_DEPENDENCIES) + .add(artifact.getFile().toPath()); + } else { + classifiedDependencies.get(LayerType.DEPENDENCIES).add(artifact.getFile().toPath()); + } + } + return classifiedDependencies; + } + @Override public List getClassFiles() throws IOException { return new DirectoryWalker(Paths.get(project.getBuild().getOutputDirectory())).walk().asList(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 81238fdf60..7efabee3fc 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; +import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -40,11 +41,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; @@ -619,4 +622,52 @@ private BuildConfiguration setupBuildConfiguration( Containerizer.to(RegistryImage.named("to")) .setExecutorService(MoreExecutors.newDirectExecutorService())); } + + @Test + public void testClassifyDependencies() { + Set artifacts = + ImmutableSet.of( + newArtifact("com.test", "dependencyA", "1.0"), + newArtifact("com.test", "dependencyB", "4.0-SNAPSHOT"), + newArtifact("com.test", "projectA", "1.0"), + newArtifact("com.test", "dependencyC", "1.0-SNAPSHOT"), + newArtifact("com.test", "dependencyD", "4.0"), + newArtifact("com.test", "projectB", "1.0-SNAPSHOT"), + newArtifact("com.test", "projectC", "3.0")); + + Set projectArtifacts = + ImmutableSet.of( + newArtifact("com.test", "projectA", "1.0"), + newArtifact("com.test", "projectB", "1.0-SNAPSHOT"), + newArtifact("com.test", "projectC", "3.0")); + + Map> classifyDependencies = + new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) + .classifyDependencies(artifacts, projectArtifacts); + + Assert.assertEquals( + classifyDependencies.get(LayerType.DEPENDENCIES), + ImmutableList.of( + newArtifact("com.test", "dependencyA", "1.0").getFile().toPath(), + newArtifact("com.test", "dependencyD", "4.0").getFile().toPath())); + + Assert.assertEquals( + classifyDependencies.get(LayerType.SNAPSHOT_DEPENDENCIES), + ImmutableList.of( + newArtifact("com.test", "dependencyB", "4.0-SNAPSHOT").getFile().toPath(), + newArtifact("com.test", "dependencyC", "1.0-SNAPSHOT").getFile().toPath())); + + Assert.assertEquals( + classifyDependencies.get(LayerType.PROJECT_DEPENDENCIES), + ImmutableList.of( + newArtifact("com.test", "projectA", "1.0").getFile().toPath(), + newArtifact("com.test", "projectB", "1.0-SNAPSHOT").getFile().toPath(), + newArtifact("com.test", "projectC", "3.0").getFile().toPath())); + } + + private Artifact newArtifact(String group, String artifactId, String version) { + Artifact artifact = new DefaultArtifact(group, artifactId, version, null, "jar", "", null); + artifact.setFile(new File("/tmp/" + group + artifactId + version)); + return artifact; + } } From cdeb1b1659aa85e2d0022f77f1c2b50ea3cfc53f Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Thu, 20 Jun 2019 19:19:11 +0200 Subject: [PATCH 0627/2020] Improve GradleProjectProperties.isWarProject() implementation (#1789) --- .../cloud/tools/jib/gradle/GradleProjectProperties.java | 3 ++- .../cloud/tools/jib/gradle/GradleProjectPropertiesTest.java | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 3a333fe2aa..7e482ec9ed 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -51,6 +51,7 @@ import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.SourceSet; import org.gradle.jvm.tasks.Jar; @@ -278,7 +279,7 @@ public String getJarPluginName() { @Override public boolean isWarProject() { - return TaskCommon.getWarTask(project) != null; + return project.getPlugins().hasPlugin(WarPlugin.class); } /** diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 1de514e77e..9faf9d7325 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -61,6 +61,8 @@ import org.gradle.api.logging.configuration.ConsoleOutput; import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.PluginContainer; +import org.gradle.api.plugins.WarPlugin; import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -170,6 +172,7 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private Project mockProject; @Mock private Convention mockConvention; @Mock private TaskContainer mockTaskContainer; + @Mock private PluginContainer mockPluginContainer; @Mock private Logger mockLogger; @Mock private Gradle mockGradle; @Mock private StartParameter mockStartParameter; @@ -190,6 +193,7 @@ public void setup() throws URISyntaxException, IOException { .thenReturn(mockJavaPluginConvention); Mockito.when(mockJavaPluginConvention.getSourceSets()).thenReturn(mockSourceSetContainer); Mockito.when(mockProject.getTasks()).thenReturn(mockTaskContainer); + Mockito.when(mockProject.getPlugins()).thenReturn(mockPluginContainer); Mockito.when(mockJar.getManifest()).thenReturn(manifest); Mockito.when(mockProject.getGradle()).thenReturn(mockGradle); Mockito.when(mockGradle.getStartParameter()).thenReturn(mockStartParameter); @@ -559,6 +563,8 @@ private BuildConfiguration setupBuildConfiguration(String appRoot) private void setUpWarProject(Path webAppDirectory) { Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); Mockito.when(mockTaskContainer.findByName("war")).thenReturn(war); + Mockito.lenient().when(mockPluginContainer.hasPlugin("war")).thenReturn(true); + Mockito.lenient().when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); WarPluginConvention warPluginConvention = Mockito.mock(WarPluginConvention.class); Mockito.when(warPluginConvention.getProject()).thenReturn(mockProject); From 95599675522a7d15a53d4dc8b44a806ffdd38d86 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Thu, 20 Jun 2019 20:58:19 +0200 Subject: [PATCH 0628/2020] Use bootWar instead of war for Spring Boot war projects (#1787) * Use bootWar instead of war for Spring Boot war projects * Don't use the WarPluginConvention anymore --- jib-gradle-plugin/build.gradle | 3 ++ .../cloud/tools/jib/gradle/TaskCommon.java | 18 +++++-- .../gradle/GradleProjectPropertiesTest.java | 11 +--- .../tools/jib/gradle/TaskCommonTest.java | 50 +++++++++++++++++++ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 64fda3e50d..0a23000b68 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -71,6 +71,9 @@ dependencies { testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' + // only for testing a concrete Spring Boot example in a test (not for test infrastructure) + testCompile 'org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE' + compile gradleApi() // NullAway errorprone plugin diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index b98ce0befe..b4f95db67e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -25,8 +25,9 @@ import java.util.logging.Level; import javax.annotation.Nullable; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.logging.Logger; -import org.gradle.api.plugins.WarPluginConvention; +import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.bundling.War; import org.gradle.internal.logging.events.LogEvent; import org.gradle.internal.logging.events.OutputEventListener; @@ -38,12 +39,19 @@ class TaskCommon { @Nullable static War getWarTask(Project project) { - WarPluginConvention warPluginConvention = - project.getConvention().findPlugin(WarPluginConvention.class); - if (warPluginConvention == null) { + + if (!project.getPlugins().hasPlugin(WarPlugin.class)) { return null; } - return (War) warPluginConvention.getProject().getTasks().findByName("war"); + + if (project.getPlugins().hasPlugin("org.springframework.boot")) { + Task bootWar = project.getTasks().findByName("bootWar"); + if (bootWar != null) { // Spring Boot > 2.0 + return (War) bootWar; + } + } + + return (War) project.getTasks().findByName(WarPlugin.WAR_TASK_NAME); } /** Disables annoying Apache HTTP client logging. */ diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 9faf9d7325..9efa6f4321 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -63,7 +63,6 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.PluginContainer; import org.gradle.api.plugins.WarPlugin; -import org.gradle.api.plugins.WarPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.SourceSetOutput; @@ -256,7 +255,7 @@ public void testGetWar_warProject() throws URISyntaxException { @Test public void testGetWar_noWarPlugin() throws URISyntaxException { setUpWarProject(getResource("gradle/webapp")); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)).thenReturn(null); + Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(false); Assert.assertNull(TaskCommon.getWarTask(mockProject)); } @@ -563,12 +562,6 @@ private BuildConfiguration setupBuildConfiguration(String appRoot) private void setUpWarProject(Path webAppDirectory) { Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); Mockito.when(mockTaskContainer.findByName("war")).thenReturn(war); - Mockito.lenient().when(mockPluginContainer.hasPlugin("war")).thenReturn(true); - Mockito.lenient().when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); - - WarPluginConvention warPluginConvention = Mockito.mock(WarPluginConvention.class); - Mockito.when(warPluginConvention.getProject()).thenReturn(mockProject); - Mockito.when(mockConvention.findPlugin(WarPluginConvention.class)) - .thenReturn(warPluginConvention); + Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 882c213a75..0e5997965e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -16,7 +16,12 @@ package com.google.cloud.tools.jib.gradle; +import org.gradle.api.Project; import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.tasks.bundling.War; +import org.gradle.testfixtures.ProjectBuilder; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -25,6 +30,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.boot.gradle.plugin.SpringBootPlugin; +import org.springframework.boot.gradle.tasks.bundling.BootWar; /** Tests for {@link TaskCommon}. */ @RunWith(MockitoJUnitRunner.class) @@ -123,4 +130,47 @@ public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesConfigured ex.getMessage()); } } + + @Test + public void testGetWarTask_normalJavaProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(JavaPlugin.class); + + War warTask = TaskCommon.getWarTask(project); + + Assert.assertNull(warTask); + } + + @Test + public void testGetWarTask_bootJavaProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(JavaPlugin.class); + project.getPlugins().apply(SpringBootPlugin.class); + + War warTask = TaskCommon.getWarTask(project); + + Assert.assertNull(warTask); + } + + @Test + public void testGetWarTask_normalWarProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(WarPlugin.class); + + War warTask = TaskCommon.getWarTask(project); + + Assert.assertNotNull(warTask); + } + + @Test + public void testGetWarTask_bootWarProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(WarPlugin.class); + project.getPlugins().apply(SpringBootPlugin.class); + + War warTask = TaskCommon.getWarTask(project); + + Assert.assertNotNull(warTask); + Assert.assertTrue(warTask instanceof BootWar); + } } From 6409a05fbbe10b0d70d6c932403274cc101efabe Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 20 Jun 2019 14:58:59 -0400 Subject: [PATCH 0629/2020] Remove ga-beacon from docs (#1784) --- README.md | 2 -- docs/README.md | 2 -- docs/_config.yml | 1 - docs/faq.md | 2 -- examples/README.md | 2 -- examples/dropwizard/README.md | 2 -- examples/helloworld/README.md | 2 -- examples/java-agent/README.md | 2 -- examples/ktor/README.md | 2 -- examples/micronaut/README.md | 2 -- examples/multi-module/README.md | 2 -- examples/spring-boot/README.md | 2 -- examples/vertx/README.md | 2 -- jib-core/README.md | 2 -- jib-gradle-plugin/README.md | 2 -- jib-maven-plugin/README.md | 2 -- jib-plugins-common/README.md | 2 -- 17 files changed, 33 deletions(-) diff --git a/README.md b/README.md index de2d3b1346..e33b0df894 100644 --- a/README.md +++ b/README.md @@ -69,5 +69,3 @@ We welcome contributions! Here's how you can contribute: * Join in on [discussion issues](https://github.com/GoogleContainerTools/jib/labels/discuss) *Make sure to follow the [Code of Conduct](https://github.com/GoogleContainerTools/jib/blob/master/CODE_OF_CONDUCT.md) when contributing so we can foster an open and welcoming community.* - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/index)](https://github.com/igrigorik/ga-beacon) diff --git a/docs/README.md b/docs/README.md index d72cead04e..4f1a51ed9b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/docs)](https://github.com/igrigorik/ga-beacon) - These docs generate [googlecontainertools.github.io/jib](https://googlecontainertools.github.io/jib/). diff --git a/docs/_config.yml b/docs/_config.yml index 2b14561636..ad6fa2c2ee 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -2,4 +2,3 @@ theme: jekyll-theme-slate # Slate theme-specific configuration. # See https://github.com/pages-themes/slate -google_analytics: UA-121724379-1 diff --git a/docs/faq.md b/docs/faq.md index 3e12f1de37..221d18aac7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,3 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/wiki/faq)](https://github.com/igrigorik/ga-beacon) - ## Frequently Asked Questions (FAQ) If a question you have is not answered below, please [submit an issue](/../../issues/new). diff --git a/examples/README.md b/examples/README.md index 24cd8b7a81..69140949fd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,3 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples)](https://github.com/igrigorik/ga-beacon) - # Example projects containerizing with Jib Please [file an issue](/../../issues/new) if you find any problems with the examples or would like to request other examples. diff --git a/examples/dropwizard/README.md b/examples/dropwizard/README.md index c423cdc41d..d2c6b2e6d4 100644 --- a/examples/dropwizard/README.md +++ b/examples/dropwizard/README.md @@ -36,5 +36,3 @@ The remainder of the archetype code was filled-in following the above guide. Learn [more about Jib](https://github.com/GoogleContainerTools/jib). Learn [more about Dropwizard](https://dropwizard.io). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/dropwizard)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md index 657775fc65..769b60c2d7 100644 --- a/examples/helloworld/README.md +++ b/examples/helloworld/README.md @@ -5,5 +5,3 @@ To build the image: 1. In `pom.xml`, replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project. 1. Run `mvn compile jib:build`. - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/helloworld)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/java-agent/README.md b/examples/java-agent/README.md index 737773ba9b..7e4b9843aa 100644 --- a/examples/java-agent/README.md +++ b/examples/java-agent/README.md @@ -8,5 +8,3 @@ To build the image: 1. Run `mvn package` or `./gradlew` to build the image. SparkJava listens on port 4567 by default. - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/java-agent)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/ktor/README.md b/examples/ktor/README.md index 04470cc141..b03a64432f 100644 --- a/examples/ktor/README.md +++ b/examples/ktor/README.md @@ -28,5 +28,3 @@ Learn [more about Jib](https://github.com/GoogleContainerTools/jib). Learn [Ktor]. [Ktor]: https://ktor.io - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/ktor)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/micronaut/README.md b/examples/micronaut/README.md index bb91526a83..adc873f434 100644 --- a/examples/micronaut/README.md +++ b/examples/micronaut/README.md @@ -50,5 +50,3 @@ Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style Learn [more about Jib](https://github.com/GoogleContainerTools/jib). Learn [more about Micronaut](https://micronaut.io). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/micronaut)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index 36d069f589..e1fe061f85 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -70,5 +70,3 @@ Visit the IP in your web browser and you should see: ``` Hello Jib Multimodule ``` - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/multi-module)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/spring-boot/README.md b/examples/spring-boot/README.md index d4840218dc..cac5d9f6a4 100644 --- a/examples/spring-boot/README.md +++ b/examples/spring-boot/README.md @@ -50,5 +50,3 @@ Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style ## More information Learn [more about Jib](https://github.com/GoogleContainerTools/jib). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/spring-boot)](https://github.com/igrigorik/ga-beacon) diff --git a/examples/vertx/README.md b/examples/vertx/README.md index 10faf3d5ce..0f25429920 100644 --- a/examples/vertx/README.md +++ b/examples/vertx/README.md @@ -13,5 +13,3 @@ docker run -d --rm -p 8080:8080 vertx-jib-example Learn [more about Jib](https://github.com/GoogleContainerTools/jib). Learn [more about Eclipse Vert.x](https://vertx.io). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/examples/vertx)](https://github.com/igrigorik/ga-beacon) diff --git a/jib-core/README.md b/jib-core/README.md index 273eb9e47f..e75c387c33 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -131,5 +131,3 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla ## Community See the [Jib project README](/../../#community). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-core)](https://github.com/igrigorik/ga-beacon) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index e6796c29cd..363e7cb7a3 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -426,5 +426,3 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla ## Community See the [Jib project README](/../../#community). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-gradle-plugin)](https://github.com/igrigorik/ga-beacon) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index f81d92aedc..d0b8e27f52 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -529,5 +529,3 @@ See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for pla ## Community See the [Jib project README](/../../#community). - -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-maven-plugin)](https://github.com/igrigorik/ga-beacon) diff --git a/jib-plugins-common/README.md b/jib-plugins-common/README.md index 2f23133d4f..622aab556b 100644 --- a/jib-plugins-common/README.md +++ b/jib-plugins-common/README.md @@ -1,3 +1 @@ -[![Analytics](https://cloud-tools-for-java-metrics.appspot.com/UA-121724379-2/jib-plugins-common)](https://github.com/igrigorik/ga-beacon) - Common code for Jib plugins. NOT intended for use outside of `jib-maven-plugin` or `jib-gradle-plugin` in its current state. \ No newline at end of file From 23beaa169bcfc3780d713410fb0f56d30c89bd6e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Jun 2019 14:59:35 -0400 Subject: [PATCH 0630/2020] Overhaul async execution infrastructure in Jib (#1771) --- .../cloud/tools/jib/api/Containerizer.java | 39 +- .../tools/jib/api/JibContainerBuilder.java | 2 +- .../tools/jib/async/AsyncDependencies.java | 84 ---- .../cloud/tools/jib/async/AsyncStep.java | 42 -- .../cloud/tools/jib/async/AsyncSteps.java | 31 -- .../tools/jib/async/NonBlockingSteps.java | 42 -- .../builder/steps/AuthenticatePushStep.java | 34 +- .../BuildAndCacheApplicationLayerStep.java | 41 +- .../jib/builder/steps/BuildImageStep.java | 68 +--- .../jib/builder/steps/CachedLayerAndName.java | 41 ++ .../jib/builder/steps/LoadDockerStep.java | 65 +-- .../steps/PullAndCacheBaseImageLayerStep.java | 31 +- .../PullAndCacheBaseImageLayersStep.java | 69 +--- .../jib/builder/steps/PullBaseImageStep.java | 69 ++-- .../tools/jib/builder/steps/PushBlobStep.java | 32 +- .../steps/PushContainerConfigurationStep.java | 66 +-- .../jib/builder/steps/PushImageStep.java | 90 +---- .../jib/builder/steps/PushLayerStep.java | 91 +++++ .../jib/builder/steps/PushLayersStep.java | 116 ------ .../RetrieveRegistryCredentialsStep.java | 28 +- .../tools/jib/builder/steps/StepsRunner.java | 380 ++++++++++-------- .../jib/builder/steps/WriteTarFileStep.java | 53 +-- .../jib/image/json/ImageToJsonTranslator.java | 1 - .../jib/api/JibContainerBuilderTest.java | 7 +- ...BuildAndCacheApplicationLayerStepTest.java | 15 +- .../jib/builder/steps/BuildImageStepTest.java | 210 ++++------ .../RetrieveRegistryCredentialsStepTest.java | 7 - .../plugins/common/JibBuildRunnerTest.java | 17 +- 28 files changed, 599 insertions(+), 1172 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 4f999aa979..cea467bfbb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -31,6 +32,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -67,18 +69,7 @@ public static Containerizer to(RegistryImage registryImage) { .build(); Function stepsRunnerFactory = - buildConfiguration -> - StepsRunner.begin(buildConfiguration) - .retrieveTargetRegistryCredentials() - .authenticatePush() - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .pushBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .pushContainerConfiguration() - .pushApplicationLayers() - .pushImage(); + buildConfiguration -> StepsRunner.begin(buildConfiguration).registryPushSteps(); return new Containerizer( DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory, true); @@ -101,12 +92,7 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { Function stepsRunnerFactory = buildConfiguration -> - StepsRunner.begin(buildConfiguration) - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .loadDocker(dockerClientBuilder.build()); + StepsRunner.begin(buildConfiguration).dockerLoadSteps(dockerClientBuilder.build()); return new Containerizer( DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory, false); @@ -124,12 +110,7 @@ public static Containerizer to(TarImage tarImage) { Function stepsRunnerFactory = buildConfiguration -> - StepsRunner.begin(buildConfiguration) - .pullBaseImage() - .pullAndCacheBaseImageLayers() - .buildAndCacheApplicationLayers() - .buildImage() - .writeTarFile(tarImage.getOutputFile()); + StepsRunner.begin(buildConfiguration).tarBuildSteps(tarImage.getOutputFile()); return new Containerizer( DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory, false); @@ -303,9 +284,8 @@ Path getApplicationLayersCacheDirectory() throws CacheDirectoryCreationException if (applicationLayersCacheDirectory == null) { // Uses a temporary directory if application layers cache directory is not set. try { - Path temporaryDirectory = Files.createTempDirectory(null); - temporaryDirectory.toFile().deleteOnExit(); - this.applicationLayersCacheDirectory = temporaryDirectory; + applicationLayersCacheDirectory = Files.createTempDirectory(null); + applicationLayersCacheDirectory.toFile().deleteOnExit(); } catch (IOException ex) { throw new CacheDirectoryCreationException(ex); @@ -338,7 +318,8 @@ ImageConfiguration getImageConfiguration() { return imageConfiguration; } - StepsRunner createStepsRunner(BuildConfiguration buildConfiguration) { - return stepsRunnerFactory.apply(buildConfiguration); + BuildResult run(BuildConfiguration buildConfiguration) + throws ExecutionException, InterruptedException { + return stepsRunnerFactory.apply(buildConfiguration).run(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 70506c17c3..865ffde866 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -475,7 +475,7 @@ JibContainer containerize( try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, containerizer.getDescription())) { - BuildResult result = containerizer.createStepsRunner(buildConfiguration).run(); + BuildResult result = containerizer.run(buildConfiguration); return new JibContainer(result.getImageDigest(), result.getImageId()); } catch (ExecutionException ex) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java deleted file mode 100644 index 7f43957e94..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncDependencies.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.async; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; - -/** - * Builds a list of dependency {@link ListenableFuture}s to wait on before calling a {@link - * Callable}. - */ -public class AsyncDependencies { - - /** - * Initialize with a {@link ListeningExecutorService}. - * - * @param listeningExecutorService the {@link ListeningExecutorService} - * @return a new {@link AsyncDependencies} - */ - public static AsyncDependencies using(ListeningExecutorService listeningExecutorService) { - return new AsyncDependencies(listeningExecutorService); - } - - private final ListeningExecutorService listeningExecutorService; - - /** Stores the list of {@link ListenableFuture}s to wait on. */ - private final List> futures = new ArrayList<>(); - - private AsyncDependencies(ListeningExecutorService listeningExecutorService) { - this.listeningExecutorService = listeningExecutorService; - } - - /** - * Adds the future of an {@link AsyncStep}. - * - * @param asyncStep the {@link AsyncStep} - * @return this - */ - public AsyncDependencies addStep(AsyncStep asyncStep) { - futures.add(asyncStep.getFuture()); - return this; - } - - /** - * Adds the futures of a list of {@link AsyncStep}s. - * - * @param asyncSteps the {@link AsyncStep}s - * @return this - */ - public AsyncDependencies addSteps(List> asyncSteps) { - asyncSteps.forEach(this::addStep); - return this; - } - - /** - * Creates the {@link ListenableFuture} which will return the result of calling {@code combiner} - * when all the added futures succeed. - * - * @param combiner the {@link Callable} - * @param the return type of {@code combiner} - * @return a {@link ListenableFuture} to handle completion of the call to {@code combiner} - */ - public ListenableFuture whenAllSucceed(Callable combiner) { - return Futures.whenAllSucceed(futures).call(combiner, listeningExecutorService); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java deleted file mode 100644 index bab5363449..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncStep.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.async; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; - -/** - * Holds the future for an asynchronously-running step. Implementations should: - * - *

    - *
  1. Be immutable - *
  2. Construct with the dependent {@link AsyncStep}s and submit a {@link Callable} to the {@link - * ListeningExecutorService} to run after all its dependent {@link AsyncStep}s (for example, - * by using {@link Futures#whenAllSucceed}) - *
  3. Have {@link #getFuture} return the submitted future - *
- * - * @param the object type passed on by this step - */ -public interface AsyncStep { - - /** @return the submitted future */ - // TODO: Consider changing this to be orchestrated by an AsyncStepsBuilder. - ListenableFuture getFuture(); -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java deleted file mode 100644 index 0bb3771a0e..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/AsyncSteps.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.async; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; - -/** Static methods for {@link AsyncStep}. */ -public class AsyncSteps { - - public static AsyncStep immediate(T returnValue) { - ListenableFuture future = Futures.immediateFuture(returnValue); - return () -> future; - } - - private AsyncSteps() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java deleted file mode 100644 index d65b3f5c9a..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/async/NonBlockingSteps.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.async; - -import com.google.common.util.concurrent.Futures; -import java.util.concurrent.ExecutionException; - -/** - * Static utility for checking at runtime that the caller attempts to get a result only from a - * completed {@link AsyncStep} by otherwise throwing a runtime exception. - */ -public class NonBlockingSteps { - - /** - * Gets the completed computation result of {@code asyncStep}. - * - * @param the type of the computation result of {@code asyncStep} - * @param asyncStep completed {@link AsyncStep} - * @return the completed computation result - * @throws ExecutionException if the {@code Future} failed with an exception - * @throws IllegalStateException if {@code asyncStep} has not been completed - */ - public static T get(AsyncStep asyncStep) throws ExecutionException { - return Futures.getDone(asyncStep.getFuture()); - } - - private NonBlockingSteps() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 196da2b4d4..2510c5f3d5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -19,19 +19,13 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; /** @@ -40,42 +34,26 @@ * @see https://docs.docker.com/registry/spec/auth/token/ */ -class AuthenticatePushStep implements AsyncStep, Callable { +class AuthenticatePushStep implements Callable { - private static final String DESCRIPTION = "Authenticating with push to %s"; + private static final String DESCRIPTION = "Authenticating push to %s"; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - - private final RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; - - private final ListenableFuture listenableFuture; + private final Credential registryCredential; AuthenticatePushStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep) { + Credential registryCredential) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.retrieveTargetRegistryCredentialsStep = retrieveTargetRegistryCredentialsStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(retrieveTargetRegistryCredentialsStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; + this.registryCredential = registryCredential; } @Override @Nullable - public Authorization call() throws ExecutionException, IOException, RegistryException { - Credential registryCredential = NonBlockingSteps.get(retrieveTargetRegistryCredentialsStep); - + public Authorization call() throws IOException, RegistryException { String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("authenticating push to " + registry, 1); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 6a2187cee8..4a4de43f40 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -28,30 +27,27 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.Optional; import java.util.concurrent.Callable; /** Builds and caches application layers. */ -class BuildAndCacheApplicationLayerStep implements AsyncStep, Callable { +class BuildAndCacheApplicationLayerStep implements Callable { - private static final String DESCRIPTION = "Building application layers"; + private static final String DESCRIPTION = "Preparing application layer builders"; /** * Makes a list of {@link BuildAndCacheApplicationLayerStep} for dependencies, resources, and * classes layers. Optionally adds an extra layer if configured to do so. */ static ImmutableList makeList( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { int layerCount = buildConfiguration.getLayerConfigurations().size(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "setting up to build application layers", layerCount); + "preparing application layer builders", layerCount); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = @@ -64,7 +60,6 @@ static ImmutableList makeList( buildAndCacheApplicationLayerSteps.add( new BuildAndCacheApplicationLayerStep( - listeningExecutorService, buildConfiguration, progressEventDispatcher.newChildProducer(), layerConfiguration.getName(), @@ -79,38 +74,28 @@ static ImmutableList makeList( private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final String layerType; + private final String layerName; private final LayerConfiguration layerConfiguration; - private final ListenableFuture listenableFuture; - private BuildAndCacheApplicationLayerStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - String layerType, + String layerName, LayerConfiguration layerConfiguration) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.layerType = layerType; + this.layerName = layerName; this.layerConfiguration = layerConfiguration; - - listenableFuture = listeningExecutorService.submit(this); } @Override - public ListenableFuture getFuture() { - return listenableFuture; - } - - @Override - public CachedLayer call() throws IOException, CacheCorruptedException { - String description = "Building " + layerType + " layer"; + public CachedLayerAndName call() throws IOException, CacheCorruptedException { + String description = "Building " + layerName + " layer"; buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("building " + layerType + " layer", 1); + progressEventDispatcherFactory.create("building " + layerName + " layer", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); @@ -119,7 +104,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { Optional optionalCachedLayer = cache.retrieve(layerConfiguration.getLayerEntries()); if (optionalCachedLayer.isPresent()) { - return optionalCachedLayer.get(); + return new CachedLayerAndName(optionalCachedLayer.get(), layerName); } Blob layerBlob = new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries()).build(); @@ -130,11 +115,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { .getEventHandlers() .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); - return cachedLayer; + return new CachedLayerAndName(cachedLayer, layerName); } } - - String getLayerType() { - return layerType; - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index 644eb5f3d4..bb3783eb75 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -18,9 +18,6 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -28,81 +25,51 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.time.Instant; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; /** Builds a model {@link Image}. */ -class BuildImageStep implements AsyncStep>, Callable> { +class BuildImageStep implements Callable { private static final String DESCRIPTION = "Building container configuration"; - private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final PullBaseImageStep pullBaseImageStep; - private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - private final ImmutableList buildAndCacheApplicationLayerSteps; - - private final ListenableFuture> listenableFuture; + private final Image baseImage; + private final List baseImageLayers; + private final List applicationLayers; BuildImageStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - PullBaseImageStep pullBaseImageStep, - PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps) { - this.listeningExecutorService = listeningExecutorService; + Image baseImage, + List baseImageLayers, + List applicationLayers) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.pullBaseImageStep = pullBaseImageStep; - this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; - this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(pullBaseImageStep) - .addStep(pullAndCacheBaseImageLayersStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture> getFuture() { - return listenableFuture; + this.baseImage = baseImage; + this.baseImageLayers = baseImageLayers; + this.applicationLayers = applicationLayers; } @Override - public AsyncStep call() throws ExecutionException { - ListenableFuture future = - AsyncDependencies.using(listeningExecutorService) - .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) - .addSteps(buildAndCacheApplicationLayerSteps) - .whenAllSucceed(this::afterCachedLayerSteps); - return () -> future; - } - - private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNotFoundException { + public Image call() throws LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("building image format", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // Constructs the image. Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); - Image baseImage = NonBlockingSteps.get(pullBaseImageStep).getBaseImage(); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); // Base image layers - List baseImageLayers = - NonBlockingSteps.get(pullAndCacheBaseImageLayersStep); - for (PullAndCacheBaseImageLayerStep pullAndCacheBaseImageLayerStep : baseImageLayers) { - imageBuilder.addLayer(NonBlockingSteps.get(pullAndCacheBaseImageLayerStep)); + for (CachedLayerAndName baseImageLayer : baseImageLayers) { + imageBuilder.addLayer(baseImageLayer.getCachedLayer()); } // Passthrough config and count non-empty history entries @@ -137,16 +104,15 @@ private Image afterCachedLayerSteps() throws ExecutionException, LayerPropertyNo } // Add built layers/configuration - for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : - buildAndCacheApplicationLayerSteps) { + for (CachedLayerAndName applicationLayer : applicationLayers) { imageBuilder - .addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)) + .addLayer(applicationLayer.getCachedLayer()) .addHistory( HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) - .setComment(buildAndCacheApplicationLayerStep.getLayerType()) + .setComment(Verify.verifyNotNull(applicationLayer.getName())) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java new file mode 100644 index 0000000000..eed59fc14e --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.cache.CachedLayer; +import javax.annotation.Nullable; + +/** Simple structure to hold a pair of {#link CachedLayer} and its string name. */ +class CachedLayerAndName { + + private CachedLayer cachedLayer; + @Nullable private String name; + + CachedLayerAndName(CachedLayer cachedLayer, @Nullable String name) { + this.cachedLayer = cachedLayer; + this.name = name; + } + + CachedLayer getCachedLayer() { + return cachedLayer; + } + + @Nullable + String getName() { + return name; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index b8f71bd167..9663669898 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -18,91 +18,48 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageTarball; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.Image; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Adds image layers to a tarball and loads into Docker daemon. */ -class LoadDockerStep implements AsyncStep, Callable { +class LoadDockerStep implements Callable { - private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final DockerClient dockerClient; - - private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - private final ImmutableList buildAndCacheApplicationLayerSteps; - private final BuildImageStep buildImageStep; - - private final ListenableFuture listenableFuture; + private final Image builtImage; LoadDockerStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient, - PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; + Image builtImage) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.dockerClient = dockerClient; - this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; - this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - this.buildImageStep = buildImageStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(pullAndCacheBaseImageLayersStep) - .addStep(buildImageStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; + this.builtImage = builtImage; } @Override - public BuildResult call() throws ExecutionException, InterruptedException { - return AsyncDependencies.using(listeningExecutorService) - .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) - .addSteps(buildAndCacheApplicationLayerSteps) - .addStep(NonBlockingSteps.get(buildImageStep)) - .whenAllSucceed(this::afterPushBaseImageLayerFuturesFuture) - .get(); - } - - private BuildResult afterPushBaseImageLayerFuturesFuture() - throws ExecutionException, InterruptedException, IOException { - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.progress("Loading to Docker daemon...")); + public BuildResult call() throws InterruptedException, IOException { + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("loading to Docker daemon", 1)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); // Load the image to docker daemon. - buildConfiguration - .getEventHandlers() - .dispatch( - LogEvent.debug(dockerClient.load(new ImageTarball(image, targetImageReference)))); + eventHandlers.dispatch( + LogEvent.debug(dockerClient.load(new ImageTarball(builtImage, targetImageReference)))); // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. for (String tag : buildConfiguration.getAllTargetImageTags()) { @@ -113,7 +70,7 @@ private BuildResult afterPushBaseImageLayerFuturesFuture() dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } - return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); + return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 9f5913cecd..7fbf042b75 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.cache.Cache; @@ -26,15 +25,13 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; /** Pulls and caches a single base image layer. */ -class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable { +class PullAndCacheBaseImageLayerStep implements Callable { private static final String DESCRIPTION = "Pulling base image layer %s"; @@ -44,10 +41,7 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable private final DescriptorDigest layerDigest; private final @Nullable Authorization pullAuthorization; - private final ListenableFuture listenableFuture; - PullAndCacheBaseImageLayerStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DescriptorDigest layerDigest, @@ -56,17 +50,10 @@ class PullAndCacheBaseImageLayerStep implements AsyncStep, Callable this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layerDigest = layerDigest; this.pullAuthorization = pullAuthorization; - - listenableFuture = listeningExecutorService.submit(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; } @Override - public CachedLayer call() throws IOException, CacheCorruptedException { + public CachedLayerAndName call() throws IOException, CacheCorruptedException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); TimerEventDispatcher ignored = @@ -77,7 +64,7 @@ public CachedLayer call() throws IOException, CacheCorruptedException { // Checks if the layer already exists in the cache. Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { - return optionalCachedLayer.get(); + return new CachedLayerAndName(optionalCachedLayer.get(), null); } else if (buildConfiguration.isOffline()) { throw new IOException( "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " @@ -95,11 +82,13 @@ public CachedLayer call() throws IOException, CacheCorruptedException { new ThrottledProgressEventDispatcherWrapper( progressEventDispatcher.newChildProducer(), "pulling base image layer " + layerDigest)) { - return cache.writeCompressedLayer( - registryClient.pullBlob( - layerDigest, - progressEventDispatcherWrapper::setProgressTarget, - progressEventDispatcherWrapper::dispatchProgress)); + CachedLayer cachedLayer = + cache.writeCompressedLayer( + registryClient.pullBlob( + layerDigest, + progressEventDispatcherWrapper::setProgressTarget, + progressEventDispatcherWrapper::dispatchProgress)); + return new CachedLayerAndName(cachedLayer, null); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java index 18a499e10a..1ade0d839e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java @@ -16,81 +16,44 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; +import java.util.ArrayList; +import java.util.List; /** Pulls and caches the base image layers. */ -class PullAndCacheBaseImageLayersStep - implements AsyncStep>, - Callable> { +// TODO: following the same pattern as "BuildAndCacheApplicationLayerStep", move the sole "makeList" +// static into "PullAndCacheBaseImageLayerStep" and remove this class. +class PullAndCacheBaseImageLayersStep { - private static final String DESCRIPTION = "Setting up base image caching"; + private static final String DESCRIPTION = "Preparing base image layer pullers"; - private final BuildConfiguration buildConfiguration; - private final ListeningExecutorService listeningExecutorService; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - - private final PullBaseImageStep pullBaseImageStep; - - private final ListenableFuture> listenableFuture; - - PullAndCacheBaseImageLayersStep( - ListeningExecutorService listeningExecutorService, + static ImmutableList makeList( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - PullBaseImageStep pullBaseImageStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.pullBaseImageStep = pullBaseImageStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(pullBaseImageStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture> getFuture() { - return listenableFuture; - } - - @Override - public ImmutableList call() - throws ExecutionException, LayerPropertyNotFoundException { - BaseImageWithAuthorization pullBaseImageStepResult = NonBlockingSteps.get(pullBaseImageStep); - ImmutableList baseImageLayers = pullBaseImageStepResult.getBaseImage().getLayers(); + ImageAndAuthorization baseImageAndAuth) { + ImmutableList baseImageLayers = baseImageAndAuth.getImage().getLayers(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "checking base image layers", baseImageLayers.size()); + "preparing base image layer pullers", baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { - ImmutableList.Builder pullAndCacheBaseImageLayerStepsBuilder = - ImmutableList.builderWithExpectedSize(baseImageLayers.size()); + + List layerPullers = new ArrayList<>(); for (Layer layer : baseImageLayers) { - pullAndCacheBaseImageLayerStepsBuilder.add( + layerPullers.add( new PullAndCacheBaseImageLayerStep( - listeningExecutorService, buildConfiguration, progressEventDispatcher.newChildProducer(), layer.getBlobDescriptor().getDigest(), - pullBaseImageStepResult.getBaseImageAuthorization())); + baseImageAndAuth.getAuthorization())); } - - return pullAndCacheBaseImageLayerStepsBuilder.build(); + return ImmutableList.copyOf(layerPullers); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 3c45b54816..0fbae70398 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -23,12 +23,10 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.BaseImageWithAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -49,75 +47,61 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.util.Optional; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; /** Pulls the base image manifest. */ -class PullBaseImageStep - implements AsyncStep, Callable { +class PullBaseImageStep implements Callable { private static final String DESCRIPTION = "Pulling base image manifest"; /** Structure for the result returned by this step. */ - static class BaseImageWithAuthorization { + static class ImageAndAuthorization { - private final Image baseImage; - private final @Nullable Authorization baseImageAuthorization; + private final Image image; + private final @Nullable Authorization authorization; @VisibleForTesting - BaseImageWithAuthorization(Image baseImage, @Nullable Authorization baseImageAuthorization) { - this.baseImage = baseImage; - this.baseImageAuthorization = baseImageAuthorization; + ImageAndAuthorization(Image image, @Nullable Authorization authorization) { + this.image = image; + this.authorization = authorization; } - Image getBaseImage() { - return baseImage; + Image getImage() { + return image; } @Nullable - Authorization getBaseImageAuthorization() { - return baseImageAuthorization; + Authorization getAuthorization() { + return authorization; } } private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final ListenableFuture listenableFuture; - PullBaseImageStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - - listenableFuture = listeningExecutorService.submit(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; } @Override - public BaseImageWithAuthorization call() + public ImageAndAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, - LayerCountMismatchException, ExecutionException, BadContainerConfigurationFormatException, - CacheCorruptedException { + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException { EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); // Skip this step if this is a scratch image ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); if (baseImageConfiguration.getImage().isScratch()) { eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); - return new BaseImageWithAuthorization( + return new ImageAndAuthorization( Image.builder(buildConfiguration.getTargetFormat()).build(), null); } @@ -128,7 +112,7 @@ public BaseImageWithAuthorization call() + "...")); if (buildConfiguration.isOffline()) { - return new BaseImageWithAuthorization(pullBaseImageOffline(), null); + return new ImageAndAuthorization(pullBaseImageOffline(), null); } try (ProgressEventDispatcher progressEventDispatcher = @@ -137,7 +121,7 @@ public BaseImageWithAuthorization call() new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { // First, try with no credentials. try { - return new BaseImageWithAuthorization(pullBaseImage(null, progressEventDispatcher), null); + return new ImageAndAuthorization(pullBaseImage(null, progressEventDispatcher), null); } catch (RegistryUnauthorizedException ex) { eventHandlers.dispatch( @@ -148,15 +132,12 @@ public BaseImageWithAuthorization call() // If failed, then, retrieve base registry credentials and try with retrieved credentials. // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to - // registry.credentials.RegistryCredentialsRetriever to avoid this direct executor hack. - ListeningExecutorService directExecutorService = MoreExecutors.newDirectExecutorService(); - RetrieveRegistryCredentialsStep retrieveBaseRegistryCredentialsStep = + // registry.credentials.RegistryCredentialsRetriever. + Credential registryCredential = RetrieveRegistryCredentialsStep.forBaseImage( - directExecutorService, - buildConfiguration, - progressEventDispatcher.newChildProducer()); + buildConfiguration, progressEventDispatcher.newChildProducer()) + .call(); - Credential registryCredential = NonBlockingSteps.get(retrieveBaseRegistryCredentialsStep); Authorization registryAuthorization = registryCredential == null || registryCredential.isOAuth2RefreshToken() ? null @@ -164,7 +145,7 @@ public BaseImageWithAuthorization call() registryCredential.getUsername(), registryCredential.getPassword()); try { - return new BaseImageWithAuthorization( + return new ImageAndAuthorization( pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); } catch (RegistryUnauthorizedException registryUnauthorizedException) { @@ -180,7 +161,7 @@ public BaseImageWithAuthorization call() Authorization pullAuthorization = registryAuthenticator.authenticatePull(registryCredential); - return new BaseImageWithAuthorization( + return new ImageAndAuthorization( pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 662b37a3cd..4a563802db 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -18,62 +18,44 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Pushes a BLOB to the target registry. */ -class PushBlobStep implements AsyncStep, Callable { +class PushBlobStep implements Callable { private static final String DESCRIPTION = "Pushing BLOB "; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDipatcherFactory; - private final AuthenticatePushStep authenticatePushStep; + private final Authorization authorization; private final BlobDescriptor blobDescriptor; private final Blob blob; - private final ListenableFuture listenableFuture; - PushBlobStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDipatcherFactory, - AuthenticatePushStep authenticatePushStep, + Authorization authorization, BlobDescriptor blobDescriptor, Blob blob) { this.buildConfiguration = buildConfiguration; this.progressEventDipatcherFactory = progressEventDipatcherFactory; - this.authenticatePushStep = authenticatePushStep; + this.authorization = authorization; this.blobDescriptor = blobDescriptor; this.blob = blob; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(authenticatePushStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; } @Override - public BlobDescriptor call() throws IOException, RegistryException, ExecutionException { + public BlobDescriptor call() throws IOException, RegistryException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDipatcherFactory.create( "pushing blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); @@ -85,7 +67,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() - .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) + .setAuthorization(authorization) .newRegistryClient(); // check if the BLOB is available diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 8ffbe1345a..ef74f438d3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -16,90 +16,58 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** Pushes the container configuration. */ -class PushContainerConfigurationStep - implements AsyncStep>, Callable> { +class PushContainerConfigurationStep implements Callable { private static final String DESCRIPTION = "Pushing container configuration"; private final BuildConfiguration buildConfiguration; - private final ListeningExecutorService listeningExecutorService; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final AuthenticatePushStep authenticatePushStep; - private final BuildImageStep buildImageStep; - - private final ListenableFuture> listenableFuture; + private final Authorization pushAuthorization; + private final Image builtImage; PushContainerConfigurationStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - AuthenticatePushStep authenticatePushStep, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; + Authorization authenticatePushStep, + Image builtImage) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.authenticatePushStep = authenticatePushStep; - this.buildImageStep = buildImageStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(buildImageStep) - .whenAllSucceed(this); + this.pushAuthorization = authenticatePushStep; + this.builtImage = builtImage; } @Override - public ListenableFuture> getFuture() { - return listenableFuture; - } - - @Override - public AsyncStep call() throws ExecutionException { - ListenableFuture pushBlobStepFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(authenticatePushStep) - .addStep(NonBlockingSteps.get(buildImageStep)) - .whenAllSucceed(this::afterBuildConfigurationFutureFuture); - return () -> pushBlobStepFuture; - } - - private PushBlobStep afterBuildConfigurationFutureFuture() - throws ExecutionException, IOException { + public BlobDescriptor call() throws IOException, RegistryException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pushing container configuration", 1); TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); JsonTemplate containerConfiguration = - new ImageToJsonTranslator(image).getContainerConfiguration(); - BlobDescriptor blobDescriptor = Digests.computeDigest(containerConfiguration); + new ImageToJsonTranslator(builtImage).getContainerConfiguration(); return new PushBlobStep( - listeningExecutorService, - buildConfiguration, - progressEventDispatcher.newChildProducer(), - authenticatePushStep, - blobDescriptor, - Blobs.from(containerConfiguration)); + buildConfiguration, + progressEventDispatcher.newChildProducer(), + pushAuthorization, + Digests.computeDigest(containerConfiguration), + Blobs.from(containerConfiguration)) + .call(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index f967286715..7cbb0172e4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -18,14 +18,13 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -40,84 +39,37 @@ import java.util.concurrent.ExecutionException; /** Pushes the final image. Outputs the pushed image digest. */ -class PushImageStep implements AsyncStep, Callable { +class PushImageStep implements Callable { private static final String DESCRIPTION = "Pushing new image"; private final BuildConfiguration buildConfiguration; - private final ListeningExecutorService listeningExecutorService; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final AuthenticatePushStep authenticatePushStep; - - private final PushLayersStep pushBaseImageLayersStep; - private final PushLayersStep pushApplicationLayersStep; - private final PushContainerConfigurationStep pushContainerConfigurationStep; - private final BuildImageStep buildImageStep; + private final Authorization pushAuthorization; + private final BlobDescriptor containerConfigurationDigestAndSize; + private final Image builtImage; - private final ListenableFuture listenableFuture; + private final ListeningExecutorService listeningExecutorService; + // TODO: remove listeningExecutorService like other siblings PushImageStep( ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - AuthenticatePushStep authenticatePushStep, - PushLayersStep pushBaseImageLayersStep, - PushLayersStep pushApplicationLayersStep, - PushContainerConfigurationStep pushContainerConfigurationStep, - BuildImageStep buildImageStep) { + Authorization pushAuthorization, + BlobDescriptor containerConfigurationDigestAndSize, + Image builtImage) { this.listeningExecutorService = listeningExecutorService; this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.authenticatePushStep = authenticatePushStep; - this.pushBaseImageLayersStep = pushBaseImageLayersStep; - this.pushApplicationLayersStep = pushApplicationLayersStep; - this.pushContainerConfigurationStep = pushContainerConfigurationStep; - this.buildImageStep = buildImageStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(pushBaseImageLayersStep) - .addStep(pushApplicationLayersStep) - .addStep(pushContainerConfigurationStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; + this.pushAuthorization = pushAuthorization; + this.containerConfigurationDigestAndSize = containerConfigurationDigestAndSize; + this.builtImage = builtImage; } @Override - public BuildResult call() throws ExecutionException, InterruptedException { - return AsyncDependencies.using(listeningExecutorService) - .addStep(authenticatePushStep) - .addSteps(NonBlockingSteps.get(pushBaseImageLayersStep)) - .addSteps(NonBlockingSteps.get(pushApplicationLayersStep)) - .addStep(NonBlockingSteps.get(pushContainerConfigurationStep)) - .addStep(NonBlockingSteps.get(buildImageStep)) - .whenAllSucceed(this::afterPushSteps) - .get(); - } - - private BuildResult afterPushSteps() throws ExecutionException, InterruptedException { - AsyncDependencies dependencies = AsyncDependencies.using(listeningExecutorService); - for (AsyncStep pushBaseImageLayerStep : - NonBlockingSteps.get(pushBaseImageLayersStep)) { - dependencies.addStep(NonBlockingSteps.get(pushBaseImageLayerStep)); - } - for (AsyncStep pushApplicationLayerStep : - NonBlockingSteps.get(pushApplicationLayersStep)) { - dependencies.addStep(NonBlockingSteps.get(pushApplicationLayerStep)); - } - return dependencies - .addStep(NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep))) - .whenAllSucceed(this::afterAllPushed) - .get(); - } - - private BuildResult afterAllPushed() - throws ExecutionException, IOException, InterruptedException { + public BuildResult call() throws IOException, InterruptedException, ExecutionException { ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); @@ -127,20 +79,16 @@ private BuildResult afterAllPushed() RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() - .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) + .setAuthorization(pushAuthorization) .newRegistryClient(); // Constructs the image. - ImageToJsonTranslator imageToJsonTranslator = - new ImageToJsonTranslator(NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep))); + ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(builtImage); // Gets the image manifest to push. - BlobDescriptor containerConfigurationBlobDescriptor = - NonBlockingSteps.get( - NonBlockingSteps.get(NonBlockingSteps.get(pushContainerConfigurationStep))); BuildableManifestTemplate manifestTemplate = imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), containerConfigurationBlobDescriptor); + buildConfiguration.getTargetFormat(), containerConfigurationDigestAndSize); // Pushes to all target image tags. List> pushAllTagsFutures = new ArrayList<>(); @@ -162,7 +110,7 @@ private BuildResult afterAllPushed() } DescriptorDigest imageDigest = Digests.computeJsonDigest(manifestTemplate); - DescriptorDigest imageId = containerConfigurationBlobDescriptor.getDigest(); + DescriptorDigest imageId = containerConfigurationDigestAndSize.getDigest(); BuildResult result = new BuildResult(imageDigest, imageId); return Futures.whenAllSucceed(pushAllTagsFutures) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java new file mode 100644 index 0000000000..26e9b552b8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +class PushLayerStep implements Callable { + + private static final String DESCRIPTION = "Preparing application layer pushers"; + + private final BuildConfiguration buildConfiguration; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + private final Authorization pushAuthorization; + private final Future cachedLayerAndName; + + static ImmutableList makeList( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Authorization pushAuthorization, + List> cachedLayers) { + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "Preparing application layer pushers", cachedLayers.size())) { + + // Constructs a PushBlobStep for each layer. + List blobPushers = new ArrayList<>(); + for (Future layer : cachedLayers) { + ProgressEventDispatcher.Factory childProgressProducer = + progressEventDispatcher.newChildProducer(); + blobPushers.add( + new PushLayerStep(buildConfiguration, childProgressProducer, pushAuthorization, layer)); + } + + return ImmutableList.copyOf(blobPushers); + } + } + + PushLayerStep( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Authorization pushAuthorization, + Future cachedLayerAndName) { + this.buildConfiguration = buildConfiguration; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.pushAuthorization = pushAuthorization; + this.cachedLayerAndName = cachedLayerAndName; + } + + @Override + public BlobDescriptor call() + throws IOException, RegistryException, ExecutionException, InterruptedException { + CachedLayer layer = cachedLayerAndName.get().getCachedLayer(); + return new PushBlobStep( + buildConfiguration, + progressEventDispatcherFactory, + pushAuthorization, + new BlobDescriptor(layer.getSize(), layer.getDigest()), + layer.getBlob()) + .call(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java deleted file mode 100644 index 299b6c0bd9..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayersStep.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -class PushLayersStep - implements AsyncStep>>, - Callable>> { - - private static final String DESCRIPTION = "Setting up to push layers"; - - private final BuildConfiguration buildConfiguration; - private final ListeningExecutorService listeningExecutorService; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - - private final AuthenticatePushStep authenticatePushStep; - private final AsyncStep>> - cachedLayerStep; - - private final ListenableFuture>> listenableFuture; - - PushLayersStep( - ListeningExecutorService listeningExecutorService, - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - AuthenticatePushStep authenticatePushStep, - AsyncStep>> - cachedLayerStep) { - this.listeningExecutorService = listeningExecutorService; - this.buildConfiguration = buildConfiguration; - this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.authenticatePushStep = authenticatePushStep; - this.cachedLayerStep = cachedLayerStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(cachedLayerStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture>> getFuture() { - return listenableFuture; - } - - @Override - public ImmutableList> call() throws ExecutionException { - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { - ImmutableList> cachedLayers = - NonBlockingSteps.get(cachedLayerStep); - - try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("setting up to push layers", cachedLayers.size())) { - // Constructs a PushBlobStep for each layer. - ImmutableList.Builder> pushBlobStepsBuilder = - ImmutableList.builder(); - for (AsyncStep cachedLayerStep : cachedLayers) { - ProgressEventDispatcher.Factory childProgressEventDispatcherFactory = - progressEventDispatcher.newChildProducer(); - ListenableFuture pushBlobStepFuture = - Futures.whenAllSucceed(cachedLayerStep.getFuture()) - .call( - () -> makePushBlobStep(cachedLayerStep, childProgressEventDispatcherFactory), - listeningExecutorService); - pushBlobStepsBuilder.add(() -> pushBlobStepFuture); - } - - return pushBlobStepsBuilder.build(); - } - } - } - - private PushBlobStep makePushBlobStep( - AsyncStep cachedLayerStep, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) - throws ExecutionException { - CachedLayer cachedLayer = NonBlockingSteps.get(cachedLayerStep); - - return new PushBlobStep( - listeningExecutorService, - buildConfiguration, - progressEventDispatcherFactory, - authenticatePushStep, - new BlobDescriptor(cachedLayer.getSize(), cachedLayer.getDigest()), - cachedLayer.getBlob()); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 0cf0d1c4c8..15da9bc07f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -19,33 +19,23 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncStep; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; /** Attempts to retrieve registry credentials. */ -class RetrieveRegistryCredentialsStep implements AsyncStep, Callable { - - private static String makeDescription(String registry) { - return "Retrieving registry credentials for " + registry; - } +class RetrieveRegistryCredentialsStep implements Callable { /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( - listeningExecutorService, buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getBaseImageConfiguration().getImageRegistry(), @@ -54,11 +44,9 @@ static RetrieveRegistryCredentialsStep forBaseImage( /** Retrieves credentials for the target image. */ static RetrieveRegistryCredentialsStep forTargetImage( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( - listeningExecutorService, buildConfiguration, progressEventDispatcherFactory, buildConfiguration.getTargetImageConfiguration().getImageRegistry(), @@ -71,11 +59,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( private final String registry; private final ImmutableList credentialRetrievers; - private final ListenableFuture listenableFuture; - - @VisibleForTesting RetrieveRegistryCredentialsStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String registry, @@ -84,20 +68,12 @@ static RetrieveRegistryCredentialsStep forTargetImage( this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registry = registry; this.credentialRetrievers = credentialRetrievers; - - listenableFuture = listeningExecutorService.submit(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; } @Override @Nullable public Credential call() throws CredentialRetrievalException { - String description = makeDescription(registry); - + String description = "Retrieving registry credentials for " + registry; buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index fff5d935eb..b4872c5ac1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -16,19 +16,30 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.AsyncSteps; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.image.Image; import com.google.common.base.Preconditions; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -40,23 +51,24 @@ */ public class StepsRunner { - /** Holds the individual steps. */ - private static class Steps { + /** Holds the individual step results. */ + private static class StepResults { - @Nullable private RetrieveRegistryCredentialsStep retrieveTargetRegistryCredentialsStep; - @Nullable private AuthenticatePushStep authenticatePushStep; - @Nullable private PullBaseImageStep pullBaseImageStep; - @Nullable private PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - - @Nullable - private ImmutableList buildAndCacheApplicationLayerSteps; - - @Nullable private PushLayersStep pushBaseImageLayersStep; - @Nullable private PushLayersStep pushApplicationLayersStep; - @Nullable private BuildImageStep buildImageStep; - @Nullable private PushContainerConfigurationStep pushContainerConfigurationStep; + private static Future failedFuture() { + return Futures.immediateFailedFuture( + new IllegalStateException("invalid usage; required step not configured")); + } - @Nullable private AsyncStep finalStep; + private Future baseImageAndAuth = failedFuture(); + private Future>> baseImageLayers = failedFuture(); + @Nullable private List> applicationLayers; + private Future builtImage = failedFuture(); + private Future targetRegistryCredentials = failedFuture(); + private Future pushAuthorization = failedFuture(); + private Future>> baseImageLayerPushResults = failedFuture(); + private Future>> applicationLayerPushResults = failedFuture(); + private Future containerConfigurationPushResult = failedFuture(); + private Future buildResult = failedFuture(); } /** @@ -74,197 +86,237 @@ public static StepsRunner begin(BuildConfiguration buildConfiguration) { return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildConfiguration); } - private final Steps steps = new Steps(); + private final StepResults results = new StepResults(); - private final ListeningExecutorService listeningExecutorService; + // TODO: use plain ExecutorService; requires refactoring PushImageStep. + private final ListeningExecutorService executorService; private final BuildConfiguration buildConfiguration; - /** Runnable to run all the steps. */ - private Runnable stepsRunnable = () -> {}; - - /** The total number of steps added. */ - private int stepsCount = 0; - - @Nullable private String rootProgressAllocationDescription; - @Nullable private ProgressEventDispatcher rootProgressEventDispatcher; + // We save steps to run by wrapping each step into a Runnable, only because of the unfortunate + // chicken-and-egg situation arising from using ProgressEventDispatcher. The current + // ProgressEventDispatcher model requires knowing in advance how many units of work (i.e., steps) + // we should perform. That is, to instantiate a root ProgressEventDispatcher instance, we should + // know ahead how many steps we will run. However, to instantiate a step, we need a root progress + // dispatcher. So, we wrap steps into Runnables and save them to run them later. Then we can count + // the number of Runnables and, create a root dispatcher, and run the saved Runnables. + private final List stepsToRun = new ArrayList<>(); + + @Nullable private String rootProgressDescription; + private Supplier childProgressDispatcherFactorySupplier = + () -> { + throw new IllegalStateException("root progress dispatcher uninstantiated"); + }; private StepsRunner( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration) { - this.listeningExecutorService = listeningExecutorService; + ListeningExecutorService executorService, BuildConfiguration buildConfiguration) { + this.executorService = executorService; this.buildConfiguration = buildConfiguration; } - public StepsRunner retrieveTargetRegistryCredentials() { - return enqueueStep( - () -> - steps.retrieveTargetRegistryCredentialsStep = - RetrieveRegistryCredentialsStep.forTargetImage( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); + private void retrieveTargetRegistryCredentials() { + results.targetRegistryCredentials = + executorService.submit( + RetrieveRegistryCredentialsStep.forTargetImage( + buildConfiguration, childProgressDispatcherFactorySupplier.get())); } - public StepsRunner authenticatePush() { - return enqueueStep( - () -> - steps.authenticatePushStep = + private void authenticatePush() { + results.pushAuthorization = + executorService.submit( + () -> new AuthenticatePushStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.retrieveTargetRegistryCredentialsStep))); + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.targetRegistryCredentials.get()) + .call()); } - public StepsRunner pullBaseImage() { - return enqueueStep( - () -> - steps.pullBaseImageStep = - new PullBaseImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); + private void pullBaseImage() { + results.baseImageAndAuth = + executorService.submit( + new PullBaseImageStep( + buildConfiguration, childProgressDispatcherFactorySupplier.get())); } - public StepsRunner pullAndCacheBaseImageLayers() { - return enqueueStep( - () -> - steps.pullAndCacheBaseImageLayersStep = - new PullAndCacheBaseImageLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.pullBaseImageStep))); + private void pullAndCacheBaseImageLayers() { + results.baseImageLayers = + executorService.submit( + () -> + scheduleCallables( + PullAndCacheBaseImageLayersStep.makeList( + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.baseImageAndAuth.get()))); } - public StepsRunner pushBaseImageLayers() { - return enqueueStep( - () -> - steps.pushBaseImageLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.authenticatePushStep), - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep))); + private void pushBaseImageLayers() { + results.baseImageLayerPushResults = + executorService.submit( + () -> + scheduleCallables( + PushLayerStep.makeList( + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.pushAuthorization.get(), + results.baseImageLayers.get()))); } - public StepsRunner buildAndCacheApplicationLayers() { - return enqueueStep( - () -> - steps.buildAndCacheApplicationLayerSteps = - BuildAndCacheApplicationLayerStep.makeList( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer())); + private void buildAndCacheApplicationLayers() { + results.applicationLayers = + scheduleCallables( + BuildAndCacheApplicationLayerStep.makeList( + buildConfiguration, childProgressDispatcherFactorySupplier.get())); } - public StepsRunner buildImage() { - return enqueueStep( - () -> - steps.buildImageStep = + private void buildImage() { + results.builtImage = + executorService.submit( + () -> new BuildImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.pullBaseImageStep), - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps))); + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.baseImageAndAuth.get().getImage(), + realizeFutures(results.baseImageLayers.get()), + realizeFutures(Verify.verifyNotNull(results.applicationLayers))) + .call()); } - public StepsRunner pushContainerConfiguration() { - return enqueueStep( - () -> - steps.pushContainerConfigurationStep = + private void pushContainerConfiguration() { + results.containerConfigurationPushResult = + executorService.submit( + () -> new PushContainerConfigurationStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.authenticatePushStep), - Preconditions.checkNotNull(steps.buildImageStep))); + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.pushAuthorization.get(), + results.builtImage.get()) + .call()); } - public StepsRunner pushApplicationLayers() { - return enqueueStep( - () -> - steps.pushApplicationLayersStep = - new PushLayersStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.authenticatePushStep), - AsyncSteps.immediate( - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps)))); + private void pushApplicationLayers() { + results.applicationLayerPushResults = + executorService.submit( + () -> + scheduleCallables( + PushLayerStep.makeList( + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.pushAuthorization.get(), + Verify.verifyNotNull(results.applicationLayers)))); } - public StepsRunner pushImage() { - rootProgressAllocationDescription = "building image to registry"; - - return enqueueStep( - () -> - steps.finalStep = - new PushImageStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - Preconditions.checkNotNull(steps.authenticatePushStep), - Preconditions.checkNotNull(steps.pushBaseImageLayersStep), - Preconditions.checkNotNull(steps.pushApplicationLayersStep), - Preconditions.checkNotNull(steps.pushContainerConfigurationStep), - Preconditions.checkNotNull(steps.buildImageStep))); + private void pushImage() { + results.buildResult = + executorService.submit( + () -> { + realizeFutures(results.baseImageLayerPushResults.get()); + realizeFutures(results.applicationLayerPushResults.get()); + + return new PushImageStep( + executorService, + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + results.pushAuthorization.get(), + results.containerConfigurationPushResult.get(), + results.builtImage.get()) + .call(); + }); } - public StepsRunner loadDocker(DockerClient dockerClient) { - rootProgressAllocationDescription = "building image to Docker daemon"; - - return enqueueStep( - () -> - steps.finalStep = + private void loadDocker(DockerClient dockerClient) { + results.buildResult = + executorService.submit( + () -> new LoadDockerStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - dockerClient, - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), - Preconditions.checkNotNull(steps.buildImageStep))); + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + dockerClient, + results.builtImage.get()) + .call()); } - public StepsRunner writeTarFile(Path outputPath) { - rootProgressAllocationDescription = "building image to tar file"; - - return enqueueStep( - () -> - steps.finalStep = + private void writeTarFile(Path outputPath) { + results.buildResult = + executorService.submit( + () -> new WriteTarFileStep( - listeningExecutorService, - buildConfiguration, - Preconditions.checkNotNull(rootProgressEventDispatcher).newChildProducer(), - outputPath, - Preconditions.checkNotNull(steps.pullAndCacheBaseImageLayersStep), - Preconditions.checkNotNull(steps.buildAndCacheApplicationLayerSteps), - Preconditions.checkNotNull(steps.buildImageStep))); + buildConfiguration, + childProgressDispatcherFactorySupplier.get(), + outputPath, + results.builtImage.get()) + .call()); } public BuildResult run() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(rootProgressAllocationDescription); + Preconditions.checkNotNull(rootProgressDescription); try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot( - buildConfiguration.getEventHandlers(), rootProgressAllocationDescription, stepsCount)) { - rootProgressEventDispatcher = progressEventDispatcher; - stepsRunnable.run(); - return Preconditions.checkNotNull(steps.finalStep).getFuture().get(); + buildConfiguration.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { + childProgressDispatcherFactorySupplier = progressEventDispatcher::newChildProducer; + stepsToRun.forEach(Runnable::run); + return results.buildResult.get(); + + } catch (ExecutionException ex) { + ExecutionException unrolled = ex; + while (unrolled.getCause() instanceof ExecutionException) { + unrolled = (ExecutionException) unrolled.getCause(); + } + throw unrolled; + } + } + + private static List realizeFutures(List> futures) + throws InterruptedException, ExecutionException { + List values = new ArrayList<>(); + for (Future future : futures) { + values.add(future.get()); } + return values; + } + + private List> scheduleCallables(ImmutableList> callables) { + return callables.stream().map(executorService::submit).collect(Collectors.toList()); + } + + public StepsRunner dockerLoadSteps(DockerClient dockerClient) { + rootProgressDescription = "building image to Docker daemon"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // load to Docker + stepsToRun.add(() -> loadDocker(dockerClient)); + return this; + } + + public StepsRunner tarBuildSteps(Path outputPath) { + rootProgressDescription = "building image to tar file"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // create a tar + stepsToRun.add(() -> writeTarFile(outputPath)); + return this; } - private StepsRunner enqueueStep(Runnable stepRunnable) { - Runnable previousStepsRunnable = stepsRunnable; - stepsRunnable = - () -> { - previousStepsRunnable.run(); - stepRunnable.run(); - }; - stepsCount++; + public StepsRunner registryPushSteps() { + rootProgressDescription = "building image to registry"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // push to registry + stepsToRun.add(this::retrieveTargetRegistryCredentials); + stepsToRun.add(this::authenticatePush); + stepsToRun.add(this::pushBaseImageLayers); + stepsToRun.add(this::pushApplicationLayers); + stepsToRun.add(this::pushContainerConfiguration); + stepsToRun.add(this::pushImage); return this; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index f57625d92a..5d1681ed80 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -17,17 +17,11 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.async.AsyncDependencies; -import com.google.cloud.tools.jib.async.AsyncStep; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -36,75 +30,42 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -public class WriteTarFileStep implements AsyncStep, Callable { +public class WriteTarFileStep implements Callable { - private final ListeningExecutorService listeningExecutorService; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Path outputPath; - private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep; - private final ImmutableList buildAndCacheApplicationLayerSteps; - private final BuildImageStep buildImageStep; - - private final ListenableFuture listenableFuture; + private final Image builtImage; WriteTarFileStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Path outputPath, - PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep, - ImmutableList buildAndCacheApplicationLayerSteps, - BuildImageStep buildImageStep) { - this.listeningExecutorService = listeningExecutorService; + Image builtImage) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.outputPath = outputPath; - this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep; - this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps; - this.buildImageStep = buildImageStep; - - listenableFuture = - AsyncDependencies.using(listeningExecutorService) - .addStep(pullAndCacheBaseImageLayersStep) - .addStep(buildImageStep) - .whenAllSucceed(this); - } - - @Override - public ListenableFuture getFuture() { - return listenableFuture; + this.builtImage = builtImage; } @Override - public BuildResult call() throws ExecutionException, InterruptedException { - return AsyncDependencies.using(listeningExecutorService) - .addSteps(NonBlockingSteps.get(pullAndCacheBaseImageLayersStep)) - .addSteps(buildAndCacheApplicationLayerSteps) - .addStep(NonBlockingSteps.get(buildImageStep)) - .whenAllSucceed(this::writeTarFile) - .get(); - } - - private BuildResult writeTarFile() throws ExecutionException, IOException { + public BuildResult call() throws ExecutionException, InterruptedException, IOException { buildConfiguration .getEventHandlers() .dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("writing to tar file", 1)) { - Image image = NonBlockingSteps.get(NonBlockingSteps.get(buildImageStep)); - // Builds the image to a tarball. Files.createDirectories(outputPath.getParent()); try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { - new ImageTarball(image, buildConfiguration.getTargetImageConfiguration().getImage()) + new ImageTarball(builtImage, buildConfiguration.getTargetImageConfiguration().getImage()) .writeTo(outputStream); } - return BuildResult.fromImage(image, buildConfiguration.getTargetFormat()); + return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index b6309455a1..407c333348 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -219,7 +219,6 @@ public T getManifestTemplate( layer.getBlobDescriptor().getSize(), layer.getBlobDescriptor().getDigest()); } - // Serializes into JSON. return template; } catch (InstantiationException diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index b1b7359774..eb05866a7f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.builder.steps.BuildResult; -import com.google.cloud.tools.jib.builder.steps.StepsRunner; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -263,14 +262,12 @@ private Containerizer createMockContainerizer() ImageReference targetImage = ImageReference.parse("target-image"); Containerizer mockContainerizer = Mockito.mock(Containerizer.class); - StepsRunner stepsRunner = Mockito.mock(StepsRunner.class); BuildResult mockBuildResult = Mockito.mock(BuildResult.class); Mockito.when(mockContainerizer.getImageConfiguration()) .thenReturn(ImageConfiguration.builder(targetImage).build()); - Mockito.when(mockContainerizer.createStepsRunner(Mockito.any(BuildConfiguration.class))) - .thenReturn(stepsRunner); - Mockito.when(stepsRunner.run()).thenReturn(mockBuildResult); + Mockito.when(mockContainerizer.run(Mockito.any(BuildConfiguration.class))) + .thenReturn(mockBuildResult); Mockito.when(mockBuildResult.getImageDigest()) .thenReturn( DescriptorDigest.fromHash( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index f3d56e4725..a8c7a76068 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LayerEntry; -import com.google.cloud.tools.jib.async.NonBlockingSteps; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; @@ -32,13 +31,11 @@ import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; -import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; @@ -126,18 +123,18 @@ public void setUp() throws IOException, URISyntaxException { Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); } - private ImageLayers buildFakeLayersToCache() throws ExecutionException { + private ImageLayers buildFakeLayersToCache() + throws LayerPropertyNotFoundException, IOException, CacheCorruptedException { ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - MoreExecutors.newDirectExecutorService(), mockBuildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - applicationLayersBuilder.add(NonBlockingSteps.get(buildAndCacheApplicationLayerStep)); + applicationLayersBuilder.add(buildAndCacheApplicationLayerStep.call().getCachedLayer()); } return applicationLayersBuilder.build(); @@ -145,8 +142,7 @@ private ImageLayers buildFakeLayersToCache() throws ExecutionException { @Test public void testRun() - throws LayerPropertyNotFoundException, IOException, ExecutionException, - CacheCorruptedException { + throws LayerPropertyNotFoundException, IOException, CacheCorruptedException { ImmutableList fakeLayerConfigurations = ImmutableList.of( fakeDependenciesLayerConfiguration, @@ -207,8 +203,7 @@ public void testRun() } @Test - public void testRun_emptyLayersIgnored() - throws IOException, ExecutionException, CacheCorruptedException { + public void testRun_emptyLayersIgnored() throws IOException, CacheCorruptedException { ImmutableList fakeLayerConfigurations = ImmutableList.of( fakeDependenciesLayerConfiguration, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 3da452d9c1..96b65640fa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -32,14 +32,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.MoreExecutors; import java.security.DigestException; import java.time.Duration; import java.time.Instant; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.stream.Stream; +import java.util.Arrays; +import java.util.List; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -52,17 +49,15 @@ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { - @Mock private EventHandlers mockEventHandlers; + @Mock private ProgressEventDispatcher.Factory mockProgressEventDispatcherFactory; @Mock private BuildConfiguration mockBuildConfiguration; @Mock private ContainerConfiguration mockContainerConfiguration; - @Mock private PullBaseImageStep mockPullBaseImageStep; - @Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep; - @Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepDependencies; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepResources; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepClasses; - @Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStepExtraFiles; @Mock private CachedLayer mockCachedLayer; + + private Image baseImage; + private List baseImageLayers; + private List applicationLayers; + private DescriptorDigest testDescriptorDigest; private HistoryEntry nonEmptyLayerHistory; private HistoryEntry emptyLayerHistory; @@ -73,7 +68,7 @@ public void setUp() throws DigestException { DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(mockEventHandlers); + Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); Mockito.when(mockBuildConfiguration.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); @@ -100,7 +95,7 @@ public void setUp() throws DigestException { .setEmptyLayer(true) .build(); - Image baseImage = + baseImage = Image.builder(V22ManifestTemplate.class) .setArchitecture("wasm") .setOs("js") @@ -125,60 +120,35 @@ public void setUp() throws DigestException { .addHistory(emptyLayerHistory) .addHistory(emptyLayerHistory) .build(); - Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture()) - .thenReturn(Futures.immediateFuture(mockCachedLayer)); - Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture()) - .thenReturn( - Futures.immediateFuture( - ImmutableList.of( - mockPullAndCacheBaseImageLayerStep, - mockPullAndCacheBaseImageLayerStep, - mockPullAndCacheBaseImageLayerStep))); - Mockito.when(mockPullBaseImageStep.getFuture()) - .thenReturn( - Futures.immediateFuture( - new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null))); - - Stream.of( - mockBuildAndCacheApplicationLayerStepClasses, - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepExtraFiles, - mockBuildAndCacheApplicationLayerStepResources) - .forEach( - layerStep -> - Mockito.when(layerStep.getFuture()) - .thenReturn(Futures.immediateFuture(mockCachedLayer))); - - Mockito.when(mockBuildAndCacheApplicationLayerStepClasses.getLayerType()).thenReturn("classes"); - Mockito.when(mockBuildAndCacheApplicationLayerStepDependencies.getLayerType()) - .thenReturn("dependencies"); - Mockito.when(mockBuildAndCacheApplicationLayerStepExtraFiles.getLayerType()) - .thenReturn("extra files"); - Mockito.when(mockBuildAndCacheApplicationLayerStepResources.getLayerType()) - .thenReturn("resources"); + baseImageLayers = + Arrays.asList( + new CachedLayerAndName(mockCachedLayer, null), + new CachedLayerAndName(mockCachedLayer, null), + new CachedLayerAndName(mockCachedLayer, null)); + applicationLayers = + Arrays.asList( + new CachedLayerAndName(mockCachedLayer, "dependencies"), + new CachedLayerAndName(mockCachedLayer, "resources"), + new CachedLayerAndName(mockCachedLayer, "classes"), + new CachedLayerAndName(mockCachedLayer, "extra files")); } @Test - public void test_validateAsyncDependencies() throws ExecutionException, InterruptedException { - BuildImageStep buildImageStep = + public void test_validateAsyncDependencies() { + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals( testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest()); } @Test - public void test_propagateBaseImageConfiguration() - throws ExecutionException, InterruptedException { + public void test_propagateBaseImageConfiguration() { Mockito.when(mockContainerConfiguration.getEnvironmentMap()) .thenReturn(ImmutableMap.of("MY_ENV", "MY_ENV_VALUE", "BASE_ENV_2", "NEW_VALUE")); Mockito.when(mockContainerConfiguration.getLabels()) @@ -189,18 +159,14 @@ public void test_propagateBaseImageConfiguration() .thenReturn( ImmutableSet.of( AbsoluteUnixPath.get("/new/path1"), AbsoluteUnixPath.get("/new/path2"))); - BuildImageStep buildImageStep = + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals("wasm", image.getArchitecture()); Assert.assertEquals("js", image.getOs()); Assert.assertEquals( @@ -248,110 +214,88 @@ public void test_propagateBaseImageConfiguration() } @Test - public void testOverrideWorkingDirectory() throws InterruptedException, ExecutionException { + public void testOverrideWorkingDirectory() { Mockito.when(mockContainerConfiguration.getWorkingDirectory()) .thenReturn(AbsoluteUnixPath.get("/my/directory")); - BuildImageStep buildImageStep = + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals("/my/directory", image.getWorkingDirectory()); } @Test - public void test_inheritedEntrypoint() throws ExecutionException, InterruptedException { + public void test_inheritedEntrypoint() { Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); Mockito.when(mockContainerConfiguration.getProgramArguments()) .thenReturn(ImmutableList.of("test")); - BuildImageStep buildImageStep = + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); Assert.assertEquals(ImmutableList.of("test"), image.getProgramArguments()); } @Test - public void test_inheritedEntrypointAndProgramArguments() - throws ExecutionException, InterruptedException { + public void test_inheritedEntrypointAndProgramArguments() { Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); - BuildImageStep buildImageStep = + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals(ImmutableList.of("baseImageEntrypoint"), image.getEntrypoint()); Assert.assertEquals(ImmutableList.of("catalina.sh", "run"), image.getProgramArguments()); } @Test - public void test_notInheritedProgramArguments() throws ExecutionException, InterruptedException { + public void test_notInheritedProgramArguments() { Mockito.when(mockContainerConfiguration.getEntrypoint()) .thenReturn(ImmutableList.of("myEntrypoint")); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); - BuildImageStep buildImageStep = + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); Assert.assertEquals(ImmutableList.of("myEntrypoint"), image.getEntrypoint()); Assert.assertNull(image.getProgramArguments()); } @Test - public void test_generateHistoryObjects() throws ExecutionException, InterruptedException { - BuildImageStep buildImageStep = + public void test_generateHistoryObjects() { + Image image = new BuildImageStep( - MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), - mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer(), - mockPullBaseImageStep, - mockPullAndCacheBaseImageLayersStep, - ImmutableList.of( - mockBuildAndCacheApplicationLayerStepDependencies, - mockBuildAndCacheApplicationLayerStepResources, - mockBuildAndCacheApplicationLayerStepClasses, - mockBuildAndCacheApplicationLayerStepExtraFiles)); - Image image = buildImageStep.getFuture().get().getFuture().get(); + mockBuildConfiguration, + mockProgressEventDispatcherFactory, + baseImage, + baseImageLayers, + applicationLayers) + .call(); // Make sure history is as expected HistoryEntry expectedAddedBaseLayerHistory = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index ad4ce0a2b4..2121f33c81 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -26,7 +26,6 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Paths; @@ -46,7 +45,6 @@ public class RetrieveRegistryCredentialsStepTest { @Mock private EventHandlers mockEventHandlers; - @Mock private ListeningExecutorService mockListeningExecutorService; @Test public void testCall_retrieved() throws CredentialRetrievalException, IOException { @@ -62,14 +60,12 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio Assert.assertEquals( Credential.from("baseusername", "basepassword"), RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); Assert.assertEquals( Credential.from("targetusername", "targetpassword"), RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); @@ -82,7 +78,6 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertNull( RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); @@ -94,7 +89,6 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Assert.assertNull( RetrieveRegistryCredentialsStep.forTargetImage( - mockListeningExecutorService, buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); @@ -116,7 +110,6 @@ public void testCall_exception() throws IOException { Collections.emptyList()); try { RetrieveRegistryCredentialsStep.forBaseImage( - mockListeningExecutorService, buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 4f1135bbf3..f5e775609d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -91,7 +91,7 @@ public void testBuildImage_httpHostConnectException() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forHttpHostConnect(), ex.getMessage()); @@ -110,7 +110,7 @@ public void testBuildImage_unknownHostException() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forUnknownHost(), ex.getMessage()); @@ -130,7 +130,7 @@ public void testBuildImage_insecureRegistryException() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forInsecureRegistry(), ex.getMessage()); @@ -155,7 +155,7 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals( @@ -181,7 +181,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals( @@ -201,7 +201,7 @@ public void testBuildImage_registryCredentialsNotSentException() try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.forCredentialsNotSent(), ex.getMessage()); @@ -212,15 +212,14 @@ public void testBuildImage_registryCredentialsNotSentException() public void testBuildImage_other() throws InterruptedException, IOException, CacheDirectoryCreationException, RegistryException, ExecutionException { - RegistryException registryException = new RegistryException("messagePrefix"); - Mockito.doThrow(registryException) + Mockito.doThrow(new RegistryException("messagePrefix")) .when(mockJibContainerBuilder) .containerize(mockContainerizer); try { testJibBuildRunner.build( mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); - Assert.fail("buildImage should have thrown an exception"); + Assert.fail(); } catch (BuildStepsExecutionException ex) { Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); From ab22479b2079973b0f6c99192b34a32b3e6d8780 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 20 Jun 2019 15:20:10 -0400 Subject: [PATCH 0631/2020] Allow parsing of scratch as an image string (#1792) --- .../java/com/google/cloud/tools/jib/api/ImageReference.java | 4 ++++ .../cloud/tools/jib/configuration/BuildConfiguration.java | 4 +++- .../com/google/cloud/tools/jib/api/ImageReferenceTest.java | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java index e9facae06f..46c23629e6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java @@ -92,6 +92,10 @@ public class ImageReference { * @throws InvalidImageReferenceException if {@code reference} is formatted incorrectly */ public static ImageReference parse(String reference) throws InvalidImageReferenceException { + if (reference.equals("scratch")) { + return ImageReference.scratch(); + } + Matcher matcher = REFERENCE_PATTERN.matcher(reference); if (!matcher.find() || matcher.groupCount() < 4) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 1c9ca20b53..3723eae8a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -244,7 +244,9 @@ public BuildConfiguration build() throws IOException { switch (missingFields.size()) { case 0: // No errors - if (Preconditions.checkNotNull(baseImageConfiguration).getImage().usesDefaultTag()) { + Preconditions.checkNotNull(baseImageConfiguration); + if (baseImageConfiguration.getImage().usesDefaultTag() + && !baseImageConfiguration.getImage().isScratch()) { eventHandlers.dispatch( LogEvent.warn( "Base image '" diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java index 9519668f96..ef90c027a1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java @@ -181,7 +181,8 @@ public void testIsTagDigest() throws InvalidImageReferenceException { } @Test - public void testIsScratch() { + public void testIsScratch() throws InvalidImageReferenceException { + Assert.assertTrue(ImageReference.parse("scratch").isScratch()); Assert.assertTrue(ImageReference.scratch().isScratch()); Assert.assertFalse(ImageReference.of("", "scratch", "").isScratch()); Assert.assertFalse(ImageReference.of(null, "scratch", null).isScratch()); From 8cb7179bce56269939d49381670dbf7066888b1c Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 20 Jun 2019 16:23:54 -0400 Subject: [PATCH 0632/2020] Update CHANGELOG for scratch option (#1795) --- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 11e4788230..1f27e4818c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'` ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) +- Can now use `jib.from.image = 'scratch'` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 72c2f7badc..1094908a63 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged` ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) +- Can now use `scratch` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) ### Changed From a5ea74a5bad74234ee4fa047fbb51b30c568ea41 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 20 Jun 2019 18:55:39 -0400 Subject: [PATCH 0633/2020] Fix root progress construction (#1799) --- .../jib/builder/ProgressEventDispatcher.java | 2 +- .../tools/jib/builder/steps/StepsRunner.java | 70 ++++++++++++++----- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java index e7af9a2a20..f1aa4837f9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java @@ -147,7 +147,7 @@ public void dispatchProgress(long progressUnits) { private long decrementRemainingAllocationUnits(long units) { Preconditions.checkState(!closed); - if (remainingAllocationUnits > units) { + if (remainingAllocationUnits >= units) { remainingAllocationUnits -= units; return units; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index b4872c5ac1..29ee23650c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -38,7 +38,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -102,10 +101,7 @@ public static StepsRunner begin(BuildConfiguration buildConfiguration) { private final List stepsToRun = new ArrayList<>(); @Nullable private String rootProgressDescription; - private Supplier childProgressDispatcherFactorySupplier = - () -> { - throw new IllegalStateException("root progress dispatcher uninstantiated"); - }; + @Nullable private ProgressEventDispatcher rootProgressDispatcher; private StepsRunner( ListeningExecutorService executorService, BuildConfiguration buildConfiguration) { @@ -114,67 +110,87 @@ private StepsRunner( } private void retrieveTargetRegistryCredentials() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.targetRegistryCredentials = executorService.submit( RetrieveRegistryCredentialsStep.forTargetImage( - buildConfiguration, childProgressDispatcherFactorySupplier.get())); + buildConfiguration, childProgressDispatcherFactory)); } private void authenticatePush() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.pushAuthorization = executorService.submit( () -> new AuthenticatePushStep( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.targetRegistryCredentials.get()) .call()); } private void pullBaseImage() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.baseImageAndAuth = executorService.submit( - new PullBaseImageStep( - buildConfiguration, childProgressDispatcherFactorySupplier.get())); + new PullBaseImageStep(buildConfiguration, childProgressDispatcherFactory)); } private void pullAndCacheBaseImageLayers() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.baseImageLayers = executorService.submit( () -> scheduleCallables( PullAndCacheBaseImageLayersStep.makeList( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.baseImageAndAuth.get()))); } private void pushBaseImageLayers() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.baseImageLayerPushResults = executorService.submit( () -> scheduleCallables( PushLayerStep.makeList( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.pushAuthorization.get(), results.baseImageLayers.get()))); } private void buildAndCacheApplicationLayers() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.applicationLayers = scheduleCallables( BuildAndCacheApplicationLayerStep.makeList( - buildConfiguration, childProgressDispatcherFactorySupplier.get())); + buildConfiguration, childProgressDispatcherFactory)); } private void buildImage() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.builtImage = executorService.submit( () -> new BuildImageStep( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.baseImageAndAuth.get().getImage(), realizeFutures(results.baseImageLayers.get()), realizeFutures(Verify.verifyNotNull(results.applicationLayers))) @@ -182,30 +198,39 @@ private void buildImage() { } private void pushContainerConfiguration() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.containerConfigurationPushResult = executorService.submit( () -> new PushContainerConfigurationStep( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.pushAuthorization.get(), results.builtImage.get()) .call()); } private void pushApplicationLayers() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.applicationLayerPushResults = executorService.submit( () -> scheduleCallables( PushLayerStep.makeList( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.pushAuthorization.get(), Verify.verifyNotNull(results.applicationLayers)))); } private void pushImage() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.buildResult = executorService.submit( () -> { @@ -215,7 +240,7 @@ private void pushImage() { return new PushImageStep( executorService, buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, results.pushAuthorization.get(), results.containerConfigurationPushResult.get(), results.builtImage.get()) @@ -224,24 +249,30 @@ private void pushImage() { } private void loadDocker(DockerClient dockerClient) { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.buildResult = executorService.submit( () -> new LoadDockerStep( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, dockerClient, results.builtImage.get()) .call()); } private void writeTarFile(Path outputPath) { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + results.buildResult = executorService.submit( () -> new WriteTarFileStep( buildConfiguration, - childProgressDispatcherFactorySupplier.get(), + childProgressDispatcherFactory, outputPath, results.builtImage.get()) .call()); @@ -253,7 +284,8 @@ public BuildResult run() throws ExecutionException, InterruptedException { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot( buildConfiguration.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { - childProgressDispatcherFactorySupplier = progressEventDispatcher::newChildProducer; + rootProgressDispatcher = progressEventDispatcher; + stepsToRun.forEach(Runnable::run); return results.buildResult.get(); From ecd1fcc33ece9cb97fd94342d4cf004c11c27a96 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 20 Jun 2019 19:22:55 -0400 Subject: [PATCH 0634/2020] Separate out project dependencies, add integration test to verify (#1785) * Separate out project dependencies, add integration test to verify * update changelog for gradle AND maven --- jib-gradle-plugin/CHANGELOG.md | 2 + ...adleLayerConfigurationIntegrationTest.java | 146 ++++++++++++++++++ .../jib/gradle/GradleProjectProperties.java | 37 ++++- .../gradle/GradleProjectPropertiesTest.java | 18 +++ .../all-local-multi-service/build.gradle | 1 + .../complex-service/build.gradle | 36 +++++ .../complex-service/libs/dependency-1.0.0.jar | Bin 0 -> 770 bytes .../libs/dependencyX-1.0.0-SNAPSHOT.jar | Bin 0 -> 770 bytes .../src/main/extra-resources-1/resource1.txt | 0 .../src/main/extra-resources-2/resource2.txt | 0 .../src/main/java/com/test/HelloWorld.java | 23 +++ .../src/main/other-jib/extra-file | 0 .../all-local-multi-service/gradle.properties | 0 .../all-local-multi-service/lib/build.gradle | 10 ++ .../lib/src/main/java/com/lib/Lib.java | 25 +++ .../lib/src/main/resources/hi.txt | 1 + .../all-local-multi-service/settings.gradle | 3 + .../simple-service/build.gradle | 18 +++ .../src/main/java/com/test/HelloWorld.java | 23 +++ jib-maven-plugin/CHANGELOG.md | 2 + 20 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 1f27e4818c..36d5c86cdb 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) + ### Fixed ## 1.3.0 diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java new file mode 100644 index 0000000000..26eea41970 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +public class GradleLayerConfigurationIntegrationTest { + + @ClassRule + public static final TestProject multiTestProject = new TestProject("all-local-multi-service"); + + @Test + public void testGradleLayerConfiguration_multiModule() throws IOException { + multiTestProject.build(":complex-service:jibBuildTar"); + + Path jibTar = multiTestProject.getProjectRoot().resolve("complex-service/build/jib-image.tar"); + + List> layers = getLayers(jibTar); + + Assert.assertEquals(6, layers.size()); + + // the expected order is: + // no base image layers (scratch) + // extra-files (0) + // classes (1) + // resources (2) + // project dependencies (3) + // snapshot dependencies (4) + // dependencies (5) + + Path complexServiceRoot = multiTestProject.getProjectRoot().resolve("complex-service"); + // verify dependencies + List dependencies = layers.get(5); + List expectedDependencies = + ImmutableList.of("app/", "app/libs/", "app/libs/dependency-1.0.0.jar"); + Assert.assertEquals(expectedDependencies, dependencies); + + // verify snapshot dependencies + List snapshotDependencies = layers.get(4); + List expectedSnapshotDependencies = + ImmutableList.of("app/", "app/libs/", "app/libs/dependencyX-1.0.0-SNAPSHOT.jar"); + Assert.assertEquals(expectedSnapshotDependencies, snapshotDependencies); + + // verify project dependencies + List projectDependencies = layers.get(3); + List expectedProjectDependencies = + ImmutableList.of("app/", "app/libs/", "app/libs/lib.jar"); + Assert.assertEquals(expectedProjectDependencies, projectDependencies); + + // verify resources + List resources = layers.get(2); + List expectedResources = + ImmutableList.of( + "app/", "app/resources/", "app/resources/resource1.txt", "app/resources/resource2.txt"); + Assert.assertEquals(expectedResources, resources); + + // verify classes + List classes = layers.get(1); + List expectedClasses = + ImmutableList.of( + "app/", + "app/classes/", + "app/classes/com/", + "app/classes/com/test/", + "app/classes/com/test/HelloWorld.class"); + Assert.assertEquals(expectedClasses, classes); + + // verify extra files + List extraFiles = layers.get(0); + List expectedExtraFiles = ImmutableList.of("extra-file"); + Assert.assertEquals(expectedExtraFiles, extraFiles); + } + + @Test + public void testGradleLayerConfiguration_simpleModule() throws IOException { + multiTestProject.build(":simple-service:jibBuildTar"); + + Path jibTar = multiTestProject.getProjectRoot().resolve("simple-service/build/jib-image.tar"); + + List> layers = getLayers(jibTar); + + Assert.assertEquals(1, layers.size()); + + // the expected order is: + // no base image layers (scratch) + // classes (0) + + // verify classes + List classes = layers.get(0); + List expectedClasses = + ImmutableList.of( + "app/", + "app/classes/", + "app/classes/com/", + "app/classes/com/test/", + "app/classes/com/test/HelloWorld.class"); + Assert.assertEquals(expectedClasses, classes); + } + + // returns all files in layers (*.tar.gz) in a image tar + private List> getLayers(Path tar) throws IOException { + List> layers = new ArrayList<>(); + + try (TarArchiveInputStream image = new TarArchiveInputStream(Files.newInputStream(tar))) { + TarArchiveEntry entry; + while ((entry = image.getNextTarEntry()) != null) { + if (entry.getName().endsWith(".tar.gz")) { + @SuppressWarnings("resource") // must not close sub-streams + TarArchiveInputStream layer = new TarArchiveInputStream(new GZIPInputStream(image)); + TarArchiveEntry layerEntry; + List layerFiles = new ArrayList<>(); + while ((layerEntry = layer.getNextTarEntry()) != null) { + layerFiles.add(layerEntry.getName()); + } + layers.add(0, layerFiles); + } + } + } + return layers; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 7e482ec9ed..2f7e1e4d62 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -48,8 +48,11 @@ import org.gradle.api.GradleException; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.SourceSet; @@ -149,21 +152,43 @@ public JibContainerBuilder createContainerBuilder( Path resourcesOutputDirectory = mainSourceSet.getOutput().getResourcesDir().toPath(); FileCollection allFiles = mainSourceSet.getRuntimeClasspath().filter(File::exists); - FileCollection allDependencyFiles = + FileCollection projectDependencies = + project.files( + project + .getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) + .getResolvedConfiguration() + .getResolvedArtifacts() + .stream() + .filter( + artifact -> + artifact.getId().getComponentIdentifier() + instanceof ProjectComponentIdentifier) + .map(ResolvedArtifact::getFile) + .collect(Collectors.toList())); + + FileCollection nonProjectDependencies = allFiles .minus(classesOutputDirectories) + .minus(projectDependencies) .filter(file -> !file.toPath().equals(resourcesOutputDirectory)); - FileCollection snapshotDependencyFiles = - allDependencyFiles.filter(file -> file.getName().contains("SNAPSHOT")); - FileCollection dependencyFiles = allDependencyFiles.minus(snapshotDependencyFiles); + FileCollection snapshotDependencies = + nonProjectDependencies.filter(file -> file.getName().contains("SNAPSHOT")); + FileCollection dependencies = nonProjectDependencies.minus(snapshotDependencies); // Adds dependency files javaContainerBuilder .addDependencies( - dependencyFiles.getFiles().stream().map(File::toPath).collect(Collectors.toList())) + dependencies.getFiles().stream().map(File::toPath).collect(Collectors.toList())) .addSnapshotDependencies( - snapshotDependencyFiles + snapshotDependencies + .getFiles() + .stream() + .map(File::toPath) + .collect(Collectors.toList())) + .addProjectDependencies( + projectDependencies .getFiles() .stream() .map(File::toPath) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 9efa6f4321..59f5b11b38 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -51,6 +51,8 @@ import org.gradle.StartParameter; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; import org.gradle.api.internal.file.FileResolver; @@ -60,6 +62,7 @@ import org.gradle.api.logging.Logger; import org.gradle.api.logging.configuration.ConsoleOutput; import org.gradle.api.plugins.Convention; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.PluginContainer; import org.gradle.api.plugins.WarPlugin; @@ -198,6 +201,21 @@ public void setup() throws URISyntaxException, IOException { Mockito.when(mockGradle.getStartParameter()).thenReturn(mockStartParameter); Mockito.when(mockStartParameter.getConsoleOutput()).thenReturn(ConsoleOutput.Auto); + // mocking to complete ignore project dependency resolution + Mockito.when(mockProject.getConfigurations()) + .thenReturn(Mockito.mock(ConfigurationContainer.class, Mockito.RETURNS_DEEP_STUBS)); + Mockito.when( + mockProject + .getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) + .getResolvedConfiguration() + .getResolvedArtifacts()) + .thenReturn(ImmutableSet.of()); + ConfigurableFileCollection emptyFileCollection = Mockito.mock(ConfigurableFileCollection.class); + Mockito.when(emptyFileCollection.getFiles()).thenReturn(ImmutableSet.of()); + Mockito.when(mockProject.files(ImmutableList.of())).thenReturn(emptyFileCollection); + // done mocking project dependency resolution + Set classesFiles = ImmutableSet.of(getResource("gradle/application/classes")); FileCollection classesFileCollection = new TestFileCollection(classesFiles); Path resourcesOutputDir = getResource("gradle/application/resources"); diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle new file mode 100644 index 0000000000..0b8f910658 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle @@ -0,0 +1 @@ +// this file doesn't do anything \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle new file mode 100644 index 0000000000..b9c6d8941b --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + flatDir { + dirs "libs" + } +} + +jib { + from { + image = "scratch" + } + extraDirectories { + paths = file('src/main/other-jib') + } +} + +sourceSets { + main { + resources { + srcDirs "src/main/extra-resources-1", "src/main/extra-resources-2" + } + } +} + +dependencies { + compile project(':lib') + compile name: "dependency-1.0.0" + compile name: "dependencyX-1.0.0-SNAPSHOT" +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependency-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/libs/dependencyX-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd68926802ed2f42df108b18fa84681be324ae42 GIT binary patch literal 770 zcmWIWW@h1HVBp|j*cqkm!vF+KAOZ+Df!NnI#8KDN&rP41ApoxMWTdvwrh2A#(m(~0 zKrDi+(AUw=)6F$FM9k6RUH3HY=W$WPI3F<;f$>@(8#yf#wY7k_r*l!oSd4nQ}Uj${8L}2M(zxj&f2_7ErhM{W<=_n zn4f~eo6K)zRC^cf+H0w3%Q5Xy*zsE*RH`BiEP}b`Ki+g8_+Lz`)irD5-1%SIF39Qs zoO*Ur@Ar3oPHDUEhDL3?>+Q7qw)M9Kk4!q8`rnttMBF^S{!xl#r3lAa<+m+oY=74| ztdO;m7tj8tqc>raKO;x_2?gyr!c&`++aGxyTx7A%fHPfhzr%dbB`Py~&sd#mdYa21 zI)_6%z%|#rX5JkJzr#O0%4FTFo4zht_mMxqn~_O`8F!=ugB1t_7~VR9Xm}Dp)`l$! zfE0tkl15D+3D<^{AdpSK7Vlu~V2jIuOr)qq*NC19AR0kdFGSUdnkE9gS=m5JS%B~- Lkp2uzL<|f7lVIYI literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle new file mode 100644 index 0000000000..313ac21fc2 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java new file mode 100644 index 0000000000..0873623756 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.lib; + +/** Shared Code! */ +public class Lib { + + public String getThing() { + return "thing"; + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt new file mode 100644 index 0000000000..32f95c0d12 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle new file mode 100644 index 0000000000..748c7dbf3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle @@ -0,0 +1,3 @@ +include ':simple-service' +include ':complex-service' +include ':lib' \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle new file mode 100644 index 0000000000..56a4b986a5 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jib { + from { + image = "scratch" + } + // only use buildTar +} \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 1094908a63..ed3b0e6c58 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) + ### Fixed ## 1.3.0 From 50d6097a68251e3e165e644c41e5f9798e6a4b67 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 21 Jun 2019 10:55:36 -0400 Subject: [PATCH 0635/2020] CHANGELOG.md for Spring Boot bootWar support (#1797) --- jib-gradle-plugin/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 36d5c86cdb..87865969d9 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'` ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) +- Now automatically supports WAR created by the Spring Boot Gradle Plugin via the `bootWar` task ([#1786](https://github.com/GoogleContainerTools/jib/issues/1786)) - Can now use `jib.from.image = 'scratch'` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) ### Changed From f9b24a2eb63d430d5a374f17407dca687aa6e0b4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 24 Jun 2019 13:37:57 -0400 Subject: [PATCH 0636/2020] Leverage Nullaway checks using Optional for async step results (#1804) * Return Optional instead of nullable --- .../builder/steps/AuthenticatePushStep.java | 19 ++++++++-------- .../jib/builder/steps/PullBaseImageStep.java | 3 ++- .../tools/jib/builder/steps/PushBlobStep.java | 5 +++-- .../steps/PushContainerConfigurationStep.java | 5 +++-- .../jib/builder/steps/PushImageStep.java | 5 +++-- .../jib/builder/steps/PushLayerStep.java | 7 +++--- .../RetrieveRegistryCredentialsStep.java | 22 +++++++++---------- .../tools/jib/builder/steps/StepsRunner.java | 15 +++++++------ .../RetrieveRegistryCredentialsStepTest.java | 14 +++++++----- 9 files changed, 51 insertions(+), 44 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 2510c5f3d5..67b6547329 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import java.io.IOException; +import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; @@ -34,26 +35,25 @@ * @see https://docs.docker.com/registry/spec/auth/token/ */ -class AuthenticatePushStep implements Callable { +class AuthenticatePushStep implements Callable> { private static final String DESCRIPTION = "Authenticating push to %s"; private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final Credential registryCredential; + @Nullable private final Credential registryCredential; AuthenticatePushStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Credential registryCredential) { + @Nullable Credential registryCredential) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registryCredential = registryCredential; } @Override - @Nullable - public Authorization call() throws IOException, RegistryException { + public Optional call() throws IOException, RegistryException { String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("authenticating push to " + registry, 1); @@ -66,15 +66,16 @@ public Authorization call() throws IOException, RegistryException { .newRegistryClient() .getRegistryAuthenticator(); if (registryAuthenticator != null) { - return registryAuthenticator.authenticatePush(registryCredential); + return Optional.of(registryAuthenticator.authenticatePush(registryCredential)); } } catch (InsecureRegistryException ex) { // Cannot skip certificate validation or use HTTP; fall through. } return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) - ? null - : Authorization.fromBasicCredentials( - registryCredential.getUsername(), registryCredential.getPassword()); + ? Optional.empty() + : Optional.of( + Authorization.fromBasicCredentials( + registryCredential.getUsername(), registryCredential.getPassword())); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 0fbae70398..c6dab7ed2f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -136,7 +136,8 @@ public ImageAndAuthorization call() Credential registryCredential = RetrieveRegistryCredentialsStep.forBaseImage( buildConfiguration, progressEventDispatcher.newChildProducer()) - .call(); + .call() + .orElse(null); Authorization registryAuthorization = registryCredential == null || registryCredential.isOAuth2RefreshToken() diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 4a563802db..d307f23df0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; import java.util.concurrent.Callable; +import javax.annotation.Nullable; /** Pushes a BLOB to the target registry. */ class PushBlobStep implements Callable { @@ -37,14 +38,14 @@ class PushBlobStep implements Callable { private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDipatcherFactory; - private final Authorization authorization; + @Nullable private final Authorization authorization; private final BlobDescriptor blobDescriptor; private final Blob blob; PushBlobStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDipatcherFactory, - Authorization authorization, + @Nullable Authorization authorization, BlobDescriptor blobDescriptor, Blob blob) { this.buildConfiguration = buildConfiguration; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index ef74f438d3..87e12d064b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import java.io.IOException; import java.util.concurrent.Callable; +import javax.annotation.Nullable; /** Pushes the container configuration. */ class PushContainerConfigurationStep implements Callable { @@ -38,13 +39,13 @@ class PushContainerConfigurationStep implements Callable { private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final Authorization pushAuthorization; + @Nullable private final Authorization pushAuthorization; private final Image builtImage; PushContainerConfigurationStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Authorization authenticatePushStep, + @Nullable Authorization authenticatePushStep, Image builtImage) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 7cbb0172e4..df29bb8672 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; /** Pushes the final image. Outputs the pushed image digest. */ class PushImageStep implements Callable { @@ -46,7 +47,7 @@ class PushImageStep implements Callable { private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final Authorization pushAuthorization; + @Nullable private final Authorization pushAuthorization; private final BlobDescriptor containerConfigurationDigestAndSize; private final Image builtImage; @@ -57,7 +58,7 @@ class PushImageStep implements Callable { ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Authorization pushAuthorization, + @Nullable Authorization pushAuthorization, BlobDescriptor containerConfigurationDigestAndSize, Image builtImage) { this.listeningExecutorService = listeningExecutorService; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 26e9b552b8..56d47a973c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -30,6 +30,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import javax.annotation.Nullable; class PushLayerStep implements Callable { @@ -38,13 +39,13 @@ class PushLayerStep implements Callable { private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final Authorization pushAuthorization; + @Nullable private final Authorization pushAuthorization; private final Future cachedLayerAndName; static ImmutableList makeList( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Authorization pushAuthorization, + @Nullable Authorization pushAuthorization, List> cachedLayers) { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION); @@ -68,7 +69,7 @@ static ImmutableList makeList( PushLayerStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Authorization pushAuthorization, + @Nullable Authorization pushAuthorization, Future cachedLayerAndName) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 15da9bc07f..fa7422a557 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -22,14 +22,14 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableList; import java.util.Optional; import java.util.concurrent.Callable; -import javax.annotation.Nullable; /** Attempts to retrieve registry credentials. */ -class RetrieveRegistryCredentialsStep implements Callable { +class RetrieveRegistryCredentialsStep implements Callable> { /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( @@ -71,28 +71,26 @@ static RetrieveRegistryCredentialsStep forTargetImage( } @Override - @Nullable - public Credential call() throws CredentialRetrievalException { + public Optional call() throws CredentialRetrievalException { String description = "Retrieving registry credentials for " + registry; - buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + eventHandlers.dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("retrieving credentials for " + registry, 1); - TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { + TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { for (CredentialRetriever credentialRetriever : credentialRetrievers) { Optional optionalCredential = credentialRetriever.retrieve(); if (optionalCredential.isPresent()) { - return optionalCredential.get(); + return optionalCredential; } } // If no credentials found, give an info (not warning because in most cases, the base image is // public and does not need extra credentials) and return null. - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.info("No credentials could be retrieved for registry " + registry)); - return null; + eventHandlers.dispatch( + LogEvent.info("No credentials could be retrieved for registry " + registry)); + return Optional.empty(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 29ee23650c..3b3ae78c77 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -34,6 +34,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -62,8 +63,8 @@ private static Future failedFuture() { private Future>> baseImageLayers = failedFuture(); @Nullable private List> applicationLayers; private Future builtImage = failedFuture(); - private Future targetRegistryCredentials = failedFuture(); - private Future pushAuthorization = failedFuture(); + private Future> targetRegistryCredentials = failedFuture(); + private Future> pushAuthorization = failedFuture(); private Future>> baseImageLayerPushResults = failedFuture(); private Future>> applicationLayerPushResults = failedFuture(); private Future containerConfigurationPushResult = failedFuture(); @@ -129,7 +130,7 @@ private void authenticatePush() { new AuthenticatePushStep( buildConfiguration, childProgressDispatcherFactory, - results.targetRegistryCredentials.get()) + results.targetRegistryCredentials.get().orElse(null)) .call()); } @@ -167,7 +168,7 @@ private void pushBaseImageLayers() { PushLayerStep.makeList( buildConfiguration, childProgressDispatcherFactory, - results.pushAuthorization.get(), + results.pushAuthorization.get().orElse(null), results.baseImageLayers.get()))); } @@ -207,7 +208,7 @@ private void pushContainerConfiguration() { new PushContainerConfigurationStep( buildConfiguration, childProgressDispatcherFactory, - results.pushAuthorization.get(), + results.pushAuthorization.get().orElse(null), results.builtImage.get()) .call()); } @@ -223,7 +224,7 @@ private void pushApplicationLayers() { PushLayerStep.makeList( buildConfiguration, childProgressDispatcherFactory, - results.pushAuthorization.get(), + results.pushAuthorization.get().orElse(null), Verify.verifyNotNull(results.applicationLayers)))); } @@ -241,7 +242,7 @@ private void pushImage() { executorService, buildConfiguration, childProgressDispatcherFactory, - results.pushAuthorization.get(), + results.pushAuthorization.get().orElse(null), results.containerConfigurationPushResult.get(), results.builtImage.get()) .call(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 2121f33c81..66a4049cb0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -58,13 +58,13 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio () -> Optional.of(Credential.from("ignored", "ignored")))); Assert.assertEquals( - Credential.from("baseusername", "basepassword"), + Optional.of(Credential.from("baseusername", "basepassword")), RetrieveRegistryCredentialsStep.forBaseImage( buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); Assert.assertEquals( - Credential.from("targetusername", "targetpassword"), + Optional.of(Credential.from("targetusername", "targetpassword")), RetrieveRegistryCredentialsStep.forTargetImage( buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) @@ -76,22 +76,24 @@ public void testCall_none() throws CredentialRetrievalException, IOException { BuildConfiguration buildConfiguration = makeFakeBuildConfiguration( Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); - Assert.assertNull( + Assert.assertFalse( RetrieveRegistryCredentialsStep.forBaseImage( buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call()); + .call() + .isPresent()); Mockito.verify(mockEventHandlers, Mockito.atLeastOnce()) .dispatch(Mockito.any(ProgressEvent.class)); Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); - Assert.assertNull( + Assert.assertFalse( RetrieveRegistryCredentialsStep.forTargetImage( buildConfiguration, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call()); + .call() + .isPresent()); Mockito.verify(mockEventHandlers) .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); From 3e0280b0493394caf95ebad5e79edf403aa342e5 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 24 Jun 2019 17:37:35 -0400 Subject: [PATCH 0637/2020] add v2.2 manifest list template (#1805) * add v2.2 manifest list template --- .../image/json/V22ManifestListTemplate.java | 131 ++++++++++++++++++ .../json/V22ManifestListTemplateTest.java | 65 +++++++++ .../resources/core/json/v22manifest_list.json | 37 +++++ 3 files changed, 233 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java create mode 100644 jib-core/src/test/resources/core/json/v22manifest_list.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java new file mode 100644 index 0000000000..a4a5be2616 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * JSON Template for Docker Manifest List Schema V2.2 + * + *

Example manifest list JSON: + * + *

{@code
+ * {
+ *   "schemaVersion": 2,
+ *   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+ *   "manifests": [
+ *     {
+ *       "mediaType": "application/vnd.docker.image.manifest.v2+json",
+ *       "size": 7143,
+ *       "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
+ *       "platform": {
+ *         "architecture": "ppc64le",
+ *         "os": "linux",
+ *       }
+ *     },
+ *     {
+ *       "mediaType": "application/vnd.docker.image.manifest.v2+json",
+ *       "size": 7682,
+ *       "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
+ *       "platform": {
+ *         "architecture": "amd64",
+ *         "os": "linux",
+ *         "features": [
+ *           "sse4"
+ *         ]
+ *       }
+ *     }
+ *   ]
+ * }
+ * }
+ * + * @see Image Manifest + * Version 2, Schema 2: Manifest List + */ +public class V22ManifestListTemplate implements ManifestTemplate { + + public static final String MANIFEST_MEDIA_TYPE = + "application/vnd.docker.distribution.manifest.list.v2+json"; + private static final int SCHEMA_VERSION = 2; + + @Override + public int getSchemaVersion() { + return SCHEMA_VERSION; + } + + @Nullable private List manifests; + + @VisibleForTesting + List getManifests() { + return Preconditions.checkNotNull(manifests); + } + + public List getDigestsForPlatform(String architecture, String os) { + return getManifests() + .stream() + .filter( + manifest -> + manifest.platform != null + && os.equals(manifest.platform.os) + && architecture.equals(manifest.platform.architecture)) + .map(ManifestDescriptorTemplate::getDigest) + .collect(Collectors.toList()); + } + + /** + * Template for inner JSON object representing content descriptor for a layer or container + * configuration. + * + * @see OCI + * Content Descriptors + */ + static class ManifestDescriptorTemplate implements JsonTemplate { + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Platform implements JsonTemplate { + @Nullable private String architecture; + @Nullable private String os; + } + + @Nullable private String mediaType; + @Nullable private String digest; + private long size; + @Nullable private Platform platform; + + @Nullable + private String getDigest() { + return digest; + } + + @Nullable + public String getMediaType() { + return mediaType; + } + + @VisibleForTesting + @Nullable + Platform getPlatform() { + return platform; + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java new file mode 100644 index 0000000000..0db48e9cc3 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate.ManifestDescriptorTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class V22ManifestListTemplateTest { + + @Test + public void testFromJson() throws IOException, URISyntaxException, DigestException { + Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + + V22ManifestListTemplate manifestListJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, V22ManifestListTemplate.class); + + Assert.assertEquals(2, manifestListJson.getSchemaVersion()); + List manifests = manifestListJson.getManifests(); + Assert.assertEquals(3, manifests.size()); + + List validPlatformPPC = manifestListJson.getDigestsForPlatform("ppc64le", "linux"); + Assert.assertEquals(1, validPlatformPPC.size()); + Assert.assertEquals( + "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + validPlatformPPC.get(0)); + + List validPlatformAMD = manifestListJson.getDigestsForPlatform("amd64", "linux"); + Assert.assertEquals(2, validPlatformAMD.size()); + Assert.assertEquals( + "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + validPlatformAMD.get(0)); + Assert.assertEquals( + "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + validPlatformAMD.get(1)); + + List invalidArch = manifestListJson.getDigestsForPlatform("amd72", "linux"); + Assert.assertEquals(0, invalidArch.size()); + + List invalidOs = manifestListJson.getDigestsForPlatform("amd64", "minix"); + Assert.assertEquals(0, invalidOs.size()); + } +} diff --git a/jib-core/src/test/resources/core/json/v22manifest_list.json b/jib-core/src/test/resources/core/json/v22manifest_list.json new file mode 100644 index 0000000000..a9df3947b0 --- /dev/null +++ b/jib-core/src/test/resources/core/json/v22manifest_list.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "features": [ + "sse4" + ] + } + }, + { + "mediaType": "application/vnd.docker.image.manifest.v2+json", + "size": 7682, + "digest": "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.version": "potato-9" + } + } + ] +} \ No newline at end of file From 740c2acca5485aa547abe32791a4614155382217 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 24 Jun 2019 23:36:00 -0400 Subject: [PATCH 0638/2020] Re-enable cross-repository blob mounts (#1793) * Nothing uses RegistryCredentials * Enable cross-repository blob mounts * move authorization check from PushBlobStep to RegistryClient; renames * add missing @Nullable * add java-jwt dependency * Add CHANGELOG * Review notes: - be more explicit for JWT payload source - use JsonTemplateMapper rather than third-party library for parsing JWT - simplify RegistryClient factory use * review comments * Move the docker-token related checks into RegistryClient * canMountBlobs -> canAttemptBlobMount, fix embarrassing inverted test * Add ability to disable blob-mounts with `jib.blobMounts` flag * whitespace --- .../tools/jib/builder/steps/PushBlobStep.java | 12 +- .../jib/configuration/BuildConfiguration.java | 12 ++ .../tools/jib/global/JibSystemProperties.java | 14 ++ .../cloud/tools/jib/http/Authorization.java | 16 +- .../tools/jib/json/JsonTemplateMapper.java | 14 ++ .../jib/registry/RegistryAuthenticator.java | 65 ++++++-- .../tools/jib/registry/RegistryClient.java | 126 +++++++++++++++- .../RegistryEndpointRequestProperties.java | 19 +++ .../credentials/RegistryCredentials.java | 48 ------ .../jib/global/JibSystemPropertiesTest.java | 30 ++++ .../AuthenticationMethodRetrieverTest.java | 4 +- .../DockerRegistryBearerTokenTest.java | 139 ++++++++++++++++++ .../registry/RegistryAuthenticatorTest.java | 21 ++- jib-gradle-plugin/CHANGELOG.md | 2 + jib-maven-plugin/CHANGELOG.md | 2 + 15 files changed, 442 insertions(+), 82 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index d307f23df0..19f4eafa6b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -79,8 +79,16 @@ public BlobDescriptor call() throws IOException, RegistryException { return blobDescriptor; } - // todo: leverage cross-repository mounts - registryClient.pushBlob(blobDescriptor.getDigest(), blob, null, throttledProgressReporter); + // If base and target images are in the same registry, then use mount/from to try mounting the + // BLOB from the base image repository to the target image repository and possibly avoid + // having to push the BLOB. See + // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. + String baseRegistry = buildConfiguration.getBaseImageConfiguration().getImageRegistry(); + String baseRepository = buildConfiguration.getBaseImageConfiguration().getImageRepository(); + String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; + registryClient.pushBlob( + blobDescriptor.getDigest(), blob, sourceRepository, throttledProgressReporter); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 3723eae8a9..6e84db8589 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -451,6 +451,18 @@ public RegistryClient.Factory newBaseImageRegistryClientFactory() { * @return a new {@link RegistryClient.Factory} */ public RegistryClient.Factory newTargetImageRegistryClientFactory() { + // if base and target are on the same registry, try enabling cross-repository mounts + if (baseImageConfiguration + .getImageRegistry() + .equals(targetImageConfiguration.getImageRegistry())) { + return RegistryClient.factory( + getEventHandlers(), + targetImageConfiguration.getImageRegistry(), + targetImageConfiguration.getImageRepository(), + baseImageConfiguration.getImageRepository()) + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setUserAgentSuffix(getToolName()); + } return newRegistryClientFactory(targetImageConfiguration); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index 9f8829558e..cf78836645 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -25,6 +25,8 @@ public class JibSystemProperties { @VisibleForTesting public static final String HTTP_TIMEOUT = "jib.httpTimeout"; + @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; + @VisibleForTesting public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; @@ -46,6 +48,18 @@ public static int getHttpTimeout() { return Integer.getInteger(HTTP_TIMEOUT); } + /** + * Gets whether or not to use cross-repository blob mounts when uploading image layers + * ({@code mount/from}). This is defined by the {@code jib.blobMounts} system property. + * + * @return {@code true} if {@code mount/from} should be used, {@code false} if not, defaulting to + * {@code true} + */ + public static boolean useCrossRepositoryBlobMounts() { + return System.getProperty(CROSS_REPOSITORY_BLOB_MOUNTS) == null + || Boolean.getBoolean(CROSS_REPOSITORY_BLOB_MOUNTS); + } + /** * Gets whether or not to serialize Jib's execution. This is defined by the {@code jibSerialize} * system property. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index 7c764f4f2f..ff1f365862 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -29,14 +29,6 @@ */ public class Authorization { - /** - * @param token the token - * @return an {@link Authorization} with a {@code Bearer} token - */ - public static Authorization fromBearerToken(String token) { - return new Authorization("Bearer", token); - } - /** * @param username the username * @param secret the secret @@ -56,6 +48,14 @@ public static Authorization fromBasicToken(String token) { return new Authorization("Basic", token); } + /** + * @param token the token + * @return an {@link Authorization} with a {@code Bearer} token + */ + public static Authorization fromBearerToken(String token) { + return new Authorization("Bearer", token); + } + private final String scheme; private final String token; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index fe462059f6..45747d3f29 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -104,6 +104,20 @@ public static T readJson(String jsonString, Class te return objectMapper.readValue(jsonString, templateClass); } + /** + * Deserializes a JSON object from a JSON byte array. + * + * @param child type of {@link JsonTemplate} + * @param jsonBytes a JSON byte array + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonBytes} + * @throws IOException if an error occurred during parsing the JSON + */ + public static T readJson(byte[] jsonBytes, Class templateClass) + throws IOException { + return objectMapper.readValue(jsonBytes, templateClass); + } + /** * Deserializes a JSON object list from a JSON string. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index f67f08644a..7820595a1c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; import com.google.common.net.MediaType; import java.io.IOException; @@ -37,6 +38,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -180,26 +183,30 @@ public Authorization authenticatePush(@Nullable Credential credential) } @VisibleForTesting - String getServiceScopeRequestParameters(String scope) { - return "service=" - + service - + "&scope=repository:" - + registryEndpointRequestProperties.getImageName() - + ":" - + scope; + String getServiceScopeRequestParameters(Map repositoryScopes) { + StringBuilder parameters = new StringBuilder("service=").append(service); + for (Entry pair : repositoryScopes.entrySet()) { + parameters + .append("&scope=repository:") + .append(pair.getKey()) + .append(":") + .append(pair.getValue()); + } + return parameters.toString(); } @VisibleForTesting - URL getAuthenticationUrl(@Nullable Credential credential, String scope) + URL getAuthenticationUrl(@Nullable Credential credential, Map repositoryScopes) throws MalformedURLException { return isOAuth2Auth(credential) ? new URL(realm) // Required parameters will be sent via POST . - : new URL(realm + "?" + getServiceScopeRequestParameters(scope)); + : new URL(realm + "?" + getServiceScopeRequestParameters(repositoryScopes)); } @VisibleForTesting - String getAuthRequestParameters(@Nullable Credential credential, String scope) { - String serviceScope = getServiceScopeRequestParameters(scope); + String getAuthRequestParameters( + @Nullable Credential credential, Map repositoryScopes) { + String serviceScope = getServiceScopeRequestParameters(repositoryScopes); return isOAuth2Auth(credential) ? serviceScope // https://github.com/GoogleContainerTools/jib/pull/1545 @@ -227,15 +234,43 @@ boolean isOAuth2Auth(@Nullable Credential credential) { */ private Authorization authenticate(@Nullable Credential credential, String scope) throws RegistryAuthenticationFailedException { + // try authorizing against both the main repository and the source repository too + // to enable cross-repository mounts on pushes + if (registryEndpointRequestProperties.getSourceImageName() != null) { + try { + Map scopes = + ImmutableMap.of( + registryEndpointRequestProperties.getImageName(), + scope, + registryEndpointRequestProperties.getSourceImageName(), + "pull"); + Authorization auth = authenticate(credential, scopes); + if (auth != null) { + return auth; + } + } catch (RegistryAuthenticationFailedException ex) { + // Unable to obtain authorization with source image: fallthrough and try without + } + } + Map repositoryScopes = + ImmutableMap.of(registryEndpointRequestProperties.getImageName(), scope); + Authorization auth = authenticate(credential, repositoryScopes); + return auth; + } + + private Authorization authenticate( + @Nullable Credential credential, Map repositoryScopes) + throws RegistryAuthenticationFailedException { try (Connection connection = - Connection.getConnectionFactory().apply(getAuthenticationUrl(credential, scope))) { + Connection.getConnectionFactory() + .apply(getAuthenticationUrl(credential, repositoryScopes))) { Request.Builder requestBuilder = Request.builder() .setHttpTimeout(JibSystemProperties.getHttpTimeout()) .setUserAgent(userAgent); if (isOAuth2Auth(credential)) { - String parameters = getAuthRequestParameters(credential, scope); + String parameters = getAuthRequestParameters(credential, repositoryScopes); requestBuilder.setBody( new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString())); } else if (credential != null) { @@ -257,9 +292,9 @@ private Authorization authenticate(@Nullable Credential credential, String scope registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), "Did not get token in authentication response from " - + getAuthenticationUrl(credential, scope) + + getAuthenticationUrl(credential, repositoryScopes) + "; parameters: " - + getAuthRequestParameters(credential, scope)); + + getAuthRequestParameters(credential, repositoryScopes)); } return Authorization.fromBearerToken(responseJson.getToken()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b1b7e9e2c9..4dfe8dd52b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; @@ -30,12 +32,18 @@ import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Multimap; import java.io.IOException; import java.net.URL; +import java.util.List; import java.util.function.Consumer; +import java.util.stream.Stream; import javax.annotation.Nullable; /** Interfaces with a registry. */ @@ -141,6 +149,92 @@ public static Factory factory(EventHandlers eventHandlers, String serverUrl, Str return new Factory(eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName)); } + public static Factory factory( + EventHandlers eventHandlers, String serverUrl, String imageName, String sourceImageName) { + return new Factory( + eventHandlers, + new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName)); + } + + /** + * A simple class representing the payload of a Docker Registry v2 Bearer Token + * which lists the set of access claims granted. + * + *
+   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
+   * 
+ * + * @see AccessClaim + */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenPayloadTemplate implements JsonTemplate { + @Nullable private List access; + } + + /** + * Represents an access claim for a repository in a Docker Registry Bearer Token payload. + * + *
{"type": "repository","name": "library/openjdk","actions":["push","pull"]}
+ */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AccessClaim implements JsonTemplate { + @Nullable private String type; + @Nullable private String name; + @Nullable private List actions; + } + + /** + * Decode the Docker Registry v2 Bearer + * Token to list the granted repositories with their levels of access. + * + * @param token a Docker Registry Bearer Token + * @return a mapping of repository to granted access scopes, or {@code null} if the token is not a + * Docker Registry Bearer Token + */ + @VisibleForTesting + @Nullable + static Multimap decodeTokenRepositoryGrants(String token) { + // Docker Registry Bearer Tokens are based on JWT. A valid JWT is a set of 3 base64-encoded + // parts (header, payload, signature), collated with a ".". The header and payload are + // JSON objects. + String[] jwtParts = token.split("\\.", -1); + byte[] payloadData; + if (jwtParts.length != 3 || (payloadData = Base64.decodeBase64(jwtParts[1])) == null) { + return null; + } + + // The payload looks like: + // { + // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], + // "aud":"registry.docker.io", + // "iss":"auth.docker.io", + // "exp":999, + // "iat":999, + // "jti":"zzzz", + // "nbf":999, + // "sub":"e3ae001d-xxx" + // } + // + try { + TokenPayloadTemplate payload = + JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + if (payload.access == null) { + return null; + } + return payload + .access + .stream() + .filter(claim -> "repository".equals(claim.type)) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + claim -> claim.name, + claim -> claim.actions == null ? Stream.empty() : claim.actions.stream())); + } catch (IOException ex) { + return null; + } + } + private final EventHandlers eventHandlers; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; @@ -290,6 +384,13 @@ public boolean pushBlob( @Nullable String sourceRepository, Consumer writtenByteCountListener) throws IOException, RegistryException { + + if (sourceRepository != null + && !(JibSystemProperties.useCrossRepositoryBlobMounts() + && canAttemptBlobMount(authorization, sourceRepository))) { + // don't bother requesting a cross-repository blob-mount if we don't have access + sourceRepository = null; + } BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); @@ -298,8 +399,8 @@ public boolean pushBlob( try (TimerEventDispatcher timerEventDispatcher2 = timerEventDispatcher.subTimer("pushBlob POST " + blobDigest)) { - // POST /v2//blobs/uploads/ OR // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} + // POST /v2//blobs/uploads/ URL patchLocation = callRegistryEndpoint(blobPusher.initializer()); if (patchLocation == null) { // The BLOB exists already. @@ -323,6 +424,29 @@ public boolean pushBlob( } } + /** + * Check if the authorization allows using the specified repository can be mounted by the remote + * registry as a source for blobs. More specifically, we can only check if the repository is not + * disallowed. + * + * @param repository repository in question + * @return {@code true} if the repository appears to be mountable + */ + @VisibleForTesting + static boolean canAttemptBlobMount(@Nullable Authorization authorization, String repository) { + if (authorization == null || !"bearer".equalsIgnoreCase(authorization.getScheme())) { + // Authorization methods other than the Docker Container Registry Token don't provide + // information as to which repositories are accessible. The caller should attempt the mount + // and rely on the registry fallback as required by the spec. + // https://docs.docker.com/registry/spec/api/#pushing-an-image + return true; + } + // if null then does not appear to be a DCRT + Multimap repositoryGrants = + decodeTokenRepositoryGrants(authorization.getToken()); + return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull"); + } + /** @return the registry endpoint's API root, without the protocol */ @VisibleForTesting String getApiRouteBase() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index 4483344f74..2e125ec847 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -16,19 +16,33 @@ package com.google.cloud.tools.jib.registry; +import javax.annotation.Nullable; + /** Properties of registry endpoint requests. */ class RegistryEndpointRequestProperties { private final String serverUrl; private final String imageName; + @Nullable private final String sourceImageName; /** * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) */ RegistryEndpointRequestProperties(String serverUrl, String imageName) { + this(serverUrl, imageName, null); + } + + /** + * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) + * @param imageName the image/repository name (also known as, namespace) + * @param sourceImageName additional source image to request pull permission from the registry + */ + RegistryEndpointRequestProperties( + String serverUrl, String imageName, @Nullable String sourceImageName) { this.serverUrl = serverUrl; this.imageName = imageName; + this.sourceImageName = sourceImageName; } String getServerUrl() { @@ -38,4 +52,9 @@ String getServerUrl() { String getImageName() { return imageName; } + + @Nullable + String getSourceImageName() { + return sourceImageName; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java deleted file mode 100644 index e2fc2ccb14..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry.credentials; - -import com.google.cloud.tools.jib.http.Authorization; - -/** - * Stores retrieved registry credentials and their source. - * - *

The credentials are referred to by the registry they are used for. - */ -public class RegistryCredentials { - - private final Authorization authorization; - - /** - * A string representation of where the credentials were retrieved from. This is useful for - * letting the user know which credentials were used. - */ - private final String credentialSource; - - public RegistryCredentials(String credentialSource, Authorization authorization) { - this.authorization = authorization; - this.credentialSource = credentialSource; - } - - public Authorization getAuthorization() { - return authorization; - } - - public String getCredentialSource() { - return credentialSource; - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index f055a3c789..4fca87bef0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -45,6 +45,7 @@ public void tearDown() { if (httpsProxyPortSaved != null) { System.setProperty("https.proxyPort", httpsProxyPortSaved); } + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); } @Test @@ -152,4 +153,33 @@ public void testCheckHttpProxyPortProperty_stringValue() { Assert.assertEquals("https.proxyPort must be an integer: some string", ex.getMessage()); } } + + @Test + public void testUseBlobMountsPropertyName() { + Assert.assertEquals("jib.blobMounts", JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + } + + @Test + public void testUseBlobMounts_undefined() { + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_true() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "true"); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_false() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "false"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_other() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "nonbool"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 7eae71f001..d1e96c1787 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.http.Response; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collections; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -149,6 +150,7 @@ public void testHandleHttpResponseException_pass() Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), - registryAuthenticator.getAuthenticationUrl(null, "someScope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someImageName", "someScope"))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java new file mode 100644 index 0000000000..451d8ba1da --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.util.Base64; +import com.google.cloud.tools.jib.http.Authorization; +import com.google.common.collect.Multimap; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Tests for {@link RegistryClient} around handling of Docker Registry Bearer Tokens. + * + *

JWTs were generated from jwt.io's JWT Debugger. Set the + * algorithm to HS256, and paste the JSON shown as the Payload. + */ +public class DockerRegistryBearerTokenTest { + @Test + public void testDecode_dockerToken() { + // A genuine token from accessing docker.io's openjdk: + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull"]}] + // Generated by + // $ cd examples/helloworld + // $ mvn package jib:dockerBuild -Djib.from.image=openjdk \ + // -Djava.util.logging.config.file= + Multimap decoded = + RegistryClient.decodeTokenRepositoryGrants( + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" + + ".eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCJdfV0sImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5pbyIsImV4cCI6MTU2MTA0MzkwNSwiaWF0IjoxNTYxMDQzNjA1LCJpc3MiOiJhdXRoLmRvY2tlci5pbyIsImp0aSI6Ikc5bWpiOE9GeU5STFlpY3ZUMFZxIiwibmJmIjoxNTYxMDQzMzA1LCJzdWIiOiIifQ" + + ".jblwG_taIVf3IRiv200ivsc8q_IUj-M9QePKPAULfXdSZlY6H9n_XWtT6lw43k-J6QHfmnY4Yuh3eZq61KS7AT9yggM1VuolRCvYztSZ-MZHMIlvSE2KCc0wXa5gNQarjmDJloYduZuyLaKaRUUbO4osk1MuruODY_c2g2j16ce0Z8XVJ-7R8_J_Z8g0GdtFAfPO4bqpg9dj31MA8AKl3h-ru8NXcs3y1PkrYHpEGCgpcGcUQwLY7uiIrzjr0trCUbsLsv6iq2XTXnN_tTrfvL1R3yTB6gITvXZdsnU3r_UIDTzexTtlZWdntucJAGKX9HMA_jYEcTZ4ZhyEzETGpw"); + Assert.assertEquals(1, decoded.size()); + Assert.assertTrue(decoded.containsEntry("library/openjdk", "pull")); + Assert.assertFalse(decoded.containsEntry("library/openjdk", "push")); + Assert.assertFalse(decoded.containsEntry("randomrepo", "push")); + } + + @Test + public void testDecode_nonToken() { + String base64Text = + Base64.encodeBase64String( + "something other than a JWT token".getBytes(StandardCharsets.UTF_8)); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(base64Text); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessString() { + // a JWT with an "access" field that is not an array: {"access": "string"} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessArray() { + // a JWT with an "access" field that is an array of non-claim objects: {"access":["string"]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + @Ignore("Annotate AccessClaim.actions to disallow coercion of integers to strings") + public void testDecode_invalidToken_actionsArray() { + // a JWT with an "access" field that is an action array of non-strings: + // {"access":[{"type": "repository","name": "library/openjdk","actions":[1]}]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_randoJwt() { + // the JWT example token from jwt.io + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); + Assert.assertNull(decoded); + } + + /** Basic credential should allow access to all. */ + @Test + public void testCanAttemptBlobMount_basicCredential() { + Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "random")); + } + + /** Basic token should allow access to all. */ + @Test + public void testCanAttemptBlobMount_basicToken() { + // basic tokens are assumed to allow all repositories to be mounted + Authorization fixture = Authorization.fromBasicToken("gobbledygook"); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "library/openjdk")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "randomrepo")); + } + + @Test + public void testCanAttemptBlobMount_bearer_withToken() { + // a synthetic token for accessing docker.io's openjdk with push and pull + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull","push"]}]} + String token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"; + Authorization authorization = Authorization.fromBearerToken(token); + Assert.assertNotNull(authorization); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertFalse(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); + } + + @Test + public void testCanAttemptBlobMount_bearer_withNonToken() { + // non-Docker Registry Bearer Tokens are assumed to allow access to all + // the JWT example token from jwt.io + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Authorization authorization = Authorization.fromBearerToken(jwt); + Assert.assertNotNull(authorization); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index d6481b8271..0f498051e6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -24,6 +24,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.GeneralSecurityException; +import java.util.Collections; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -55,7 +56,8 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( @@ -64,14 +66,16 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test public void testAuthRequestParameters_basicAuth() { Assert.assertEquals( "service=someservice&scope=repository:someimage:scope", - registryAuthenticator.getAuthRequestParameters(null, "scope")); + registryAuthenticator.getAuthRequestParameters( + null, Collections.singletonMap("someimage", "scope"))); } @Test @@ -81,7 +85,8 @@ public void testAuthRequestParameters_oauth2() { "service=someservice&scope=repository:someimage:scope" + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + "&grant_type=refresh_token&refresh_token=oauth2_access_token", - registryAuthenticator.getAuthRequestParameters(credential, "scope")); + registryAuthenticator.getAuthRequestParameters( + credential, Collections.singletonMap("someimage", "scope"))); } @Test @@ -105,7 +110,8 @@ public void isOAuth2Auth_oauth2() { public void getAuthenticationUrl_basicAuth() throws MalformedURLException { Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test @@ -113,7 +119,7 @@ public void istAuthenticationUrl_oauth2() throws MalformedURLException { Credential credential = Credential.from("", "oauth2_token"); Assert.assertEquals( new URL("https://somerealm"), - registryAuthenticator.getAuthenticationUrl(credential, "scope")); + registryAuthenticator.getAuthenticationUrl(credential, Collections.emptyMap())); } @Test @@ -176,7 +182,8 @@ public void testFromAuthenticationMethod_noService() Assert.assertEquals( new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 87865969d9..3e786f2334 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) + ## 1.3.0 ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ed3b0e6c58..c9a260a56b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) + ## 1.3.0 ### Changed From 084c428efa91c93f4a384cd8313aea392355ddee Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 25 Jun 2019 13:24:19 -0400 Subject: [PATCH 0639/2020] fix javadoc comment for manifest list json template (#1810) * fix javadoc comment for manifest list json template --- .../tools/jib/image/json/V22ManifestListTemplate.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java index a4a5be2616..f758411e27 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java @@ -92,13 +92,7 @@ public List getDigestsForPlatform(String architecture, String os) { .collect(Collectors.toList()); } - /** - * Template for inner JSON object representing content descriptor for a layer or container - * configuration. - * - * @see OCI - * Content Descriptors - */ + /** Template for inner JSON object representing a single platform specific manifest. */ static class ManifestDescriptorTemplate implements JsonTemplate { @JsonIgnoreProperties(ignoreUnknown = true) From 9052912b3ac5f6be7ead607db2fb22d0e456c516 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 25 Jun 2019 15:50:45 -0400 Subject: [PATCH 0640/2020] Add test for proxy user/password properties (#1809) --- .../cloud/tools/jib/http/TestWebServer.java | 70 +++++++++++-------- .../jib/http/WithServerConnectionTest.java | 34 +++++++++ 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 3633b33302..8fee967e83 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -31,6 +31,8 @@ import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -47,14 +49,26 @@ public class TestWebServer implements Closeable { private final Semaphore threadStarted = new Semaphore(0); private final StringBuilder inputRead = new StringBuilder(); + private final List responses; + public TestWebServer(boolean https) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!")); + } + + public TestWebServer(boolean https, List responses) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { this.https = https; - serverSocket = createServerSocket(https); - ignoreReturn(executorService.submit(this::serve200)); + this.responses = responses; + serverSocket = https ? createHttpsServerSocket() : new ServerSocket(0); + ignoreReturn(executorService.submit(this::serveResponses)); threadStarted.acquire(); } + public int getLocalPort() { + return serverSocket.getLocalPort(); + } + public String getEndpoint() { String host = serverSocket.getInetAddress().getHostAddress(); return (https ? "https" : "http") + "://" + host + ":" + serverSocket.getLocalPort(); @@ -66,47 +80,45 @@ public void close() throws IOException { executorService.shutdown(); } - private ServerSocket createServerSocket(boolean https) + private ServerSocket createHttpsServerSocket() throws IOException, GeneralSecurityException, URISyntaxException { - if (https) { - KeyStore keyStore = KeyStore.getInstance("JKS"); - // generated with: keytool -genkey -keyalg RSA -keystore ./TestWebServer-keystore - Path keyStoreFile = Paths.get(Resources.getResource("core/TestWebServer-keystore").toURI()); - try (InputStream in = Files.newInputStream(keyStoreFile)) { - keyStore.load(in, "password".toCharArray()); - } + KeyStore keyStore = KeyStore.getInstance("JKS"); + // generated with: keytool -genkey -keyalg RSA -keystore ./TestWebServer-keystore + Path keyStoreFile = Paths.get(Resources.getResource("core/TestWebServer-keystore").toURI()); + try (InputStream in = Files.newInputStream(keyStoreFile)) { + keyStore.load(in, "password".toCharArray()); + } - KeyManagerFactory keyManagerFactory = - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, "password".toCharArray()); + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, "password".toCharArray()); - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagerFactory.getKeyManagers(), null, null); - return sslContext.getServerSocketFactory().createServerSocket(0); - } else { - return new ServerSocket(0); - } + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + return sslContext.getServerSocketFactory().createServerSocket(0); } - private Void serve200() throws IOException { + private Void serveResponses() throws IOException { threadStarted.release(); try (Socket socket = serverSocket.accept()) { - InputStream in = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - for (String line = reader.readLine(); - line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. - line = reader.readLine()) { - inputRead.append(line + "\n"); - } - String response = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; - socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); - socket.getOutputStream().flush(); + for (String response : responses) { + for (String line = reader.readLine(); + line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. + line = reader.readLine()) { + inputRead.append(line + "\n"); + } + socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); + socket.getOutputStream().flush(); + } } return null; } + // For use to ignore (i.e., accept and do nothing) a return value from ExecutionService.submit(). + // Without "consuming" the return value this way, Error Prone will complain to use it. private void ignoreReturn(Future future) { // do nothing; to make Error Prone happy } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index 52dd89eb78..678b92ff53 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -22,7 +22,9 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.Arrays; import javax.net.ssl.SSLException; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -89,4 +91,36 @@ public void testInsecureConnection() ByteStreams.toByteArray(response.getBody())); } } + + @Test + public void testProxyCredentialProperties() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + String proxyResponse = + "HTTP/1.1 407 Proxy Authentication Required\n" + + "Proxy-Authenticate: BASIC realm=\"some-realm\"\n" + + "Cache-Control: no-cache\n" + + "Pragma: no-cache\n" + + "Content-Length: 0\n\n"; + String targetServerResponse = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + + try (TestWebServer server = + new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse))) { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", String.valueOf(server.getLocalPort())); + System.setProperty("http.proxyUser", "user_sys_prop"); + System.setProperty("http.proxyPassword", "pass_sys_prop"); + + try (Connection connection = + Connection.getConnectionFactory().apply(new URL("http://does.not.matter"))) { + Response response = connection.send("GET", new Request.Builder().build()); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "Proxy-Authorization: Basic dXNlcl9zeXNfcHJvcDpwYXNzX3N5c19wcm9w")); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + } + } + } } From 5cc9ed79714d648e86bbd2a26449c398ca3d936a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 27 Jun 2019 17:13:35 -0400 Subject: [PATCH 0641/2020] Minor fixes (#1812) --- .../registry/BlobCheckerIntegrationTest.java | 13 ++++--- .../registry/BlobPullerIntegrationTest.java | 13 ++++--- .../ManifestPullerIntegrationTest.java | 16 ++++---- .../ManifestPusherIntegrationTest.java | 19 ++++----- .../tools/jib/cache/CacheStorageReader.java | 39 +++++++++---------- .../jib/image/json/ImageToJsonTranslator.java | 18 ++------- .../jib/cache/CacheStorageReaderTest.java | 2 +- .../json/V22ManifestListTemplateTest.java | 3 +- 8 files changed, 59 insertions(+), 64 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index b89082fd10..bb90d21af3 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.security.DigestException; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -31,9 +32,13 @@ public class BlobCheckerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - @Test - public void testCheck_exists() throws IOException, RegistryException, InterruptedException { + @BeforeClass + public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + + @Test + public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -46,9 +51,7 @@ public void testCheck_exists() throws IOException, RegistryException, Interrupte } @Test - public void testCheck_doesNotExist() - throws IOException, RegistryException, DigestException, InterruptedException { - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index be33010b24..da187049de 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -37,12 +38,15 @@ public class BlobPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + @BeforeClass + public static void setUp() throws IOException, InterruptedException { + localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void testPull() throws IOException, RegistryException, InterruptedException { - // Pulls the busybox image. - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testPull() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -69,8 +73,7 @@ public void testPull() throws IOException, RegistryException, InterruptedExcepti } @Test - public void testPull_unknownBlob() throws IOException, DigestException, InterruptedException { - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testPull_unknownBlob() throws IOException, DigestException { DescriptorDigest nonexistentDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 8d5539dd63..e4f82691f5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -24,6 +24,7 @@ import java.io.IOException; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -32,9 +33,13 @@ public class ManifestPullerIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - @Test - public void testPull_v21() throws IOException, RegistryException, InterruptedException { + @BeforeClass + public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + + @Test + public void testPull_v21() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) @@ -47,8 +52,7 @@ public void testPull_v21() throws IOException, RegistryException, InterruptedExc } @Test - public void testPull_v22() throws IOException, RegistryException, InterruptedException { - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); @@ -59,9 +63,7 @@ public void testPull_v22() throws IOException, RegistryException, InterruptedExc } @Test - public void testPull_unknownManifest() - throws RegistryException, IOException, InterruptedException { - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testPull_unknownManifest() throws RegistryException, IOException { try { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index f3aa9138bb..5765a3ca99 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.security.DigestException; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -36,18 +37,20 @@ public class ManifestPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final EventHandlers EVENT_HANDLERS = EventHandlers.NONE; - @Test - public void testPush_missingBlobs() throws IOException, RegistryException, InterruptedException { + @BeforeClass + public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); + } + @Test + public void testPush_missingBlobs() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "busybox") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") .setAllowInsecureRegistries(true) .newRegistryClient(); try { @@ -63,9 +66,7 @@ public void testPush_missingBlobs() throws IOException, RegistryException, Inter /** Tests manifest pushing. This test is a comprehensive test of push and pull. */ @Test - public void testPush() - throws DigestException, IOException, RegistryException, InterruptedException { - localRegistry.pullAndPushToLocal("busybox", "busybox"); + public void testPush() throws DigestException, IOException, RegistryException { Blob testLayerBlob = Blobs.from("crepecake"); // Known digest for 'crepecake' DescriptorDigest testLayerBlobDigest = @@ -83,7 +84,7 @@ public void testPush() // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory(EVENT_HANDLERS, "localhost:5000", "testimage") + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage") .setAllowInsecureRegistries(true) .newRegistryClient(); Assert.assertFalse( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index 2c70a9b61d..b9b4488836 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -162,28 +162,27 @@ Optional retrieve(DescriptorDigest layerDigest) return Optional.empty(); } - CachedLayer.Builder cachedLayerBuilder = CachedLayer.builder().setLayerDigest(layerDigest); - - try (Stream filesInLayerDirectory = Files.list(layerDirectory)) { - for (Path fileInLayerDirectory : filesInLayerDirectory.collect(Collectors.toList())) { - if (CacheStorageFiles.isLayerFile(fileInLayerDirectory)) { - if (cachedLayerBuilder.hasLayerBlob()) { - throw new CacheCorruptedException( - cacheStorageFiles.getCacheDirectory(), - "Multiple layer files found for layer with digest " - + layerDigest.getHash() - + " in directory: " - + layerDirectory); - } - cachedLayerBuilder - .setLayerBlob(Blobs.from(fileInLayerDirectory)) - .setLayerDiffId(cacheStorageFiles.getDiffId(fileInLayerDirectory)) - .setLayerSize(Files.size(fileInLayerDirectory)); - } + try (Stream files = Files.list(layerDirectory)) { + List layerFiles = + files.filter(CacheStorageFiles::isLayerFile).collect(Collectors.toList()); + if (layerFiles.size() != 1) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "No or multiple layer files found for layer with digest " + + layerDigest.getHash() + + " in directory: " + + layerDirectory); } - } - return Optional.of(cachedLayerBuilder.build()); + Path layerFile = layerFiles.get(0); + return Optional.of( + CachedLayer.builder() + .setLayerDigest(layerDigest) + .setLayerSize(Files.size(layerFile)) + .setLayerBlob(Blobs.from(layerFile)) + .setLayerDiffId(cacheStorageFiles.getDiffId(layerFile)) + .build()); + } } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java index 407c333348..ed2ac78181 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.Port; -import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.image.Image; @@ -36,18 +35,7 @@ import java.util.function.Function; import javax.annotation.Nullable; -/** - * Translates an {@link Image} into a manifest or container configuration JSON BLOB. - * - *

Example usage: - * - *

{@code
- * ImageToJsonTranslator translator = new ImageToJsonTranslator(image);
- * Blob containerConfigurationBlob = translator.getContainerConfigurationBlob();
- * BlobDescriptor containerConfigurationBlobDescriptor = blob.writeTo(outputStream);
- * Blob manifestBlob = translator.getManifestBlob(containerConfigurationBlobDescriptor);
- * }
- */ +/** Translates an {@link Image} into a manifest or container configuration JSON. */ public class ImageToJsonTranslator { /** @@ -141,9 +129,9 @@ public ImageToJsonTranslator(Image image) { } /** - * Gets the container configuration as a {@link Blob}. + * Gets the container configuration. * - * @return the container configuration {@link Blob} + * @return the container configuration */ public JsonTemplate getContainerConfiguration() { // Set up the JSON template. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 0e52bcbec3..315358f39f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -198,7 +198,7 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Assert.assertThat( ex.getMessage(), CoreMatchers.startsWith( - "Multiple layer files found for layer with digest " + "No or multiple layer files found for layer with digest " + layerDigest.getHash() + " in directory: " + cacheStorageFiles.getLayerDirectory(layerDigest))); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java index 0db48e9cc3..87002549f8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java @@ -23,7 +23,6 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.DigestException; import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -31,7 +30,7 @@ public class V22ManifestListTemplateTest { @Test - public void testFromJson() throws IOException, URISyntaxException, DigestException { + public void testFromJson() throws IOException, URISyntaxException { Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); V22ManifestListTemplate manifestListJson = From 15be096f8914af34358c8c4211a3e173c358c854 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 27 Jun 2019 18:35:28 -0400 Subject: [PATCH 0642/2020] Apply consistent async schedule pattern / decouple async aspect from PushImageStep (#1803) --- .../BuildAndCacheApplicationLayerStep.java | 22 ++- .../steps/PullAndCacheBaseImageLayerStep.java | 31 ++++ .../PullAndCacheBaseImageLayersStep.java | 59 ------- .../jib/builder/steps/PushImageStep.java | 132 +++++++------- .../jib/builder/steps/PushLayerStep.java | 19 +- .../tools/jib/builder/steps/StepsRunner.java | 166 +++++++++--------- .../jib/builder/steps/BuildImageStepTest.java | 2 +- 7 files changed, 202 insertions(+), 229 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 4a4de43f40..632720f85a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -34,7 +35,7 @@ /** Builds and caches application layers. */ class BuildAndCacheApplicationLayerStep implements Callable { - private static final String DESCRIPTION = "Preparing application layer builders"; + private static final String DESCRIPTION = "Building %s layer"; /** * Makes a list of {@link BuildAndCacheApplicationLayerStep} for dependencies, resources, and @@ -49,7 +50,8 @@ static ImmutableList makeList( progressEventDispatcherFactory.create( "preparing application layer builders", layerCount); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), "Preparing application layer builders")) { ImmutableList.Builder buildAndCacheApplicationLayerSteps = ImmutableList.builderWithExpectedSize(layerCount); for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { @@ -65,9 +67,7 @@ static ImmutableList makeList( layerConfiguration.getName(), layerConfiguration)); } - ImmutableList steps = - buildAndCacheApplicationLayerSteps.build(); - return steps; + return buildAndCacheApplicationLayerSteps.build(); } } @@ -90,14 +90,14 @@ private BuildAndCacheApplicationLayerStep( @Override public CachedLayerAndName call() throws IOException, CacheCorruptedException { - String description = "Building " + layerName + " layer"; + String description = String.format(DESCRIPTION, layerName); - buildConfiguration.getEventHandlers().dispatch(LogEvent.progress(description + "...")); + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + eventHandlers.dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("building " + layerName + " layer", 1); - TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), description)) { + TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { Cache cache = buildConfiguration.getApplicationLayersCache(); // Don't build the layer if it exists already. @@ -111,9 +111,7 @@ public CachedLayerAndName call() throws IOException, CacheCorruptedException { CachedLayer cachedLayer = cache.writeUncompressedLayer(layerBlob, layerConfiguration.getLayerEntries()); - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); + eventHandlers.dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); return new CachedLayerAndName(cachedLayer, layerName); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java index 7fbf042b75..0868aef0ac 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java @@ -19,13 +19,18 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; @@ -35,6 +40,32 @@ class PullAndCacheBaseImageLayerStep implements Callable { private static final String DESCRIPTION = "Pulling base image layer %s"; + static ImmutableList makeList( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + ImageAndAuthorization baseImageAndAuth) { + ImmutableList baseImageLayers = baseImageAndAuth.getImage().getLayers(); + + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "preparing base image layer pullers", baseImageLayers.size()); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), "Preparing base image layer pullers")) { + + List layerPullers = new ArrayList<>(); + for (Layer layer : baseImageLayers) { + layerPullers.add( + new PullAndCacheBaseImageLayerStep( + buildConfiguration, + progressEventDispatcher.newChildProducer(), + layer.getBlobDescriptor().getDigest(), + baseImageAndAuth.getAuthorization())); + } + return ImmutableList.copyOf(layerPullers); + } + } + private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java deleted file mode 100644 index 1ade0d839e..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayersStep.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.image.Layer; -import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.List; - -/** Pulls and caches the base image layers. */ -// TODO: following the same pattern as "BuildAndCacheApplicationLayerStep", move the sole "makeList" -// static into "PullAndCacheBaseImageLayerStep" and remove this class. -class PullAndCacheBaseImageLayersStep { - - private static final String DESCRIPTION = "Preparing base image layer pullers"; - - static ImmutableList makeList( - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - ImageAndAuthorization baseImageAndAuth) { - ImmutableList baseImageLayers = baseImageAndAuth.getImage().getLayers(); - - try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - "preparing base image layer pullers", baseImageLayers.size()); - TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { - - List layerPullers = new ArrayList<>(); - for (Layer layer : baseImageLayers) { - layerPullers.add( - new PullAndCacheBaseImageLayerStep( - buildConfiguration, - progressEventDispatcher.newChildProducer(), - layer.getBlobDescriptor().getDigest(), - baseImageAndAuth.getAuthorization())); - } - return ImmutableList.copyOf(layerPullers); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index df29bb8672..cdd0f4da7f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -18,110 +18,112 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.registry.RegistryClient; -import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.collect.ImmutableList; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; -/** Pushes the final image. Outputs the pushed image digest. */ +/** + * Pushes a manifest for a tag. Returns the manifest digest ("image digest") and the container + * configuration digest ("image id") as {#link BuildResult}. + */ class PushImageStep implements Callable { - private static final String DESCRIPTION = "Pushing new image"; + private static final String DESCRIPTION = "Pushing manifest"; + + static ImmutableList makeList( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Authorization pushAuthorization, + BlobDescriptor containerConfigurationDigestAndSize, + Image builtImage) + throws IOException { + Set tags = buildConfiguration.getAllTargetImageTags(); + + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), "Preparing manifest pushers"); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("preparing manifest pushers", tags.size())) { + + // Gets the image manifest to push. + BuildableManifestTemplate manifestTemplate = + new ImageToJsonTranslator(builtImage) + .getManifestTemplate( + buildConfiguration.getTargetFormat(), containerConfigurationDigestAndSize); + + DescriptorDigest manifestDigest = Digests.computeJsonDigest(manifestTemplate); + + return tags.stream() + .map( + tag -> + new PushImageStep( + buildConfiguration, + progressEventDispatcher.newChildProducer(), + pushAuthorization, + manifestTemplate, + tag, + manifestDigest, + containerConfigurationDigestAndSize.getDigest())) + .collect(ImmutableList.toImmutableList()); + } + } private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final BuildableManifestTemplate manifestTemplate; @Nullable private final Authorization pushAuthorization; - private final BlobDescriptor containerConfigurationDigestAndSize; - private final Image builtImage; - - private final ListeningExecutorService listeningExecutorService; + private final String tag; + private final DescriptorDigest imageDigest; + private final DescriptorDigest imageId; - // TODO: remove listeningExecutorService like other siblings PushImageStep( - ListeningExecutorService listeningExecutorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, - BlobDescriptor containerConfigurationDigestAndSize, - Image builtImage) { - this.listeningExecutorService = listeningExecutorService; + BuildableManifestTemplate manifestTemplate, + String tag, + DescriptorDigest imageDigest, + DescriptorDigest imageId) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pushAuthorization = pushAuthorization; - this.containerConfigurationDigestAndSize = containerConfigurationDigestAndSize; - this.builtImage = builtImage; + this.manifestTemplate = manifestTemplate; + this.tag = tag; + this.imageDigest = imageDigest; + this.imageId = imageId; } @Override - public BuildResult call() throws IOException, InterruptedException, ExecutionException { - ImmutableSet targetImageTags = buildConfiguration.getAllTargetImageTags(); - ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("pushing image manifest", targetImageTags.size()); + public BuildResult call() throws IOException, RegistryException { + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION); + ProgressEventDispatcher ignored2 = + progressEventDispatcherFactory.create("pushing manifest for " + tag, 1)) { + eventHandlers.dispatch(LogEvent.info("Pushing manifest for " + tag + "...")); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() .setAuthorization(pushAuthorization) .newRegistryClient(); - // Constructs the image. - ImageToJsonTranslator imageToJsonTranslator = new ImageToJsonTranslator(builtImage); - - // Gets the image manifest to push. - BuildableManifestTemplate manifestTemplate = - imageToJsonTranslator.getManifestTemplate( - buildConfiguration.getTargetFormat(), containerConfigurationDigestAndSize); - - // Pushes to all target image tags. - List> pushAllTagsFutures = new ArrayList<>(); - for (String tag : targetImageTags) { - ProgressEventDispatcher.Factory progressEventDispatcherFactory = - progressEventDispatcher.newChildProducer(); - pushAllTagsFutures.add( - listeningExecutorService.submit( - () -> { - try (ProgressEventDispatcher ignored2 = - progressEventDispatcherFactory.create("tagging with " + tag, 1)) { - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.info("Tagging with " + tag + "...")); - registryClient.pushManifest(manifestTemplate, tag); - } - return null; - })); - } - - DescriptorDigest imageDigest = Digests.computeJsonDigest(manifestTemplate); - DescriptorDigest imageId = containerConfigurationDigestAndSize.getDigest(); - BuildResult result = new BuildResult(imageDigest, imageId); - - return Futures.whenAllSucceed(pushAllTagsFutures) - .call( - () -> { - progressEventDispatcher.close(); - return result; - }, - listeningExecutorService) - .get(); + registryClient.pushManifest(manifestTemplate, tag); + return new BuildResult(imageDigest, imageId); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 56d47a973c..05fb34b8f9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -34,24 +34,17 @@ class PushLayerStep implements Callable { - private static final String DESCRIPTION = "Preparing application layer pushers"; - - private final BuildConfiguration buildConfiguration; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - - @Nullable private final Authorization pushAuthorization; - private final Future cachedLayerAndName; - static ImmutableList makeList( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, List> cachedLayers) { try (TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION); + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), "Preparing application layer pushers"); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "Preparing application layer pushers", cachedLayers.size())) { + "preparing application layer pushers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. List blobPushers = new ArrayList<>(); @@ -66,6 +59,12 @@ static ImmutableList makeList( } } + private final BuildConfiguration buildConfiguration; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + + @Nullable private final Authorization pushAuthorization; + private final Future cachedLayerAndName; + PushLayerStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 3b3ae78c77..ec5ffe2ea6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -86,10 +86,18 @@ public static StepsRunner begin(BuildConfiguration buildConfiguration) { return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildConfiguration); } + private static List realizeFutures(List> futures) + throws InterruptedException, ExecutionException { + List values = new ArrayList<>(); + for (Future future : futures) { + values.add(future.get()); + } + return values; + } + private final StepResults results = new StepResults(); - // TODO: use plain ExecutorService; requires refactoring PushImageStep. - private final ListeningExecutorService executorService; + private final ExecutorService executorService; private final BuildConfiguration buildConfiguration; // We save steps to run by wrapping each step into a Runnable, only because of the unfortunate @@ -110,6 +118,67 @@ private StepsRunner( this.buildConfiguration = buildConfiguration; } + public StepsRunner dockerLoadSteps(DockerClient dockerClient) { + rootProgressDescription = "building image to Docker daemon"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // load to Docker + stepsToRun.add(() -> loadDocker(dockerClient)); + return this; + } + + public StepsRunner tarBuildSteps(Path outputPath) { + rootProgressDescription = "building image to tar file"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // create a tar + stepsToRun.add(() -> writeTarFile(outputPath)); + return this; + } + + public StepsRunner registryPushSteps() { + rootProgressDescription = "building image to registry"; + // build and cache + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(this::buildAndCacheApplicationLayers); + stepsToRun.add(this::buildImage); + // push to registry + stepsToRun.add(this::retrieveTargetRegistryCredentials); + stepsToRun.add(this::authenticatePush); + stepsToRun.add(this::pushBaseImageLayers); + stepsToRun.add(this::pushApplicationLayers); + stepsToRun.add(this::pushContainerConfiguration); + stepsToRun.add(this::pushImages); + return this; + } + + public BuildResult run() throws ExecutionException, InterruptedException { + Preconditions.checkNotNull(rootProgressDescription); + + try (ProgressEventDispatcher progressEventDispatcher = + ProgressEventDispatcher.newRoot( + buildConfiguration.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { + rootProgressDispatcher = progressEventDispatcher; + + stepsToRun.forEach(Runnable::run); + return results.buildResult.get(); + + } catch (ExecutionException ex) { + ExecutionException unrolled = ex; + while (unrolled.getCause() instanceof ExecutionException) { + unrolled = (ExecutionException) unrolled.getCause(); + } + throw unrolled; + } + } + private void retrieveTargetRegistryCredentials() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); @@ -151,7 +220,7 @@ private void pullAndCacheBaseImageLayers() { executorService.submit( () -> scheduleCallables( - PullAndCacheBaseImageLayersStep.makeList( + PullAndCacheBaseImageLayerStep.makeList( buildConfiguration, childProgressDispatcherFactory, results.baseImageAndAuth.get()))); @@ -228,7 +297,7 @@ private void pushApplicationLayers() { Verify.verifyNotNull(results.applicationLayers)))); } - private void pushImage() { + private void pushImages() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); @@ -238,14 +307,17 @@ private void pushImage() { realizeFutures(results.baseImageLayerPushResults.get()); realizeFutures(results.applicationLayerPushResults.get()); - return new PushImageStep( - executorService, - buildConfiguration, - childProgressDispatcherFactory, - results.pushAuthorization.get().orElse(null), - results.containerConfigurationPushResult.get(), - results.builtImage.get()) - .call(); + List> manifestPushResults = + scheduleCallables( + PushImageStep.makeList( + buildConfiguration, + childProgressDispatcherFactory, + results.pushAuthorization.get().orElse(null), + results.containerConfigurationPushResult.get(), + results.builtImage.get())); + realizeFutures(manifestPushResults); + // Manifest pushers return the same BuildResult. + return manifestPushResults.get(0).get(); }); } @@ -279,77 +351,7 @@ private void writeTarFile(Path outputPath) { .call()); } - public BuildResult run() throws ExecutionException, InterruptedException { - Preconditions.checkNotNull(rootProgressDescription); - - try (ProgressEventDispatcher progressEventDispatcher = - ProgressEventDispatcher.newRoot( - buildConfiguration.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { - rootProgressDispatcher = progressEventDispatcher; - - stepsToRun.forEach(Runnable::run); - return results.buildResult.get(); - - } catch (ExecutionException ex) { - ExecutionException unrolled = ex; - while (unrolled.getCause() instanceof ExecutionException) { - unrolled = (ExecutionException) unrolled.getCause(); - } - throw unrolled; - } - } - - private static List realizeFutures(List> futures) - throws InterruptedException, ExecutionException { - List values = new ArrayList<>(); - for (Future future : futures) { - values.add(future.get()); - } - return values; - } - private List> scheduleCallables(ImmutableList> callables) { return callables.stream().map(executorService::submit).collect(Collectors.toList()); } - - public StepsRunner dockerLoadSteps(DockerClient dockerClient) { - rootProgressDescription = "building image to Docker daemon"; - // build and cache - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); - stepsToRun.add(this::buildAndCacheApplicationLayers); - stepsToRun.add(this::buildImage); - // load to Docker - stepsToRun.add(() -> loadDocker(dockerClient)); - return this; - } - - public StepsRunner tarBuildSteps(Path outputPath) { - rootProgressDescription = "building image to tar file"; - // build and cache - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); - stepsToRun.add(this::buildAndCacheApplicationLayers); - stepsToRun.add(this::buildImage); - // create a tar - stepsToRun.add(() -> writeTarFile(outputPath)); - return this; - } - - public StepsRunner registryPushSteps() { - rootProgressDescription = "building image to registry"; - // build and cache - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); - stepsToRun.add(this::buildAndCacheApplicationLayers); - stepsToRun.add(this::buildImage); - // push to registry - stepsToRun.add(this::retrieveTargetRegistryCredentials); - stepsToRun.add(this::authenticatePush); - stepsToRun.add(this::pushBaseImageLayers); - stepsToRun.add(this::pushApplicationLayers); - stepsToRun.add(this::pushContainerConfiguration); - stepsToRun.add(this::pushImage); - return this; - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 96b65640fa..5fe9044450 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -134,7 +134,7 @@ public void setUp() throws DigestException { } @Test - public void test_validateAsyncDependencies() { + public void test_basicCase() { Image image = new BuildImageStep( mockBuildConfiguration, From 7b0406ab48d87ac2edb79e8aa74914b0f4fd6d29 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 28 Jun 2019 14:09:41 -0400 Subject: [PATCH 0643/2020] Support manifest lists when referenced by sha256 (#1811) * Support manifest lists when referenced by sha256 --- .../tools/jib/api/JibIntegrationTest.java | 19 ++++++ .../ManifestPullerIntegrationTest.java | 40 ++++++++++++ .../jib/builder/steps/PullBaseImageStep.java | 34 ++++++++++ .../image/json/V22ManifestListTemplate.java | 6 +- .../tools/jib/registry/ManifestPuller.java | 15 +++-- .../tools/jib/registry/RegistryClient.java | 6 +- .../jib/registry/ManifestPullerTest.java | 62 +++++++++++++++++++ 7 files changed, 172 insertions(+), 10 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 7124df8d56..8ad467771a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.registry.LocalRegistry; +import com.google.cloud.tools.jib.registry.ManifestPullerIntegrationTest; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -188,4 +189,22 @@ public void testProvidedExecutorNotDisposed() executorService.shutdown(); } + + @Test + public void testManifestListReferenceByShaDoesNotFail() + throws InvalidImageReferenceException, IOException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + ImageReference sourceImageReferenceAsManifestList = + ImageReference.of( + "registry-1.docker.io", + "library/openjdk", + ManifestPullerIntegrationTest.KNOWN_MANIFEST_LIST_SHA); + Containerizer containerizer = + Containerizer.to( + TarImage.named("whatever") + .saveTo(cacheFolder.newFolder("goose").toPath().resolve("moose"))); + + Jib.from(sourceImageReferenceAsManifestList).containerize(containerizer); + // pass, no exceptions thrown + } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index e4f82691f5..43b8c80e98 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -18,8 +18,10 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import org.hamcrest.CoreMatchers; @@ -31,6 +33,10 @@ /** Integration tests for {@link ManifestPuller}. */ public class ManifestPullerIntegrationTest { + /** A known manifest list sha for openjdk:11-jre-slim */ + public static final String KNOWN_MANIFEST_LIST_SHA = + "sha256:8ab7b3078b01ba66b937b7fbe0b9eccf60449cc101c42e99aeefaba0e1781155"; + @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); @BeforeClass @@ -62,6 +68,40 @@ public void testPull_v22() throws IOException, RegistryException { Assert.assertTrue(v22ManifestTemplate.getLayers().size() > 0); } + @Test + public void testPull_v22ManifestList() throws IOException, RegistryException { + RegistryClient.Factory factory = + RegistryClient.factory(EventHandlers.NONE, "registry-1.docker.io", "library/openjdk"); + Authorization authorization = + factory.newRegistryClient().getRegistryAuthenticator().authenticatePull(null); + RegistryClient registryClient = factory.setAuthorization(authorization).newRegistryClient(); + + // Ensure 11-jre-slim is a manifest list + V22ManifestListTemplate manifestListTemplate = + registryClient.pullManifest("11-jre-slim", V22ManifestListTemplate.class); + Assert.assertEquals(2, manifestListTemplate.getSchemaVersion()); + Assert.assertTrue(manifestListTemplate.getManifests().size() > 0); + + // Generic call to 11-jre-slim should NOT pull a manifest list (delegate to registry default) + ManifestTemplate manifestTemplate = registryClient.pullManifest("11-jre-slim"); + Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); + Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestTemplate.class)); + + // Make sure we can't cast a v22ManifestTemplate to v22ManifestListTemplate in ManifestPuller + try { + registryClient.pullManifest(KNOWN_MANIFEST_LIST_SHA, V22ManifestTemplate.class); + Assert.fail(); + } catch (ClassCastException ex) { + // pass + } + + // Referencing a manifest list by sha256, should return a manifest list + ManifestTemplate sha256ManifestList = registryClient.pullManifest(KNOWN_MANIFEST_LIST_SHA); + Assert.assertEquals(2, sha256ManifestList.getSchemaVersion()); + Assert.assertThat(sha256ManifestList, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + Assert.assertTrue(((V22ManifestListTemplate) sha256ManifestList).getManifests().size() > 0); + } + @Test public void testPull_unknownManifest() throws RegistryException, IOException { try { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index c6dab7ed2f..bab08c12cf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -44,12 +44,14 @@ import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import javax.annotation.Nullable; @@ -207,6 +209,19 @@ private Image pullBaseImage( ManifestTemplate manifestTemplate = registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); + // special handling if we happen upon a manifest list, redirect to a manifest and continue + // handling it normally + if (manifestTemplate instanceof V22ManifestListTemplate) { + buildConfiguration + .getEventHandlers() + .dispatch( + LogEvent.lifecycle( + "The base image reference is manifest list, searching for linux/amd64")); + manifestTemplate = + obtainPlatformSpecificImageManifest( + registryClient, (V22ManifestListTemplate) manifestTemplate); + } + // TODO: Make schema version be enum. switch (manifestTemplate.getSchemaVersion()) { case 1: @@ -258,6 +273,25 @@ private Image pullBaseImage( throw new IllegalStateException("Unknown manifest schema version"); } + /** + * Looks through a manifest list for any amd64/linux manifest and downloads and returns the first + * manifest it finds. + */ + private ManifestTemplate obtainPlatformSpecificImageManifest( + RegistryClient registryClient, V22ManifestListTemplate manifestListTemplate) + throws RegistryException, IOException { + + List digests = manifestListTemplate.getDigestsForPlatform("amd64", "linux"); + if (digests.size() == 0) { + String errorMessage = + "Unable to find amd64/linux manifest in manifest list at: " + + buildConfiguration.getBaseImageConfiguration().getImage(); + buildConfiguration.getEventHandlers().dispatch(LogEvent.error(errorMessage)); + throw new RegistryException(errorMessage); + } + return registryClient.pullManifest(digests.get(0)); + } + /** * Retrieves the cached base image. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java index f758411e27..de5437aea6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java @@ -76,7 +76,7 @@ public int getSchemaVersion() { @Nullable private List manifests; @VisibleForTesting - List getManifests() { + public List getManifests() { return Preconditions.checkNotNull(manifests); } @@ -93,10 +93,10 @@ public List getDigestsForPlatform(String architecture, String os) { } /** Template for inner JSON object representing a single platform specific manifest. */ - static class ManifestDescriptorTemplate implements JsonTemplate { + public static class ManifestDescriptorTemplate implements JsonTemplate { @JsonIgnoreProperties(ignoreUnknown = true) - static class Platform implements JsonTemplate { + public static class Platform implements JsonTemplate { @Nullable private String architecture; @Nullable private String os; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index 615a4a9500..84918eb0ed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.CharStreams; @@ -71,7 +72,13 @@ public List getAccept() { if (manifestTemplateClass.equals(OCIManifestTemplate.class)) { return Collections.singletonList(OCIManifestTemplate.MANIFEST_MEDIA_TYPE); } + if (manifestTemplateClass.equals(V22ManifestListTemplate.class)) { + return Collections.singletonList(V22ManifestListTemplate.MANIFEST_MEDIA_TYPE); + } + // V22ManifestListTemplate is not included by default, we don't explicitly accept + // it, we only handle it if referenced by sha256 (see getManifestTemplateFromJson) in which + // case registries ignore the "accept" directive and just return a manifest list anyway. return Arrays.asList( OCIManifestTemplate.MANIFEST_MEDIA_TYPE, V22ManifestTemplate.MANIFEST_MEDIA_TYPE, @@ -118,10 +125,6 @@ private T getManifestTemplateFromJson(String jsonString) throw new UnknownManifestFormatException("Cannot find field 'schemaVersion' in manifest"); } - if (!manifestTemplateClass.equals(ManifestTemplate.class)) { - return JsonTemplateMapper.readJson(jsonString, manifestTemplateClass); - } - int schemaVersion = node.get("schemaVersion").asInt(-1); if (schemaVersion == -1) { throw new UnknownManifestFormatException("`schemaVersion` field is not an integer"); @@ -142,6 +145,10 @@ private T getManifestTemplateFromJson(String jsonString) return manifestTemplateClass.cast( JsonTemplateMapper.readJson(jsonString, OCIManifestTemplate.class)); } + if (V22ManifestListTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + return manifestTemplateClass.cast( + JsonTemplateMapper.readJson(jsonString, V22ManifestListTemplate.class)); + } throw new UnknownManifestFormatException("Unknown mediaType: " + mediaType); } throw new UnknownManifestFormatException( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 4dfe8dd52b..9e8cf51bd9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -28,10 +28,9 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; @@ -282,7 +281,8 @@ public RegistryAuthenticator getRegistryAuthenticator() throws IOException, Regi * @param child type of ManifestTemplate * @param imageTag the tag to pull on * @param manifestTemplateClass the specific version of manifest template to pull, or {@link - * ManifestTemplate} to pull either {@link V22ManifestTemplate} or {@link V21ManifestTemplate} + * ManifestTemplate} to pull predefined subclasses; see: {@link + * ManifestPuller#handleResponse(Response)} * @return the manifest template * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index 0c5a37fdd4..cd583c5381 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; +import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; @@ -89,6 +90,60 @@ public void testHandleResponse_v22() Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestTemplate.class)); } + @Test + public void testHandleResponse_v22ManifestListFailsWhenParsedAsV22Manifest() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + try { + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) + .handleResponse(mockResponse); + Assert.fail(); + } catch (ClassCastException ex) { + // pass + } + } + + @Test + public void testHandleResponse_v22ManifestListFromParentType() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + ManifestTemplate manifestTemplate = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) + .handleResponse(mockResponse); + + Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + Assert.assertTrue(((V22ManifestListTemplate) manifestTemplate).getManifests().size() > 0); + } + + @Test + public void testHandleResponse_v22ManifestList() + throws URISyntaxException, IOException, UnknownManifestFormatException { + Path v22ManifestListFile = + Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); + InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); + V22ManifestListTemplate manifestTemplate = + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, + "test-image-tag", + V22ManifestListTemplate.class) + .handleResponse(mockResponse); + + Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); + Assert.assertTrue(manifestTemplate.getManifests().size() > 0); + } + @Test public void testHandleResponse_noSchemaVersion() throws IOException { Mockito.when(mockResponse.getBody()).thenReturn(stringToInputStreamUtf8("{}")); @@ -175,5 +230,12 @@ public void testGetAccept() { new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .getAccept()); + Assert.assertEquals( + Collections.singletonList(V22ManifestListTemplate.MANIFEST_MEDIA_TYPE), + new ManifestPuller<>( + fakeRegistryEndpointRequestProperties, + "test-image-tag", + V22ManifestListTemplate.class) + .getAccept()); } } From 6e5d8ec087a62848b01ceacaf3ed78fc2c637bb8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 28 Jun 2019 14:27:07 -0400 Subject: [PATCH 0644/2020] Remove ImageLayers in Image (#1814) --- .../BuildAndCacheApplicationLayerStep.java | 33 ++-- .../google/cloud/tools/jib/image/Image.java | 8 +- .../cloud/tools/jib/image/ImageLayers.java | 160 ------------------ ...BuildAndCacheApplicationLayerStepTest.java | 23 +-- .../tools/jib/image/ImageLayersTest.java | 111 ------------ 5 files changed, 31 insertions(+), 304 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 632720f85a..96a3d3cafd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -29,6 +29,7 @@ import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; @@ -44,30 +45,26 @@ class BuildAndCacheApplicationLayerStep implements Callable static ImmutableList makeList( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { - int layerCount = buildConfiguration.getLayerConfigurations().size(); + List layerConfigurations = buildConfiguration.getLayerConfigurations(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "preparing application layer builders", layerCount); + "preparing application layer builders", layerConfigurations.size()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing application layer builders")) { - ImmutableList.Builder buildAndCacheApplicationLayerSteps = - ImmutableList.builderWithExpectedSize(layerCount); - for (LayerConfiguration layerConfiguration : buildConfiguration.getLayerConfigurations()) { - // Skips the layer if empty. - if (layerConfiguration.getLayerEntries().isEmpty()) { - continue; - } - - buildAndCacheApplicationLayerSteps.add( - new BuildAndCacheApplicationLayerStep( - buildConfiguration, - progressEventDispatcher.newChildProducer(), - layerConfiguration.getName(), - layerConfiguration)); - } - return buildAndCacheApplicationLayerSteps.build(); + return layerConfigurations + .stream() + // Skips the layer if empty. + .filter(layerConfiguration -> !layerConfiguration.getLayerEntries().isEmpty()) + .map( + layerConfiguration -> + new BuildAndCacheApplicationLayerStep( + buildConfiguration, + progressEventDispatcher.newChildProducer(), + layerConfiguration.getName(), + layerConfiguration)) + .collect(ImmutableList.toImmutableList()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index 8e3dac8b6d..bbadd46223 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -39,7 +39,7 @@ public class Image { public static class Builder { private final Class imageFormat; - private final ImageLayers.Builder imageLayersBuilder = ImageLayers.builder(); + private final ImmutableList.Builder imageLayersBuilder = ImmutableList.builder(); private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); // Don't use ImmutableMap.Builder because it does not allow for replacing existing keys with new @@ -287,7 +287,7 @@ public static Builder builder(Class imageFormat) { private final String os; /** The layers of the image, in the order in which they are applied. */ - private final ImageLayers layers; + private final ImmutableList layers; /** The commands used to build each layer of the image */ private final ImmutableList history; @@ -324,7 +324,7 @@ private Image( @Nullable Instant created, String architecture, String os, - ImageLayers layers, + ImmutableList layers, ImmutableList history, @Nullable ImmutableMap environment, @Nullable ImmutableList entrypoint, @@ -415,7 +415,7 @@ public String getUser() { } public ImmutableList getLayers() { - return layers.getLayers(); + return layers; } public ImmutableList getHistory() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java deleted file mode 100644 index fd37b6b29e..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageLayers.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.annotation.Nullable; - -/** Holds the layers for an image. */ -public class ImageLayers implements Iterable { - - public static class Builder { - - private final List layers = new ArrayList<>(); - private final ImmutableSet.Builder layerDigestsBuilder = - ImmutableSet.builder(); - private boolean removeDuplicates = false; - - /** - * Adds a layer. Removes any prior occurrences of the same layer. - * - *

Note that only subclasses of {@link Layer} that implement {@code equals/hashCode} will be - * guaranteed to not be duplicated. - * - * @param layer the layer to add - * @return this - * @throws LayerPropertyNotFoundException if adding the layer fails - */ - public Builder add(Layer layer) throws LayerPropertyNotFoundException { - layerDigestsBuilder.add(layer.getBlobDescriptor().getDigest()); - layers.add(layer); - return this; - } - - /** - * Adds all layers in {@code layers}. - * - * @param layers the layers to add - * @return this - * @throws LayerPropertyNotFoundException if adding a layer fails - */ - public Builder addAll(ImageLayers layers) throws LayerPropertyNotFoundException { - for (Layer layer : layers) { - add(layer); - } - return this; - } - - /** - * Remove any duplicate layers, keeping the last occurrence of the layer. - * - * @return this - */ - public Builder removeDuplicates() { - removeDuplicates = true; - return this; - } - - public ImageLayers build() { - if (!removeDuplicates) { - return new ImageLayers(ImmutableList.copyOf(layers), layerDigestsBuilder.build()); - } - - // LinkedHashSet maintains the order but keeps the first occurrence. Keep last occurrence by - // adding elements in reverse, and then reversing the result - Set dedupedButReversed = new LinkedHashSet<>(Lists.reverse(this.layers)); - ImmutableList deduped = ImmutableList.copyOf(dedupedButReversed).reverse(); - return new ImageLayers(deduped, layerDigestsBuilder.build()); - } - } - - public static Builder builder() { - return new Builder(); - } - - /** The layers of the image, in the order in which they are applied. */ - private final ImmutableList layers; - - /** Keeps track of the layers already added. */ - private final ImmutableSet layerDigests; - - private ImageLayers(ImmutableList layers, ImmutableSet layerDigests) { - this.layers = layers; - this.layerDigests = layerDigests; - } - - /** @return a read-only view of the image layers. */ - public ImmutableList getLayers() { - return layers; - } - - /** @return the layer count */ - public int size() { - return layers.size(); - } - - public boolean isEmpty() { - return layers.isEmpty(); - } - - /** - * @param index the index of the layer to get - * @return the layer at the specified index - */ - public Layer get(int index) { - return layers.get(index); - } - - /** - * @param digest the digest used to retrieve the layer - * @return the layer found, or {@code null} if not found - * @throws LayerPropertyNotFoundException if getting the layer's blob descriptor fails - */ - @Nullable - public Layer get(DescriptorDigest digest) throws LayerPropertyNotFoundException { - if (!has(digest)) { - return null; - } - for (Layer layer : layers) { - if (layer.getBlobDescriptor().getDigest().equals(digest)) { - return layer; - } - } - throw new IllegalStateException("Layer digest exists but layer not found"); - } - - /** - * @param digest the digest to check for - * @return true if the layer with the specified digest exists; false otherwise - */ - public boolean has(DescriptorDigest digest) { - return layerDigests.contains(digest); - } - - @Override - public Iterator iterator() { - return getLayers().iterator(); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index a8c7a76068..38a45bbf07 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -27,7 +27,7 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.image.ImageLayers; +import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; @@ -36,6 +36,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; @@ -79,12 +81,11 @@ private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOExce Assert.assertArrayEquals(Blobs.writeToByteArray(expectedBlob), Blobs.writeToByteArray(blob)); } - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private BuildConfiguration mockBuildConfiguration; private Cache cache; - @Mock private EventHandlers mockEventHandlers; private LayerConfiguration fakeDependenciesLayerConfiguration; private LayerConfiguration fakeSnapshotDependenciesLayerConfiguration; @@ -119,25 +120,25 @@ public void setUp() throws IOException, URISyntaxException { cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(mockEventHandlers); + Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); } - private ImageLayers buildFakeLayersToCache() + private List buildFakeLayersToCache() throws LayerPropertyNotFoundException, IOException, CacheCorruptedException { - ImageLayers.Builder applicationLayersBuilder = ImageLayers.builder(); + List applicationLayers = new ArrayList<>(); ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( mockBuildConfiguration, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()); + ProgressEventDispatcher.newRoot(EventHandlers.NONE, "ignored", 1).newChildProducer()); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - applicationLayersBuilder.add(buildAndCacheApplicationLayerStep.call().getCachedLayer()); + applicationLayers.add(buildAndCacheApplicationLayerStep.call().getCachedLayer()); } - return applicationLayersBuilder.build(); + return applicationLayers; } @Test @@ -154,7 +155,7 @@ public void testRun() .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + List applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(5, applicationLayers.size()); ImmutableList dependenciesLayerEntries = @@ -215,7 +216,7 @@ public void testRun_emptyLayersIgnored() throws IOException, CacheCorruptedExcep .thenReturn(fakeLayerConfigurations); // Populates the cache. - ImageLayers applicationLayers = buildFakeLayersToCache(); + List applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(3, applicationLayers.size()); ImmutableList dependenciesLayerEntries = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java deleted file mode 100644 index 828150e22f..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageLayersTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.image; - -import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import java.util.Arrays; -import java.util.List; -import org.hamcrest.CoreMatchers; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -/** Tests for {@link ImageLayers}. */ -@RunWith(MockitoJUnitRunner.class) -public class ImageLayersTest { - - @Mock private Layer mockLayer; - @Mock private ReferenceLayer mockReferenceLayer; - @Mock private DigestOnlyLayer mockDigestOnlyLayer; - @Mock private Layer mockLayer2; - - @Before - public void setUpFakes() throws LayerPropertyNotFoundException { - DescriptorDigest mockDescriptorDigest1 = Mockito.mock(DescriptorDigest.class); - DescriptorDigest mockDescriptorDigest2 = Mockito.mock(DescriptorDigest.class); - DescriptorDigest mockDescriptorDigest3 = Mockito.mock(DescriptorDigest.class); - - BlobDescriptor layerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); - BlobDescriptor referenceLayerBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest2); - BlobDescriptor referenceNoDiffIdLayerBlobDescriptor = - new BlobDescriptor(0, mockDescriptorDigest3); - // Intentionally the same digest as the mockLayer. - BlobDescriptor anotherBlobDescriptor = new BlobDescriptor(0, mockDescriptorDigest1); - - Mockito.when(mockLayer.getBlobDescriptor()).thenReturn(layerBlobDescriptor); - Mockito.when(mockReferenceLayer.getBlobDescriptor()).thenReturn(referenceLayerBlobDescriptor); - Mockito.when(mockDigestOnlyLayer.getBlobDescriptor()) - .thenReturn(referenceNoDiffIdLayerBlobDescriptor); - Mockito.when(mockLayer2.getBlobDescriptor()).thenReturn(anotherBlobDescriptor); - } - - @Test - public void testAddLayer_success() throws LayerPropertyNotFoundException { - List expectedLayers = Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer); - - ImageLayers imageLayers = - ImageLayers.builder() - .add(mockLayer) - .add(mockReferenceLayer) - .add(mockDigestOnlyLayer) - .build(); - - Assert.assertThat(imageLayers.getLayers(), CoreMatchers.is(expectedLayers)); - } - - @Test - public void testAddLayer_maintainDuplicates() throws LayerPropertyNotFoundException { - // must maintain duplicate - List expectedLayers = - Arrays.asList(mockLayer, mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); - - ImageLayers imageLayers = - ImageLayers.builder() - .add(mockLayer) - .add(mockReferenceLayer) - .add(mockDigestOnlyLayer) - .add(mockLayer2) - .add(mockLayer) - .build(); - - Assert.assertEquals(expectedLayers, imageLayers.getLayers()); - } - - @Test - public void testAddLayer_removeDuplicates() throws LayerPropertyNotFoundException { - // remove duplicates: last layer should be kept - List expectedLayers = - Arrays.asList(mockReferenceLayer, mockDigestOnlyLayer, mockLayer2, mockLayer); - - ImageLayers imageLayers = - ImageLayers.builder() - .removeDuplicates() - .add(mockLayer) - .add(mockReferenceLayer) - .add(mockDigestOnlyLayer) - .add(mockLayer2) - .add(mockLayer) - .build(); - - Assert.assertEquals(expectedLayers, imageLayers.getLayers()); - } -} From c49dafee0177c117cfddcb036e4759fae86fd818 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 28 Jun 2019 17:08:12 -0400 Subject: [PATCH 0645/2020] Update changelog for manifest lists (#1817) * Update changelog for manifest lists --- jib-core/CHANGELOG.md | 2 ++ jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 9e336804b2..0c3c01159e 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + ## 0.10.0 ### Added diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3e786f2334..7c648500ed 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -16,6 +16,8 @@ All notable changes to this project will be documented in this file. ### Fixed - Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + ## 1.3.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index c9a260a56b..915f35178c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. ### Fixed - Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) + ## 1.3.0 From 61b2e000567c6a5a3bb0e63b5481982385ee3b1d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 3 Jul 2019 11:53:14 -0400 Subject: [PATCH 0646/2020] Introduce support for enforcing minimum Jib plugin versions (#1813) - adds new property `jib.requiredVersion` which can be either a minimum version number or a Maven-like version-range - incorporate checks into various goals and tasks --- .../tools/jib/gradle/CheckJibVersionTask.java | 46 +++++ .../cloud/tools/jib/gradle/JibPlugin.java | 32 +++ .../cloud/tools/jib/gradle/JibPluginTest.java | 30 +++ .../tools/jib/maven/BuildDockerMojo.java | 1 + .../cloud/tools/jib/maven/BuildImageMojo.java | 1 + .../cloud/tools/jib/maven/BuildTarMojo.java | 1 + .../jib/maven/JibPluginConfiguration.java | 9 + .../cloud/tools/jib/maven/MojoCommon.java | 37 +++- .../maven/skaffold/CheckJibVersionMojo.java | 54 +++++ .../tools/jib/maven/skaffold/FilesMojo.java | 4 +- .../tools/jib/maven/skaffold/FilesMojoV2.java | 4 +- .../jib/maven/skaffold/PackageGoalsMojo.java | 4 +- .../maven/skaffold/SkaffoldBindingMojo.java | 34 ++++ .../jib/maven/skaffold/SkaffoldInitMojo.java | 1 + .../maven/BuildDockerMojoIntegrationTest.java | 26 +++ .../maven/BuildImageMojoIntegrationTest.java | 26 +++ .../maven/BuildTarMojoIntegrationTest.java | 27 +++ .../skaffold/CheckJibVersionMojoTest.java | 73 +++++++ .../jib/plugins/common/VersionChecker.java | 146 +++++++++++++ .../plugins/common/VersionCheckerTest.java | 191 ++++++++++++++++++ 20 files changed, 740 insertions(+), 7 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java new file mode 100644 index 0000000000..a62bee3320 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.common.base.Strings; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; + +/** + * This internal Skaffold-related goal checks that the Jib plugin version is within some specified + * range. It is only required so that older versions of Jib (prior to the introduction of the {@code + * jib.requiredVersion} property) will error in such a way that it indicates the jib version is out + * of date. This goal can be removed once there are no users of Jib prior to 1.4.0. + * + *

Expected use: {@code ./gradlew _skaffoldFailIfJibOutOfDate -Djib.requiredVersion='[1.4,2)' + * jibDockerBuild --image=xxx} + */ +public class CheckJibVersionTask extends DefaultTask { + + @TaskAction + public void checkVersion() { + if (Strings.isNullOrEmpty(System.getProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME))) { + throw new GradleException( + JibPlugin.CHECK_REQUIRED_VERSION_TASK_NAME + + " requires " + + JibPlugin.REQUIRED_VERSION_PROPERTY_NAME + + " to be set"); + } + // no-op as Jib version compatibility is actually checked in JibPlugin + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 25de43cce8..74ce5ed5b8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.stream.Collectors; @@ -42,8 +44,13 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; @VisibleForTesting static final String FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; @VisibleForTesting static final String INIT_TASK_NAME = "_jibSkaffoldInit"; + @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; + static final String CHECK_REQUIRED_VERSION_TASK_NAME = "_skaffoldFailIfJibOutOfDate"; + + static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; + /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of * configuration [compile, runtime, etc]. It potentially will collect common test libraries in @@ -79,9 +86,30 @@ private static void checkGradleVersion() { } } + /** Check the Jib version matches the required version (if specified). */ + private static void checkJibVersion(Project project) { + // todo: should retrieve from project properties? + String requiredVersion = System.getProperty(REQUIRED_VERSION_PROPERTY_NAME); + if (requiredVersion == null) { + return; + } + String actualVersion = ProjectInfo.VERSION; + if (actualVersion == null) { + throw new GradleException("Could not determine Jib plugin version"); + } + VersionChecker checker = new VersionChecker<>(GradleVersion::version); + if (!checker.compatibleVersion(requiredVersion, actualVersion)) { + String failure = + String.format( + "Jib plugin version is %s but is required to be %s", actualVersion, requiredVersion); + throw new GradleException(failure); + } + } + @Override public void apply(Project project) { checkGradleVersion(); + checkJibVersion(project); JibExtension jibExtension = project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); @@ -123,6 +151,10 @@ public void apply(Project project) { project.getTasks().create(FILES_TASK_V2_NAME, FilesTaskV2.class).setJibExtension(jibExtension); project.getTasks().create(INIT_TASK_NAME, SkaffoldInitTask.class).setJibExtension(jibExtension); + // A check to catch older versions of Jib. This can be removed once we are certain people + // are using Jib 1.3.1 or later. + project.getTasks().create(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); + project.afterEvaluate( projectAfterEvaluation -> { try { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index f69d283153..e7f5e1609b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -26,6 +26,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.stream.Collectors; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; @@ -34,6 +35,7 @@ import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.After; import org.junit.Assert; import org.junit.Assume; import org.junit.Rule; @@ -55,6 +57,11 @@ private static boolean isJava8Runtime() { @Rule public final TemporaryFolder testProjectRoot = new TemporaryFolder(); + @After + public void tearDown() { + System.clearProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME); + } + @Test public void testCheckGradleVersion_pass() throws IOException { Assume.assumeTrue(isJava8Runtime()); @@ -103,6 +110,29 @@ public void testCheckGradleVersion_fail() throws IOException { } } + @Test + public void testCheckJibVersionNames() { + // These identifiers will be baked into Skaffold and should not be changed + Assert.assertEquals(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME, "jib.requiredVersion"); + Assert.assertEquals(JibPlugin.CHECK_REQUIRED_VERSION_TASK_NAME, "_skaffoldFailIfJibOutOfDate"); + } + + @Test + public void testCheckJibVersionInvoked() { + Project rootProject = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + System.setProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME, "10000.0"); // not here yet + try { + rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + Assert.fail("should have failed"); + } catch (GradleException ex) { + // Gradle tests aren't run from a jar and so don't have an identifiable plugin version + Assert.assertEquals( + "Failed to apply plugin [id 'com.google.cloud.tools.jib']", ex.getMessage()); + Assert.assertEquals("Could not determine Jib plugin version", ex.getCause().getMessage()); + } + } + @Test public void testProjectDependencyAssembleTasksAreRun() { // root project is our jib packaged service diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 69dcc1be1f..fbe015dfc9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -69,6 +69,7 @@ public static class DockerClientConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { + checkJibVersion(); if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index e2b19894b1..9db4d08608 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -55,6 +55,7 @@ public class BuildImageMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { + checkJibVersion(); if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index cf08df5f43..7502f820e1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -53,6 +53,7 @@ public class BuildTarMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { + checkJibVersion(); if (isSkipped()) { getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); return; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 4572eb7c5a..bd4c7bcc9d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -36,6 +36,8 @@ import javax.annotation.Nullable; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -229,6 +231,8 @@ public List getPaths() { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; + @Component protected PluginDescriptor descriptor; + @Parameter private FromConfiguration from = new FromConfiguration(); @Parameter private ToConfiguration to = new ToConfiguration(); @@ -261,6 +265,11 @@ protected MavenProject getProject() { return Preconditions.checkNotNull(project); } + protected void checkJibVersion() throws MojoExecutionException { + Preconditions.checkNotNull(descriptor); + MojoCommon.checkJibVersion(descriptor); + } + /** * Gets the base image reference. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 36cce87772..20364e55ee 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; +import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.nio.file.Path; @@ -26,10 +27,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; /** Collection of common methods to share between Maven goals. */ -class MojoCommon { +public class MojoCommon { + /** Describes a minimum required version or version range for Jib. */ + @VisibleForTesting + public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; /** * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code @@ -72,5 +79,33 @@ static Map convertPermissionsList( return permissionsMap; } + /** + * Check that the actual version satisfies required Jib version range when specified. No check is + * performed if the provided Jib version is {@code null}, which should only occur during debug. + * + * @param acceptableVersionSpec a version range or minimum version; see {@link VersionChecker} + * @param descriptor the plugin version + * @throws MojoExecutionException if the version is not acceptable + */ + public static void checkJibVersion(PluginDescriptor descriptor) throws MojoExecutionException { + String acceptableVersionSpec = System.getProperty(MojoCommon.REQUIRED_VERSION_PROPERTY_NAME); + if (acceptableVersionSpec == null) { + return; + } + String actualVersion = descriptor.getVersion(); + if (actualVersion == null) { + throw new MojoExecutionException("Could not determine Jib plugin version"); + } + VersionChecker checker = + new VersionChecker<>(DefaultArtifactVersion::new); + if (!checker.compatibleVersion(acceptableVersionSpec, actualVersion)) { + String failure = + String.format( + "Jib plugin version is %s but is required to be %s", + actualVersion, acceptableVersionSpec); + throw new MojoExecutionException(failure); + } + } + private MojoCommon() {} } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java new file mode 100644 index 0000000000..ef1306eeea --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MojoCommon; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * This internal Skaffold-related goal checks that the Jib plugin version is within some specified + * range. It is only required so that older versions of Jib (prior to the introduction of the {@code + * jib.requiredVersion} property) will error in such a way that it indicates the jib version is out + * of date. This goal can be removed once there are no users of Jib prior to 1.4.0. + * + *

Expected use: {@code mvn jib:_skaffold-fail-if-jib-out-of-date -Djib.requiredVersion='[1.4,2)' + * jib:build -Dimage=xxx} + */ +@Mojo( + name = CheckJibVersionMojo.GOAL_NAME, + requiresProject = false, + requiresDependencyCollection = ResolutionScope.NONE, + defaultPhase = LifecyclePhase.INITIALIZE) +public class CheckJibVersionMojo extends SkaffoldBindingMojo { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-fail-if-jib-out-of-date"; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (Strings.isNullOrEmpty(System.getProperty(MojoCommon.REQUIRED_VERSION_PROPERTY_NAME))) { + throw new MojoExecutionException( + GOAL_NAME + " requires " + MojoCommon.REQUIRED_VERSION_PROPERTY_NAME + " to be set"); + } + checkJibVersion(); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java index 4c77e6698e..c7d2ce685c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -35,7 +35,6 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.FileSet; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; @@ -62,7 +61,7 @@ @Mojo( name = FilesMojo.GOAL_NAME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) -public class FilesMojo extends AbstractMojo { +public class FilesMojo extends SkaffoldBindingMojo { @VisibleForTesting static final String GOAL_NAME = "_skaffold-files"; @@ -94,6 +93,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Preconditions.checkNotNull(projects); Preconditions.checkNotNull(session); Preconditions.checkNotNull(projectDependenciesResolver); + checkJibVersion(); // print out pom configuration files System.out.println(project.getFile()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index e098846c7a..6985dfa739 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -36,7 +36,6 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.FileSet; import org.apache.maven.model.Plugin; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; @@ -64,7 +63,7 @@ name = FilesMojoV2.GOAL_NAME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, aggregator = true) -public class FilesMojoV2 extends AbstractMojo { +public class FilesMojoV2 extends SkaffoldBindingMojo { @VisibleForTesting static final String GOAL_NAME = "_skaffold-files-v2"; @@ -86,6 +85,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { Preconditions.checkNotNull(projects); Preconditions.checkNotNull(session); Preconditions.checkNotNull(projectDependenciesResolver); + checkJibVersion(); for (MavenProject project : projects) { // Add pom configuration files diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java index d50daa37ba..d460ea374c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojo.java @@ -23,7 +23,6 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.lifecycle.LifecycleExecutor; import org.apache.maven.lifecycle.MavenExecutionPlan; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -44,7 +43,7 @@ name = PackageGoalsMojo.GOAL_NAME, requiresDirectInvocation = true, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) -public class PackageGoalsMojo extends AbstractMojo { +public class PackageGoalsMojo extends SkaffoldBindingMojo { @VisibleForTesting static final String GOAL_NAME = "_skaffold-package-goals"; @@ -58,6 +57,7 @@ public class PackageGoalsMojo extends AbstractMojo { public void execute() throws MojoExecutionException, MojoFailureException { Preconditions.checkNotNull(lifecycleExecutor); Preconditions.checkNotNull(session); + checkJibVersion(); MavenExecutionPlan mavenExecutionPlan; try { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java new file mode 100644 index 0000000000..59281df829 --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MojoCommon; +import com.google.common.base.Preconditions; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.plugins.annotations.Component; + +/** Base class for Skaffold-related goals. */ +abstract class SkaffoldBindingMojo extends AbstractMojo { + @Component protected PluginDescriptor descriptor; + + protected void checkJibVersion() throws MojoExecutionException { + Preconditions.checkNotNull(descriptor); + MojoCommon.checkJibVersion(descriptor); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java index 510ca46d20..fdd5784847 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java @@ -36,6 +36,7 @@ public class SkaffoldInitMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { + checkJibVersion(); SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); skaffoldInitOutput.setImage(getTargetImage()); if (getProject().getParent() != null) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 418f209480..5d440069c1 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -224,4 +224,30 @@ public void testExecute_jarContainerizationOnMissingJar() throws IOException { + "clean jib:build\" instead of \"mvn clean package jib:build\"?)")); } } + + @Test + public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + // this plugin should match 1.0 + verifier.setSystemProperty("jib.requiredVersion", "1.0"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); + verifier.verifyErrorFreeLog(); + } + + @Test + public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); + verifier.verifyTextInLog("BUILD FAILURE"); + verifier.verifyTextInLog("but is required to be"); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 8f28be4fc9..77bcdb9dff 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -544,6 +544,32 @@ public void testExecute_jibContainerizeSkips() throws VerificationException, IOE SkippedGoalVerifier.verifyJibContainerizeSkips(simpleTestProject, BuildDockerMojo.GOAL_NAME); } + @Test + public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + // this plugin should match 1.0 + verifier.setSystemProperty("jib.requiredVersion", "1.0"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:build")); + verifier.verifyErrorFreeLog(); + } + + @Test + public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:build")); + verifier.verifyTextInLog("BUILD FAILURE"); + verifier.verifyTextInLog("but is required to be"); + } + @Test public void testExecute_jettyServlet25() throws VerificationException, IOException, InterruptedException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 615ea32fc9..b43b8df461 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.security.DigestException; import java.time.Instant; +import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; import org.junit.Assert; @@ -88,4 +89,30 @@ public void testExecute_jibSkip() throws VerificationException, IOException { public void testExecute_jibContainerizeSkips() throws VerificationException, IOException { SkippedGoalVerifier.verifyJibContainerizeSkips(simpleTestProject, BuildDockerMojo.GOAL_NAME); } + + @Test + public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + // this plugin should match 1.0 + verifier.setSystemProperty("jib.requiredVersion", "1.0"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:buildTar")); + verifier.verifyErrorFreeLog(); + } + + @Test + public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + String targetImage = "simpleimage:maven" + System.nanoTime(); + + Instant before = Instant.now(); + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:buildTar")); + verifier.verifyTextInLog("BUILD FAILURE"); + verifier.verifyTextInLog("but is required to be"); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java new file mode 100644 index 0000000000..2fff0678b7 --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.maven.MojoCommon; +import com.google.cloud.tools.jib.maven.TestPlugin; +import com.google.cloud.tools.jib.maven.TestProject; +import java.io.IOException; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link CheckJibVersionMojo}. */ +public class CheckJibVersionMojoTest { + @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + + @ClassRule + public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + + @Test + public void testIdentifiers() { + // These identifiers will be baked into Skaffold and should not be changed + Assert.assertEquals("_skaffold-fail-if-jib-out-of-date", CheckJibVersionMojo.GOAL_NAME); + Assert.assertEquals("jib.requiredVersion", MojoCommon.REQUIRED_VERSION_PROPERTY_NAME); + } + + @Test + public void testFailOnMissingProperty() throws VerificationException, IOException { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + try { + verifier.executeGoal("jib:" + CheckJibVersionMojo.GOAL_NAME); + Assert.fail("build should have failed"); + } catch (VerificationException ex) { + verifier.verifyTextInLog("requires jib.requiredVersion to be set"); + } + } + + @Test + public void testFailOnOutOfDate() throws VerificationException, IOException { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty(MojoCommon.REQUIRED_VERSION_PROPERTY_NAME, "[,1.0)"); + try { + verifier.executeGoal("jib:" + CheckJibVersionMojo.GOAL_NAME); + Assert.fail("build should have failed"); + } catch (VerificationException ex) { + verifier.verifyTextInLog("but is required to be [,1.0)"); + } + } + + @Test + public void testSuccess() throws VerificationException, IOException { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty(MojoCommon.REQUIRED_VERSION_PROPERTY_NAME, "[1.0,)"); + verifier.executeGoal("jib:" + CheckJibVersionMojo.GOAL_NAME); + verifier.verifyErrorFreeLog(); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java new file mode 100644 index 0000000000..c7ec80f4f4 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple version-range checker, intended to check whether a Jib plugin version falls in some + * range. A version range can be in one of two forms: + * + *

    + *
  1. An interval such as {@code [1.0,3.5)}, where square brackets indicate an closed + * boundary (includes the value) and parentheses indicates an open boundary (excludes the + * actual value). Either of the left or right bounds can be dropped (but not both!) to to + * indicate an half-bound. For example, {@code [,4)} will include any version up to but not + * including version 4.0. + *
  2. a single version such a {@code 1.0} which acts as an interval with an open left bound and + * akin to {@code [1.0,)} + *
+ * + * To support custom version representations, the actual version object type ({@code }) is + * pluggable. It must implement {@link Comparable}. The versions in the range must have at most 3 + * components (e.g., {@code major.minor.micro}). + * + *

This class exists as Gradle has no version-range-type class and Maven's {@code + * org.apache.maven.artifact.versioning.VersionRange} treats a single version as an exact bound. + * Note that Gradle's {@code org.gradle.util.GradleVersion} class does not support + * major-version-only versions. + */ +public class VersionChecker> { + /** Regular expression to match a single version. */ + private static final String VERSION_REGEX = "\\d+(\\.\\d+(\\.\\d+)?)?"; + + /** Regular expression to match an interval version range. */ + private static final String INTERVAL_REGEX = + "[\\[(](?" + VERSION_REGEX + ")?,(?" + VERSION_REGEX + ")?[])]"; + + private static final Pattern INTERVAL_PATTERN = Pattern.compile(INTERVAL_REGEX); + + // Helper functions to avoid the cognitive burden of {@link Comparable#compareTo()} + + /** Return {@code true} if {@code a} is less than {@code b}. */ + @VisibleForTesting + static > boolean LT(T a, T b) { + return a.compareTo(b) < 0; + } + + /** Return {@code true} if {@code a} is less than or equal to {@code b}. */ + @VisibleForTesting + static > boolean LE(T a, T b) { + return a.compareTo(b) <= 0; + } + + /** Return {@code true} if {@code a} is greater than {@code b}. */ + @VisibleForTesting + static > boolean GT(T a, T b) { + return a.compareTo(b) > 0; + } + + /** Return {@code true} if {@code a} is greater than or equal to {@code b}. */ + @VisibleForTesting + static > boolean GE(T a, T b) { + return a.compareTo(b) >= 0; + } + + /** Responsible for converting a string representation to a comparable version representation. */ + private Function converter; + + public VersionChecker(Function converter) { + this.converter = converter; + } + + /** + * Return {@code true} if {@code actualVersion} is contained within the version range represented + * by {@code acceptableVersionRange}. + * + * @param acceptableVersionRange the encoded version range + * @param actualVersion the version to be compared + * @return true if the version is acceptable + * @throws IllegalArgumentException if the version could not be parsed + */ + @VisibleForTesting + public boolean compatibleVersion(String acceptableVersionRange, String actualVersion) { + V pluginVersion = parseVersion(actualVersion); + + // Treat a single version "1.4" as a left bound, equivalent to "[1.4,)" + if (acceptableVersionRange.matches(VERSION_REGEX)) { + return GE(pluginVersion, parseVersion(acceptableVersionRange)); + } + + // Otherwise ensure it is a version range with bounds + Matcher matcher = INTERVAL_PATTERN.matcher(acceptableVersionRange); + Preconditions.checkArgument(matcher.matches(), "invalid version range"); + String leftBound = matcher.group("left"); + String rightBound = matcher.group("right"); + Preconditions.checkArgument( + leftBound != null || rightBound != null, "left and right bounds cannot both be empty"); + BiPredicate leftComparator = + acceptableVersionRange.startsWith("[") ? VersionChecker::GE : VersionChecker::GT; + BiPredicate rightComparator = + acceptableVersionRange.endsWith("]") ? VersionChecker::LE : VersionChecker::LT; + + if (leftBound != null && !leftComparator.test(pluginVersion, parseVersion(leftBound))) { + return false; + } + if (rightBound != null && !rightComparator.test(pluginVersion, parseVersion(rightBound))) { + return false; + } + return true; + } + + /** + * @return the parsed version + * @throws IllegalArgumentException if an exception occurred + */ + private V parseVersion(String versionString) { + // catch other exceptions and turn into an IllegalArgumentException + try { + return converter.apply(versionString); + } catch (IllegalArgumentException ex) { + throw ex; // rethrow + } catch (Throwable ex) { + // Gradle's GradleVersion throws all sorts of unchecked exceptions + throw new IllegalArgumentException("unable to parse '" + versionString + "'", ex); + } + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java new file mode 100644 index 0000000000..c2552ac276 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link VersionChecker}. */ +public class VersionCheckerTest { + private static class TestVersion implements Comparable { + private int[] components; + + TestVersion(String version) { + String[] parts = version.split("\\.", -1); + components = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + components[i] = Integer.parseInt(parts[i]); + } + } + + @Override + public int compareTo(TestVersion other) { + for (int i = 0; i < Math.max(components.length, other.components.length); i++) { + int a = i < components.length ? components[i] : 0; + int b = i < other.components.length ? other.components[i] : 0; + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + } + return 0; + } + } + + private VersionChecker checker; + + @Before + public void setUp() { + Assert.assertTrue(new TestVersion("1.0").compareTo(new TestVersion("1.1.1")) < 0); + Assert.assertTrue(new TestVersion("1.1.1").compareTo(new TestVersion("1.0")) > 0); + Assert.assertTrue(new TestVersion("1.1").compareTo(new TestVersion("1.1.0.0")) == 0); + checker = new VersionChecker<>(TestVersion::new); + } + + @Test + public void testComparators_LT() { + Assert.assertTrue(VersionChecker.LT(0, 1)); + Assert.assertFalse(VersionChecker.LT(1, 1)); + Assert.assertFalse(VersionChecker.LT(2, 1)); + } + + @Test + public void testComparators_LE() { + Assert.assertTrue(VersionChecker.LE(0, 1)); + Assert.assertTrue(VersionChecker.LE(1, 1)); + Assert.assertFalse(VersionChecker.LE(2, 1)); + } + + @Test + public void testComparators_GE() { + Assert.assertFalse(VersionChecker.GE(0, 1)); + Assert.assertTrue(VersionChecker.GE(1, 1)); + Assert.assertTrue(VersionChecker.GE(2, 1)); + } + + @Test + public void testComparators_GT() { + Assert.assertFalse(VersionChecker.GT(0, 1)); + Assert.assertFalse(VersionChecker.GT(1, 1)); + Assert.assertTrue(VersionChecker.GT(2, 1)); + } + + @Test + public void testRange_leftClosed() { + Assert.assertFalse(checker.compatibleVersion("[2.3,4.3]", "1.0")); + Assert.assertFalse(checker.compatibleVersion("[2.3,4.3)", "1.0")); + Assert.assertFalse(checker.compatibleVersion("[2.3,)", "1.0")); + Assert.assertFalse(checker.compatibleVersion("[2.3,]", "1.0")); + } + + @Test + public void testRange_leftClosed_exact() { + Assert.assertTrue(checker.compatibleVersion("[2.3,4.3]", "2.3")); + Assert.assertTrue(checker.compatibleVersion("[2.3,4.3)", "2.3")); + Assert.assertTrue(checker.compatibleVersion("[2.3,)", "2.3")); + Assert.assertTrue(checker.compatibleVersion("[2.3,]", "2.3")); + } + + @Test + public void testRange_leftOpen() { + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3]", "1.0")); + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3)", "1.0")); + Assert.assertFalse(checker.compatibleVersion("(2.3,)", "1.0")); + Assert.assertFalse(checker.compatibleVersion("(2.3,]", "1.0")); + } + + @Test + public void testRange_leftOpen_exact() { + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3]", "2.3")); + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3)", "2.3")); + Assert.assertFalse(checker.compatibleVersion("(2.3,)", "2.3")); + Assert.assertFalse(checker.compatibleVersion("(2.3,]", "2.3")); + } + + @Test + public void testRange_rightClosed() { + Assert.assertFalse(checker.compatibleVersion("[2.3,4.3]", "5.0")); + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3]", "5.0")); + Assert.assertFalse(checker.compatibleVersion("[,4.3]", "5.0")); + Assert.assertFalse(checker.compatibleVersion("(,4.3]", "5.0")); + } + + @Test + public void testRange_rightClosed_exact() { + Assert.assertTrue(checker.compatibleVersion("[2.3,4.3]", "4.3")); + Assert.assertTrue(checker.compatibleVersion("(2.3,4.3]", "4.3")); + Assert.assertTrue(checker.compatibleVersion("[,4.3]", "4.3")); + Assert.assertTrue(checker.compatibleVersion("(,4.3]", "4.3")); + } + + @Test + public void testRange_between() { + Assert.assertTrue(checker.compatibleVersion("[2.3,4.3]", "2.4")); + Assert.assertTrue(checker.compatibleVersion("(2.3,4.3]", "4.2")); + Assert.assertTrue(checker.compatibleVersion("[2.3,4.3)", "2.4")); + Assert.assertTrue(checker.compatibleVersion("(2.3,4.3)", "4.2")); + } + + @Test + public void testRange_rightOpen() { + Assert.assertFalse(checker.compatibleVersion("[2.3,4.3)", "5.0")); + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3)", "5.0")); + Assert.assertFalse(checker.compatibleVersion("[,4.3)", "5.0")); + Assert.assertFalse(checker.compatibleVersion("(,4.3)", "5.0")); + } + + @Test + public void testRange_rightOpen_exact() { + Assert.assertFalse(checker.compatibleVersion("[2.3,4.3)", "4.3")); + Assert.assertFalse(checker.compatibleVersion("(2.3,4.3)", "4.3")); + Assert.assertFalse(checker.compatibleVersion("[,4.3)", "4.3")); + Assert.assertFalse(checker.compatibleVersion("(,4.3)", "4.3")); + } + + @Test + public void testMinimumBound_low() { + Assert.assertFalse(checker.compatibleVersion("2.3", "1.0")); + Assert.assertFalse(checker.compatibleVersion("2.3", "2.2")); + } + + @Test + public void testMinimumBound_exact() { + Assert.assertTrue(checker.compatibleVersion("2.3", "2.3")); + } + + @Test + public void testMinimumBound_high() { + Assert.assertTrue(checker.compatibleVersion("2.3", "2.4")); + Assert.assertTrue(checker.compatibleVersion("2.3", "4.0")); + } + + // @SuppressWarnings({"TryFailThrowable", "AssertionFailureIgnored"}) + @Test + public void testRange_invalid() { + for (String rangeSpec : + new String[] {"[]", "[,]", "(,]", "[,)", "(,)", "[1,2,3]", "[1]", "foo", "{,2.3)", ""}) { + try { + checker.compatibleVersion(rangeSpec, "1.3"); + Assert.fail("should have thrown an exception for " + rangeSpec); + } catch (IllegalArgumentException ex) { + // as expected + } + } + } +} From 2194f5b0c5afcd37d9a4405808d2f9268cc0772a Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 3 Jul 2019 11:53:30 -0400 Subject: [PATCH 0647/2020] Remove useless version field from skaffold-files-v2 (#1821) --- .../cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java | 4 ---- .../tools/jib/plugins/common/SkaffoldFilesOutputTest.java | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java index 4fd4ccdf0d..33594d3648 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutput.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.ProjectInfo; import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -26,7 +25,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; /** * Builds a JSON string containing files and directories that build = new ArrayList<>(); private final List inputs = new ArrayList<>(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java index a8de049be9..e25d2070f2 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldFilesOutputTest.java @@ -26,7 +26,7 @@ public class SkaffoldFilesOutputTest { private static final String TEST_JSON = - "{\"version\":null,\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}"; + "{\"build\":[\"buildFile1\",\"buildFile2\"],\"inputs\":[\"input1\",\"input2\"],\"ignore\":[\"ignore1\",\"ignore2\"]}"; @Test public void testGetJsonString() throws IOException { @@ -44,8 +44,7 @@ public void testGetJsonString() throws IOException { public void testGetJsonString_empty() throws IOException { SkaffoldFilesOutput skaffoldFilesOutput = new SkaffoldFilesOutput(); Assert.assertEquals( - "{\"version\":null,\"build\":[],\"inputs\":[],\"ignore\":[]}", - skaffoldFilesOutput.getJsonString()); + "{\"build\":[],\"inputs\":[],\"ignore\":[]}", skaffoldFilesOutput.getJsonString()); } @Test From 051d3e5e7f615a9d0b1bcea86c433fc0a41a7744 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 4 Jul 2019 00:00:40 -0400 Subject: [PATCH 0648/2020] Fix check-version integration tests (#1822) --- .../maven/BuildDockerMojoIntegrationTest.java | 19 ++++++++++-------- .../maven/BuildImageMojoIntegrationTest.java | 20 ++++++++++--------- .../maven/BuildTarMojoIntegrationTest.java | 20 +++++++++++-------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 5d440069c1..782f9ac3dc 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -239,15 +239,18 @@ public void testExecute_jibRequireVersion_ok() throws VerificationException, IOE } @Test - public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + public void testExecute_jibRequireVersion_fail() throws IOException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); - Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); - verifier.verifyTextInLog("BUILD FAILURE"); - verifier.verifyTextInLog("but is required to be"); + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:dockerBuild")); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), CoreMatchers.containsString("but is required to be [,1.0]")); + } } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 77bcdb9dff..23028671b9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -558,16 +558,18 @@ public void testExecute_jibRequireVersion_ok() throws VerificationException, IOE } @Test - public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + public void testExecute_jibRequireVersion_fail() throws IOException { String targetImage = "simpleimage:maven" + System.nanoTime(); - - Instant before = Instant.now(); - Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.executeGoals(Arrays.asList("package", "jib:build")); - verifier.verifyTextInLog("BUILD FAILURE"); - verifier.verifyTextInLog("but is required to be"); + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:build")); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), CoreMatchers.containsString("but is required to be [,1.0]")); + } } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index b43b8df461..12e5616a5e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -104,15 +105,18 @@ public void testExecute_jibRequireVersion_ok() throws VerificationException, IOE } @Test - public void testExecute_jibRequireVersion_fail() throws VerificationException, IOException { + public void testExecute_jibRequireVersion_fail() throws IOException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); - Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); - verifier.setSystemProperty("_TARGET_IMAGE", targetImage); - verifier.executeGoals(Arrays.asList("package", "jib:buildTar")); - verifier.verifyTextInLog("BUILD FAILURE"); - verifier.verifyTextInLog("but is required to be"); + try { + Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.executeGoals(Arrays.asList("package", "jib:buildTar")); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), CoreMatchers.containsString("but is required to be [,1.0]")); + } } } From 611ef1649fc8b2c23f9ba319524ea4135a9cb6e2 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 5 Jul 2019 17:27:54 -0400 Subject: [PATCH 0649/2020] Fix BuildImageMojoIntegrationTest (#1829) --- .../maven/BuildImageMojoIntegrationTest.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 23028671b9..645c6cb9af 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -546,24 +546,30 @@ public void testExecute_jibContainerizeSkips() throws VerificationException, IOE @Test public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { - String targetImage = "simpleimage:maven" + System.nanoTime(); + String targetImage = "localhost:6000/simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - // this plugin should match 1.0 - verifier.setSystemProperty("jib.requiredVersion", "1.0"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + // properties required to push to :6000 for plain pom.xml + verifier.setSystemProperty("jib.to.auth.username", "testuser2"); + verifier.setSystemProperty("jib.to.auth.password", "testpassword2"); + verifier.setSystemProperty("sendCredentialsOverHttp", "true"); + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + // this test plugin should match 1.0 + verifier.setSystemProperty("jib.requiredVersion", "1.0"); verifier.executeGoals(Arrays.asList("package", "jib:build")); verifier.verifyErrorFreeLog(); } @Test public void testExecute_jibRequireVersion_fail() throws IOException { - String targetImage = "simpleimage:maven" + System.nanoTime(); + String targetImage = "localhost:6000/simpleimage:maven" + System.nanoTime(); try { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); - verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); + // other properties aren't required as this should fail due to jib.requiredVersion verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + // this plugin should be > 1.0 and so jib:build should fail + verifier.setSystemProperty("jib.requiredVersion", "[,1.0]"); verifier.executeGoals(Arrays.asList("package", "jib:build")); Assert.fail(); } catch (VerificationException ex) { From a5caf17f037c48408888b11138306c6c2c92c376 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 9 Jul 2019 15:41:35 -0400 Subject: [PATCH 0650/2020] Simplify DockerClient (#1832) --- .../cloud/tools/jib/api/Containerizer.java | 12 +-- .../tools/jib/api/DockerDaemonImage.java | 9 +- .../cloud/tools/jib/docker/DockerClient.java | 102 +++++------------- .../tools/jib/api/DockerDaemonImageTest.java | 7 +- 4 files changed, 38 insertions(+), 92 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index cea467bfbb..235dcff7a8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.UserCacheHome; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -85,14 +84,11 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { ImageConfiguration imageConfiguration = ImageConfiguration.builder(dockerDaemonImage.getImageReference()).build(); - DockerClient.Builder dockerClientBuilder = DockerClient.builder(); - dockerDaemonImage.getDockerExecutable().ifPresent(dockerClientBuilder::setDockerExecutable); - dockerClientBuilder.setDockerEnvironment( - ImmutableMap.copyOf(dockerDaemonImage.getDockerEnvironment())); - + DockerClient dockerClient = + new DockerClient( + dockerDaemonImage.getDockerExecutable(), dockerDaemonImage.getDockerEnvironment()); Function stepsRunnerFactory = - buildConfiguration -> - StepsRunner.begin(buildConfiguration).dockerLoadSteps(dockerClientBuilder.build()); + buildConfiguration -> StepsRunner.begin(buildConfiguration).dockerLoadSteps(dockerClient); return new Containerizer( DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory, false); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index d9b39ff429..517c32e515 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -17,11 +17,10 @@ package com.google.cloud.tools.jib.api; // TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. +import com.google.cloud.tools.jib.docker.DockerClient; import java.nio.file.Path; import java.util.Collections; import java.util.Map; -import java.util.Optional; -import javax.annotation.Nullable; /** Builds to the Docker daemon. */ public class DockerDaemonImage { @@ -51,7 +50,7 @@ public static DockerDaemonImage named(String imageReference) } private final ImageReference imageReference; - @Nullable private Path dockerExecutable; + private Path dockerExecutable = DockerClient.DEFAULT_DOCKER_CLIENT; private Map dockerEnvironment = Collections.emptyMap(); /** Instantiate with {@link #named}. */ @@ -85,8 +84,8 @@ ImageReference getImageReference() { return imageReference; } - Optional getDockerExecutable() { - return Optional.ofNullable(dockerExecutable); + Path getDockerExecutable() { + return dockerExecutable; } Map getDockerEnvironment() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 0ddcb34614..274ba7bcdd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -35,57 +35,34 @@ /** Calls out to the {@code docker} CLI. */ public class DockerClient { - /** Builds a {@link DockerClient}. */ - public static class Builder { - - private Path dockerExecutable = DEFAULT_DOCKER_CLIENT; - private ImmutableMap dockerEnvironment = ImmutableMap.of(); - - private Builder() {} - - /** - * Sets a path for a {@code docker} executable. - * - * @param dockerExecutable path to {@code docker} - * @return this - */ - public Builder setDockerExecutable(Path dockerExecutable) { - this.dockerExecutable = dockerExecutable; - return this; - } - - /** - * Sets environment variables to use when executing the {@code docker} executable. - * - * @param dockerEnvironment environment variables for {@code docker} - * @return this - */ - public Builder setDockerEnvironment(ImmutableMap dockerEnvironment) { - this.dockerEnvironment = dockerEnvironment; - return this; - } - - public DockerClient build() { - return new DockerClient(dockerExecutable, dockerEnvironment); - } - } + /** Default path to the docker executable. */ + public static final Path DEFAULT_DOCKER_CLIENT = Paths.get("docker"); /** - * Gets a new {@link Builder} for {@link DockerClient} with defaults. + * Checks if Docker is installed on the user's system and accessible by running the default {@code + * docker} command. * - * @return a new {@link Builder} + * @return {@code true} if Docker is installed on the user's system and accessible */ - public static Builder builder() { - return new Builder(); + public static boolean isDefaultDockerInstalled() { + return isDockerInstalled(DEFAULT_DOCKER_CLIENT); } /** - * Instantiates with the default {@code docker} executable. + * Checks if Docker is installed on the user's system and accessible by running the given {@code + * docker} executable. * - * @return a new {@link DockerClient} + * @param dockerExecutable path to the executable to test running + * @return {@code true} if Docker is installed on the user's system and accessible */ - public static DockerClient newDefaultClient() { - return builder().build(); + public static boolean isDockerInstalled(Path dockerExecutable) { + try { + new ProcessBuilder(dockerExecutable.toString()).start(); + return true; + + } catch (IOException ex) { + return false; + } } /** @@ -111,51 +88,24 @@ static Function, ProcessBuilder> defaultProcessBuilderFactory( }; } - private static final Path DEFAULT_DOCKER_CLIENT = Paths.get("docker"); - /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ private final Function, ProcessBuilder> processBuilderFactory; - @VisibleForTesting - DockerClient(Function, ProcessBuilder> processBuilderFactory) { - this.processBuilderFactory = processBuilderFactory; - } - /** * Instantiates with a {@code docker} executable and environment variables. * * @param dockerExecutable path to {@code docker} * @param dockerEnvironment environment variables for {@code docker} */ - private DockerClient(Path dockerExecutable, ImmutableMap dockerEnvironment) { - this(defaultProcessBuilderFactory(dockerExecutable.toString(), dockerEnvironment)); + public DockerClient(Path dockerExecutable, Map dockerEnvironment) { + this( + defaultProcessBuilderFactory( + dockerExecutable.toString(), ImmutableMap.copyOf(dockerEnvironment))); } - /** - * Checks if Docker is installed on the user's system and accessible by running the default {@code - * docker} command. - * - * @return {@code true} if Docker is installed on the user's system and accessible - */ - public static boolean isDefaultDockerInstalled() { - return isDockerInstalled(DEFAULT_DOCKER_CLIENT); - } - - /** - * Checks if Docker is installed on the user's system and accessible by running the given {@code - * docker} executable. - * - * @param dockerExecutable path to the executable to test running - * @return {@code true} if Docker is installed on the user's system and accessible - */ - public static boolean isDockerInstalled(Path dockerExecutable) { - try { - new ProcessBuilder(dockerExecutable.toString()).start(); - return true; - - } catch (IOException ex) { - return false; - } + @VisibleForTesting + DockerClient(Function, ProcessBuilder> processBuilderFactory) { + this.processBuilderFactory = processBuilderFactory; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java index 82fa4ceda9..52ae1db7d8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java @@ -16,9 +16,9 @@ package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.common.collect.ImmutableMap; import java.nio.file.Paths; -import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -30,7 +30,8 @@ public void testGetters_default() throws InvalidImageReferenceException { DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named("docker/daemon/image"); Assert.assertEquals("docker/daemon/image", dockerDaemonImage.getImageReference().toString()); - Assert.assertEquals(Optional.empty(), dockerDaemonImage.getDockerExecutable()); + Assert.assertEquals( + DockerClient.DEFAULT_DOCKER_CLIENT, dockerDaemonImage.getDockerExecutable()); Assert.assertEquals(0, dockerDaemonImage.getDockerEnvironment().size()); } @@ -41,7 +42,7 @@ public void testGetters() throws InvalidImageReferenceException { .setDockerExecutable(Paths.get("docker/binary")) .setDockerEnvironment(ImmutableMap.of("key", "value")); - Assert.assertEquals(Paths.get("docker/binary"), dockerDaemonImage.getDockerExecutable().get()); + Assert.assertEquals(Paths.get("docker/binary"), dockerDaemonImage.getDockerExecutable()); Assert.assertEquals(ImmutableMap.of("key", "value"), dockerDaemonImage.getDockerEnvironment()); } } From 4dcc4685c05ec004a1e0d9e6a32d0d00d94899a7 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 10 Jul 2019 13:14:43 -0400 Subject: [PATCH 0651/2020] Leverage NullAway checks with Optional for registry client framework (#1831) * Enforce null checks with Optional+NullAway --- ...icationMethodRetrieverIntegrationTest.java | 8 ++- .../registry/BlobCheckerIntegrationTest.java | 4 +- .../ManifestPullerIntegrationTest.java | 2 +- .../RegistryAuthenticatorIntegrationTest.java | 7 ++- .../builder/steps/AuthenticatePushStep.java | 6 +- .../jib/builder/steps/PullBaseImageStep.java | 6 +- .../tools/jib/builder/steps/PushBlobStep.java | 2 +- .../AuthenticationMethodRetriever.java | 14 ++--- .../cloud/tools/jib/registry/BlobChecker.java | 15 ++--- .../cloud/tools/jib/registry/BlobPusher.java | 16 ++--- .../jib/registry/RegistryAuthenticator.java | 9 +-- .../tools/jib/registry/RegistryClient.java | 37 +++++------- .../jib/registry/RegistryEndpointCaller.java | 7 +-- .../registry/RegistryEndpointProvider.java | 2 - .../AuthenticationMethodRetrieverTest.java | 8 ++- .../tools/jib/registry/BlobCheckerTest.java | 8 +-- .../tools/jib/registry/BlobPusherTest.java | 4 +- .../registry/RegistryAuthenticatorTest.java | 60 +++++++++++-------- 18 files changed, 107 insertions(+), 108 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 39f4f655b3..936efa0a0f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -31,9 +32,10 @@ public void testGetRegistryAuthenticator() throws IOException, RegistryException RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") .newRegistryClient(); - RegistryAuthenticator registryAuthenticator = registryClient.getRegistryAuthenticator(); - Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(null); + Optional registryAuthenticator = + registryClient.getRegistryAuthenticator(); + Assert.assertTrue(registryAuthenticator.isPresent()); + Authorization authorization = registryAuthenticator.get().authenticatePull(null); RegistryClient authorizedRegistryClient = RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index bb90d21af3..b94e078a72 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -47,7 +47,7 @@ public void testCheck_exists() throws IOException, RegistryException { registryClient.pullManifest("latest", V22ManifestTemplate.class); DescriptorDigest blobDigest = manifestTemplate.getLayers().get(0).getDigest(); - Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).getDigest()); + Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).get().getDigest()); } @Test @@ -60,6 +60,6 @@ public void testCheck_doesNotExist() throws IOException, RegistryException, Dige DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Assert.assertNull(registryClient.checkBlob(fakeBlobDigest)); + Assert.assertFalse(registryClient.checkBlob(fakeBlobDigest).isPresent()); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 43b8c80e98..3c3ee29754 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -73,7 +73,7 @@ public void testPull_v22ManifestList() throws IOException, RegistryException { RegistryClient.Factory factory = RegistryClient.factory(EventHandlers.NONE, "registry-1.docker.io", "library/openjdk"); Authorization authorization = - factory.newRegistryClient().getRegistryAuthenticator().authenticatePull(null); + factory.newRegistryClient().getRegistryAuthenticator().get().authenticatePull(null); RegistryClient registryClient = factory.setAuthorization(authorization).newRegistryClient(); // Ensure 11-jre-slim is a manifest list diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index ccb7ed873e..ed4165bf6a 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import java.io.IOException; +import java.util.Optional; import org.junit.Assert; import org.junit.Test; @@ -32,15 +33,15 @@ public class RegistryAuthenticatorIntegrationTest { public void testAuthenticate() throws IOException, RegistryException, InvalidImageReferenceException { ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); - RegistryAuthenticator registryAuthenticator = + Optional registryAuthenticator = RegistryClient.factory( EventHandlers.NONE, dockerHubImageReference.getRegistry(), dockerHubImageReference.getRepository()) .newRegistryClient() .getRegistryAuthenticator(); - Assert.assertNotNull(registryAuthenticator); - Authorization authorization = registryAuthenticator.authenticatePull(null); + Assert.assertTrue(registryAuthenticator.isPresent()); + Authorization authorization = registryAuthenticator.get().authenticatePull(null); // Checks that some token was received. Assert.assertTrue(0 < authorization.getToken().length()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 67b6547329..783fbad8ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -60,13 +60,13 @@ public Optional call() throws IOException, RegistryException { TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, registry))) { - RegistryAuthenticator registryAuthenticator = + Optional registryAuthenticator = buildConfiguration .newTargetImageRegistryClientFactory() .newRegistryClient() .getRegistryAuthenticator(); - if (registryAuthenticator != null) { - return Optional.of(registryAuthenticator.authenticatePush(registryCredential)); + if (registryAuthenticator.isPresent()) { + return Optional.of(registryAuthenticator.get().authenticatePush(registryCredential)); } } catch (InsecureRegistryException ex) { // Cannot skip certificate validation or use HTTP; fall through. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index bab08c12cf..1e93d1e003 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -155,14 +155,14 @@ public ImageAndAuthorization call() // The registry requires us to authenticate using the Docker Token Authentication. // See https://docs.docker.com/registry/spec/auth/token try { - RegistryAuthenticator registryAuthenticator = + Optional registryAuthenticator = buildConfiguration .newBaseImageRegistryClientFactory() .newRegistryClient() .getRegistryAuthenticator(); - if (registryAuthenticator != null) { + if (registryAuthenticator.isPresent()) { Authorization pullAuthorization = - registryAuthenticator.authenticatePull(registryCredential); + registryAuthenticator.get().authenticatePull(registryCredential); return new ImageAndAuthorization( pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 19f4eafa6b..c3711d8ef0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -72,7 +72,7 @@ public BlobDescriptor call() throws IOException, RegistryException { .newRegistryClient(); // check if the BLOB is available - if (registryClient.checkBlob(blobDescriptor.getDigest()) != null) { + if (registryClient.checkBlob(blobDescriptor.getDigest()).isPresent()) { buildConfiguration .getEventHandlers() .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index ed28efee19..7176376629 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -26,10 +26,12 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** Retrieves the {@code WWW-Authenticate} header from the registry API. */ -class AuthenticationMethodRetriever implements RegistryEndpointProvider { +class AuthenticationMethodRetriever + implements RegistryEndpointProvider> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String userAgent; @@ -55,12 +57,11 @@ public List getAccept() { * The request did not error, meaning that the registry does not require authentication. * * @param response ignored - * @return {@code null} + * @return {@link Optional#empty()} */ @Override - @Nullable - public RegistryAuthenticator handleResponse(Response response) { - return null; + public Optional handleResponse(Response response) { + return Optional.empty(); } @Override @@ -79,8 +80,7 @@ public String getActionDescription() { } @Override - @Nullable - public RegistryAuthenticator handleHttpResponseException( + public Optional handleHttpResponseException( HttpResponseException httpResponseException) throws HttpResponseException, RegistryErrorException { // Only valid for status code of '401 Unauthorized'. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index 7c9176b6d7..e2b61300a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -27,13 +27,14 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; /** * Checks if an image's BLOB exists on a registry, and retrieves its {@link BlobDescriptor} if it * exists. */ -class BlobChecker implements RegistryEndpointProvider { +class BlobChecker implements RegistryEndpointProvider> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final DescriptorDigest blobDigest; @@ -47,7 +48,7 @@ class BlobChecker implements RegistryEndpointProvider { /** @return the BLOB's content descriptor */ @Override - public BlobDescriptor handleResponse(Response response) throws RegistryErrorException { + public Optional handleResponse(Response response) throws RegistryErrorException { long contentLength = response.getContentLength(); if (contentLength < 0) { throw new RegistryErrorExceptionBuilder(getActionDescription()) @@ -55,12 +56,12 @@ public BlobDescriptor handleResponse(Response response) throws RegistryErrorExce .build(); } - return new BlobDescriptor(contentLength, blobDigest); + return Optional.of(new BlobDescriptor(contentLength, blobDigest)); } @Override - @Nullable - public BlobDescriptor handleHttpResponseException(HttpResponseException httpResponseException) + public Optional handleHttpResponseException( + HttpResponseException httpResponseException) throws RegistryErrorException, HttpResponseException { if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw httpResponseException; @@ -70,12 +71,12 @@ public BlobDescriptor handleHttpResponseException(HttpResponseException httpResp if (httpResponseException.getContent() == null) { // TODO: The Google HTTP client gives null content for HEAD requests. Make the content never // be null, even for HEAD requests. - return null; + return Optional.empty(); } ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); if (errorCode == ErrorCodes.BLOB_UNKNOWN) { - return null; + return Optional.empty(); } // BLOB_UNKNOWN was not found as a error response code. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index d0ebfcafef..3d04288d5b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -29,6 +29,7 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -51,7 +52,7 @@ class BlobPusher { @Nullable private final String sourceRepository; /** Initializes the BLOB upload. */ - private class Initializer implements RegistryEndpointProvider { + private class Initializer implements RegistryEndpointProvider> { @Nullable @Override @@ -65,19 +66,18 @@ public List getAccept() { } /** - * @return a URL to continue pushing the BLOB to, or {@code null} if the BLOB already exists on - * the registry + * @return a URL to continue pushing the BLOB to, or {@link Optional#empty()} if the BLOB + * already exists on the registry */ - @Nullable @Override - public URL handleResponse(Response response) throws RegistryErrorException { + public Optional handleResponse(Response response) throws RegistryErrorException { switch (response.getStatusCode()) { case HttpURLConnection.HTTP_CREATED: // The BLOB exists in the registry. - return null; + return Optional.empty(); case HttpURLConnection.HTTP_ACCEPTED: - return getRedirectLocation(response); + return Optional.of(getRedirectLocation(response)); default: throw buildRegistryErrorException( @@ -211,7 +211,7 @@ private Committer(URL location) { * @return a {@link RegistryEndpointProvider} for initializing the BLOB upload with an existence * check */ - RegistryEndpointProvider initializer() { + RegistryEndpointProvider> initializer() { return new Initializer(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 7820595a1c..2b34e5e080 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -40,6 +40,7 @@ import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -64,8 +65,7 @@ public class RegistryAuthenticator { * @see https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ - @Nullable - static RegistryAuthenticator fromAuthenticationMethod( + static Optional fromAuthenticationMethod( String authenticationMethod, RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent) @@ -73,7 +73,7 @@ static RegistryAuthenticator fromAuthenticationMethod( // If the authentication method starts with 'basic ' (case insensitive), no registry // authentication is needed. if (authenticationMethod.matches("^(?i)(basic) .*")) { - return null; + return Optional.empty(); } // Checks that the authentication method starts with 'bearer ' (case insensitive). @@ -104,7 +104,8 @@ static RegistryAuthenticator fromAuthenticationMethod( ? serviceMatcher.group(1) : registryEndpointRequestProperties.getServerUrl(); - return new RegistryAuthenticator(realm, service, registryEndpointRequestProperties, userAgent); + return Optional.of( + new RegistryAuthenticator(realm, service, registryEndpointRequestProperties, userAgent)); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 9e8cf51bd9..f6ccdcf97c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -34,13 +34,12 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.base.Verify; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimap; import java.io.IOException; import java.net.URL; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -263,12 +262,12 @@ private RegistryClient( /** * @return the {@link RegistryAuthenticator} to authenticate pulls/pushes with the registry, or - * {@code null} if no token authentication is necessary + * {@link Optional#empty()} if no token authentication is necessary * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - @Nullable - public RegistryAuthenticator getRegistryAuthenticator() throws IOException, RegistryException { + public Optional getRegistryAuthenticator() + throws IOException, RegistryException { // Gets the WWW-Authenticate header (eg. 'WWW-Authenticate: Bearer // realm="https://gcr.io/v2/token",service="gcr.io"') return callRegistryEndpoint( @@ -291,11 +290,7 @@ public T pullManifest( String imageTag, Class manifestTemplateClass) throws IOException, RegistryException { ManifestPuller manifestPuller = new ManifestPuller<>(registryEndpointRequestProperties, imageTag, manifestTemplateClass); - T manifestTemplate = callRegistryEndpoint(manifestPuller); - if (manifestTemplate == null) { - throw new IllegalStateException("ManifestPuller#handleResponse does not return null"); - } - return manifestTemplate; + return callRegistryEndpoint(manifestPuller); } public ManifestTemplate pullManifest(String imageTag) throws IOException, RegistryException { @@ -313,21 +308,19 @@ public ManifestTemplate pullManifest(String imageTag) throws IOException, Regist */ public DescriptorDigest pushManifest(BuildableManifestTemplate manifestTemplate, String imageTag) throws IOException, RegistryException { - return Verify.verifyNotNull( - callRegistryEndpoint( - new ManifestPusher( - registryEndpointRequestProperties, manifestTemplate, imageTag, eventHandlers))); + return callRegistryEndpoint( + new ManifestPusher( + registryEndpointRequestProperties, manifestTemplate, imageTag, eventHandlers)); } /** * @param blobDigest the blob digest to check for - * @return the BLOB's {@link BlobDescriptor} if the BLOB exists on the registry, or {@code null} - * if it doesn't + * @return the BLOB's {@link BlobDescriptor} if the BLOB exists on the registry, or {@link + * Optional#empty()} if it doesn't * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - @Nullable - public BlobDescriptor checkBlob(DescriptorDigest blobDigest) + public Optional checkBlob(DescriptorDigest blobDigest) throws IOException, RegistryException { BlobChecker blobChecker = new BlobChecker(registryEndpointRequestProperties, blobDigest); return callRegistryEndpoint(blobChecker); @@ -401,8 +394,8 @@ && canAttemptBlobMount(authorization, sourceRepository))) { // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} // POST /v2//blobs/uploads/ - URL patchLocation = callRegistryEndpoint(blobPusher.initializer()); - if (patchLocation == null) { + Optional patchLocation = callRegistryEndpoint(blobPusher.initializer()); + if (!patchLocation.isPresent()) { // The BLOB exists already. return true; } @@ -411,8 +404,7 @@ && canAttemptBlobMount(authorization, sourceRepository))) { // PATCH with BLOB URL putLocation = - callRegistryEndpoint(blobPusher.writer(patchLocation, writtenByteCountListener)); - Preconditions.checkNotNull(putLocation); + callRegistryEndpoint(blobPusher.writer(patchLocation.get(), writtenByteCountListener)); timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); @@ -465,7 +457,6 @@ String getUserAgent() { * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - @Nullable private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { return new RegistryEndpointCaller<>( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index c0e10f7eea..c57e9136d2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -159,12 +159,10 @@ static boolean isBrokenPipe(IOException original) { * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ - @Nullable T call() throws IOException, RegistryException { return callWithAllowInsecureRegistryHandling(initialRequestUrl); } - @Nullable private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, RegistryException { if (!isHttpsProtocol(url) && !allowInsecureRegistries) { throw new InsecureRegistryException(url); @@ -186,7 +184,6 @@ private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, Reg } } - @Nullable private T handleUnverifiableServerException(URL url) throws IOException, RegistryException { if (!allowInsecureRegistries) { throw new InsecureRegistryException(url); @@ -203,7 +200,6 @@ private T handleUnverifiableServerException(URL url) throws IOException, Registr } } - @Nullable private T fallBackToHttp(URL url) throws IOException, RegistryException { GenericUrl httpUrl = new GenericUrl(url); httpUrl.setScheme("http"); @@ -229,11 +225,10 @@ private Function getInsecureConnectionFactory() throws Registry * Calls the registry endpoint with a certain {@link URL}. * * @param url the endpoint URL to call - * @return an object representing the response, or {@code null} + * @return an object representing the response * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ - @Nullable private T call(URL url, Function connectionFactory) throws IOException, RegistryException { // Only sends authorization if using HTTPS or explicitly forcing over HTTP. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index f07dd476f4..be409a09e8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -50,7 +50,6 @@ interface RegistryEndpointProvider { List getAccept(); /** Handles the response specific to the registry action. */ - @Nullable T handleResponse(Response response) throws IOException, RegistryException; /** @@ -61,7 +60,6 @@ interface RegistryEndpointProvider { * could not be handled * @throws RegistryErrorException if there is an error with a remote registry */ - @Nullable default T handleHttpResponseException(HttpResponseException httpResponseException) throws HttpResponseException, RegistryErrorException { throw httpResponseException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index d1e96c1787..06bdca6365 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -56,8 +56,8 @@ public void testGetAccept() { @Test public void testHandleResponse() { - Assert.assertNull( - testAuthenticationMethodRetriever.handleResponse(Mockito.mock(Response.class))); + Assert.assertFalse( + testAuthenticationMethodRetriever.handleResponse(Mockito.mock(Response.class)).isPresent()); } @Test @@ -146,7 +146,9 @@ public void testHandleHttpResponseException_pass() Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); RegistryAuthenticator registryAuthenticator = - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever + .handleHttpResponseException(mockHttpResponseException) + .get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 0625d57cda..06ce13e2c0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -62,7 +62,7 @@ public void testHandleResponse() throws RegistryErrorException { Mockito.when(mockResponse.getContentLength()).thenReturn(0L); BlobDescriptor expectedBlobDescriptor = new BlobDescriptor(0, fakeDigest); - BlobDescriptor blobDescriptor = testBlobChecker.handleResponse(mockResponse); + BlobDescriptor blobDescriptor = testBlobChecker.handleResponse(mockResponse).get(); Assert.assertEquals(expectedBlobDescriptor, blobDescriptor); } @@ -93,10 +93,8 @@ public void testHandleHttpResponseException() throws IOException, RegistryErrorE Mockito.when(mockHttpResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); - BlobDescriptor blobDescriptor = - testBlobChecker.handleHttpResponseException(mockHttpResponseException); - - Assert.assertNull(blobDescriptor); + Assert.assertFalse( + testBlobChecker.handleHttpResponseException(mockHttpResponseException).isPresent()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index 44bf89b649..b5e302506e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -80,7 +80,7 @@ public void testGetAccept() { @Test public void testInitializer_handleResponse_created() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(201); // Created - Assert.assertNull(testBlobPusher.initializer().handleResponse(mockResponse)); + Assert.assertFalse(testBlobPusher.initializer().handleResponse(mockResponse).isPresent()); } @Test @@ -92,7 +92,7 @@ public void testInitializer_handleResponse_accepted() throws IOException, Regist Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://someurl/location"), - testBlobPusher.initializer().handleResponse(mockResponse)); + testBlobPusher.initializer().handleResponse(mockResponse).get()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 0f498051e6..59ca1c8df9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -41,9 +41,10 @@ public class RegistryAuthenticatorTest { public void setUp() throws RegistryAuthenticationFailedException { registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent"); + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .get(); } @Test @@ -51,9 +52,10 @@ public void testFromAuthenticationMethod_bearer() throws MalformedURLException, RegistryAuthenticationFailedException { RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent"); + "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl( @@ -61,9 +63,10 @@ public void testFromAuthenticationMethod_bearer() registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent"); + "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), registryAuthenticator.getAuthenticationUrl( @@ -124,23 +127,26 @@ public void istAuthenticationUrl_oauth2() throws MalformedURLException { @Test public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFailedException { - Assert.assertNull( + Assert.assertFalse( RegistryAuthenticator.fromAuthenticationMethod( - "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent")); + "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .isPresent()); - Assert.assertNull( + Assert.assertFalse( RegistryAuthenticator.fromAuthenticationMethod( - "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent")); + "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .isPresent()); - Assert.assertNull( + Assert.assertFalse( RegistryAuthenticator.fromAuthenticationMethod( - "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", - registryEndpointRequestProperties, - "user-agent")); + "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent") + .isPresent()); } @Test @@ -178,7 +184,10 @@ public void testFromAuthenticationMethod_noService() throws MalformedURLException, RegistryAuthenticationFailedException { RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties, "user-agent"); + "Bearer realm=\"https://somerealm\"", + registryEndpointRequestProperties, + "user-agent") + .get(); Assert.assertEquals( new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), @@ -193,9 +202,10 @@ public void testUserAgent() try { RegistryAuthenticator authenticator = RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"" + server.getEndpoint() + "\"", - registryEndpointRequestProperties, - "Competent-Agent"); + "Bearer realm=\"" + server.getEndpoint() + "\"", + registryEndpointRequestProperties, + "Competent-Agent") + .get(); authenticator.authenticatePush(null); } catch (RegistryAuthenticationFailedException ex) { // Doesn't matter if auth fails. We only examine what we sent. From 46d52113b6618bbea273cf3c47161e1ccb81344e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 10 Jul 2019 14:11:54 -0400 Subject: [PATCH 0652/2020] Remove outdated TODOs (#1834) --- .../main/java/com/google/cloud/tools/jib/api/Containerizer.java | 1 - .../java/com/google/cloud/tools/jib/api/DockerDaemonImage.java | 1 - jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java | 2 -- .../com/google/cloud/tools/jib/api/JibContainerBuilder.java | 2 -- .../main/java/com/google/cloud/tools/jib/api/RegistryImage.java | 1 - .../src/main/java/com/google/cloud/tools/jib/api/TarImage.java | 1 - .../google/cloud/tools/jib/builder/steps/PullBaseImageStep.java | 1 - .../google/cloud/tools/jib/plugins/common/JibBuildRunner.java | 1 - 8 files changed, 10 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 235dcff7a8..119b38ae4e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -15,7 +15,6 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.builder.steps.StepsRunner; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java index 517c32e515..a97e5afbad 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java @@ -15,7 +15,6 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import com.google.cloud.tools.jib.docker.DockerClient; import java.nio.file.Path; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index fb54c73f63..9f43899636 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -15,10 +15,8 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. /** Build containers with Jib. */ -// TODO: Add tests once JibContainerBuilder#containerize() is added. public class Jib { /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 865ffde866..c190b65dff 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -15,7 +15,6 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.BuildResult; @@ -57,7 +56,6 @@ * .containerize(...); * } */ -// TODO: Add tests once containerize() is added. public class JibContainerBuilder { private static String capitalizeFirstLetter(String string) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java index 43242e49e0..a7fd5854bb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java @@ -15,7 +15,6 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import java.util.ArrayList; import java.util.List; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index 0744870c79..fbb0284b86 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -15,7 +15,6 @@ */ package com.google.cloud.tools.jib.api; -// TODO: Move to com.google.cloud.tools.jib once that package is cleaned up. import java.nio.file.Path; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 1e93d1e003..40a3f1f1c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -222,7 +222,6 @@ private Image pullBaseImage( registryClient, (V22ManifestListTemplate) manifestTemplate); } - // TODO: Make schema version be enum. switch (manifestTemplate.getSchemaVersion()) { case 1: V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index e55926a19c..228bde108b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -232,7 +232,6 @@ public JibContainer build( throw new BuildStepsExecutionException(message, ex.getCause()); } catch (InterruptedException ex) { - // TODO: Add more suggestions for various build failures. throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); } From 6cd2d8b54b8281bc151b61f482e8fbe43b60ff5c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 11 Jul 2019 19:01:58 -0400 Subject: [PATCH 0653/2020] Minor cleanups (#1838) --- .../google/cloud/tools/jib/builder/steps/StepsRunner.java | 2 +- .../cloud/tools/jib/global/JibSystemProperties.java | 4 ++-- .../google/cloud/tools/jib/registry/RegistryClient.java | 4 +--- .../cloud/tools/jib/registry/RegistryEndpointCaller.java | 3 +-- .../com/google/cloud/tools/jib/http/ConnectionTest.java | 8 +++----- jib-gradle-plugin/CHANGELOG.md | 1 - jib-maven-plugin/CHANGELOG.md | 1 - .../jib/plugins/common/PluginConfigurationProcessor.java | 2 +- 8 files changed, 9 insertions(+), 16 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index ec5ffe2ea6..cf0e838f31 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -79,7 +79,7 @@ private static Future failedFuture() { */ public static StepsRunner begin(BuildConfiguration buildConfiguration) { ExecutorService executorService = - JibSystemProperties.isSerializedExecutionEnabled() + JibSystemProperties.serializeExecution() ? MoreExecutors.newDirectExecutorService() : buildConfiguration.getExecutorService(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index cf78836645..2d11ac09f7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -66,7 +66,7 @@ public static boolean useCrossRepositoryBlobMounts() { * * @return {@code true} if Jib's execution should be serialized, {@code false} if not */ - public static boolean isSerializedExecutionEnabled() { + public static boolean serializeExecution() { return Boolean.getBoolean(SERIALIZE); } @@ -77,7 +77,7 @@ public static boolean isSerializedExecutionEnabled() { * @return {@code true} if authentication information is allowed to be sent over insecure * connections, {@code false} if not */ - public static boolean isSendCredentialsOverHttpEnabled() { + public static boolean sendCredentialsOverHttp() { return Boolean.getBoolean(SEND_CREDENTIALS_OVER_HTTP); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index f6ccdcf97c..60a494fa4a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -125,9 +125,7 @@ private String makeUserAgent() { return ""; } - StringBuilder userAgentBuilder = new StringBuilder(); - userAgentBuilder.append("jib"); - userAgentBuilder.append(" ").append(ProjectInfo.VERSION); + StringBuilder userAgentBuilder = new StringBuilder("jib ").append(ProjectInfo.VERSION); if (userAgentSuffix != null) { userAgentBuilder.append(" ").append(userAgentSuffix); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index c57e9136d2..0300879379 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -232,8 +232,7 @@ private Function getInsecureConnectionFactory() throws Registry private T call(URL url, Function connectionFactory) throws IOException, RegistryException { // Only sends authorization if using HTTPS or explicitly forcing over HTTP. - boolean sendCredentials = - isHttpsProtocol(url) || JibSystemProperties.isSendCredentialsOverHttpEnabled(); + boolean sendCredentials = isHttpsProtocol(url) || JibSystemProperties.sendCredentialsOverHttp(); try (Connection connection = connectionFactory.apply(url)) { Request.Builder requestBuilder = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index a7e26c2eb2..e68be69f92 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -119,15 +119,13 @@ private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { Mockito.when( mockHttpRequestFactory.buildRequest( - Mockito.any(String.class), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) + Mockito.anyString(), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) .thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setHeaders(Mockito.any(HttpHeaders.class))) .thenReturn(mockHttpRequest); - if (httpTimeout != null) { - Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); - } + Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); mockHttpResponse = Mockito.mock(HttpResponse.class); Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7c648500ed..5a9b3e687b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -18,7 +18,6 @@ All notable changes to this project will be documented in this file. - Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) - Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) - ## 1.3.0 ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 915f35178c..4d58ddf7fe 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -17,7 +17,6 @@ All notable changes to this project will be documented in this file. - Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) - Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) - ## 1.3.0 ### Changed diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 593f70f94d..568799202f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -164,7 +164,7 @@ static PluginConfigurationProcessor processCommonConfiguration( ImageReference baseImageReference = ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); - if (JibSystemProperties.isSendCredentialsOverHttpEnabled()) { + if (JibSystemProperties.sendCredentialsOverHttp()) { projectProperties.log( LogEvent.warn( "Authentication over HTTP is enabled. It is strongly recommended that you do not " From ad063a220d7f7a48370f2926f549e43913b4835a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Jul 2019 12:46:16 -0400 Subject: [PATCH 0654/2020] Fix Javadoc (#1844) --- .../main/java/com/google/cloud/tools/jib/maven/MojoCommon.java | 1 - 1 file changed, 1 deletion(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 20364e55ee..88d741dd42 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -83,7 +83,6 @@ static Map convertPermissionsList( * Check that the actual version satisfies required Jib version range when specified. No check is * performed if the provided Jib version is {@code null}, which should only occur during debug. * - * @param acceptableVersionSpec a version range or minimum version; see {@link VersionChecker} * @param descriptor the plugin version * @throws MojoExecutionException if the version is not acceptable */ From 714c6e1ab1df3ceb1de7619c339367781668bce8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Jul 2019 13:16:38 -0400 Subject: [PATCH 0655/2020] Increase Timeout in MultithreadedExecutor (#1846) --- .../java/com/google/cloud/tools/jib/MultithreadedExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java index c2b053856f..658aee81ef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java @@ -33,7 +33,7 @@ /** Testing infrastructure for running code across multiple threads. */ public class MultithreadedExecutor implements Closeable { - private static final Duration MULTITHREADED_TEST_TIMEOUT = Duration.ofSeconds(1); + private static final Duration MULTITHREADED_TEST_TIMEOUT = Duration.ofSeconds(3); private static final int THREAD_COUNT = 20; private final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT); From a7f9e5af9f73c37cfce6a10e0b3f67365cf7c386 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Jul 2019 14:32:00 -0400 Subject: [PATCH 0656/2020] Gradle release v1.4.0 (#1847) * [Gradle Release Plugin] - pre tag commit: 'v1.4.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.4.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index cfe19d8482..897ec68146 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.3.1-SNAPSHOT +version = 1.4.1-SNAPSHOT From 3dd68cdbeb44ef3a7253a2f8d22e4de6d1600a0c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 15 Jul 2019 14:32:13 -0400 Subject: [PATCH 0657/2020] Maven release v1.4.0 (#1842) * preparing release 1.4.0 * 1.4.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 01134b0348..23cc2ad3bd 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.3.1-SNAPSHOT + 1.4.1-SNAPSHOT maven-plugin Jib From 0d9a4cee9d7efd4d4335f00b9b970a431d0fe60a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 16 Jul 2019 12:17:09 -0400 Subject: [PATCH 0658/2020] Update changelog and version strings for 1.4.0 (#1848) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 16 files changed, 33 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2302c6f889..f6afb0648b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.3.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.4.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.3.0' + // id 'com.google.cloud.tools.jib' version '1.4.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index e3ee297adf..aadcd82d2a 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.3.0 + 1.4.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 2c16724822..5ff1c98e78 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 8ce33408bf..0fb72c37b1 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.3.0 + 1.4.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 9b195ab9d7..8b102c1fae 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 7b18a5b327..cfea3c172e 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.3.0 + 1.4.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index c392f4a262..ade4c4a3f2 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 8d66114303..e4e68d20f8 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.3.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.4.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 03b9023b1f..d98e5ba294 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.3.0 + 1.4.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index c5fa4d45cd..e67f0ddc4b 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 1ea97766d1..a7523da6dd 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.3.0 + 1.4.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 3ddad16d1d..3a48402f84 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 5a9b3e687b..c4743e46ed 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.4.0 + +### Added + - Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'` ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) - Now automatically supports WAR created by the Spring Boot Gradle Plugin via the `bootWar` task ([#1786](https://github.com/GoogleContainerTools/jib/issues/1786)) - Can now use `jib.from.image = 'scratch'` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 363e7cb7a3..d212e038e7 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.3.0' + id 'com.google.cloud.tools.jib' version '1.4.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4d58ddf7fe..19f881fe62 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.4.0 + +### Added + - Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged` ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) - Can now use `scratch` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index d0b8e27f52..9db15e0862 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.3.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.4.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.3.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.4.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.3.0 + 1.4.0 myimage From cbc92bc5c9646d6199862759c103be2893b0fdb0 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Jul 2019 12:38:36 -0400 Subject: [PATCH 0659/2020] Update README and FAQ about containerizing mode (#1779) --- docs/faq.md | 14 +++++++++++--- jib-gradle-plugin/README.md | 1 + jib-maven-plugin/README.md | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 221d18aac7..129bd64417 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,7 +8,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint-at-runtime)\ -[I want to containerize an executable JAR.](#i-want-to-containerize-an-executable-jar)\ +[I want to containerize a JAR.](#i-want-to-containerize-an-executable-jar)\ [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ @@ -157,9 +157,17 @@ See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.for Normally, the plugin sets a default entrypoint for java applications, or lets you configure a custom entrypoint using the `container.entrypoint` configuration parameter. You can also override the default/configured entrypoint by defining a custom entrypoint when running the container. See [`docker run --entrypoint` reference](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) for running the image with Docker and overriding the entrypoint command, or see [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. -### I want to containerize an executable JAR. +### I want to containerize a JAR. -Although it is possible to configure Jib to add a JAR to the container and run it using a custom `java -jar ...` or `java -cp ...` entrypoint command, the intention of Jib is to add individual class files and dependency JARs into the container instead of putting a runnable JAR into the container. This lets Jib choose an opinionated, optimal layout for the application on the container image, which also allows it to skip the extra JAR-packaging step. +The intention of Jib is to add individual class files, resources, and dependency JARs into the container instead of putting a JAR. This lets Jib choose an opinionated, optimal layout for the application on the container image, which also allows it to skip the extra JAR-packaging step. + +However, you can set `packaged` (Maven) or `jib.containerizingMode = 'packaged'` (Gradle) to containerize a JAR, but note that your application will always be run via `java -cp ... your.MainClass` (even if it is an executable JAR). Some disadvantages: + +- You need to run the JAR-packaging step (`mvn package` in Maven or the `jar` task in Gradle). +- Reduced granularity in building and caching: if any of your Java source files or resource files are updated, not only the JAR has to be rebuilt, but the entire layer containing the JAR in the image has to be recreated and pushed to the destination. +- If it is a fat or shaded JAR embedding all dependency JARs, you are duplicating the dependency JARs in the image. Worse, it results in far more reduced granularity in building and caching, as dependency JARs can be huge and all of them need to be pushed repeatedly even if they do not change. + +Note that for runnable JARs/WARs, currently Jib does not natively support creating an image that runs a JAR (or WAR) through `java -jar runnable.jar` (although it is not impossible to configure Jib to do so at the expense of more complex project setup.) ### Where is the application in the container filesystem? diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index d212e038e7..023ed53876 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -176,6 +176,7 @@ Field | Type | Default | Description `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. `extraDirectories` | [`extraDirectories`](#extradirectories-closure) | See [`extraDirectories`](#extradirectories-closure) | Configures the directories used to add arbitrary files to the image. +`containerizingMode` | `String` | `exploded` | If set to `packaged`, puts the JAR artifact built by the Gradle Java plugin into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `from` is a closure with the following properties: diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 9db15e0862..4032edfd5c 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -221,6 +221,7 @@ Field | Type | Default | Description `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. `extraDirectories` | [`extraDirectories`](#extradirectories-object) | See [`extraDirectories`](#extradirectories-object) | Configures the directories used to add arbitrary files to the image. +`containerizingMode` | string | `exploded` | If set to `packaged`, puts the JAR artifact built at `${project.build.directory}/${project.build.finalName}.jar` (the default location where many JAR-buidiling plugins put a JAR registered as a main artifact, such as the Maven JAR Plugin) into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. From 9cbad71ccb00c4d4ef473027d5866605a22730b9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 16 Jul 2019 12:47:38 -0400 Subject: [PATCH 0660/2020] Increase executor shutdown timeout in tests (#1849) --- .../jib/gradle/GradleProjectProperties.java | 5 ++++- .../tools/jib/maven/MavenProjectProperties.java | 5 ++++- .../common/logging/SingleThreadedExecutor.java | 17 +++++++++-------- .../logging/AnsiLoggerWithFooterTest.java | 11 +++++++---- .../common/logging/PlainConsoleLoggerTest.java | 7 +++++-- .../logging/SingleThreadedExecutorTest.java | 5 ++++- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 2f7e1e4d62..b9479d47dd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -40,6 +40,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -73,6 +74,8 @@ class GradleProjectProperties implements ProjectProperties { /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; + private static final Duration LOGGING_THREAD_SHUTDOWN_TIMEOUT = Duration.ofSeconds(1); + /** @return a GradleProjectProperties from the given project and logger. */ static GradleProjectProperties getForProject(Project project, Logger logger) { return new GradleProjectProperties(project, logger); @@ -245,7 +248,7 @@ public List getClassFiles() throws IOException { @Override public void waitForLoggingThread() { - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(LOGGING_THREAD_SHUTDOWN_TIMEOUT); } @Override diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index dcf284564f..d643039da0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -72,6 +73,8 @@ public class MavenProjectProperties implements ProjectProperties { /** Used for logging during main class inference. */ private static final String JAR_PLUGIN_NAME = "'maven-jar-plugin'"; + private static final Duration LOGGING_THREAD_SHUTDOWN_TIMEOUT = Duration.ofSeconds(1); + /** * @param project the {@link MavenProject} for the plugin. * @param session the {@link MavenSession} for the plugin. @@ -277,7 +280,7 @@ public List getClassFiles() throws IOException { @Override public void waitForLoggingThread() { - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(LOGGING_THREAD_SHUTDOWN_TIMEOUT); } @Override diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java index 3468896489..d3369e026b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutor.java @@ -29,20 +29,21 @@ */ public class SingleThreadedExecutor { - private static final Duration EXECUTOR_SHUTDOWN_WAIT = Duration.ofSeconds(1); - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - /** Shuts down the {@link #executorService} and waits for it to terminate. */ - public void shutDownAndAwaitTermination() { + /** + * Shuts down the {@link #executorService} and waits for it to terminate. + * + * @param timeout timeout to wait. The method may call {@link ExecutorService#awaitTermination} + * times with the given timeout, so the overall wait time can go up to 2 times the timeout + */ + public void shutDownAndAwaitTermination(Duration timeout) { executorService.shutdown(); try { - if (!executorService.awaitTermination( - EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + if (!executorService.awaitTermination(timeout.getSeconds(), TimeUnit.SECONDS)) { executorService.shutdownNow(); - if (!executorService.awaitTermination( - EXECUTOR_SHUTDOWN_WAIT.getSeconds(), TimeUnit.SECONDS)) { + if (!executorService.awaitTermination(timeout.getSeconds(), TimeUnit.SECONDS)) { System.err.println("Could not shut down SingleThreadedExecutor"); } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index a0acdf8a78..9d1597f88e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.collect.ImmutableMap; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -31,6 +32,8 @@ /** Tests for {@link AnsiLoggerWithFooter}. */ public class AnsiLoggerWithFooterTest { + private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(3); + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final List messages = new ArrayList<>(); @@ -89,7 +92,7 @@ public void testLog_noFooter() { testAnsiLoggerWithFooter.log(Level.WARN, "warn"); testAnsiLoggerWithFooter.log(Level.ERROR, "error"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals( Arrays.asList("lifecycle", "progress", "info", "debug", "warn", "error"), messages); @@ -113,7 +116,7 @@ public void testLog_ignoreIfNoMessageConsumer() { testAnsiLoggerWithFooter.log(Level.WARN, "warn"); testAnsiLoggerWithFooter.log(Level.ERROR, "error"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals(Collections.singletonList("lifecycle"), messages); Assert.assertEquals(Collections.singletonList(Level.LIFECYCLE), levels); @@ -125,7 +128,7 @@ public void testLog_sameFooter() { testAnsiLoggerWithFooter.log(Level.INFO, "message"); testAnsiLoggerWithFooter.log(Level.INFO, "another message"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals( Arrays.asList( @@ -156,7 +159,7 @@ public void testLog_changingFooter() { testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); testAnsiLoggerWithFooter.log(Level.WARN, "another message"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals( Arrays.asList( diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java index c21603f8c7..b6cc368c4b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLoggerTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.common.collect.ImmutableMap; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -30,6 +31,8 @@ /** Tests for {@link PlainConsoleLogger}. */ public class PlainConsoleLoggerTest { + private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(3); + private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final List levels = new ArrayList<>(); @@ -60,7 +63,7 @@ public void testLog() { testPlainConsoleLogger.log(Level.WARN, "warn"); testPlainConsoleLogger.log(Level.ERROR, "error"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals( Arrays.asList( @@ -84,7 +87,7 @@ public void testLog_ignoreIfNoMessageConsumer() { testPlainConsoleLogger.log(Level.WARN, "warn"); testPlainConsoleLogger.log(Level.ERROR, "error"); - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); Assert.assertEquals(Collections.singletonList(Level.WARN), levels); Assert.assertEquals(Collections.singletonList("warn"), messages); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java index ca55e04269..e875eb9fbe 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/SingleThreadedExecutorTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.MultithreadedExecutor; import java.io.IOException; +import java.time.Duration; import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; @@ -28,6 +29,8 @@ /** Tests for {@link SingleThreadedExecutor}. */ public class SingleThreadedExecutorTest { + private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(3); + @SuppressWarnings("ThreadPriorityCheck") // use of Thread.yield() @Test public void testExecute_mutualExclusion() @@ -50,6 +53,6 @@ public void testExecute_mutualExclusion() })); } - singleThreadedExecutor.shutDownAndAwaitTermination(); + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); } } From 173574bd5882756e577af0fa0973cc06e8079841 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 18 Jul 2019 18:27:20 -0400 Subject: [PATCH 0661/2020] Update jackson-databind to 2.9.9.1 (#1856) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index a7808dd240..d75483fc77 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'com.google.http-client:google-http-client:1.27.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:27.0.1-jre' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' implementation 'org.ow2.asm:asm:7.0' testImplementation 'junit:junit:4.12' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 0a23000b68..9465169fae 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -64,7 +64,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 23cc2ad3bd..d50f9f7624 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -71,7 +71,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.9 + 2.9.9.1 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 5aa426dcd5..57c021e599 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -32,7 +32,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' From 2c9ed928075b3ae160b362b2503e768543df7a9e Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 22 Jul 2019 17:43:37 -0400 Subject: [PATCH 0662/2020] Tweak issue template and reorganize FAQ (#1861) --- .github/ISSUE_TEMPLATE.md | 29 ++- docs/faq.md | 369 ++++++++++++++++++++------------------ 2 files changed, 210 insertions(+), 188 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 385f28d889..fb190d0958 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,33 +1,30 @@ -**Description of the issue**: - - +- *OS:* - - UNAUTHORIZED errors: - https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized +**Description of the issue**: - - Other errors accessing remote registries - https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries ---> **Expected behavior**: + **Steps to reproduce**: -**Environment**: + 1. + 2. + 3. **`jib-maven-plugin` Configuration**: ```xml diff --git a/docs/faq.md b/docs/faq.md index 129bd64417..87e51976a3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,29 +5,35 @@ If a question you have is not answered below, please [submit an issue](/../../is [But, I'm not a Java developer.](#but-im-not-a-java-developer)\ [How do I run the image I built?](#how-do-i-run-the-image-i-built)\ [Where is bash?](#where-is-bash)\ -[How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [What image format does Jib use?](#what-image-format-does-jib-use)\ +[Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ +[Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ +[Can I learn more about container images?](#can-i-learn-more-about-container-images) + +**How-Tos**\ +[How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint-at-runtime)\ [I want to containerize a JAR.](#i-want-to-containerize-an-executable-jar)\ -[Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ [I need to add files generated during the build process to a custom directory on the image.](#i-need-to-add-files-generated-during-the-build-process-to-a-custom-directory-on-the-image)\ [Can I build to a local Docker daemon?](#can-i-build-to-a-local-docker-daemon)\ +[How do I enable debugging?](#how-do-i-enable-debugging)\ [What would a Dockerfile for a Jib-built image look like?](#what-would-a-dockerfile-for-a-jib-built-image-look-like)\ [How can I inspect the image Jib built?](#how-can-i-inspect-the-image-jib-built)\ -[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ -[How do I configure a proxy?](#how-do-i-configure-a-proxy)\ +[I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ +[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp) + +**Common Problems**\ +[How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ [What should I do when the registry responds with Forbidden or DENIED?](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied)\ [What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized)\ -[How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ +[How do I configure a proxy?](#how-do-i-configure-a-proxy)\ [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ [How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ -[How do I enable debugging?](#how-do-i-enable-debugging)\ -[Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ -[I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ -[How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp)\ -[Can I learn more about container images?](#can-i-learn-more-about-container-images) +[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube) + +--- ### But, I'm not a Java developer. @@ -47,7 +53,7 @@ kubectl run jib-deployment --image= For more information, see [steps 4-6 of the Kubernetes Engine deployment tutorial](https://cloud.google.com/kubernetes-engine/docs/tutorials/hello-app#step_4_create_a_container_cluster). -#### Where is bash? +### Where is bash? By default, Jib uses [`distroless/java`](https://github.com/GoogleContainerTools/distroless/tree/master/java) as the base image. Distroless images contain only runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Check out the [distroless project](https://github.com/GoogleContainerTools/distroless#distroless-docker-images) for more information about distroless images. @@ -83,6 +89,70 @@ jib.from.image = 'gcr.io/distroless/java:debug' You can then run the image in shell form with Docker: `docker run -it --entrypoint /busybox/sh ` +### What image format does Jib use? + +Jib currently builds into the [Docker V2.2](https://docs.docker.com/registry/spec/manifest-v2-2/) image format or [OCI image format](https://github.com/opencontainers/image-spec). + +#### Maven + +See [Extended Usage](../jib-maven-plugin#extended-usage) for the `` configuration. + +#### Gradle + +See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.format` configuration. + +### Why is my image created 48+ years ago? + +For reproducibility purposes, Jib sets the creation time of the container images to 1 second past the Unix epoch (00:00:01, January 1st, 1970 in UTC). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. + +

+Setting `useCurrentTimestamp` parameter +

+ +#### Maven + +```xml + + + true + + +``` + +#### Gradle + +```groovy +jib.container.useCurrentTimestamp = true +``` + +

+
+ +#### Please tell me more about reproducibility! + +_Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. + +To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.useCurrentTimestamp=true` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. + +For more details see [reproducible-builds.org](https://reproducible-builds.org). + +### Where is the application in the container filesystem? + +Jib packages your Java application into the following paths on the image: + +* `/app/libs/` contains all the dependency artifacts +* `/app/resources/` contains all the resource files +* `/app/classes/` contains all the classes files +* the contents of the extra directory (default `src/main/jib`) are placed relative to the container's root directory (`/`) + + +### Can I learn more about container images? + +If you'd like to learn more about container images, [@coollog](https://github.com/coollog) has a guide: [Build Containers the Hard Way](https://containers.gitbook.io/build-containers-the-hard-way/), which takes a deep dive into everything involved in getting your code into a container and onto a container registry. + + +## Configuring Jib + ### How do I set parameters for my image at runtime? #### JVM Flags @@ -141,18 +211,6 @@ spec: For more information, see the [`JAVA_TOOL_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html), the [`docker run -e` reference](https://docs.docker.com/engine/reference/run/#env-environment-variables), and [defining environment variables for a container in Kubernetes](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/). -### What image format does Jib use? - -Jib currently builds into the [Docker V2.2](https://docs.docker.com/registry/spec/manifest-v2-2/) image format or [OCI image format](https://github.com/opencontainers/image-spec). - -#### Maven - -See [Extended Usage](../jib-maven-plugin#extended-usage) for the `` configuration. - -#### Gradle - -See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.format` configuration. - ### Can I define a custom entrypoint at runtime? Normally, the plugin sets a default entrypoint for java applications, or lets you configure a custom entrypoint using the `container.entrypoint` configuration parameter. You can also override the default/configured entrypoint by defining a custom entrypoint when running the container. See [`docker run --entrypoint` reference](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) for running the image with Docker and overriding the entrypoint command, or see [Define a Command and Arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/) for running the image in a [Kubernetes](https://kubernetes.io/) Pod and overriding the entrypoint command. @@ -169,15 +227,6 @@ However, you can set `packaged` (Maven) or `jib.containerizi Note that for runnable JARs/WARs, currently Jib does not natively support creating an image that runs a JAR (or WAR) through `java -jar runnable.jar` (although it is not impossible to configure Jib to do so at the expense of more complex project setup.) -### Where is the application in the container filesystem? - -Jib packages your Java application into the following paths on the image: - -* `/app/libs/` contains all the dependency artifacts -* `/app/resources/` contains all the resource files -* `/app/classes/` contains all the classes files -* the contents of the extra directory (default `src/main/jib`) are placed relative to the container's root directory (`/`) - ### I need to RUN commands like `apt-get`. Running commands like `apt-get` slows down the container build process. We **do not recommend or support** running commands as part of the build. @@ -212,11 +261,11 @@ jib.from.image = 'custom-base-image' ### Can I ADD a custom directory to the image? -We currently support adding a custom directory with an **incubating** feature. This feature may change in later versions. If your application needs to use custom files, place them into the `src/main/jib` folder. Files placed here will be added to the filesystem of the container. For example, `src/main/jib/foo/bar` would add `/foo/bar` into the container filesystem. +We currently support adding a custom directory with an **incubating** feature called _extra directories_. This feature may change in later versions. If your application needs to use custom files, place them into the `src/main/jib` folder. Files placed here will be added to the filesystem of the container. For example, `src/main/jib/foo/bar` would add `/foo/bar` into the container filesystem. ### I need to add files generated during the build process to a custom directory on the image. -If the current extra directory design doesn't meet your needs (e.g. you need to set up the extra files directory with files generated during the build process), you can use additional goals/tasks to create the extra directory as part of your build. +If the current extra directories design doesn't meet your needs (e.g. you need to set up the extra files directory with files generated during the build process), you can use additional goals/tasks to create the extra directory as part of your build.
File copying examples @@ -287,6 +336,79 @@ There are several ways of doing this: - [`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/) the image built with Jib to have it available in your local Docker daemon. - Alternatively, instead of using a Docker daemon, you can run a local container registry, such as [Docker registry](https://docs.docker.com/registry/deploying/) or other repository managers, and point Jib to push to the local registry. +### How do I enable debugging? + +If using the `distroless/java` base image, then use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept local debug connections on port 5005, but not suspend: +``` +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005 +``` + +Then connect your debugger to port 5005 on your local host. You can port-forward the container port to a localhost port for easy access. + +Using Docker: `docker run -p 5005:5005 ` + +Using Kubernetes: `kubectl port-forward 5005:5005` + +Beware: in Java 8 and earlier, specifying only a port meant that the JDWP socket was open to all incoming connections which is insecure. It is recommended to limit the debug port to localhost. + + +### I would like to run my application with a javaagent. + +You can run your container with a javaagent by placing it somewhere in the `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. + +#### Maven + +```xml + + + + -javaagent:/myfolder/agent.jar + + + +``` + +#### Gradle + +```groovy +jib.container.jvmFlags = ['-javaagent:/myfolder/agent.jar'] +``` + +See also [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image) + +### How can I tag my image with a timestamp? + +#### Maven + +To tag the image with a simple timestamp, add the following to your `pom.xml`: + +```xml + + yyyyMMdd-HHmmssSSS + +``` + +Then in the `jib-maven-plugin` configuration, set the `tag` to: + +```xml + + + my-image-name:${maven.build.timestamp} + + +``` + +You can then use the same timestamp to reference the image in other plugins. + +#### Gradle + +To tag the image with a timestamp, simply set the timestamp as the tag for `to.image` in your `jib` configuration. For example: + +```groovy +jib.to.image = 'gcr.io/my-gcp-project/my-app:' + System.nanoTime() +``` + + ### What would a Dockerfile for a Jib-built image look like? A Dockerfile that performs a Jib-like build is shown below: @@ -317,39 +439,20 @@ Some plugins, such as the [Docker Prepare Gradle Plugin](https://github.com/gcla To inspect the image that is produced from the build using Docker, you can use commands such as `docker inspect your/image:tag` to view the image configuration, or you can also download the image using `docker save` to manually inspect the container image. Other tools, such as [dive](https://github.com/wagoodman/dive), provide nicer UI to inspect the image. -### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). - -When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). - -```shell -kubectl create secret docker-registry registry-json-key \ - --docker-server= \ - --docker-username= \ - --docker-password= \ - --docker-email= - -kubectl patch serviceaccount default \ - -p '{"imagePullSecrets":[{"name":"registry-json-key"}]}' -``` -For example, if you are using GCR, the commands would look like (see [Advanced Authentication Methods](https://cloud.google.com/container-registry/docs/advanced-authentication)): - -```shell -kubectl create secret docker-registry gcr-json-key \ - --docker-server=https://gcr.io \ - --docker-username=_json_key \ - --docker-password="$(cat keyfile.json)" \ - --docker-email=any@valid.com +## Common Problems -kubectl patch serviceaccount default \ - -p '{"imagePullSecrets":[{"name":"gcr-json-key"}]}' -``` +### How can I diagnose problems pulling or pushing from remote registries? -See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). +There are a few reasons why Jib may be unable to connect to a remote registry, including: -### How do I configure a proxy? +- **Registry reports FORBIDDEN.** See [_What should I do when the registry responds with Forbidden or DENIED?_](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied) +- **Registry reports UNAUTHORIZED.** See [_What should I do when the registry responds with UNAUTHORIZED?_](#what-should-i-do-when-the-registry-responds-with-unauthorized) +- **Access requires a proxy.** See [_How do I configure a proxy?_](#how-do-i-configure-a-proxy) for details. +- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.9_). +- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.9_). +- **The registry does not support the [Docker Image Format V2 Schema 2](https://github.com/GoogleContainerTools/jib/issues/601)** (sometimes referred to as _v2-2_). This problem is usually shown by failures wth `INVALID_MANIFEST` errors. Some registries can be configured to support V2-2 such as [Artifactory](https://www.jfrog.com/confluence/display/RTF/Docker+Registry#DockerRegistry-LocalDockerRepositories) and [OpenShift](https://docs.openshift.com/container-platform/3.9/install_config/registry/extended_registry_configuration.html#middleware-repository-acceptschema2). Other registries, such as Quay.io/Quay Enterprise, are in the process of adding support. -Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). ### What should I do when the registry responds with Forbidden or DENIED? @@ -375,14 +478,40 @@ If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is oft If you encounter issues interacting with a registry other than `UNAUTHORIZED`, check ["How can I diagnose problems pulling or pushing from remote registries?"](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). -### How can I diagnose problems pulling or pushing from remote registries? +### How do I configure a proxy? -There are a few reasons why Jib may be unable to connect to a remote registry, including: +Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). + +### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). + +When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). + +```shell +kubectl create secret docker-registry registry-json-key \ + --docker-server= \ + --docker-username= \ + --docker-password= \ + --docker-email= + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"registry-json-key"}]}' +``` + +For example, if you are using GCR, the commands would look like (see [Advanced Authentication Methods](https://cloud.google.com/container-registry/docs/advanced-authentication)): + +```shell +kubectl create secret docker-registry gcr-json-key \ + --docker-server=https://gcr.io \ + --docker-username=_json_key \ + --docker-password="$(cat keyfile.json)" \ + --docker-email=any@valid.com + +kubectl patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"gcr-json-key"}]}' +``` + +See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). -- **Access requires a proxy.** See [_How do I configure a proxy?_](#how-do-i-configure-a-proxy) for details. -- **The registry does not support HTTPS.** We do not pass authentication details on non-HTTPS connections, though this can be overridden with the `sendCredentialsOverHttp` system property, but it is not recommend (_version 0.9.9_). -- **The registry's SSL certificates have expired or are not trusted.** We have a separate document on [handling registries that use self-signed certificates](self_sign_cert.md), which may also apply if the SSL certificate is signed by an untrusted Certificate Authority. Jib supports an `allowInsecureRegistries` flag to ignore SSL certificate validation, but it is not recommend (_version 0.9.9_). -- **The registry does not support the [Docker Image Format V2 Schema 2](https://github.com/GoogleContainerTools/jib/issues/601)** (sometimes referred to as _v2-2_). This problem is usually shown by failures wth `INVALID_MANIFEST` errors. Some registries can be configured to support V2-2 such as [Artifactory](https://www.jfrog.com/confluence/display/RTF/Docker+Registry#DockerRegistry-LocalDockerRepositories) and [OpenShift](https://docs.openshift.com/container-platform/3.9/install_config/registry/extended_registry_configuration.html#middleware-repository-acceptschema2). Other registries, such as Quay.io/Quay Enterprise, are in the process of adding support. ### How can I examine network traffic? @@ -412,107 +541,3 @@ gradle -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=tru Maven: use `mvn -X -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. Gradle: use `grade --debug -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. - -### How do I enable debugging? - -If using the `distroless/java` base image, then use the [`JAVA_TOOL_OPTIONS`](#how-do-i-set-parameters-for-my-image-at-runtime) to pass along debugging configuration arguments. For example, to have the remote VM accept local debug connections on port 5005, but not suspend: -``` --agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005 -``` - -Then connect your debugger to port 5005 on your local host. You can port-forward the container port to a localhost port for easy access. - -Using Docker: `docker run -p 5005:5005 ` - -Using Kubernetes: `kubectl port-forward 5005:5005` - -Beware: in Java 8 and earlier, specifying only a port meant that the JDWP socket was open to all incoming connections which is insecure. It is recommended to limit the debug port to localhost. - -### Why is my image created 48+ years ago? - -For reproducibility purposes, Jib sets the creation time of the container images to 0 (January 1st, 1970). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. - -#### Maven - -```xml - - - true - - -``` - -#### Gradle - -```groovy -jib.container.useCurrentTimestamp = true -``` - -#### Please tell me more about reproducibility! - -_Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. - -To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.useCurrentTimestamp=true` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. - -For more details see [reproducible-builds.org](https://reproducible-builds.org). - - -### I would like to run my application with a javaagent. - -You can run your container with a javaagent by placing it somewhere in the `src/main/jib` directory to add it to the container's filesystem, then pointing to it using Jib's `container.jvmFlags` configuration. - -#### Maven - -```xml - - - - -javaagent:/myfolder/agent.jar - - - -``` - -#### Gradle - -```groovy -jib.container.jvmFlags = ['-javaagent:/myfolder/agent.jar'] -``` - -See also [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image) - -### How can I tag my image with a timestamp? - -#### Maven - -To tag the image with a simple timestamp, add the following to your `pom.xml`: - -```xml - - yyyyMMdd-HHmmssSSS - -``` - -Then in the `jib-maven-plugin` configuration, set the `tag` to: - -```xml - - - my-image-name:${maven.build.timestamp} - - -``` - -You can then use the same timestamp to reference the image in other plugins. - -#### Gradle - -To tag the image with a timestamp, simply set the timestamp as the tag for `to.image` in your `jib` configuration. For example: - -```groovy -jib.to.image = 'gcr.io/my-gcp-project/my-app:' + System.nanoTime() -``` - -### Can I learn more about container images? - -If you'd like to learn more about container images, [@coollog](https://github.com/coollog) has a guide: [Build Containers the Hard Way](https://containers.gitbook.io/build-containers-the-hard-way/), which takes a deep dive into everything involved in getting your code into a container and onto a container registry. From e3e4bcbd83bf10c0c4a6f66d953c11aa84a5799d Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 23 Jul 2019 14:10:15 -0400 Subject: [PATCH 0663/2020] Update faq.md (#1862) --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 87e51976a3..a467e6e001 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -13,7 +13,7 @@ If a question you have is not answered below, please [submit an issue](/../../is **How-Tos**\ [How do I set parameters for my image at runtime?](#how-do-i-set-parameters-for-my-image-at-runtime)\ [Can I define a custom entrypoint?](#can-i-define-a-custom-entrypoint-at-runtime)\ -[I want to containerize a JAR.](#i-want-to-containerize-an-executable-jar)\ +[I want to containerize a JAR.](#i-want-to-containerize-a-jar)\ [I need to RUN commands like `apt-get`.](#i-need-to-run-commands-like-apt-get)\ [Can I ADD a custom directory to the image?](#can-i-add-a-custom-directory-to-the-image)\ [I need to add files generated during the build process to a custom directory on the image.](#i-need-to-add-files-generated-during-the-build-process-to-a-custom-directory-on-the-image)\ From 9d28fe28991d77a9abbc715571fcf80a634bb799 Mon Sep 17 00:00:00 2001 From: sullis Date: Tue, 23 Jul 2019 15:18:49 -0400 Subject: [PATCH 0664/2020] spring-boot 2.1.6 (#1863) --- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index e67f0ddc4b..4e9d843337 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'eclipse' id 'idea' - id 'org.springframework.boot' version '2.0.4.RELEASE' + id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'com.google.cloud.tools.jib' version '1.4.0' } diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index a7523da6dd..b472af67ca 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -10,7 +10,7 @@ org.springframework.boot spring-boot-starter-parent - 2.0.4.RELEASE + 2.1.6.RELEASE From fb29ea705d151a8159ad22b1c72235d55b5bb020 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 25 Jul 2019 13:02:02 -0400 Subject: [PATCH 0665/2020] Update faq for expired credentials in config.json (#1869) * Update faq for expired credentials in config.json --- docs/faq.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/faq.md b/docs/faq.md index a467e6e001..31892950b7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -466,6 +466,7 @@ If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is oft * You did not configure auth information in the default places where Jib searches. - `$HOME/.docker/config.json`, [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). + - `$HOME/.docker/config.json` may also contain short-lived authorizations in the `auths` block that may have expired. In the case of Google Container Registry, if you had previously used `gcloud docker` to configure these authorizations, you should remove these stale authorizations by editing your `config.json` and deleting lines from `auths` associated with `gcr.io` (for example: `"https://asia.gcr.io"`). You can then run `gcloud auth configure-docker` to correctly configure the `credHelpers` block for more robust interactions with gcr. - Some common credential helpers on `$PATH` (for example, `docker-credential-osxkeychain`, `docker-credential-ecr-login`, etc.) for well-known registries. - Jib configurations - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) for Maven / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) for Gradle From 6ebc11c92baa54b3f14a9c23f4d2e773f8cf4b2d Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 25 Jul 2019 23:22:22 -0400 Subject: [PATCH 0666/2020] Support running integration tests against local registry (#1824) --- CONTRIBUTING.md | 16 ++++++++-- .../jib/IntegrationTestingConfiguration.java | 14 ++++++--- .../gradle/EmptyProjectIntegrationTest.java | 6 ++-- .../cloud/tools/jib/gradle/JibRunHelper.java | 3 ++ .../gradle/SingleProjectIntegrationTest.java | 6 ++-- .../jib/gradle/WarProjectIntegrationTest.java | 2 +- .../maven/BuildImageMojoIntegrationTest.java | 29 ++++++++++++------- 7 files changed, 51 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6afb0648b..c2caef8984 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,9 @@ Jib comes as 3 components: - `jib-maven-plugin`: a Maven plugin that uses `jib-core` and `jib-plugins-common` - `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` and `jib-plugins-common` -To build, use the provided `build.sh` which builds and tests each of the components into your local `~/.m2/repository`. Note that this script does not run integration tests. +To build, use the provided `build.sh` which builds and tests each of the +components into your local `~/.m2/repository`. Note that this script does +not run integration tests. ## Code Reviews @@ -45,7 +47,17 @@ To build, use the provided `build.sh` which builds and tests each of the compone 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! -**Note** that in order to run integration tests, you will need to set the environment variable `JIB_INTEGRATION_TESTING_PROJECT` to the GCP project you would like to use for testing. You will also need Docker installed with the daemon running. Otherwise, feel free to skip integration tests. +**Note** that in order to run integration tests, you will need to set one of the +following environment variables: + + - `JIB_INTEGRATION_TESTING_PROJECT`: the GCP project to use for testing; + the registry tested will be `gcr.io/`. + - `JIB_INTEGRATION_TESTING_LOCATION`: a specific registry for testing. + To run the integration tests locally, you can run + `docker run -d -p 9990:5000 registry:2` and use `localhost:9990`. + +You will also need Docker installed with the daemon running. Note that the +integration tests will create local registries on ports 5000 and 6000. # Development Tips diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java index f39ef24abc..7c3e0ef844 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java @@ -22,13 +22,19 @@ /** Configuration for integration tests. */ public class IntegrationTestingConfiguration { - public static String getGCPProject() { + public static String getTestRepositoryLocation() { String projectId = System.getenv("JIB_INTEGRATION_TESTING_PROJECT"); - if (Strings.isNullOrEmpty(projectId)) { + if (!Strings.isNullOrEmpty(projectId)) { + return "gcr.io/" + projectId; + } + String location = System.getenv("JIB_INTEGRATION_TESTING_LOCATION"); + if (Strings.isNullOrEmpty(location)) { Assert.fail( - "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the GCP project to use for integration testing."); + "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " + + "GCP project to use for integration testing or " + + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); } - return projectId; + return location; } private IntegrationTestingConfiguration() {} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index f382198c46..db839e9e0e 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -62,8 +62,7 @@ private static void assertDockerInspect(String imageReference) @Test public void testBuild_empty() throws IOException, InterruptedException, DigestException { String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() + IntegrationTestingConfiguration.getTestRepositoryLocation() + "/emptyimage:gradle" + System.nanoTime(); Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); @@ -75,8 +74,7 @@ public void testBuild_empty() throws IOException, InterruptedException, DigestEx public void testBuild_multipleTags() throws IOException, InterruptedException, InvalidImageReferenceException, DigestException { String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() + IntegrationTestingConfiguration.getTestRepositoryLocation() + "/multitag-image:gradle" + System.nanoTime(); JibRunHelper.buildAndRunAdditionalTag( diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 975483883a..17e909a05f 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -55,6 +55,7 @@ static String buildAndRun( "-Djib.useOnlyProjectCache=true", "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); assertImageDigestAndId(testProject.getProjectRoot()); @@ -73,6 +74,7 @@ static void buildAndRunAdditionalTag( "-Djib.useOnlyProjectCache=true", "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), "-D_ADDITIONAL_TAG=" + additionalTag); assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); assertImageDigestAndId(testProject.getProjectRoot()); @@ -99,6 +101,7 @@ static BuildResult buildToDockerDaemon( "-Djib.useOnlyProjectCache=true", "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, + "-Djib.allowInsecureRegistries=" + imageReference.startsWith("localhost"), "-b=" + gradleBuildFile); assertBuildSuccess(buildResult, "jibDockerBuild", "Built image to Docker daemon as "); assertImageDigestAndId(testProject.getProjectRoot()); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 4598457ae0..29da90afb2 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -179,8 +179,7 @@ public void setup() throws IOException, InterruptedException { @Test public void testBuild_simple() throws IOException, InterruptedException, DigestException { String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() + IntegrationTestingConfiguration.getTestRepositoryLocation() + "/simpleimage:gradle" + System.nanoTime(); @@ -218,8 +217,7 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE @Test public void testBuild_failOffline() { String targetImage = - "gcr.io/" - + IntegrationTestingConfiguration.getGCPProject() + IntegrationTestingConfiguration.getTestRepositoryLocation() + "/simpleimageoffline:gradle" + System.nanoTime(); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 5a093ee96c..1b161061ce 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -74,7 +74,7 @@ public void testBuild_tomcatServlet25() private void verifyBuildAndRun(TestProject project, String label, String gradleBuildFile) throws IOException, InterruptedException, DigestException { - String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; String targetImage = nameBase + label + System.nanoTime(); String output = JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 645c6cb9af..603d2dfe02 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -74,8 +74,8 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject servlet25Project = new TestProject(testPlugin, "war_servlet25"); - private static String getGcrImageReference(String label) { - String nameBase = "gcr.io/" + IntegrationTestingConfiguration.getGCPProject() + '/'; + private static String getTestImageReference(String label) { + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; return nameBase + label + System.nanoTime(); } @@ -104,6 +104,9 @@ private static Verifier build( Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); + if (imageReference.startsWith("localhost")) { + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + } verifier.setAutoclean(false); verifier.addCliOption("-X"); verifier.addCliOption("--file=" + pomXml); @@ -163,6 +166,9 @@ private static String buildAndRunAdditionalTag( verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); verifier.setSystemProperty("_ADDITIONAL_TAG", additionalTag); + if (imageReference.startsWith("localhost")) { + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + } verifier.setAutoclean(false); verifier.addCliOption("-X"); verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); @@ -324,7 +330,7 @@ public void tearDown() throws IOException, InterruptedException { @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException, DigestException { - String targetImage = getGcrImageReference("simpleimage:maven"); + String targetImage = getTestImageReference("simpleimage:maven"); // Test empty output error try { @@ -372,7 +378,7 @@ public void testExecute_simple() @Test public void testExecute_failOffline() throws IOException { - String targetImage = getGcrImageReference("simpleimageoffline:maven"); + String targetImage = getTestImageReference("simpleimageoffline:maven"); // Test empty output error try { @@ -395,7 +401,7 @@ public void testExecute_simpleOnJava11() throws DigestException, VerificationException, IOException, InterruptedException { Assume.assumeTrue(isJava11RuntimeOrHigher()); - String targetImage = getGcrImageReference("simpleimage:maven"); + String targetImage = getTestImageReference("simpleimage:maven"); Assert.assertEquals( "Hello, world. An argument.\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-java11.xml", false)); @@ -424,7 +430,7 @@ public void testExecute_simpleWithIncomptiableJava11() @Test public void testExecute_empty() throws InterruptedException, IOException, VerificationException, DigestException { - String targetImage = getGcrImageReference("emptyimage:maven"); + String targetImage = getTestImageReference("emptyimage:maven"); Assert.assertEquals( "", buildAndRun(emptyTestProject.getProjectRoot(), targetImage, "pom.xml", false)); assertCreationTimeEpoch(targetImage); @@ -435,7 +441,7 @@ public void testExecute_empty() public void testExecute_multipleTags() throws IOException, InterruptedException, InvalidImageReferenceException, VerificationException, DigestException { - String targetImage = getGcrImageReference("multitag-image:maven"); + String targetImage = getTestImageReference("multitag-image:maven"); Assert.assertEquals( "", buildAndRunAdditionalTag( @@ -445,7 +451,7 @@ public void testExecute_multipleTags() @Test public void testExecute_multipleExtraDirectories() throws DigestException, VerificationException, IOException, InterruptedException { - String targetImage = getGcrImageReference("simpleimage:maven"); + String targetImage = getTestImageReference("simpleimage:maven"); Assert.assertEquals( "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-extra-dirs.xml", false)); @@ -469,7 +475,7 @@ public void testExecute_bothDeprecatedAndNewExtraDirectoryConfigUsed() throws IO @Test public void testExecute_deprecatedExtraDirectoryConfigUsed() throws IOException, VerificationException { - String targetImage = getGcrImageReference("simpleimage:maven"); + String targetImage = getTestImageReference("simpleimage:maven"); build(simpleTestProject.getProjectRoot(), targetImage, "pom-deprecated-extra-dir.xml", false) .verifyTextInLog( " is deprecated; use with "); @@ -594,11 +600,14 @@ public void testExecute_tomcatServlet25() private void buildAndRunWar(String label, String pomXml) throws VerificationException, IOException, InterruptedException { - String targetImage = getGcrImageReference(label); + String targetImage = getTestImageReference(label); Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + if (targetImage.startsWith("localhost")) { + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + } verifier.setAutoclean(false); verifier.addCliOption("-X"); verifier.addCliOption("--file=" + pomXml); From b1af34b94349aaa3abc3dcd55c5de20d290cd9e8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 30 Jul 2019 11:53:49 -0400 Subject: [PATCH 0667/2020] Rename DockerLoadManifestEntryTemplate (#1872) --- .../cloud/tools/jib/docker/ImageTarball.java | 4 ++-- ....java => DockerManifestEntryTemplate.java} | 16 +++++++++++-- .../tools/jib/docker/ImageTarballTest.java | 4 ++-- ...t.java => DockerManifestTemplateTest.java} | 24 +++++++++++++++---- 4 files changed, 38 insertions(+), 10 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/{DockerLoadManifestEntryTemplate.java => DockerManifestEntryTemplate.java} (86%) rename jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/{DockerLoadManifestTemplateTest.java => DockerManifestTemplateTest.java} (63%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 5dbed705df..14b86d54f5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.docker; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -57,7 +57,7 @@ public ImageTarball(Image image, ImageReference imageReference) { public void writeTo(OutputStream out) throws IOException { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); - DockerLoadManifestEntryTemplate manifestTemplate = new DockerLoadManifestEntryTemplate(); + DockerManifestEntryTemplate manifestTemplate = new DockerManifestEntryTemplate(); // Adds all the layers to the tarball and manifest. for (Layer layer : image.getLayers()) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java similarity index 86% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java index e2c311a1e4..b131321a9b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestEntryTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java @@ -45,12 +45,16 @@ * @see Docker load * source */ -public class DockerLoadManifestEntryTemplate implements JsonTemplate { +public class DockerManifestEntryTemplate implements JsonTemplate { - private final String config = "config.json"; + private String config = "config.json"; private List repoTags = Collections.singletonList(null); private final List layers = new ArrayList<>(); + public void setConfig(String config) { + this.config = config; + } + public void setRepoTags(String repoTags) { this.repoTags = Collections.singletonList(repoTags); } @@ -58,4 +62,12 @@ public void setRepoTags(String repoTags) { public void addLayerFile(String layer) { layers.add(layer); } + + public String getConfig() { + return config; + } + + public List getLayerFiles() { + return layers; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java index c61a7342b8..630fd6988e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.docker.json.DockerLoadManifestEntryTemplate; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -120,7 +120,7 @@ public void testWriteTo() String manifestJson = CharStreams.toString( new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - JsonTemplateMapper.readListOfJson(manifestJson, DockerLoadManifestEntryTemplate.class); + JsonTemplateMapper.readListOfJson(manifestJson, DockerManifestEntryTemplate.class); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java similarity index 63% rename from jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java index f55cd546fa..e338cfbb3d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerLoadManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.docker.json; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -30,8 +32,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link DockerLoadManifestEntryTemplate}. */ -public class DockerLoadManifestTemplateTest { +/** Tests for {@link DockerManifestEntryTemplate}. */ +public class DockerManifestTemplateTest { @Test public void testToJson() throws URISyntaxException, IOException { @@ -39,14 +41,28 @@ public void testToJson() throws URISyntaxException, IOException { Path jsonFile = Paths.get(Resources.getResource("core/json/loadmanifest.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); - DockerLoadManifestEntryTemplate template = new DockerLoadManifestEntryTemplate(); + DockerManifestEntryTemplate template = new DockerManifestEntryTemplate(); template.setRepoTags( ImageReference.of("testregistry", "testrepo", "testtag").toStringWithTag()); template.addLayerFile("layer1.tar.gz"); template.addLayerFile("layer2.tar.gz"); template.addLayerFile("layer3.tar.gz"); - List loadManifest = Collections.singletonList(template); + List loadManifest = Collections.singletonList(template); Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(loadManifest)); } + + @Test + public void testFromJson() throws URISyntaxException, IOException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/loadmanifest.json").toURI()); + String sourceJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + DockerManifestEntryTemplate template = + new ObjectMapper().readValue(sourceJson, DockerManifestEntryTemplate[].class)[0]; + + Assert.assertEquals( + ImmutableList.of("layer1.tar.gz", "layer2.tar.gz", "layer3.tar.gz"), + template.getLayerFiles()); + Assert.assertEquals("config.json", template.getConfig()); + } } From 62c9cab9c83e99c9ecc21a2e9c96a48940dd1656 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 30 Jul 2019 12:50:26 -0400 Subject: [PATCH 0668/2020] Allow setting file timestamps from plugins (#1818) * Allow setting file timestamps from plugins (#1608) (#1782) * Allow setting file timestamps from plugins (#1608) * Addressed comment related to unnecessary extraDirectories modificationTimes configuration * Addressed remaining comments * Fix integration tests / Clean up * Fix integration test * Handle invalid config value * Refactor * Refactor * Add tests * Refactor and add tests * Take mod time into account when retrieving local application cache * Fix Javadoc * Update error messages * Add Gradle integration tests * Clean up * Remove KEEP_ORIGINAL * Rename methods and variables * Rename things / minor updates * Restore LayerEntriesSelector to consider target modification time too * Fix test * Pass correct argument * format * Fix Javadoc --- jib-core/CHANGELOG.md | 2 +- .../tools/jib/api/JavaContainerBuilder.java | 23 ++++- .../tools/jib/api/LayerConfiguration.java | 52 ++++++++---- .../cloud/tools/jib/api/LayerEntry.java | 17 ++-- .../tools/jib/cache/LayerEntriesSelector.java | 31 ++++--- .../jib/image/ReproducibleLayerBuilder.java | 8 +- .../tools/jib/api/LayerConfigurationTest.java | 2 +- .../cloud/tools/jib/cache/CacheTest.java | 2 +- .../jib/cache/LayerEntriesSelectorTest.java | 34 +++++--- .../image/ReproducibleLayerBuilderTest.java | 14 +-- .../gradle/SingleProjectIntegrationTest.java | 36 ++++++-- ...uild-files-modification-time-custom.gradle | 20 +++++ .../src/main/java/com/test/HelloWorld.java | 8 ++ .../tools/jib/gradle/BuildDockerTask.java | 8 ++ .../tools/jib/gradle/BuildImageTask.java | 8 ++ .../cloud/tools/jib/gradle/BuildTarTask.java | 8 ++ .../tools/jib/gradle/ContainerParameters.java | 14 +++ .../jib/gradle/GradleProjectProperties.java | 11 +-- .../jib/gradle/GradleRawConfiguration.java | 5 ++ .../gradle/GradleProjectPropertiesTest.java | 53 ++++++++---- .../gradle/GradleRawConfigurationTest.java | 2 + .../tools/jib/gradle/JibExtensionTest.java | 8 ++ .../tools/jib/maven/BuildDockerMojo.java | 8 ++ .../cloud/tools/jib/maven/BuildImageMojo.java | 8 ++ .../cloud/tools/jib/maven/BuildTarMojo.java | 8 ++ .../jib/maven/JibPluginConfiguration.java | 25 ++++-- .../jib/maven/MavenProjectProperties.java | 9 +- .../jib/maven/MavenRawConfiguration.java | 5 ++ .../maven/BuildDockerMojoIntegrationTest.java | 6 +- .../maven/BuildImageMojoIntegrationTest.java | 36 ++++++-- .../maven/BuildTarMojoIntegrationTest.java | 2 +- .../jib/maven/JibPluginConfigurationTest.java | 18 ++-- .../jib/maven/MavenProjectPropertiesTest.java | 52 ++++++++---- .../jib/maven/MavenRawConfigurationTest.java | 3 + ...complex-files-modification-time-custom.xml | 76 +++++++++++++++++ .../src/main/java/com/test/HelloWorld.java | 8 ++ ...InvalidFilesModificationTimeException.java | 34 ++++++++ .../common/JavaContainerBuilderHelper.java | 12 ++- .../common/PluginConfigurationProcessor.java | 59 +++++++++++-- .../jib/plugins/common/ProjectProperties.java | 10 +-- .../jib/plugins/common/PropertyNames.java | 2 + .../jib/plugins/common/RawConfiguration.java | 2 + .../JavaContainerBuilderHelperTest.java | 3 +- .../PluginConfigurationProcessorTest.java | 85 ++++++++++++++----- 44 files changed, 660 insertions(+), 177 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-files-modification-time-custom.gradle create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 0c3c01159e..86f1fc7c45 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -42,7 +42,7 @@ All notable changes to this project will be documented in this file. ### Added -- Overloads for `LayerConfiguration#addEntryRecursive` that take providers allowing for setting file permissions/file modification timestamps on a per-file basis ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) +- Overloads for `LayerConfiguration#addEntryRecursive` that take providers to set file permissions and file modification time on a per-file basis ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) ### Changed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index e161b30378..de99f39a9c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -27,6 +27,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; @@ -34,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -175,6 +177,8 @@ public static JavaContainerBuilder from(RegistryImage registryImage) { private RelativeUnixPath dependenciesDestination = RelativeUnixPath.get("libs"); private RelativeUnixPath othersDestination = RelativeUnixPath.get("classpath"); @Nullable private String mainClass; + private BiFunction modificationTimeProvider = + LayerConfiguration.DEFAULT_MODIFICATION_TIME_PROVIDER; private JavaContainerBuilder(JibContainerBuilder jibContainerBuilder) { this.jibContainerBuilder = jibContainerBuilder; @@ -484,6 +488,19 @@ public JavaContainerBuilder setMainClass(String mainClass) { return this; } + /** + * Sets the modification time provider for container files. + * + * @param modificationTimeProvider a provider that takes a source path and destination path on the + * container and returns the file modification time that should be set for that path + * @return this + */ + public JavaContainerBuilder setModificationTimeProvider( + BiFunction modificationTimeProvider) { + this.modificationTimeProvider = modificationTimeProvider; + return this; + } + /** * Returns a new {@link JibContainerBuilder} using the parameters specified on the {@link * JavaContainerBuilder}. @@ -643,7 +660,8 @@ private void addFileToLayer( if (!layerBuilders.containsKey(layerType)) { layerBuilders.put(layerType, LayerConfiguration.builder()); } - layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer); + Instant modificationTime = modificationTimeProvider.apply(sourceFile, pathInContainer); + layerBuilders.get(layerType).addEntry(sourceFile, pathInContainer, modificationTime); } private void addDirectoryContentsToLayer( @@ -665,7 +683,8 @@ private void addDirectoryContentsToLayer( path -> { AbsoluteUnixPath pathOnContainer = basePathInContainer.resolve(sourceRoot.relativize(path)); - builder.addEntry(path, pathOnContainer); + Instant modificationTime = modificationTimeProvider.apply(path, pathOnContainer); + builder.addEntry(path, pathOnContainer, modificationTime); }); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java index 55da591bf1..f3899794cb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java @@ -99,23 +99,40 @@ public Builder addEntry(Path sourceFile, AbsoluteUnixPath pathInContainer) { */ public Builder addEntry( Path sourceFile, AbsoluteUnixPath pathInContainer, FilePermissions permissions) { + return addEntry(sourceFile, pathInContainer, permissions, DEFAULT_MODIFICATION_TIME); + } + + /** + * Adds an entry to the layer with the given file modification time. Only adds the single source + * file to the exact path in the container file system. See {@link Builder#addEntry(Path, + * AbsoluteUnixPath)} for more information. + * + * @param sourceFile the source file to add to the layer + * @param pathInContainer the path in the container file system corresponding to the {@code + * sourceFile} + * @param modificationTime the file modification time + * @return this + * @see Builder#addEntry(Path, AbsoluteUnixPath) + */ + public Builder addEntry( + Path sourceFile, AbsoluteUnixPath pathInContainer, Instant modificationTime) { return addEntry( sourceFile, pathInContainer, - permissions, - DEFAULT_MODIFIED_TIME_PROVIDER.apply(sourceFile, pathInContainer)); + DEFAULT_FILE_PERMISSIONS_PROVIDER.apply(sourceFile, pathInContainer), + modificationTime); } /** - * Adds an entry to the layer with the given permissions. Only adds the single source file to - * the exact path in the container file system. See {@link Builder#addEntry(Path, - * AbsoluteUnixPath)} for more information. + * Adds an entry to the layer with the given permissions and file modification time. Only adds + * the single source file to the exact path in the container file system. See {@link + * Builder#addEntry(Path, AbsoluteUnixPath)} for more information. * * @param sourceFile the source file to add to the layer * @param pathInContainer the path in the container file system corresponding to the {@code * sourceFile} * @param permissions the file permissions on the container - * @param lastModifiedTime the file modification timestamp + * @param modificationTime the file modification time * @return this * @see Builder#addEntry(Path, AbsoluteUnixPath) * @see FilePermissions#DEFAULT_FILE_PERMISSIONS @@ -125,8 +142,8 @@ public Builder addEntry( Path sourceFile, AbsoluteUnixPath pathInContainer, FilePermissions permissions, - Instant lastModifiedTime) { - return addEntry(new LayerEntry(sourceFile, pathInContainer, permissions, lastModifiedTime)); + Instant modificationTime) { + return addEntry(new LayerEntry(sourceFile, pathInContainer, permissions, modificationTime)); } /** @@ -167,7 +184,7 @@ public Builder addEntryRecursive( BiFunction filePermissionProvider) throws IOException { return addEntryRecursive( - sourceFile, pathInContainer, filePermissionProvider, DEFAULT_MODIFIED_TIME_PROVIDER); + sourceFile, pathInContainer, filePermissionProvider, DEFAULT_MODIFICATION_TIME_PROVIDER); } /** @@ -179,7 +196,7 @@ public Builder addEntryRecursive( * sourceFile} * @param filePermissionProvider a provider that takes a source path and destination path on the * container and returns the file permissions that should be set for that path - * @param lastModifiedTimeProvider a provider that takes a source path and destination path on + * @param modificationTimeProvider a provider that takes a source path and destination path on * the container and returns the file modification time that should be set for that path * @return this * @throws IOException if an exception occurred when recursively listing the directory @@ -188,11 +205,11 @@ public Builder addEntryRecursive( Path sourceFile, AbsoluteUnixPath pathInContainer, BiFunction filePermissionProvider, - BiFunction lastModifiedTimeProvider) + BiFunction modificationTimeProvider) throws IOException { FilePermissions permissions = filePermissionProvider.apply(sourceFile, pathInContainer); - Instant modifiedTime = lastModifiedTimeProvider.apply(sourceFile, pathInContainer); - addEntry(sourceFile, pathInContainer, permissions, modifiedTime); + Instant modificationTime = modificationTimeProvider.apply(sourceFile, pathInContainer); + addEntry(sourceFile, pathInContainer, permissions, modificationTime); if (!Files.isDirectory(sourceFile)) { return this; } @@ -202,7 +219,7 @@ public Builder addEntryRecursive( file, pathInContainer.resolve(file.getFileName()), filePermissionProvider, - lastModifiedTimeProvider); + modificationTimeProvider); } } return this; @@ -227,11 +244,12 @@ public LayerConfiguration build() { : FilePermissions.DEFAULT_FILE_PERMISSIONS; /** Default file modification time (EPOCH + 1 second). */ - public static final Instant DEFAULT_MODIFIED_TIME = Instant.ofEpochSecond(1); + public static final Instant DEFAULT_MODIFICATION_TIME = Instant.ofEpochSecond(1); /** Provider that returns default file modification time (EPOCH + 1 second). */ - public static final BiFunction DEFAULT_MODIFIED_TIME_PROVIDER = - (sourcePath, destinationPath) -> DEFAULT_MODIFIED_TIME; + public static final BiFunction + DEFAULT_MODIFICATION_TIME_PROVIDER = + (sourcePath, destinationPath) -> DEFAULT_MODIFICATION_TIME; /** * Gets a new {@link Builder} for {@link LayerConfiguration}. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java index b17ebda94f..d1de767b80 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java @@ -29,7 +29,7 @@ public class LayerEntry { private final Path sourceFile; private final AbsoluteUnixPath extractionPath; private final FilePermissions permissions; - private final Instant lastModifiedTime; + private final Instant modificationTime; /** * Instantiates with a source file and the path to place the source file in the container file @@ -55,18 +55,17 @@ public class LayerEntry { * @param extractionPath the path in the container file system corresponding to the {@code * sourceFile} * @param permissions the file permissions on the container - * @param lastModifiedTime the file modification time, default to 1 second since the epoch - * (https://github.com/GoogleContainerTools/jib/issues/1079) + * @param modificationTime the file modification time */ public LayerEntry( Path sourceFile, AbsoluteUnixPath extractionPath, FilePermissions permissions, - Instant lastModifiedTime) { + Instant modificationTime) { this.sourceFile = sourceFile; this.extractionPath = extractionPath; this.permissions = permissions; - this.lastModifiedTime = lastModifiedTime; + this.modificationTime = modificationTime; } /** @@ -74,8 +73,8 @@ public LayerEntry( * * @return the modification time */ - public Instant getLastModifiedTime() { - return lastModifiedTime; + public Instant getModificationTime() { + return modificationTime; } /** @@ -119,11 +118,11 @@ public boolean equals(Object other) { return sourceFile.equals(otherLayerEntry.sourceFile) && extractionPath.equals(otherLayerEntry.extractionPath) && Objects.equals(permissions, otherLayerEntry.permissions) - && Objects.equals(lastModifiedTime, otherLayerEntry.lastModifiedTime); + && Objects.equals(modificationTime, otherLayerEntry.modificationTime); } @Override public int hashCode() { - return Objects.hash(sourceFile, extractionPath, permissions, lastModifiedTime); + return Objects.hash(sourceFile, extractionPath, permissions, modificationTime); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java index d43a93cd58..b81185b77f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java @@ -41,13 +41,15 @@ * { * "sourceFile": "source/file/for/layer/entry/1", * "extractionPath": "/extraction/path/for/layer/entry/1" - * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" + * "sourceModificationTime": "2018-10-03T15:48:32.416152Z" + * "targetModificationTime": "1970-01-01T00:00:01Z", * "permissions": "777" * }, * { * "sourceFile": "source/file/for/layer/entry/2", * "extractionPath": "/extraction/path/for/layer/entry/2" - * "lastModifiedTime": "2018-10-03T15:48:32.416152Z" + * "sourceModificationTime": "2018-10-03T15:48:32.416152Z" + * "targetModificationTime": "1970-01-01T00:00:01Z", * "permissions": "777" * } * ] @@ -61,14 +63,16 @@ static class LayerEntryTemplate implements JsonTemplate, Comparable 0 ? args[0] : "")); + Path worldFilePath = Paths.get("/app/resources/world"); + if (worldFilePath.toFile().exists()) { + System.out.println(Files.getLastModifiedTime(worldFilePath).toString()); + } // Prints the contents of the extra files. if (Files.exists(Paths.get("/foo"))) { @@ -52,11 +57,14 @@ public static void main(String[] args) throws IOException, URISyntaxException { new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); + System.out.println(Files.getLastModifiedTime(Paths.get("/foo")).toString()); + System.out.println(Files.getLastModifiedTime(Paths.get("/bar/cat")).toString()); } // Prints the contents of the files in the second extra directory. if (Files.exists(Paths.get("/baz"))) { System.out.println( new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + System.out.println(Files.getLastModifiedTime(Paths.get("/baz")).toString()); } // Prints jvm flags diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index bc636f872c..b131b9df19 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -164,6 +165,13 @@ public void buildDocker() throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 0a31bd08f7..ef55b10451 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -144,6 +145,13 @@ public void buildImage() throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5cfeaeac80..179b93d21f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -162,6 +163,13 @@ public void buildTar() throw new GradleException( "container.volumes is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new GradleException( + "container.filesModificationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index f84d308649..d9db45e31e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -47,6 +47,7 @@ public class ContainerParameters { private String appRoot = ""; @Nullable private String user; @Nullable private String workingDirectory; + private String filesModificationTime = "EPOCH_PLUS_SECOND"; @Input @Optional @@ -247,4 +248,17 @@ public String getWorkingDirectory() { public void setWorkingDirectory(String workingDirectory) { this.workingDirectory = workingDirectory; } + + @Input + @Optional + public String getFilesModificationTime() { + if (System.getProperty(PropertyNames.CONTAINER_FILES_MODIFICATION_TIME) != null) { + return System.getProperty(PropertyNames.CONTAINER_FILES_MODIFICATION_TIME); + } + return filesModificationTime; + } + + public void setFilesModificationTime(String filesModificationTime) { + this.filesModificationTime = filesModificationTime; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index b9479d47dd..3ca008e507 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -16,12 +16,10 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -133,11 +131,8 @@ private static boolean isProgressFooterEnabled(Project project) { } @Override - public JibContainerBuilder createContainerBuilder( - RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) { - JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); - + public JibContainerBuilder createJibContainerBuilder( + JavaContainerBuilder javaContainerBuilder, ContainerizingMode containerizingMode) { try { if (isWarProject()) { logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); @@ -233,7 +228,7 @@ public JibContainerBuilder createContainerBuilder( @Override public List getClassFiles() throws IOException { - // TODO: Consolidate with createContainerBuilder + // TODO: Consolidate with createJibContainerBuilder JavaPluginConvention javaPluginConvention = project.getConvention().getPlugin(JavaPluginConvention.class); SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName(MAIN_SOURCE_SET_NAME); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 4f7900eb24..c7ae362e26 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -150,6 +150,11 @@ public Optional getProperty(String propertyName) { return Optional.ofNullable(System.getProperty(propertyName)); } + @Override + public String getFilesModificationTime() { + return jibExtension.getContainer().getFilesModificationTime(); + } + @Override public List getExtraDirectories() { return jibExtension.getExtraDirectories().getPaths(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 59f5b11b38..dc2ef244f7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -41,6 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -90,6 +91,7 @@ public class GradleProjectPropertiesTest { private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; + private static final Instant SAMPLE_FILE_MODIFICATION_TIME = Instant.ofEpochSecond(32); /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ private static class TestFileCollection extends AbstractFileCollection { @@ -163,6 +165,15 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } + private static void assertModificationTime(Instant instant, List layers) { + for (LayerConfiguration layer : layers) { + for (LayerEntry entry : layer.getLayerEntries()) { + String message = "wrong time: " + entry.getSourceFile() + "-->" + entry.getExtractionPath(); + Assert.assertEquals(message, instant, entry.getModificationTime()); + } + } + } + private static Path getResource(String path) throws URISyntaxException { return Paths.get(Resources.getResource(path).toURI()); } @@ -338,7 +349,7 @@ public void testGetMajorJavaVersion_jvm11() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration("/app"); + BuildConfiguration configuration = setupBuildConfiguration("/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); Path applicationDirectory = getResource("gradle/application"); @@ -366,6 +377,11 @@ public void testCreateContainerBuilder_correctFiles() applicationDirectory.resolve("classes/HelloWorld.class"), applicationDirectory.resolve("classes/some.class")), layers.classesLayerEntries.get(0).getLayerEntries()); + + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.snapshotsLayerEntries); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.dependenciesLayerEntries); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.resourcesLayerEntries); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.classesLayerEntries); } @Test @@ -373,17 +389,16 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer Path nonexistentFile = Paths.get("/nonexistent/file"); Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); - gradleProjectProperties.createContainerBuilder( - RegistryImage.named("base"), - AbsoluteUnixPath.get("/anything"), - DEFAULT_CONTAINERIZING_MODE); + gradleProjectProperties.createJibContainerBuilder( + JavaContainerBuilder.from(RegistryImage.named("base")), DEFAULT_CONTAINERIZING_MODE); Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + BuildConfiguration configuration = + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( @@ -413,7 +428,7 @@ public void testCreateContainerBuilder_nonDefaultAppRoot() public void testCreateContainerBuilder_defaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { BuildConfiguration configuration = - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_APP_ROOT); + setupBuildConfiguration(JavaContainerBuilder.DEFAULT_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( Arrays.asList( @@ -443,7 +458,8 @@ public void testCreateContainerBuilder_war() Path webAppDirectory = getResource("gradle/webapp"); setUpWarProject(webAppDirectory); - BuildConfiguration configuration = setupBuildConfiguration("/my/app"); + BuildConfiguration configuration = + setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertSourcePathsUnordered( ImmutableList.of( @@ -509,7 +525,8 @@ public void testCreateContainerBuilder_defaultWebAppRoot() setUpWarProject(getResource("gradle/webapp")); BuildConfiguration configuration = - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); + setupBuildConfiguration( + JavaContainerBuilder.DEFAULT_WEB_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), @@ -544,7 +561,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -552,7 +569,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -560,17 +577,19 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("jib-exploded-war"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } - private BuildConfiguration setupBuildConfiguration(String appRoot) + private BuildConfiguration setupBuildConfiguration( + String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(RegistryImage.named("base")) + .setAppRoot(AbsoluteUnixPath.get(appRoot)) + .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); JibContainerBuilder jibContainerBuilder = new GradleProjectProperties(mockProject, mockLogger) - .createContainerBuilder( - RegistryImage.named("base"), - AbsoluteUnixPath.get(appRoot), - DEFAULT_CONTAINERIZING_MODE); + .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, Containerizer.to(RegistryImage.named("to")) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index 083ebde5c6..5fcab8e405 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -68,6 +68,7 @@ public void testGetters() { Mockito.when(containerParameters.getPorts()).thenReturn(Arrays.asList("80/tcp", "0")); Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); Mockito.when(containerParameters.getUser()).thenReturn("admin:wheel"); + Mockito.when(containerParameters.getFilesModificationTime()).thenReturn("2011-12-03T22:42:05Z"); GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); @@ -96,5 +97,6 @@ public void testGetters() { Sets.newHashSet(rawConfiguration.getToTags())); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); + Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 2a12108c88..851271c620 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -54,6 +54,7 @@ private static void clearProperties() { System.clearProperty("jib.container.ports"); System.clearProperty("jib.container.useCurrentTimestamp"); System.clearProperty("jib.container.user"); + System.clearProperty("jib.container.filesModificationTime"); System.clearProperty("jib.containerizingMode"); System.clearProperty("jib.extraDirectory.path"); System.clearProperty("jib.extraDirectory.permissions"); @@ -124,6 +125,8 @@ public void testContainer() { Assert.assertEquals(Collections.emptyList(), testJibExtension.getContainer().getPorts()); Assert.assertEquals(Collections.emptyMap(), testJibExtension.getContainer().getLabels()); Assert.assertEquals("", testJibExtension.getContainer().getAppRoot()); + Assert.assertEquals( + "EPOCH_PLUS_SECOND", testJibExtension.getContainer().getFilesModificationTime()); testJibExtension.container( container -> { @@ -137,6 +140,7 @@ public void testContainer() { container.setLabels(ImmutableMap.of("label1", "value1", "label2", "value2")); container.setFormat(ImageFormat.OCI); container.setAppRoot("some invalid appRoot value"); + container.setFilesModificationTime("some invalid time value"); }); ContainerParameters container = testJibExtension.getContainer(); Assert.assertEquals(Arrays.asList("foo", "bar", "baz"), container.getEntrypoint()); @@ -151,6 +155,7 @@ public void testContainer() { ImmutableMap.of("label1", "value1", "label2", "value2"), container.getLabels()); Assert.assertSame(ImageFormat.OCI, container.getFormat()); Assert.assertEquals("some invalid appRoot value", container.getAppRoot()); + Assert.assertEquals("some invalid time value", container.getFilesModificationTime()); } @Test @@ -294,6 +299,9 @@ public void testProperties() { Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); + System.setProperty("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); + Assert.assertEquals( + "2011-12-03T22:42:05Z", testJibExtension.getContainer().getFilesModificationTime()); System.setProperty("jib.containerizingMode", "packaged"); Assert.assertEquals("packaged", testJibExtension.getContainerizingMode()); System.setProperty("jib.extraDirectories.paths", "/foo,/bar/baz"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index fbe015dfc9..ee8ee77276 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -162,6 +163,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 9db4d08608..2f0271a6c9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -156,6 +157,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 7502f820e1..a1998e11f3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; @@ -134,6 +135,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException( " is not an absolute Unix-style path: " + ex.getInvalidVolume(), ex); + } catch (InvalidFilesModificationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or special keyword \"EPOCH_PLUS_SECOND\": " + + ex.getInvalidFilesModificationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index bd4c7bcc9d..a02d311cef 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -129,9 +129,7 @@ Optional getMode() { /** Configuration for {@code from} parameter, */ public static class FromConfiguration { - @Nullable - @Parameter(required = true) - private String image; + @Nullable @Parameter private String image; @Nullable @Parameter private String credHelper; @@ -186,6 +184,8 @@ public static class ContainerParameters { @Nullable @Parameter private String user; @Nullable @Parameter private String workingDirectory; + + @Parameter private String filesModificationTime = "EPOCH_PLUS_SECOND"; } /** Configuration for the {@code extraDirectories} parameter. */ @@ -281,7 +281,7 @@ String getBaseImage() { if (property != null) { return property; } - return Preconditions.checkNotNull(from).image; + return from.image; } /** @@ -295,7 +295,7 @@ String getBaseImageCredentialHelperName() { if (property != null) { return property; } - return Preconditions.checkNotNull(from).credHelper; + return from.credHelper; } AuthConfiguration getBaseImageAuth() { @@ -348,7 +348,7 @@ String getTargetImageCredentialHelperName() { if (property != null) { return property; } - return Preconditions.checkNotNull(to).credHelper; + return to.credHelper; } AuthConfiguration getTargetImageAuth() { @@ -543,6 +543,19 @@ String getFormat() { return container.format; } + /** + * Gets the configured files modification time value. + * + * @return the configured files modification time value + */ + String getFilesModificationTime() { + String property = getProperty(PropertyNames.CONTAINER_FILES_MODIFICATION_TIME); + if (property != null) { + return property; + } + return container.filesModificationTime; + } + /** * Gets the list of configured extra directory paths. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index d643039da0..c5f1019856 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -16,13 +16,11 @@ package com.google.cloud.tools.jib.maven; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -184,12 +182,9 @@ static int getVersionFromString(String versionString) { } @Override - public JibContainerBuilder createContainerBuilder( - RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) + public JibContainerBuilder createJibContainerBuilder( + JavaContainerBuilder javaContainerBuilder, ContainerizingMode containerizingMode) throws IOException { - JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.from(baseImage).setAppRoot(appRoot); - try { if (isWarProject()) { Path explodedWarPath = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index ecb17b52c7..f7c69cc162 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -155,6 +155,11 @@ public Optional getProperty(String propertyName) { return Optional.ofNullable(jibPluginConfiguration.getProperty(propertyName)); } + @Override + public String getFilesModificationTime() { + return jibPluginConfiguration.getFilesModificationTime(); + } + @Override public List getExtraDirectories() { return MojoCommon.getExtraDirectories(jibPluginConfiguration); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 782f9ac3dc..06a3515bd0 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -103,7 +103,8 @@ public void testExecute_simple() Instant before = Instant.now(); Assert.assertEquals( - "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", buildToDockerDaemonAndRun(simpleTestProject.getProjectRoot(), targetImage)); Instant buildTime = Instant.parse( @@ -191,7 +192,8 @@ public void testExecute_noToImageAndInvalidProjectName() buildToDockerDaemon( simpleTestProject.getProjectRoot(), "image reference ignored", "pom-no-to-image.xml"); Assert.assertEquals( - "Hello, world. \n", new Command("docker", "run", "--rm", "my-artifact-id:1").run()); + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", "my-artifact-id:1").run()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 603d2dfe02..e2bf745290 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -365,7 +365,10 @@ public void testExecute_simple() before.toString().getBytes(StandardCharsets.UTF_8)); Assert.assertEquals( - "Hello, " + before + ". An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", + "Hello, " + + before + + ". An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom.xml", true)); Instant buildTime = @@ -403,7 +406,7 @@ public void testExecute_simpleOnJava11() String targetImage = getTestImageReference("simpleimage:maven"); Assert.assertEquals( - "Hello, world. An argument.\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-java11.xml", false)); } @@ -453,7 +456,8 @@ public void testExecute_multipleExtraDirectories() throws DigestException, VerificationException, IOException, InterruptedException { String targetImage = getTestImageReference("simpleimage:maven"); Assert.assertEquals( - "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\nbaz\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\nbaz\n1970-01-01T00:00:01Z\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom-extra-dirs.xml", false)); assertLayerSize(9, targetImage); // one more than usual } @@ -505,21 +509,37 @@ public void testExecute_complex() throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex( targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml")); assertWorkingDirectory("", targetImage); assertEntrypoint( - "[java -Xms512m -Xdebug -cp /other:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]", + "[java -Xms512m -Xdebug -cp /other:/app/resources:/app/classes:/app/libs/* " + + "com.test.HelloWorld]", targetImage); } + @Test + public void testExecute_filesModificationTimeCustom() + throws IOException, InterruptedException, VerificationException, DigestException { + String targetImage = "localhost:6000/simpleimage:maven" + System.nanoTime(); + String pom = "pom-complex-files-modification-time-custom.xml"; + Assert.assertEquals( + "Hello, world. \n2019-06-17T16:30:00Z\nrw-r--r--\nrw-r--r--\n" + + "foo\ncat\n2019-06-17T16:30:00Z\n2019-06-17T16:30:00Z\n", + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2, pom)); + } + @Test public void testExecute_complex_sameFromAndToRegistry() throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex( targetImage, "testuser", "testpassword", localRegistry1, "pom-complex.xml")); assertWorkingDirectory("", targetImage); @@ -530,7 +550,9 @@ public void testExecute_complexProperties() throws InterruptedException, DigestException, VerificationException, IOException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( - "Hello, world. An argument.\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex( targetImage, "testuser2", diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 12e5616a5e..19f231e98f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -72,7 +72,7 @@ public void testExecute_simple() .toString()) .run(); Assert.assertEquals( - "Hello, world. An argument.\nrw-r--r--\nrw-r--r--\nfoo\ncat\n", + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", new Command("docker", "run", "--rm", targetImage).run()); Instant buildTime = diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 4ac459b4be..1457b52e85 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -69,6 +69,7 @@ public void testDefaults() { Assert.assertNull(testPluginConfiguration.getWorkingDirectory()); Assert.assertTrue(testPluginConfiguration.getExtraClasspath().isEmpty()); Assert.assertEquals("exploded", testPluginConfiguration.getContainerizingMode()); + Assert.assertEquals("EPOCH_PLUS_SECOND", testPluginConfiguration.getFilesModificationTime()); } @Test @@ -118,8 +119,10 @@ public void testSystemProperties() { Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); sessionProperties.put("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); - sessionProperties.put("jib.container.workingDirectory", "working directory"); - Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); + sessionProperties.put("jib.container.workingDirectory", "/working/directory"); + Assert.assertEquals("/working/directory", testPluginConfiguration.getWorkingDirectory()); + sessionProperties.put("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); + Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getFilesModificationTime()); sessionProperties.put("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); @@ -185,8 +188,12 @@ public void testPomProperties() { Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); project.getProperties().setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); - project.getProperties().setProperty("jib.container.workingDirectory", "working directory"); - Assert.assertEquals("working directory", testPluginConfiguration.getWorkingDirectory()); + project.getProperties().setProperty("jib.container.workingDirectory", "/working/directory"); + Assert.assertEquals("/working/directory", testPluginConfiguration.getWorkingDirectory()); + project + .getProperties() + .setProperty("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); + Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getFilesModificationTime()); project.getProperties().setProperty("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); @@ -210,8 +217,7 @@ public void testPomProperties() { @Test public void testEmptyOrNullTags() { // https://github.com/GoogleContainerTools/jib/issues/1534 - // Maven turns empty tags into null entries, and its possible - // to have empty tags in jib.to.tags + // Maven turns empty tags into null entries, and its possible to have empty tags in jib.to.tags sessionProperties.put("jib.to.tags", "a,,b"); try { testPluginConfiguration.getTargetImageAdditionalTags(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 7efabee3fc..e1acf1f5ae 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; @@ -38,6 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -70,6 +72,7 @@ public class MavenProjectPropertiesTest { private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; + private static final Instant SAMPLE_FILE_MODIFICATION_TIME = Instant.ofEpochSecond(32); /** Helper for reading back layers in a {@code BuildConfiguration}. */ private static class ContainerBuilderLayers { @@ -119,6 +122,15 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } + private static void assertModificationTime(Instant instant, List layers) { + for (LayerConfiguration layer : layers) { + for (LayerEntry entry : layer.getLayerEntries()) { + String message = "wrong time: " + entry.getSourceFile() + "-->" + entry.getExtractionPath(); + Assert.assertEquals(message, instant, entry.getModificationTime()); + } + } + } + private static void assertNonDefaultAppRoot(BuildConfiguration configuration) { ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertExtractionPathsUnordered( @@ -403,6 +415,11 @@ public void testCreateContainerBuilder_correctFiles() applicationDirectory.resolve("output/package/some.class"), applicationDirectory.resolve("output/some.class")), layers.classesLayers.get(0).getLayerEntries()); + + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.dependenciesLayers); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.snapshotsLayers); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.resourcesLayers); + assertModificationTime(SAMPLE_FILE_MODIFICATION_TIME, layers.classesLayers); } @Test @@ -551,7 +568,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -562,7 +579,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -573,7 +590,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -610,19 +627,6 @@ public void testIsWarProject_GwtLibPackagingIsNotWar() { Assert.assertFalse(mavenProjectProperties.isWarProject()); } - private BuildConfiguration setupBuildConfiguration( - String appRoot, ContainerizingMode containerizingMode) - throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - JibContainerBuilder JibContainerBuilder = - new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) - .createContainerBuilder( - RegistryImage.named("base"), AbsoluteUnixPath.get(appRoot), containerizingMode); - return JibContainerBuilderTestHelper.toBuildConfiguration( - JibContainerBuilder, - Containerizer.to(RegistryImage.named("to")) - .setExecutorService(MoreExecutors.newDirectExecutorService())); - } - @Test public void testClassifyDependencies() { Set artifacts = @@ -665,6 +669,22 @@ public void testClassifyDependencies() { newArtifact("com.test", "projectC", "3.0").getFile().toPath())); } + private BuildConfiguration setupBuildConfiguration( + String appRoot, ContainerizingMode containerizingMode) + throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(RegistryImage.named("base")) + .setAppRoot(AbsoluteUnixPath.get(appRoot)) + .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); + JibContainerBuilder JibContainerBuilder = + new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) + .createJibContainerBuilder(javaContainerBuilder, containerizingMode); + return JibContainerBuilderTestHelper.toBuildConfiguration( + JibContainerBuilder, + Containerizer.to(RegistryImage.named("to")) + .setExecutorService(MoreExecutors.newDirectExecutorService())); + } + private Artifact newArtifact(String group, String artifactId, String version) { Artifact artifact = new DefaultArtifact(group, artifactId, version, null, "jar", "", null); artifact.setFile(new File("/tmp/" + group + artifactId + version)); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index 0ec69cc561..a796fcc415 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -75,6 +75,8 @@ public void testGetters() { .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); Mockito.when(jibPluginConfiguration.getUseCurrentTimestamp()).thenReturn(true); Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); + Mockito.when(jibPluginConfiguration.getFilesModificationTime()) + .thenReturn("2011-12-03T22:42:05Z"); MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); @@ -102,6 +104,7 @@ public void testGetters() { new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); + Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); Mockito.verifyNoMoreInteractions(eventHandlers); } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml new file mode 100644 index 0000000000..02acd783bd --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + localhost:5000/distroless/java + + testuser + testpassword + + + + ${_TARGET_IMAGE} + + ${_TARGET_USERNAME} + ${_TARGET_PASSWORD} + + + + true + + 1000/tcp + 2000-2003/udp + + + value1 + value2 + + 2019-06-17T16:30:00Z + + + src/main/jib-custom + + true + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java index bc41a9f3f2..cb492a0904 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/src/main/java/com/test/HelloWorld.java @@ -24,6 +24,7 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; @@ -41,6 +42,10 @@ public static void main(String[] args) throws IOException, URISyntaxException { HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8))) { String world = reader.readLine(); System.out.println(greeting + ", " + world + ". " + (args.length > 0 ? args[0] : "")); + Path worldFilePath = Paths.get("/app/resources/world"); + if (worldFilePath.toFile().exists()) { + System.out.println(Files.getLastModifiedTime(worldFilePath).toString()); + } // Prints the contents of the extra files. if (Files.exists(Paths.get("/foo"))) { @@ -52,11 +57,14 @@ public static void main(String[] args) throws IOException, URISyntaxException { new String(Files.readAllBytes(Paths.get("/foo")), StandardCharsets.UTF_8)); System.out.println( new String(Files.readAllBytes(Paths.get("/bar/cat")), StandardCharsets.UTF_8)); + System.out.println(Files.getLastModifiedTime(Paths.get("/foo")).toString()); + System.out.println(Files.getLastModifiedTime(Paths.get("/bar/cat")).toString()); } // Prints the contents of the files in the second extra directory. if (Files.exists(Paths.get("/baz"))) { System.out.println( new String(Files.readAllBytes(Paths.get("/baz")), StandardCharsets.UTF_8)); + System.out.println(Files.getLastModifiedTime(Paths.get("/baz")).toString()); } // Prints jvm flags diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java new file mode 100644 index 0000000000..421f328eb0 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.time.format.DateTimeParseException; + +public class InvalidFilesModificationTimeException extends Exception { + + private final String invalidValue; + + public InvalidFilesModificationTimeException( + String message, String invalidValue, DateTimeParseException ex) { + super(message, ex); + this.invalidValue = invalidValue; + } + + public String getInvalidFilesModificationTime() { + return invalidValue; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java index 38958cfc33..fa2bc1f267 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelper.java @@ -27,7 +27,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Predicate; /** Helper for constructing {@link JavaContainerBuilder}-based {@link JibContainerBuilder}s. */ @@ -38,11 +40,14 @@ public class JavaContainerBuilderHelper { * * @param extraDirectory the source extra directory path * @param extraDirectoryPermissions map from path on container to file permissions + * @param modificationTimeProvider file modification time provider * @return a {@link LayerConfiguration} for adding the extra directory to the container * @throws IOException if walking the extra directory fails */ public static LayerConfiguration extraDirectoryLayerConfiguration( - Path extraDirectory, Map extraDirectoryPermissions) + Path extraDirectory, + Map extraDirectoryPermissions, + BiFunction modificationTimeProvider) throws IOException { LayerConfiguration.Builder builder = LayerConfiguration.builder().setName(LayerType.EXTRA_FILES.getName()); @@ -52,11 +57,12 @@ public static LayerConfiguration extraDirectoryLayerConfiguration( localPath -> { AbsoluteUnixPath pathOnContainer = AbsoluteUnixPath.get("/").resolve(extraDirectory.relativize(localPath)); + Instant modificationTime = modificationTimeProvider.apply(localPath, pathOnContainer); FilePermissions permissions = extraDirectoryPermissions.get(pathOnContainer); if (permissions == null) { - builder.addEntry(localPath, pathOnContainer); + builder.addEntry(localPath, pathOnContainer, modificationTime); } else { - builder.addEntry(localPath, pathOnContainer, permissions); + builder.addEntry(localPath, pathOnContainer, permissions, modificationTime); } }); return builder.build(); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 568799202f..17e7889a93 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -39,6 +39,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -46,6 +48,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import javax.annotation.Nullable; /** @@ -64,7 +67,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -94,7 +97,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -116,7 +119,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -157,7 +160,7 @@ static PluginConfigurationProcessor processCommonConfiguration( throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); @@ -184,11 +187,16 @@ static PluginConfigurationProcessor processCommonConfiguration( inferredAuthProvider, rawConfiguration.getFromCredHelper().orElse(null)); + BiFunction modificationTimeProvider = + createModificationTimeProvider(rawConfiguration.getFilesModificationTime()); + JavaContainerBuilder javaContainerBuilder = + JavaContainerBuilder.from(baseImage) + .setAppRoot(getAppRootChecked(rawConfiguration, projectProperties)) + .setModificationTimeProvider(modificationTimeProvider); JibContainerBuilder jibContainerBuilder = projectProperties - .createContainerBuilder( - baseImage, - getAppRootChecked(rawConfiguration, projectProperties), + .createJibContainerBuilder( + javaContainerBuilder, getContainerizingModeChecked(rawConfiguration, projectProperties)) .setEntrypoint(computeEntrypoint(rawConfiguration, projectProperties)) .setProgramArguments(rawConfiguration.getProgramArguments().orElse(null)) @@ -211,7 +219,9 @@ static PluginConfigurationProcessor processCommonConfiguration( if (Files.exists(directory)) { jibContainerBuilder.addLayer( JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( - directory, rawConfiguration.getExtraDirectoryPermissions())); + directory, + rawConfiguration.getExtraDirectoryPermissions(), + modificationTimeProvider)); } } @@ -431,6 +441,39 @@ static Optional getWorkingDirectoryChecked(RawConfiguration ra } } + /** + * Creates a modification time provider based on the config value. The value can be: + * + *
    + *
  1. {@code EPOCH_PLUS_SECOND} to create a provider which trims file modification time to + * EPOCH + 1 second + *
  2. date in ISO 8601 format + *
+ * + * @param modificationTime modification time config value + * @return corresponding modification time provider + * @throws InvalidFilesModificationTimeException if the config value is not in ISO 8601 format + */ + @VisibleForTesting + static BiFunction createModificationTimeProvider( + String modificationTime) throws InvalidFilesModificationTimeException { + try { + switch (modificationTime) { + case "EPOCH_PLUS_SECOND": + Instant epochPlusSecond = Instant.ofEpochSecond(1); + return (ignored1, ignored2) -> epochPlusSecond; + + default: + Instant timestamp = + DateTimeFormatter.ISO_DATE_TIME.parse(modificationTime, Instant::from); + return (ignored1, ignored2) -> timestamp; + } + + } catch (DateTimeParseException ex) { + throw new InvalidFilesModificationTimeException(modificationTime, modificationTime, ex); + } + } + // TODO: find a way to reduce the number of arguments. private static boolean configureCredentialRetrievers( RawConfiguration rawConfiguration, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 3b716a59be..46aaad508d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -16,11 +16,10 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.api.RegistryImage; import java.io.IOException; import java.nio.file.Path; import java.util.List; @@ -58,14 +57,13 @@ public interface ProjectProperties { /** * Starts the containerization process. * - * @param baseImage the base image - * @param appRoot root directory in the image where the app will be placed + * @param javaContainerBuilder Java container builder to start with * @param containerizingMode mode to containerize the app * @return a {@link JibContainerBuilder} with classes, resources, and dependencies added to it * @throws IOException if there is a problem walking the project files */ - JibContainerBuilder createContainerBuilder( - RegistryImage baseImage, AbsoluteUnixPath appRoot, ContainerizingMode containerizingMode) + JibContainerBuilder createJibContainerBuilder( + JavaContainerBuilder javaContainerBuilder, ContainerizingMode containerizingMode) throws IOException; List getClassFiles() throws IOException; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 33ebab924a..9fe63a276d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -43,6 +43,8 @@ public class PropertyNames { public static final String CONTAINER_VOLUMES = "jib.container.volumes"; public static final String CONTAINER_PORTS = "jib.container.ports"; public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; + public static final String CONTAINER_FILES_MODIFICATION_TIME = + "jib.container.filesModificationTime"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; public static final String BASE_IMAGE_CACHE = "jib.baseImageCache"; public static final String APPLICATION_CACHE = "jib.applicationCache"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 93cbf06e52..c2fa1ea6c3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -76,6 +76,8 @@ public interface RawConfiguration { Optional getProperty(String propertyName); + String getFilesModificationTime(); + List getExtraDirectories(); Map getExtraDirectoryPermissions(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 516a14bc3b..5cf1c06d49 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -37,6 +37,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -84,7 +85,7 @@ public void testExtraDirectoryLayerConfiguration() throws URISyntaxException, IO Path extraFilesDirectory = Paths.get(Resources.getResource("core/layer").toURI()); LayerConfiguration layerConfiguration = JavaContainerBuilderHelper.extraDirectoryLayerConfiguration( - extraFilesDirectory, Collections.emptyMap()); + extraFilesDirectory, Collections.emptyMap(), (ignored1, ignored2) -> Instant.EPOCH); assertSourcePathsUnordered( Arrays.asList( extraFilesDirectory.resolve("a"), diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 85e087e2eb..9870c2d886 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilderTestHelper; @@ -36,10 +37,13 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -92,6 +96,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(rawConfiguration.getFromAuth()).thenReturn(authProperty); Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); + Mockito.when(rawConfiguration.getFilesModificationTime()).thenReturn("EPOCH_PLUS_SECOND"); Mockito.when(rawConfiguration.getExtraDirectories()) .thenReturn(Arrays.asList(Paths.get("nonexistent/path"))); Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("exploded"); @@ -100,10 +105,8 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(projectProperties.getMainClassFromJar()).thenReturn("java.lang.Object"); Mockito.when(projectProperties.getDefaultCacheDirectory()).thenReturn(Paths.get("cache")); Mockito.when( - projectProperties.createContainerBuilder( - Mockito.any(RegistryImage.class), - Mockito.any(AbsoluteUnixPath.class), - Mockito.any(ContainerizingMode.class))) + projectProperties.createJibContainerBuilder( + Mockito.any(JavaContainerBuilder.class), Mockito.any(ContainerizingMode.class))) .thenReturn(Jib.from("base")); Mockito.when(projectProperties.isOffline()).thenReturn(false); @@ -122,7 +125,8 @@ public void testPluginConfigurationProcessor_defaults() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -146,7 +150,7 @@ public void testPluginConfigurationProcessor_extraDirectory() InvalidAppRootException, IOException, IncompatibleBaseImageJavaVersionException, InvalidWorkingDirectoryException, InvalidImageReferenceException, CacheDirectoryCreationException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Mockito.when(rawConfiguration.getExtraDirectories()).thenReturn(Arrays.asList(extraDirectory)); Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) @@ -192,7 +196,7 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() throws InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidImageReferenceException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); @@ -210,7 +214,7 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -226,7 +230,8 @@ public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -287,7 +292,8 @@ public void testEntrypoint_defaultWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -305,7 +311,8 @@ public void testEntrypoint_defaultNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -327,7 +334,8 @@ public void testEntrypoint_extraClasspathNonWarPackaging() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getExtraClasspath()) .thenReturn(Collections.singletonList("/foo")); @@ -352,7 +360,8 @@ public void testUser() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -368,7 +377,8 @@ public void testUser_null() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -382,7 +392,8 @@ public void testEntrypoint_warningOnJvmFlags() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -406,7 +417,8 @@ public void testEntrypoint_warningOnMainclass() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -430,7 +442,8 @@ public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -453,7 +466,8 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, - NumberFormatException, InvalidContainerizingModeException { + NumberFormatException, InvalidContainerizingModeException, + InvalidFilesModificationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -837,11 +851,44 @@ public void testGetInvalidVolumesList() { } } + @Test + public void createModificationTimeProvider_epochPlusSecond() + throws InvalidFilesModificationTimeException { + BiFunction timeProvider = + PluginConfigurationProcessor.createModificationTimeProvider("EPOCH_PLUS_SECOND"); + Assert.assertEquals( + Instant.ofEpochSecond(1), + timeProvider.apply(Paths.get("foo"), AbsoluteUnixPath.get("/bar"))); + } + + @Test + public void createModificationTimeProvider_isoDateTimeValue() + throws InvalidFilesModificationTimeException { + BiFunction timeProvider = + PluginConfigurationProcessor.createModificationTimeProvider("2011-12-03T10:15:30+09:00"); + Instant expected = DateTimeFormatter.ISO_DATE_TIME.parse("2011-12-03T01:15:30Z", Instant::from); + Assert.assertEquals( + expected, timeProvider.apply(Paths.get("foo"), AbsoluteUnixPath.get("/bar"))); + } + + @Test + public void createModificationTimeProvider_invalidValue() { + try { + BiFunction timeProvider = + PluginConfigurationProcessor.createModificationTimeProvider("invalid format"); + timeProvider.apply(Paths.get("foo"), AbsoluteUnixPath.get("/bar")); + Assert.fail(); + } catch (InvalidFilesModificationTimeException ex) { + Assert.assertEquals("invalid format", ex.getMessage()); + Assert.assertEquals("invalid format", ex.getInvalidFilesModificationTime()); + } + } + private PluginConfigurationProcessor createPluginConfigurationProcessor() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException { return PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, ignored -> Optional.empty(), From 3192e9fc5d6ee35f64aa8625ab270ea0a44e4eac Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 30 Jul 2019 13:15:25 -0400 Subject: [PATCH 0669/2020] Stay on Ubuntu Trusty on Travis for oraclejdk8 (#1874) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 21507dd4a1..c5ac8dab8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,10 @@ git: language: java matrix: include: - - name: Oracle JDK 8 + - name: Oracle JDK 8 on Trusty # using `openjdk8` leads to javadoc errors jdk: oraclejdk8 + dist: trusty # oraclejdk8 absent in Xenial - name: OpenJDK 11 jdk: openjdk11 env: From 3d9d016ea431abf63c4617c48a776e39045f2bec Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 30 Jul 2019 15:55:12 -0400 Subject: [PATCH 0670/2020] Add save to DockerClient (#1873) --- .../cloud/tools/jib/docker/DockerClient.java | 24 +++++++++++++ .../tools/jib/docker/DockerClientTest.java | 35 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 274ba7bcdd..ddd0b9d238 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -158,6 +158,30 @@ public String load(ImageTarball imageTarball) throws InterruptedException, IOExc } } + /** + * Saves an image tarball from the Docker daemon. + * + * @see https://docs.docker.com/engine/reference/commandline/save + * @param imageReference the image to save + * @param outputPath the destination path to save the output tarball + * @throws InterruptedException if the 'docker save' process is interrupted + * @throws IOException if creating the tarball fails + */ + public void save(ImageReference imageReference, Path outputPath) + throws InterruptedException, IOException { + // Runs 'docker save'. + Process dockerProcess = docker("save", imageReference.toString(), "-o", outputPath.toString()); + + if (dockerProcess.waitFor() != 0) { + try (InputStreamReader stderr = + new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { + throw new IOException( + "'docker save' command failed with output: " + CharStreams.toString(stderr)); + } + } + } + /** * Tags the image referenced by {@code originalImageReference} with a new image reference {@code * newImageReference}. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 54da3c412f..2df82e1502 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -173,6 +173,41 @@ public void testLoad_stdoutFail() throws InterruptedException { } } + @Test + public void testSave() throws InterruptedException, IOException { + DockerClient testDockerClient = + new DockerClient( + subcommand -> { + Assert.assertEquals(Arrays.asList("save", "testimage", "-o", "out.tar"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + + testDockerClient.save(ImageReference.of(null, "testimage", null), Paths.get("out.tar")); + } + + @Test + public void testSave_fail() throws InterruptedException { + DockerClient testDockerClient = + new DockerClient( + subcommand -> { + Assert.assertEquals(Arrays.asList("save", "testimage", "-o", "out.tar"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.save(ImageReference.of(null, "testimage", null), Paths.get("out.tar")); + Assert.fail("docker save should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker save' command failed with output: error", ex.getMessage()); + } + } + @Test public void testTag() throws InterruptedException, IOException, InvalidImageReferenceException { DockerClient testDockerClient = From dbb6cd4d4f6c52afbacffe792130662bff5ae458 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 31 Jul 2019 11:52:00 -0400 Subject: [PATCH 0671/2020] Fix deprecated @Component annotation warning (#1878) --- .../cloud/tools/jib/maven/JibPluginConfiguration.java | 4 +++- .../tools/jib/maven/skaffold/SkaffoldBindingMojo.java | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index a02d311cef..5c5ea0ddb1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -231,7 +231,9 @@ public List getPaths() { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; - @Component protected PluginDescriptor descriptor; + @Nullable + @Parameter(defaultValue = "${plugin}", readonly = true) + protected PluginDescriptor descriptor; @Parameter private FromConfiguration from = new FromConfiguration(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java index 59281df829..aa90a7fba6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java @@ -18,14 +18,18 @@ import com.google.cloud.tools.jib.maven.MojoCommon; import com.google.common.base.Preconditions; +import javax.annotation.Nullable; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.descriptor.PluginDescriptor; -import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; /** Base class for Skaffold-related goals. */ abstract class SkaffoldBindingMojo extends AbstractMojo { - @Component protected PluginDescriptor descriptor; + + @Nullable + @Parameter(defaultValue = "${plugin}", readonly = true) + protected PluginDescriptor descriptor; protected void checkJibVersion() throws MojoExecutionException { Preconditions.checkNotNull(descriptor); From 795a442c488a7a87f48ac02eb76b9c49e4db6a07 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 31 Jul 2019 16:47:41 -0400 Subject: [PATCH 0672/2020] Skip downloading base image layers if they exists in target repository (#1840) --- .../BuildAndCacheApplicationLayerStep.java | 8 +- .../jib/builder/steps/BuildImageStep.java | 19 +- .../jib/builder/steps/CachedLayerAndName.java | 41 ---- ...tep.java => ObtainBaseImageLayerStep.java} | 81 ++++++-- .../jib/builder/steps/PreparedLayer.java | 90 +++++++++ .../tools/jib/builder/steps/PushBlobStep.java | 32 ++-- .../steps/PushContainerConfigurationStep.java | 3 +- .../jib/builder/steps/PushLayerStep.java | 42 +++-- .../tools/jib/builder/steps/StepsRunner.java | 37 ++-- .../tools/jib/global/JibSystemProperties.java | 12 ++ ...BuildAndCacheApplicationLayerStepTest.java | 2 +- .../jib/builder/steps/BuildImageStepTest.java | 18 +- .../steps/ObtainBaseImageLayerStepTest.java | 176 ++++++++++++++++++ .../jib/builder/steps/PushBlobStepTest.java | 97 ++++++++++ .../jib/global/JibSystemPropertiesTest.java | 27 ++- .../gradle/SingleProjectIntegrationTest.java | 41 ++-- .../maven/BuildImageMojoIntegrationTest.java | 19 +- 17 files changed, 596 insertions(+), 149 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java rename jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/{PullAndCacheBaseImageLayerStep.java => ObtainBaseImageLayerStep.java} (60%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 96a3d3cafd..10dfc33153 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -34,7 +34,7 @@ import java.util.concurrent.Callable; /** Builds and caches application layers. */ -class BuildAndCacheApplicationLayerStep implements Callable { +class BuildAndCacheApplicationLayerStep implements Callable { private static final String DESCRIPTION = "Building %s layer"; @@ -86,7 +86,7 @@ private BuildAndCacheApplicationLayerStep( } @Override - public CachedLayerAndName call() throws IOException, CacheCorruptedException { + public PreparedLayer call() throws IOException, CacheCorruptedException { String description = String.format(DESCRIPTION, layerName); EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); @@ -101,7 +101,7 @@ public CachedLayerAndName call() throws IOException, CacheCorruptedException { Optional optionalCachedLayer = cache.retrieve(layerConfiguration.getLayerEntries()); if (optionalCachedLayer.isPresent()) { - return new CachedLayerAndName(optionalCachedLayer.get(), layerName); + return new PreparedLayer.Builder(optionalCachedLayer.get()).setName(layerName).build(); } Blob layerBlob = new ReproducibleLayerBuilder(layerConfiguration.getLayerEntries()).build(); @@ -110,7 +110,7 @@ public CachedLayerAndName call() throws IOException, CacheCorruptedException { eventHandlers.dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); - return new CachedLayerAndName(cachedLayer, layerName); + return new PreparedLayer.Builder(cachedLayer).setName(layerName).build(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index bb3783eb75..e221317557 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; import com.google.cloud.tools.jib.image.json.HistoryEntry; -import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import java.time.Instant; import java.util.List; @@ -40,15 +39,15 @@ class BuildImageStep implements Callable { private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Image baseImage; - private final List baseImageLayers; - private final List applicationLayers; + private final List baseImageLayers; + private final List applicationLayers; BuildImageStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Image baseImage, - List baseImageLayers, - List applicationLayers) { + List baseImageLayers, + List applicationLayers) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.baseImage = baseImage; @@ -68,9 +67,7 @@ public Image call() throws LayerPropertyNotFoundException { buildConfiguration.getContainerConfiguration(); // Base image layers - for (CachedLayerAndName baseImageLayer : baseImageLayers) { - imageBuilder.addLayer(baseImageLayer.getCachedLayer()); - } + baseImageLayers.stream().forEach(imageBuilder::addLayer); // Passthrough config and count non-empty history entries int nonEmptyLayerCount = 0; @@ -104,15 +101,15 @@ public Image call() throws LayerPropertyNotFoundException { } // Add built layers/configuration - for (CachedLayerAndName applicationLayer : applicationLayers) { + for (PreparedLayer applicationLayer : applicationLayers) { imageBuilder - .addLayer(applicationLayer.getCachedLayer()) + .addLayer(applicationLayer) .addHistory( HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) - .setComment(Verify.verifyNotNull(applicationLayer.getName())) + .setComment(applicationLayer.getName()) .build()); } if (containerConfiguration != null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java deleted file mode 100644 index eed59fc14e..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CachedLayerAndName.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.cache.CachedLayer; -import javax.annotation.Nullable; - -/** Simple structure to hold a pair of {#link CachedLayer} and its string name. */ -class CachedLayerAndName { - - private CachedLayer cachedLayer; - @Nullable private String name; - - CachedLayerAndName(CachedLayer cachedLayer, @Nullable String name) { - this.cachedLayer = cachedLayer; - this.name = name; - } - - CachedLayer getCachedLayer() { - return cachedLayer; - } - - @Nullable - String getName() { - return name; - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java similarity index 60% rename from jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 0868aef0ac..735be6a676 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullAndCacheBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; @@ -27,6 +28,7 @@ import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.ArrayList; @@ -36,14 +38,50 @@ import javax.annotation.Nullable; /** Pulls and caches a single base image layer. */ -class PullAndCacheBaseImageLayerStep implements Callable { +class ObtainBaseImageLayerStep implements Callable { private static final String DESCRIPTION = "Pulling base image layer %s"; - static ImmutableList makeList( + @FunctionalInterface + private interface BlobExistenceChecker { + + Optional exists(DescriptorDigest digest) throws IOException, RegistryException; + } + + static ImmutableList makeListForForcedDownload( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, ImageAndAuthorization baseImageAndAuth) { + BlobExistenceChecker noOpBlobChecker = ignored -> Optional.empty(); + return makeList( + buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, noOpBlobChecker); + } + + static ImmutableList makeListForSelectiveDownload( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + ImageAndAuthorization baseImageAndAuth, + Authorization pushAuthorization) { + Verify.verify(!buildConfiguration.isOffline()); + + RegistryClient targetRegistryClient = + buildConfiguration + .newTargetImageRegistryClientFactory() + .setAuthorization(pushAuthorization) + .newRegistryClient(); + // TODO: also check if cross-repo blob mount is possible. + BlobExistenceChecker blobExistenceChecker = + digest -> Optional.of(targetRegistryClient.checkBlob(digest).isPresent()); + + return makeList( + buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, blobExistenceChecker); + } + + private static ImmutableList makeList( + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + ImageAndAuthorization baseImageAndAuth, + BlobExistenceChecker blobExistenceChecker) { ImmutableList baseImageLayers = baseImageAndAuth.getImage().getLayers(); try (ProgressEventDispatcher progressEventDispatcher = @@ -53,14 +91,15 @@ static ImmutableList makeList( new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing base image layer pullers")) { - List layerPullers = new ArrayList<>(); + List layerPullers = new ArrayList<>(); for (Layer layer : baseImageLayers) { layerPullers.add( - new PullAndCacheBaseImageLayerStep( + new ObtainBaseImageLayerStep( buildConfiguration, progressEventDispatcher.newChildProducer(), - layer.getBlobDescriptor().getDigest(), - baseImageAndAuth.getAuthorization())); + layer, + baseImageAndAuth.getAuthorization(), + blobExistenceChecker)); } return ImmutableList.copyOf(layerPullers); } @@ -69,38 +108,50 @@ static ImmutableList makeList( private final BuildConfiguration buildConfiguration; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final DescriptorDigest layerDigest; + private final Layer layer; private final @Nullable Authorization pullAuthorization; + private final BlobExistenceChecker blobExistenceChecker; - PullAndCacheBaseImageLayerStep( + ObtainBaseImageLayerStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - DescriptorDigest layerDigest, - @Nullable Authorization pullAuthorization) { + Layer layer, + @Nullable Authorization pullAuthorization, + BlobExistenceChecker blobExistenceChecker) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.layerDigest = layerDigest; + this.layer = layer; this.pullAuthorization = pullAuthorization; + this.blobExistenceChecker = blobExistenceChecker; } @Override - public CachedLayerAndName call() throws IOException, CacheCorruptedException { + public PreparedLayer call() throws IOException, CacheCorruptedException, RegistryException { + DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, layerDigest))) { + + Optional layerExists = blobExistenceChecker.exists(layerDigest); + if (layerExists.orElse(false)) { + return new PreparedLayer.Builder(layer).setStateInTarget(layerExists).build(); + } + Cache cache = buildConfiguration.getBaseImageLayersCache(); // Checks if the layer already exists in the cache. Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { - return new CachedLayerAndName(optionalCachedLayer.get(), null); + CachedLayer cachedLayer = optionalCachedLayer.get(); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(layerExists).build(); } else if (buildConfiguration.isOffline()) { throw new IOException( "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + layerDigest - + ". You may need to rerun Jib in online mode to re-download the base image layers."); + + ". Rerun Jib in online mode with \"-Djib.alwaysCacheBaseImage=true\" to " + + "re-download the base image layers."); } RegistryClient registryClient = @@ -119,7 +170,7 @@ public CachedLayerAndName call() throws IOException, CacheCorruptedException { layerDigest, progressEventDispatcherWrapper::setProgressTarget, progressEventDispatcherWrapper::dispatchProgress)); - return new CachedLayerAndName(cachedLayer, null); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(layerExists).build(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java new file mode 100644 index 0000000000..dd02e8a2cd --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.image.Layer; +import java.util.Optional; + +/** + * Layer prepared from {@link BuildAndCacheApplicationLayerStep} and {@link + * ObtainBaseImageLayerStep} to hold information about either a base image layer or an application + * layer. + */ +class PreparedLayer implements Layer { + + static class Builder { + + private Layer layer; + private String name = "unnamed layer"; + private Optional stateInTarget = Optional.empty(); + + Builder(Layer layer) { + this.layer = layer; + } + + Builder setName(String name) { + this.name = name; + return this; + } + + /** Sets whether the layer exists in a target destination. Empty (absence) means unknown. */ + Builder setStateInTarget(Optional stateInTarget) { + this.stateInTarget = stateInTarget; + return this; + } + + PreparedLayer build() { + return new PreparedLayer(layer, name, stateInTarget); + } + } + + private final Layer layer; + private final String name; + private final Optional stateInTarget; + + private PreparedLayer(Layer layer, String name, Optional stateInTarget) { + this.layer = layer; + this.name = name; + this.stateInTarget = stateInTarget; + } + + String getName() { + return name; + } + + Optional existsInTarget() { + return stateInTarget; + } + + @Override + public Blob getBlob() { + return layer.getBlob(); + } + + @Override + public BlobDescriptor getBlobDescriptor() { + return layer.getBlobDescriptor(); + } + + @Override + public DescriptorDigest getDiffId() { + return layer.getDiffId(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index c3711d8ef0..12bba2e04e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; @@ -23,6 +24,7 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -36,33 +38,37 @@ class PushBlobStep implements Callable { private static final String DESCRIPTION = "Pushing BLOB "; private final BuildConfiguration buildConfiguration; - private final ProgressEventDispatcher.Factory progressEventDipatcherFactory; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Authorization authorization; private final BlobDescriptor blobDescriptor; private final Blob blob; + private final boolean forcePush; PushBlobStep( BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDipatcherFactory, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization authorization, BlobDescriptor blobDescriptor, - Blob blob) { + Blob blob, + boolean forcePush) { this.buildConfiguration = buildConfiguration; - this.progressEventDipatcherFactory = progressEventDipatcherFactory; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authorization = authorization; this.blobDescriptor = blobDescriptor; this.blob = blob; + this.forcePush = forcePush; } @Override public BlobDescriptor call() throws IOException, RegistryException { + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + DescriptorDigest blobDigest = blobDescriptor.getDigest(); try (ProgressEventDispatcher progressEventDispatcher = - progressEventDipatcherFactory.create( - "pushing blob " + blobDescriptor.getDigest(), blobDescriptor.getSize()); + progressEventDispatcherFactory.create( + "pushing blob " + blobDigest, blobDescriptor.getSize()); TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), DESCRIPTION + blobDescriptor); + new TimerEventDispatcher(eventHandlers, DESCRIPTION + blobDescriptor); ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { RegistryClient registryClient = @@ -72,10 +78,9 @@ public BlobDescriptor call() throws IOException, RegistryException { .newRegistryClient(); // check if the BLOB is available - if (registryClient.checkBlob(blobDescriptor.getDigest()).isPresent()) { - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); + if (!forcePush && registryClient.checkBlob(blobDigest).isPresent()) { + eventHandlers.dispatch( + LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); return blobDescriptor; } @@ -87,8 +92,7 @@ public BlobDescriptor call() throws IOException, RegistryException { String baseRepository = buildConfiguration.getBaseImageConfiguration().getImageRepository(); String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; - registryClient.pushBlob( - blobDescriptor.getDigest(), blob, sourceRepository, throttledProgressReporter); + registryClient.pushBlob(blobDigest, blob, sourceRepository, throttledProgressReporter); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 87e12d064b..54049feaa7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -67,7 +67,8 @@ public BlobDescriptor call() throws IOException, RegistryException { progressEventDispatcher.newChildProducer(), pushAuthorization, Digests.computeDigest(containerConfiguration), - Blobs.from(containerConfiguration)) + Blobs.from(containerConfiguration), + false) .call(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 05fb34b8f9..de654b26f1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -20,12 +20,10 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.common.collect.ImmutableList; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -38,7 +36,7 @@ static ImmutableList makeList( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, - List> cachedLayers) { + List> cachedLayers) { try (TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing application layer pushers"); @@ -47,15 +45,16 @@ static ImmutableList makeList( "preparing application layer pushers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. - List blobPushers = new ArrayList<>(); - for (Future layer : cachedLayers) { - ProgressEventDispatcher.Factory childProgressProducer = - progressEventDispatcher.newChildProducer(); - blobPushers.add( - new PushLayerStep(buildConfiguration, childProgressProducer, pushAuthorization, layer)); - } - - return ImmutableList.copyOf(blobPushers); + return cachedLayers + .stream() + .map( + layer -> + new PushLayerStep( + buildConfiguration, + progressEventDispatcher.newChildProducer(), + pushAuthorization, + layer)) + .collect(ImmutableList.toImmutableList()); } } @@ -63,29 +62,36 @@ static ImmutableList makeList( private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Authorization pushAuthorization; - private final Future cachedLayerAndName; + private final Future preparedLayer; PushLayerStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, - Future cachedLayerAndName) { + Future preparedLayer) { this.buildConfiguration = buildConfiguration; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pushAuthorization = pushAuthorization; - this.cachedLayerAndName = cachedLayerAndName; + this.preparedLayer = preparedLayer; } @Override public BlobDescriptor call() throws IOException, RegistryException, ExecutionException, InterruptedException { - CachedLayer layer = cachedLayerAndName.get().getCachedLayer(); + PreparedLayer layer = preparedLayer.get(); + + boolean queriedExistence = layer.existsInTarget().isPresent(); + if (queriedExistence && layer.existsInTarget().get()) { + return layer.getBlobDescriptor(); // skip pushing if known to exist in registry + } + return new PushBlobStep( buildConfiguration, progressEventDispatcherFactory, pushAuthorization, - new BlobDescriptor(layer.getSize(), layer.getDigest()), - layer.getBlob()) + layer.getBlobDescriptor(), + layer.getBlob(), + queriedExistence) .call(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index cf0e838f31..1a73e959c2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -60,8 +60,8 @@ private static Future failedFuture() { } private Future baseImageAndAuth = failedFuture(); - private Future>> baseImageLayers = failedFuture(); - @Nullable private List> applicationLayers; + private Future>> baseImageLayers = failedFuture(); + @Nullable private List> applicationLayers; private Future builtImage = failedFuture(); private Future> targetRegistryCredentials = failedFuture(); private Future> pushAuthorization = failedFuture(); @@ -120,9 +120,10 @@ private StepsRunner( public StepsRunner dockerLoadSteps(DockerClient dockerClient) { rootProgressDescription = "building image to Docker daemon"; + // build and cache stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(() -> obtainBaseImageLayers(true)); // always pull layers for docker builds stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); // load to Docker @@ -132,9 +133,10 @@ public StepsRunner dockerLoadSteps(DockerClient dockerClient) { public StepsRunner tarBuildSteps(Path outputPath) { rootProgressDescription = "building image to tar file"; + // build and cache stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(() -> obtainBaseImageLayers(true)); // always pull layers for tar builds stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); // create a tar @@ -144,14 +146,17 @@ public StepsRunner tarBuildSteps(Path outputPath) { public StepsRunner registryPushSteps() { rootProgressDescription = "building image to registry"; - // build and cache + boolean layersRequiredLocally = JibSystemProperties.alwaysCacheBaseImage(); + + stepsToRun.add(this::retrieveTargetRegistryCredentials); + stepsToRun.add(this::authenticatePush); + stepsToRun.add(this::pullBaseImage); - stepsToRun.add(this::pullAndCacheBaseImageLayers); + stepsToRun.add(() -> obtainBaseImageLayers(layersRequiredLocally)); stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); + // push to registry - stepsToRun.add(this::retrieveTargetRegistryCredentials); - stepsToRun.add(this::authenticatePush); stepsToRun.add(this::pushBaseImageLayers); stepsToRun.add(this::pushApplicationLayers); stepsToRun.add(this::pushContainerConfiguration); @@ -212,7 +217,7 @@ private void pullBaseImage() { new PullBaseImageStep(buildConfiguration, childProgressDispatcherFactory)); } - private void pullAndCacheBaseImageLayers() { + private void obtainBaseImageLayers(boolean layersRequiredLocally) { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); @@ -220,10 +225,16 @@ private void pullAndCacheBaseImageLayers() { executorService.submit( () -> scheduleCallables( - PullAndCacheBaseImageLayerStep.makeList( - buildConfiguration, - childProgressDispatcherFactory, - results.baseImageAndAuth.get()))); + layersRequiredLocally + ? ObtainBaseImageLayerStep.makeListForForcedDownload( + buildConfiguration, + childProgressDispatcherFactory, + results.baseImageAndAuth.get()) + : ObtainBaseImageLayerStep.makeListForSelectiveDownload( + buildConfiguration, + childProgressDispatcherFactory, + results.baseImageAndAuth.get(), + results.pushAuthorization.get().orElse(null)))); } private void pushBaseImageLayers() { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index 2d11ac09f7..4a97dcb82b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -27,6 +27,8 @@ public class JibSystemProperties { @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; + @VisibleForTesting static final String ALWAYS_CACHE_BASE_IMAGE = "jib.alwaysCacheBaseImage"; + @VisibleForTesting public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; @@ -91,6 +93,16 @@ public static boolean isUserAgentEnabled() { return Strings.isNullOrEmpty(System.getProperty(DISABLE_USER_AGENT)); } + /** + * Gets whether to always cache base image layers. Determined from the {@code + * jib.alwaysCacheBaseImage} system property. + * + * @return true if the property is set to {@code always}; false otherwise + */ + public static boolean alwaysCacheBaseImage() { + return Boolean.getBoolean(ALWAYS_CACHE_BASE_IMAGE); + } + /** * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) * values. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index 38a45bbf07..cc7d3c9bf9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -135,7 +135,7 @@ private List buildFakeLayersToCache() for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : buildAndCacheApplicationLayerSteps) { - applicationLayers.add(buildAndCacheApplicationLayerStep.call().getCachedLayer()); + applicationLayers.add(buildAndCacheApplicationLayerStep.call()); } return applicationLayers; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index 5fe9044450..c0d7040c5e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -55,8 +55,8 @@ public class BuildImageStepTest { @Mock private CachedLayer mockCachedLayer; private Image baseImage; - private List baseImageLayers; - private List applicationLayers; + private List baseImageLayers; + private List applicationLayers; private DescriptorDigest testDescriptorDigest; private HistoryEntry nonEmptyLayerHistory; @@ -122,15 +122,15 @@ public void setUp() throws DigestException { .build(); baseImageLayers = Arrays.asList( - new CachedLayerAndName(mockCachedLayer, null), - new CachedLayerAndName(mockCachedLayer, null), - new CachedLayerAndName(mockCachedLayer, null)); + new PreparedLayer.Builder(mockCachedLayer).build(), + new PreparedLayer.Builder(mockCachedLayer).build(), + new PreparedLayer.Builder(mockCachedLayer).build()); applicationLayers = Arrays.asList( - new CachedLayerAndName(mockCachedLayer, "dependencies"), - new CachedLayerAndName(mockCachedLayer, "resources"), - new CachedLayerAndName(mockCachedLayer, "classes"), - new CachedLayerAndName(mockCachedLayer, "extra files")); + new PreparedLayer.Builder(mockCachedLayer).setName("dependencies").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("resources").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("classes").build(), + new PreparedLayer.Builder(mockCachedLayer).setName("extra files").build()); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java new file mode 100644 index 0000000000..ab7025d47d --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.image.ReferenceLayer; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.security.DigestException; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer3; + +/** Tests for {@link ObtainBaseImageLayerStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class ObtainBaseImageLayerStepTest { + + private ImageAndAuthorization baseImageAndAuth; + + private DescriptorDigest existingLayerDigest; + private DescriptorDigest freshLayerDigest; + + @Mock private Image image; + @Mock private RegistryClient registryClient; + + @Mock(answer = Answers.RETURNS_MOCKS) + private BuildConfiguration buildConfiguration; + + @Mock(answer = Answers.RETURNS_MOCKS) + private ProgressEventDispatcher.Factory progressDispatcherFactory; + + @Before + public void setUp() throws IOException, RegistryException, DigestException { + baseImageAndAuth = new ImageAndAuthorization(image, null); + + existingLayerDigest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + freshLayerDigest = + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + DescriptorDigest diffId = Mockito.mock(DescriptorDigest.class); + Layer existingLayer = new ReferenceLayer(new BlobDescriptor(existingLayerDigest), diffId); + Layer freshLayer = new ReferenceLayer(new BlobDescriptor(freshLayerDigest), diffId); + Mockito.when(image.getLayers()).thenReturn(ImmutableList.of(existingLayer, freshLayer)); + + Mockito.when(registryClient.checkBlob(existingLayerDigest)) + .thenReturn(Optional.of(Mockito.mock(BlobDescriptor.class))); + Mockito.when(registryClient.checkBlob(freshLayerDigest)).thenReturn(Optional.empty()); + + RegistryClient.Factory registryClientFactory = + Mockito.mock(RegistryClient.Factory.class, Answers.RETURNS_SELF); + Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); + + Mockito.lenient() + .when(buildConfiguration.newBaseImageRegistryClientFactory()) + .thenReturn(registryClientFactory); + Mockito.when(buildConfiguration.newTargetImageRegistryClientFactory()) + .thenReturn(registryClientFactory); + + // necessary to prevent error from classes dealing with progress report + Answer3, Consumer> progressSizeSetter = + (ignored1, progressSizeConsumer, ignored2) -> { + progressSizeConsumer.accept(Long.valueOf(12345)); + return null; + }; + Mockito.when(registryClient.pullBlob(Mockito.any(), Mockito.any(), Mockito.any())) + .thenAnswer(AdditionalAnswers.answer(progressSizeSetter)); + } + + @Test + public void testMakeListForSelectiveDownload() + throws IOException, CacheCorruptedException, RegistryException { + ImmutableList pullers = + ObtainBaseImageLayerStep.makeListForSelectiveDownload( + buildConfiguration, progressDispatcherFactory, baseImageAndAuth, null); + + Assert.assertEquals(2, pullers.size()); + PreparedLayer preparedExistingLayer = pullers.get(0).call(); + PreparedLayer preparedFreshLayer = pullers.get(1).call(); + + Assert.assertTrue(preparedExistingLayer.existsInTarget().get()); + Assert.assertFalse(preparedFreshLayer.existsInTarget().get()); + + // Should have queried all blobs. + Mockito.verify(registryClient).checkBlob(existingLayerDigest); + Mockito.verify(registryClient).checkBlob(freshLayerDigest); + + // Only the missing layer should be pulled. + Mockito.verify(registryClient, Mockito.never()) + .pullBlob(Mockito.eq(existingLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(freshLayerDigest), Mockito.any(), Mockito.any()); + } + + @Test + public void testMakeListForForcedDownload() + throws IOException, CacheCorruptedException, RegistryException { + ImmutableList pullers = + ObtainBaseImageLayerStep.makeListForForcedDownload( + buildConfiguration, progressDispatcherFactory, baseImageAndAuth); + + Assert.assertEquals(2, pullers.size()); + PreparedLayer preparedExistingLayer = pullers.get(0).call(); + PreparedLayer preparedFreshLayer = pullers.get(1).call(); + + // Unknown if layers exist in target registry. + Assert.assertEquals(Optional.empty(), preparedExistingLayer.existsInTarget()); + Assert.assertEquals(Optional.empty(), preparedFreshLayer.existsInTarget()); + + // No blob checking should happen. + Mockito.verify(registryClient, Mockito.never()).checkBlob(existingLayerDigest); + Mockito.verify(registryClient, Mockito.never()).checkBlob(freshLayerDigest); + + // All layers should be pulled. + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(existingLayerDigest), Mockito.any(), Mockito.any()); + Mockito.verify(registryClient) + .pullBlob(Mockito.eq(freshLayerDigest), Mockito.any(), Mockito.any()); + } + + @Test + public void testLayerMissingInCacheInOfflineMode() + throws CacheCorruptedException, RegistryException { + Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + + ImmutableList pullers = + ObtainBaseImageLayerStep.makeListForForcedDownload( + buildConfiguration, progressDispatcherFactory, baseImageAndAuth); + try { + pullers.get(1).call(); + Assert.fail(); + } catch (IOException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb. Rerun " + + "Jib in online mode with \"-Djib.alwaysCacheBaseImage=true\" to re-download the " + + "base image layers.", + ex.getMessage()); + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java new file mode 100644 index 0000000000..514cc4b674 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.registry.RegistryClient; +import java.io.IOException; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PushBlobStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class PushBlobStepTest { + + @Mock private BlobDescriptor blobDescriptor; + @Mock private RegistryClient registryClient; + + @Mock(answer = Answers.RETURNS_MOCKS) + private ProgressEventDispatcher.Factory progressDispatcherFactory; + + @Mock(answer = Answers.RETURNS_MOCKS) + private BuildConfiguration buildConfiguration; + + @Before + public void setUp() { + RegistryClient.Factory registryClientFactory = + Mockito.mock(RegistryClient.Factory.class, Answers.RETURNS_SELF); + Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); + + Mockito.when(buildConfiguration.newTargetImageRegistryClientFactory()) + .thenReturn(registryClientFactory); + Mockito.when(buildConfiguration.getTargetImageConfiguration()) + .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).build()); + } + + @Test + public void testCall_doBlobCheckAndBlobExists() throws IOException, RegistryException { + Mockito.when(registryClient.checkBlob(Mockito.any())).thenReturn(Optional.of(blobDescriptor)); + + call(false); + + Mockito.verify(registryClient).checkBlob(Mockito.any()); + Mockito.verify(registryClient, Mockito.never()) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testCall_doBlobCheckAndBlobDoesNotExist() throws IOException, RegistryException { + Mockito.when(registryClient.checkBlob(Mockito.any())).thenReturn(Optional.empty()); + + call(false); + + Mockito.verify(registryClient).checkBlob(Mockito.any()); + Mockito.verify(registryClient) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testCall_forcePushWithNoBlobCheck() throws IOException, RegistryException { + call(true); + + Mockito.verify(registryClient, Mockito.never()).checkBlob(Mockito.any()); + Mockito.verify(registryClient) + .pushBlob(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + private void call(boolean forcePush) throws IOException, RegistryException { + new PushBlobStep( + buildConfiguration, progressDispatcherFactory, null, blobDescriptor, null, forcePush) + .call(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index 4fca87bef0..85996ac10b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -37,6 +37,8 @@ public void setUp() { @After public void tearDown() { System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + System.clearProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE); System.clearProperty("http.proxyPort"); System.clearProperty("https.proxyPort"); if (httpProxyPortSaved != null) { @@ -45,7 +47,6 @@ public void tearDown() { if (httpsProxyPortSaved != null) { System.setProperty("https.proxyPort", httpsProxyPortSaved); } - System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); } @Test @@ -182,4 +183,28 @@ public void testUseBlobMounts_other() { System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "nonbool"); Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); } + + @Test + public void testAlwaysCacheBaseImage_undefined() { + System.clearProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE); + Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); + } + + @Test + public void testAlwaysCacheBaseImage_true() { + System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "true"); + Assert.assertTrue(JibSystemProperties.alwaysCacheBaseImage()); + } + + @Test + public void testAlwaysCacheBaseImage_false() { + System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "false"); + Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); + } + + @Test + public void testAlwaysCacheBaseImage_other() { + System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "nonbool"); + Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); + } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 38e9ffa85e..6ed601e0ab 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.common.base.Splitter; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.DigestException; import java.time.Instant; import org.gradle.testkit.runner.BuildResult; @@ -146,14 +148,15 @@ private static void assertExtraDirectoryDeprecationWarning(String pomXml) + "'jib.extraDirectories.permissions'")); } - private static String buildAndRunComplex( + private static void buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) throws IOException, InterruptedException { + Path baseCache = simpleTestProject.getProjectRoot().resolve("build/jib-base-cache"); BuildResult buildResult = simpleTestProject.build( "clean", "jib", - "-Djib.useOnlyProjectCache=true", + "-Djib.baseImageCache=" + baseCache, "-Djib.console=plain", "-D_TARGET_IMAGE=" + imageReference, "-D_TARGET_USERNAME=" + username, @@ -168,7 +171,11 @@ private static String buildAndRunComplex( assertDockerInspect(imageReference); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); - return new Command("docker", "run", "--rm", imageReference).run(); + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", + new Command("docker", "run", "--rm", imageReference).run()); } @Before @@ -318,11 +325,7 @@ public void testDockerDaemon_simple_multipleExtraDirectoriesWithAlternativeConfi public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); - Assert.assertEquals( - "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" - + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" - + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2)); + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("", targetImage); } @@ -331,11 +334,7 @@ public void testBuild_complex() throws IOException, InterruptedException { public void testBuild_complex_sameFromAndToRegistry() throws IOException, InterruptedException { String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); - Assert.assertEquals( - "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" - + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" - + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1)); + buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1); assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("", targetImage); } @@ -363,6 +362,22 @@ public void testDockerDaemon_jarContainerization() simpleTestProject, targetImage, "build-jar-containerization.gradle")); } + @Test + public void testBuild_skipDownloadingBaseImageLayers() throws IOException, InterruptedException { + Path baseLayersCacheDirectory = + simpleTestProject.getProjectRoot().resolve("build/jib-base-cache/layers"); + String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); + + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); + // Base image layer tarballs exist. + Assert.assertTrue(Files.exists(baseLayersCacheDirectory)); + Assert.assertTrue(baseLayersCacheDirectory.toFile().list().length >= 2); + + buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); + // no base layers downloaded after "gradle clean jib ..." + Assert.assertFalse(Files.exists(baseLayersCacheDirectory)); + } + @Test public void testDockerDaemon_filesModificationTimeCustom() throws DigestException, IOException, InterruptedException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index e2bf745290..84851ac8a9 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -112,19 +112,22 @@ private static Verifier build( verifier.addCliOption("--file=" + pomXml); verifier.executeGoals(Arrays.asList("clean", "compile")); + if (!buildTwice) { + verifier.executeGoal("jib:build"); + return verifier; + } + // Builds twice, and checks if the second build took less time. + verifier.addCliOption("-Djib.alwaysCacheBaseImage=true"); verifier.executeGoal("jib:build"); float timeOne = getBuildTimeFromVerifierLog(verifier); - if (buildTwice) { - verifier.resetStreams(); - verifier.executeGoal("jib:build"); - float timeTwo = getBuildTimeFromVerifierLog(verifier); - - String failMessage = "First build time (%s) is not greater than second build time (%s)"; - Assert.assertTrue(String.format(failMessage, timeOne, timeTwo), timeOne > timeTwo); - } + verifier.resetStreams(); + verifier.executeGoal("jib:build"); + float timeTwo = getBuildTimeFromVerifierLog(verifier); + String failMessage = "First build time (%s) is not greater than second build time (%s)"; + Assert.assertTrue(String.format(failMessage, timeOne, timeTwo), timeOne > timeTwo); return verifier; } From 8d82956f38c43d6c23f37fcb39ea1ed895372283 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 31 Jul 2019 17:15:56 -0400 Subject: [PATCH 0673/2020] Update CHANGELOG for #1818 and #1840 (#1865) --- jib-core/CHANGELOG.md | 3 +++ jib-gradle-plugin/CHANGELOG.md | 4 ++++ jib-maven-plugin/CHANGELOG.md | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 86f1fc7c45..28548086de 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,9 +5,12 @@ All notable changes to this project will be documented in this file. ### Added +- `JavaContainerBuilder#setLastModifiedTimeProvider` to set file timestamps ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) + ### Changed - `JibContainerBuilder#addDependencies` is now split into three methods: `addDependencies`, `addSnapshotDependencies`, `addProjectDependencies` ([#1773](https://github.com/GoogleContainerTools/jib/pull/1773)) +- For building and pushing to a registry, Jib now skips downloading and caching base image layers if the layers already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) ### Fixed diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c4743e46ed..68ccd2831e 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,8 +5,12 @@ All notable changes to this project will be documented in this file. ### Added +- Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) + ### Changed +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) + ### Fixed ## 1.4.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 19f881fe62..92042d7524 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,8 +5,12 @@ All notable changes to this project will be documented in this file. ### Added +- Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) + ### Changed +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) + ### Fixed ## 1.4.0 From 82fd3e40a9994b49ef3946b775abb943e85be63a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 1 Aug 2019 16:29:23 -0400 Subject: [PATCH 0674/2020] Use tri-state enum for blob existence in target repository (#1879) --- .../steps/ObtainBaseImageLayerStep.java | 22 +++++++++++-------- .../jib/builder/steps/PreparedLayer.java | 19 ++++++++++------ .../jib/builder/steps/PushLayerStep.java | 7 +++--- .../steps/ObtainBaseImageLayerStepTest.java | 11 +++++----- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 735be6a676..02f3b5cbbb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; @@ -45,16 +46,16 @@ class ObtainBaseImageLayerStep implements Callable { @FunctionalInterface private interface BlobExistenceChecker { - Optional exists(DescriptorDigest digest) throws IOException, RegistryException; + StateInTarget check(DescriptorDigest digest) throws IOException, RegistryException; } static ImmutableList makeListForForcedDownload( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, ImageAndAuthorization baseImageAndAuth) { - BlobExistenceChecker noOpBlobChecker = ignored -> Optional.empty(); + BlobExistenceChecker noOpChecker = ignored -> StateInTarget.UNKNOWN; return makeList( - buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, noOpBlobChecker); + buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, noOpChecker); } static ImmutableList makeListForSelectiveDownload( @@ -71,7 +72,10 @@ static ImmutableList makeListForSelectiveDownload( .newRegistryClient(); // TODO: also check if cross-repo blob mount is possible. BlobExistenceChecker blobExistenceChecker = - digest -> Optional.of(targetRegistryClient.checkBlob(digest).isPresent()); + digest -> + targetRegistryClient.checkBlob(digest).isPresent() + ? StateInTarget.EXISTING + : StateInTarget.MISSING; return makeList( buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, blobExistenceChecker); @@ -134,9 +138,9 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr new TimerEventDispatcher( buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, layerDigest))) { - Optional layerExists = blobExistenceChecker.exists(layerDigest); - if (layerExists.orElse(false)) { - return new PreparedLayer.Builder(layer).setStateInTarget(layerExists).build(); + StateInTarget stateInTarget = blobExistenceChecker.check(layerDigest); + if (stateInTarget == StateInTarget.EXISTING) { + return new PreparedLayer.Builder(layer).setStateInTarget(stateInTarget).build(); } Cache cache = buildConfiguration.getBaseImageLayersCache(); @@ -145,7 +149,7 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { CachedLayer cachedLayer = optionalCachedLayer.get(); - return new PreparedLayer.Builder(cachedLayer).setStateInTarget(layerExists).build(); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); } else if (buildConfiguration.isOffline()) { throw new IOException( "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " @@ -170,7 +174,7 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr layerDigest, progressEventDispatcherWrapper::setProgressTarget, progressEventDispatcherWrapper::dispatchProgress)); - return new PreparedLayer.Builder(cachedLayer).setStateInTarget(layerExists).build(); + return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java index dd02e8a2cd..52e3eda7a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.Layer; -import java.util.Optional; /** * Layer prepared from {@link BuildAndCacheApplicationLayerStep} and {@link @@ -29,11 +28,17 @@ */ class PreparedLayer implements Layer { + enum StateInTarget { + UNKNOWN, + EXISTING, + MISSING + } + static class Builder { private Layer layer; private String name = "unnamed layer"; - private Optional stateInTarget = Optional.empty(); + private StateInTarget stateInTarget = StateInTarget.UNKNOWN; Builder(Layer layer) { this.layer = layer; @@ -44,8 +49,8 @@ Builder setName(String name) { return this; } - /** Sets whether the layer exists in a target destination. Empty (absence) means unknown. */ - Builder setStateInTarget(Optional stateInTarget) { + /** Sets whether the layer exists in a target destination. */ + Builder setStateInTarget(StateInTarget stateInTarget) { this.stateInTarget = stateInTarget; return this; } @@ -57,9 +62,9 @@ PreparedLayer build() { private final Layer layer; private final String name; - private final Optional stateInTarget; + private final StateInTarget stateInTarget; - private PreparedLayer(Layer layer, String name, Optional stateInTarget) { + private PreparedLayer(Layer layer, String name, StateInTarget stateInTarget) { this.layer = layer; this.name = name; this.stateInTarget = stateInTarget; @@ -69,7 +74,7 @@ String getName() { return name; } - Optional existsInTarget() { + StateInTarget getStateInTarget() { return stateInTarget; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index de654b26f1..31e233bbc1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.http.Authorization; import com.google.common.collect.ImmutableList; @@ -80,18 +81,18 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionException, InterruptedException { PreparedLayer layer = preparedLayer.get(); - boolean queriedExistence = layer.existsInTarget().isPresent(); - if (queriedExistence && layer.existsInTarget().get()) { + if (layer.getStateInTarget() == StateInTarget.EXISTING) { return layer.getBlobDescriptor(); // skip pushing if known to exist in registry } + boolean forcePush = layer.getStateInTarget() == StateInTarget.MISSING; return new PushBlobStep( buildConfiguration, progressEventDispatcherFactory, pushAuthorization, layer.getBlobDescriptor(), layer.getBlob(), - queriedExistence) + forcePush) .call(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java index ab7025d47d..c03516e9b8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; @@ -113,8 +114,8 @@ public void testMakeListForSelectiveDownload() PreparedLayer preparedExistingLayer = pullers.get(0).call(); PreparedLayer preparedFreshLayer = pullers.get(1).call(); - Assert.assertTrue(preparedExistingLayer.existsInTarget().get()); - Assert.assertFalse(preparedFreshLayer.existsInTarget().get()); + Assert.assertEquals(StateInTarget.EXISTING, preparedExistingLayer.getStateInTarget()); + Assert.assertEquals(StateInTarget.MISSING, preparedFreshLayer.getStateInTarget()); // Should have queried all blobs. Mockito.verify(registryClient).checkBlob(existingLayerDigest); @@ -138,9 +139,9 @@ public void testMakeListForForcedDownload() PreparedLayer preparedExistingLayer = pullers.get(0).call(); PreparedLayer preparedFreshLayer = pullers.get(1).call(); - // Unknown if layers exist in target registry. - Assert.assertEquals(Optional.empty(), preparedExistingLayer.existsInTarget()); - Assert.assertEquals(Optional.empty(), preparedFreshLayer.existsInTarget()); + // existence unknown + Assert.assertEquals(StateInTarget.UNKNOWN, preparedExistingLayer.getStateInTarget()); + Assert.assertEquals(StateInTarget.UNKNOWN, preparedFreshLayer.getStateInTarget()); // No blob checking should happen. Mockito.verify(registryClient, Mockito.never()).checkBlob(existingLayerDigest); From e275d9edfe022f41e315c29e113e132aea4bec12 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 1 Aug 2019 17:01:25 -0400 Subject: [PATCH 0675/2020] Add tarball extractor (#1883) --- .../cloud/tools/jib/tar/TarExtractor.java | 71 ++++++++++++++++++ .../cloud/tools/jib/tar/TarExtractorTest.java | 53 +++++++++++++ jib-core/src/test/resources/core/extract.tar | Bin 0 -> 9216 bytes 3 files changed, 124 insertions(+) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java create mode 100644 jib-core/src/test/resources/core/extract.tar diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java new file mode 100644 index 0000000000..462e005b20 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.tar; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; + +/** Extracts a tarball. */ +public class TarExtractor { + + /** + * Extracts a tarball to the specified destination. + * + * @param source the tarball to extract + * @param destination the output directory + * @throws IOException if extraction fails + */ + public static void extract(Path source, Path destination) throws IOException { + String canonicalDestination = destination.toFile().getCanonicalPath(); + + try (InputStream in = new BufferedInputStream(Files.newInputStream(source)); + TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { + byte[] buffer = new byte[1024]; + TarArchiveEntry entry = tarArchiveInputStream.getNextTarEntry(); + while (entry != null) { + Path entryPath = destination.resolve(entry.getName()); + + String canonicalTarget = entryPath.toFile().getCanonicalPath(); + if (!canonicalTarget.startsWith(canonicalDestination + File.separator)) { + String offender = entry.getName() + " from " + source; + throw new IOException("Blocked unzipping files outside destination: " + offender); + } + + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { + int read = tarArchiveInputStream.read(buffer); + while (read != -1) { + out.write(buffer, 0, read); + read = tarArchiveInputStream.read(buffer); + } + } + } + entry = tarArchiveInputStream.getNextTarEntry(); + } + } + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java new file mode 100644 index 0000000000..cfa67a3fbc --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.tar; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link TarExtractor}. */ +public class TarExtractorTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testExtract() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/extract.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + TarExtractor.extract(source, destination); + + Assert.assertTrue(Files.exists(destination.resolve("file A"))); + Assert.assertTrue(Files.exists(destination.resolve("file B"))); + Assert.assertTrue( + Files.exists(destination.resolve("folder").resolve("nested folder").resolve("file C"))); + + try (Stream lines = Files.lines(destination.resolve("file A"))) { + String contents = lines.collect(Collectors.joining()); + Assert.assertEquals("Hello", contents); + } + } +} diff --git a/jib-core/src/test/resources/core/extract.tar b/jib-core/src/test/resources/core/extract.tar new file mode 100644 index 0000000000000000000000000000000000000000..fdc05b14272d1db9dabb4df0dff47075c549e229 GIT binary patch literal 9216 zcmeI1!EVAZ42FB=De?rxabgD!9JXCJ?iJ7|6H*zd6p6k4LQ9pE!KhTCvCTatRdCba ze7}=)vCijNF?^owr`hb#dds$DHdl-4esYrw034A@Ty?1g$q^cr`1|!mT4_TF42}sE zphn_E?Qin*D?xVKa#r}MlrQUIo@W)qzup!!Xkms013+~YlM)owYT zkfAa20G(Qu%T+ef#Fia{hSY+^(P(gwlb1R5R-op*P5rQU)OG-mM#Vn@G8_KNKR}y* z?F?h0s2wIw@1OG@jQ^I7-%eGN|1SPdtQBY`;E(+lY%b-mc{?$=>OAE zkNzKZ8V&zl{J-YkvNhuWQIm2E{>lHBd8Qs?Jk#f&t?jn{_q%|D{~JT%|1olL8uN<( zBcyKZtV8ktQ|10fe-VxPpijepC;zYbw}w3b5JJRj$@>3z@KgO09%2aHOWau{}GKpCYJi&Q}-@Ezf%9LPSF_zssCNX{DouA o@?R|q`cfanX!yTe|HJp6ntw2d@4vGD3mQsG6c7bOfm Date: Thu, 1 Aug 2019 18:00:25 -0400 Subject: [PATCH 0676/2020] Update jackson-databind (#1885) --- jib-core/build.gradle | 2 +- jib-gradle-plugin/build.gradle | 2 +- jib-maven-plugin/pom.xml | 2 +- jib-plugins-common/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index d75483fc77..4402863195 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'com.google.http-client:google-http-client:1.27.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:27.0.1-jre' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' implementation 'org.ow2.asm:asm:7.0' testImplementation 'junit:junit:4.12' diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 9465169fae..a1b7635e6c 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -64,7 +64,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index d50f9f7624..2526fe4442 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -71,7 +71,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.9.1 + 2.9.9.2 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 57c021e599..6e41ea7cd4 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -32,7 +32,7 @@ dependencies { compile 'com.google.http-client:google-http-client:1.27.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' testCompile 'junit:junit:4.12' From 61ff620852a0049d837b56f1cb8fe557c58c84ea Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 1 Aug 2019 18:00:53 -0400 Subject: [PATCH 0677/2020] Upgrade google-http-client to eliminate deprecated usage (#1882) * Upgrade google-http-client * Handle SSLHandshakeException * SSLException for all * Migrate deprecated usage * Upgrade Guava and Google HTTP Client * Migrate * No need to set proxy credentials on our side * Wrap up * Update comment * Upgrade to newer verisons * wip * Apply fix from google-http-client PR * Unset default SSLSocketFactory to create new one * Add test for proxy credential properties * Update comment * Add proxy properties test * No need to create readers repeatedly * Simplify * Remove blank lines * Use Apache v2 client * Migrate to 1.31 --- jib-core/build.gradle | 5 +- .../cloud/tools/jib/http/Connection.java | 71 ++------ .../ConnectionWithProxyCredentialsTest.java | 167 ------------------ .../cloud/tools/jib/http/MockConnection.java | 2 +- jib-gradle-plugin/build.gradle | 3 +- jib-maven-plugin/pom.xml | 8 +- jib-plugins-common/build.gradle | 3 +- 7 files changed, 34 insertions(+), 225 deletions(-) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 4402863195..6b640838a1 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -16,6 +16,8 @@ group 'com.google.cloud.tools' sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 compileJava.options.encoding = 'UTF-8' +compileJava.options.compilerArgs += [ '-Xlint:deprecation' ] +compileTestJava.options.compilerArgs += [ '-Xlint:deprecation' ] repositories { mavenCentral() @@ -35,7 +37,8 @@ configurations { dependencies { // Make sure these are consistent with jib-maven-plugin. - implementation 'com.google.http-client:google-http-client:1.27.0' + implementation 'com.google.http-client:google-http-client:1.31.0' + implementation 'com.google.http-client:google-http-client-apache-v2:1.31.0' implementation 'org.apache.commons:commons-compress:1.18' implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 750d6e27cc..834c87779f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -22,7 +22,8 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.util.SslUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.Closeable; @@ -31,9 +32,8 @@ import java.security.GeneralSecurityException; import java.util.function.Function; import javax.annotation.Nullable; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.HttpClientBuilder; /** * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than @@ -56,17 +56,12 @@ public class Connection implements Closeable { * @return {@link Connection} factory, a function that generates a {@link Connection} to a URL */ public static Function getConnectionFactory() { - /* - * Do not use {@link NetHttpTransport}. It does not process response errors properly. A new - * {@link ApacheHttpTransport} needs to be created for each connection because otherwise HTTP - * connection persistence causes the connection to throw {@link NoHttpResponseException}. - * - * @see https://github.com/google/google-http-java-client/issues/39 - */ - ApacheHttpTransport transport = new ApacheHttpTransport(); - addProxyCredentials(transport); - return url -> new Connection(url, transport); + // Do not use NetHttpTransport. It does not process response errors properly. + // See https://github.com/google/google-http-java-client/issues/39 + // + // A new ApacheHttpTransport needs to be created for each connection because otherwise HTTP + // connection persistence causes the connection to throw NoHttpResponseException. + return url -> new Connection(url, new ApacheHttpTransport()); } /** @@ -77,44 +72,14 @@ public static Function getConnectionFactory() { */ public static Function getInsecureConnectionFactory() throws GeneralSecurityException { - // Do not use {@link NetHttpTransport}. See {@link getConnectionFactory} for details. - ApacheHttpTransport transport = - new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); - addProxyCredentials(transport); - return url -> new Connection(url, transport); - } - - /** - * Registers proxy credentials onto transport client, in order to deal with proxies that require - * basic authentication. - * - * @param transport Apache HTTP transport - */ - @VisibleForTesting - static void addProxyCredentials(ApacheHttpTransport transport) { - addProxyCredentials(transport, "https"); - addProxyCredentials(transport, "http"); - } - - private static void addProxyCredentials(ApacheHttpTransport transport, String protocol) { - Preconditions.checkArgument(protocol.equals("http") || protocol.equals("https")); - - String proxyHost = System.getProperty(protocol + ".proxyHost"); - String proxyUser = System.getProperty(protocol + ".proxyUser"); - String proxyPassword = System.getProperty(protocol + ".proxyPassword"); - if (proxyHost == null || proxyUser == null || proxyPassword == null) { - return; - } - - String defaultProxyPort = protocol.equals("http") ? "80" : "443"; - int proxyPort = Integer.parseInt(System.getProperty(protocol + ".proxyPort", defaultProxyPort)); - - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - httpClient - .getCredentialsProvider() - .setCredentials( - new AuthScope(proxyHost, proxyPort), - new UsernamePasswordCredentials(proxyUser, proxyPassword)); + HttpClientBuilder httpClientBuilder = + ApacheHttpTransport.newDefaultHttpClientBuilder() + .setSSLSocketFactory(null) // creates new factory with the SSLContext given below + .setSSLContext(SslUtils.trustAllSSLContext()) + .setSSLHostnameVerifier(new NoopHostnameVerifier()); + + // Do not use NetHttpTransport. See comments in getConnectionFactory for details. + return url -> new Connection(url, new ApacheHttpTransport(httpClientBuilder.build())); } private HttpRequestFactory requestFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java deleted file mode 100644 index 0b86e54a59..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.apache.ApacheHttpTransport; -import com.google.common.collect.ImmutableList; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.Credentials; -import org.apache.http.impl.client.DefaultHttpClient; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** Tests for {@link Connection} with setting proxy credentials. */ -public class ConnectionWithProxyCredentialsTest { - - private static final ImmutableList proxyProperties = - ImmutableList.of( - "http.proxyHost", - "http.proxyPort", - "http.proxyUser", - "http.proxyPassword", - "https.proxyHost", - "https.proxyPort", - "https.proxyUser", - "https.proxyPassword"); - - // HashMap to allow saving null values. - private final HashMap savedProperties = new HashMap<>(); - - private final ApacheHttpTransport transport = new ApacheHttpTransport(); - - @Before - public void setUp() { - proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); - proxyProperties.stream().forEach(key -> System.clearProperty(key)); - } - - @After - public void tearDown() { - Consumer> restoreProperty = - entry -> { - if (entry.getValue() == null) { - System.clearProperty(entry.getKey()); - } else { - System.setProperty(entry.getKey(), entry.getValue()); - } - }; - savedProperties.entrySet().stream().forEach(restoreProperty); - } - - @Test - public void testAddProxyCredentials_undefined() { - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPort", "1080"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPort", "1443"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 1443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_defaultPorts() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_hostUndefined() { - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_userUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPassword", "s-pass"); - - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_passwordUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - - Connection.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java index 3470ba8d19..2ec95c638a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; import java.io.IOException; import java.util.function.BiFunction; diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index a1b7635e6c..9b8b922b6e 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -61,7 +61,8 @@ configurations { dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. - compile 'com.google.http-client:google-http-client:1.27.0' + compile 'com.google.http-client:google-http-client:1.31.0' + compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 2526fe4442..3ccb8f23d7 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -53,7 +53,13 @@ com.google.http-client google-http-client - 1.27.0 + 1.31.0 + compile + + + com.google.http-client + google-http-client-apache-v2 + 1.31.0 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 6e41ea7cd4..c9e27e5d50 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -29,7 +29,8 @@ sourceSets { dependencies { // Make sure these are consistent with jib-maven-plugin. - compile 'com.google.http-client:google-http-client:1.27.0' + compile 'com.google.http-client:google-http-client:1.31.0' + compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' compile 'org.apache.commons:commons-compress:1.18' compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' From 8a93a353af4fdbfde0eab58b0c734b8f6e99505c Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 6 Aug 2019 10:11:55 -0400 Subject: [PATCH 0678/2020] Minor tests cleanup (#1890) * Minor tests cleanup * Change iterable -> set on tags --- .../google/cloud/tools/jib/gradle/GradleRawConfiguration.java | 3 ++- .../google/cloud/tools/jib/maven/MavenRawConfiguration.java | 3 ++- .../test/resources/maven/application/source/HelloWorld.java | 1 + .../test/resources/maven/application/source/package/some.java | 1 + .../src/test/resources/maven/application/source/some.java | 1 + .../cloud/tools/jib/plugins/common/RawConfiguration.java | 3 ++- 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index c7ae362e26..bcbf1786bf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** Gradle-specific adapter for providing raw configuration parameter values. */ class GradleRawConfiguration implements RawConfiguration { @@ -66,7 +67,7 @@ public Optional getToCredHelper() { } @Override - public Iterable getToTags() { + public Set getToTags() { return jibExtension.getTo().getTags(); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index f7c69cc162..913c9c89fa 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** Maven-specific adapter for providing raw configuration parameter values. */ class MavenRawConfiguration implements RawConfiguration { @@ -71,7 +72,7 @@ public Optional getToCredHelper() { } @Override - public Iterable getToTags() { + public Set getToTags() { return jibPluginConfiguration.getTargetImageAdditionalTags(); } diff --git a/jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java index e69de29bb2..8b13789179 100644 --- a/jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java +++ b/jib-maven-plugin/src/test/resources/maven/application/source/HelloWorld.java @@ -0,0 +1 @@ + diff --git a/jib-maven-plugin/src/test/resources/maven/application/source/package/some.java b/jib-maven-plugin/src/test/resources/maven/application/source/package/some.java index e69de29bb2..8b13789179 100644 --- a/jib-maven-plugin/src/test/resources/maven/application/source/package/some.java +++ b/jib-maven-plugin/src/test/resources/maven/application/source/package/some.java @@ -0,0 +1 @@ + diff --git a/jib-maven-plugin/src/test/resources/maven/application/source/some.java b/jib-maven-plugin/src/test/resources/maven/application/source/some.java index e69de29bb2..8b13789179 100644 --- a/jib-maven-plugin/src/test/resources/maven/application/source/some.java +++ b/jib-maven-plugin/src/test/resources/maven/application/source/some.java @@ -0,0 +1 @@ + diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index c2fa1ea6c3..92c57636dc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * Holds raw plugin configuration parameter values. Acts as a common adapter for heterogeneous @@ -42,7 +43,7 @@ public interface RawConfiguration { Optional getToCredHelper(); - Iterable getToTags(); + Set getToTags(); Optional> getEntrypoint(); From 187c2e038c15e52ea53366b828834c6f7fa963ef Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 6 Aug 2019 12:59:41 -0400 Subject: [PATCH 0679/2020] Restore proxy system properties after test (#1886) --- jib-core/build.gradle | 1 + .../jib/global/JibSystemPropertiesTest.java | 33 ++------- .../jib/http/WithServerConnectionTest.java | 4 ++ .../registry/RegistryEndpointCallerTest.java | 13 ++-- jib-gradle-plugin/build.gradle | 1 + .../tools/jib/gradle/JibExtensionTest.java | 38 ++--------- .../tools/jib/gradle/TaskCommonTest.java | 13 ++-- jib-maven-plugin/pom.xml | 6 ++ .../maven/MavenSettingsProxyProviderTest.java | 68 +++++++++---------- jib-plugins-common/build.gradle | 1 + .../PluginConfigurationProcessorTest.java | 7 +- 11 files changed, 68 insertions(+), 117 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 6b640838a1..d6a32062fb 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -47,6 +47,7 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' testImplementation 'org.slf4j:slf4j-api:1.7.25' + testImplementation 'com.github.stefanbirkner:system-rules:1.19.0' // NullAway errorprone plugin annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index 85996ac10b..5fc616a90e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -16,42 +16,19 @@ package com.google.cloud.tools.jib.global; -import javax.annotation.Nullable; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link JibSystemProperties}. */ public class JibSystemPropertiesTest { - @Nullable private String httpProxyPortSaved; - @Nullable private String httpsProxyPortSaved; - - @Before - public void setUp() { - httpProxyPortSaved = System.getProperty("http.proxyPort"); - httpsProxyPortSaved = System.getProperty("https.proxyPort"); - } - - @After - public void tearDown() { - System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); - System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); - System.clearProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE); - System.clearProperty("http.proxyPort"); - System.clearProperty("https.proxyPort"); - if (httpProxyPortSaved != null) { - System.setProperty("http.proxyPort", httpProxyPortSaved); - } - if (httpsProxyPortSaved != null) { - System.setProperty("https.proxyPort", httpsProxyPortSaved); - } - } + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @Test - public void testCheckHttpTimeoutProperty_ok() throws NumberFormatException { - Assert.assertNull(System.getProperty(JibSystemProperties.HTTP_TIMEOUT)); + public void testCheckHttpTimeoutProperty_okWhenUndefined() throws NumberFormatException { + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); JibSystemProperties.checkHttpTimeoutProperty(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index 678b92ff53..d4104633c8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -26,11 +26,15 @@ import javax.net.ssl.SSLException; import org.hamcrest.CoreMatchers; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link Connection} using an actual local server. */ public class WithServerConnectionTest { + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Test public void testGet() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 26a5a3e30b..0e50b15164 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -48,10 +48,11 @@ import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.NoHttpResponseException; import org.hamcrest.CoreMatchers; -import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -113,6 +114,8 @@ private static HttpResponse mockRedirectHttpResponse(String redirectLocation) th return mockHttpResponse(code307, new HttpHeaders().setLocation(redirectLocation)); } + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private EventHandlers mockEventHandlers; @Mock private Connection mockConnection; @Mock private Connection mockInsecureConnection; @@ -133,12 +136,6 @@ public void setUp() throws IOException { .thenReturn(new ByteArrayInputStream("body".getBytes(StandardCharsets.UTF_8))); } - @After - public void tearDown() { - System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); - System.clearProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP); - } - @Test public void testCall_secureCallerOnUnverifiableServer() throws IOException, RegistryException { Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) @@ -439,7 +436,7 @@ public void testHttpTimeout_propertyNotSet() throws IOException, RegistryExcepti MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Assert.assertNull(System.getProperty(JibSystemProperties.HTTP_TIMEOUT)); + System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); secureEndpointCaller.call(); // We fall back to the default timeout: diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 9b8b922b6e..2bf0cd04b3 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -71,6 +71,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' + testCompile 'com.github.stefanbirkner:system-rules:1.19.0' // only for testing a concrete Spring Boot example in a test (not for test infrastructure) testCompile 'org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE' diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 851271c620..62411e64de 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -23,58 +23,32 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.Properties; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; -import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link JibExtension}. */ public class JibExtensionTest { + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + private JibExtension testJibExtension; private Project fakeProject; - private static void clearProperties() { - System.clearProperty("jib.from.image"); - System.clearProperty("jib.from.credHelper"); - System.clearProperty("jib.to.image"); - System.clearProperty("jib.to.tags"); - System.clearProperty("jib.to.credHelper"); - System.clearProperty("jib.container.appRoot"); - System.clearProperty("jib.container.args"); - System.clearProperty("jib.container.entrypoint"); - System.clearProperty("jib.container.environment"); - System.clearProperty("jib.container.extraClasspath"); - System.clearProperty("jib.container.format"); - System.clearProperty("jib.container.jvmFlags"); - System.clearProperty("jib.container.labels"); - System.clearProperty("jib.container.mainClass"); - System.clearProperty("jib.container.ports"); - System.clearProperty("jib.container.useCurrentTimestamp"); - System.clearProperty("jib.container.user"); - System.clearProperty("jib.container.filesModificationTime"); - System.clearProperty("jib.containerizingMode"); - System.clearProperty("jib.extraDirectory.path"); - System.clearProperty("jib.extraDirectory.permissions"); - System.clearProperty("jib.extraDirectories.paths"); - System.clearProperty("jib.extraDirectories.permissions"); - } - @Before public void setUp() { - clearProperties(); fakeProject = ProjectBuilder.builder().build(); testJibExtension = fakeProject .getExtensions() .create(JibPlugin.JIB_EXTENSION_NAME, JibExtension.class, fakeProject); - } - @After - public void teardown() { - clearProperties(); + System.setProperties(new Properties()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 0e5997965e..203da7d672 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -22,10 +22,11 @@ import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.bundling.War; import org.gradle.testfixtures.ProjectBuilder; -import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; @@ -37,19 +38,13 @@ @RunWith(MockitoJUnitRunner.class) public class TaskCommonTest { + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private JibExtension jibExtension; @Mock private Logger logger; @Before public void setUp() { - Assert.assertNull(System.getProperty("jib.extraDirectory.path")); - Assert.assertNull(System.getProperty("jib.extraDirectory.permissions")); - Assert.assertNull(System.getProperty("jib.extraDirectories.paths")); - Assert.assertNull(System.getProperty("jib.extraDirectories.permissions")); - } - - @After - public void tearDown() { System.clearProperty("jib.extraDirectory.path"); System.clearProperty("jib.extraDirectory.permissions"); System.clearProperty("jib.extraDirectories.paths"); diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 3ccb8f23d7..e407b4f3d4 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -131,6 +131,12 @@ 1.7.25 test + + com.github.stefanbirkner + system-rules + 1.19.0 + test + org.apache.maven.plugin-testing diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java index f0d1c08a5c..5b219de94d 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsProxyProviderTest.java @@ -16,21 +16,19 @@ package com.google.cloud.tools.jib.maven; -import com.google.common.collect.ImmutableList; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; +import java.util.Arrays; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.settings.Proxy; import org.apache.maven.settings.Settings; import org.apache.maven.settings.crypto.SettingsDecrypter; import org.hamcrest.CoreMatchers; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -38,28 +36,15 @@ @RunWith(MockitoJUnitRunner.class) public class MavenSettingsProxyProviderTest { + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + private static Settings noActiveProxiesSettings; private static Settings httpOnlyProxySettings; private static Settings httpsOnlyProxySettings; private static Settings mixedProxyEncryptedSettings; private static Settings badProxyEncryptedSettings; private static SettingsDecrypter settingsDecrypter; - private static SettingsDecrypter emptySettingsDescypter; - - private static final ImmutableList proxyProperties = - ImmutableList.of( - "http.proxyHost", - "http.proxyPort", - "http.proxyUser", - "http.proxyPassword", - "https.proxyHost", - "https.proxyPort", - "https.proxyUser", - "https.proxyPassword", - "http.nonProxyHosts"); - - // HashMap to allow saving null values. - private final HashMap savedProperties = new HashMap<>(); + private static SettingsDecrypter emptySettingsDecrypter; @BeforeClass public static void setUpTestFixtures() { @@ -81,28 +66,24 @@ public static void setUpTestFixtures() { settingsDecrypter = SettingsFixture.newSettingsDecrypter( Paths.get("src/test/resources/maven/settings/settings-security.xml")); - emptySettingsDescypter = + emptySettingsDecrypter = SettingsFixture.newSettingsDecrypter( Paths.get("src/test/resources/maven/settings/settings-security.empty.xml")); } @Before public void setUp() { - proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); - proxyProperties.stream().forEach(key -> System.clearProperty(key)); - } - - @After - public void tearDown() { - Consumer> restoreProperty = - entry -> { - if (entry.getValue() == null) { - System.clearProperty(entry.getKey()); - } else { - System.setProperty(entry.getKey(), entry.getValue()); - } - }; - savedProperties.entrySet().stream().forEach(restoreProperty); + Arrays.asList( + "http.proxyHost", + "http.proxyPort", + "http.proxyUser", + "http.proxyPassword", + "https.proxyHost", + "https.proxyPort", + "https.proxyUser", + "https.proxyPassword", + "http.nonProxyHosts") + .forEach(System::clearProperty); } @Test @@ -298,4 +279,17 @@ public void testActivateHttpAndHttpsProxies_decryptionFailure() { CoreMatchers.startsWith("Unable to decrypt proxy info from settings.xml:")); } } + + @Test + public void testActivateHttpAndHttpsProxies_emptySettingsDecrypter() { + try { + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + mixedProxyEncryptedSettings, emptySettingsDecrypter); + Assert.fail(); + } catch (MojoExecutionException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith("Unable to decrypt proxy info from settings.xml:")); + } + } } diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index c9e27e5d50..52330c3077 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -39,6 +39,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' testCompile 'org.slf4j:slf4j-api:1.7.25' + testCompile 'com.github.stefanbirkner:system-rules:1.19.0' // NullAway errorprone plugin annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 9870c2d886..d32a804441 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -49,7 +49,9 @@ import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; @@ -84,6 +86,8 @@ private static void assertExtractionPathsUnordered( expectedPaths, entries, layerEntry -> layerEntry.getExtractionPath().toString()); } + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private RawConfiguration rawConfiguration; @Mock private ProjectProperties projectProperties; @Mock private Containerizer containerizer; @@ -204,9 +208,6 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() Mockito.verify(containerizer).setBaseImageLayersCache(Paths.get("new/base/cache")); Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("/new/application/cache")); - - System.clearProperty(PropertyNames.BASE_IMAGE_CACHE); - System.clearProperty(PropertyNames.APPLICATION_CACHE); } @Test From 74dd1e36b0dab75de1b2e5be08e4f83da64abd6b Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 6 Aug 2019 15:17:37 -0400 Subject: [PATCH 0680/2020] fix variable naming (#1892) --- .../cloud/tools/jib/maven/MavenProjectPropertiesTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index e1acf1f5ae..433e260104 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -676,11 +676,11 @@ private BuildConfiguration setupBuildConfiguration( JavaContainerBuilder.from(RegistryImage.named("base")) .setAppRoot(AbsoluteUnixPath.get(appRoot)) .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); - JibContainerBuilder JibContainerBuilder = + JibContainerBuilder jibContainerBuilder = new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( - JibContainerBuilder, + jibContainerBuilder, Containerizer.to(RegistryImage.named("to")) .setExecutorService(MoreExecutors.newDirectExecutorService())); } From 16111ba96401a0679e6c0e517562f6e04ea6746a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 8 Aug 2019 18:10:42 -0400 Subject: [PATCH 0681/2020] Apply consistency (#1896) --- .../google/cloud/tools/jib/tar/TarExtractor.java | 15 ++++++--------- .../cloud/tools/jib/plugins/common/ZipUtil.java | 7 ++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java index 462e005b20..71261a4c94 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.tar; +import com.google.common.io.ByteStreams; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -42,9 +43,10 @@ public static void extract(Path source, Path destination) throws IOException { try (InputStream in = new BufferedInputStream(Files.newInputStream(source)); TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { - byte[] buffer = new byte[1024]; - TarArchiveEntry entry = tarArchiveInputStream.getNextTarEntry(); - while (entry != null) { + + for (TarArchiveEntry entry = tarArchiveInputStream.getNextTarEntry(); + entry != null; + entry = tarArchiveInputStream.getNextTarEntry()) { Path entryPath = destination.resolve(entry.getName()); String canonicalTarget = entryPath.toFile().getCanonicalPath(); @@ -57,14 +59,9 @@ public static void extract(Path source, Path destination) throws IOException { Files.createDirectories(entryPath); } else { try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { - int read = tarArchiveInputStream.read(buffer); - while (read != -1) { - out.write(buffer, 0, read); - read = tarArchiveInputStream.read(buffer); - } + ByteStreams.copy(tarArchiveInputStream, out); } } - entry = tarArchiveInputStream.getNextTarEntry(); } } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java index d81abb1630..e899780992 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java @@ -18,6 +18,7 @@ import com.google.common.io.ByteStreams; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -40,8 +41,8 @@ public class ZipUtil { public static void unzip(Path archive, Path destination) throws IOException { String canonicalDestination = destination.toFile().getCanonicalPath(); - try (InputStream fileIn = Files.newInputStream(archive); - ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(fileIn))) { + try (InputStream fileIn = new BufferedInputStream(Files.newInputStream(archive)); + ZipInputStream zipIn = new ZipInputStream(fileIn)) { for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { Path entryPath = destination.resolve(entry.getName()); @@ -55,7 +56,7 @@ public static void unzip(Path archive, Path destination) throws IOException { if (entry.isDirectory()) { Files.createDirectories(entryPath); } else { - try (OutputStream out = Files.newOutputStream(entryPath)) { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { ByteStreams.copy(zipIn, out); } } From 577e442e13481a77ea7c24faf0829448f2fd0db2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 9 Aug 2019 11:14:55 -0400 Subject: [PATCH 0682/2020] Add container creation timestamp configuration (#1888) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../gradle/EmptyProjectIntegrationTest.java | 7 +- ...adleLayerConfigurationIntegrationTest.java | 1 - .../cloud/tools/jib/gradle/JibRunHelper.java | 22 ++++-- .../gradle/SingleProjectIntegrationTest.java | 72 ++++++++++-------- ....gradle => build-timestamps-custom.gradle} | 1 + .../simple/build-usecurrent-deprecated.gradle | 20 +++++ .../build-usecurrent-deprecated2.gradle | 21 ++++++ .../gradle/projects/simple/build.gradle | 2 +- .../projects/simple/complex-build.gradle | 2 +- .../tools/jib/gradle/BuildDockerTask.java | 9 +++ .../tools/jib/gradle/BuildImageTask.java | 9 +++ .../cloud/tools/jib/gradle/BuildTarTask.java | 9 +++ .../tools/jib/gradle/ContainerParameters.java | 14 ++++ .../jib/gradle/GradleRawConfiguration.java | 5 ++ .../cloud/tools/jib/gradle/TaskCommon.java | 11 +++ .../tools/jib/gradle/JibExtensionTest.java | 1 + .../tools/jib/gradle/TaskCommonTest.java | 28 +++++++ jib-maven-plugin/CHANGELOG.md | 2 + .../tools/jib/maven/BuildDockerMojo.java | 11 +++ .../cloud/tools/jib/maven/BuildImageMojo.java | 11 +++ .../cloud/tools/jib/maven/BuildTarMojo.java | 11 +++ .../jib/maven/JibPluginConfiguration.java | 16 ++++ .../jib/maven/MavenRawConfiguration.java | 5 ++ .../cloud/tools/jib/maven/MojoCommon.java | 16 ++++ .../maven/BuildDockerMojoIntegrationTest.java | 10 +-- .../maven/BuildImageMojoIntegrationTest.java | 56 +++++++++++--- .../maven/BuildTarMojoIntegrationTest.java | 17 +---- .../jib/maven/JibPluginConfigurationTest.java | 9 ++- .../maven/projects/simple/pom-complex.xml | 2 +- ...e-custom.xml => pom-timestamps-custom.xml} | 2 +- .../simple/pom-usecurrent-deprecated.xml | 52 +++++++++++++ .../simple/pom-usecurrent-deprecated2.xml | 53 +++++++++++++ .../resources/maven/projects/simple/pom.xml | 2 +- .../common/InvalidCreationTimeException.java | 35 +++++++++ ...InvalidFilesModificationTimeException.java | 1 + .../common/PluginConfigurationProcessor.java | 52 ++++++++++++- .../jib/plugins/common/PropertyNames.java | 15 ++-- .../jib/plugins/common/RawConfiguration.java | 3 + .../PluginConfigurationProcessorTest.java | 75 ++++++++++++++----- 40 files changed, 584 insertions(+), 108 deletions(-) rename jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/{build-files-modification-time-custom.gradle => build-timestamps-custom.gradle} (85%) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle rename jib-maven-plugin/src/test/resources/maven/projects/simple/{pom-complex-files-modification-time-custom.xml => pom-timestamps-custom.xml} (97%) create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidCreationTimeException.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 68ccd2831e..4af7c7ad99 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,10 +6,12 @@ All notable changes to this project will be documented in this file. ### Added - Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ### Changed - When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `jib.container.useCurrentTimestamp` has been deprecated in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java index db839e9e0e..e35c3e0aca 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import java.io.IOException; import java.security.DigestException; +import java.time.Instant; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.ClassRule; @@ -67,7 +68,7 @@ public void testBuild_empty() throws IOException, InterruptedException, DigestEx + System.nanoTime(); Assert.assertEquals("", JibRunHelper.buildAndRun(emptyTestProject, targetImage)); assertDockerInspect(targetImage); - JibRunHelper.assertCreationTimeEpoch(targetImage); + JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); } @Test @@ -87,9 +88,7 @@ public void testDockerDaemon_empty() throws IOException, InterruptedException, D String targetImage = "emptyimage:gradle" + System.nanoTime(); Assert.assertEquals( "", JibRunHelper.buildToDockerDaemonAndRun(emptyTestProject, targetImage, "build.gradle")); - Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); + JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); assertDockerInspect(targetImage); } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java index 26eea41970..250c6d72d6 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java @@ -53,7 +53,6 @@ public void testGradleLayerConfiguration_multiModule() throws IOException { // snapshot dependencies (4) // dependencies (5) - Path complexServiceRoot = multiTestProject.getProjectRoot().resolve("complex-service"); // verify dependencies List dependencies = layers.get(5); List expectedDependencies = diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 17e909a05f..8db0811e48 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.DigestException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -87,8 +88,8 @@ static void buildAndRunAdditionalTag( Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(imageReference)); Assert.assertEquals(expectedOutput, pullAndRunBuiltImage(additionalImageReference)); - assertCreationTimeEpoch(imageReference); - assertCreationTimeEpoch(additionalImageReference); + assertSimpleCreationTimeIsEqual(Instant.EPOCH, imageReference); + assertSimpleCreationTimeIsEqual(Instant.EPOCH, additionalImageReference); } static BuildResult buildToDockerDaemon( @@ -138,11 +139,20 @@ static void assertBuildSuccess(BuildResult buildResult, String taskName, String Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(successMessage)); } - static void assertCreationTimeEpoch(String imageReference) + static void assertSimpleCreationTimeIsEqual(Instant match, String imageReference) throws IOException, InterruptedException { - Assert.assertEquals( - "1970-01-01T00:00:00Z", - new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); + Instant parsed = Instant.parse(inspect); + Assert.assertEquals(match, parsed); + } + + static void assertSimpleCreationTimeIsAfter(Instant before, String imageReference) + throws IOException, InterruptedException { + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); + Instant parsed = Instant.parse(inspect); + Assert.assertTrue(parsed.isAfter(before)); } static void assertImageDigestAndId(Path projectRoot) throws IOException, DigestException { diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 6ed601e0ab..a01bc3c9b1 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -52,24 +52,6 @@ private static boolean isJava11RuntimeOrHigher() { return Integer.valueOf(split.iterator().next()) >= 11; } - /** - * Asserts that the creation time of the simple test project is set. If the time parsed from the - * {@code docker inspect} command occurs before the specified time (i.e. if it is 1970), then the - * assertion will fail. - * - * @param before the specified time to compare the resulting image's creation time to - * @param imageReference the image to test - * @throws IOException if the {@code docker inspect} command fails to run - * @throws InterruptedException if the {@code docker inspect} command is interrupted - */ - private static void assertSimpleCreationTimeIsAfter(Instant before, String imageReference) - throws IOException, InterruptedException { - String inspect = - new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); - Instant parsed = Instant.parse(inspect); - Assert.assertTrue(parsed.isAfter(before) || parsed.equals(before)); - } - private static void assertWorkingDirectory(String expected, String imageReference) throws IOException, InterruptedException { Assert.assertEquals( @@ -131,11 +113,11 @@ private static void assertDockerInspect(String imageReference) + " }")); } - private static void assertExtraDirectoryDeprecationWarning(String pomXml) + private static void assertExtraDirectoryDeprecationWarning(String buildFile) throws DigestException, IOException, InterruptedException { String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); BuildResult buildResult = - JibRunHelper.buildToDockerDaemon(simpleTestProject, targetImage, pomXml); + JibRunHelper.buildToDockerDaemon(simpleTestProject, targetImage, buildFile); Assert.assertEquals( "Hello, world. \n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", @@ -209,13 +191,12 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE "No classes files were found - did you compile your project?")); } - Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", JibRunHelper.buildAndRun(simpleTestProject, targetImage)); assertDockerInspect(targetImage); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); assertWorkingDirectory("/home", targetImage); assertEntrypoint( "[java -cp /d1:/d2:/app/resources:/app/classes:/app/libs/* com.test.HelloWorld]", @@ -326,7 +307,7 @@ public void testBuild_complex() throws IOException, InterruptedException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + JibRunHelper.assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("", targetImage); } @@ -335,19 +316,18 @@ public void testBuild_complex_sameFromAndToRegistry() throws IOException, Interr String targetImage = "localhost:5000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); buildAndRunComplex(targetImage, "testuser", "testpassword", localRegistry1); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + JibRunHelper.assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("", targetImage); } @Test public void testDockerDaemon_simple() throws IOException, InterruptedException, DigestException { String targetImage = "simpleimage:gradle" + System.nanoTime(); - Instant beforeBuild = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", JibRunHelper.buildToDockerDaemonAndRun(simpleTestProject, targetImage, "build.gradle")); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); assertDockerInspect(targetImage); assertWorkingDirectory("/home", targetImage); } @@ -379,13 +359,46 @@ public void testBuild_skipDownloadingBaseImageLayers() throws IOException, Inter } @Test - public void testDockerDaemon_filesModificationTimeCustom() + public void testDockerDaemon_timestampCustom() throws DigestException, IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); Assert.assertEquals( "Hello, world. \n2011-12-03T01:15:30Z\n", JibRunHelper.buildToDockerDaemonAndRun( - simpleTestProject, targetImage, "build-files-modification-time-custom.gradle")); + simpleTestProject, targetImage, "build-timestamps-custom.gradle")); + JibRunHelper.assertSimpleCreationTimeIsEqual( + Instant.parse("2013-11-04T21:29:30Z"), targetImage); + } + + @Test + public void testDockerDaemon_timestampDeprecated() + throws DigestException, IOException, InterruptedException { + Instant beforeBuild = Instant.now(); + String targetImage = "simpleimage:gradle" + System.nanoTime(); + BuildResult buildResult = + JibRunHelper.buildToDockerDaemon( + simpleTestProject, targetImage, "build-usecurrent-deprecated.gradle"); + JibRunHelper.assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + Assert.assertThat( + buildResult.getOutput(), + CoreMatchers.containsString( + "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' with the value 'USE_CURRENT_TIMESTAMP' instead")); + } + + @Test + public void testDockerDaemon_timestampFail() + throws InterruptedException, IOException, DigestException { + try { + String targetImage = "simpleimage:gradle" + System.nanoTime(); + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-usecurrent-deprecated2.gradle"); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "You cannot configure both 'jib.container.useCurrentTimestamp' and 'jib.container.creationTime'")); + } } @Test @@ -420,7 +433,6 @@ public void testBuildTar_simple() throws IOException, InterruptedException { String outputPath = simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); - Instant beforeBuild = Instant.now(); BuildResult buildResult = simpleTestProject.build( "clean", @@ -438,7 +450,7 @@ public void testBuildTar_simple() throws IOException, InterruptedException { + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", new Command("docker", "run", "--rm", targetImage).run()); assertDockerInspect(targetImage); - assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); + JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); assertWorkingDirectory("/home", targetImage); } } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-files-modification-time-custom.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle similarity index 85% rename from jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-files-modification-time-custom.gradle rename to jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle index 2a9fd1340a..6fc9bb42a4 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-files-modification-time-custom.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle @@ -17,4 +17,5 @@ dependencies { jib { to.image = System.getProperty("_TARGET_IMAGE") container.filesModificationTime = '2011-12-03T10:15:30+09:00' + container.creationTime = '2013-11-05T06:29:30+09:00' } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle new file mode 100644 index 0000000000..919c215900 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + to.image = System.getProperty("_TARGET_IMAGE") + container.useCurrentTimestamp = true +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle new file mode 100644 index 0000000000..092247c19b --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + to.image = System.getProperty("_TARGET_IMAGE") + container.useCurrentTimestamp = true + container.creationTime = 'USE_CURRENT_TIMESTAMP' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle index 84b461ad2a..4c9914c010 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle @@ -20,7 +20,7 @@ jib { credHelper = 'gcr' } container { - useCurrentTimestamp = true + creationTime = 'EPOCH' args = ['An argument.'] ports = ['1000/tcp', '2000-2003/udp'] labels = [key1:'value1', key2:'value2'] diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle index de303ff081..4d7ad8c00c 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle @@ -30,7 +30,7 @@ jib { } } container { - useCurrentTimestamp = true + creationTime = 'USE_CURRENT_TIMESTAMP' args = ['An argument.'] mainClass = 'com.test.HelloWorld' jvmFlags = ['-Xms512m', '-Xdebug'] diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index b131b9df19..bee61893c7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -172,6 +173,14 @@ public void buildDocker() + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index ef55b10451..cf16d06939 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -152,6 +153,14 @@ public void buildImage() + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 179b93d21f..f7612d523b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -170,6 +171,14 @@ public void buildTar() + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new GradleException( + "container.creationTime should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index d9db45e31e..21d1831e28 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -48,6 +48,7 @@ public class ContainerParameters { @Nullable private String user; @Nullable private String workingDirectory; private String filesModificationTime = "EPOCH_PLUS_SECOND"; + private String creationTime = "EPOCH"; @Input @Optional @@ -261,4 +262,17 @@ public String getFilesModificationTime() { public void setFilesModificationTime(String filesModificationTime) { this.filesModificationTime = filesModificationTime; } + + @Input + @Optional + public String getCreationTime() { + if (System.getProperty(PropertyNames.CONTAINER_CREATION_TIME) != null) { + return System.getProperty(PropertyNames.CONTAINER_CREATION_TIME); + } + return creationTime; + } + + public void setCreationTime(String creationTime) { + this.creationTime = creationTime; + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index bcbf1786bf..d5517d7744 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -156,6 +156,11 @@ public String getFilesModificationTime() { return jibExtension.getContainer().getFilesModificationTime(); } + @Override + public String getCreationTime() { + return jibExtension.getContainer().getCreationTime(); + } + @Override public List getExtraDirectories() { return jibExtension.getExtraDirectories().getPaths(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index b4f95db67e..27b084dc98 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -89,6 +89,17 @@ static void checkDeprecatedUsage(JibExtension jibExtension, Logger logger) { "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); } } + + if (jibExtension.getContainer().getUseCurrentTimestamp()) { + if (!jibExtension.getContainer().getCreationTime().equals("EPOCH")) { + throw new IllegalArgumentException( + "You cannot configure both 'jib.container.useCurrentTimestamp' and " + + "'jib.container.creationTime'"); + } + logger.warn( + "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' " + + "with the value 'USE_CURRENT_TIMESTAMP' instead"); + } } /** diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 62411e64de..24cb345a82 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -101,6 +101,7 @@ public void testContainer() { Assert.assertEquals("", testJibExtension.getContainer().getAppRoot()); Assert.assertEquals( "EPOCH_PLUS_SECOND", testJibExtension.getContainer().getFilesModificationTime()); + Assert.assertEquals("EPOCH", testJibExtension.getContainer().getCreationTime()); testJibExtension.container( container -> { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 203da7d672..a8ab91f92e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -41,6 +41,7 @@ public class TaskCommonTest { @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @Mock private JibExtension jibExtension; + @Mock private ContainerParameters containerParameters; @Mock private Logger logger; @Before @@ -49,6 +50,7 @@ public void setUp() { System.clearProperty("jib.extraDirectory.permissions"); System.clearProperty("jib.extraDirectories.paths"); System.clearProperty("jib.extraDirectories.permissions"); + Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); } @Test @@ -126,6 +128,32 @@ public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesConfigured } } + @Test + public void testCheckDeprecatedUsage_useCurrentTimestampConfigured() { + Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); + Mockito.when(containerParameters.getCreationTime()).thenReturn("EPOCH"); + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Mockito.verify(logger) + .warn( + "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' " + + "with the value 'USE_CURRENT_TIMESTAMP' instead"); + } + + @Test + public void testCheckDeprecatedUsage_useCurrentTimestampAndCreationTimeConfigured() { + Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); + Mockito.when(containerParameters.getCreationTime()).thenReturn("USE_CURRENT_TIMESTAMP"); + try { + TaskCommon.checkDeprecatedUsage(jibExtension, logger); + Assert.fail(); + } catch (IllegalArgumentException ex) { + Assert.assertEquals( + "You cannot configure both 'jib.container.useCurrentTimestamp' and " + + "'jib.container.creationTime'", + ex.getMessage()); + } + } + @Test public void testGetWarTask_normalJavaProject() { Project project = ProjectBuilder.builder().build(); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 92042d7524..70ec67d215 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,10 +6,12 @@ All notable changes to this project will be documented in this file. ### Added - Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ### Changed - When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `` has been deprecated in favor of `` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index ee8ee77276..52a79a1dd1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -97,6 +98,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } + MojoCommon.checkUseCurrentTimestampDeprecation(this); + try { RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); MavenProjectProperties projectProperties = @@ -170,6 +173,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 2f0271a6c9..1f5c82e248 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -26,6 +26,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -73,6 +74,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } + MojoCommon.checkUseCurrentTimestampDeprecation(this); + // Validates 'format'. if (Arrays.stream(ImageFormat.values()).noneMatch(value -> value.name().equals(getFormat()))) { throw new MojoFailureException( @@ -164,6 +167,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a1998e11f3..a30d47de21 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerVolumeException; import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; @@ -71,6 +72,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } + MojoCommon.checkUseCurrentTimestampDeprecation(this); + try { RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); MavenProjectProperties projectProperties = @@ -142,6 +145,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { + ex.getInvalidFilesModificationTime(), ex); + } catch (InvalidCreationTimeException ex) { + throw new MojoExecutionException( + " should be an ISO 8601 date-time (see " + + "DateTimeFormatter.ISO_DATE_TIME) or a special keyword (\"EPOCH\", " + + "\"USE_CURRENT_TIMESTAMP\"): " + + ex.getInvalidCreationTime(), + ex); + } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 5c5ea0ddb1..9f1304e4d6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -186,6 +186,8 @@ public static class ContainerParameters { @Nullable @Parameter private String workingDirectory; @Parameter private String filesModificationTime = "EPOCH_PLUS_SECOND"; + + @Parameter private String creationTime = "EPOCH"; } /** Configuration for the {@code extraDirectories} parameter. */ @@ -363,6 +365,7 @@ AuthConfiguration getTargetImageAuth() { * * @return {@code true} if the build should use the current timestamp, {@code false} if not */ + @Deprecated boolean getUseCurrentTimestamp() { String property = getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); if (property != null) { @@ -558,6 +561,19 @@ String getFilesModificationTime() { return container.filesModificationTime; } + /** + * Gets the configured container creation time value. + * + * @return the configured container creation time value + */ + String getCreationTime() { + String property = getProperty(PropertyNames.CONTAINER_CREATION_TIME); + if (property != null) { + return property; + } + return container.creationTime; + } + /** * Gets the list of configured extra directory paths. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 913c9c89fa..d08649986e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -161,6 +161,11 @@ public String getFilesModificationTime() { return jibPluginConfiguration.getFilesModificationTime(); } + @Override + public String getCreationTime() { + return jibPluginConfiguration.getCreationTime(); + } + @Override public List getExtraDirectories() { return MojoCommon.getExtraDirectories(jibPluginConfiguration); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 88d741dd42..62a820e557 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -38,6 +38,22 @@ public class MojoCommon { @VisibleForTesting public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; + @Deprecated + static void checkUseCurrentTimestampDeprecation(JibPluginConfiguration jibPluginConfiguration) { + if (jibPluginConfiguration.getUseCurrentTimestamp()) { + if (!jibPluginConfiguration.getCreationTime().equals("EPOCH")) { + throw new IllegalArgumentException( + "You cannot configure both and " + + ""); + } + jibPluginConfiguration + .getLog() + .warn( + " is deprecated; use with " + + "the value USE_CURRENT_TIMESTAMP instead"); + } + } + /** * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code * (project dir)/src/main/jib} by default if not configured. diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 06a3515bd0..61e1b8e88f 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.nio.file.Path; import java.security.DigestException; -import java.time.Instant; import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; @@ -101,15 +100,13 @@ public void testExecute_simple() throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", buildToDockerDaemonAndRun(simpleTestProject.getProjectRoot(), targetImage)); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test @@ -231,7 +228,6 @@ public void testExecute_jarContainerizationOnMissingJar() throws IOException { public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); // this plugin should match 1.0 verifier.setSystemProperty("jib.requiredVersion", "1.0"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 84851ac8a9..3fab07385e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -202,7 +202,6 @@ private static String buildAndRunComplex( LocalRegistry targetRegistry, String pomFile) throws VerificationException, IOException, InterruptedException, DigestException { - Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); @@ -220,10 +219,6 @@ private static String buildAndRunComplex( // Verify output targetRegistry.pull(imageReference); assertDockerInspectParameters(imageReference); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); return new Command("docker", "run", "--rm", imageReference).run(); } @@ -289,6 +284,14 @@ private static void assertCreationTimeEpoch(String imageReference) new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim()); } + private static void assertCreationTimeIsAfter(Instant before, String imageReference) + throws IOException, InterruptedException { + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", imageReference).run().trim(); + Instant after = Instant.parse(inspect); + Assert.assertTrue(after.isAfter(before)); + } + private static void assertWorkingDirectory(String expected, String imageReference) throws IOException, InterruptedException { Assert.assertEquals( @@ -374,10 +377,7 @@ public void testExecute_simple() + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", buildAndRun(simpleTestProject.getProjectRoot(), targetImage, "pom.xml", true)); - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + assertCreationTimeEpoch(targetImage); assertWorkingDirectory("/home", targetImage); assertLayerSize(8, targetImage); } @@ -511,12 +511,14 @@ public void testExecute_defaultTarget() throws IOException { public void testExecute_complex() throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); + Instant before = Instant.now(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", buildAndRunComplex( targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml")); + assertCreationTimeIsAfter(before, targetImage); assertWorkingDirectory("", targetImage); assertEntrypoint( "[java -Xms512m -Xdebug -cp /other:/app/resources:/app/classes:/app/libs/* " @@ -525,14 +527,46 @@ public void testExecute_complex() } @Test - public void testExecute_filesModificationTimeCustom() + public void testExecute_timestampCustom() throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:6000/simpleimage:maven" + System.nanoTime(); - String pom = "pom-complex-files-modification-time-custom.xml"; + String pom = "pom-timestamps-custom.xml"; Assert.assertEquals( "Hello, world. \n2019-06-17T16:30:00Z\nrw-r--r--\nrw-r--r--\n" + "foo\ncat\n2019-06-17T16:30:00Z\n2019-06-17T16:30:00Z\n", buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2, pom)); + + String inspect = + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim(); + Instant parsed = Instant.parse(inspect); + Assert.assertEquals(Instant.parse("2013-11-05T06:29:30Z"), parsed); + } + + @Test + public void testDockerDaemon_timestampDeprecated() + throws IOException, VerificationException, InterruptedException { + Instant before = Instant.now(); + String targetImage = getTestImageReference("simpleimage:gradle" + System.nanoTime()); + build(simpleTestProject.getProjectRoot(), targetImage, "pom-usecurrent-deprecated.xml", false) + .verifyTextInLog( + " is deprecated; use with the value USE_CURRENT_TIMESTAMP instead"); + new Command("docker", "pull", targetImage).run(); + assertCreationTimeIsAfter(before, targetImage); + } + + @Test + public void testDockerDaemon_timestampFail() throws IOException { + try { + String targetImage = getTestImageReference("simpleimage:gradle" + System.nanoTime()); + build( + simpleTestProject.getProjectRoot(), targetImage, "pom-usecurrent-deprecated2.xml", false); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.containsString( + "You cannot configure both and ")); + } } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 19f231e98f..03643d68d4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.Command; import java.io.IOException; import java.security.DigestException; -import java.time.Instant; import java.util.Arrays; import org.apache.maven.it.VerificationException; import org.apache.maven.it.Verifier; @@ -38,18 +37,11 @@ public class BuildTarMojoIntegrationTest { @ClassRule public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); - /** - * Builds and runs jib:buildTar on a project at {@code projectRoot} pushing to {@code - * imageReference}. - * - * @throws DigestException - */ @Test public void testExecute_simple() throws VerificationException, IOException, InterruptedException, DigestException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); @@ -74,11 +66,9 @@ public void testExecute_simple() Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", new Command("docker", "run", "--rm", targetImage).run()); - - Instant buildTime = - Instant.parse( - new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); - Assert.assertTrue(buildTime.isAfter(before) || buildTime.equals(before)); + Assert.assertEquals( + "1970-01-01T00:00:00Z", + new Command("docker", "inspect", "-f", "{{.Created}}", targetImage).run().trim()); } @Test @@ -95,7 +85,6 @@ public void testExecute_jibContainerizeSkips() throws VerificationException, IOE public void testExecute_jibRequireVersion_ok() throws VerificationException, IOException { String targetImage = "simpleimage:maven" + System.nanoTime(); - Instant before = Instant.now(); Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); // this plugin should match 1.0 verifier.setSystemProperty("jib.requiredVersion", "1.0"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 1457b52e85..14267a1c20 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -70,6 +70,7 @@ public void testDefaults() { Assert.assertTrue(testPluginConfiguration.getExtraClasspath().isEmpty()); Assert.assertEquals("exploded", testPluginConfiguration.getContainerizingMode()); Assert.assertEquals("EPOCH_PLUS_SECOND", testPluginConfiguration.getFilesModificationTime()); + Assert.assertEquals("EPOCH", testPluginConfiguration.getCreationTime()); } @Test @@ -123,6 +124,8 @@ public void testSystemProperties() { Assert.assertEquals("/working/directory", testPluginConfiguration.getWorkingDirectory()); sessionProperties.put("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getFilesModificationTime()); + sessionProperties.put("jib.container.creationTime", "2011-12-03T22:42:05Z"); + Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getCreationTime()); sessionProperties.put("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); @@ -194,6 +197,8 @@ public void testPomProperties() { .getProperties() .setProperty("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getFilesModificationTime()); + project.getProperties().setProperty("jib.container.creationTime", "2011-12-03T22:42:05Z"); + Assert.assertEquals("2011-12-03T22:42:05Z", testPluginConfiguration.getCreationTime()); project.getProperties().setProperty("jib.container.extraClasspath", "/foo,/bar"); Assert.assertEquals( ImmutableList.of("/foo", "/bar"), testPluginConfiguration.getExtraClasspath()); @@ -228,7 +233,7 @@ public void testEmptyOrNullTags() { } @Test - public void testDeprecatedSystemProperties() { + public void testDeprecatedSystemProperties_extraDirectory() { sessionProperties.put("jib.extraDirectory.path", "custom-jib"); Assert.assertEquals( Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); @@ -247,7 +252,7 @@ public void testDeprecatedSystemProperties() { } @Test - public void testDeprecatedProperties() { + public void testDeprecatedProperties_extraDirectory() { Properties projectProperties = project.getProperties(); projectProperties.setProperty("jib.extraDirectory.path", "this-is-extra"); diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml index 6c3a379ef3..82610e9c8e 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml @@ -54,7 +54,7 @@ - true + USE_CURRENT_TIMESTAMP An argument. diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml similarity index 97% rename from jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml rename to jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml index 02acd783bd..e9928e11d8 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-files-modification-time-custom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml @@ -54,7 +54,6 @@ - true 1000/tcp 2000-2003/udp @@ -64,6 +63,7 @@ value2 2019-06-17T16:30:00Z + 2013-11-05T06:29:30Z src/main/jib-custom diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml new file mode 100644 index 0000000000..a666d0bebd --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + true + + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml new file mode 100644 index 0000000000..1220d24bb8 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + true + USE_CURRENT_TIMESTAMP + + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml index 6d6776aa38..9847100fa3 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml @@ -43,7 +43,7 @@ ${_TARGET_IMAGE} - true + EPOCH An argument. 1000/tcp diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidCreationTimeException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidCreationTimeException.java new file mode 100644 index 0000000000..f24ca19583 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidCreationTimeException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import java.time.format.DateTimeParseException; + +/** Exception when an invalid container creation timestamp configuration is encountered. */ +public class InvalidCreationTimeException extends Exception { + + private final String invalidValue; + + public InvalidCreationTimeException( + String message, String invalidValue, DateTimeParseException ex) { + super(message, ex); + this.invalidValue = invalidValue; + } + + public String getInvalidCreationTime() { + return invalidValue; + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java index 421f328eb0..392fd1098b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/InvalidFilesModificationTimeException.java @@ -18,6 +18,7 @@ import java.time.format.DateTimeParseException; +/** Exception when an invalid file timestamp configuration is encountered. */ public class InvalidFilesModificationTimeException extends Exception { private final String invalidValue; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 17e7889a93..64d6f97d8a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -67,7 +67,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); @@ -97,7 +98,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); @@ -119,7 +121,8 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { Preconditions.checkArgument(rawConfiguration.getToImage().isPresent()); ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); @@ -160,7 +163,8 @@ static PluginConfigurationProcessor processCommonConfiguration( throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); @@ -212,6 +216,9 @@ static PluginConfigurationProcessor processCommonConfiguration( LogEvent.warn( "Setting image creation time to current time; your image may not be reproducible.")); jibContainerBuilder.setCreationTime(Instant.now()); + } else { + jibContainerBuilder.setCreationTime( + getCreationTime(rawConfiguration.getCreationTime(), projectProperties)); } // Adds all the extra files. @@ -474,6 +481,43 @@ static BiFunction createModificationTimeProvide } } + /** + * Creates an {@link Instant} based on the config value. The value can be: + * + *
    + *
  1. {@code EPOCH} to return epoch + *
  2. {@code USE_CURRENT_TIMESTAMP} to return the current time + *
  3. date in ISO 8601 format + *
+ * + * @param configuredCreationTime the config value + * @param projectProperties used for logging warnings + * @return corresponding {@link Instant} + * @throws InvalidCreationTimeException if the config value is invalid + */ + @VisibleForTesting + static Instant getCreationTime(String configuredCreationTime, ProjectProperties projectProperties) + throws DateTimeParseException, InvalidCreationTimeException { + try { + switch (configuredCreationTime) { + case "EPOCH": + return Instant.EPOCH; + + case "USE_CURRENT_TIMESTAMP": + projectProperties.log( + LogEvent.warn( + "Setting image creation time to current time; your image may not be reproducible.")); + return Instant.now(); + + default: + return DateTimeFormatter.ISO_DATE_TIME.parse(configuredCreationTime, Instant::from); + } + + } catch (DateTimeParseException ex) { + throw new InvalidCreationTimeException(configuredCreationTime, configuredCreationTime, ex); + } + } + // TODO: find a way to reduce the number of arguments. private static boolean configureCredentialRetrievers( RawConfiguration rawConfiguration, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 9fe63a276d..132ed6a0ec 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -42,19 +42,14 @@ public class PropertyNames { public static final String CONTAINER_WORKING_DIRECTORY = "jib.container.workingDirectory"; public static final String CONTAINER_VOLUMES = "jib.container.volumes"; public static final String CONTAINER_PORTS = "jib.container.ports"; - public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; public static final String CONTAINER_FILES_MODIFICATION_TIME = "jib.container.filesModificationTime"; + public static final String CONTAINER_CREATION_TIME = "jib.container.creationTime"; public static final String USE_ONLY_PROJECT_CACHE = "jib.useOnlyProjectCache"; public static final String BASE_IMAGE_CACHE = "jib.baseImageCache"; public static final String APPLICATION_CACHE = "jib.applicationCache"; public static final String ALLOW_INSECURE_REGISTRIES = "jib.allowInsecureRegistries"; - @Deprecated public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; public static final String EXTRA_DIRECTORIES_PATHS = "jib.extraDirectories.paths"; - - @Deprecated - public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; - public static final String EXTRA_DIRECTORIES_PERMISSIONS = "jib.extraDirectories.permissions"; public static final String DOCKER_CLIENT_EXECUTABLE = "jib.dockerClient.executable"; public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; @@ -63,5 +58,13 @@ public class PropertyNames { public static final String CONSOLE = "jib.console"; public static final String CONTAINERIZE = "jib.containerize"; + @Deprecated + public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; + + @Deprecated public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; + + @Deprecated + public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; + private PropertyNames() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 92c57636dc..72f308c8e8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -69,6 +69,7 @@ public interface RawConfiguration { Optional getWorkingDirectory(); + @Deprecated boolean getUseCurrentTimestamp(); boolean getAllowInsecureRegistries(); @@ -79,6 +80,8 @@ public interface RawConfiguration { String getFilesModificationTime(); + String getCreationTime(); + List getExtraDirectories(); Map getExtraDirectoryPermissions(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index d32a804441..1c5e42b41d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -101,6 +101,7 @@ public void setUp() throws IOException, InvalidImageReferenceException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); Mockito.when(rawConfiguration.getFilesModificationTime()).thenReturn("EPOCH_PLUS_SECOND"); + Mockito.when(rawConfiguration.getCreationTime()).thenReturn("EPOCH"); Mockito.when(rawConfiguration.getExtraDirectories()) .thenReturn(Arrays.asList(Paths.get("nonexistent/path"))); Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("exploded"); @@ -130,7 +131,7 @@ public void testPluginConfigurationProcessor_defaults() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -154,7 +155,8 @@ public void testPluginConfigurationProcessor_extraDirectory() InvalidAppRootException, IOException, IncompatibleBaseImageJavaVersionException, InvalidWorkingDirectoryException, InvalidImageReferenceException, CacheDirectoryCreationException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { Path extraDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Mockito.when(rawConfiguration.getExtraDirectories()).thenReturn(Arrays.asList(extraDirectory)); Mockito.when(rawConfiguration.getExtraDirectoryPermissions()) @@ -200,7 +202,8 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() throws InvalidContainerVolumeException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidImageReferenceException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); @@ -215,7 +218,8 @@ public void testPluginConfigurationProcessor_warProjectBaseImage() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -232,7 +236,7 @@ public void testEntrypoint() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); @@ -294,7 +298,7 @@ public void testEntrypoint_defaultWarPackaging() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); @@ -313,7 +317,7 @@ public void testEntrypoint_defaultNonWarPackaging() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); @@ -336,7 +340,7 @@ public void testEntrypoint_extraClasspathNonWarPackaging() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getExtraClasspath()) .thenReturn(Collections.singletonList("/foo")); @@ -362,7 +366,7 @@ public void testUser() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -379,7 +383,7 @@ public void testUser_null() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); BuildConfiguration buildConfiguration = getBuildConfiguration(processor.getJibContainerBuilder()); @@ -394,7 +398,7 @@ public void testEntrypoint_warningOnJvmFlags() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); @@ -419,7 +423,7 @@ public void testEntrypoint_warningOnMainclass() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); @@ -444,7 +448,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -468,7 +472,7 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, - InvalidFilesModificationTimeException { + InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); @@ -853,7 +857,7 @@ public void testGetInvalidVolumesList() { } @Test - public void createModificationTimeProvider_epochPlusSecond() + public void testCreateModificationTimeProvider_epochPlusSecond() throws InvalidFilesModificationTimeException { BiFunction timeProvider = PluginConfigurationProcessor.createModificationTimeProvider("EPOCH_PLUS_SECOND"); @@ -863,7 +867,7 @@ public void createModificationTimeProvider_epochPlusSecond() } @Test - public void createModificationTimeProvider_isoDateTimeValue() + public void testCreateModificationTimeProvider_isoDateTimeValue() throws InvalidFilesModificationTimeException { BiFunction timeProvider = PluginConfigurationProcessor.createModificationTimeProvider("2011-12-03T10:15:30+09:00"); @@ -873,7 +877,7 @@ public void createModificationTimeProvider_isoDateTimeValue() } @Test - public void createModificationTimeProvider_invalidValue() { + public void testCreateModificationTimeProvider_invalidValue() { try { BiFunction timeProvider = PluginConfigurationProcessor.createModificationTimeProvider("invalid format"); @@ -885,11 +889,46 @@ public void createModificationTimeProvider_invalidValue() { } } + @Test + public void testGetCreationTime_epoch() throws InvalidCreationTimeException { + Instant time = PluginConfigurationProcessor.getCreationTime("EPOCH", projectProperties); + Assert.assertEquals(Instant.EPOCH, time); + } + + @Test + public void testGetCreationTime_useCurrentTimestamp() throws InvalidCreationTimeException { + Instant now = Instant.now().minusSeconds(2); + Instant time = + PluginConfigurationProcessor.getCreationTime("USE_CURRENT_TIMESTAMP", projectProperties); + Assert.assertTrue(time.isAfter(now)); + } + + @Test + public void testGetCreationTime_isoDateTimeValue() throws InvalidCreationTimeException { + Instant time = + PluginConfigurationProcessor.getCreationTime( + "2011-12-03T10:15:30+09:00", projectProperties); + Instant expected = DateTimeFormatter.ISO_DATE_TIME.parse("2011-12-03T01:15:30Z", Instant::from); + Assert.assertEquals(expected, time); + } + + @Test + public void testGetCreationTime_invalidValue() { + try { + PluginConfigurationProcessor.getCreationTime("invalid format", projectProperties); + Assert.fail(); + } catch (InvalidCreationTimeException ex) { + Assert.assertEquals("invalid format", ex.getMessage()); + Assert.assertEquals("invalid format", ex.getInvalidCreationTime()); + } + } + private PluginConfigurationProcessor createPluginConfigurationProcessor() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException { + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { return PluginConfigurationProcessor.processCommonConfiguration( rawConfiguration, ignored -> Optional.empty(), From dad50adede9771ca0bfac42848f007f806392ac6 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 12 Aug 2019 10:51:13 -0400 Subject: [PATCH 0683/2020] Update FAQ about unauthorized registry error (#1900) --- docs/faq.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 31892950b7..3cc832f571 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -466,14 +466,15 @@ If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is oft * You did not configure auth information in the default places where Jib searches. - `$HOME/.docker/config.json`, [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). - - `$HOME/.docker/config.json` may also contain short-lived authorizations in the `auths` block that may have expired. In the case of Google Container Registry, if you had previously used `gcloud docker` to configure these authorizations, you should remove these stale authorizations by editing your `config.json` and deleting lines from `auths` associated with `gcr.io` (for example: `"https://asia.gcr.io"`). You can then run `gcloud auth configure-docker` to correctly configure the `credHelpers` block for more robust interactions with gcr. - Some common credential helpers on `$PATH` (for example, `docker-credential-osxkeychain`, `docker-credential-ecr-login`, etc.) for well-known registries. - Jib configurations - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) for Maven / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) for Gradle - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) for Maven / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) for Gradle - These parameters can also be set through properties: [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#system-properties) / [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#system-properties) +* `$HOME/.docker/config.json` may also contain short-lived authorizations in the `auths` block that may have expired. In the case of Google Container Registry, if you had previously used `gcloud docker` to configure these authorizations, you should remove these stale authorizations by editing your `config.json` and deleting lines from `auths` associated with `gcr.io` (for example: `"https://asia.gcr.io"`). You can then run `gcloud auth configure-docker` to correctly configure the `credHelpers` block for more robust interactions with gcr. * Different auth configurations exist in multiple places, and Jib is not picking up the auth information you are working on. * You configured a credential helper, but the helper is not on `$PATH`. This is especially common when running Jib inside IDE where the IDE binary is launched directly from an OS menu and does not have access to your shell's environment. +* Configured credentials have access to the base image repository but not to the target image repository (or vice versa). * Typos in username, password, image names, or registry names. * You are using a private registry without HTTPS. See [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). From deb1947faabcb81365a2f83b9d20206348d7b2d8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 12 Aug 2019 14:27:46 -0400 Subject: [PATCH 0684/2020] Simplify unauth handling code (#1899) * Simplify unauth handling code * Revive a test --- .../api/RegistryUnauthorizedException.java | 8 -- .../registry/RegistryEndpointCallerTest.java | 3 +- .../tools/jib/gradle/BuildDockerTask.java | 15 +-- .../tools/jib/gradle/BuildImageTask.java | 11 +-- .../cloud/tools/jib/gradle/BuildTarTask.java | 14 +-- .../jib/gradle/GradleHelpfulSuggestions.java | 27 ++++++ .../GradleHelpfulSuggestionsBuilder.java | 75 -------------- .../tools/jib/maven/BuildDockerMojo.java | 14 +-- .../cloud/tools/jib/maven/BuildImageMojo.java | 11 +-- .../cloud/tools/jib/maven/BuildTarMojo.java | 15 +-- .../jib/maven/MavenHelpfulSuggestions.java | 27 ++++++ .../maven/MavenHelpfulSuggestionsBuilder.java | 83 ---------------- .../ConfigurationPropertyValidator.java | 49 ++++------ .../plugins/common/HelpfulSuggestions.java | 39 +------- .../jib/plugins/common/JibBuildRunner.java | 3 +- .../common/PluginConfigurationProcessor.java | 97 ++++++------------- .../ConfigurationPropertyValidatorTest.java | 11 +-- .../common/HelpfulSuggestionsTest.java | 18 +--- .../plugins/common/JibBuildRunnerTest.java | 17 +--- .../PluginConfigurationProcessorTest.java | 18 +--- 20 files changed, 130 insertions(+), 425 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestions.java delete mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java index 692ff65d75..4a0132143e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java @@ -38,14 +38,6 @@ public RegistryUnauthorizedException( this.repository = repository; } - public String getRegistry() { - return registry; - } - - public String getRepository() { - return repository; - } - public String getImageReference() { return registry + "/" + repository; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 0e50b15164..77b540b0e9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -572,8 +572,7 @@ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) Assert.fail("Call should have failed"); } catch (RegistryUnauthorizedException ex) { - Assert.assertEquals("serverUrl", ex.getRegistry()); - Assert.assertEquals("imageName", ex.getRepository()); + Assert.assertEquals("serverUrl/imageName", ex.getImageReference()); Assert.assertSame(httpResponseException, ex.getHttpResponseException()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index bee61893c7..7a4d7314a6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -110,9 +110,6 @@ public void buildDocker() GradleProjectProperties projectProperties = GradleProjectProperties.getForProject(getProject(), getLogger()); - GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); - PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( gradleRawConfiguration, @@ -120,17 +117,9 @@ public void buildDocker() projectProperties, dockerClientParameters.getExecutablePath(), dockerClientParameters.getEnvironment(), - gradleHelpfulSuggestionsBuilder.build()); + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - HelpfulSuggestions helpfulSuggestions = - gradleHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .build(); - Path buildOutput = getProject().getBuildDir().toPath(); try { @@ -141,7 +130,7 @@ public void buildDocker() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index cf16d06939..401958e9b8 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -101,15 +101,6 @@ public void buildImage() gradleRawConfiguration, ignored -> Optional.empty(), projectProperties); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - HelpfulSuggestions helpfulSuggestions = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension) - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials( - pluginConfigurationProcessor.isTargetImageCredentialPresent()) - .build(); Path buildOutput = getProject().getBuildDir().toPath(); @@ -121,7 +112,7 @@ public void buildImage() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index f7612d523b..5958753f4e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -109,8 +109,6 @@ public void buildTar() RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); GradleProjectProperties projectProperties = GradleProjectProperties.getForProject(getProject(), getLogger()); - GradleHelpfulSuggestionsBuilder gradleHelpfulSuggestionsBuilder = - new GradleHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, jibExtension); Path tarOutputPath = getTargetPath(); PluginConfigurationProcessor pluginConfigurationProcessor = @@ -119,15 +117,7 @@ public void buildTar() ignored -> Optional.empty(), projectProperties, tarOutputPath, - gradleHelpfulSuggestionsBuilder.build()); - - HelpfulSuggestions helpfulSuggestions = - gradleHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(pluginConfigurationProcessor.getTargetImageReference()) - .build(); + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); Path buildOutput = getProject().getBuildDir().toPath(); @@ -139,7 +129,7 @@ public void buildTar() pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java new file mode 100644 index 0000000000..dc6135c080 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; + +/** Gradle-specific {@link HelpfulSuggestions}. */ +class GradleHelpfulSuggestions extends HelpfulSuggestions { + + GradleHelpfulSuggestions(String messagePrefix) { + super(messagePrefix, "gradle clean", "jib.to.image", "--image", "build.gradle"); + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java deleted file mode 100644 index 2694febc2a..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestionsBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import javax.annotation.Nullable; - -/** Builder for Gradle-specific {@link HelpfulSuggestions}. */ -class GradleHelpfulSuggestionsBuilder { - - private final String messagePrefix; - private final JibExtension jibExtension; - - @Nullable private ImageReference baseImageReference; - @Nullable private ImageReference targetImageReference; - private boolean baseImageHasConfiguredCredentials; - private boolean targetImageHasConfiguredCredentials; - - GradleHelpfulSuggestionsBuilder(String messagePrefix, JibExtension jibExtension) { - this.messagePrefix = messagePrefix; - this.jibExtension = jibExtension; - } - - GradleHelpfulSuggestionsBuilder setBaseImageReference(ImageReference baseImageReference) { - this.baseImageReference = baseImageReference; - return this; - } - - GradleHelpfulSuggestionsBuilder setBaseImageHasConfiguredCredentials( - boolean areKnownCredentialsDefined) { - baseImageHasConfiguredCredentials = areKnownCredentialsDefined; - return this; - } - - GradleHelpfulSuggestionsBuilder setTargetImageReference(ImageReference targetImageReference) { - this.targetImageReference = targetImageReference; - return this; - } - - GradleHelpfulSuggestionsBuilder setTargetImageHasConfiguredCredentials( - boolean areKnownCredentialsDefined) { - targetImageHasConfiguredCredentials = areKnownCredentialsDefined; - return this; - } - - HelpfulSuggestions build() { - boolean isCredHelperDefinedForBaseImage = jibExtension.getFrom().getCredHelper() != null; - boolean isCredHelperDefinedForTargetImage = jibExtension.getTo().getCredHelper() != null; - return new HelpfulSuggestions( - messagePrefix, - "gradle clean", - baseImageReference, - !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, - targetImageReference, - !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, - "jib.to.image", - "--image", - "build.gradle"); - } -} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 52a79a1dd1..5691a486f7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -105,9 +105,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); - MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - PluginConfigurationProcessor pluginConfigurationProcessor = PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( mavenRawConfiguration, @@ -116,18 +113,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { projectProperties, dockerExecutable, getDockerClientEnvironment(), - mavenHelpfulSuggestionsBuilder.build()); + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - HelpfulSuggestions helpfulSuggestions = - mavenHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .build(); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); @@ -139,7 +129,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 1f5c82e248..5cbebb0894 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -113,15 +113,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { getSession().getSettings(), getSettingsDecrypter()); ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - HelpfulSuggestions helpfulSuggestions = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this) - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(targetImageReference) - .setTargetImageHasConfiguredCredentials( - pluginConfigurationProcessor.isTargetImageCredentialPresent()) - .build(); Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); @@ -133,7 +124,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index a30d47de21..427e2faea6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -79,9 +79,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); - MavenHelpfulSuggestionsBuilder mavenHelpfulSuggestionsBuilder = - new MavenHelpfulSuggestionsBuilder(HELPFUL_SUGGESTIONS_PREFIX, this); - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); Path tarOutputPath = buildOutput.resolve("jib-image.tar"); PluginConfigurationProcessor pluginConfigurationProcessor = @@ -91,18 +88,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { getSession().getSettings(), getSettingsDecrypter()), projectProperties, tarOutputPath, - mavenHelpfulSuggestionsBuilder.build()); + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); - HelpfulSuggestions helpfulSuggestions = - mavenHelpfulSuggestionsBuilder - .setBaseImageReference(pluginConfigurationProcessor.getBaseImageReference()) - .setBaseImageHasConfiguredCredentials( - pluginConfigurationProcessor.isBaseImageCredentialPresent()) - .setTargetImageReference(pluginConfigurationProcessor.getTargetImageReference()) - .build(); - try { JibBuildRunner.forBuildTar(tarOutputPath) .writeImageDigest(buildOutput.resolve("jib-image.digest")) @@ -111,7 +100,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { pluginConfigurationProcessor.getJibContainerBuilder(), pluginConfigurationProcessor.getContainerizer(), projectProperties::log, - helpfulSuggestions); + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); } finally { // TODO: This should not be called on projectProperties. diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestions.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestions.java new file mode 100644 index 0000000000..70009db14a --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestions.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven; + +import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; + +/** Maven-specific {@link HelpfulSuggestions}. */ +class MavenHelpfulSuggestions extends HelpfulSuggestions { + + MavenHelpfulSuggestions(String messagePrefix) { + super(messagePrefix, "mvn clean", "", "-Dimage", "pom.xml"); + } +} diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java deleted file mode 100644 index 302434d843..0000000000 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenHelpfulSuggestionsBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; -import javax.annotation.Nullable; - -/** Builder for Maven-specific {@link HelpfulSuggestions}. */ -class MavenHelpfulSuggestionsBuilder { - - private final String messagePrefix; - private final JibPluginConfiguration jibPluginConfiguration; - - @Nullable private ImageReference baseImageReference; - @Nullable private ImageReference targetImageReference; - private boolean baseImageHasConfiguredCredentials; - private boolean targetImageHasConfiguredCredentials; - - MavenHelpfulSuggestionsBuilder( - String messagePrefix, JibPluginConfiguration jibPluginConfiguration) { - this.messagePrefix = messagePrefix; - this.jibPluginConfiguration = jibPluginConfiguration; - } - - MavenHelpfulSuggestionsBuilder setBaseImageReference(ImageReference baseImageReference) { - this.baseImageReference = baseImageReference; - return this; - } - - MavenHelpfulSuggestionsBuilder setBaseImageHasConfiguredCredentials( - boolean areKnownCredentialsDefined) { - baseImageHasConfiguredCredentials = areKnownCredentialsDefined; - return this; - } - - MavenHelpfulSuggestionsBuilder setTargetImageReference(ImageReference targetImageReference) { - this.targetImageReference = targetImageReference; - return this; - } - - MavenHelpfulSuggestionsBuilder setTargetImageHasConfiguredCredentials( - boolean areKnownCredentialsDefined) { - targetImageHasConfiguredCredentials = areKnownCredentialsDefined; - return this; - } - - /** - * Builds the {@link HelpfulSuggestions}. - * - * @return the {@link HelpfulSuggestions} - */ - HelpfulSuggestions build() { - boolean isCredHelperDefinedForBaseImage = - jibPluginConfiguration.getTargetImageCredentialHelperName() != null; - boolean isCredHelperDefinedForTargetImage = - jibPluginConfiguration.getTargetImageCredentialHelperName() != null; - return new HelpfulSuggestions( - messagePrefix, - "mvn clean", - baseImageReference, - !isCredHelperDefinedForBaseImage && !baseImageHasConfiguredCredentials, - targetImageReference, - !isCredHelperDefinedForTargetImage && !targetImageHasConfiguredCredentials, - "", - "-Dimage", - "pom.xml"); - } -} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index 4cd2e7e4a4..e5173f1374 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -57,51 +57,38 @@ public static Optional getImageCredential( AuthProperty auth, RawConfiguration rawConfiguration) { // System property takes priority over build configuration - String commandlineUsername = rawConfiguration.getProperty(usernameProperty).orElse(null); - String commandlinePassword = rawConfiguration.getProperty(passwordProperty).orElse(null); - if (!Strings.isNullOrEmpty(commandlineUsername) - && !Strings.isNullOrEmpty(commandlinePassword)) { + String commandlineUsername = rawConfiguration.getProperty(usernameProperty).orElse(""); + String commandlinePassword = rawConfiguration.getProperty(passwordProperty).orElse(""); + if (!commandlineUsername.isEmpty() && !commandlinePassword.isEmpty()) { return Optional.of(Credential.from(commandlineUsername, commandlinePassword)); } // Warn if a system property is missing - if (!Strings.isNullOrEmpty(commandlinePassword) && Strings.isNullOrEmpty(commandlineUsername)) { + String missingProperty = + "%s system property is set, but %s is not; attempting other authentication methods."; + if (!commandlinePassword.isEmpty() && commandlineUsername.isEmpty()) { logger.accept( - LogEvent.warn( - passwordProperty - + " system property is set, but " - + usernameProperty - + " is not; attempting other authentication methods.")); + LogEvent.warn(String.format(missingProperty, passwordProperty, usernameProperty))); } - if (!Strings.isNullOrEmpty(commandlineUsername) && Strings.isNullOrEmpty(commandlinePassword)) { + if (!commandlineUsername.isEmpty() && commandlinePassword.isEmpty()) { logger.accept( - LogEvent.warn( - usernameProperty - + " system property is set, but " - + passwordProperty - + " is not; attempting other authentication methods.")); + LogEvent.warn(String.format(missingProperty, usernameProperty, passwordProperty))); } // Check auth configuration next; warn if they aren't both set - if (Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { - return Optional.empty(); + if (!Strings.isNullOrEmpty(auth.getUsername()) && !Strings.isNullOrEmpty(auth.getPassword())) { + return Optional.of(Credential.from(auth.getUsername(), auth.getPassword())); } - if (Strings.isNullOrEmpty(auth.getUsername())) { - logger.accept( - LogEvent.warn( - auth.getUsernameDescriptor() - + " is missing from build configuration; ignoring auth section.")); - return Optional.empty(); + + String missingConfig = "%s is missing from build configuration; ignoring auth section."; + if (!Strings.isNullOrEmpty(auth.getPassword()) && Strings.isNullOrEmpty(auth.getUsername())) { + logger.accept(LogEvent.warn(String.format(missingConfig, auth.getUsernameDescriptor()))); } - if (Strings.isNullOrEmpty(auth.getPassword())) { - logger.accept( - LogEvent.warn( - auth.getPasswordDescriptor() - + " is missing from build configuration; ignoring auth section.")); - return Optional.empty(); + if (!Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + logger.accept(LogEvent.warn(String.format(missingConfig, auth.getPasswordDescriptor()))); } - return Optional.of(Credential.from(auth.getUsername(), auth.getPassword())); + return Optional.empty(); } /** diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 797ef6778c..10a985c2c6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -16,10 +16,7 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.api.ImageReference; -import com.google.common.base.Preconditions; import java.nio.file.Path; -import javax.annotation.Nullable; /** Builds messages that provides suggestions on how to fix the error. */ public class HelpfulSuggestions { @@ -107,10 +104,6 @@ private static String forIncompatibleBaseImageJavaVesion( private final String messagePrefix; private final String clearCacheCommand; - @Nullable private final ImageReference baseImageReference; - private final boolean noCredentialsDefinedForBaseImage; - @Nullable private final ImageReference targetImageReference; - private final boolean noCredentialsDefinedForTargetImage; private final String toImageConfiguration; private final String buildConfigurationFilename; private final String toImageFlag; @@ -120,12 +113,6 @@ private static String forIncompatibleBaseImageJavaVesion( * * @param messagePrefix the initial message text * @param clearCacheCommand the command for clearing the cache - * @param baseImageReference the base image reference - * @param noCredentialsDefinedForBaseImage {@code true} if no credentials were defined for the - * base image; {@code false} otherwise - * @param targetImageReference the target image reference - * @param noCredentialsDefinedForTargetImage {@code true} if no credentials were defined for the - * target image; {@code false} otherwise * @param toImageConfiguration the configuration defining the target image * @param toImageFlag the commandline flag used to set the target image * @param buildConfigurationFilename the filename of the build configuration @@ -133,19 +120,11 @@ private static String forIncompatibleBaseImageJavaVesion( public HelpfulSuggestions( String messagePrefix, String clearCacheCommand, - @Nullable ImageReference baseImageReference, - boolean noCredentialsDefinedForBaseImage, - @Nullable ImageReference targetImageReference, - boolean noCredentialsDefinedForTargetImage, String toImageConfiguration, String toImageFlag, String buildConfigurationFilename) { this.messagePrefix = messagePrefix; this.clearCacheCommand = clearCacheCommand; - this.baseImageReference = baseImageReference; - this.noCredentialsDefinedForBaseImage = noCredentialsDefinedForBaseImage; - this.targetImageReference = targetImageReference; - this.noCredentialsDefinedForTargetImage = noCredentialsDefinedForTargetImage; this.toImageConfiguration = toImageConfiguration; this.buildConfigurationFilename = buildConfigurationFilename; this.toImageFlag = toImageFlag; @@ -179,24 +158,10 @@ public String forHttpStatusCodeForbidden(String imageReference) { + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied for help"); } - public String forNoCredentialsDefined(String registry, String repository) { - Preconditions.checkNotNull(baseImageReference); - Preconditions.checkNotNull(targetImageReference); - - String unauthorizedEntity = registry; - if (registry.equals(baseImageReference.getRegistry()) - && repository.equals(baseImageReference.getRepository()) - && noCredentialsDefinedForBaseImage) { - unauthorizedEntity = baseImageReference.toString(); - } else if (registry.equals(targetImageReference.getRegistry()) - && repository.equals(targetImageReference.getRepository()) - && noCredentialsDefinedForTargetImage) { - unauthorizedEntity = targetImageReference.toString(); - } - + public String forNoCredentialsDefined(String imageReference) { return suggest( "make sure your credentials for '" - + unauthorizedEntity + + imageReference + "' are set up correctly. See " + "https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help"); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index 228bde108b..c9f5faebec 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -140,8 +140,7 @@ private static void handleRegistryUnauthorizedException( } else { throw new BuildStepsExecutionException( helpfulSuggestions.forNoCredentialsDefined( - registryUnauthorizedException.getRegistry(), - registryUnauthorizedException.getRepository()), + registryUnauthorizedException.getImageReference()), registryUnauthorizedException); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 64d6f97d8a..70e0af802e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -85,8 +85,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa inferredAuthProvider, projectProperties, containerizer, - targetImageReference, - false); + targetImageReference); } public static PluginConfigurationProcessor processCommonConfigurationForTarImage( @@ -110,8 +109,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForTarImage inferredAuthProvider, projectProperties, containerizer, - targetImageReference, - false); + targetImageReference); } public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( @@ -128,17 +126,16 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry ImageReference targetImageReference = ImageReference.parse(rawConfiguration.getToImage().get()); RegistryImage targetImage = RegistryImage.named(targetImageReference); - boolean isTargetImageCredentialPresent = - configureCredentialRetrievers( - rawConfiguration, - projectProperties, - targetImage, - targetImageReference, - PropertyNames.TO_AUTH_USERNAME, - PropertyNames.TO_AUTH_PASSWORD, - rawConfiguration.getToAuth(), - inferredAuthProvider, - rawConfiguration.getToCredHelper().orElse(null)); + configureCredentialRetrievers( + rawConfiguration, + projectProperties, + targetImage, + targetImageReference, + PropertyNames.TO_AUTH_USERNAME, + PropertyNames.TO_AUTH_PASSWORD, + rawConfiguration.getToAuth(), + inferredAuthProvider, + rawConfiguration.getToCredHelper().orElse(null)); PluginConfigurationProcessor processor = processCommonConfiguration( @@ -146,8 +143,7 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry inferredAuthProvider, projectProperties, Containerizer.to(targetImage), - targetImageReference, - isTargetImageCredentialPresent); + targetImageReference); processor.getJibContainerBuilder().setFormat(rawConfiguration.getImageFormat()); return processor; } @@ -158,8 +154,7 @@ static PluginConfigurationProcessor processCommonConfiguration( InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, Containerizer containerizer, - ImageReference targetImageReference, - boolean isTargetImageCredentialPresent) + ImageReference targetImageReference) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, @@ -179,17 +174,16 @@ static PluginConfigurationProcessor processCommonConfiguration( } RegistryImage baseImage = RegistryImage.named(baseImageReference); - boolean isBaseImageCredentialPresent = - configureCredentialRetrievers( - rawConfiguration, - projectProperties, - baseImage, - baseImageReference, - PropertyNames.FROM_AUTH_USERNAME, - PropertyNames.FROM_AUTH_PASSWORD, - rawConfiguration.getFromAuth(), - inferredAuthProvider, - rawConfiguration.getFromCredHelper().orElse(null)); + configureCredentialRetrievers( + rawConfiguration, + projectProperties, + baseImage, + baseImageReference, + PropertyNames.FROM_AUTH_USERNAME, + PropertyNames.FROM_AUTH_PASSWORD, + rawConfiguration.getFromAuth(), + inferredAuthProvider, + rawConfiguration.getFromCredHelper().orElse(null)); BiFunction modificationTimeProvider = createModificationTimeProvider(rawConfiguration.getFilesModificationTime()); @@ -235,12 +229,7 @@ static PluginConfigurationProcessor processCommonConfiguration( configureContainerizer(containerizer, rawConfiguration, projectProperties); return new PluginConfigurationProcessor( - jibContainerBuilder, - containerizer, - baseImageReference, - targetImageReference, - isBaseImageCredentialPresent, - isTargetImageCredentialPresent); + jibContainerBuilder, containerizer, targetImageReference); } /** @@ -519,14 +508,14 @@ static Instant getCreationTime(String configuredCreationTime, ProjectProperties } // TODO: find a way to reduce the number of arguments. - private static boolean configureCredentialRetrievers( + private static void configureCredentialRetrievers( RawConfiguration rawConfiguration, ProjectProperties projectProperties, RegistryImage registryImage, ImageReference imageReference, String usernamePropertyName, String passwordPropertyName, - AuthProperty knownAuth, + AuthProperty rawAuthConfiguration, InferredAuthProvider inferredAuthProvider, @Nullable String credHelper) throws FileNotFoundException { @@ -538,17 +527,15 @@ private static boolean configureCredentialRetrievers( projectProperties::log, usernamePropertyName, passwordPropertyName, - knownAuth, + rawAuthConfiguration, rawConfiguration); - boolean credentialPresent = optionalCredential.isPresent(); if (optionalCredential.isPresent()) { defaultCredentialRetrievers.setKnownCredential( - optionalCredential.get(), knownAuth.getAuthDescriptor()); + optionalCredential.get(), rawAuthConfiguration.getAuthDescriptor()); } else { try { Optional optionalInferredAuth = inferredAuthProvider.inferAuth(imageReference.getRegistry()); - credentialPresent = optionalInferredAuth.isPresent(); if (optionalInferredAuth.isPresent()) { AuthProperty auth = optionalInferredAuth.get(); String username = Verify.verifyNotNull(auth.getUsername()); @@ -560,10 +547,9 @@ private static boolean configureCredentialRetrievers( projectProperties.log(LogEvent.warn("InferredAuthException: " + ex.getMessage())); } } + defaultCredentialRetrievers.setCredentialHelper(credHelper); defaultCredentialRetrievers.asList().forEach(registryImage::addCredentialRetriever); - - return credentialPresent; } private static ImageReference getGeneratedTargetDockerTag( @@ -660,25 +646,16 @@ private static boolean isKnownDistrolessJava11Image(String imageReference) { } private final JibContainerBuilder jibContainerBuilder; - private final ImageReference baseImageReference; private final ImageReference targetImageReference; - private final boolean isBaseImageCredentialPresent; - private final boolean isTargetImageCredentialPresent; private final Containerizer containerizer; private PluginConfigurationProcessor( JibContainerBuilder jibContainerBuilder, Containerizer containerizer, - ImageReference baseImageReference, - ImageReference targetImageReference, - boolean isBaseImageCredentialPresent, - boolean isTargetImageCredentialPresent) { + ImageReference targetImageReference) { this.jibContainerBuilder = jibContainerBuilder; this.containerizer = containerizer; - this.baseImageReference = baseImageReference; this.targetImageReference = targetImageReference; - this.isBaseImageCredentialPresent = isBaseImageCredentialPresent; - this.isTargetImageCredentialPresent = isTargetImageCredentialPresent; } public JibContainerBuilder getJibContainerBuilder() { @@ -689,19 +666,7 @@ public Containerizer getContainerizer() { return containerizer; } - public ImageReference getBaseImageReference() { - return baseImageReference; - } - public ImageReference getTargetImageReference() { return targetImageReference; } - - public boolean isBaseImageCredentialPresent() { - return isBaseImageCredentialPresent; - } - - public boolean isTargetImageCredentialPresent() { - return isTargetImageCredentialPresent; - } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index 0ae2515420..abb28e0f56 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -102,16 +102,7 @@ public void testGetImageAuth() { @Test public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceException { HelpfulSuggestions helpfulSuggestions = - new HelpfulSuggestions( - "", - "", - mockImageReference, - false, - mockImageReference, - false, - "to", - "--to", - "build.txt"); + new HelpfulSuggestions("", "", "to", "--to", "build.txt"); // Target configured ProjectProperties mockProjectProperties = Mockito.mock(ProjectProperties.class); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 1f1185dd45..30b7db2e2c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.cloud.tools.jib.api.ImageReference; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; @@ -26,15 +25,7 @@ public class HelpfulSuggestionsTest { private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = new HelpfulSuggestions( - "messagePrefix", - "clearCacheCommand", - ImageReference.of("baseregistry", "baserepository", null), - true, - ImageReference.of("targetregistry", "targetrepository", null), - false, - "toProperty", - "toFlag", - "buildFile"); + "messagePrefix", "clearCacheCommand", "toProperty", "toFlag", "buildFile"); @Test public void testSuggestions_smoke() { @@ -54,11 +45,8 @@ public void testSuggestions_smoke() { "messagePrefix, perhaps you should make sure you have permissions for imageReference and set correct credentials. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied for help", TEST_HELPFUL_SUGGESTIONS.forHttpStatusCodeForbidden("imageReference")); Assert.assertEquals( - "messagePrefix, perhaps you should make sure your credentials for 'baseregistry/baserepository' are set up correctly. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("baseregistry", "baserepository")); - Assert.assertEquals( - "messagePrefix, perhaps you should make sure your credentials for 'targetregistry' are set up correctly. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help", - TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("targetregistry", "targetrepository")); + "messagePrefix, perhaps you should make sure your credentials for 'registry/repository' are set up correctly. See https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized for help", + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("registry/repository")); Assert.assertEquals( "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", HelpfulSuggestions.forMainClassNotFound("messagePrefix", "plugin")); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index f5e775609d..0048bad27f 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -20,7 +20,6 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryException; @@ -46,15 +45,7 @@ public class JibBuildRunnerTest { private static final HelpfulSuggestions TEST_HELPFUL_SUGGESTIONS = new HelpfulSuggestions( - "messagePrefix", - "clearCacheCommand", - ImageReference.of("someregistry", "somerepository", null), - false, - ImageReference.of("toRegistry", "torepository", null), - false, - "toConfig", - "toFlag", - "buildFile"); + "messagePrefix", "clearCacheCommand", "toConfig", "toFlag", "buildFile"); @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -170,8 +161,8 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() ExecutionException { Mockito.when(mockRegistryUnauthorizedException.getHttpResponseException()) .thenReturn(mockHttpResponseException); - Mockito.when(mockRegistryUnauthorizedException.getRegistry()).thenReturn("someregistry"); - Mockito.when(mockRegistryUnauthorizedException.getRepository()).thenReturn("somerepository"); + Mockito.when(mockRegistryUnauthorizedException.getImageReference()) + .thenReturn("someregistry/somerepository"); Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); // Unknown Mockito.doThrow(mockRegistryUnauthorizedException) @@ -185,7 +176,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() } catch (BuildStepsExecutionException ex) { Assert.assertEquals( - TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry", "somerepository"), + TEST_HELPFUL_SUGGESTIONS.forNoCredentialsDefined("someregistry/somerepository"), ex.getMessage()); } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 1c5e42b41d..4950c7819a 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -214,20 +214,13 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() } @Test - public void testPluginConfigurationProcessor_warProjectBaseImage() - throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException, - InvalidCreationTimeException { + public void testGetBaseImage_warProject() + throws IncompatibleBaseImageJavaVersionException, NumberFormatException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - Assert.assertEquals( - ImageReference.parse("gcr.io/distroless/java/jetty:java8").toString(), - processor.getBaseImageReference().toString()); - Mockito.verifyNoMoreInteractions(logger); + "gcr.io/distroless/java/jetty:java8", + PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); } @Test @@ -934,7 +927,6 @@ private PluginConfigurationProcessor createPluginConfigurationProcessor() ignored -> Optional.empty(), projectProperties, containerizer, - targetImageReference, - false); + targetImageReference); } } From 7e3695b5a3884c6c88f8c146358dc2b577c7c87a Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 14 Aug 2019 09:39:19 -0400 Subject: [PATCH 0685/2020] Retry cache installation attempts on Windows (#1887) --- .../tools/jib/cache/CacheStorageWriter.java | 40 +++-- .../google/cloud/tools/jib/cache/Retry.java | 139 ++++++++++++++++++ .../cloud/tools/jib/cache/RetryTest.java | 121 +++++++++++++++ 3 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index a427db0c70..ed2f6ac7b8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -43,6 +43,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.concurrent.TimeUnit; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.annotation.Nullable; @@ -75,20 +76,31 @@ private WrittenLayer( * @throws IOException if an I/O exception occurs */ private static void moveIfDoesNotExist(Path source, Path destination) throws IOException { - // If the file already exists, we skip renaming and use the existing file. This happens if a - // new layer happens to have the same content as a previously-cached layer. - if (Files.exists(destination)) { - return; - } - - try { - Files.move(source, destination); - - } catch (FileSystemException ex) { - if (!Files.exists(destination)) { - // TODO to log that the destination exists - throw ex; - } + // Some Windows users report java.nio.file.AccessDeniedException that we suspect is caused + // by anti-virus programs, like Windows Defender, that open new files for scanning. + // Retry the rename up to 5 times, with 15ms pause between each retry. + boolean success = + Retry.action( + () -> { + if (Files.exists(destination)) { + // If the file already exists, we skip renaming and use the existing file. + // This happens if a new layer happens to have the same content as a + // previously-cached layer. + return true; + } + Files.move(source, destination); + return Files.exists(destination); + }) + .maximumRetries(5) + .retryOnException(ex -> ex instanceof FileSystemException) + .sleep(15, TimeUnit.MILLISECONDS) + .run(); + if (!success) { + String message = + String.format( + "unable to move: %s to %s; such failures are often caused by interference from antivirus", + source, destination); + throw new IOException(message); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java new file mode 100644 index 0000000000..211a5df445 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.cache; + +import com.google.common.base.Preconditions; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +/** + * Retries an action until it succeeds, or has retried too often and failed. By default the action + * will be run up to 5 times. The action is deemed successful if it runs to completion without + * throwing an exception, and returns true. + * + *
    + *
  • Exceptions are caught and, if deemed {@link #retryOnException(Predicate) retryable} then + * the action will be re-attempted. By default, any exception is considered retryable. + *
  • The retry instance can be configured to {@link #sleep(long, TimeUnit) sleep between + * retries}. + *
  • The maximum retry count {@link #maximumRetries(int) is configurable} (5 times by default). + *
+ * + * @param the class of exceptions that may be thrown + */ +public class Retry { + + /** A runnable action that may throw an exception of type {@code E}. */ + @FunctionalInterface + public interface Action { + /** + * Perform the action. + * + * @return {@code true} if the action was successful and {@code false} otherwise + * @throws E exception thrown during the action + */ + boolean run() throws E; + } + + /** + * Create a retryable action. + * + * @param action the action to be run + * @return the instance + * @param the class of exceptions that may be thrown + */ + public static Retry action(Action action) { + return new Retry(action); + } + + private final Action action; + private int maximumRetries = 5; + private Predicate retryOnException = ignored -> true; // continue to retry + private long sleepMilliseconds = -1; // no sleep + + private Retry(Action action) { + this.action = action; + } + + public Retry maximumRetries(int maximumRetries) { + Preconditions.checkArgument(maximumRetries > 0); + this.maximumRetries = maximumRetries; + return this; + } + + /** + * Provide a predicate to determine if a thrown exception can be retried. + * + * @param retryOnException determine if provided exception is retryable. + * @return the instance for further configuration + */ + public Retry retryOnException(Predicate retryOnException) { + this.retryOnException = retryOnException; + return this; + } + + /** + * Set the sleep time between retries. + * + * @param duration the time to sleep + * @param unit the unit of time of duration + * @return the instance for further configuration + */ + public Retry sleep(long duration, TimeUnit unit) { + Preconditions.checkArgument(duration >= 0); + this.sleepMilliseconds = unit.convert(duration, TimeUnit.MILLISECONDS); + return this; + } + + /** + * Run the action until it runs successfully, to a {@link #maximumRetries(int) maximum number of + * retries} (default: 5). If an exception occurs then the action will be retried providing {@link + * #retryOnException(Predicate) the exception is retryable}. + * + * @return true if the action was run successfully, or {@code false} if the action was unable to + * complete + * @throws E exception thrown during the action + */ + public boolean run() throws E { + for (int i = 0; i < maximumRetries; i++) { + try { + // sleep between attempts, but not on the first attempt + if (i > 0 && sleepMilliseconds >= 0) { + Thread.sleep(sleepMilliseconds); + } + + // Do we need to continue? + if (action.run()) { + return true; + } + + } catch (InterruptedException ex) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + return false; + + } catch (Exception ex) { + // if this is the last iteration, no more retries + if (i + 1 == maximumRetries || !retryOnException.test(ex)) { + throw ex; + } + } + } + // we did not complete + return false; + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java new file mode 100644 index 0000000000..3659d0ceb0 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.tools.jib.cache; + +import java.util.concurrent.TimeUnit; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Retry}. */ +public class RetryTest { + private int actionCount = 0; + private final Retry.Action successfulAction = + () -> { + ++actionCount; + return true; + }; + private final Retry.Action unsuccessfulAction = + () -> { + ++actionCount; + return false; + }; + private final Retry.Action exceptionAction = + () -> { + ++actionCount; + throw new Exception("whee"); + }; + + @Test + public void testSuccessfulAction() throws Exception { + boolean result = Retry.action(successfulAction).run(); + Assert.assertTrue(result); + Assert.assertEquals(1, actionCount); + } + + @Test + public void testMaximumRetries_default() throws Exception { + boolean result = Retry.action(unsuccessfulAction).run(); + Assert.assertFalse(result); + Assert.assertEquals(5, actionCount); + } + + @Test + public void testMaximumRetries_specified() throws Exception { + boolean result = Retry.action(unsuccessfulAction).maximumRetries(2).run(); + Assert.assertFalse(result); + Assert.assertEquals(2, actionCount); + } + + @Test + public void testRetryableException() { + // all exceptions are retryable by default, so should retry 5 times + try { + Retry.action(exceptionAction).run(); + Assert.fail("should have thrown exception"); + } catch (Exception ex) { + Assert.assertEquals("whee", ex.getMessage()); + Assert.assertEquals(5, actionCount); + } + } + + @Test + public void testNonRetryableException() { + // the exception is not ok and so should only try 1 time + try { + Retry.action(exceptionAction).retryOnException(ex -> false).run(); + Assert.fail("should have thrown exception"); + } catch (Exception ex) { + Assert.assertEquals("whee", ex.getMessage()); + Assert.assertEquals(1, actionCount); + } + } + + @Test + public void testInterruptSleep() throws Exception { + // interrupt the current thread so as to cause the retry's sleep() to throw + // an InterruptedException + Thread.currentThread().interrupt(); + try { + boolean result = Retry.action(unsuccessfulAction).sleep(10, TimeUnit.SECONDS).run(); + Assert.assertFalse(result); + Assert.assertEquals(1, actionCount); + } finally { + // This thread should be marked as interrupted (plus clear the flag for the test) + Assert.assertTrue(Thread.interrupted()); + } + } + + @Test + public void testInvalid_maximumRetries() { + try { + Retry.action(successfulAction).maximumRetries(0); + Assert.fail(); + } catch (IllegalArgumentException ex) { + /* maximumRetries() ensures the retry value is at least 1. */ + } + } + + @Test + public void testInvalid_sleep() { + try { + Retry.action(successfulAction).sleep(-1, TimeUnit.DAYS); + Assert.fail(); + } catch (IllegalArgumentException ex) { + /* sleep() ensures the sleep value is non-negative. */ + } + } +} From 384cf91bc9013433ea3328d1c7aecf6392299e4f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 14 Aug 2019 17:22:01 -0400 Subject: [PATCH 0686/2020] Refactor PluginConfigurationProcessor and Maven/Gradle plugin code (#1901) * Simplify unauth handling code * wip * Revive a test * Configure target auth * Remove redundant code comments * Refactor code * Rename public methods to createJibRunner... --- .../tools/jib/gradle/BuildDockerTask.java | 38 ++---- .../tools/jib/gradle/BuildImageTask.java | 41 ++---- .../cloud/tools/jib/gradle/BuildTarTask.java | 38 ++---- .../jib/gradle/GradleProjectProperties.java | 5 + .../tools/jib/maven/BuildDockerMojo.java | 46 ++----- .../cloud/tools/jib/maven/BuildImageMojo.java | 49 +++----- .../cloud/tools/jib/maven/BuildTarMojo.java | 46 ++----- .../jib/maven/MavenProjectProperties.java | 5 + .../jib/plugins/common/JibBuildRunner.java | 73 ++++++++--- .../common/PluginConfigurationProcessor.java | 117 +++++++++--------- .../jib/plugins/common/ProjectProperties.java | 2 + .../plugins/common/JibBuildRunnerTest.java | 33 +++-- .../PluginConfigurationProcessorTest.java | 60 +++------ 13 files changed, 228 insertions(+), 325 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 7a4d7314a6..2803254911 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -29,10 +28,8 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -105,37 +102,17 @@ public void buildDocker() TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject(getProject(), getLogger()); try { - RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); - - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - gradleRawConfiguration, + PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( + new GradleRawConfiguration(jibExtension), ignored -> java.util.Optional.empty(), projectProperties, dockerClientParameters.getExecutablePath(), dockerClientParameters.getEnvironment(), - new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - Path buildOutput = getProject().getBuildDir().toPath(); - - try { - JibBuildRunner.forBuildToDockerDaemon(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - } + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new GradleException( @@ -179,6 +156,9 @@ public void buildDocker() } catch (InvalidImageReferenceException ex) { throw new GradleException( HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } finally { + projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 401958e9b8..6e0a19794b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; @@ -28,14 +27,11 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.IOException; -import java.nio.file.Path; import java.util.Optional; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; @@ -82,11 +78,9 @@ public void buildImage() TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject(getProject(), getLogger()); try { - RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); - if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( HelpfulSuggestions.forToNotConfigured( @@ -96,28 +90,12 @@ public void buildImage() "gradle jib --image ")); } - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - gradleRawConfiguration, ignored -> Optional.empty(), projectProperties); - - ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - - Path buildOutput = getProject().getBuildDir().toPath(); - - try { - JibBuildRunner.forBuildImage(targetImageReference, jibExtension.getTo().getTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - } + PluginConfigurationProcessor.createJibBuildRunnerForRegistryImage( + new GradleRawConfiguration(jibExtension), + ignored -> Optional.empty(), + projectProperties, + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new GradleException( @@ -161,6 +139,9 @@ public void buildImage() } catch (InvalidImageReferenceException ex) { throw new GradleException( HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } finally { + projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 5958753f4e..63c8bff5d0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -27,10 +27,8 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; @@ -105,36 +103,15 @@ public void buildTar() TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject(getProject(), getLogger()); try { - RawConfiguration gradleRawConfiguration = new GradleRawConfiguration(jibExtension); - GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); - - Path tarOutputPath = getTargetPath(); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForTarImage( - gradleRawConfiguration, + PluginConfigurationProcessor.createJibBuildRunnerForTarImage( + new GradleRawConfiguration(jibExtension), ignored -> Optional.empty(), projectProperties, - tarOutputPath, - new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - Path buildOutput = getProject().getBuildDir().toPath(); - - try { - JibBuildRunner.forBuildTar(tarOutputPath) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - } + new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new GradleException( @@ -178,6 +155,9 @@ public void buildTar() } catch (InvalidImageReferenceException ex) { throw new GradleException( HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); + + } finally { + projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 3ca008e507..85f4ac4ce6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -340,6 +340,11 @@ public String getVersion() { return project.getVersion().toString(); } + @Override + public Path getOutputDirectory() { + return project.getBuildDir().toPath(); + } + @Override public int getMajorJavaVersion() { JavaVersion version = JavaVersion.current(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 5691a486f7..8b76f8a801 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; @@ -30,11 +29,9 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; @@ -100,42 +97,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.checkUseCurrentTimestampDeprecation(this); - try { - RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForDockerDaemonImage( - mavenRawConfiguration, + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + try { + PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( + new MavenRawConfiguration(this), new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter()), projectProperties, dockerExecutable, getDockerClientEnvironment(), - new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - MavenSettingsProxyProvider.activateHttpAndHttpsProxies( - getSession().getSettings(), getSettingsDecrypter()); - - ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - - try { - JibBuildRunner.forBuildToDockerDaemon(targetImageReference, getTargetImageAdditionalTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); - } + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new MojoExecutionException( @@ -186,6 +162,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); + + } finally { + projectProperties.waitForLoggingThread(); + getLog().info(""); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 5cbebb0894..4f9538f18e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageFormat; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; @@ -29,16 +28,12 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -98,39 +93,19 @@ public void execute() throws MojoExecutionException, MojoFailureException { "mvn compile jib:build -Dimage=")); } - try { - RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForRegistryImage( - mavenRawConfiguration, + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + try { + PluginConfigurationProcessor.createJibBuildRunnerForRegistryImage( + new MavenRawConfiguration(this), new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter()), - projectProperties); - MavenSettingsProxyProvider.activateHttpAndHttpsProxies( - getSession().getSettings(), getSettingsDecrypter()); - - ImageReference targetImageReference = pluginConfigurationProcessor.getTargetImageReference(); - - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - - try { - JibBuildRunner.forBuildImage(targetImageReference, getTargetImageAdditionalTags()) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); - } + projectProperties, + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new MojoExecutionException( @@ -181,6 +156,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); + + } finally { + projectProperties.waitForLoggingThread(); + getLog().info(""); } } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 427e2faea6..eed89a836e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -27,15 +27,11 @@ import com.google.cloud.tools.jib.plugins.common.InvalidCreationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidFilesModificationTimeException; import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; -import com.google.cloud.tools.jib.plugins.common.JibBuildRunner; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import com.google.cloud.tools.jib.plugins.common.RawConfiguration; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -74,39 +70,19 @@ public void execute() throws MojoExecutionException, MojoFailureException { MojoCommon.checkUseCurrentTimestampDeprecation(this); + MavenSettingsProxyProvider.activateHttpAndHttpsProxies( + getSession().getSettings(), getSettingsDecrypter()); + + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); try { - RawConfiguration mavenRawConfiguration = new MavenRawConfiguration(this); - MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); - - Path buildOutput = Paths.get(getProject().getBuild().getDirectory()); - Path tarOutputPath = buildOutput.resolve("jib-image.tar"); - PluginConfigurationProcessor pluginConfigurationProcessor = - PluginConfigurationProcessor.processCommonConfigurationForTarImage( - mavenRawConfiguration, + PluginConfigurationProcessor.createJibBuildRunnerForTarImage( + new MavenRawConfiguration(this), new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter()), projectProperties, - tarOutputPath, - new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - MavenSettingsProxyProvider.activateHttpAndHttpsProxies( - getSession().getSettings(), getSettingsDecrypter()); - - try { - JibBuildRunner.forBuildTar(tarOutputPath) - .writeImageDigest(buildOutput.resolve("jib-image.digest")) - .writeImageId(buildOutput.resolve("jib-image.id")) - .build( - pluginConfigurationProcessor.getJibContainerBuilder(), - pluginConfigurationProcessor.getContainerizer(), - projectProperties::log, - new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)); - - } finally { - // TODO: This should not be called on projectProperties. - projectProperties.waitForLoggingThread(); - getLog().info(""); - } + new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) + .runBuild(); } catch (InvalidAppRootException ex) { throw new MojoExecutionException( @@ -157,6 +133,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (BuildStepsExecutionException ex) { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); + + } finally { + projectProperties.waitForLoggingThread(); + getLog().info(""); } } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index c5f1019856..04292e2d3d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -367,6 +367,11 @@ public String getVersion() { return project.getVersion(); } + @Override + public Path getOutputDirectory() { + return Paths.get(project.getBuild().getDirectory()); + } + @Override public int getMajorJavaVersion() { // Check properties for version diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index c9f5faebec..a913c1e264 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -81,13 +81,26 @@ private static String buildMessageWithTargetImageReferences( /** * Creates a runner to build an image. Creates a directory for the cache, if needed. * + * @param jibContainerBuilder the {@link JibContainerBuilder} + * @param containerizer the {@link Containerizer} + * @param logger consumer for handling log events + * @param helpfulSuggestions suggestions to use in help messages for exceptions * @param targetImageReference the target image reference * @param additionalTags additional tags to push to * @return a {@link JibBuildRunner} for building to a registry */ public static JibBuildRunner forBuildImage( - ImageReference targetImageReference, Set additionalTags) { + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + Consumer logger, + HelpfulSuggestions helpfulSuggestions, + ImageReference targetImageReference, + Set additionalTags) { return new JibBuildRunner( + jibContainerBuilder, + containerizer, + logger, + helpfulSuggestions, buildMessageWithTargetImageReferences( targetImageReference, additionalTags, @@ -100,13 +113,26 @@ public static JibBuildRunner forBuildImage( /** * Creates a runner to build to the Docker daemon. Creates a directory for the cache, if needed. * + * @param jibContainerBuilder the {@link JibContainerBuilder} + * @param containerizer the {@link Containerizer} + * @param logger consumer for handling log events + * @param helpfulSuggestions suggestions to use in help messages for exceptions * @param targetImageReference the target image reference * @param additionalTags additional tags to push to * @return a {@link JibBuildRunner} for building to a Docker daemon */ public static JibBuildRunner forBuildToDockerDaemon( - ImageReference targetImageReference, Set additionalTags) { + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + Consumer logger, + HelpfulSuggestions helpfulSuggestions, + ImageReference targetImageReference, + Set additionalTags) { return new JibBuildRunner( + jibContainerBuilder, + containerizer, + logger, + helpfulSuggestions, buildMessageWithTargetImageReferences( targetImageReference, additionalTags, STARTUP_MESSAGE_PREFIX_FOR_DOCKER_DAEMON, "..."), buildMessageWithTargetImageReferences( @@ -116,11 +142,24 @@ public static JibBuildRunner forBuildToDockerDaemon( /** * Creates a runner to build an image tarball. Creates a directory for the cache, if needed. * + * @param jibContainerBuilder the {@link JibContainerBuilder} + * @param containerizer the {@link Containerizer} + * @param logger consumer for handling log events + * @param helpfulSuggestions suggestions to use in help messages for exceptions * @param outputPath the path to output the tarball to * @return a {@link JibBuildRunner} for building a tarball */ - public static JibBuildRunner forBuildTar(Path outputPath) { + public static JibBuildRunner forBuildTar( + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + Consumer logger, + HelpfulSuggestions helpfulSuggestions, + Path outputPath) { return new JibBuildRunner( + jibContainerBuilder, + containerizer, + logger, + helpfulSuggestions, String.format(STARTUP_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString()), String.format(SUCCESS_MESSAGE_FORMAT_FOR_TARBALL, outputPath.toString())); } @@ -147,32 +186,38 @@ private static void handleRegistryUnauthorizedException( private final String startupMessage; private final String successMessage; + private final JibContainerBuilder jibContainerBuilder; + private final Containerizer containerizer; + private final Consumer logger; + private final HelpfulSuggestions helpfulSuggestions; @Nullable private Path imageDigestOutputPath; @Nullable private Path imageIdOutputPath; @VisibleForTesting - JibBuildRunner(String startupMessage, String successMessage) { + JibBuildRunner( + JibContainerBuilder jibContainerBuilder, + Containerizer containerizer, + Consumer logger, + HelpfulSuggestions helpfulSuggestions, + String startupMessage, + String successMessage) { + this.jibContainerBuilder = jibContainerBuilder; + this.containerizer = containerizer; + this.logger = logger; + this.helpfulSuggestions = helpfulSuggestions; this.startupMessage = startupMessage; this.successMessage = successMessage; } /** - * Runs the build steps. + * Runs the Jib build. * - * @param jibContainerBuilder the {@link JibContainerBuilder} - * @param containerizer the {@link Containerizer} - * @param logger consumer for handling log events - * @param helpfulSuggestions suggestions to use in help messages for exceptions * @return the built {@link JibContainer} * @throws BuildStepsExecutionException if another exception is thrown during the build * @throws IOException if an I/O exception occurs * @throws CacheDirectoryCreationException if the cache directory could not be created */ - public JibContainer build( - JibContainerBuilder jibContainerBuilder, - Containerizer containerizer, - Consumer logger, - HelpfulSuggestions helpfulSuggestions) + public JibContainer runBuild() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { try { logger.accept(LogEvent.lifecycle("")); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 70e0af802e..8764568f7d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -52,12 +52,12 @@ import javax.annotation.Nullable; /** - * Configures and provides {@code JibContainerBuilder} for the image building tasks based on raw - * plugin configuration values and project properties. + * Configures and provides {@code JibBuildRunner} for the image building tasks based on raw plugin + * configuration values and project properties. */ public class PluginConfigurationProcessor { - public static PluginConfigurationProcessor processCommonConfigurationForDockerDaemonImage( + public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, @@ -78,44 +78,58 @@ public static PluginConfigurationProcessor processCommonConfigurationForDockerDa if (dockerEnvironment != null) { targetImage.setDockerEnvironment(dockerEnvironment); } + Containerizer containerizer = Containerizer.to(targetImage); + JibContainerBuilder jibContainerBuilder = + processCommonConfiguration( + rawConfiguration, inferredAuthProvider, projectProperties, containerizer); - return processCommonConfiguration( - rawConfiguration, - inferredAuthProvider, - projectProperties, - containerizer, - targetImageReference); + return JibBuildRunner.forBuildToDockerDaemon( + jibContainerBuilder, + containerizer, + projectProperties::log, + helpfulSuggestions, + targetImageReference, + rawConfiguration.getToTags()) + .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) + .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); } - public static PluginConfigurationProcessor processCommonConfigurationForTarImage( + public static JibBuildRunner createJibBuildRunnerForTarImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, - Path tarImagePath, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { + Path tarImagePath = projectProperties.getOutputDirectory().resolve("jib-image.tar"); ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); + Containerizer containerizer = Containerizer.to(targetImage); + JibContainerBuilder jibContainerBuilder = + processCommonConfiguration( + rawConfiguration, inferredAuthProvider, projectProperties, containerizer); - return processCommonConfiguration( - rawConfiguration, - inferredAuthProvider, - projectProperties, - containerizer, - targetImageReference); + return JibBuildRunner.forBuildTar( + jibContainerBuilder, + containerizer, + projectProperties::log, + helpfulSuggestions, + tarImagePath) + .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) + .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); } - public static PluginConfigurationProcessor processCommonConfigurationForRegistryImage( + public static JibBuildRunner createJibBuildRunnerForRegistryImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, - ProjectProperties projectProperties) + ProjectProperties projectProperties, + HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, @@ -137,24 +151,31 @@ public static PluginConfigurationProcessor processCommonConfigurationForRegistry inferredAuthProvider, rawConfiguration.getToCredHelper().orElse(null)); - PluginConfigurationProcessor processor = + Containerizer containerizer = Containerizer.to(targetImage); + JibContainerBuilder jibContainerBuilder = processCommonConfiguration( - rawConfiguration, - inferredAuthProvider, - projectProperties, - Containerizer.to(targetImage), - targetImageReference); - processor.getJibContainerBuilder().setFormat(rawConfiguration.getImageFormat()); - return processor; + rawConfiguration, inferredAuthProvider, projectProperties, containerizer); + + // Note Docker and tar builds don't set the configured format. + jibContainerBuilder.setFormat(rawConfiguration.getImageFormat()); + + return JibBuildRunner.forBuildImage( + jibContainerBuilder, + containerizer, + projectProperties::log, + helpfulSuggestions, + targetImageReference, + rawConfiguration.getToTags()) + .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) + .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); } @VisibleForTesting - static PluginConfigurationProcessor processCommonConfiguration( + static JibContainerBuilder processCommonConfiguration( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, - Containerizer containerizer, - ImageReference targetImageReference) + Containerizer containerizer) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, @@ -173,7 +194,11 @@ static PluginConfigurationProcessor processCommonConfiguration( + "enable this on a public network!")); } + // 1. configure containerizer + configureContainerizer(containerizer, rawConfiguration, projectProperties); + RegistryImage baseImage = RegistryImage.named(baseImageReference); + // 2. create and configure credential retrievers for base image configureCredentialRetrievers( rawConfiguration, projectProperties, @@ -185,6 +210,7 @@ static PluginConfigurationProcessor processCommonConfiguration( inferredAuthProvider, rawConfiguration.getFromCredHelper().orElse(null)); + // 3. create and configure JibContainerBuilder BiFunction modificationTimeProvider = createModificationTimeProvider(rawConfiguration.getFilesModificationTime()); JavaContainerBuilder javaContainerBuilder = @@ -225,11 +251,7 @@ static PluginConfigurationProcessor processCommonConfiguration( modificationTimeProvider)); } } - - configureContainerizer(containerizer, rawConfiguration, projectProperties); - - return new PluginConfigurationProcessor( - jibContainerBuilder, containerizer, targetImageReference); + return jibContainerBuilder; } /** @@ -644,29 +666,4 @@ private static boolean isKnownDistrolessJava11Image(String imageReference) { || imageReference.equals("gcr.io/distroless/java/jetty:java11") || imageReference.equals("gcr.io/distroless/java/jetty:java11-debug"); } - - private final JibContainerBuilder jibContainerBuilder; - private final ImageReference targetImageReference; - private final Containerizer containerizer; - - private PluginConfigurationProcessor( - JibContainerBuilder jibContainerBuilder, - Containerizer containerizer, - ImageReference targetImageReference) { - this.jibContainerBuilder = jibContainerBuilder; - this.containerizer = containerizer; - this.targetImageReference = targetImageReference; - } - - public JibContainerBuilder getJibContainerBuilder() { - return jibContainerBuilder; - } - - public Containerizer getContainerizer() { - return containerizer; - } - - public ImageReference getTargetImageReference() { - return targetImageReference; - } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 46aaad508d..258238e681 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -82,6 +82,8 @@ JibContainerBuilder createJibContainerBuilder( String getVersion(); + Path getOutputDirectory(); + int getMajorJavaVersion(); boolean isOffline(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index 0048bad27f..aa8e4c16ec 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -59,14 +59,20 @@ public class JibBuildRunnerTest { @Before public void setUpMocks() { - testJibBuildRunner = new JibBuildRunner("ignored", "ignored"); + testJibBuildRunner = + new JibBuildRunner( + mockJibContainerBuilder, + mockContainerizer, + ignored -> {}, + TEST_HELPFUL_SUGGESTIONS, + "ignored", + "ignored"); } @Test public void testBuildImage_pass() throws BuildStepsExecutionException, IOException, CacheDirectoryCreationException { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); } @Test @@ -80,8 +86,7 @@ public void testBuildImage_httpHostConnectException() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -99,8 +104,7 @@ public void testBuildImage_unknownHostException() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -119,8 +123,7 @@ public void testBuildImage_insecureRegistryException() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -144,8 +147,7 @@ public void testBuildImage_registryUnauthorizedException_statusCodeForbidden() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -170,8 +172,7 @@ public void testBuildImage_registryUnauthorizedException_noCredentials() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -190,8 +191,7 @@ public void testBuildImage_registryCredentialsNotSentException() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { @@ -208,8 +208,7 @@ public void testBuildImage_other() .containerize(mockContainerizer); try { - testJibBuildRunner.build( - mockJibContainerBuilder, mockContainerizer, logEvent -> {}, TEST_HELPFUL_SUGGESTIONS); + testJibBuildRunner.runBuild(); Assert.fail(); } catch (BuildStepsExecutionException ex) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 4950c7819a..7623c04421 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.Jib; @@ -91,7 +90,6 @@ private static void assertExtractionPathsUnordered( @Mock private RawConfiguration rawConfiguration; @Mock private ProjectProperties projectProperties; @Mock private Containerizer containerizer; - @Mock private ImageReference targetImageReference; @Mock private AuthProperty authProperty; @Mock private Consumer logger; @@ -132,9 +130,7 @@ public void testPluginConfigurationProcessor_defaults() InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -163,9 +159,7 @@ public void testPluginConfigurationProcessor_extraDirectory() .thenReturn( ImmutableMap.of(AbsoluteUnixPath.get("/foo"), FilePermissions.fromOctalString("123"))); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); List extraFiles = buildConfiguration .getLayerConfigurations() @@ -207,7 +201,7 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() System.setProperty(PropertyNames.BASE_IMAGE_CACHE, "new/base/cache"); System.setProperty(PropertyNames.APPLICATION_CACHE, "/new/application/cache"); - createPluginConfigurationProcessor(); + processCommonConfiguration(); Mockito.verify(containerizer).setBaseImageLayersCache(Paths.get("new/base/cache")); Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("/new/application/cache")); @@ -233,9 +227,7 @@ public void testEntrypoint() Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -295,9 +287,7 @@ public void testEntrypoint_defaultWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); @@ -314,9 +304,7 @@ public void testEntrypoint_defaultNonWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -339,9 +327,7 @@ public void testEntrypoint_extraClasspathNonWarPackaging() .thenReturn(Collections.singletonList("/foo")); Mockito.when(projectProperties.isWarProject()).thenReturn(false); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - JibContainerBuilder jibContainerBuilder = processor.getJibContainerBuilder(); - BuildConfiguration buildConfiguration = getBuildConfiguration(jibContainerBuilder); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -362,9 +348,7 @@ public void testUser() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); @@ -377,9 +361,7 @@ public void testUser_null() InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); @@ -396,9 +378,7 @@ public void testEntrypoint_warningOnJvmFlags() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -421,9 +401,7 @@ public void testEntrypoint_warningOnMainclass() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertEquals( @@ -444,9 +422,7 @@ public void testEntrypointClasspath_nonDefaultAppRoot() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); @@ -468,9 +444,7 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); - PluginConfigurationProcessor processor = createPluginConfigurationProcessor(); - BuildConfiguration buildConfiguration = - getBuildConfiguration(processor.getJibContainerBuilder()); + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); @@ -916,17 +890,13 @@ public void testGetCreationTime_invalidValue() { } } - private PluginConfigurationProcessor createPluginConfigurationProcessor() + private JibContainerBuilder processCommonConfiguration() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { return PluginConfigurationProcessor.processCommonConfiguration( - rawConfiguration, - ignored -> Optional.empty(), - projectProperties, - containerizer, - targetImageReference); + rawConfiguration, ignored -> Optional.empty(), projectProperties, containerizer); } } From 05601cf6bcec905cad797897ddf40760ea691f50 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 14 Aug 2019 17:22:41 -0400 Subject: [PATCH 0687/2020] Try Google Application Default Credentials for GCR (gcr.io) auth (#1902) * Retrieve Application Default Credentials * Log messages --- jib-core/build.gradle | 8 +- .../RetrieveRegistryCredentialsStep.java | 2 +- .../frontend/CredentialRetrieverFactory.java | 135 +++++++---- .../DockerConfigCredentialRetriever.java | 1 - .../credentials/DockerCredentialHelper.java | 4 +- .../jib/api/JibContainerBuilderTest.java | 3 +- .../CredentialRetrieverFactoryTest.java | 223 ++++++++++++------ jib-gradle-plugin/CHANGELOG.md | 1 + jib-gradle-plugin/build.gradle | 4 +- jib-maven-plugin/CHANGELOG.md | 1 + jib-maven-plugin/pom.xml | 15 +- jib-plugins-common/build.gradle | 4 +- .../common/DefaultCredentialRetrievers.java | 22 +- .../DefaultCredentialRetrieversTest.java | 21 +- 14 files changed, 290 insertions(+), 154 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index d6a32062fb..bf1bf7585a 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -37,10 +37,16 @@ configurations { dependencies { // Make sure these are consistent with jib-maven-plugin. + + // For Google libraries, check , , , + // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml + // for best compatibility. implementation 'com.google.http-client:google-http-client:1.31.0' implementation 'com.google.http-client:google-http-client-apache-v2:1.31.0' + implementation 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + implementation 'com.google.guava:guava:28.0-jre' + implementation 'org.apache.commons:commons-compress:1.18' - implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' implementation 'org.ow2.asm:asm:7.0' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index fa7422a557..b149b213fe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -87,7 +87,7 @@ public Optional call() throws CredentialRetrievalException { } // If no credentials found, give an info (not warning because in most cases, the base image is - // public and does not need extra credentials) and return null. + // public and does not need extra credentials) and return empty. eventHandlers.dispatch( LogEvent.info("No credentials could be retrieved for registry " + registry)); return Optional.empty(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 14ba14124b..382df76d1e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.frontend; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; @@ -30,8 +32,9 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Consumer; @@ -45,12 +48,25 @@ interface DockerCredentialHelperFactory { DockerCredentialHelper create(String registry, Path credentialHelper); } - /** - * Defines common credential helpers to use as defaults. Maps from registry suffix to credential - * helper suffix. - */ - private static final ImmutableMap COMMON_CREDENTIAL_HELPERS = - ImmutableMap.of("gcr.io", "gcr", "amazonaws.com", "ecr-login"); + /** Used for passing in mock {@link GoogleCredentials} for testing. */ + @VisibleForTesting + @FunctionalInterface + interface GoogleCredentialsProvider { + GoogleCredentials get() throws IOException; + } + + // com.google.api.services.storage.StorageScopes.DEVSTORAGE_READ_WRITE + // OAuth2 credentials require at least the GCS write scope for GCR push. We need to manually set + // this scope for "OAuth2 credentials" instantiated from a service account, which are not scoped + // (i.e., createScopedRequired() returns true). Note that for a service account, the IAM roles of + // the service account determine the IAM permissions. + private static final String OAUTH_SCOPE_STORAGE_READ_WRITE = + "https://www.googleapis.com/auth/devstorage.read_write"; + + /** Mapping between well-known credential helpers and registries (suffixes). */ + private static final ImmutableMap WELL_KNOWN_CREDENTIAL_HELPERS = + ImmutableMap.of( + "gcr.io", "docker-credential-gcr", "amazonaws.com", "docker-credential-ecr-login"); /** * Creates a new {@link CredentialRetrieverFactory} for an image. @@ -61,32 +77,28 @@ interface DockerCredentialHelperFactory { */ public static CredentialRetrieverFactory forImage( ImageReference imageReference, Consumer logger) { - return new CredentialRetrieverFactory(imageReference, logger, DockerCredentialHelper::new); - } - - /** - * Creates a new {@link CredentialRetrieverFactory} for an image. - * - * @param imageReference the image the credential are for - * @return a new {@link CredentialRetrieverFactory} - */ - public static CredentialRetrieverFactory forImage(ImageReference imageReference) { return new CredentialRetrieverFactory( - imageReference, logEvent -> {}, DockerCredentialHelper::new); + imageReference, + logger, + DockerCredentialHelper::new, + GoogleCredentials::getApplicationDefault); } private final ImageReference imageReference; private final Consumer logger; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; + private final GoogleCredentialsProvider googleCredentialsProvider; @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, Consumer logger, - DockerCredentialHelperFactory dockerCredentialHelperFactory) { + DockerCredentialHelperFactory dockerCredentialHelperFactory, + GoogleCredentialsProvider googleCredentialsProvider) { this.imageReference = imageReference; this.logger = logger; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; + this.googleCredentialsProvider = googleCredentialsProvider; } /** @@ -125,8 +137,6 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) { */ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { return () -> { - logger.accept(LogEvent.info("Checking credentials from " + credentialHelper)); - try { return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper)); @@ -143,33 +153,21 @@ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) { } /** - * Creates a new {@link CredentialRetriever} that tries common Docker credential helpers to + * Creates a new {@link CredentialRetriever} that tries well-known Docker credential helpers to * retrieve credentials based on the registry of the image, such as {@code docker-credential-gcr} - * for images with the registry as {@code gcr.io}. + * for images with the registry ending with {@code gcr.io}. * * @return a new {@link CredentialRetriever} */ - public CredentialRetriever inferCredentialHelper() { - List inferredCredentialHelperSuffixes = new ArrayList<>(); - for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) { - if (!imageReference.getRegistry().endsWith(registrySuffix)) { - continue; - } - String inferredCredentialHelperSuffix = COMMON_CREDENTIAL_HELPERS.get(registrySuffix); - if (inferredCredentialHelperSuffix == null) { - throw new IllegalStateException("No COMMON_CREDENTIAL_HELPERS should be null"); - } - inferredCredentialHelperSuffixes.add(inferredCredentialHelperSuffix); - } - + public CredentialRetriever wellKnownCredentialHelpers() { return () -> { - for (String inferredCredentialHelperSuffix : inferredCredentialHelperSuffixes) { + for (Map.Entry entry : WELL_KNOWN_CREDENTIAL_HELPERS.entrySet()) { try { - return Optional.of( - retrieveFromDockerCredentialHelper( - Paths.get( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX - + inferredCredentialHelperSuffix))); + String registrySuffix = entry.getKey(); + if (imageReference.getRegistry().endsWith(registrySuffix)) { + String credentialHelper = entry.getValue(); + return Optional.of(retrieveFromDockerCredentialHelper(Paths.get(credentialHelper))); + } } catch (CredentialHelperNotFoundException | CredentialHelperUnhandledServerUrlException ex) { @@ -213,18 +211,55 @@ public CredentialRetriever dockerConfig(Path dockerConfigFile) { new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile)); } + /** + * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Google Application Default + * Credentials. + * + * @return a new {@link CredentialRetriever} + * @see https://cloud.google.com/docs/authentication/production + */ + public CredentialRetriever googleApplicationDefaultCredentials() { + return () -> { + try { + if (imageReference.getRegistry().endsWith("gcr.io")) { + GoogleCredentials googleCredentials = googleCredentialsProvider.get(); + logger.accept(LogEvent.info("Google ADC found")); + if (googleCredentials.createScopedRequired()) { // not scoped if service account + // The short-lived OAuth2 access token to be generated from the service account with + // refreshIfExpired() below will have one-hour expiry (as of Aug 2019). Instead of using + // an access token, it is technically possible to use the service account private key to + // auth with GCR, but it does not worth writing complex code to achieve that. + logger.accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); + List scope = Collections.singletonList(OAUTH_SCOPE_STORAGE_READ_WRITE); + googleCredentials = googleCredentials.createScoped(scope); + } + googleCredentials.refreshIfExpired(); + + logGotCredentialsFrom("Google Application Default Credentials"); + AccessToken accessToken = googleCredentials.getAccessToken(); + // https://cloud.google.com/container-registry/docs/advanced-authentication#access_token + return Optional.of(Credential.from("oauth2accesstoken", accessToken.getTokenValue())); + } + + } catch (IOException ex) { // Includes the case where ADC is simply not available. + logger.accept( + LogEvent.info("ADC not present or error fetching access token: " + ex.getMessage())); + } + return Optional.empty(); + }; + } + @VisibleForTesting CredentialRetriever dockerConfig( DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { return () -> { try { - Optional dockerConfigCredentials = - dockerConfigCredentialRetriever.retrieve(logger); - if (dockerConfigCredentials.isPresent()) { - logger.accept( - LogEvent.info( - "Using credentials from Docker config for " + imageReference.getRegistry())); - return dockerConfigCredentials; + Optional credentials = dockerConfigCredentialRetriever.retrieve(logger); + if (credentials.isPresent()) { + logGotCredentialsFrom("credentials from Docker config"); + return credentials; } } catch (IOException ex) { @@ -241,7 +276,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) dockerCredentialHelperFactory .create(imageReference.getRegistry(), credentialHelper) .retrieve(); - logGotCredentialsFrom(credentialHelper.getFileName().toString()); + logGotCredentialsFrom("credentials from " + credentialHelper.getFileName().toString()); return credentials; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 0e13727cde..968a5126c6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -61,7 +61,6 @@ public DockerConfigCredentialRetriever(String registry) { this(registry, DOCKER_CONFIG_FILE); } - @VisibleForTesting public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index b07cfc78ec..873d0ba339 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -40,8 +40,6 @@ */ public class DockerCredentialHelper { - public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-"; - private final String serverUrl; private final Path credentialHelper; @@ -65,7 +63,7 @@ public DockerCredentialHelper(String serverUrl, Path credentialHelper) { } DockerCredentialHelper(String registry, String credentialHelperSuffix) { - this(registry, Paths.get(CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix)); + this(registry, Paths.get("docker-credential-" + credentialHelperSuffix)); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index eb05866a7f..efe28fb3fa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.MoreExecutors; @@ -225,7 +224,7 @@ public void testContainerize_executorCreated() throws Exception { Containerizer mockContainerizer = createMockContainerizer(); - jibContainerBuilder.containerize(mockContainerizer, Suppliers.ofInstance(mockExecutorService)); + jibContainerBuilder.containerize(mockContainerizer, () -> mockExecutorService); Mockito.verify(mockExecutorService).shutdown(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 16537a52da..c01b3f4aef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.frontend; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; @@ -28,6 +30,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.Optional; import java.util.function.Consumer; import org.junit.Assert; @@ -44,98 +47,72 @@ public class CredentialRetrieverFactoryTest { private static final Credential FAKE_CREDENTIALS = Credential.from("username", "password"); - /** - * Returns a {@link DockerCredentialHelperFactory} that checks given parameters upon creating a - * {@link DockerCredentialHelper} instance. - * - * @param expectedRegistry the expected registry given to the factory - * @param expectedCredentialHelper the expected credential helper path given to the factory - * @param returnedCredentialHelper the mock credential helper to return - * @return a new {@link DockerCredentialHelperFactory} - */ - private static DockerCredentialHelperFactory getTestFactory( - String expectedRegistry, - Path expectedCredentialHelper, - DockerCredentialHelper returnedCredentialHelper) { - return (registry, credentialHelper) -> { - Assert.assertEquals(expectedRegistry, registry); - Assert.assertEquals(expectedCredentialHelper, credentialHelper); - return returnedCredentialHelper; - }; - } - @Mock private Consumer mockLogger; @Mock private DockerCredentialHelper mockDockerCredentialHelper; - @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; - - /** A {@link DockerCredentialHelper} that throws {@link CredentialHelperNotFoundException}. */ - @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - - @Mock private CredentialHelperNotFoundException mockCredentialHelperNotFoundException; + @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private GoogleCredentials mockGoogleCredentials; @Before public void setUp() throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { + Mockito.when( + mockDockerCredentialHelperFactory.create(Mockito.anyString(), Mockito.any(Path.class))) + .thenReturn(mockDockerCredentialHelper); Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); - Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockCredentialHelperNotFoundException); + Mockito.when(mockGoogleCredentials.getAccessToken()) + .thenReturn(new AccessToken("my-token", null)); } @Test public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("registry", "repository", null), - mockLogger, - getTestFactory( - "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); + createCredentialRetrieverFactory("registry", "repository"); Assert.assertEquals( - FAKE_CREDENTIALS, + Optional.of(FAKE_CREDENTIALS), credentialRetrieverFactory .dockerCredentialHelper(Paths.get("docker-credential-helper")) - .retrieve() - .orElseThrow(AssertionError::new)); - Mockito.verify(mockLogger).accept(LogEvent.info("Using docker-credential-helper for registry")); + .retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("registry", Paths.get("docker-credential-helper")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using credentials from docker-credential-helper for registry")); } @Test - public void testInferCredentialHelper() throws CredentialRetrievalException { + public void testWellKnownCredentialHelpers() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("something.gcr.io", "repository", null), - mockLogger, - getTestFactory( - "something.gcr.io", - Paths.get("docker-credential-gcr"), - mockDockerCredentialHelper)); + createCredentialRetrieverFactory("something.gcr.io", "repository"); Assert.assertEquals( - FAKE_CREDENTIALS, - credentialRetrieverFactory - .inferCredentialHelper() - .retrieve() - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("something.gcr.io", Paths.get("docker-credential-gcr")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); + .accept(LogEvent.info("Using credentials from docker-credential-gcr for something.gcr.io")); } @Test - public void testInferCredentialHelper_info() throws CredentialRetrievalException { + public void testWellKnownCredentialHelpers_info() + throws CredentialRetrievalException, IOException { + CredentialHelperNotFoundException notFoundException = + Mockito.mock(CredentialHelperNotFoundException.class); + Mockito.when(notFoundException.getMessage()).thenReturn("warning"); + Mockito.when(notFoundException.getCause()).thenReturn(new IOException("the root cause")); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenThrow(notFoundException); + CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("something.amazonaws.com", "repository", null), - mockLogger, - getTestFactory( - "something.amazonaws.com", - Paths.get("docker-credential-ecr-login"), - mockNonexistentDockerCredentialHelper)); - - Mockito.when(mockCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); - Mockito.when(mockCredentialHelperNotFoundException.getCause()) - .thenReturn(new IOException("the root cause")); - Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); + createCredentialRetrieverFactory("something.amazonaws.com", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve().isPresent()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("something.amazonaws.com", Paths.get("docker-credential-ecr-login")); Mockito.verify(mockLogger).accept(LogEvent.info("warning")); Mockito.verify(mockLogger).accept(LogEvent.info(" Caused by: the root cause")); } @@ -143,19 +120,121 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage( - ImageReference.of("registry", "repository", null), mockLogger); + createCredentialRetrieverFactory("registry", "repository"); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockLogger)) + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + Mockito.mock(DockerConfigCredentialRetriever.class); + Mockito.when(dockerConfigCredentialRetriever.retrieve(mockLogger)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( - FAKE_CREDENTIALS, - credentialRetrieverFactory - .dockerConfig(mockDockerConfigCredentialRetriever) - .retrieve() - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.dockerConfig(dockerConfigCredentialRetriever).retrieve()); + Mockito.verify(mockLogger) .accept(LogEvent.info("Using credentials from Docker config for registry")); } + + @Test + public void testGoogleApplicationDefaultCredentials_notGoogleContainerRegistry() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("non.gcr.registry", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verifyZeroInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_adcNotPresent() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + new CredentialRetrieverFactory( + ImageReference.of("awesome.gcr.io", "repository", null), + mockLogger, + mockDockerCredentialHelperFactory, + () -> { + throw new IOException("ADC not present"); + }); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: ADC not present")); + } + + @Test + public void testGoogleApplicationDefaultCredentials_refreshFailure() + throws CredentialRetrievalException, IOException { + Mockito.doThrow(new IOException("refresh failed")) + .when(mockGoogleCredentials) + .refreshIfExpired(); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: refresh failed")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_endUserCredentials() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials, Mockito.never()).createScoped(Mockito.anyString()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using Google Application Default Credentials for awesome.gcr.io")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_serviceAccount() + throws CredentialRetrievalException { + Mockito.when(mockGoogleCredentials.createScopedRequired()).thenReturn(true); + Mockito.when(mockGoogleCredentials.createScoped(Mockito.anyCollection())) + .thenReturn(mockGoogleCredentials); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("gcr.io", "repository"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials) + .createScoped( + Collections.singletonList("https://www.googleapis.com/auth/devstorage.read_write")); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using Google Application Default Credentials for gcr.io")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + private CredentialRetrieverFactory createCredentialRetrieverFactory( + String registry, String repository) { + ImageReference imageReference = ImageReference.of(registry, repository, null); + return new CredentialRetrieverFactory( + imageReference, mockLogger, mockDockerCredentialHelperFactory, () -> mockGoogleCredentials); + } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4af7c7ad99..7424776e00 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 2bf0cd04b3..997d6073f9 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -63,8 +63,10 @@ dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. compile 'com.google.http-client:google-http-client:1.31.0' compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' + compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile 'com.google.guava:guava:28.0-jre' + compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 70ec67d215..375d9db0ba 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index e407b4f3d4..9bda66f030 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -63,15 +63,20 @@ compile
- org.apache.commons - commons-compress - 1.18 - compile + com.google.auth + google-auth-library-oauth2-http + 0.16.2 com.google.guava guava - 27.0.1-jre + 28.0-jre + compile + + + org.apache.commons + commons-compress + 1.18 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 52330c3077..81ea47811f 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -31,8 +31,10 @@ dependencies { // Make sure these are consistent with jib-maven-plugin. compile 'com.google.http-client:google-http-client:1.31.0' compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' + compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile 'com.google.guava:guava:28.0-jre' + compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 399d3cc2d6..97b6633d7f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import java.io.FileNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -34,11 +33,14 @@ *

The retrievers are, in order of first-checked to last-checked: * *

    + *
  1. {@link CredentialRetrieverFactory#known} for known credential, if set *
  2. {@link CredentialRetrieverFactory#dockerCredentialHelper} for a known credential helper, if * set - *
  3. {@link CredentialRetrieverFactory#known} for known credential, if set - *
  4. {@link CredentialRetrieverFactory#inferCredentialHelper} + *
  5. {@link CredentialRetrieverFactory#known} for known inferred credential, if set *
  6. {@link CredentialRetrieverFactory#dockerConfig} + *
  7. {@link CredentialRetrieverFactory#wellKnownCredentialHelpers} for well-known credential + * helper-registry pairs + *
  8. {@link CredentialRetrieverFactory#googleApplicationDefaultCredentials} for GCR registry *
*/ public class DefaultCredentialRetrievers { @@ -121,24 +123,24 @@ public List asList() throws FileNotFoundException { if (credentialHelper != null) { // If credential helper contains file separator, treat as path; otherwise treat as suffix if (credentialHelper.contains(FileSystems.getDefault().getSeparator())) { - if (Files.exists(Paths.get(credentialHelper))) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelper)); - } else { + if (!Files.exists(Paths.get(credentialHelper))) { throw new FileNotFoundException( "Specified credential helper was not found: " + credentialHelper); } + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelper)); } else { + String suffix = credentialHelper; // not path; treat as suffix credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + credentialHelper)); + credentialRetrieverFactory.dockerCredentialHelper("docker-credential-" + suffix)); } } if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); } credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.wellKnownCredentialHelpers()); + credentialRetrievers.add(credentialRetrieverFactory.googleApplicationDefaultCredentials()); return credentialRetrievers; } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 2d2ac99e3c..1d589ab43b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -39,14 +39,15 @@ @RunWith(MockitoJUnitRunner.class) public class DefaultCredentialRetrieversTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private CredentialRetrieverFactory mockCredentialRetrieverFactory; @Mock private CredentialRetriever mockDockerCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockKnownCredentialRetriever; @Mock private CredentialRetriever mockInferredCredentialRetriever; - @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; + @Mock private CredentialRetriever mockWellKnownCredentialHelpersCredentialRetriever; @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockApplicationDefaultCredentialRetriever; private final Credential knownCredential = Credential.from("username", "password"); private final Credential inferredCredential = Credential.from("username2", "password2"); @@ -60,10 +61,12 @@ public void setUp() { Mockito.when( mockCredentialRetrieverFactory.known(inferredCredential, "inferredCredentialSource")) .thenReturn(mockInferredCredentialRetriever); - Mockito.when(mockCredentialRetrieverFactory.inferCredentialHelper()) - .thenReturn(mockInferCredentialHelperCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.wellKnownCredentialHelpers()) + .thenReturn(mockWellKnownCredentialHelpersCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.dockerConfig()) .thenReturn(mockDockerConfigCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.googleApplicationDefaultCredentials()) + .thenReturn(mockApplicationDefaultCredentialRetriever); } @Test @@ -72,7 +75,9 @@ public void testInitAsList() throws FileNotFoundException { DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); Assert.assertEquals( Arrays.asList( - mockDockerConfigCredentialRetriever, mockInferCredentialHelperCredentialRetriever), + mockDockerConfigCredentialRetriever, + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); } @@ -90,7 +95,8 @@ public void testInitAsList_all() throws FileNotFoundException { mockDockerCredentialHelperCredentialRetriever, mockInferredCredentialRetriever, mockDockerConfigCredentialRetriever, - mockInferCredentialHelperCredentialRetriever), + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource"); @@ -112,7 +118,8 @@ public void testInitAsList_credentialHelperPath() throws IOException { Arrays.asList( mockDockerCredentialHelperCredentialRetriever, mockDockerConfigCredentialRetriever, - mockInferCredentialHelperCredentialRetriever), + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory) .dockerCredentialHelper(fakeCredentialHelperPath.toString()); From e1cfd8c0490095b70abb2c9364166865fdfbedaf Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 16 Aug 2019 11:37:41 -0400 Subject: [PATCH 0688/2020] Gradle release v1.5.0 (#1908) * [Gradle Release Plugin] - pre tag commit: 'v1.5.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.5.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 897ec68146..ced16a7bbb 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.4.1-SNAPSHOT +version = 1.5.1-SNAPSHOT From 998ed0738353be8e80c24d769382ad40e037dc4c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 16 Aug 2019 11:40:14 -0400 Subject: [PATCH 0689/2020] Core release v0.10.1 (#1910) * [Gradle Release Plugin] - pre tag commit: 'v0.10.1-core'. * [Gradle Release Plugin] - new version commit: 'v0.10.2-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 0340ae781e..3b0f75f558 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.10.1-SNAPSHOT +version = 0.10.2-SNAPSHOT From d3c23093bd08d71ee8d85a8816f88ae300f9b63b Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 16 Aug 2019 12:25:48 -0400 Subject: [PATCH 0690/2020] Maven release v1.5.0 (#1909) * preparing release 1.5.0 * 1.5.1-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 9bda66f030..f50fe24b4a 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.4.1-SNAPSHOT + 1.5.1-SNAPSHOT maven-plugin Jib From 6a93c356702f0e43d55d5e44494026935bc4c5b9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 16 Aug 2019 13:39:19 -0400 Subject: [PATCH 0691/2020] Update version strings for 1.5.0 plugins/0.10.1 core (#1911) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 10 ++++++++-- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 19 files changed, 44 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2caef8984..c21937c098 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,12 +146,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.4.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.5.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.4.0' + // id 'com.google.cloud.tools.jib' version '1.5.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index aadcd82d2a..97a26f43eb 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.4.0 + 1.5.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 5ff1c98e78..21e44e48d0 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 0fb72c37b1..67cbc8d5f9 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.4.0 + 1.5.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 8b102c1fae..cb9796a8f7 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index cfea3c172e..5f92d888ed 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.4.0 + 1.5.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index ade4c4a3f2..39e8e533e2 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index e4e68d20f8..7e66712afb 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.4.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.5.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index d98e5ba294..77b4b4a0f3 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.4.0 + 1.5.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 4e9d843337..0add96cf62 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index b472af67ca..6732cd25ae 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.4.0 + 1.5.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 3a48402f84..2f60ee1c3c 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 28548086de..cbdc27c657 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.10.1 + +### Added + - `JavaContainerBuilder#setLastModifiedTimeProvider` to set file timestamps ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) ### Changed diff --git a/jib-core/README.md b/jib-core/README.md index e75c387c33..a6cadff315 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.10.0 + 0.10.1 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.10.0' + compile 'com.google.cloud.tools:jib-core:0.10.1' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index b881eff264..30ec935d84 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.10.0' + classpath 'com.google.cloud.tools:jib-core:0.10.1' } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7424776e00..7500fa1b27 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.5.0 + +### Added + - Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) - For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) @@ -14,8 +22,6 @@ All notable changes to this project will be documented in this file. - When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) - `jib.container.useCurrentTimestamp` has been deprecated in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) -### Fixed - ## 1.4.0 ### Added diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 023ed53876..b1d4a8fb92 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.4.0' + id 'com.google.cloud.tools.jib' version '1.5.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 375d9db0ba..3285553815 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.5.0 + +### Added + - Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) - For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) @@ -14,8 +22,6 @@ All notable changes to this project will be documented in this file. - When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) - `` has been deprecated in favor of `` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) -### Fixed - ## 1.4.0 ### Added diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 4032edfd5c..79c47ce245 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.4.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.4.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.4.0 + 1.5.0 myimage From a177e3e7d5740b4545be3dc38433071abec6607f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 16 Aug 2019 13:47:21 -0400 Subject: [PATCH 0692/2020] Update README and FAQ for #1818, #1814, and #1888 (#1866) --- docs/faq.md | 10 ++++++---- jib-gradle-plugin/README.md | 3 ++- jib-maven-plugin/README.md | 11 ++++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 3cc832f571..21f4592dea 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -103,10 +103,10 @@ See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.for ### Why is my image created 48+ years ago? -For reproducibility purposes, Jib sets the creation time of the container images to 1 second past the Unix epoch (00:00:01, January 1st, 1970 in UTC). If you would like to forgo reproducibility and use the real creation time, set the `useCurrentTimestamp` parameter to `true` in your build configuration. +For reproducibility purposes, Jib sets the creation time of the container images to the Unix epoch (00:00:00, January 1st, 1970 in UTC). If you would like to use a different timestamp, set the `jib.container.creationTime` / `` parameter to an ISO 8601 date-time. You may also use the value `USE_CURRENT TIMESTAMP` to set the creation time to the actual build time, but this sacrifices reproducibility since the timestamp will change with every build.
-Setting `useCurrentTimestamp` parameter +Setting `creationTime` parameter

#### Maven @@ -114,7 +114,7 @@ For reproducibility purposes, Jib sets the creation time of the container images ```xml - true + 2019-07-15T10:15:30+09:00 ``` @@ -122,12 +122,14 @@ For reproducibility purposes, Jib sets the creation time of the container images #### Gradle ```groovy -jib.container.useCurrentTimestamp = true +jib.container.creationTime = '2019-07-15T10:15:30+09:00' ```

+Note that the modification time of the files in the built image put by Jib will still be 1 second past the epoch. The file modification time can be configured using [``](../jib-maven-plugin#container-object) (Maven) or [`jib.container.filesModificationTime`](../jib-gradle-plugin#container-closure) (Gradle). + #### Please tell me more about reproducibility! _Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index b1d4a8fb92..64d8037f54 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -209,15 +209,16 @@ Property | Type | Default | Description --- | --- | --- | --- `appRoot` | `String` | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. `args` | `List` | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. +`creationTime` | `String` | `EPOCH` | Sets the container creation time. (Note that this property does not affect the file modification times, which are configured using `jib.container.filesModificationTime`.) The value can be `EPOCH` to set the timestamps to Epoch (default behavior), `USE_CURRENT_TIMESTAMP` to forgo reproducibility and use the real creation time, or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. `entrypoint` | `List` | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `jib.container.entrypoint = 'INHERIT'` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | `Map` | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). `extraClasspath` | `List` | *None* | Additional paths in the container to prepend to the computed Java classpath. +`filesModificationTime` | `String` | `EPOCH_PLUS_SECOND` | Sets the modification time (last modified time) of files in the image put by Jib. (Note that this does not set the image creation time, which can be set using `jib.container.creationTime`.) The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. `format` | `String` | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | `List` | *None* | Additional flags to pass into the JVM when running your application. `labels` | `Map` | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `mainClass` | `String` | *Inferred*\*\* | The main class to launch your application from. `ports` | `List` | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). -`useCurrentTimestamp` | `boolean` | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. `user` | `String` | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. `volumes` | `List` | *None* | Specifies a list of mount points on the container. `workingDirectory` | `String` | *None* | The working directory in the container. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 79c47ce245..db8a9bf6d4 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -246,8 +246,8 @@ Property | Type | Default | Description Property | Type --- | --- -`username` | `String` -`password` | `String` +`username` | string +`password` | string `container` is an object with the following properties: @@ -255,15 +255,16 @@ Property | Type | Default | Description --- | --- | --- | --- `appRoot` | string | `/app` | The root directory on the container where the app's contents are placed. Particularly useful for WAR-packaging projects to work with different Servlet engine base images by designating where to put exploded WAR contents; see [WAR usage](#war-projects) as an example. `args` | list | *None* | Additional program arguments appended to the command to start the container (similar to Docker's [CMD](https://docs.docker.com/engine/reference/builder/#cmd) instruction in relation with [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)). In the default case where you do not set a custom `entrypoint`, this parameter is effectively the arguments to the main method of your Java application. +`creationTime` | string | `EPOCH` | Sets the container creation time. (Note that this property does not affect the file modification times, which are configured using ``.) The value can be `EPOCH` to set the timestamps to Epoch (default behavior), `USE_CURRENT_TIMESTAMP` to forgo reproducibility and use the real creation time, or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. `entrypoint` | list | *None* | The command to start the container with (similar to Docker's [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint) instruction). If set, then `jvmFlags` and `mainClass` are ignored. You may also set `INHERIT` to indicate that the `entrypoint` and `args` should be inherited from the base image.\* `environment` | map | *None* | Key-value pairs for setting environment variables on the container (similar to Docker's [ENV](https://docs.docker.com/engine/reference/builder/#env) instruction). -`extraClasspath` | `list` | *None* | Additional paths in the container to prepend to the computed Java classpath. +`extraClasspath` | list | *None* | Additional paths in the container to prepend to the computed Java classpath. +`filesModificationTime` | string | `EPOCH_PLUS_SECOND` | Sets the modification time (last modified time) of files in the image put by Jib. (Note that this does not set the image creation time, which can be set using ``.) The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date-time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_DATE_TIME) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. `format` | string | `Docker` | Use `OCI` to build an [OCI container image](https://www.opencontainers.org/). `jvmFlags` | list | *None* | Additional flags to pass into the JVM when running your application. `labels` | map | *None* | Key-value pairs for applying image metadata (similar to Docker's [LABEL](https://docs.docker.com/engine/reference/builder/#label) instruction). `mainClass` | string | *Inferred*\*\* | The main class to launch the application from. `ports` | list | *None* | Ports that the container exposes at runtime (similar to Docker's [EXPOSE](https://docs.docker.com/engine/reference/builder/#expose) instruction). -`useCurrentTimestamp` | boolean | `false` | By default, Jib wipes all timestamps to guarantee reproducibility. If this parameter is set to `true`, Jib will set the image's creation timestamp to the time of the build, which sacrifices reproducibility for easily being able to tell when your image was created. `user` | string | *None* | The user and group to run the container as. The value can be a username or UID along with an optional groupname or GID. The following are all valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. `volumes` | list | *None* | Specifies a list of mount points on the container. `workingDirectory` | string | *None* | The working directory in the container. @@ -305,7 +306,7 @@ Property | Type | Default | Description `jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects (i.e. `jib.baseImageCache` defaults to `[project dir]/target/jib-cache` instead of `[user cache home]/google-cloud-tools-java/jib`). `jib.baseImageCache` | string | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. `jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. -`jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. +`jib.console` | string | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. *\* If you configure `args` while `entrypoint` is set to `'INHERIT'`, the configured `args` value will take precedence over the CMD propagated from the base image.* From 206d2f5284852f36163d73b05429075056c3e972 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 16 Aug 2019 19:36:40 -0400 Subject: [PATCH 0693/2020] Add ExtractTarStep and SaveDockerStep (#1906) --- .../google/cloud/tools/jib/blob/Blobs.java | 16 ++ .../jib/builder/steps/ExtractTarStep.java | 150 ++++++++++++++++++ .../jib/builder/steps/SaveDockerStep.java | 48 ++++++ .../cloud/tools/jib/cache/CachedLayer.java | 14 +- .../tools/jib/filesystem/FileOperations.java | 21 +++ .../json/ContainerConfigurationTemplate.java | 8 +- .../tools/jib/json/JsonTemplateMapper.java | 28 ++-- .../jib/builder/steps/ExtractTarStepTest.java | 91 +++++++++++ .../jib/cache/CacheStorageWriterTest.java | 17 +- .../cloud/tools/jib/cache/CacheTest.java | 32 +--- .../core/extraction/compressed.tar.gz | Bin 0 -> 200 bytes .../resources/core/extraction/docker-save.tar | Bin 0 -> 28160 bytes .../resources/core/extraction/jib-image.tar | Bin 0 -> 8704 bytes .../core/extraction/not-compressed.tar | Bin 0 -> 2560 bytes 14 files changed, 357 insertions(+), 68 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java create mode 100644 jib-core/src/test/resources/core/extraction/compressed.tar.gz create mode 100644 jib-core/src/test/resources/core/extraction/docker-save.tar create mode 100644 jib-core/src/test/resources/core/extraction/jib-image.tar create mode 100644 jib-core/src/test/resources/core/extraction/not-compressed.tar diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 643572f529..062f02dc0e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.zip.GZIPOutputStream; /** Static methods for {@link Blob}. */ public class Blobs { @@ -77,5 +78,20 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { return byteArrayOutputStream.toByteArray(); } + /** + * Gets a {@link Blob} that is {@code blob} compressed. + * + * @param blob the {@link Blob} to compress + * @return the compressed {@link Blob} + */ + public static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }); + } + private Blobs() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java new file mode 100644 index 0000000000..62bb7cd6ea --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.tar.TarExtractor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.zip.GZIPInputStream; + +/** Extracts a tar file base image. */ +public class ExtractTarStep implements Callable { + + /** Contains an {@link Image} and its layers. * */ + static class LocalImage { + final Image baseImage; + final List layers; + + LocalImage(Image baseImage, List layers) { + this.baseImage = baseImage; + this.layers = layers; + } + } + + /** + * Checks the first two bytes of a file to see if it has been gzipped. + * + * @param path the file to check + * @return {@code true} if the file is gzipped, {@code false} if not + * @throws IOException if reading the file fails + * @see GZIP file format + */ + @VisibleForTesting + static boolean isGzipped(Path path) throws IOException { + try (InputStream inputStream = Files.newInputStream(path)) { + inputStream.mark(2); + int magic = (inputStream.read() & 0xff) | ((inputStream.read() << 8) & 0xff00); + return magic == GZIPInputStream.GZIP_MAGIC; + } + } + + private final Path tarPath; + private final Path destination; + + ExtractTarStep(Path tarPath, Path destination) { + this.tarPath = tarPath; + this.destination = destination; + } + + @Override + public LocalImage call() + throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException { + Files.createDirectories(destination); + FileOperations.deleteRecursiveOnExit(destination); + TarExtractor.extract(tarPath, destination); + + InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); + DockerManifestEntryTemplate loadManifest = + new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; + manifestStream.close(); + ContainerConfigurationTemplate configurationTemplate = + JsonTemplateMapper.readJsonFromFile( + destination.resolve(loadManifest.getConfig()), ContainerConfigurationTemplate.class); + + List layerFiles = loadManifest.getLayerFiles(); + if (configurationTemplate.getLayerCount() != layerFiles.size()) { + throw new LayerCountMismatchException( + "Invalid base image format: manifest contains " + + layerFiles.size() + + " layers, but container configuration contains " + + configurationTemplate.getLayerCount() + + " layers"); + } + + // Check the first layer to see if the layers are compressed already. 'docker save' output is + // uncompressed, but a jib-built tar has compressed layers. + boolean layersAreCompressed = + layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); + + // Process layer blobs + // TODO: Optimize; compressing/calculating layer digests is slow + List layers = new ArrayList<>(); + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + for (int index = 0; index < layerFiles.size(); index++) { + Path file = destination.resolve(layerFiles.get(index)); + + // Compress layers if necessary and calculate the digest/size + Blob blob = layersAreCompressed ? Blobs.from(file) : Blobs.compress(Blobs.from(file)); + BlobDescriptor blobDescriptor = blob.writeTo(ByteStreams.nullOutputStream()); + + // 'manifest' contains the layer files in the same order as the diff ids in 'configuration', + // so we don't need to recalculate those. + // https://containers.gitbook.io/build-containers-the-hard-way/#docker-load-format + CachedLayer layer = + CachedLayer.builder() + .setLayerBlob(blob) + .setLayerDigest(blobDescriptor.getDigest()) + .setLayerSize(blobDescriptor.getSize()) + .setLayerDiffId(configurationTemplate.getLayerDiffId(index)) + .build(); + + layers.add(new PreparedLayer.Builder(layer).build()); + v22Manifest.addLayer(blobDescriptor.getSize(), blobDescriptor.getDigest()); + } + + BlobDescriptor configDescriptor = + Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); + v22Manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); + Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); + return new LocalImage(image, layers); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java new file mode 100644 index 0000000000..789781aa35 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.filesystem.FileOperations; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +/** Saves an image from the docker daemon. */ +public class SaveDockerStep implements Callable { + + private final BuildConfiguration buildConfiguration; + private final DockerClient dockerClient; + + SaveDockerStep(BuildConfiguration buildConfiguration, DockerClient dockerClient) { + this.buildConfiguration = buildConfiguration; + this.dockerClient = dockerClient; + } + + @Override + public Path call() throws IOException, InterruptedException { + Path outputDir = Files.createTempDirectory("jib-docker-save"); + FileOperations.deleteRecursiveOnExit(outputDir); + Path outputPath = outputDir.resolve("out.tar"); + ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); + dockerClient.save(imageReference, outputPath); + return outputPath; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java index 43a543693a..8efa2defb0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java @@ -27,7 +27,7 @@ public class CachedLayer implements Layer { /** Builds a {@link CachedLayer}. */ - static class Builder { + public static class Builder { @Nullable private DescriptorDigest layerDigest; @Nullable private DescriptorDigest layerDiffId; @@ -36,22 +36,22 @@ static class Builder { private Builder() {} - Builder setLayerDigest(DescriptorDigest layerDigest) { + public Builder setLayerDigest(DescriptorDigest layerDigest) { this.layerDigest = layerDigest; return this; } - Builder setLayerDiffId(DescriptorDigest layerDiffId) { + public Builder setLayerDiffId(DescriptorDigest layerDiffId) { this.layerDiffId = layerDiffId; return this; } - Builder setLayerSize(long layerSize) { + public Builder setLayerSize(long layerSize) { this.layerSize = layerSize; return this; } - Builder setLayerBlob(Blob layerBlob) { + public Builder setLayerBlob(Blob layerBlob) { this.layerBlob = layerBlob; return this; } @@ -60,7 +60,7 @@ boolean hasLayerBlob() { return layerBlob != null; } - CachedLayer build() { + public CachedLayer build() { return new CachedLayer( Preconditions.checkNotNull(layerDigest, "layerDigest required"), Preconditions.checkNotNull(layerDiffId, "layerDiffId required"), @@ -74,7 +74,7 @@ CachedLayer build() { * * @return the new {@link Builder} */ - static Builder builder() { + public static Builder builder() { return new Builder(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java index d4f6e09839..5a36dbec78 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.filesystem; import com.google.common.collect.ImmutableList; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; @@ -85,5 +87,24 @@ public static OutputStream newLockingOutputStream(Path file) throws IOException return Channels.newOutputStream(channel); } + /** + * Sets up a shutdown hook that tries to delete a file or directory. + * + * @param path the path to the file or directory + */ + public static void deleteRecursiveOnExit(Path path) { + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + if (Files.exists(path)) { + try { + MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); + } catch (IOException ignored) { + } + } + })); + } + private FileOperations() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java index 895f59d8c0..cbf776a82e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; @@ -369,8 +368,11 @@ String getContainerUser() { return config.Volumes; } - @VisibleForTesting - DescriptorDigest getLayerDiffId(int index) { + public DescriptorDigest getLayerDiffId(int index) { return rootfs.diff_ids.get(index); } + + public int getLayerCount() { + return rootfs.diff_ids.size(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index 45747d3f29..916a3d8540 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.json; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.CollectionType; import java.io.ByteArrayOutputStream; @@ -134,50 +132,42 @@ public static List readListOfJson( return objectMapper.readValue(jsonString, listType); } - public static String toUtf8String(JsonTemplate template) - throws JsonGenerationException, JsonMappingException, IOException { + public static String toUtf8String(JsonTemplate template) throws IOException { return toUtf8String((Object) template); } - public static String toUtf8String(List templates) - throws JsonGenerationException, JsonMappingException, IOException { + public static String toUtf8String(List templates) throws IOException { return toUtf8String((Object) templates); } - public static byte[] toByteArray(JsonTemplate template) - throws JsonGenerationException, JsonMappingException, IOException { + public static byte[] toByteArray(JsonTemplate template) throws IOException { return toByteArray((Object) template); } - public static byte[] toByteArray(List templates) - throws JsonGenerationException, JsonMappingException, IOException { + public static byte[] toByteArray(List templates) throws IOException { return toByteArray((Object) templates); } - public static void writeTo(JsonTemplate template, OutputStream out) - throws JsonGenerationException, JsonMappingException, IOException { + public static void writeTo(JsonTemplate template, OutputStream out) throws IOException { writeTo((Object) template, out); } public static void writeTo(List templates, OutputStream out) - throws JsonGenerationException, JsonMappingException, IOException { + throws IOException { writeTo((Object) templates, out); } - private static String toUtf8String(Object template) - throws JsonGenerationException, JsonMappingException, IOException { + private static String toUtf8String(Object template) throws IOException { return new String(toByteArray(template), StandardCharsets.UTF_8); } - private static byte[] toByteArray(Object template) - throws JsonGenerationException, JsonMappingException, IOException { + private static byte[] toByteArray(Object template) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); writeTo(template, out); return out.toByteArray(); } - private static void writeTo(Object template, OutputStream out) - throws JsonGenerationException, JsonMappingException, IOException { + private static void writeTo(Object template, OutputStream out) throws IOException { objectMapper.writeValue(out, template); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java new file mode 100644 index 0000000000..664743ab89 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ExtractTarStepTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static Path getResource(String resource) throws URISyntaxException { + return Paths.get(Resources.getResource(resource).toURI()); + } + + @Test + public void testCall_validDocker() + throws URISyntaxException, LayerCountMismatchException, + BadContainerConfigurationFormatException, IOException { + Path dockerBuild = getResource("core/extraction/docker-save.tar"); + LocalImage result = new ExtractTarStep(dockerBuild, temporaryFolder.getRoot().toPath()).call(); + + Assert.assertEquals(2, result.layers.size()); + Assert.assertEquals( + "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", + result.layers.get(0).getDiffId().getHash()); + Assert.assertEquals( + "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", + result.layers.get(0).getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals( + "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", + result.layers.get(1).getDiffId().getHash()); + Assert.assertEquals( + "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", + result.layers.get(1).getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals("value1", result.baseImage.getLabels().get("label1")); + } + + @Test + public void testCall_validTar() + throws URISyntaxException, LayerCountMismatchException, + BadContainerConfigurationFormatException, IOException { + Path tarBuild = getResource("core/extraction/jib-image.tar"); + LocalImage result = new ExtractTarStep(tarBuild, temporaryFolder.getRoot().toPath()).call(); + + Assert.assertEquals(2, result.layers.size()); + Assert.assertEquals( + "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", + result.layers.get(0).getDiffId().getHash()); + Assert.assertEquals( + "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", + result.layers.get(0).getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals( + "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", + result.layers.get(1).getDiffId().getHash()); + Assert.assertEquals( + "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", + result.layers.get(1).getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals("value1", result.baseImage.getLabels().get("label1")); + } + + @Test + public void testIsGzipped() throws URISyntaxException, IOException { + Assert.assertTrue(ExtractTarStep.isGzipped(getResource("core/extraction/compressed.tar.gz"))); + Assert.assertFalse(ExtractTarStep.isGzipped(getResource("core/extraction/not-compressed.tar"))); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index ecf48e2f40..e5bcdb6466 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -35,7 +35,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -49,15 +48,6 @@ private static BlobDescriptor getDigest(Blob blob) throws IOException { return blob.writeTo(ByteStreams.nullOutputStream()); } - private static Blob compress(Blob blob) { - return Blobs.from( - outputStream -> { - try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { - blob.writeTo(compressorStream); - } - }); - } - private static Blob decompress(Blob blob) throws IOException { return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); } @@ -78,7 +68,8 @@ public void testWrite_compressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); CachedLayer cachedLayer = - new CacheStorageWriter(cacheStorageFiles).writeCompressed(compress(uncompressedLayerBlob)); + new CacheStorageWriter(cacheStorageFiles) + .writeCompressed(Blobs.compress(uncompressedLayerBlob)); verifyCachedLayer(cachedLayer, uncompressedLayerBlob); } @@ -86,7 +77,7 @@ public void testWrite_compressed() throws IOException { @Test public void testWrite_uncompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); - DescriptorDigest layerDigest = getDigest(compress(uncompressedLayerBlob)).getDigest(); + DescriptorDigest layerDigest = getDigest(Blobs.compress(uncompressedLayerBlob)).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")).getDigest(); CachedLayer cachedLayer = @@ -159,7 +150,7 @@ public void testWriteMetadata_v22() private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { - BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); + BlobDescriptor layerBlobDescriptor = getDigest(Blobs.compress(uncompressedLayerBlob)); DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob).getDigest(); // Verifies cachedLayer is correct. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 10408c3113..6d8533ba8c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -24,7 +24,6 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; -import com.google.common.io.CountingOutputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; @@ -33,7 +32,6 @@ import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -43,21 +41,6 @@ /** Tests for {@link Cache}. */ public class CacheTest { - /** - * Gets a {@link Blob} that is {@code blob} compressed. - * - * @param blob the {@link Blob} to compress - * @return the compressed {@link Blob} - */ - private static Blob compress(Blob blob) { - return Blobs.from( - outputStream -> { - try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { - blob.writeTo(compressorStream); - } - }); - } - /** * Gets a {@link Blob} that is {@code blob} decompressed. * @@ -88,10 +71,7 @@ private static DescriptorDigest digestOf(Blob blob) throws IOException { * @throws IOException if an I/O exception occurs */ private static long sizeOf(Blob blob) throws IOException { - CountingOutputStream countingOutputStream = - new CountingOutputStream(ByteStreams.nullOutputStream()); - blob.writeTo(countingOutputStream); - return countingOutputStream.getCount(); + return blob.writeTo(ByteStreams.nullOutputStream()).getSize(); } private static LayerEntry defaultLayerEntry(Path source, AbsoluteUnixPath destination) { @@ -125,9 +105,9 @@ public void setUp() throws IOException { Files.createFile(directory.resolve("another/source/file")); layerBlob1 = Blobs.from("layerBlob1"); - layerDigest1 = digestOf(compress(layerBlob1)); + layerDigest1 = digestOf(Blobs.compress(layerBlob1)); layerDiffId1 = digestOf(layerBlob1); - layerSize1 = sizeOf(compress(layerBlob1)); + layerSize1 = sizeOf(Blobs.compress(layerBlob1)); layerEntries1 = ImmutableList.of( defaultLayerEntry( @@ -137,9 +117,9 @@ public void setUp() throws IOException { AbsoluteUnixPath.get("/another/extraction/path"))); layerBlob2 = Blobs.from("layerBlob2"); - layerDigest2 = digestOf(compress(layerBlob2)); + layerDigest2 = digestOf(Blobs.compress(layerBlob2)); layerDiffId2 = digestOf(layerBlob2); - layerSize2 = sizeOf(compress(layerBlob2)); + layerSize2 = sizeOf(Blobs.compress(layerBlob2)); layerEntries2 = ImmutableList.of(); } @@ -161,7 +141,7 @@ public void testWriteCompressed_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1(cache.writeCompressedLayer(compress(layerBlob1))); + verifyIsLayer1(cache.writeCompressedLayer(Blobs.compress(layerBlob1))); verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } diff --git a/jib-core/src/test/resources/core/extraction/compressed.tar.gz b/jib-core/src/test/resources/core/extraction/compressed.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..bb55593d55d53daa92e66a9e38cc5030e005fc5f GIT binary patch literal 200 zcmV;(05|_1iwFQnKvZ1-1MSnj4uUWcz;R}t;+{a;yIzX}yQ8l_q)`$Gq(XvkZ-uDw zYix|6zokvv;X3?x#kjRaY2Es0>$*)s5JG6BATbk2kPRcj^?b$>Br&p3evcreCZ+_& zAk(}UPJQA0DE+>5Wo^H{>s-|o?tF0V(0#g}e**XQPe=trRpv~*(K-seS3IG6t Cnqp)C literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/core/extraction/docker-save.tar b/jib-core/src/test/resources/core/extraction/docker-save.tar new file mode 100644 index 0000000000000000000000000000000000000000..08ba90b4c97be0d68dac49b16872ac9c8795a489 GIT binary patch literal 28160 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 literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/core/extraction/jib-image.tar b/jib-core/src/test/resources/core/extraction/jib-image.tar new file mode 100644 index 0000000000000000000000000000000000000000..1281542d7edf0aac71d9c1a6f65a6ade52db71a3 GIT binary patch literal 8704 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 Date: Wed, 21 Aug 2019 11:29:16 -0400 Subject: [PATCH 0694/2020] Refactor DockerCredentialHelper to remove suffix constructor (#1920) --- .../DockerCredentialHelperIntegrationTest.java | 15 +++++++++------ .../jib/registry/credentials/DockerConfig.java | 7 +++++-- .../credentials/DockerCredentialHelper.java | 5 ----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 1f4bf6970b..7bdb07a790 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -30,15 +30,19 @@ /** Integration tests for {@link DockerCredentialHelper}. */ public class DockerCredentialHelperIntegrationTest { + // This binary must exist and be functioning properly for these tests to succeed. + private static final String GCR_CREDENTIAL_HELPER = "docker-credential-gcr"; + /** Tests retrieval via {@code docker-credential-gcr} CLI. */ @Test public void testRetrieveGCR() throws IOException, CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, URISyntaxException, InterruptedException { - new Command("docker-credential-gcr", "store") + new Command(GCR_CREDENTIAL_HELPER, "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); - DockerCredentialHelper dockerCredentialHelper = new DockerCredentialHelper("myregistry", "gcr"); + DockerCredentialHelper dockerCredentialHelper = + new DockerCredentialHelper("myregistry", Paths.get(GCR_CREDENTIAL_HELPER)); Credential credentials = dockerCredentialHelper.retrieve(); Assert.assertEquals("myusername", credentials.getUsername()); @@ -50,15 +54,14 @@ public void testRetrieve_nonexistentCredentialHelper() throws IOException, CredentialHelperUnhandledServerUrlException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelper("", "fake-cloud-provider"); + new DockerCredentialHelper("", Paths.get("non-existing-helper")); fakeDockerCredentialHelper.retrieve(); Assert.fail("Retrieve should have failed for nonexistent credential helper"); } catch (CredentialHelperNotFoundException ex) { - Assert.assertEquals( - "The system does not have docker-credential-fake-cloud-provider CLI", ex.getMessage()); + Assert.assertEquals("The system does not have non-existing-helper CLI", ex.getMessage()); } } @@ -67,7 +70,7 @@ public void testRetrieve_nonexistentServerUrl() throws IOException, CredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = - new DockerCredentialHelper("fake.server.url", "gcr"); + new DockerCredentialHelper("fake.server.url", Paths.get(GCR_CREDENTIAL_HELPER)); fakeDockerCredentialHelper.retrieve(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index 2dfd2800b1..f613a1a62e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -103,14 +104,16 @@ DockerCredentialHelper getCredentialHelperFor(String registry) { findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); if (firstAuthMatch != null && dockerConfigTemplate.getCredsStore() != null) { return new DockerCredentialHelper( - firstAuthMatch.getKey(), dockerConfigTemplate.getCredsStore()); + firstAuthMatch.getKey(), + Paths.get("docker-credential-" + dockerConfigTemplate.getCredsStore())); } Map.Entry firstCredHelperMatch = findFirstInMapByKey(dockerConfigTemplate.getCredHelpers(), registryMatchers); if (firstCredHelperMatch != null) { return new DockerCredentialHelper( - firstCredHelperMatch.getKey(), firstCredHelperMatch.getValue()); + firstCredHelperMatch.getKey(), + Paths.get("docker-credential-" + firstCredHelperMatch.getValue())); } return null; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index 873d0ba339..ae7c745e07 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -29,7 +29,6 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.nio.file.Paths; import javax.annotation.Nullable; /** @@ -62,10 +61,6 @@ public DockerCredentialHelper(String serverUrl, Path credentialHelper) { this.credentialHelper = credentialHelper; } - DockerCredentialHelper(String registry, String credentialHelperSuffix) { - this(registry, Paths.get("docker-credential-" + credentialHelperSuffix)); - } - /** * Calls the credential helper CLI in the form: * From 7bb9167e13064d053271d00ba08fefbff13e2a2d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 21 Aug 2019 16:40:06 -0400 Subject: [PATCH 0695/2020] Fix a JavaContainerBuilder error message (#1925) --- .../com/google/cloud/tools/jib/api/JavaContainerBuilder.java | 4 ++-- .../google/cloud/tools/jib/api/JavaContainerBuilderTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index de99f39a9c..cda0a64dd6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -514,8 +514,8 @@ public JibContainerBuilder toContainerBuilder() throws IOException { throw new IllegalStateException( "Failed to construct entrypoint on JavaContainerBuilder; " + "jvmFlags were set, but mainClass is null. Specify the main class using " - + "JavaContainerBuilder#setMainClass(String), or consider using a " - + "jib.frontend.MainClassFinder to infer the main class."); + + "JavaContainerBuilder#setMainClass(String), or consider using MainClassFinder to " + + "infer the main class."); } if (classpathOrder.isEmpty()) { throw new IllegalStateException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index dcd54dd85b..0221580c87 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -281,8 +281,8 @@ public void testToJibContainerBuilder_mainClassNull() Assert.assertEquals( "Failed to construct entrypoint on JavaContainerBuilder; jvmFlags were set, but " + "mainClass is null. Specify the main class using " - + "JavaContainerBuilder#setMainClass(String), or consider using a " - + "jib.frontend.MainClassFinder to infer the main class.", + + "JavaContainerBuilder#setMainClass(String), or consider using MainClassFinder to " + + "infer the main class.", ex.getMessage()); } } From 50bc8f087ed7e0a5cf9ef7bff17e5d30c3808144 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 22 Aug 2019 14:31:56 -0400 Subject: [PATCH 0696/2020] Force downgrading to org.apache.httpcomponents:httpclient:4.5.6 prevent URI normalization (#1924) * Force org.apache.httpcomponents:httpclient:4.5.6 * Update CHANGELOGs --- jib-core/CHANGELOG.md | 2 ++ jib-core/build.gradle | 18 +++++++++++++++--- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-gradle-plugin/build.gradle | 13 ++++++++++--- jib-maven-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/pom.xml | 6 ++++++ jib-plugins-common/build.gradle | 13 ++++++++++--- 7 files changed, 47 insertions(+), 9 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index cbdc27c657..97984a9e4f 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) + ## 0.10.1 ### Added diff --git a/jib-core/build.gradle b/jib-core/build.gradle index bf1bf7585a..ff02b8c97c 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -41,9 +41,21 @@ dependencies { // For Google libraries, check , , , // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml // for best compatibility. - implementation 'com.google.http-client:google-http-client:1.31.0' - implementation 'com.google.http-client:google-http-client-apache-v2:1.31.0' - implementation 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + // + // TODO: remove once https://github.com/googleapis/google-http-java-client/issues/795 is fixed and released. + // Forcing to downgrade this to 4.5.6 fixes https://github.com/GoogleContainerTools/jib/issues/1914 + // However, #795 and upgrading httpclient alone may not fix #1914. We may need to explicitly disable URI + // normalization as discussed in #795. + implementation('com.google.http-client:google-http-client:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + implementation('com.google.http-client:google-http-client-apache-v2:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + implementation('com.google.auth:google-auth-library-oauth2-http:0.16.2') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + implementation 'org.apache.httpcomponents:httpclient:4.5.6' implementation 'com.google.guava:guava:28.0-jre' implementation 'org.apache.commons:commons-compress:1.18' diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7500fa1b27..dbc013a8d7 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) + ## 1.5.0 ### Added diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 997d6073f9..4513a2c180 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -61,9 +61,16 @@ configurations { dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. - compile 'com.google.http-client:google-http-client:1.31.0' - compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' - compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile('com.google.http-client:google-http-client:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile('com.google.http-client:google-http-client-apache-v2:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile('com.google.auth:google-auth-library-oauth2-http:0.16.2') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile 'org.apache.httpcomponents:httpclient:4.5.6' compile 'com.google.guava:guava:28.0-jre' compile 'org.apache.commons:commons-compress:1.18' diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 3285553815..f64fe90573 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) + ## 1.5.0 ### Added diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index f50fe24b4a..6a433ace4b 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -62,6 +62,12 @@ 1.31.0 compile
+ + org.apache.httpcomponents + httpclient + 4.5.6 + compile + com.google.auth google-auth-library-oauth2-http diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 81ea47811f..8ee67d0889 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -29,9 +29,16 @@ sourceSets { dependencies { // Make sure these are consistent with jib-maven-plugin. - compile 'com.google.http-client:google-http-client:1.31.0' - compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' - compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile('com.google.http-client:google-http-client:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile('com.google.http-client:google-http-client-apache-v2:1.31.0') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile('com.google.auth:google-auth-library-oauth2-http:0.16.2') { + exclude group: "org.apache.httpcomponents", module: "httpclient" + } + compile 'org.apache.httpcomponents:httpclient:4.5.6' compile 'com.google.guava:guava:28.0-jre' compile 'org.apache.commons:commons-compress:1.18' From 618f4a35b5fa53185e0282b93508390d0bb4b1ea Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 22 Aug 2019 15:26:30 -0400 Subject: [PATCH 0697/2020] Adjust TarImage API in preparation for local base image support (#1926) --- jib-core/CHANGELOG.md | 2 + .../jib/api/ContainerizerIntegrationTest.java | 4 +- .../tools/jib/api/JibIntegrationTest.java | 4 +- .../tools/jib/api/ReproducibleImageTest.java | 2 +- .../cloud/tools/jib/api/Containerizer.java | 10 ++- .../google/cloud/tools/jib/api/TarImage.java | 76 ++++++++----------- .../tools/jib/api/ContainerizerTest.java | 4 +- .../cloud/tools/jib/api/TarImageTest.java | 15 +++- .../common/PluginConfigurationProcessor.java | 2 +- 9 files changed, 60 insertions(+), 59 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 97984a9e4f..2d18038c34 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)` ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) + ### Fixed - Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 46f7de15d9..09f5a5a6fe 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -322,9 +322,7 @@ private JibContainer buildTarImage( throws IOException, InterruptedException, RegistryException, CacheDirectoryCreationException, ExecutionException { return buildImage( - baseImage, - Containerizer.to(TarImage.named(targetImage).saveTo(outputPath)), - additionalTags); + baseImage, Containerizer.to(TarImage.at(outputPath).named(targetImage)), additionalTags); } private JibContainer buildImage( diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 8ad467771a..bc4705d25f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -201,8 +201,8 @@ public void testManifestListReferenceByShaDoesNotFail() ManifestPullerIntegrationTest.KNOWN_MANIFEST_LIST_SHA); Containerizer containerizer = Containerizer.to( - TarImage.named("whatever") - .saveTo(cacheFolder.newFolder("goose").toPath().resolve("moose"))); + TarImage.at(cacheFolder.newFolder("goose").toPath().resolve("moose")) + .named("whatever")); Jib.from(sourceImageReferenceAsManifestList).containerize(containerizer); // pass, no exceptions thrown diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 689a222408..28af05adaf 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -72,7 +72,7 @@ public static void createImage() imageTar = new File(imageLocation.getRoot(), "image.tar"); Containerizer containerizer = - Containerizer.to(TarImage.named("jib-core/reproducible").saveTo(imageTar.toPath())); + Containerizer.to(TarImage.at(imageTar.toPath()).named("jib-core/reproducible")); Jib.fromScratch() .setEntrypoint("echo", "Hello World") diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 119b38ae4e..5e8b2e4706 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -100,12 +100,18 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { * @return a new {@link Containerizer} */ public static Containerizer to(TarImage tarImage) { + if (!tarImage.getImageReference().isPresent()) { + throw new IllegalArgumentException( + "Image name must be set when building a TarImage; use TarImage#named(...) to set the name" + + " of the target image"); + } + ImageConfiguration imageConfiguration = - ImageConfiguration.builder(tarImage.getImageReference()).build(); + ImageConfiguration.builder(tarImage.getImageReference().get()).build(); Function stepsRunnerFactory = buildConfiguration -> - StepsRunner.begin(buildConfiguration).tarBuildSteps(tarImage.getOutputFile()); + StepsRunner.begin(buildConfiguration).tarBuildSteps(tarImage.getPath()); return new Containerizer( DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory, false); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java index fbb0284b86..34da7112e3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java @@ -17,6 +17,8 @@ package com.google.cloud.tools.jib.api; import java.nio.file.Path; +import java.util.Optional; +import javax.annotation.Nullable; /** * Builds to a tarball archive. @@ -24,73 +26,59 @@ *

Usage example: * *

{@code
- * TarImage tarImage = TarImage.named("myimage")
- *                             .saveTo(Paths.get("image.tar"));
+ * TarImage tarImage = TarImage.at(Paths.get("image.tar"))
+ *                             .named("myimage");
  * }
*/ public class TarImage { - /** Finishes constructing a {@link TarImage}. */ - public static class Builder { - - private final ImageReference imageReference; + /** + * Constructs a {@link TarImage} with the specified path. + * + * @param path the path to the tarball archive + * @return a new {@link TarImage} + */ + public static TarImage at(Path path) { + return new TarImage(path); + } - private Builder(ImageReference imageReference) { - this.imageReference = imageReference; - } + private final Path path; + @Nullable private ImageReference imageReference; - /** - * Sets the output file to save the tarball archive to. - * - * @param outputFile the output file - * @return a new {@link TarImage} - */ - public TarImage saveTo(Path outputFile) { - return new TarImage(imageReference, outputFile); - } + /** Instantiate with {@link #at}. */ + private TarImage(Path path) { + this.path = path; } /** - * Configures the output tarball archive with an image reference. This image reference will be the - * name of the image if loaded into the Docker daemon. + * Sets the name of the image. This is the name that shows up when the tar is loaded by the Docker + * daemon. * * @param imageReference the image reference - * @return a {@link Builder} to finish constructing a new {@link TarImage} + * @return this */ - public static Builder named(ImageReference imageReference) { - return new Builder(imageReference); + public TarImage named(ImageReference imageReference) { + this.imageReference = imageReference; + return this; } /** - * Configures the output tarball archive with an image reference to set as its tag. + * Sets the name of the image. This is the name that shows up when the tar is loaded by the Docker + * daemon. * * @param imageReference the image reference - * @return a {@link Builder} to finish constructing a new {@link TarImage} + * @return this * @throws InvalidImageReferenceException if {@code imageReference} is not a valid image reference */ - public static Builder named(String imageReference) throws InvalidImageReferenceException { + public TarImage named(String imageReference) throws InvalidImageReferenceException { return named(ImageReference.parse(imageReference)); } - private final ImageReference imageReference; - private final Path outputFile; - - /** Instantiate with {@link #named}. */ - private TarImage(ImageReference imageReference, Path outputFile) { - this.imageReference = imageReference; - this.outputFile = outputFile; - } - - /** - * Gets the output file to save the tarball archive to. - * - * @return the output file - */ - Path getOutputFile() { - return outputFile; + Path getPath() { + return path; } - ImageReference getImageReference() { - return imageReference; + Optional getImageReference() { + return Optional.ofNullable(imageReference); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index a2a8c22090..f439570efb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -40,7 +40,7 @@ public void testTo() throws CacheDirectoryCreationException { DockerDaemonImage dockerDaemonImage = DockerDaemonImage.named(ImageReference.of(null, "repository", null)); TarImage tarImage = - TarImage.named(ImageReference.of(null, "repository", null)).saveTo(Paths.get("ignored")); + TarImage.at(Paths.get("ignored")).named(ImageReference.of(null, "repository", null)); verifyTo(Containerizer.to(registryImage)); verifyTo(Containerizer.to(dockerDaemonImage)); @@ -119,7 +119,7 @@ public void testGetImageConfiguration_dockerDaemonImage() throws InvalidImageRef @Test public void testGetImageConfiguration_tarImage() throws InvalidImageReferenceException { Containerizer containerizer = - Containerizer.to(TarImage.named("tar/image").saveTo(Paths.get("output/file"))); + Containerizer.to(TarImage.at(Paths.get("output/file")).named("tar/image")); ImageConfiguration imageConfiguration = containerizer.getImageConfiguration(); Assert.assertEquals("tar/image", imageConfiguration.getImage().toString()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java index 321870b2d3..834ee60316 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java @@ -24,9 +24,16 @@ public class TarImageTest { @Test - public void testGetters() throws InvalidImageReferenceException { - TarImage tarImage = TarImage.named("tar/image").saveTo(Paths.get("output/file")); - Assert.assertEquals("tar/image", tarImage.getImageReference().toString()); - Assert.assertEquals(Paths.get("output/file"), tarImage.getOutputFile()); + public void testGetters_bothSet() throws InvalidImageReferenceException { + TarImage tarImage = TarImage.at(Paths.get("output/file")).named("tar/image"); + Assert.assertEquals("tar/image", tarImage.getImageReference().get().toString()); + Assert.assertEquals(Paths.get("output/file"), tarImage.getPath()); + } + + @Test + public void testGetters_nameMissing() { + TarImage tarImage = TarImage.at(Paths.get("output/file")); + Assert.assertFalse(tarImage.getImageReference().isPresent()); + Assert.assertEquals(Paths.get("output/file"), tarImage.getPath()); } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 8764568f7d..8f1237b16e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -108,7 +108,7 @@ public static JibBuildRunner createJibBuildRunnerForTarImage( Path tarImagePath = projectProperties.getOutputDirectory().resolve("jib-image.tar"); ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); - TarImage targetImage = TarImage.named(targetImageReference).saveTo(tarImagePath); + TarImage targetImage = TarImage.at(tarImagePath).named(targetImageReference); Containerizer containerizer = Containerizer.to(targetImage); JibContainerBuilder jibContainerBuilder = From 27592b64fdeca685f734c4c28f67d405a76b08f4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 22 Aug 2019 15:55:49 -0400 Subject: [PATCH 0698/2020] Maven release v1.5.1 (#1927) * preparing release 1.5.1 * 1.5.2-SNAPSHOT --- jib-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 6a433ace4b..0e0e8ab1f2 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.google.cloud.tools jib-maven-plugin - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT maven-plugin Jib From 00a8cfeabd0d250929421e05d17153e425cf8614 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 22 Aug 2019 15:55:59 -0400 Subject: [PATCH 0699/2020] Gradle release v1.5.1 (#1928) * [Gradle Release Plugin] - pre tag commit: 'v1.5.1-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.5.2-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index ced16a7bbb..fd0eb383c9 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.5.1-SNAPSHOT +version = 1.5.2-SNAPSHOT From af34d9fdeb06203697b6ecf06cc4a759e90c7215 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 27 Aug 2019 14:56:41 -0400 Subject: [PATCH 0700/2020] Move to unified gradle build (#1835) - also upgrades gradle to 5.2.1 - all builds should happen from root ::, build.sh removed - tests that run the plugin may not work in IDEs unless correct setup task run first - special eclipse helpers to clear up classpath - adds protections for transitive dependency overrides --- .gitignore | 1 + .travis.yml | 6 +- CONTRIBUTING.md | 70 +-- build.gradle | 348 ++++++++++++ build.sh | 104 ---- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes .../wrapper/gradle-wrapper.properties | 2 +- jib-plugins-common/gradlew => gradlew | 2 +- jib-core/gradlew.bat => gradlew.bat | 2 +- jib-core/build.gradle | 230 +------- jib-core/gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - jib-core/gradlew | 172 ------ jib-core/kokoro/release_build.sh | 4 +- jib-core/scripts/prepare_release.sh | 8 +- jib-gradle-plugin/build.gradle | 186 ++----- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 0 bytes jib-gradle-plugin/gradlew | 172 ------ jib-gradle-plugin/gradlew.bat | 84 --- jib-gradle-plugin/scripts/prepare_release.sh | 8 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 48336 -> 0 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 - jib-maven-plugin/build.gradle | 74 +++ jib-maven-plugin/gradle.properties | 1 + jib-maven-plugin/kokoro/release_build.sh | 9 +- jib-maven-plugin/mvnw | 227 -------- jib-maven-plugin/mvnw.cmd | 145 ----- jib-maven-plugin/pom.xml | 504 ------------------ jib-maven-plugin/scripts/prepare_release.sh | 58 +- .../maven/BuildDockerMojoIntegrationTest.java | 11 +- .../maven/BuildImageMojoIntegrationTest.java | 17 +- .../maven/BuildTarMojoIntegrationTest.java | 8 +- .../maven/skaffold/SkaffoldBindingMojo.java | 1 - .../jib/maven/MavenRawConfigurationTest.java | 4 +- .../cloud/tools/jib/maven/TestPlugin.java | 55 -- .../cloud/tools/jib/maven/TestProject.java | 13 +- .../skaffold/CheckJibVersionMojoTest.java | 5 +- .../jib/maven/skaffold/FilesMojoTest.java | 9 +- .../jib/maven/skaffold/FilesMojoV2Test.java | 9 +- .../maven/skaffold/PackageGoalsMojoTest.java | 6 +- .../maven/skaffold/SkaffoldInitMojoTest.java | 9 +- .../lib/src/test/java/com/lib/LibTest.java | 16 +- jib-plugins-common/build.gradle | 120 +---- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - jib-plugins-common/gradlew.bat | 84 --- jib-plugins-common/settings.gradle | 1 - .../JavaContainerBuilderHelperTest.java | 1 - kokoro/continuous.bat | 5 +- kokoro/continuous.sh | 5 +- kokoro/presubmit.bat | 5 +- kokoro/presubmit.sh | 6 +- settings.gradle | 4 + 53 files changed, 585 insertions(+), 2237 deletions(-) create mode 100644 build.gradle delete mode 100755 build.sh create mode 100644 gradle/wrapper/gradle-wrapper.jar rename {jib-gradle-plugin/gradle => gradle}/wrapper/gradle-wrapper.properties (91%) rename jib-plugins-common/gradlew => gradlew (99%) rename jib-core/gradlew.bat => gradlew.bat (98%) delete mode 100644 jib-core/gradle/wrapper/gradle-wrapper.jar delete mode 100644 jib-core/gradle/wrapper/gradle-wrapper.properties delete mode 100755 jib-core/gradlew delete mode 100644 jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar delete mode 100755 jib-gradle-plugin/gradlew delete mode 100644 jib-gradle-plugin/gradlew.bat delete mode 100755 jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar delete mode 100755 jib-maven-plugin/.mvn/wrapper/maven-wrapper.properties create mode 100644 jib-maven-plugin/build.gradle create mode 100644 jib-maven-plugin/gradle.properties delete mode 100755 jib-maven-plugin/mvnw delete mode 100755 jib-maven-plugin/mvnw.cmd delete mode 100644 jib-maven-plugin/pom.xml rename jib-maven-plugin/src/{test => integration-test}/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java (96%) rename jib-maven-plugin/src/{test => integration-test}/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java (97%) rename jib-maven-plugin/src/{test => integration-test}/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java (93%) delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java delete mode 100644 jib-plugins-common/gradle/wrapper/gradle-wrapper.jar delete mode 100644 jib-plugins-common/gradle/wrapper/gradle-wrapper.properties delete mode 100644 jib-plugins-common/gradlew.bat delete mode 100644 jib-plugins-common/settings.gradle create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore index 7d9acc3bab..0e2b2801f1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build target out +bin *.iml *.ipr *.iws diff --git a/.travis.yml b/.travis.yml index c5ac8dab8d..d56ac09d63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,11 +16,7 @@ env: install: true script: -- (cd jib-core; ./gradlew --stacktrace build) -- (cd jib-plugins-common; ./gradlew --stacktrace build) -- (cd jib-gradle-plugin; ./gradlew --stacktrace build) -# unset _JAVA_OPTIONS to avoid spurious test failures -- (cd jib-maven-plugin; unset _JAVA_OPTIONS; ./mvnw -B verify -U) +- ./gradlew clean build --stacktrace # cobbled together from # https://docs.travis-ci.com/user/languages/java/#projects-using-gradle diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c21937c098..a0cddb60ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; +Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. @@ -17,16 +17,19 @@ again. ## Building Jib -Jib comes as 3 components: +Jib comes as 3 public components: - - `jib-core`: a library - - `jib-plugins-common`: a library with helpers for plugin builders + - `jib-core`: a library for building containers - `jib-maven-plugin`: a Maven plugin that uses `jib-core` and `jib-plugins-common` - `jib-gradle-plugin`: a Gradle plugin that uses `jib-core` and `jib-plugins-common` -To build, use the provided `build.sh` which builds and tests each of the -components into your local `~/.m2/repository`. Note that this script does -not run integration tests. +And 1 internal component: + + - `jib-plugins-common`: a library with helpers for maven/gradle plugins + +The project is configured as a single gradle build. Run `./gradlew build` to build the +whole project. Run `./gradlew install` to install all public components into the +local maven repository. ## Code Reviews @@ -40,10 +43,7 @@ not run integration tests. 3. We follow our own [Java style guide](STYLE_GUIDE.md) that extends the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html). 3. Please include unit tests (and integration tests if applicable) for all new code. 4. Make sure all existing tests pass (but see the note below about integration tests). - * In `jib-core`, run `./gradlew clean goJF build integrationTest` - * In `jib-gradle-plugin`, run `./gradlew clean goJF build integrationTest` - * In `jib-plugins-common`, run `./gradlew clean goJF build` - * In `jib-maven-plugin`, run `./mvnw clean fmt:format verify -PintegrationTest` + * run `./gradlew clean goJF build integrationTest` 5. Associate the change with an existing issue or file a [new issue](../../issues). 6. Create a pull request! @@ -63,56 +63,29 @@ integration tests will create local registries on ports 5000 and 6000. ## Configuring Eclipse -Jib is a mix of Gradle and Maven projects. But since we do not -publish all artifacts, we instead have the projects reference -source from their dependencies directly using `sourceSets` and -`build-helper-maven-plugin`. This complicates importing these projects -as the Gradle and Maven tooling for Eclipse faithfully replicate the -direct source references. This has a side-effect that classes -appear both in the originating project _and_ in the subprojects, -and can interfere with Eclipse's incremental compilation. - -The following instructions first import the projects, and then replaces -the source-set-style references to use Eclipse's project references -instead. +Although jib is a mix of Gradle and Maven projects, we build everything using one +unifed gradle build. There is special code to include some projects directly as +source, but importing your project should be pretty straight forward. 1. Ensure you have installed the Gradle tooling for Eclipse, called _Buildship_ (available from [the Eclipse Marketplace](https://marketplace.eclipse.org/content/buildship-gradle-integration)). - 2. **Import the Maven project:** Use _File → Open Projects from File System..._ - to use the Eclipe Smart Import feature and specify the root of the Jib project. - Import the `jib-maven-plugin` project. - 3. **Import the Gradle projects:** Buildship does [not yet support + 1. **Import the Gradle project:** Buildship does [not yet support Eclipse Smart Import](https://github.com/eclipse/buildship/issues/356). Use _File → Import → Gradle → Existing Gradle Project_ - and import each of `jib-core`, `jib-plugins-common`, and `jib-gradle-plugin`. - 4. **Turn source-set references to project references:** For each of - `jib-plugins-common`, `jib-maven-plugin`, and `jib-gradle-plugin`: - - Right-click on the correponding project and select _Properties → Java Build Path_ - - Open the _Source_ panel and remove all _linked_ source folders: these are like - symbolic links to other locations and appear like - `jib-plugins-common/main-java - /path/to/jib-core/src/main/java`. - Only folders within the project should remain when complete. - - you may wish to remove `jib-gradle-plugin`'s `src/test/resources` and - `src/integration-test/resources` too as these contain test projects, - and are not linked in as separate projects and so seem have compilation errors - - Open the _Projects_ panel and click _Add_ to select the dependencies: - - `jib-plugins-common` depends on `jib-core` - - `jib-maven-plugin` depends on `jib-core` and `jib-plugins-common` - - `jib-gradle-plugin` depends on `jib-core` and `jib-plugins-common` + and import `jib`. Note that you will likely need to re-apply these changes whenever you refresh or update these projects. ## Debugging the Jib Maven Plugin (`jib-maven-plugin`) -### Build and use a local snapshot +### Build and use a local snapshot To use a local build of the `jib-maven-plugin`: 1. Build and install `jib-maven-plugin` into your local `~/.m2/repository` - with `(cd jib-maven-plugin && ./mvnw install)`; this also builds `jib-core`. - Alternatively, use the provided `build.sh` which performs an `install`. + with `./gradlew jib-maven-plugin:install`; 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` version of the `com.google.cloud.tools.jib` plugin. @@ -120,7 +93,7 @@ If developing from within Eclipse with M2Eclipse (the Maven tooling for Eclipse) 1. Modify your test project's `pom.xml` to reference the `-SNAPSHOT` version of the `com.google.cloud.tools.jib` plugin. - 2. Create and launch a _Maven Build_ launch configuration for the + 1. Create and launch a _Maven Build_ launch configuration for the test project, and ensure the _Resolve Workspace artifacts_ is checked. ### Attaching a debugger @@ -131,13 +104,12 @@ If developing with Eclipse and M2Eclipse (the Maven tooling for Eclipse), just l ## Debugging the Jib Gradle Plugin (`jib-gradle-plugin`) -### Build and use a local snapshot +### Build and use a local snapshot To use a local build of the `jib-gradle-plugin`: 1. Build and install `jib-gradle-plugin` into your local `~/.m2/repository` - with `(cd jib-gradle-plugin && ./gradlew build install)`; this also builds `jib-core`. - Alternatively, use the provided `build.sh` which performs an `install`. + with `./gradlew jib-gradle-plugin:install`; 1. Modify your test project's `build.gradle` to look like the following: ```groovy buildscript { diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..e6aa95d4b4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,348 @@ +// define all versioned plugins here and apply in subprojects as necessary without version +plugins { + id 'com.github.sherter.google-java-format' version '0.8' apply false + id 'net.ltgt.apt' version '0.19' apply false + id 'net.ltgt.errorprone' version '0.6' apply false + id 'net.researchgate.release' version '2.7.0' apply false + id 'com.gradle.plugin-publish' version '0.10.1' apply false + id 'io.freefair.maven-plugin' version '3.8.1' apply false +} + +import net.ltgt.gradle.errorprone.CheckSeverity + +subprojects { + group 'com.google.cloud.tools' + + repositories { + mavenCentral() + } + + apply plugin: 'java' + apply plugin: 'checkstyle' + apply plugin: 'com.github.sherter.google-java-format' + apply plugin: 'net.ltgt.apt' + apply plugin: 'net.ltgt.errorprone' + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + compileJava.options.encoding = 'UTF-8' + compileJava.options.compilerArgs += [ '-Xlint:deprecation' ] + compileTestJava.options.compilerArgs += [ '-Xlint:deprecation' ] + + /* PROJECT DEPENDENCY VERSIONS */ + // define all common versioned dependencies here + project.ext.dependencyVersions = [ + // For Google libraries, check , , , + // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml + // for best compatibility. + GOOGLE_HTTP_CLIENT: '1.31.0', + GOOGLE_HTTP_CLIENT_APACHE_V2: '1.31.0', + GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.16.2', + GUAVA: '28.0-jre', + + // TODO: remove once https://github.com/googleapis/google-http-java-client/issues/795 is fixed and released. + // Forcing to downgrade this to 4.5.6 fixes https://github.com/GoogleContainerTools/jib/issues/1914 + // However, #795 and upgrading httpclient alone may not fix #1914. We may need to explicitly disable URI + // normalization as discussed in #795. + APACHE_HTTP_CLIENT_OVERRIDE: '4.5.6', + COMMONS_COMPRESS: '1.18', + JACKSON_DATABIND: '2.9.9.2', + ASM: '7.0', + + //test + JUNIT: '4.12', + MOCKITO_CORE: '2.23.4', + SLF4J_API: '1.7.25', + SYSTEM_RULES: '1.19.0', + ] + + // Use this to ensure we correctly override transitive dependencies + // TODO: There might be a plugin that does this + task ensureTransitiveDependencyOverrides { + def rules = ["httpclient": dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE] + doLast { + configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + def dependency = artifact.moduleVersion.id + if (rules[dependency.name] && rules[dependency.name] != dependency.version) { + throw new GradleException( + dependency.name + " version error in " + project + + ", expected:" + rules[dependency.name] + + ", found:" + dependency.version); + } + } + } + } + compileJava.dependsOn ensureTransitiveDependencyOverrides + /* PROJECT DEPENDENCY VERSIONS */ + + /* NULLAWAY */ + dependencies { + // NullAway errorprone plugin + annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' + errorprone 'com.google.errorprone:error_prone_core:2.3.2' + // Using github.com/google/error-prone-javac is required when running on + // JDK 8. Remove when migrating to JDK 11. + if (System.getProperty("java.version").startsWith("1.8.")) { + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") + } + } + + // Adds NullAway errorprone checks. + tasks.withType(JavaCompile) { + if (!name.toLowerCase().contains("test")) { + options.errorprone { + check('NullAway', CheckSeverity.ERROR) + option('NullAway:ExcludedFieldAnnotations', 'org.apache.maven.plugins.annotations.Component') + option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') + } + } + } + /* NULLAWAY */ + + /* GOOGLE JAVA FORMAT */ + googleJavaFormat { + toolVersion = '1.6' + } + check.dependsOn verifyGoogleJavaFormat + /* GOOGLE JAVA FORMAT */ + + /* CHECKSTYLE */ + checkstyle { + toolVersion = '8.18' + + // get the google_checks.xml file from the checkstyle jar and take out the java checks + def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() + def fileExtensionsBefore = '' + def fileExtensionsAfter = '' + def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) + assert !googleChecks.equals(googleChecksNoJava) + + config = resources.text.fromString(googleChecksNoJava) + + maxErrors = 0 + maxWarnings = 0 + } + /* CHECKSTYLE */ + + /* TEST CONFIG */ + tasks.withType(Test) { + reports.html.setDestination file("${reporting.baseDir}/${name}") + } + + test { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } + } + // jar to export tests classes for import in other project by doing: + // testCompile project(path:':project-name', configuration:'tests') + task testJar(type: Jar) { + from sourceSets.test.output.classesDirs + classifier = 'tests' + } + // to import resources do: sourceSets.test.resources.srcDirs project(':project-name').sourceSets.test.resources + + configurations { + tests + } + + artifacts { + tests testJar + } + /* TEST CONFIG */ + + /* INTEGRATION TESTS */ + sourceSets { + integrationTest { + java.srcDir file('src/integration-test/java') + resources.srcDir file('src/integration-test/resources') + } + } + + configurations { + integrationTestImplementation.extendsFrom testImplementation + integrationTestRuntime.extendsFrom testRuntime + } + + dependencies { + integrationTestImplementation sourceSets.main.output + integrationTestImplementation sourceSets.test.output + integrationTestImplementation configurations.compile + integrationTestImplementation configurations.testImplementation + integrationTestImplementation configurations.runtime + integrationTestImplementation configurations.testRuntime + } + + // Integration tests must be run explicitly + task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + systemProperty '_JIB_DISABLE_USER_AGENT', true + } + integrationTest.dependsOn test + + task integrationTestJar(type: Jar) { + from sourceSets.integrationTest.output.classesDirs + classifier = 'integration-tests' + } + + configurations { + integrationTests + } + + artifacts { + integrationTests integrationTestJar + } + /* INTEGRATION TESTS */ + + /* JAVADOC ENFORCEMENT */ + // Fail build on javadoc warnings + tasks.withType(Javadoc) { + options.addBooleanOption('Xwerror', true) + } + assemble.dependsOn javadoc + /* JAVADOC ENFORCEMENT */ + + /* JAR */ + jar { + manifest { + attributes 'Implementation-Title': project.name, + 'Implementation-Version': version, + 'Built-By': System.getProperty('user.name'), + 'Built-Date': new Date(), + 'Built-JDK': System.getProperty('java.version'), + 'Built-Gradle': gradle.gradleVersion + } + } + /* JAR */ + + /* MAVEN CENTRAL RELEASES */ + // for projects that release to maven central + project.ext.configureMavenRelease = { + apply plugin: 'maven-publish' + task sourceJar(type: Jar) { + from sourceSets.main.allJava + classifier 'sources' + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + from javadoc.destinationDir + classifier 'javadoc' + } + + publishing { + publications { + mavenJava(MavenPublication) { + pom { + // to be filled by subproject after calling configure configureMavenRelease + // name = '' + // description = '' + + url = 'https://github.com/GoogleContainerTools/jib' + inceptionYear = '2018' + + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + developers { + developer { + id = 'chanseokoh' + name = 'Chanseok Oh' + email = 'chanseok@google.com' + } + developer { + id = 'loosebazooka' + name = 'Appu Goundan' + email = 'appu@google.com' + } + developer { + id = 'TadCordle' + name = 'Tad Cordle' + email = 'tcordle@google.com' + } + developer { + id = 'briandealwis' + name = 'Brian de Alwis' + email = 'bdealwis@google.com' + } + developer { + id = 'coollog' + name = 'Qingyang Chen' + } + } + scm { + url = 'https://github.com/GoogleContainerTools/jib' + connection = 'scm:https://github.com/GoogleContainerTools/jib.git' + developerConnection = 'scm:git://github.com/GoogleContainerTools/jib.git' + } + } + } + } + } + generatePomFileForMavenJavaPublication { + destination = file("${project.buildDir}/pom/${project.name}-${project.version}.pom") + } + // define a special install task that handles installing locally for manual testing + task install { + dependsOn publishToMavenLocal + } + + // For kokoro sign and release to maven central + task prepareRelease(type: Copy) { + from jar + from sourceJar + from javadocJar + from generatePomFileForMavenJavaPublication + into "${project.buildDir}/release-artifacts" + dependsOn build + dependsOn cleanPrepareRelease + } + } + /* MAVEN CENTRAL RELEASE */ + + /* INCLUDED PROJECT DEPENDENCY HELPER */ + // to keep track of all source projects + project.ext.sourceProjects = [] + // sourceProject(Project) accepts a project and adds it as a dependency in a special manner: + // 1. add the project classes as "compileOnly" and make it available to tests in "testImplementation" + // 2. add the project's depedencies as "implementation" + // 3. remove any transitive reference of any sourceProject depenency that may have appeared + // 4. add the project's classes to the final jar + // Other nice effects (vs shadowJar) + // 1. Generated poms will be correct + // 2. Configuration is isolated to this single "sourceProject" call + // 3. These configurations are compliant with IDEs + project.ext.sourceProject = { Project dependencyProject -> + def dependencyProjectClasses = dependencyProject.sourceSets.main.output + dependencies { + // add the dependencyProject classes as compileOnly, make it available to tests + compileOnly(dependencyProject) { transitive = false } + testImplementation dependencyProjectClasses + // add dependencyProject's dependencies as implementation dependencies + implementation dependencyProject.configurations.implementation.dependencies + if (dependencyProject.configurations.hasProperty('api')) { + implementation dependencyProject.configurations.api.dependencies + } + // if we find any project dependencies that are brought in transitively, go remove them + project.sourceProjects.each { projectToRemove -> + project.configurations.implementation.dependencies.remove projectToRemove + } + } + // keep track of all dependencyProjects for later removal + sourceProjects += dependencyProject + // adds dependencyProject's classes to jar (fat jar-esque) + jar { + from dependencyProjectClasses + } + // also configure the java-gradle-plugin if necessary + if (project.hasProperty("gradlePlugin")) { + project.tasks.pluginUnderTestMetadata.pluginClasspath.from dependencyProjectClasses + } + } + /* INCLUDED PROJECT DEPENDENCY HELPER */ +} diff --git a/build.sh b/build.sh deleted file mode 100755 index 33ba046a21..0000000000 --- a/build.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/sh -# Build Jib - -quickMode=false -mavenOptions="" -gradleOptions="" -dryRun="" - -usage() -{ - eval 1>&2 - echo "Simple builder for Jib for jib-core, jib-plugins-common, jib-maven-plugin, and jib-gradle-plugin" - echo "use: $0 [-qe] [clean | core | plugins | maven | gradle | it | format]" - echo " providing no target is the same as 'core plugins gradle maven'" - echo " -q quick mode: skip tests, formatting" - echo " -e show error information (mvn: -e, gradle: --stacktrace --info)" - echo " -n dry run: show what will be performed" - exit 1 -} - -# doBuild: Run a command in a directory -# $1 = directory -# $2... = build command -doBuild() { - (directory="$1"; shift; echo ">>> (cd $directory; $*)"; [ -z "$dryRun" ] && cd "$directory" && eval '"$@"') -} - -while getopts qen c; do - case $c in - q) quickMode=true;; - e) mavenOptions="$mavenOptions -e" - gradleOptions="$gradleOptions --stacktrace --info" - ;; - n) dryRun=true;; - \?) usage;; - esac -done -shift $( expr $OPTIND - 1 ) - -if [ $# -eq 0 ]; then - set -- core plugins gradle maven -fi - -set -o errexit # exit on error -for target in "$@"; do - case "$target" in - clean) - doBuild jib-core ./gradlew $gradleOptions clean - doBuild jib-plugins-common ./gradlew $gradleOptions clean - doBuild jib-gradle-plugin ./gradlew $gradleOptions clean - doBuild jib-maven-plugin ./mvnw $mavenOptions clean - ;; - - core) - if [ "$quickMode" = false ]; then - doBuild jib-core ./gradlew $gradleOptions googleJavaFormat build - else - doBuild jib-core ./gradlew $gradleOptions build \ - --exclude-task test --exclude-task check - fi - ;; - - plugins) - if [ "$quickMode" = false ]; then - doBuild jib-plugins-common ./gradlew $gradleOptions googleJavaFormat build - else - doBuild jib-plugins-common ./gradlew $gradleOptions build \ - --exclude-task test --exclude-task check - fi - ;; - - maven) - if [ "$quickMode" = false ]; then - doBuild jib-maven-plugin ./mvnw $mavenOptions fmt:format install -U - else - # jib-maven-plugin pulls in jib-core directly - doBuild jib-maven-plugin ./mvnw $mavenOptions -Dcheckstyle.skip -Dfmt.skip -DskipTests install -U - fi - ;; - - gradle) - if [ "$quickMode" = false ]; then - doBuild jib-gradle-plugin ./gradlew $gradleOptions googleJavaFormat build install - else - # jib-gradle-plugin pulls in jib-core directly - doBuild jib-gradle-plugin ./gradlew $gradleOptions build install \ - --exclude-task test --exclude-task check - fi - ;; - - it) - doBuild jib-core ./gradlew $gradleOptions integrationTest - doBuild jib-maven-plugin ./mvnw $mavenOptions -PintegrationTest verify -U - doBuild jib-gradle-plugin ./gradlew $gradleOptions integrationTest - ;; - - format) - doBuild jib-core ./gradlew googleJavaFormat - doBuild jib-plugins-common ./gradlew googleJavaFormat - doBuild jib-gradle-plugin ./gradlew googleJavaFormat - doBuild jib-maven-plugin ./mvnw fmt:format - ;; - esac -done diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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, , , - // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml - // for best compatibility. - // - // TODO: remove once https://github.com/googleapis/google-http-java-client/issues/795 is fixed and released. - // Forcing to downgrade this to 4.5.6 fixes https://github.com/GoogleContainerTools/jib/issues/1914 - // However, #795 and upgrading httpclient alone may not fix #1914. We may need to explicitly disable URI - // normalization as discussed in #795. - implementation('com.google.http-client:google-http-client:1.31.0') { + implementation("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { exclude group: "org.apache.httpcomponents", module: "httpclient" } - implementation('com.google.http-client:google-http-client-apache-v2:1.31.0') { + implementation("com.google.http-client:google-http-client-apache-v2:${dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2}") { exclude group: "org.apache.httpcomponents", module: "httpclient" } - implementation('com.google.auth:google-auth-library-oauth2-http:0.16.2') { + implementation("com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}") { exclude group: "org.apache.httpcomponents", module: "httpclient" } - implementation 'org.apache.httpcomponents:httpclient:4.5.6' - implementation 'com.google.guava:guava:28.0-jre' + implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" - implementation 'org.apache.commons:commons-compress:1.18' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' - implementation 'org.ow2.asm:asm:7.0' + implementation "org.apache.commons:commons-compress:${dependencyVersions.COMMONS_COMPRESS}" + implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" + implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" + implementation "org.ow2.asm:asm:${dependencyVersions.ASM}" - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.23.4' - testImplementation 'org.slf4j:slf4j-api:1.7.25' - testImplementation 'com.github.stefanbirkner:system-rules:1.19.0' - - // NullAway errorprone plugin - annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' - errorprone 'com.google.errorprone:error_prone_core:2.3.2' - // Using github.com/google/error-prone-javac is required when running on - // JDK 8. Remove when migrating to JDK 11. - if (System.getProperty("java.version").startsWith("1.8.")) { - errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") - } - - integrationTestCompile sourceSets.main.output - integrationTestCompile sourceSets.test.output - integrationTestCompile configurations.compile - integrationTestCompile configurations.testCompile - integrationTestCompile configurations.runtime - integrationTestCompile configurations.testRuntime + testImplementation "junit:junit:${dependencyVersions.JUNIT}" + testImplementation "org.mockito:mockito-core:${dependencyVersions.MOCKITO_CORE}" + testImplementation "org.slf4j:slf4j-api:${dependencyVersions.SLF4J_API}" + testImplementation "com.github.stefanbirkner:system-rules:${dependencyVersions.SYSTEM_RULES}" } jar { @@ -99,149 +41,21 @@ jar { } } -// Integration tests must be run explicitly -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - systemProperty '_JIB_DISABLE_USER_AGENT', true -} -integrationTest.dependsOn test - -test { - testLogging { - showStandardStreams = true - exceptionFormat = 'full' - } -} - -import net.ltgt.gradle.errorprone.CheckSeverity - -// Adds NullAway errorprone checks. -tasks.withType(JavaCompile) { - if (!name.toLowerCase().contains("test")) { - options.errorprone { - check('NullAway', CheckSeverity.ERROR) - option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') - } - } -} - -// Fail build on javadoc warnings -tasks.withType(Javadoc) { - options.addBooleanOption('Xwerror', true) -} -assemble.dependsOn javadoc - -tasks.withType(Test) { - reports.html.setDestination file("${reporting.baseDir}/${name}") -} - -/* GOOGLE JAVA FORMAT */ -googleJavaFormat { - toolVersion = '1.6' -} -check.dependsOn verifyGoogleJavaFormat -/* GOOGLE JAVA FORMAT */ - -/* CHECKSTYLE */ -checkstyle { - toolVersion = '8.18' - - // get the google_checks.xml file from the checkstyle jar and take out the java checks - def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - - config = resources.text.fromString(googleChecksNoJava) - - maxErrors = 0 - maxWarnings = 0 -} -/* CHECKSTYLE */ +configureMavenRelease() /* RELEASE */ -task sourceJar(type: Jar) { - from sourceSets.main.allJava - classifier 'sources' -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - from javadoc.destinationDir - classifier 'javadoc' -} - -task writePom { - project.afterEvaluate { - def outputFile = file("${project.buildDir}/pom/${project.name}-${project.version}.pom") - outputs.file outputFile - - doLast { +publishing { + publications { + mavenJava(MavenPublication) { pom { - project { - name 'Jib Core' - description 'Build container images.' - url 'https://github.com/GoogleContainerTools/jib' - inceptionYear '2018' - - scm { - url 'https://github.com/GoogleContainerTools/jib' - connection 'scm:https://github.com/GoogleContainerTools/jib.git' - developerConnection 'scm:git://github.com/GoogleContainerTools/jib.git' - } - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - - developers { - developer { - id 'coollog' - name 'Qingyang Chen' - email 'qingyangc@google.com' - } - developer { - id 'loosebazooka' - name 'Appu Goundan' - email 'appu@google.com' - } - developer { - id 'TadCordle' - name 'Tad Cordle' - email 'tcordle@google.com' - } - developer { - id 'chanseokoh' - name 'Chanseok Oh' - email 'chanseok@google.com' - } - developer { - id 'briandealwis' - name 'Brian de Alwis' - email 'bdealwis@google.com' - } - } - } - }.writeTo(outputFile) + name = "Jib Core" + description = 'Build container images.' + } + from components.java } } } -// For Kokoro sign and releases -task prepareRelease(type: Copy) { - from jar - from sourceJar - from javadocJar - from writePom - into "${project.buildDir}/release-artifacts" - dependsOn build - dependsOn cleanPrepareRelease -} // Release plugin (git release commits and version updates) release { tagTemplate = 'v$version-core' diff --git a/jib-core/gradle/wrapper/gradle-wrapper.jar b/jib-core/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/jib-core/kokoro/release_build.sh b/jib-core/kokoro/release_build.sh index 44d19b9b0f..f795c4d446 100644 --- a/jib-core/kokoro/release_build.sh +++ b/jib-core/kokoro/release_build.sh @@ -5,5 +5,5 @@ set -e # Display commands to stderr. set -x -cd github/jib/jib-core -./gradlew prepareRelease +cd github/jib +./gradlew :jib-core:prepareRelease diff --git a/jib-core/scripts/prepare_release.sh b/jib-core/scripts/prepare_release.sh index 120cc81eb0..8fb09d3236 100755 --- a/jib-core/scripts/prepare_release.sh +++ b/jib-core/scripts/prepare_release.sh @@ -1,5 +1,5 @@ #!/bin/bash - -# Usage: ./scripts/prepare_release.sh +# Usage: ./jib-core/scripts/prepare_release.sh set -e @@ -16,7 +16,7 @@ Die() { } DieUsage() { - Die "Usage: ./prepare_release.sh []" + Die "Usage: ./jib-core/scripts/prepare_release.sh []" } # Usage: CheckVersion @@ -40,14 +40,14 @@ if [[ $(git status -uno --porcelain) ]]; then fi # Runs integration tests. -./gradlew integrationTest --info --stacktrace +./gradlew :jib-core:integrationTest --info --stacktrace # Checks out a new branch for this version release (eg. 1.5.7). BRANCH=core_release_v${VERSION} git checkout -b ${BRANCH} # Changes the version for release and creates the commits/tags. -echo | ./gradlew release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} +echo | ./gradlew :jib-core:release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} # Pushes the release branch and tag to Github. git push origin ${BRANCH} diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 4513a2c180..e3c6b8045b 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -1,176 +1,42 @@ plugins { id 'java-gradle-plugin' - id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.8' - - // For local install + id 'net.researchgate.release' + id 'com.gradle.plugin-publish' + // for local build/test id 'maven' - - // Error-prone checker - id 'net.ltgt.apt' version '0.19' - id 'net.ltgt.errorprone' version '0.6' - - // Prepare release - id 'net.researchgate.release' version '2.6.0' - // Gradle Plugin Portal publish - id 'com.gradle.plugin-publish' version '0.9.10' + // for eclipse import modifications + id 'eclipse' } -group 'com.google.cloud.tools' - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 -compileJava.options.encoding = 'UTF-8' - repositories { - // Use jcenter for gradle plugin portal releases. + // because gradle plugin dependencies are pulling from jcenter jcenter() } -sourceSets { - main { - java.srcDir file('../jib-core/src/main/java') - resources.srcDir file('../jib-core/src/main/resources') - java.srcDir file('../jib-plugins-common/src/main/java') - resources.srcDir file('../jib-plugins-common/src/main/resources') - } - test { - java.srcDir file('../jib-core/src/test/java') - resources.srcDir file('../jib-core/src/test/resources') - java.srcDir file('../jib-plugins-common/src/test/java') - resources.srcDir file('../jib-plugins-common/src/test/resources') - } - integrationTest { - java { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/java') - srcDir file('../jib-core/src/integration-test/java') - } - resources { - srcDir file('src/integration-test/resources') - srcDir file('../jib-core/src/integration-test/resources') - } - } -} - -configurations { - integrationTestCompile.extendsFrom testCompile - integrationTestRuntime.extendsFrom testRuntime -} - dependencies { - // These are copied over from jib-core and are necessary for the jib-core sourcesets. - compile('com.google.http-client:google-http-client:1.31.0') { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - compile('com.google.http-client:google-http-client-apache-v2:1.31.0') { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - compile('com.google.auth:google-auth-library-oauth2-http:0.16.2') { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - compile 'org.apache.httpcomponents:httpclient:4.5.6' - compile 'com.google.guava:guava:28.0-jre' - - compile 'org.apache.commons:commons-compress:1.18' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' - compile 'org.ow2.asm:asm:7.0' - - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.23.4' - testCompile 'org.slf4j:slf4j-api:1.7.25' - testCompile 'com.github.stefanbirkner:system-rules:1.19.0' - - // only for testing a concrete Spring Boot example in a test (not for test infrastructure) - testCompile 'org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE' - - compile gradleApi() - // NullAway errorprone plugin - annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' - errorprone 'com.google.errorprone:error_prone_core:2.3.2' - // Using github.com/google/error-prone-javac is required when running on - // JDK 8. Remove when migrating to JDK 11. - if (System.getProperty("java.version").startsWith("1.8.")) { - errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") - } -} - -// Integration tests must be run explicitly -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath - systemProperty '_JIB_DISABLE_USER_AGENT', true -} -integrationTest.dependsOn test + sourceProject project(":jib-core") + sourceProject project(":jib-plugins-common") -test { - testLogging { - showStandardStreams = true - exceptionFormat = 'full' + implementation ("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { + exclude group: "org.apache.httpcomponents", module: "httpclient" } -} + implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" -import net.ltgt.gradle.errorprone.CheckSeverity + implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" + implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" -// Adds NullAway errorprone checks. -tasks.withType(JavaCompile) { - if (!name.toLowerCase().contains("test")) { - options.errorprone { - check('NullAway', CheckSeverity.ERROR) - option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') - } - } -} - -// Fail build on javadoc warnings -tasks.withType(Javadoc) { - options.addBooleanOption('Xwerror', true) -} + testImplementation "junit:junit:${dependencyVersions.JUNIT}" + testImplementation "org.mockito:mockito-core:${dependencyVersions.MOCKITO_CORE}" + testImplementation "org.slf4j:slf4j-api:${dependencyVersions.SLF4J_API}" + testImplementation "com.github.stefanbirkner:system-rules:${dependencyVersions.SYSTEM_RULES}" -tasks.withType(Test) { - reports.html.setDestination file("${reporting.baseDir}/${name}") -} + testImplementation project(path:':jib-plugins-common', configuration:'tests') + integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') -/* GOOGLE JAVA FORMAT */ -googleJavaFormat { - toolVersion = '1.6' -} -check.dependsOn verifyGoogleJavaFormat -/* GOOGLE JAVA FORMAT */ - -/* CHECKSTYLE */ -checkstyle { - toolVersion = '8.18' - - // get the google_checks.xml file from the checkstyle jar and take out the java checks - def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - - config = resources.text.fromString(googleChecksNoJava) - - maxErrors = 0 - maxWarnings = 0 -} -/* CHECKSTYLE */ - -/* JAR */ -// Necessary for adding version information into the JAR. -jar { - manifest { - attributes 'Implementation-Title': project.name, - 'Implementation-Version': version, - 'Built-By': System.getProperty('user.name'), - 'Built-Date': new Date(), - 'Built-JDK': System.getProperty('java.version'), - 'Built-Gradle': gradle.gradleVersion - } + // only for testing a concrete Spring Boot example in a test (not for test infrastructure) + testImplementation 'org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE' } -/* JAR */ /* RELEASE */ // Prepare release @@ -196,3 +62,13 @@ pluginBundle { } tasks.publishPlugins.dependsOn integrationTest /* RELEASE */ + +/* ECLIPSE */ +eclipse.classpath.file.whenMerged { cp -> + cp.entries.each { + if (it.path == "src/test/resources" || it.path == "src/integration-test/resources") { + it.excludes += "gradle/projects/" + } + } +} +/* ECLIPSE */ diff --git a/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/jib-gradle-plugin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56177 zcmagFV{~WVwk?_pE4FRhwr$(CRk3Z`c2coz+fFL^#m=jD_df5v|GoR1_hGCxKaAPt z?5)i;2YO!$(jcHHKtMl#0s#RD{xu*V;Q#dm0)qVemK9YIq?MEtqXz*}_=jUJ`nb5z zUkCNS_ILXK>nJNICn+YXtU@O%b}u_MDI-lwHxDaKOEoh!+oZ&>#JqQWH$^)pIW0R) zElKkO>LS!6^{7~jvK^hY^r+ZqY@j9c3=``N6W|1J`tiT5`FENBXLF!`$M#O<|Hr=m zzdq3a_Az%dG_f)LA6=3E>FVxe=-^=L^nXkt;*h0g0|Nr0hXMkk{m)Z`?Co8gUH;CO zHMF!-b}@8vF?FIdwlQ>ej#1NgUlc?5LYq`G68Sj-$su4QLEuKmR+5|=T>6WUWDgWe zxE!*C;%NhMOo?hz$E$blz1#Poh2GazA4f~>{M`DT`i=e#G$*Bc4?Fwhs9KG=iTU1_ znfp#3-rpN&56JH)Q82UMm6+B@cJwQOmm^!avj=B5n8}b6-%orx(1!3RBhL~LO~Q_) z08-2}(`c{;%({toq#^5eD&g&LhE&rdu6Xo6?HW)dn#nW17y(4VDNRo}2Tz*KZeOJ=Gqg{aO>;;JnlqFiMVA+byk#lYskJf)bJ=Q) z8Z9b3bI9$rE-t9r5=Uhh={6sj%B;jj)M&G`lVH9Y*O*|2Qx{g3u&tETV~m)LwKEm7 zT}U%CvR7RA&X0<;L?i24Vi<+zU^$IbDbi|324Qk)pPH={pEwumUun5Zs*asDRPM8b z5ubzmua81PTymsv=oD9C!wsc%ZNy20pg(ci)Tela^>YG-p}A()CDp}KyJLp7^&ZEd z**kfem_(nl!mG9(IbD|-i?9@BbLa{R>y-AA+MIlrS7eH44qYo%1exzFTa1p>+K&yc z<5=g{WTI8(vJWa!Sw-MdwH~r;vJRyX}8pFLp7fEWHIe2J+N;mJkW0t*{qs_wO51nKyo;a zyP|YZy5it}{-S^*v_4Sp4{INs`_%Apd&OFg^iaJ;-~2_VAN?f}sM9mX+cSn-j1HMPHM$PPC&s>99#34a9HUk3;Bwf6BZG%oLAS*cq*)yqNs=7}gqn^ZKvuW^kN+x2qym zM_7hv4BiTDMj#<>Ax_0g^rmq=`4NbKlG1@CWh%_u&rx`9Xrlr0lDw zf}|C`$ey5IS3?w^Y#iZ!*#khIx8Vm+0msFN>$B~cD~;%#iqV|mP#EHY@t_VV77_@I zK@x`ixdjvu=j^jTc%;iiW`jIptKpX09b9LV{(vPu1o0LcG)50H{Wg{1_)cPq9rH+d zP?lSPp;sh%n^>~=&T533yPxuXFcTNvT&eGl9NSt8qTD5{5Z`zt1|RV%1_>;odK2QV zT=PT^2>(9iMtVP==YMXX#=dxN{~Z>=I$ob}1m(es=ae^3`m5f}C~_YbB#3c1Bw&3lLRp(V)^ZestV)Xe{Yk3^ijWw@xM16StLG)O zvCxht23Raf)|5^E3Mjt+b+*U7O%RM$fX*bu|H5E{V^?l_z6bJ8jH^y2J@9{nu)yCK z$MXM!QNhXH!&A`J#lqCi#nRZ&#s1&1CPi7-9!U^|7bJPu)Y4J4enraGTDP)ssm_9d z4Aj_2NG8b&d9jRA#$ehl3??X9-{c^vXH5**{}=y+2ShoNl-71whx;GS=a~*?bN{cm zCy+j0p4J4h{?MSnkQ5ZV4UJ(fs7p#3tmo7i*sWH?FmuDj0o>4|CIYAj=g@ZbEmMgl z6J-XPr67r}Ke$)WkD)hVD2|tn{e!x-z)koN$iH!2AUD0#&3&3g8mHKMr%iUusrnOd>R?l~q-#lr2Ki zb)XkR$bT5#or!s~fN5(K@`VL)5=CrQDiLQE;KrxvC78a+BXkAL$!KCJ3m1g%n4o4Z z@+*qk1bK{*U#?bZ$>8-Syw@3dG~GF=)-`%bU56v^)3b7`EW+tkkrSA?osI4}*~X?i zWO^kL8*xM{x-Ix}u=$wq8=Nl5bzHhAT)N&dg{HA$_n!ys67s~R1r7)(4i^ZB@P9sF z|N4Y-G$9R8Rz1J`EL)hhVuCdsX)!cl)`ZIXF>D+$NazAcg3$y)N1g~`ibIxbdAOtE zb2!M7*~GEENaTc+x#hOFY_n0y3`1mnNGu&QTmNh~%X$^tdi_4%ZjQk{_O^$=mcm|! z%xAxO*?qsc`IPrL?xgPmHAvEdG5A>rJ{Lo;-uQf3`5I~EC(PPgq2@n1Wc}lV&2O~t z1{|U92JH6zB?#yX!M`}Ojw+L1Z8{Is0pe?^ZxzOe_ZQcPCXnEVCy;+Yugc`E!nA(I z%O%hk_^!(IZso}h@Qe3{Fwl3nztZ$&ipk?FSr2Mo@18#FM^=PCyaDZ35%7gPt-%35 z$P4|4J8DnNH{_l_z@JQPY07;`(!M-{9j2=y__fxmbp59aaV4d)Y=@N(iUgGm0K!28 zMp;Ig3KkNy9z>t5BvQWtMY82$c}}d6;1`IJ^~At0(2|*C(NG#SWoa2rs|hBM8+HW(P5TMki>=KRlE+dThLZkdG387dOSY2X zWHr}5+)x`9lO#fSD1v&fL&wqU@b&THBot8Z?V;E4ZA$y42=95pP3iW)%$=UW_xC3; zB6t^^vl~v5csW5=aiZLZt9JLP*ph4~Q*l96@9!R8?{~a#m)tdNxFzQaeCgYIBA1+o+4UMmZoUO9z?Owi@Z=9VeCI6_ z7DV)=*v<&VRY|hWLdn^Ps=+L2+#Yg9#5mHcf*s8xp4nbrtT-=ju6wO976JQ(L+r=)?sfT?!(-}k!y?)>5c}?GB-zU zS*r8)PVsD;^aVhf^57tq(S%&9a;}F}^{ir}y0W|0G_=U9#W6y2FV}8NTpXJX*ivt{ zwQLhX0sSB8J?bmh(eUKq#AVmTO{VudFZpsIn-|i-8WlsexQ<;@WNn)OF=UpDJ7BI= z%-95NYqOY#)S?LIW-+rfw84@6Me}ya4*ltE*R^fy&W7?rEggZBxN@BR6=0!WH%4x0 zXg7=Ws|9Em`0pAt8k0cyQlr+>htn8GYs)+o>)IIf)p+yR`>lvz>5xFt(ep7>no4?4 zA%SUJ=L2D=;wq*f8WFl|&57Apa1;cT?b?bfJc8h&vkBvm%#ypP{=`6RL#Tf-dCq`;$!eR%>29EqpIkV*9 zEZl_>P3&}hY7)~q6UYw?*cBCsuPi$TU zRe}A|5nl7L_#e`8W0Hcpd~NWjAaV#3ngl$CoE3dz!= z?$3`dPgn5I+Q8 z@Bk>MqB7;kQqnDK=buPc+DsEDP-S;8#I(_z!*u&%_%nqI3+srxxsf9-Qg6%$l$Rtl zK2Wn-OtsBE5<1d}1Hl!l-r8eqD+{%b5$jfxQZw`2%)f+_^HMfbWyW4@j!^9M({>e; zeqCfR5b?^xh7MhHfmDvoXm8Wq;Jl2RU;jY*+a&o*H02$`#5HsG9#HOR4{g9 z#2mgNt%ep|IWrmctj=e%3xV&o^@8%OrR6io()6^sr!nQ3WIyQ3)0Mn}w}p^&t*V0G z03mUjJXbSCUG!o#-x*;_v>N8n-`yh1%Dp(1P)vz$^`oevMVh?u3}mgh}Qr(jhy;-09o$EB6jjWR!2F&xz^66M!F z-g}JBWLcw=j&Vb>xW#PQ3vICRT_UZ@wllScxk@ZQe&h-y)4B5kUJptVO%U-Ff3Hka zEyLldFsaM5E5`k>m}||+u`11;)tG@FL6TGzoF`A{R}?RZ@Ba!AS(tqAf{a_wtnlv>p|+&EEs(x%d4eq*RQ;Pq;) za9*J(n&C2dmFcNXb`WJi&XPu>t+m)Qp}c;$^35-Fj6soilnd4=b;ZePF27IdjE6PZ zvx{|&5tApKU2=ItX*ilhDx-a2SqQVjcV40Yn})Kaz$=$+3ZK~XXtrzTlKbR7C9)?2 zJ<^|JKX!eG231Oo=94kd1jC49mqE6G0x!-Qd}UkEm)API zKEemM1b4u_4LRq9IGE3e8XJq0@;%BCr|;BYW_`3R2H86QfSzzDg8eA>L)|?UEAc$< zaHY&MN|V#{!8}cryR+ygu!HI#$^;fxT|rmDE0zx|;V!ER3yW@09`p#zt}4S?Eoqx8 zk3FxI12)>eTd+c0%38kZdNwB`{bXeqO;vNI>F-l3O%-{`<3pNVdCdwqYsvso!Fw($ z`@$1&U=XH|%FFs>nq#e0tnS_jHVZLaEmnK#Ci==~Q!%Vr?{K0b$dSu(S!2VjZ}316b_I5Uk*L!8cJd>6W67+#0>-1P0i{eI%`C(_FkwRC zm}5eHEb0v^w3Wkqv#biSHXBG4yPC=^E!@hV8J5*JYf73=BqO!Ps#sP0fx~&C9PMN= z+V%$50uI|KE4^LCUXI74-qw$aRG&3kN-aOzVpRS1AX(Ua;Ewy>SlDn@lV(<^W?t-x z%K2iVK+;lG_~XF&Glk7w4<=Z!@-qDLc7)$q!>H^AU{s6e7krRmr!AZLf?8~$rRuP) zc$@c*PhIA^Lsu;uR{^x2)9nvsm}-67I`+iFZkhfNASUD>*LqxD=sAtpn{zY0xMxFp z4@USzYjMULeKc1lBe*8vxJDGNiSTtq_b#zd+Vzdc%$~+xf0;s|LR{F$YKe7YJVR$U}jKOo6=D+|6vnryopFbmNXEo-~I z*nm(LHmEGwkB%h%tXF4r|5h2p%VnRLx5rRsFpPR|e)*)C`WG-Iz94xsO&>1k8g6W? zG6#40`>I=B^scgmt_6!uU}=b3HgE@Jhj-X3jP!w-y>81ZD*~9C6ZRN4vlAFJQwK&l zP9&CP4%l-eN@0>Ihb_UWtp2kcPnh+L(fFJfQLc0`qqFbCkzr`8y2%{@RNrQbx*;tj zKtW!BWJFR$9(9^!Y%I%@3p?0zX#;(G?}sRkL{U>2rH4Wc{3{0@MV+vEaFcD18KIy% z7OyQTp?-N_)i%g+O#h(eLt_3ZDo)2l4PwjVS#=FzUNVvW{kFijz-@Y9-66fQL=xoc zXfLAC8<-!nnpM87K#eT;D^sW^HL5kS))Qj`kxT`%OewTXS(FT^X~VlkkZJJ?3*R8J zR>c>6)9K+9lg_a7!#<`KC$oEk-!~2N)@V}eq4O2xP)~N-lc}vH8qSe7tmQ3p@$pPde;Xk30uHYJ+VXeA@=yordN?7_ zpGsTlLlI{(qgtjOIlbx8DI{Nczj!*I>_-3ahzG;Kt&~8G_4G8qqF6IDn&g+zo>^L< z@zeVTB`{B9S*@M2_7@_(iHTQMCdC3zDi3_pE2!Lsg`K)$SiZj2X>=b2U#h^?x0j$Y zYuRf9vtRT~dxvF2Onn>?FfYPan1uc&eKyfBOK(|g7}E)t7}?{4GI%_KoO#8;_{N6! zDAqx7%0J`PG@O{(_)9yAFF!7l zWy1|Utdlc)^&J3OKhPI+S|Fc3R7vMVdN?PgoiQzo200oGpcy;TjSQ^e$a}Kh&C~xm zsG!Pqpqt5T`1`X$yas7{1hk?-r(Um>%&@?P2#NMETeQYhvk~nZW#BApGOLS2hdH)d zn!sf)7DotO?tRXBE#UpfKk-s}6%TfS0|7#>Rgk z%Np7ln*SH#6tzufY<0|UT+M}zJ1)1ap_cE@;QZp)+e-;k24 z3lZG_EA?tM$Eg|x3CK3!k`T7!*0}{fh8#=t^2EJ>TTo`6!CUm(HFUl7fFIB9Zlt4a z!4=|s-ZSn!@6Yc&+r1w*?*2fxKX>Hz2(vBwgE*>E=`A?Y1W-;{d2$4B%$NFAI?v5e zmYT{blxWeHn2J(0Vbz%FDz9~baqE#)R2TMG24xMZjCLcPfc1mR?5H4L%GnMR7ua{B zCu=nN(vV)5dJ_B80WBCy`tJ#YH6GyltGBSQvsN#q0;6XU1&60$&PC$0r}FUdr@1I+ zINcU{Ow6t4Qzmyk=A6u*z_!A*$^hBXJeKQ96bnF2qD$46hN!?1C|io|<_u@g16@Wd z(Fg?1=p8)dkWz<^ml6Tj5gO$hpB1N5msV!#PB5pfwCOBu`cv__=7kQq*r#Tc7E@6z zdr}5qs*slXK39`Yn%?=rslQgOTH0x?@z|h%fI5Y7kQ{X00BcL#8Jae4Dc9M zR%ySU5qODGnM;n#&up^M+PIddhxizA9@V%@0QQMY#1n z%{E8NS=?1?d((9Bk_ZC|{^(juH!;Mih{pTo&tu<^$Twk1aF;#W$;gxw!3g-zy(iiM z^+8nFS<9DJfk4+}(_Nza@Ukw}!*svpqJ)Nkh^sd%oHva}7+y)|5_aZ=JOZ6jnoYHQ zE2$FAnQ2mILoK*+6&(O9=%_tfQCYO%#(4t_5xP~W%Yw7Y4wcK|Ynd#YB3`rxli+9(uIQcRuQW_2EFA@J_ae$<%!EbI9c5htL`8>3Myy)@^=J)4p@nB2*&sWCOmwH zwYi;-9HOboaw0ov-WBk89LqGY!{)>8KxU1g%%wMq9h@Aie^42!f9`?o32T4;!dly? z(N?67=yo%jNp;oIVu7;esQ$wG=Vr+`rqPB&RLzr@@v`H-KK6wTa=8b<;$yE1lQGy?A1;JX|2hSzg9`a{;-5oh|=bFSzv&b zst=xa%|xW;id+~(8Fj7hS5BPVD(@(`3t@HUu))Q{0ZrqE2Jg zm6Gv~A*$A7Q#MU25zXD)iEUbLML1b++l4fJvP^PYOSK~^;n$EzdTE(zW3F1OpKztF zharBT_Ym7Y%lt#=p2&$3gs=g4xkM8A%Cbm*xR)9BnI}5=Oxp4GEF*bjFF^87xkP4L z;StW)zkX!yzz5^Q4HfEicKi{8elkFQx|0TH5Mtzsln>TN2*5Nypl(7sj_UxoN|KSyOP0g{L+vTbHlOyIEJ@ zjfku4x;`_FLga2P{FJLrgpIt;A-ukDuPsuW4#ApWE7|&i85Frv()~gOM`v`YVsF0c zx|J0}YRtNo7DIl>N&+%c(o1^C?%>Zf5<-<(yVcj~p88d;@=(jtox_$Af#v4%=g4oD ziv4MKh%Uf}NHP$SqF6mZj>}_HfC-@2>S~<3qOIu*R^%7;`VGN{ay@0(xmKM^5g9H4 zaq4>^38z|jszHqa)d>j#7Ccxz$*DGEG9PtB(d31?a;2$u>bY`CigPsg$zpDTW?zKg z+Ye-wtTjYHi#Hs`5$aDA=5Gl4J>p1Xs3PJZWWgax9~(h;G{hDip2I=+bW1ng3BrMC za72TsJR+;*0fSYuVnHsA;BnH5x8yc5Z=Bno0CUc14%hAC=b4*&iEzgAB!L= z`hhC!k&WLZPFYJY4X1pELFsAnJ!}Y@cW6I~)S53UOve!$ECM^q8ZE{e{o}hoflqqy z1*ubPGaeqs1&92?_Z|pDIR*gw{Tf^KJV)G*JLdzktzF;w@W<(X2;}XY0Mlzs8J?$L z$HVp2*+(o8?*n6cqx3_k6 z_&05@yeYRSfWQk)=oa0v#3BHNBBd>{fP`)#O^*^0_#?tW5jf!vCBp<2W+WCTEYeSv z9x0#bu>tB9M0W%_p^S7&BHa{2hfNL5eUUq4dFsGvgW}38M#j+AdeC5Q0pg^g zVzX3vrRi^YI(~*BW_Jv^o?2;5SRY4UiQy4mO}td`T?9Cn>K+dHL)+V&T+H2e9cz36 z3w!e<82_a0Abraxx8?L{a%&###&w=O83@y6xz0Yz{8$Wp? zpRHDDFRKHe+@^Y7*&@z$+aA;ksdi7xdV}c(i1><3F00dIA(v8LW(^O*HX)5kc#IRw zqF;w9l3uQK5us~@YEWk+?*7*(7!*}^OBGk+&H=rcQ31wWiI7@}vU8P`@-3x85BGy25yPLiFcZ9Ix z&g>o*aIM5;Y#3A-9~8-WmTezK5V~98kP{j^ZZ|WDa{ZX{nzq*qy3?Lw?|D4hN>kzB|OT6-b>reho-)KPiAg^M6 z^V7T^-LL<$VK9OM_AsP21hWykSObS?gk4L=NQ@Wevk9nXUWk~lu4S>zqFX4H{cWCE z8{eF=%>j8Xll5o2)cdA;Gx}>chr}9ZPv2kT=8x~q=B4i_@+{8-#jh5lsK}aj>0zxd zIl8*E$!(}Vii%YIB_2V6>|Ove`W+f~dqsd+*K|~yHvkUoMukz^XnLgcXunf+E9#k| zU0yT>#IG*W)+6ue)vv=xfDT{9k$;BDL!duM&qpGVui6NbuaKa`h?7i(W~4YUu2O@t zV=FEUMaC0QAIZg2c%Yb_WFI$vZ0z*fj-GdWkVMt>lDy@w)qhCE7c^Vx0i34{@bnQJ zMhB3B>8stMqGsKyqUsN>cE5xczm}r!D&5+?zTtYl6!U!4nmiPv?E)Pe$l(A@E1T7dD)Px*$)#pB(Mccz%i%RKcuskizkH& zM^+m#S#sK2?f8;gH5BaXCfyI z=Mo5s;fHbBh@$hNB(!H7;BeU>q)!Z^jaCks!;!d2W7 zv{8hf2+z&R2zAS%9Tu1(dKX~*{rOT|yjLsg6Bx_1@bTy#0{R-?J}i!IObk@Tql*9w zzz?AV8Z)xiNz}%2zKEIZ6UoVuri+AT8vVZBot|VA=8|~z-!4-N@}@Bfq$~F4`^LO) z?K#tKQ7_DzB_Z%wfZ*v)GUASW0eOy}aw!V^?FkG?fcp7dg4lvM$f-%IEnIAQEx7dJ zjeQdmuCCRe*a?o*QD#kfEAsvNYaVL>s2?e^Vg|OK!_F0B;_5TuXF?H0Pn&9-qO85; zmDYsjdxHi?{3_Il0sibc3V2IAP74l2a#&X0f6EdwEb_ zCHuQC@Q$(2$$0W&FuxtPzZJ`{zM{%lcw)>^c&ZZe3{GU#x8ZmhC${E>XcP+}<0zKn z`!He406MT}e^f*=$WZoCHO>xt?AE)A6xB*54a+>4&{!W0*`Q93ibK&4*}N2!PdjOa z8?@WRHjyEXqa(1=JSuglKreLS>x>SiHMYiH7)EW4L&&HyJUh+>opC2p&vz)-)hLZx z$xgyMGH)3R3o|Ptu(n3@oM8uX^(hq+q=`-aC1BlQp2I$eKj1tJuqDUh( zDkDsZ^23iaH3;bn7U>k)AD&%$u4G55$I=scldY;vFs+SJmR6mE&8&=C%8}PL3Pz1e zQ8C!gVj0PV2ym8>BOJZh9EPGH7B0X&x$=hK?E>1-@+vYaj!Grfw5!*_$pLHotuVn@tVzDd6inT? zVRbufqa&mdvhz=1^!A^mshoYUOn2TjV3fhuz*2mdNqBX{nUrI%6StBzCpt&mPbl5F zvw_Cj$en(bhzY^UOim8~W)nxy)zWKuy$oSS;qRzt zGB#g+Xbic&C4Zo0-$ZvuXA7-ka&rf8*Kn)MO$ggardqZ=0LyU3(T};RwH9seBsgBc z$6-BI}BN*-yID>S62)&!|-r4rDIfw zn19#SN$JA4xngbeGE4txEV5qszS(EnvzvVfh08c;IO5>d^UpU#m~24P{^7AVO7JAS zXZ6RdAp5-_yL;j@AlsMp8N&HVwHV>9DfH4c81xmzCzVZ3fXAQ+=RnI0B<;YfHZuqa zH|&*09Aj{ZsDVS+5jB{XEkd)PR5JO&0q`JK;9>!6T7%b14rbcBtNiw}OPI9h?u#%^ z{#w3(2+S5shq7N4smmX#Ns_ayWl5jP^7M^2hVn&gl1y>C@BvQ$Ah*^_cgzF=iG z39Lr1x6KpDuS0W9tH%r}N=vnOgCk^E`0I|6X8%H)E5a1{r;Ooi{4RF@DssCC6!o~J zDpXb3^$sNds;bMqm6n#cJ8M2#j7A_?^(fYr0QA$GrTQV$n;9;Qkh~$WT|e1Yq}o;h zEk_Ww1Kf4%%?R!{!c91CSJ*2fr<8xHF)(7!_%EKZ*$KsDg&ALtP>P19z99^whu6ms z^F(P(PMjgfp#lXpZt(?04@z5J{`JHow@|N~KFN{8WLok3u$zxk=`cv$?EaF;?XU6*mT&GJ_`>Ma3MgI?U07^UN9N3Fe37d_Q@ z-K2Z>R)Wso&W%+APtaorr8H4bEP6FH4p7!F)=w=jfs{I20h3Vck4N=Y(~XC1-kIAd zy5x^LnlUYu)zXH(P}oXq?U#Bgp{4bf<(9x%vx;I>b+jS0&jtaYZ?(5Pfi=RUF`r58 zPQbIAX=tIC=*W@cR#+`*i)vPR-|p^(ORBp*UB+Ei6;0-CF@No`$y^MQ8{I(2`CNzye&0=Q^qYjw%}y zZk$+l#(MVftcugPvORxL+@7k(4XzR~ti3!@toSymCaI5}vo}ri9vdMZa)_TzEsCB^ zLAkET9Z0E*!fv>)%Z#tIxUhYw%QRE2;98~{O{W%9rXI<-_{I=y%%qwb%iNi=+!>Qf zK(HtaA|ze7afz`txb*_lkb0u$(ijK97^%;axfg0J0#7NIs61X5HEQ=zq4Zv>VMu>$ z2~v10H$A`~ZB}6dK%@F2UgC9sMoSgd@q}!<7mY~z+C3H5tBW}xeKN&KIXP_?N=ed~ zFv^}TDs}$Eb(JDOQ;H7ZUNrivfKib({Ix|*X$AZawRj(j{g<^=Frb3--rEyv z6xZd8uQqr-K=@KuDrN*E`gfQ`mxKf_5w*!nJcKf(S=suW%7rFjx+s2> zi#9ouh%>Rl2Ch+}ie_3lybm-tkHbTSJILVkcjl~h@Q}u~N~u`668%(zQ9>9i7C#5$ zx{s(#H|$tR^Isy#9Q9XsY<1MHT-F7OyLQJdGEvzDtP8S6C2h^jU=C=>>*UM{Ijd1dNe~wr z+2V*%W+RpfrPRjc)E0!+gT^{TN*3CN1C}}95a1F4XwxwLS9A^ttvzq%M4HJ+$y?4I z`yKD+?Z?h%Uf%Z`@?6k*M1Nf&Cz(V^NgBygk_J*oqqX3`NcK^Lkg7rqVHhw@z>zv- z%X}I!;8!nQ^_RTCBos2Bl+SVD9Fa##0@yip*+{E)wPQxv$$hRA!c&QWLoLFG2$U zYDR(@dUI1w4`Zyv?%zhHwZ){BfpG(vq}!Y;6q(jI@xnbko7P(N3{;tEgWTp9X{GP3 z8Eh9fNgec!7)M?OE!e8wyw>Gtn}5IO|5~^)!F(*STx1KCRz?o>7RZbDJd>Dg##z!; zo}rG4d{6=c-pIFA4k|&90#~oqAIhkOeb6poAgkn^-%j66XICvZs}RA0IXj6u*rG#zR07|(JUt8bvX^$La@O#!;a) ziCtKmEDwgAp}1=mhU`6(nvaz%KG1c@?X8FbZK*QU*6mn${cWs15OGLA-803ZO-?=7 zah4u9yUPx8iI^Q~Bc7;DSaf@k0S@+p?!2(*$4}3v|?Nx~swkjwTmia)C!dVfht zzo1E-1vmsM(nC);|(Kp4yaPusRKec@I0b0J(n9k*tg>E zC-M)?LH%OLASR6}G-`?oyQ%KJ3(+KfS;-Rndh?ku8frhoZdKm<$0bj0e4I_lCX`7S#zIYBZ*s)i1dsNx5wX6~IDx z(Oz=(Bo4-fnzObxxiw~v`H}FuI<4v9nlM*7QryonD7aNenD4Iivwde7(TYd34Y|)E zZ;|i*$m}OZEsYWN9Xn+cJ?tl$HcJt&tK#m5)0pE@XV}gwcJV80^2W;>rR>%lUXzzrnFRHk2?0nQST``j1g;Rr}E@4Bo##q3%WJ3kW9`oLwIq zA0vY(vUKK{!(xz~Aai`k?GLCg(L^>jk7c19wzM!kci)KXbo`HMF5|jVUqOh5zPHx~ z7u)Wv`L*($bdq$~K@z$=!D+{HF@qBwO~Iv@@Nxw?Fyp2O5_#Ys8J$}5^H>J%`@CS{ zt-hYIu7NOhv0I=tr-?4EH2w4i=#_UUmFjs z%A-veHM(n~V=b%q0^_6lN0yt~Pi!0-4-LyFFewUhvZI$BFGs7)rVm2-{L|9h^f~Z)eyKyr z7?*u`rR)t7ZJ=8!I1#4|5kHXDmljgsWr(i6WPJ0eCg9K=mNGR7`F@<9Y)ptr=d(G2 zyFZ6ui;z7lu4{L3aCARB69KtaMekNz59bzEC8)@)F`W`q&hnF!@hlaZlivmQh~9 z8R-`kyDt3>Is4#t4`YaCAl(Y_9rDyTs1KYE_5gKHl-~>Ih(L@+s?${L`>}yrDEr-q zaZJ6`3Uhb_efWr)4dESDe#xM2C-gvCth%+_s@(-6U(RvIlv?Ex6v_UD{5h)9b*>N7 zzip!Gp<%x}c#!@x5`?mLYygtk7JG(HNpnAPnU%2^Gmjs75I>IS^yb*`pyeYn!J7D^ z_Z#@1;rrh7(T48tPjx2LKtKflO``Iz@cr-po+gBW$}#TuxAUQHEQAn2AEUg92@)F; z3M`=n3n&Q;h^mjIUSbe7;14c|RaJ{dweE`QJlDm5psETI1Mo@!_NG-@iUZ5tf+VTP5naWV2+Jq7qEv=`|Y`Kg-zESx3Ez zQ)3pq8v?(5LV8cnz-rlKv&6J}4*g7EdUU6RwAv#hOEPPngAzg>(I@$3kIb+#Z%^>q zC6ClJv0EE@{7Gk%QkBdOEd0}w2A}A(xKmF(szcN4$yDCezH)ILk`wx*R!dqa012KxWj{K;{m4IE$*u6C-i^Xn@6TimgZXs~mpQrA%YziFDYm9%33^x>MsMr{K`bk4 zmTYOFO0uD{fWnFuXf{4lKEGfjCSAEiBcUh~-RK~vwagYh%d^zqS*rgiNnc4TX!3<4FL7tr3;DA>RcYrMt3 z7h~TlyR(x;>v|5s1e#?b~H|Pqc=q};~YvHmKp(4Zk9bYF9IcEMmW{Q;%denJT?l4 z70{bSJ{{dIb)jJC54M+j%am#jwFugdb8V~47)xgJ;{uA!=Zs?&88BQVhSI&P+}(>q_==| z7JnM15Q4kwb~Px<@LEs%cxdZlH`{A~E3?IKpfJGR2rv7%N}=c)V?JJ@W7AH|AkZUh zvi2w)>RY)$6mkHQRo9L;PYl3PPg~?S(CX$-5+P!2B}GqIGEw- z3&}?!>|j7^Vh!EMc2U!gsDhS&8#Pq)SlamRXJ#FxX`caWHH_RW3%~WsoF&WECP$2g z3vaHqsO>V7k2xZwX3!-T2cj>VPidn8C|_4c?CyU;gpnaO(?YGO=a)9=Sc(n>Zb)C_ z>8fRKP6=d9Wg?&2G&5nNVU7Xk_8F-TmDrM6uNLZNK!U|gEn(vb`sw~_Q7LRLhitWE zJ{DBl&v1l}uTVoMM*y8$1{W*UIP`Ju*BeYbo`gJO3-K_tZ&4g%BSpS&lGf9 zD<3|fTK@&&<9U(QZ?zOW4zHKQXw`?v;uSZJ3ZIAji)F;jrOD;GeX1VSR+>@*5?@>z zVUfy2G!UmbDU$F&S&~3{;e=EUs{9uU^x(oT)!;)yX4Es>NE-7X%5^brZcL7_$KhIv zr5CGYP6|tw9`3$Cz3Myl8 znbJvOI4#W@<>Cyg>1I0>WiZtflPr-GM&DAaVv>AI;InpOh-5usQbSpOmTKY9e3EKR z;Hno1gPK2lJj!r+UKn9Zp#3yQStL5eP+`n?y*fm?v zA84*u&xPM4%6OaA%lsEMxp<}G&L4b#3zXfT`Q&U=2$xO!&?4X~_EUw`E}jd$70B`D z%VO!*-NSxZ=hz=*vGi#2+0DPI?Nr{|cA-Xm?8(IBQT5razQXk&(-b@ZJgwDKQH#!m zNC}wPd|`LEdw{jkq}>P?kLv_l`1H;`3Ypo z<=~^h)h>9lcSp#~`+8{d*nkO{Q57=hcqST+<>@KCkjsY4-m!~JrSs!7e3YBf5+gie z@3YxN5s{0Nw97uJlOQ$kM!sMpu6~+PJ9*Ym^Ru?p*)mlo*nLP}tQcyY@^-0%KE==U z9_PrE;U|ZK{=rZX`6#d#514_!C+5->pSvmgNS}EpK($i?)6CZ!Huf)`&x;5Z1A(&Q z@DlP6YDZ(sbd(>nxM#=4mhsQA4E;<+v`Q%cvx`xmNiP4h>WvTUPJ22uWaL49LZe&$ zu1$oP!=mMt@SLsRR9nk&V1bN$rN33*%D|rhd|xC)oT5}P_9ccwLRy4*EnFy#-VG|7&>jsJ2#RpDz#r@68GuOAE*sQSmL#Re$ z8y$k2M}GP&w8RPob)Z+eZez0hGJ6;ig$hoS`OMO5oKKR#YtoGWNpHT|{A-<2v@r9k zdHaj`SnX5h4E^0M=!*2hM>m9i#hdJD+AEofPeP$bAN9B`?Qin)0|4sWhwTizniPlA$1E6xG?)-y`KbWVB#R7|wk*IeoeRw}# zv0XV|5pzw9*e0TCxIsLcdLNFOYX4Y^gpD&=N$!;WMK)%4;Wh80b>{oPy}ot6_RYmF zZFlk2_X|kWVuVY)O#Vf9iHpmhr1G2no4g{P?=gJ_UpU}HpD|jo+qJb=ynu~|cc+v- z;x`}SwQprny~&aqm;cD>#RsRo_#Tf(pEw{Z8_{2^g#CKVen}EUK}tsX@2GvX6kFB{ zz@BgZBarBKocTk%rxxP`3yE^XTF~#~>G?6S_kr*M-OA&x38`~(+>=FcD7CF1Zzp~R z`rhZwkz2j21wH7{BU2yzTYRZMGS+cNw5Qs<(MJzN+PcO{SFY&&dRNlj2{vylsOs_+ zxNOcD(t>RX?HVbjT||`Df>@!92R)`K$w3^9!FYA7Zh8->KU!x)e?ztv$;IVrH@|W@fd8 z7BiE@%*;%u*_qv$`FHN(BD$hGqB^>w>&yBw^JV6HC=#GpjX!WQ(zeKjLwM3%)TCMT z#xyLTD8e|^YTKwg=Vv1|?|13o6!&U$_A}W2wWMcD^#DSn@g(5GbsHO6W$I9JNSxoCmsH}pFn8j_Wxk~5^ zVhEXZ+s@i0YjOeagPLSQYoxR{i2biszj7RW*S<_0j2Dw-Ef7qqLN%~y`ZAHIINOP} zvmaSn7x|DlC&W$UxkMbbJ&xpGD97rRFi#}3H61(AYVcPN9YUF0n72Zo#a#jfh`6TX z7!Pw#0~N0S?BC*wDZ0l04tmB!J145jwS;Pci*%m~ID_r&x0H;>J>$x}okimL!WLb^ z%m!KzacfeEw#alud8ZbsYF& z1@a|GCQHDAcQ3iM5LfSbz{fwQEh%&k<8f6$Q`yJ~Y7aO&6=u1}-*Gqw6$crh2cZ*X zMJE4cPZcdI%GQ>e=U|%r7EWn5pWBsM{|l8thH#qb@2{EkxwMBgjvOdH_IVX`Hh3}l zHcZa5HIB;>NekQX)ukMQJ`DTqS}jZ#j|$iH=Y_~kA^2?d%gm$PmPGuA)POynhUyaK zegRG1n2fzKfWg9@a>C@^5M)xpFSicmIRz7$?!Cq3uh(hTvD(>sag!Yf5*aMvtv=^^ zleZUVg$1$=zDs9p6Q1CAH&);!jkC-ZJ{fW`hE2o0x^4F_jcyr4#!ggqbcMo}icm`y zQ_77P#ZDAzmQz~g1=4DW!t7IZa}Z7thh#dEqn7+`5Lf8=4OAj_>AZ3IGQlz5loU2V zh|Ok)*^>O^ITIz*6(a6LT46*2Z8qn|UEzXV(Cl(`t!NL2^RU)JQ5CwNXU<%q`gjnv zF8YRI{0Qs{HiYEeK^2%=T5HFvrq^)R3Z~s+&dp-ZNpWu25qg9QUYwJZRjYFp(D>*A=`$9U_~N!BjcnQhdaf0Wf4k~Wb-yz6v=9i4rRTbdv0 zO)%vr@`J~@XKn3Cmo;jazVHe{VYoA-^m4ZO7VwZ~TARsMO7PY(!ck&QGkAgY9Q9RJ zLr}6J8cX!W%WFefwo9}P-hOjJJd>||gfOKNQ$xEbxDL$!N<$66h}w{A$tdnEEUq5; zQB17>Yh#_2o^GIeLQ`D^c**S1E;}*EAjaUHZAmh>Q~WW`RrCigz!CK>NF|IY`w>Yt zHl!vK+Cf`LljiFI=u=(p3$f!)&jk0aE{~>@e!_NZAc2Omti-mkw)JiJbz_^F-VP%u zQ&y+sQ5}T;hcIKT?jPxfEv!MA!t{oa;sV+#hIQ7_qx8Lz5Sulr_iep}MwMTaYYHyE z;th6PF7kKkE$1mPSGQC0?W9DiI&FS zPw(Wqb7k(snDvn6ol!D7!#GhJjH2M&gJc}C(-vuZ?+cGXPm&H#hftWUx3POg66a6n zfN##yl=25{SXg!9w>RJsk>cLGe2X4*AU?QPz|qi6XRQfR&>EZ1ay72<=1iIAao!gl z=iXCdaqY-04x%}=Y(<*>tlU_^(VrHIH)W}5({50@Pf_Emkvmy1_vz}FN4%!arFz{@ zGv%Z<%-w_KloV$v=!Z~|Z<%S|Y2a7~>BkxgdN}R+5+GE`KL1&xvnC1ZF`O&)@+-)Gcq!xuuB9S0X>R-t2pteqfiBX18=s!G>_Y z1xdnN_B)8}I9o<`n6y`b6?TV^e{iJi5!y5A8#Yc0miLEe zI33k{;HS8^<|IEkcVzjj#3rzLtPbmdq8r6_xeOf+1flw@2u{ z7ph8+9FzeiT#-P8tS?i#BdQ^$h{Ww*F=6X>5d^;jC>JrKa`a2vZCP4F`(r%|qT)+p z8I(A**}QO~>w_{AcjCG6S2(!)!0Q0koYHOqp0J7jIN>?pqxj+UPbG(ZzH%R7XM90` zj$jS22XlLiS_ef1-*ioM!Q*00STA}&18-3EN|(Q&<%b4;8@@tEm^uU}c!LZu9o`^A zX?d0=!n9~@Op+U(i2*`#N{3pe!XtMPb%k4>*#6S)3<-sC5x+);@IFHe;)vLac7gVb+ zVy%FX+y_#;fY94b0?IYZkO^Ow#D_#PU~5k6IsF|@9#PExC0GDbVu*%(SN5nu45KYs zKy!crklZl|C;1xq4#gk_`Nhg`S}5lC++i0e&GcafLxzk_hVLkBG5d2y{94=Z+|x=1 z%axSnz&LR0GB_NUJ02Lc;Ywvu?Q4ScA)Ezcg)!G2B1)N>;~wK=y{3lDg{gpiV|7Qn z#pOEzcxTd{r1`A7Q=fO{Wkuq(Nu{edMD>fb`0?+_%wU!>D5zX;AqW)-;3!Ex0vhNX zU(=77+{)#g(yr-uoy1;VzA7=eqw-JnGPqHOS9eh-G-@b?^PL|t*sa0#ONj?=tb;`? zl3AWgQ;F`_s;d-UQw4ap81^{HPK`38^=*#j0=$C|aKZrRIa{?amtPS#3sAyjQNNE= zMb?g$oC)nJIPC#jz%sw{QK8};07-+BdV^4n4PcL?xNe2Unx(ja7Qv=z_StA;h(t@` z(NNC7C@e%oWn=;U?G`?^0-gqzf+ur;K~}LsU5XJOUlJ1+>uC@)ch>nl zTSAKzE;N|>ob6G}%w)1smx;CC>fI+tlBydTE74*M`xWyfEVkhU0|-YvvQ@BS*=1*E z51c1H+!>B81O@#;EpxFY;eQ!72d*%yDa90owz9bww$P3P!PL8B1NB1>hZm6;z}(0;}OlhLJezvWPX0@NORT*jtJ!^cR@vI;g*o2t`ZiJwUsBg)gff zZE|OPnxbToa;liDWvy7?*;dfZj1DP^FbC{!haAw0nvpCY1``va4NgJN+5Q4oFCb0h zt^a99;!%c9Qzhh3JiTHZ?tWHR5Wz2sk&=FEtvf)LAVL}ekqCQE?nH=)#wWLp>@1CT zsg*%F!$+?0Z2>!V;;{xXE<^&RS}z%8PcOkF{p!LGufDBPhMPC^ zG$q{wZ z#Ja4}W6245crq5zje}Y@*c9{lc@AzpQqmGuXJ~LY$*{`hg&Gf3P11|WiFee_O|b}! zVRY5AG_P@)S3`T7$B`vU`zoGU;5|1#4QY$XU%4+;XJ0S*Gf z^`C83$;j1G*u}-n&e+z>nM}^X#K>0cbBxQ`${65k4P9l~vmH4wj!dK9Ds-qvw$pf(6VOiY2 zE?B}k{2zUxzM&EhG6jZ^@X=))R&lRCJ#H4rUE-D}<&<(5y_%LK&nIcv={%BK0e!`un#9Tp#Xwr-Fflcti3K={AE}6#+kt{Qie|AZ6 z6*&nr;n(wh^uhJE3@XxoOU#BJE&q;S)ux&^y%En`f>||6x$_bSMn;dC71xBhpU~E{ z5f2v|P{1Cv^jl+$^NJs3E!XibZM8w%4kl>uy8yA#xpwUfn$HvbVs|_LMy>AUN(Ar4 z6ZtLFzwcQpxj;zF&-MnRPYxT3{|`I(dzBso9p=4TUAQ4of#Wd3q@H-0Gz8C6U2uxl#VXmC}x+B`>D)ffK;%ZXO>H zPVvNavG%b4+j~NPJ?rVff87JMOM5lOQOltlI~`eXFb2A)9UhlOiw3q{Ke>OF<`kMl zD=jNgN&(C4hl51!cB-wzNNv$JDl%R#CFx^wJ8zI;*wqhcfv8FGOLzgs8B8@F<^2`p z%)SN|zLITOn%{T>nk3;{6-GYt$(;vrEOutbF+({n^elu<|244j+ z86+n$mOkc15>j*V=xfd1B$*G_jnCJcV9-J8EZ4((lhmZiNJw`_M7fwG&8pHy-Ke_I zrkS&<(%!(i9Q}xb&7WPk`{_kfquVmahoIG>3~7f7S+RSV+E92f8X9;%>e3J=Cr>x0 z&~#wS|C19#Hq^JQmKY}+yCL3daSWFY*=wp%?jSI5|8X-huuF_swuyAM*laABQv<nM&9OUnkdus9i3(4|D}`eMP1@}Y5Bb1U(z#8*%%$T>s4~qFx5>;H zHo2s5PKg@JpAq1ZZ4ryNp{ihW>z)*VLmyu=cWSVjU!#O$Av&KhM`<{OsHeT4W^L$D z{FjnPLb}b$BGoEeF$aDxO-llzmVFo67b$7hXg_8Tqtl11I(W(^t~3EMSd=YsUc-tL zeLEb+dK9(xLL!m2ow1)kliqtx)H+c?rCAXtFh}k)h<{do_@=OvP_jjD3nLJIHX;cA zVfvn9=>eu_t@R0_vlV-GJm~znRBf*`LeMt24Wb(uH5ag1#POrx5gcU1N=^GbQA zX9vONEw_HE$REtCE;n>zdhek^PUnZ};@#Hm_lec6sYLgf#WB9v_nsZ5KeZMY7auW5 z_kJ*q9eK)**B@+THL8Vch#NR9ncS;4qP#j6})Vi(T4b#5_y$z z7?C9%S=An`M&>9nt=_&CMr#bKi5!PK%Oi^X!xk~)OE$*!pzhBbDl|3c_cJ?Jt|od% zuYTxQifMN~M*;jbwvtdar!}ipi6*ul!tJ)0=`QptvVjiLWO?Ld6ii1euZ#(56TeW0VKXYA zO;JSEAuLdOhiOC(zo^YHO>63rTdS-vZ#(9539=q3ZSysm;qjs%@UoRNo1fD+cYOcer$pT%eNH6nAI) zF#HH}KZtL)Sp+0rH3lrc-tc*6T!UfgJ4jfcO4jby`$s!NkCaEoshYG5Jo6~Z904c_ zN@%e>N*~A}l2(TI*J0P&&ek!u&;b12$=W|DWJ0HN04;s(4eX5ydQQ`7)_VOrV%JU| zAsp{6!;B$uFYtT>M{r;b#P62;8PhsNPB~ zDoO@&p=doKv4mZP-D#zF_D~qc8PYJQJ|xuo%cr(3q7)B2GZMPwDGIJ&zZi;fUEyQ^ zlcs~)j^o>q<<~(~Ioj!$ZboT%dYqkYXq&vL*WDjLt_ESAA*A_+)v9X4Z~1?D*Gu@I zNYE?q&aC%8EUc1@Gw-PszuMQ!Erq`S#kHQj5KwM@PRZ4NlK(ROXVva0&c~E!#qtJ0ujV8(>y;aKR3G#1Mf43 zs*c3YkGCB~5XCJWkhOHBOJ@*-bm(s=s<7LjkA==WAdsxiSCN_HG*VRQs+ZOv^y!x- z2C;A|nMuaXAm|6=uTAFdv78xK6bw>VseGo>i1Y#EWJOx3B56}m<5I*`T}qD9x%_qM z>9{{znOJ%GMVUDWcqR9C$0bwpMbQjd+S2r_HA|s-X~_nZcDoQ?DCv38rI(hSCE_ZV zbvPUoTrAj=%zqNQ7P^-Fp>bqVgI}m6*^!WlyGKv+92^oWZlrs7 zLP%PeYC`}14V}Z>{6=9~EdATJEHiIgFI)OD3;bRds~f#P3rA87s!!-^uI1br2CapZ z`1v@|yHda{pTH)AkuX@Swr8a=g6N?>VNRM z7dRL!$B(sDymlKemGkMDPE2d*y(`$P4}_OZoiG2^U!|m)OKnsrH$J?=XL-5>htARqAgN!n1k0v0x4yHek#IorCFRo7^?-1;kV#W$fYQ!QZ- zomxY^(n$ZyZEU3bRd(Qmx=%pGu6}>mQ28S?VS|^mSzr&Wfbtc!fa(?ZZ>1~p-zrz^ zzm3k-e4;KOo(bR9U`{KmT>prvOF+)a;9Ml_ou|vL{IM=Wwe`oeC6zehu8qmGfVHua z1Y$@hbgk2??zN>r8?u<}nJOl7GDqOU+A)^>wkuZ=$Y+0?aq+`izt9p#hof!8mlE^O zf~Gi`+8)>#I!~O!_k0@}6j5)Cw87lr9N9gq4%B4BC9m4se#V(Ln8hzIpyRB}YGS^g zuNz)bukTc4-C-cH9TGtxvp~CV=`XTDd&4S2E=a~QX zH34ta32)bdsH=6WJ#2@#8V6}tbI48DGdKfUvU_^LA8y+nb4GUQkR}LPxm+CNd1|r_ z1{{kl@@K!{B?`H_fqa2bMp=P_xGQl3^UVQO)zE&*>6|fd0-ij2&(}+rzuIf z5BCVJgPeH`_W2=)_-9p+r-e~Ku;noOyq)`Rpluve)JTNOUH0EkxO#^Pz8g7A>2|Gu zo_MJ?scrYD45&6ToEltGJj8>3)|>Uy;dJZ@3c-Eg_+sB9D&U1|zG;L97$k}{!5VLm zZTG>$Pkz}N1Z_+lLxbHRQ6so1{TgU- zNgLZjHZh}%$P)p3^Gekk&O5Tieo9&&cDwA6`Vp6H4v$08e1lb0n7X`!_x6ZQd5Ncr z-1or8K7tmVoT%EEwQD=~7Pr?K#Q{0Fu|sSC$>>4Wb1Msgv(Z1Z(3m7U zMO0y=!H*S-W8oYSQ1PnB#xO?}$Q)^p(#SI7QlV{J=a2?GYE5VN`98&>h?oe*R}ep{ zozpe2vsQT@R#sltkEM-?rp}MoSIFEzNh`e`A6Ph1sa~lqf`_P8wdR(|ad7+8L@kAF z;vhFm@833@Jipi6uq3Pp_bF!`={6RZ)_q3e&#G#EWcSA-dg~O=vK_0rWH@i|&I%f1 zoygC}jg8DWcewP#zZ&O+CV8OUQ)Dm2p4Bjk$?oZgE_%JhAOFZW({kXYL>TpT;Lzz_ zI|FZMvT5ZIj4~Y)tmhAPt~%q0DYhX1((N?ZWM}JC*I_>20dJ=5-SmxUPm+W65rj^`Sjpw$s`^3 zE*(gDcZAiVe8og}D*eTK{{60Jzb!|N-s5|xL@(8VWewvmO-}3iw=6G!_s9I7pXH&* zrdXkqzmYytJaFoVEQefFHzj&&L-8Ck-zIBhH1+A6Dx7TbAE^RAhyx%HXL5skx89S4{#ET7{&c zmPoAZzn~8EGBAIa)Vb6MJ!#GZi5MYbm5C>b(F_nXi)XRA1togzy^M087T#tVYDd`x z;*c=}(IpnMfRND&nI{v8vJ54n?8f4lN`3K^%b)}oat1TifJuxO&ZZTXv5pUhub0Va z0wwYURnZ6}Gm9@r5z`F%e3zeTCje1FB69h@e{T5iwyiaFBF^|31@L?}B2xY5NZ=o~ zE$(4v0{AEMu;!Eh>^}AfO&zIZILKE}6cHN{5EEVqDy8a~1SAO{o{UWYu(Q(T`PAts5V>@5aLwuP6?A4V6(t8AZ*csoO|B$?XQ9mzToari6>M0&(#_q-@sf0G2g@us?RlnK?i5>!_})FfdEnul&4?fFyZ!m znCK()B;nqc9yH<3(+;1HNFSx>BO2|cmH9_>Fz+Q=1y^syP5ZMgbdJd#BU7(9as%Ha z^HX%VEDCVvM$S*Chwpb+?xd6lMjE*fvLWo&C>YLzd&w85R^HGrZ7(kpVPCu?l0Gs1 z>hIk~pj+7mBThy96}uG6s>OMG6mD=@i)9C}#fhwl)Jyp^xn=OVCWhssK}rg8=eT@_ z#MM-!#b3{H*Xr$FEUim5yRH+?cP*`J{c|f&rbWvFlCDFuH4#)*;lNUt$}#2XSF&9v zrQcdn7C`A`pBI)gGu9`(w@al@TAb`ex0c_we6RkY{rql>Q9pi>PGM8b2KT7qFnaxV5b zmoEvhO^tU`ABvOe!>+KynhALJ%$E>t)0)=h(O|==6SCC1QdZFZD5R7X(TTm*Q7_hO z7=l`B@tJOngSoFD`AxA6D{dmf-hq?o<*Jej1-3o?L1`s6?+mT&LguymtaBrJyuUnZ z?rVkLYMuzew?h6~WR}&&rjgWu%Ol0zRpK~!e`c9{nSB|I6c>-U%w~d<3Pru2oslnD z!7N9~Pvko?^+^eupC}q1Sey*kNzo2lD|DB`-Rbj%!6@17B|U@DbT%ss`OK13)V3c zBwneSClO9vQ^N*Z%RXYO`Wr~pe)sPVHe|_LFY!-A<-IfJFyW4DQ`-%WQ$+9`xjvG( zpQ|w~wLPi9e&l?tir%<7e!wa+NTIeV($?_M8K9Ok9K|eg(1Gw$>)_r!@~1mMWch?I zlu47XEEFQ?B*b6E2Mn(`k^R%I5MNchehcs$@A>Qon=44fmd(0d!g;b+#n@O=a#iwYWb+LEvPA@*#Kw4&DzJnYfh;LQnC6!87g zdeW^0s%^91PAO0q`>$Mb==p<41NxthJ-IB>>x%WSPot3rFI* zMf_9_Wl1cS$EV%`sC?Jhn@_2EIcHtJ_h7LBu5E^=&na;`bMz8S&E_6(zjFs3RZeiQ zuRTJN2!tO#0FHtOBj@_b2Se=SHmzr0Tt=WHWsm zPs9+a0tP&xdv8i{VnZqpkkTa`J-)KLAX(5g`{CFP0HkK9R?;p};94=j88#urqEf@h zNp86`#tPiH=peJZ1GkQ~j!|~G>DtG7jQ3c|>9GN9;LJVY1=w~3+AxFB$^Eo!vtkY< z^lHsv3=oH=6dYkZUJB8!gnGuu>Mpma_%KKAHQD%Qw+A~YE zE7L`H=rT?lQtq`I0KgG}wsC>BEIza!{njtF{Q`O>%)n&}o3jSMpQUFP%j1UC+HN<| z%(W?wu*JQbLVt+3ZDuiiDA#YyF+Ybg*l!h`SyN{^k0hQeu)8@TkKFQCrJXjud)K0> zE{25F{XD-Q59a5JYP&@17qn_&5_&P?3hqsnwKyDL`c}1=5ZJU0UskWz3a|b_9B++G zN)j91j2Rf7HbdQc&*p52&{LV;l9GveK^#X>?Yyoup(pf4w|r>&$=OG@Y_VMwA6hl! zIwQFIwy79_k(kp+&XQW7iS%nnfT|GF1~u@KPe&}8SiTJ;%RF2cz}~XJ6NDb<=rK#j zVHko2=aA8x+I!P%vZ!O9)e9UMJ0?eeR#JpbX0d512u#wxBlv;hf62v?LqwumZ%wcg zHVp25KY-e>DBPKKKy-JtDgj!RZ(S-1&dd=Xfl&QQQBJ6^qysCBFAbkG_9f#dv+)s1 z-L3APDR&JQ*PJ&s9> zB@&43RN*^1zQA-|GKN~I4qBYTZiMEPc`j3U596%W1rSO;yzSV-svR6&RH9>mD7B=u z8}eph-j#vh0v4B6McTDb$}TryMb+$sTV5 zi}_AlY6U+=R!x+it_{Fws^cQRi&m1^#pnUclQP{S=|M!jX6e!UuBpP(5qVg`=VuE5 zSpDtgx;0OGi1AVvVZScV;hZR4>PKLNj0j~Daguy8P6p8aJ#Wk2&=#n`iu={^&Cuoy z-OsacXUkkO&0G=_vb3pgg0D+_3b#{KW7s4b3?1@R)oPF<|d zG_ke%UusA5tAf>hpXrV2XKnZ|oQZ$?y0G!zbdF41MIG$yJ~1FUD|@rgG{@}|75Z;9 zC`IibDim;0C(9(jCO=WZUxP;=Hp0PKO>Q?1=4@jTW27?wUSwYJ5=htt-^akbm08Acywa z?nLL@sHAx-9N~vRRHk5`7W$g&)+fS=7KXruHCEE+=h`IRE~j?$(+$Nuv|ud;8rc|h zjdgESU_~0ZjvT}PN$$DBE25Xd!H!-qq-$f;-@rXwG-;l9#g7}!%cbSj%7`g-jyxA_ z0$^z@B zu8A=6hEd*PVO0if!FvNKOXTxHr=b0u@#o{$PVZQee5{z+S>bCizS`MmieM)ykX4gZhRpUGL6F zOkE$%^Gm`Lbd9qfXKCCp+^1dWmdg-NcoY+kwC`Rb+&@P{ix_T1_FL9HZn=tICT|&< z$H{Fd^@RXGa-_mGD1nN-V{GI0VrHfZ-iIa5NBVY7d=2t7+GO%A8@~x-5WU&2kH3_D zqk`_7tUqx{tWQlZ-v4d6|80u@L?!?4Mp>n?rirVL^s#1|6k-NPhJuub9zPdcC}t;X zlSfrFHxP;_4{1f~)}Y-ZvKZ5b3;!(mc+UO%q3O5S6&}Cuz2Hp2pO&BT6t;!bgS)$a zV_9(B5LMlN&4d5ZT`tN%!FUkZm!{_`EP1t|i5H*9W6l-hV^L zx!qJXeRAxC%aOh`>VU)L$Lc!pX&4TJA|Y^ok|g zGfQh;Rq}&N2EcF_JpyGSyGxM67#h+Ah=vdzPjUHZ_san!2g91j89&82?co8PbaI{{V*nJH-6oY-Z7TN1S54VidmMQ1IuCPAZY34*eyYOy*dkm= zWBmKt^*?yxjMko^(;OB+>mxwSTDg_&Nl3kTd_i5(x1YIH)T#2#9z=oU?&C~X&VJh* zC&dao)x@Os%2go&Td7bn6)YQM?7DCgOVd$hW<_kcf^{WhDRMGkvZ{&qjlF;(tv{(W z7$>A%gQ_qOYF&LitAX_s zomK?d5dU)Ok%o9z@e`X9dtYzo3)In;lfq*F;iGLslrQFTj^L#bFN^{P8Tk8zAsf z#keSh$;y9iM*Sqr_l1wz=EFXba$=NjYTWp-_yIAkN(S$eb$CC-PN#PoowN+o!DMey z#1(8Z4#=6dGYIRbLJMW+NVx09_`a_oo2N5P6Z`Tkkoz#_$XUhstzb@kZOA5N-Y!&% zw`TU0oGR(@E?u*=*M7z>?Wu^u7Z1R*c26GLw>%x<^sLJa@s8Z>F+cnGE%Ai`xC$d^wpgSo<>ze4WIAUE6Lvdxh;telK?xt9P)*x!)dTu6T=j*xL zkiLe*hoAV9l5hLoLxsK<7T_|lg=&wrp z*p>*BX3Uskrs5!gzfdod;X7^vSzcbzyR-0=!S>ltmUOBo(|z6E{s8j`iup7Rq~vE7 zRnWHm0f!Stlaf!zjvNbv9ylRrAYS{z{=tAs9k;ZNLce>*n4SX8jOywN_%rLNaG}t~ z3h7z*K+BU_xjdJ`t2JLTP$_d_le(Q74H##t9LWR}SnS@N19=Bkcl~6^qYRq5j{F_{(HdqNhjv^v)WoRlgkB#D!dh)d)H`V7AzDMv^$;{C4^ z(Dq~@#uN*gj+&HwR7MHYDiPnX`kXeGWIfJ9eqj8bvQ2arlrH)hxXo0QSh5|MBTKeE zn5cG-Uw&+L!y!~bvoll=Czr{~1HZ_c!tHx2zp8bUQBFMx795^CHcZ}?I3aiRZ8Jt@ z_{Hn+8>RJw9-4C{0#Rp|wR+54)ebE0`@9tpTE5X1Xwi_`zv5^+*X5_|WJ80m%iU#! zT$4bGhj}sl7l<6Z0^tq*6CTg}-@Q72iy{Bz{wn^9sb^_OyU%K%z3+0RnnaOdp-_&A zQpL(UuCU2T_aYTHVh0pT!zd})&LdL+6U;(qJd1Bq<=yFVF^WpMKADb6Dj1$ITTdnr zkEq|WD~GPtoLj?PH)h*5-p)HVd?zkG0du&3gDZJxTqlEp5F{V2jX(sCDo9KxX{~aP zv9JUY9(aVBC`pL{5iA~t(Polf=)9)gCaTKHT4&*1Q6EEeIM(pMN8<=dWxi^di<509 z(Sc7PN2z!hPuWQ`IF#i9hKhwb)9IO*-DGnF8Ot9ttlIN585zN6DTZM(vZCYWiK?k( z7OX+Nw@PZPs(N$ve{RS5vNXIEVz8|9x=3v*9zwT!STp~?Qmg(NmI|Nik%c~5QgbqB zYEC2?PcR%9L%(TgZ6eC+%rKl7BV#Sj;Ak`*nMxvU=@)1JNif^6T!`Pdk1J#2sVZBR znwpA)HPg__PDhM$6HM5|rkcgs*u9Po^PZrmgIYu~Cg$X1z*^GJDa@6o5`#TI*T1|3 zznkgm;}!R_d3@?ilQRYNV-;l9{Kma&PfC-Er}SYZ{KO0|#PQyAu1iHR9Xr5GZ+xX1 z$YVe3p(Ocvf+RYOR}K zqi8EWh=!!)B@I*IE%9u;V<-m1N_NcrdL8g z?a`g{d?N z(w+7w)4f1)n_7Zi9{9NXYDO>am#{o);@PlG(P+lnkeTc2M^U1R`+n3=5-SaTeBM0) z%kNRG@}o6-%AToQ(590ntVT?F6@U)=&6Isy2)}N*L1f4m5LPgamROcTYv*(iPyZ7c z#oWFCg`-d6eUw=UClhNO#vmqk7d}WW7zq;B057V=1_yWz^`sQ|iCPKK-*76K4e|ht!@`_yeX!1BAATkU7xFeYV z1PZo?&s`Us8+@fNYnk8(bz&7v_8NI9_DcEqlA8O-SC!D9g9; ze)c@z0tWx5DPDXxE&%#5N?4|>b4aw8>yRvSSEiX0?vLOiRHB=2|NhsXiZGo^5&B@< zeI31A+X0#Tx|c~iFv?`0v!=blr=KbwgLb78Gt8U_OIAAE2z9eNK&!s5F3F0>=8W!r zKT;oYg44jC_`bW%@*i!jZbKwGRx%8gdl9{Hbb1jDI`x3IjAJZW5Ei6(S>l@9E&B&0 zB3*=O@#A7@kk#)a|5-MdEKD-rCeGj6t~5#M&W2oS;K0izF)(Eg#omlB(Rx#OB)aoT z#GwXoK_5A|4xhFvu3CMq($#~xb8~18q6z}|Mk(d{j*7ZYQanRcz1UwW+(Xbs<`luO zHb8f`LI0u?3T)Otb_0X6$!xt|`V&k)`37wFO)&S%>7x!C60RXywvpkR*hEEuATHLB zx@Mc;`Zkyu+td&XI? zbu%d4p@UVsAW5iTL@C%3XR+Bptl=TbDEL_lvW3tV3l)rQ*yEL9_5{2}*ri^pn2SG} zR+-zw0QeD)q(v=8w55$|>$m^`e=SRmAT^m5fBNae&*Lv;slWJ>PpPj@Hs}8)xC)6D z{+kM@_=jba4xHOwYq(92K^_%!WFTeunUd}dMB?$5o(Bjbd2zGrme0Pwz*zf#={HE= zk-#G(=Qp%0W&TPr?xACqCk52iu;mm2Y}17p~)Pp;4!j)g8pxkGAfftTfDxEj~L%JS-YlQ79DmS zN^OP@{~`ohPv?81{MqY#@>z!a4@vL8_|AX)S7Gx{=taWH*~L{AVEm8Me{X*6*Emr? zRYrPOpr*5hLko^{?~9y*>xc*tZ&YiM%KMfA@nN^p#E|?c8W35t>GBAcZmA?4{UPUr zmeY-OaEd_%oDz|Gb=lAS!M&m9W`6(rdUJ;x06jy(gJfSoPLhvmgsi*@_=ffX5ej3s65C6K;Qq$m8<98QKQ&(2=PnxU-p zy1o$8j9+3oDY6_(6~00AZvJDQX{iOaWATzEh(B-7G*n?ii^k5}^sObC8mWZ$GqLO` zFQk3dGhc3LgXh1}46U4`@|u=PV=ro6Gk-U&3KzERYKq8iQ&`M{ z66z)|kDF*;2!t0`h2%3jtiMmCM!^ZbbEazf%%%b%rN^OWL#s=lwAd}0e;=qX?usTA z9(Zn-UmlKH6$@~yBkPop@gA+{^6&}OC$4EF1IHAN{w%|uvsCbY>|1Y3+n*y}m=gfM_MD2y2ybg5Ee#G4-0q!EQiw8pk8 zajMzrRw<+V4n|~tR*qNe&{ACV!QlqG+Tu_laOhYoqD#AJ;#RB7epfO@XP3?5L=4w| zHUPUmS;`H7X9qE!R2UvMsm6A;@=1O#5XSU1sWSQI@4a zZGFgOeXx}tmJs?=@*}5@_Cw*EWqjMYiP;ArX6+xYip?F}`38=k++5@zfoItr7BvNp zF4AQz;o;d5e2Pd(OFTD+j|Q|942$uF+L(@u_{M20MhtWi8oj``eZXbdJ;tUMbs@T5 z2y5LW6wZ&jO#>UCoMKMSy6g6DP)D&BF@YE9UtKg?xrubeFm**3WxIPdoUuJm6|>fa+?m%l%uRVj9gvr3LL<9h zzwJCHAAzE&-HEze3O~GobD}0Q8+EwwOWusWqu$p8zx0Xc)rsjG`nO_2#mkonxKUW8 zdT^tvODb;w?|v&f4=o3rG4P^EMVhblocIjZ`>hvC`9QX&{`gG;d5Q(*;i-d2Xpw&Q z(C@{o(K1N_^R@FKtK=F!$oRG`ANJ|~1L!u@kE-(fHSnoz^B9DTIMV%qFHDsLJLx;a z{kiDL9o$beEYbKDFhRicb1(FhJbGP|=3Wa8j344(w4YiN#2MMp;ozg{ZV|3@nlHrC zW^uW#Wd@qdwly%Kn#Y-3@(E1S1%~fg$8y?v55Ejv(DaH8Mi2lDLbwD&5!bxl1li;o z(LdPNVw+uqJe!`sO+I-1;BEVZO!%Dz_O@S66!?*QN}cGHJ0w6VOK24*rD{2LcnT6} z?;~uSqXzkQdoCHMAs~sk5Ds?W8B0!Ldi>wV}UtY5jdD4LGbGekgSgCxr;tWYlL{X}jf-~Z+7*=_Z1Km-EIkFnc0w}d*@k;T?0~RO(X-cMt?gUsdi*&sn>-7~!6{jts1NIoIy~YrX86%dgI}?$~|o75S{0+o3V$9hED;=AC2cw%Uuz zn%c_kE}cfHoSWej)Zc!aoh-n&ZK3_#(~$eJS8R2BuOn~A=IX3_35k7z6YhpHcdy?T zKih&CDm+TZQ+|d2B7GxKmyr)L^LpH%>r{7P+NA>@T2c_uw_wh}K= z{~#_+Nj<<2q>=ewjhBlt2DB&B#;NNHLLb&fj9u06uW|Ud5K!YyMi_OJ%*>q>C92EM z;>IlY(CJs-@UI?NF>1~-TU(XGwu|5~DS1{Lf9-8?OV3s@sIuccBOP*vKf>i@a+@$VGIzJD@${J?%^ zbWR$Kh@|3gAi3o+$wOkin1d7AoX>tYxR^ft5(7R*bJfR)v>mbg6-;nitLx>KfB0b0 z^R~_tVhPem2#B0P>L0Ca+st1MG&OmIKG0GA=mB{yop&crMUe&u{f>E@M9R(+e8Ni% z*kG=uijDODHo=eQsQfCP4ijs#+ve{s^Ck58tsW-rT2IDABK( zeZdFd?BB}%F6P((0YEmP3v&Vnlj%yt>UUG<0=6c-yY4qn()-Z5_dBePVW5rSoXDv6 zv8I!H;5&?F&m}_q9}C63GW9WD8U(lJ|8ioI7FNCX;8Vp}8QfcR?|g8Q>Enk2oF z%&lWU`bbvMjQq9e!|U7LrSj=juRk{#iT|GsM%2i~OxoVX%-+Sy^;6eO^>gme-r_S3 zb~O5Iyma_Si+Yi&yu<7#aChR<4D%Ji3O83tM<(wnUtt6^PYoRjhFS$ys_g$z_7+fi zC0Q3J1h?Ss?(QDk-3jjQuEE{i-Q6L$JA~kF!GaT9-`9W7yzXXt`pv7g?&7i*wd+#% zRNYfm=j`pVNwQiy*i_M^bg6a^-)2XN1Tm228%TlQ(5#}Y2#Ex7J~7qh&TQN9^zalC z1H^Vo0E6t>kUAp;eRo}NlV8|xjI4spihPIp{qy&vUN)h8%} zz?D7T5Tc;y#e*q4HO2E?Jtj9&@8CVOJCW6!pyTmRco8Kv0Xe@6$Aa0@irX*O@&*?;0Xf=JVLq>VInqATRQrg0KFw6m) zYg7;|g=VSrv)PxGi8one{g1!M%v@sL?hdjIV?Y@vbPGfEogW)9_IE1kkDEfOO9HE> zYwdcQW>QETgH6=aL}R#kOEDiOF+E%)Fg#=%8_Y}-im<;Z@9{>u{=gWSNna4S1xp!i zAp$Z{_|iqq(#N5J$R*J%UzJ5r*LjUrR#bPJU>Hs&SnMxaTLXxHH(F*_2V~o8hA|nc zp3>%Gs8VfFxr5*6ZDUmI(nJcX0m( zYBNX@GlF#qx-^JPA^N33M@fAMI*Z(nd!S}V)@;#^^kg&FUafSD$R=LIXP^A9zF-U( zH$4Wx4}3%f0^fE3yj8TPNFT;nA0(Zw3*4 zrB&9mN&Yb5^O_1&=JFLH13`qCvwlv+Q_`9U>}z+ZaViQ51E_P&%67bG!@m8FJg-oA z(H`d$B-%*g$70WK@hf+v7$rs^YtUhvm zHNWOcwjm+ukW6e!ptxSP#z>z}0xX0Yz%+@Algwn)EqKbBhT=UeQ#cuNu`WYx%-Bnl zt29^>_UO?mZfPJheZdvvf?K5wkq2;ys>AL{1du4}apz}9PKeB>gLKFs8-Lt6Bk{L$ z6_P1=jn$8sIE!1$aC+3U=C6J{O}hRGCFHD#Mp>QK-1+@Uwp=uSp5GOs!tv3$z4&y3 z{EkQOEa__=H|_`ig#*(ZW0Wi69Q?y&zvXY_2!~9&feRWFNHTC%-zzibWhC+w#U@hI zPn2l0y1fm)%pjF&8K(9JAIvA3Rgav1vQg+`Gs4PJC1TCRjP9AgS>CotwJrypkL;^-V)FCwm@eg^K46Nze^kOIrx>Xm8;V1!@~5 zjePDRBu#2!$$GR&S@dX{ss-0edeZ{El>0Y0=SODhhkB;oX$+_ui6vV77$DHsXMPfE zpR*zx19U6vU42UUQy!XKeNK4v%ToprR+MHPX5+y|OJ~`bF`8_&k6Do)wI~fqtGDKL z{2q{jPaA2Ru{ZfTn&gIx)Cmg^tC&`5m5aL?rH34}hzcMS{Dx+q5~oU3J{zXzfQ~<( z?vtESZ-7w3vlkP#kfY<$ZR{|F~eYQaL!%@WRn^)=9Suhl8TN zY)-M#liNT`Tnt;$%w(1( zg}2^JS8f-j6fSZtO&|A5Gw6M zYKO*RxVR%@k##Du;j)qW1$B2tW+d5e%ZiNjk+~9>xOq3Pbf*7D8PDDd&M9 z{!%^(kHTc$I_nSki$=X~yO&{Vq0%Nb4HI))Tv@YL8z`rpSTGZ5f&_?C*bE^|NvfX3 zwMCad0|fcQ`mPfyF!t6C%~Ym3r?Se{+nAksT#IeQYvRYvw7-mxkF^GUjR#v(Fh8Jr zTnQ4)2a?$yLPQB1#DMN6M^NVv&PPNE$q*$7$`C_<;SDb$IjIQ4L_m1M7!}bdpV_h~lgB{l{?ze1J5!l0w-9X3U zGyVmIb>DbJScwTXf=NEc-JS0U+GF7EKz<#3I)kF(Jx)UwuESdYv3k?^F;{QYK(j_* z;Le43=8!W~vmPBsWDrleZqHsB`lL4#S-mw|pYQ2VnS7rKVF!7K3tGhMCss1ANZ0nU zwoV>GTsCu8lS_IU<>BWi2ILHb;)FaX5dqz}t>FN2dc{E6-B)bGb_nMLt(z~EV^Bs= zzW8EIrp^ij$lM_t>IEE&+E%bQl0vl{xQV1~0Zg(GqH?nwQ-%$wjU2jL*jfnIR(K+l z+rFvcKjtjLmwaD+YVNR18KQj~A*&|TsN58f?N z`sBJk#VpbL3`tzVbfI_ekY8p*s6phlB-CGkhdUCw=pot+$OIls^wlm-E)yp{;YHQ{ zvOn$l)r#42pH>%Ie~Pjoe#jk!1actbgIwzI}$(lrU6Co)9xQL(kItc^-ug$3N+ zN)toZeqHnQ(ill$2%O4%yV~Y1LUIV#M`5&emYxdJwM}HOB1(RpS}(zpFc=NJ*nq0z z)Jzl-ea6fF%bWXhv}Ne7YPtg2fMEJL#9LbfE;mTtdt!+AFU!-vZNJkH0I@(B28pvLecY{H*DArFRNkf%@R`Pa}@rm?Qm zZlL8~M%iA^0(N482GD(g_!BSJnkRszhLXunIa>~%rwmsBVQVko3=ycfP$*6$3exc` zRdX3!im3{wq@+o^sZqOV0sB^-$;3OUh8P~(qW?EyPRz80IZ54jFgA+9}W-3;&y@QUu8Qnb3`fPU#*+ymcX zqURlh7>E(hjLDVwT-mLb4{!7;te)HK;$drFN%uKLHbuLbg&+i%WY4j#~h|Vxt1INLW8So(L_McXXgO7AHCm2>eK`_a_wgl+^ zMCpgZ%Bo%K$Nm1|XS-Sqtu%Gh!SHo6Jgb}iE*?>$2Eadh8obE?;t(Mgun@J&I3 zf$2cf`-~vn#gk`p^&#{;hvUtgRhBktk9~HNoIsR(L^wB@LWC_5V)}=fBL}Ro}t*KOD{~mH*p@^f^;qsG_zZ znn3sJWi+zt(UXit*ZmSoD9e(j;lFv-%tifK%7%L;XNUeG0-ptuHU76ChapF)-ndDW zFkO!`&V#mTM~~^Y(`nsJUmywt)?khymcv#;wOuS;0Qp$#Z0vAhI3*kvG?fXe3Ckmf86&t4znPfK40DOkk2q9Y>{k6doM4N=0G z@nYkzu9$cx0o%P-$f)4PlhsOfP?$?rE#<*(LlrXNu!$#FwyLcRMduKx8gxQGN24uQ z7RKn%yEK>g==N^l#+e2*6S$)VT7!D1m^;%BwG(Jxn=N9=*Fa$V<(sd=yZ3|0TCjrZ zsiiCGSS~XOCq#tM){+X7mllexaghdMP}^4`=vsGnjc;f3n_p7T-N=7L`KdOq=9^Sz zTn#8{gU%`{i+zy5HD#$Tl!;Mf^tgGDpSUTzGH(1$W2UlkUJxtqD;ghak ztEOJQZkWo2dC(iD0DmK^=CEd(%5VG`lk9EJO{J3Ii$0Ir3Uk8-iV^(6nKu$i<`Di9r@K zFQ!;FXBGi`FBD|75XU1tFz*`bYRQEMc1qG@Y5 zVvZ@gH(q(_QzV1JO`P#2f_umu-yH4HD69&ecgz5v!RM|D@9Pa!3yXL^8N#t*Zl?&b zuOhm4TvaN8LwIH4$VPM2Tmdjfj>@8$ulxr|2)I^wizpB1V}|JnjP(s9Ok!xGhqiwm z3e4s^PrZPlPz4wY?ElN!>-VAXev2UK--BRbMu82ZX3R^#ehfO2=@UXY`W^~>E;c`Y4<6|DZq~W?QzYtE)dOD zkUxtF%5{VozKQV!Wh_HYZYUUL1XD5!$sk{tF(&ngSK*=ZNLEZPq3N&Y8L!|%JT+%b z;-scI%&^MR8Mf@$o@?HQCmMyAelx#@(; ztyb4)HG&W91!+`qTB_%@4L5f*Cz)9L*kC<%1Kq7#@mw8KI4RiM7FHB;)gGuJKgjW7 zxKT?n4Jd?ciIyc1750xn;*Tz0nVGNst; zRbA|!Qy@zaJb;pCFgVf_mU_|3OMd(o5$o6n;h7UNgVJi7b8=(Pg~3WRmp*$vT9r8aMf`?_kijY9*qyhS?hiFHQmAhqx4k zWTMe7LXER#MdLvO*OUhM5~2F3*}Q_IUHXAPl!1CEYy`E0EEEo({YH=)>83LYe87)r zxkYx6J*Eh4r(H@H3Ykd;yIL6NvOaNkg)YQ!Ao>n7Jo!=HHlR9F>U}JLK0>o;VbU1F zjSoBkSsMg>ke%s0iz6{^rf7fCccC^S)F~`6otj~ndP6RZuHi7?f=ov2))KFmw4|wo zKi0{q1G0-V{{Vj(dO}3+H!WmcHQOq1OfpXs^}*d(f=<4Y#2k7ql*Zcu+AZ?r-KfZh zx!NxU#JCmzCvVo@pHBUk&4?sL?caE_cpEetj>v{c=Eb|M=1>YkD|R9ZA=%_LAvMJ> z^K280mSmSE#!d?F(VscJsjhng@%%{VRv!e222OY~xm~AuQ#{Ys_@BE$>>}m(n3gWK z4f=&9`^kiE8W9b3_L%3NJB9m;|k zUY9SQ0b_4C<$S0gLHJfUt#9bsb*-epuUg281#OJc#j*nO8Ulf+rvHsmv%I#g)_@UZ zA6u@t+-Se15m7})tPc_%;M**jPb~6TtjKV%hrr&X)Rrlb;~iz+Q=KZ7GiQQu>jO)T zc$6~Z(04%xf1fKFKl^lTHu55(Ww4aa4=rSkH(E7=?4sXIgTsy7_H%}ofFz=>@eY1U z7aHe>V*JeuS`7tVB-BM6Y-=N1qEh9Sb9jZiRGq~y(s3_lM1E2yvYiw6%b%$XXmSND zZYjx~au4{Wyc8*UzYyIQhoSYu?6MGw)`@S=2L)%H^LZG=HL5;&!u7@O3TB(wp+0q+qbWt(23#?l3&o1 zdu)^dCgS(B6leE^YS)++mSC*+R?77Tl(TwZdpiYkMz<*piGX(~65AxVH>ir2dH4 zw!4eGy*tK=6W}CKV6qad6P!YA&$_h0&g zCdw1q=PKJc`EAprZSd~;!o5J>Qzd_uE_ZPLB(0ds0}nCsyIg7>zItBRcMgg1Fv{7q z_%0m}M{gtR_@vy1VGhB*RIX3oQ~7{aQ_5bLXeG`QUI~kH6G&tAC17KHS!DYOs(}@e zjZ^1@34@$gL>r_jto3H@gN^8%L!;?2UV)u|L7MBk#OKV|L!MFxN7H|u(mGM_5p?*8 zpe~)nbB)n5x(n`2l^E7SW%GS-1PVAo7BQ9SW8Qg|6FTuxNvtBHqN)?$g0xP-R|!8W zX&HQhW&VulO{VowAzAQzgAPsvRCi8b!b?(yFr9%LzR{&q_LdS=}sc%(-pEdt>W z`Q(=fEI0z`M?D~qeEY%h z%M|A(CwGf(SLYj~9%2R8W87@sxR8*JkU~hf*j4JH-k4=P43;Do8fN@)vtyNSeN?d7f@_Ht)J~b(8)&nLa!yS6wtuvge+wlA38{lW$mYA|j@a zO+xlW(qgSL%%aKdybn}^ZVJuuMw?)*9mztFA9?sma6BLS32e*p!iOrzcUospllr(l zLsW@rTs^N;;G|$fFLy+P zQ@)8@UQ9V)`f<6HE-w);J%yLot%V^850q`D3`0W2E1`#Q`w+krMzhG!{}j8+CFunu z#e<5d86DvQDRGKsBSz9<7s4X@Bbgz%J&`%We2rL!6b>beg>6|4gNEt=`D#6a_F9udtCDAgC| zxg}dx+7r~enD`(xecQC#)^=YIuAe!c0jYMi&p)76BQn}mY1YB-7|<@aq;nBqU(~ zohC}+GxO*aO3n#t4h>#jd?BywPK$lU9vPFDVt=@~qbQuKhD}{y!W+zA%_n zRyKgcE&l(-tW<0)|KVt>Q$X`bTscPqxp5f~6#Q9Zu8N*PgS#zBahO zJ)Lp`xv!}r^tbwdly>??MLto;ptM6!qld+;pcS=)6`*z7S|Y|cjNm)4UVl~{1{Cnv z)9mcJyt7xYW0IxkA8 zwU&O6-Yg(?*+-bHe^1dctyH;7E^gG@C}SHZAct>iCHqb1GR-;oqF$+R=c~w=MNwl} zd(1;|Q3N_Cm`#=ABFYm1#%*>w$@d=Qr?%6MMtmFhV#7C5Qy9`r(BcDE%&)FFDJfb7 zir=kc=44FSC{C6Vw>|woBNy*OGwWMuv?G_`z!^Fo z;o+>ZdH2{gRB|Pe4CsX0j_c#(R*GYqlH|qX)A`Hw-4N8%a&_ zRT2d`|4<_nrg|zKT|@ES`7}E;wAPldMw1uL4Rgwn;nV(y!pc+Pt9{6OPh9nCKl)fE zl?xpABa#bv{LFH)IUSPS{5K-9A?{p_LL7S$!Bx^G7sM5@#7wV|Qb@F0Wc%BS>O$e9 zB(Cof#Zkt?@I5Zk$~V2k)5?w(DuZ^U-#CM30K|shyQU11F1d;ICrrol z6P_7Fc2a||(B4uTIAm0Gh++aUGBmW{seRw&UXPFpwH6@(0Vz=Z2Wjo!F2a8Iyt6di z^%Ccs-m)gHWV*bp{D2B*5RpbDfd~cFL4?61fCBW?2M8a;!GqH{m=SlPrL-;b7K*?u zEzMcyEsjNj3YMs~MN$+-cFd?Ic-CR2+u}j1O5s$#@P~MM#DRKH6jMuni=T>o7{E?l8wu zw*{w?1xx83{0~A~n!#sP1YEsY&rzNcgl~nRQ%RgU;E)DUJ~RK)*?ACjm9MQn_DhKDok6 zvF6(5V$|ZsGm6kshJ~^>Wt1VhFitFY!Xh3?XyM_9gYlvV@@L}!EbZ+Cvc0URVypPc zVyif6?|K#UzF)0liC?UKNi=9$F%F=8(yM|DIX$eGCqQd3^slQ}-R%``WyFIE{+uG> z(gcz3=SE^N;?n!W*e|t{2&bXHPLIbeYCT7s;rq7ifhB5WH%|vM&N8kG+9GH^Blijh z{D8I4O6zWssRj(RsBzi`Aw?;){=M((#5~y4v^>F@<{o5fHx-g~l|>Y|rl5<8BZYcWt+fh+75CVbu5enxhdg;B zS8uzR^?19KPi)^m@aEX-Xkls><`b9u(!vjYSQTW;I@Cshh1iV%t&abG^Wm;uJfiCQ zKo$_<-rT`ELLBtNtYxI0o+g;5}Z<-WB!e^q9=7I@Z$hA?}Ge1+_0ZljRpD2ub4x14Mz zs7Ucar1@!l0-|Inr6`w7SahQ)8VqQJOGT!OSVFam+PtvKaYH{a>oG$`3y zMAJ%f@crm8;m;>#Ov{-XMY^7I8`aY!oXkuz-73AQipx#2XCxh3$dJxF9p~rK3ahQi?VPCCNpUK2z1|1{~C=jNsdCcTxe&jfy znt}=LFkqw81hQfG1W>h*HB$a0cs!;;7-FeND(S0Zg{h~A^|Pd|JNignb+El_m__!fl2 z+Qw*S$5TPf&5|o`e&)}J&&5L|e%}Qz7H62tuNO0047f6u>LP-m;Vi|uj6G@jQE^pE zs+;gc`@mH?One2m(?J@N*!T*;K~PHjQ0x_vq=|N~EO4bd1Y8rb!UnI-;27$xy7?sR zey1?cV&Oet0hoR>`7Z=2HnkmW~*tApcum_s%BG zL$t$I!c`*aW)eB?1o9`Y8=s}7ufvcbp1 zubAR>eS(8}qlihCh7CeFgkq>KjA$_CO-KS&tOy1&D|HdB#^pLDa6eLYII1|W^%^3fZmmW+cU%|O@fZhQHglOrY=~QiDD-A{L(!joMUy?i{di-Wt%SbW;usj$Zw~C=kWj*P8Pxo1jB;w z?hT2c^q$5xJ#WiHHom=Wt45b`{O9oFWS4o7dKpbGzyj9KlYedl;Jw^q#TsRn!yZUo$%Vf7B9h4YgHnTY9M-UJZk?{K6;Cm;FVxW{htB)QqiR?#>r-XUN-w1j26pdz zXWR&lUJRIwjXnm9MiTP0K6$$`_-~_m#(225n}3IP&ZMr-FtNCpF{e;ZKQ-e!-f$0F zrEn?pi1q;C5(>lCFwQCZSb(9+6YqhNVx;2jR)K5EJ6qCqG$%;-c{`EaDCG05HJ9|! zmk#k(LL^zdEpeGNmIB$M0}GXJ4nECG<7i8C8xyeE3uc7{-a_)H2|3v}KZ*Ur8_Wa9 zor#E^{6w!7W-WDWRI#DGq3aoVrLkf?{9?w$bq^APuNED+7jWRnx{I4CO5WCJ$lzz7 zHnLnwM1O31N8AAK!N!EMe_b!>7Bs`cZ_z#X%D8Yi6b||2oOh0!<b_~5R!$;2kxcsIITT^RU^G~Pi_}lxBBYK07*XZ|rS1TJ z(vpT}U!Vhh2s)6hUe5BLdlX{4$%OYEc$@wFT^ToS-9N>m)nd3`@kFusikCNrb)~j< zLdT88w&;%iN{%2qLgIc!?sw#z+9?7#ZVhQgj@WMlzt-d6@r2ShY>v0w0V`6w!z>@v zPSaBJLldlq?gIUU>qZmf|kw*@C@A4IGmWgF}&U99xR~zeB_**D8O)qcgXP2 zV@u+V$ut~6#_@9o?f>b?&{0QiXUjx~)=?z-|3h@J%bqw7Lzrd0w$w!WT z2q(7WIs4h)CX)9{952RVq53ep(`bL@t?OxNJ?=Xt@zHJ&N(byV@RpI)i$7&mzNfHaRwbVn9q9~{9 zE<`zqXl+D6&&!owK6tN}@_g~?rZ=Zk>0P(*@CYd3Y9UZ-tNe+u|DEbp(FJuOHH~O8 zP@I|6!K2^0?fblEK1@VeL}5jS`nlkxo(Cn768>^za5XbCRXbzDjyWzNRd%)r*lH8T zv~X&;=$rwr>W)M6F=7w+$pGr1FtSabXmLN;(7FjvIISC=+7850IQ}lxb9f@Y9`)4(v? z!S}$knJ+s0`b!vwKe=w7nD5Hw1s2Sz_b&9rDb1adpk*0p`S|~GknJ1S*X-i1bxzzh zbRz_ob>t{u=%;YR53Z<$mz0LXe=-|-W#M5$GJ!O02#*COIx7f$Y6xA5!0R{+jg?%n zv9oCq%qC7%(cO@D?^ro4zeRC_UJFT`1IyN6-3T{w(TNp8HaXDix5hK+c|sj#5c?*7 z)Pp#rLiVjxQ(swxo$lo4OKBy2dC5h`r|$d11PS3D%##ZDa7#>5Y`34-m|&8dlRTFa zkt7FNGW&f}!t&_bUqOc@4u&XDeg(qM^feW_rG5SiHH~~z*4`LM@@QkiM{#|_=&I9O zaV>pSnU#i|sbI>BdZrV8gXK2aa}2(rNA0vaOuzYa=-3!78~1Uffqfbw`}Kb7vgTVAvYk_m!c|woPx# z;oQ(i_jORr9?CTjnmTc5F|NcIKQOL49@)mXdXpzuN;}*KoLFpKq9SoplDj4xt7@Hu zRnp89#SH~T6<5T&Da5`|9Sgj^u|!>!njWVgYqFZ1zlF%R>WNfq;fEqjl>d-TWr4si zs`y(iStaPun&V&W9HQ<_BN=N@VIK|8c_SC8vn2+9Hbs6yAa@8u@yQpav^PLAG=-ZX z>S| z)1UD@yv2xpBl*QmOs7BQhfD|cIRasV_#;8`u60mEYuZw^0e6Zge{{D#4))p$Uq=8w zQ#8LIqL1)bturpfbBk!!xuS@Tt95VQfeRWzl$T_CRnUzJ(n@5P9QH_`!hl&F%Uw2$$5xrg|YA zAosxu7#3bR#C%EMK#k#&!LD5T*(U<44bA!HHPYV27@tg5jX)6p z>Ciag6<4-9GJlimunzNDg>_>XX=7Ka%pR9-uC6Y0MY(qB8S+h5?uk=&&7~6Y738hV z-j?(=g1k!JhSDc$(<~yHf$z3x(NvW4ZM@QGrJ&{^ddk^m=f{PkTtLePkwez+_qS-5+mGxLRRa|BEPyr-P zFB_TBc1Tu^Di@A;CFSM@}5c4wSMEw4G-a+7F*HY$+#?UTn zn)I$BNL75_P*bFGgjn(6b4!N4sVNAuo);3_Bcz!e2{yvyfVOypHm z7h7+0Q%0}IwAdq=vu|+;Sr5CF+~Wu?#kPDByvr6h&~{U1Cx=6_8;oakt=iN27Cwg* zF1!%!=a>7+oQ|oq^DAQ4&$Xm|qY3Fh=*<=x`26KNg^tz7UoE;Q3r-AA4jN(_&h>oZ z22V}8Lo%~YYMe7#qhD?^@rPf*Z`td+!;brxHz$1PpFXc~wkEw;7j|d89Ei7QcHDoq zJ$rkXwcbE;2J-^gA~pnUc9H$(Hu3+RH5mOXIsG@zz<(Vvs~zj&sA2k;&`;D$L(0?n zksXok)ze6QBUu5WO!_tu2n0}XBAGu7%%Vx4<2G_d6S9=~T%~#LDpR#s?iQ9l2P%1a zE92{P_qqEfN8a}VEXUErWyv@MynCYKVB(4Iz&q#8!R5{U{Ina0Ba~lc#vcqdCz9w( zkOhgo%Af&?zUgJA8&A!Sl7ccfH~rk!Y^!Pj`enRZN97JP6(6<;E?WLln3}}}r9crpBED>xpqWg3=UtWLP&^z{^p_ahC7Rw7tz3 z#oRE2>Atgt5NCPdD7rDSGNsz}d?C?aJl4O*%?BZwo5^TOi$Mury3lHIaJ{Ydl|jtQ zW-e(fG7UiI*JW-Ab5dSlvd|cU(l{W6BD*Xq+nve?-abtU8Kq7ssYMbo-zONfJcx*IkSvFubJA6=28~V^^CZY%cW9YEg#0diCV% zB%99)q36QH)1m5?l3G)EBl{y`VQyPy@ZbXxs+iYx%*G~fTrzG#Gv6;7OL@V%RF!Ap zLAk7CMTWzaN^60LKvAoTCHSaIn{FI)HRxn(SW~5fWXh{8U2LCZ6?b$E=fDnenci&r zC1_1**l5%V=`n;fwaI5F=9H3T2OW|PdY+sQ`%7EG3U*GbXk9vL(?1^!W>^QQS-&1B ztyi9*?Q4|aN+3@LH$;exFStpl#Hgo5G7@W`FK{!fdQ7M@FzFz(KT%VQ-}@}(`+B}i zU&FsVljVocSa(nUoDKH&n!PZmSdc%uKdM|>Bl?2tK}Cu32L@nwz3~6lnf@r! zM}L2~(GB$)W5;TGg*JU$iXqN-c+JXXj_SZX1f?YHw-0>}(q|4QcEODFRp7e>FaLP- z;w4G>YHuC4>P84<|CjasMtO#liCo^ zY0hJ5iYOr{NgbclRCT*cfpb#4DVupU+s_a1gH9%D-amPx3;7@vEJaD2_(gTPVZv{t z4%{>Q;zxhqApxmZh!A58q|*9?j@KV@FJ=@U+Rq`{p|BIPWgq+snVqN$;{O3>80wQG zK3TZGQX*?tR+fTf31tg$qila}I3wyV71L1e8L?5sD^Y@xe^#_h=M1fyN^ zN8)cDSm_n7k;zAT{;;LgORSu@NCr_T{eqE@m$Z!=i46W9hZ}{04>{&{xo{8yrYB8f z&#BI`w1u!6F1FmvMn>m8iC@q-+Nq1%eC+eo5n@@c^~Cfnj)(Kyt6p)a=y z;Q~%c9@P;65}#?~e@buO&}@*wDoe7Y1FtK_;bdt3vc3gJ&pr7=Em0G@Z9}elWz+~= z14WFybXGKEz%T#YQ0LOs^USHgr>K4ho!dOc9!XxqEgs( z_T?66y$W0I6}Nri8{_&n%=n^B;&M+gZC{!2K4{5BY@-Rv+iHOar1k71n_-+DBy`*% z3r;9uF^ED-L<-lLL9!ny<8BMa^>R!wfg--vXT{PI>_OUYDnQ^5mEC{i-WXlSDj-;=LKdg zesdllPgSy-wnyTZbJf{Wag0hCkI44)osR$e#Q^-p!%qR#tP-7 z_rOGa?0RZn0!uwbd8#s&=!f@ zROV>B9%OFObFdYv=r{!myU8WFC3b95T(L&Olx@D3QZ@|i%Ab-uRbuH@;Y#{)phjJ` zaE=m?B!u8SP@S@Bwe4`4X(=rag=GO6D=4s8PTFiTHVg?gm-pYFpzrD^h=C^6tk3po zSI2E@X|qiiTsFFK66$Aa!$Yu47%Fo4rOEdnH2bfG*MA5UOO?fZnw@T@n!mvKg@s0v zH}i&lPMMf=BcnqIzbY3Kd=^RV^5Hz$yl8t&frec-C^xY(`g@NiII2%VS4E$8`Fy9f zR-P|~6h8)>^jGn7IxdlKQ5>hE4x04xMjsVcfR}gp5_brRET2MsL{1uVyyH|Kbp5Fe zlxM}bX-9@hub=KgT5$|c1J!2-Z9~uVPZ7eJGQY%SNP)xqiOgU3 z+ifY+PuCOD=v*DDn?sUkfuHg{@=A9{wNC`RjKW++>4ZPR%6{a{N|+3izHZdT2IAw` z_=kls__3-{xFmH!7-TC7Lobqy3;?eXxy@RPVK50-PM4e<1iLw~`&;tCeeERN`4y{5 zXIG%zOE%aEWKAfy)t5Yo%_H)F)X z*237(>3^X^&We|k>-&TfGz|tS?8PtNpMTN=nvUVTORNw{olk;sC&Zo1XdMCz0`(@T zMn?CW4DK#UIpdP>F3s6dCg1s&0BjCvG(kmvO6v57Q2( zVh%|crSI2B6Ok9dqmeG7gQ9V$LUhAQ_d5A+7DBlwh(dV$Rss!tCFi4Vq0n)wtCqr@ zu1t<~sHE;%=W(Gon~LGoRW>fLR6B7a3)ajT@ECnZEaCckeLqIoaRg+!LTJ`)aws#H zp7CR0%3tdjPi3T8Cq_=4@&;s22tk7>H6T0U!W5&G02f3cdqIseYQ=0{YyPwcr}Y+^ z)jgE_ke)3v9(HK)Aw5lm8mjccmAvfcofJ3pGzaf*@AMfk_i_H`JAJRa_opS)J8IIb z_;JbpPbk6DOBL2l%?lRuB5SOI$npb0=&@+%iuCeFKIwR~aU{rOvw|CvYW^_zJt0Ws z<_Kj10~(pkzoy?NGut|RJGy{-fUQyp;G>AFQ1UbaCqG!B=86#bj`5I9Lm90+#(ruZ z9~RGDF~!@EUPlb~%X5~5OPksYYato_oXkOQ;Y2!_jTrumT>LZ4u!6M0RH z5EESc?CTu1ScFR(yAn}2@&{IIV*_Yg@6lGV+?j=^7$;Gg5RYcgSbz8C`eq+>PYOy$ zJ83<3W4c;UDODP{du4UE(fsh6?nDz|Fy&kzkq?Dpxi|yz!)hpgyTFpx)n-2RRYUkJ zoC2p7ZdFY)wQyClj{Ro06L6+;Y56t?9M8k7Wvkk`bfSJJbMf7dwGf;)TMFYJ!lv?f z>ao(Okdqvr=s#tvm_kWX?Hks8G)AR%3>c$k?1G*LJtMIz?z(RL!q%OaM(;!mHc6Au zU1kRONtdq)UCw8DqWSiYT^9bWUk#w21O!+L|DU@0zxezC0U!U&<-hly!5@fLjA+b1NfS2V+BHb33O$s{%;TQcX=v|Dv9hk)*9>ondDA#{2;gkpcl}`P7z# z2B`VlW64Vae?a-|?oa3dEBoDMjsUu1pKiY;Q9^rk3tE! z{eP>;2*^r^iYO`5$%wv3_^rmj8wLa|{;6aE?thah_@^2G{-HmW-hb8jm$1P;Ww3A6od` zUwaSd?kAm}2Y?v^T)&ZI|526!=Kc?Gfaf)JFm`m52B^Io+x%OA;ypa2M`3>lpew^* zf6s;Z1AY|qZ{YzH+*Zzx04^C(b1P#3Lqk9dGWs_9rvI&htlLpg4?u?p13LUSMZiDG z0>R%lAm*SCP)}6>Fjb1%S{qB-+FCl>{e9PvZ4aY80Bo)U&=G(bvOkp!fUW#Z*ZdBx z1~5E;QtNNF_xHGuI~e=r0JK%WMf4|BAfPq6zr~gKx7GbU9``Cak1xQw*b(024blHS zo{giEzLnK~v*BOHH&%3jX~l>d2#DY>&ldzp@%x+q8^8ec8{XeP-9eLe z{$J28rT!L8+Sc^HzU@GBexQ25pjQQWVH|$}%aZ+DFnNG>i-4n}v9$p}F_%Qz)==L{ z7+|mt<_6Ax@Vvh_+V^tze>7Ai|Nq^}-*>}%o!>t&fzO6ZBt23g4r?*WLL8)z|!gQsH?I_!|Jg%KoqXrnK`% z*#H3k$!LFz{d`~fz3$E*mEkP@qw>F{PyV|*_#XbfmdYRSsaF3L{(o6Yyl?2e;=vyc zeYXFPhW_;Y|3&}cJ^Xv>{y*R^9sUXaowxiR_B~_$AFv8e{{;KzZHV`n?^%ogz|8ab zC(PdyGydDm_?{p5|Ec8cRTBuJD7=ktkw-{nV;#0k5o;S?!9D>&LLkM0AP6Feg`f{0 zDQpB`k<`JrvB<<-J;OKd%+1!z`DQP}{M_XnsTQvW)#kKd4xjO+0(FK~P*t8f?34gT zNeb{dG5{jMk|Z%xPNd?)Kr$uFk;z0bG4oFYGnNlV6q8Vd`WhQhkz5p#m^vZSc48n^ z)8XlE1_e=c^$WG1no(|j8Tc`PgwP}{$Z2MV1V$=SXvP)gXKtqW)?5PUcJu&?e*#h! zqs>gH(jDQk$9cz8;-w$cc*dE1}qLepfsBCXA@(bAJ66ft0aCq$Wrcq)WXX{0nm+#w=uBj1o9rLyA i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/jib-gradle-plugin/gradlew.bat b/jib-gradle-plugin/gradlew.bat deleted file mode 100644 index f9553162f1..0000000000 --- a/jib-gradle-plugin/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh index 024587e193..da14f408aa 100755 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ b/jib-gradle-plugin/scripts/prepare_release.sh @@ -16,7 +16,7 @@ Die() { } DieUsage() { - Die "Usage: ./scripts/prepare_release.sh []" + Die "Usage: ./jib-gradle-plugin/scripts/prepare_release.sh []" } # Usage: CheckVersion @@ -40,14 +40,14 @@ if [[ $(git status -uno --porcelain) ]]; then fi # Runs integration tests. -./gradlew integrationTest --info --stacktrace +./gradlew jib-gradle-plugin:integrationTest --info --stacktrace # Checks out a new branch for this version release (eg. 1.5.7). BRANCH=gradle_release_v${VERSION} git checkout -b ${BRANCH} # Changes the version for release and creates the commits/tags. -echo | ./gradlew release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} +echo | ./gradlew jib-gradle-plugin:release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} # Pushes the release branch and tag to Github. git push origin ${BRANCH} @@ -57,4 +57,4 @@ git push origin v${VERSION}-gradle EchoGreen 'File a PR for the new release branch:' echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} -EchoGreen "Once approved and merged, checkout the 'v${VERSION}-gradle' tag and run './gradlew publishPlugins'." +EchoGreen "Once approved and merged, checkout the 'v${VERSION}-gradle' tag and run './gradlew jib-gradle-plugin:publishPlugins'." diff --git a/jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar b/jib-maven-plugin/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index f775b1c04cf89b25c7814d3a8a7c810301092e57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48336 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 + cp.entries.each { + if (it.path == "src/test/resources") { + it.excludes += "maven/projects/" + } + } +} +/* ECLIPSE */ diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties new file mode 100644 index 0000000000..ced16a7bbb --- /dev/null +++ b/jib-maven-plugin/gradle.properties @@ -0,0 +1 @@ +version = 1.5.1-SNAPSHOT diff --git a/jib-maven-plugin/kokoro/release_build.sh b/jib-maven-plugin/kokoro/release_build.sh index 6fa083b173..393547a807 100644 --- a/jib-maven-plugin/kokoro/release_build.sh +++ b/jib-maven-plugin/kokoro/release_build.sh @@ -5,10 +5,5 @@ set -e # Display commands to stderr. set -x -cd github/jib/jib-maven-plugin -./mvnw -Prelease -B -U verify - -# copy pom with the name expected in the Maven repository -ARTIFACT_ID=$(mvn -B help:evaluate -Dexpression=project.artifactId 2>/dev/null | grep -v "^\[") -PROJECT_VERSION=$(mvn -B help:evaluate -Dexpression=project.version 2>/dev/null| grep -v "^\[") -cp pom.xml target/${ARTIFACT_ID}-${PROJECT_VERSION}.pom +cd github/jib +./gradlew :jib-maven-plugin:prepareRelease diff --git a/jib-maven-plugin/mvnw b/jib-maven-plugin/mvnw deleted file mode 100755 index e96ccd5fbb..0000000000 --- a/jib-maven-plugin/mvnw +++ /dev/null @@ -1,227 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/jib-maven-plugin/mvnw.cmd b/jib-maven-plugin/mvnw.cmd deleted file mode 100755 index 6a6eec39ba..0000000000 --- a/jib-maven-plugin/mvnw.cmd +++ /dev/null @@ -1,145 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml deleted file mode 100644 index 0e0e8ab1f2..0000000000 --- a/jib-maven-plugin/pom.xml +++ /dev/null @@ -1,504 +0,0 @@ - - - 4.0.0 - - com.google.cloud.tools - jib-maven-plugin - 1.5.2-SNAPSHOT - maven-plugin - - Jib - A tool for building container images for your Java applications. - https://github.com/GoogleContainerTools/jib - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - coollog - Qingyang Chen - qingyangc@google.com - - - loosebazooka - Appu Goundan - appu@google.com - - - - - scm:git:https://github.com/GoogleContainerTools/jib.git - - scm:git:https://github.com/GoogleContainerTools/jib.git - - https://github.com/GoogleContainerTools/jib - - - - UTF-8 - UTF-8 - - 8 - 8 - - - - - - com.google.http-client - google-http-client - 1.31.0 - compile - - - com.google.http-client - google-http-client-apache-v2 - 1.31.0 - compile - - - org.apache.httpcomponents - httpclient - 4.5.6 - compile - - - com.google.auth - google-auth-library-oauth2-http - 0.16.2 - - - com.google.guava - guava - 28.0-jre - compile - - - org.apache.commons - commons-compress - 1.18 - compile - - - com.fasterxml.jackson.core - jackson-databind - 2.9.9.2 - compile - - - org.ow2.asm - asm - 7.0 - compile - - - - - org.apache.maven - maven-plugin-api - 3.5.2 - - - - - org.apache.maven.plugin-tools - maven-plugin-annotations - 3.5 - provided - - - - org.apache.maven - maven-core - 3.5.2 - - - org.apache.maven.shared - maven-verifier - 1.6 - - - - junit - junit - 4.12 - test - - - org.mockito - mockito-core - 2.23.4 - test - - - org.slf4j - slf4j-api - 1.7.25 - test - - - com.github.stefanbirkner - system-rules - 1.19.0 - test - - - - org.apache.maven.plugin-testing - maven-plugin-testing-harness - 3.3.0 - test - - - org.apache.maven - maven-compat - 3.5.4 - test - - - - - - - - integrationTest - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.22.1 - - ${project.build.directory}/classes - - **/*IntegrationTest.java - - - <_JIB_DISABLE_USER_AGENT>true - - false - - - - integration-test - - integration-test - verify - - - - - - - - - - - release - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.0 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar - - - - - - - - - - - jdk8 - - 1.8 - - - 9+181-r4173-1 - - - - - org.apache.maven.plugins - maven-compiler-plugin - - true - - -J-Xbootclasspath/p:${settings.localRepository}/com/google/errorprone/javac/${javac.version}/javac-${javac.version}.jar - - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - - add-jib-core-sources - generate-sources - - add-source - - - - ${basedir}/../jib-core/src/main/java - ${basedir}/../jib-plugins-common/src/main/java - - - - - add-jib-core-resources - generate-sources - - add-resource - - - - - ${basedir}/../jib-core/src/main/resources - - - ${basedir}/../jib-plugins-common/src/main/resources - - - - - - add-jib-core-test-sources - generate-test-sources - - add-test-source - - - - ${basedir}/../jib-core/src/test/java - ${basedir}/../jib-plugins-common/src/test/java - - - - - add-jib-core-test-resources - generate-test-sources - - add-test-resource - - - - - ${basedir}/../jib-core/src/test/resources - - - ${basedir}/../jib-plugins-common/src/test/resources - - - - - - add-jib-core-integration-test-sources - generate-test-sources - - add-test-source - - - - ${basedir}/../jib-core/src/integration-test/java - - - - - add-jib-core-integration-test-resources - generate-test-sources - - add-test-resource - - - - - ${basedir}/../jib-core/src/integration-test/resources - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.0 - - - compile-with-nullaway - compile - - compile - - - - ${maven.compiler.source} - ${maven.compiler.target} - true - true - - - com.google.errorprone - error_prone_core - 2.3.2 - - - com.uber.nullaway - nullaway - 0.6.4 - - - - -XDcompilePolicy=simple - -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:ExcludedFieldAnnotations=org.apache.maven.plugins.annotations.Component -XepOpt:NullAway:AnnotatedPackages=com.google.cloud.tools - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.1 - - - **/*IntegrationTest.java - - false - - - - - org.apache.maven.plugins - maven-plugin-plugin - 3.6.0 - - - - - com.coveo - fmt-maven-plugin - 2.6.0 - - - com.google.googlejavaformat - google-java-format - 1.6 - - - - - validate - - check - - - - - - - - maven-checkstyle-plugin - 3.0.0 - - - com.puppycrawl.tools - checkstyle - 8.18 - - - - true - true - warning - - {project.build.sourceDirectory} - - - - - google-checks - validate - - check - - - config/google-checks-no-indent.xml - - - - header-check - validate - - check - - - config/checkstyle-header.xml - config/header-java.txt - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.0 - - - - - true - true - - - - - - - - diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index 23f0133299..a82daa259e 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -1,35 +1,27 @@ #!/bin/bash - -# Usage: ./scripts/prepare_release.sh [] +# Usage: ./jib-maven-plugin/scripts/prepare_release.sh [] set -e EchoRed() { - echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" } EchoGreen() { - echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" } Die() { - EchoRed "$1" - exit 1 + EchoRed "$1" + exit 1 } DieUsage() { - Die "Usage: ./scripts/prepare_release.sh []" + Die "Usage: ./jib-maven-plugin/scripts/prepare_release.sh []" } # Usage: CheckVersion CheckVersion() { - [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." -} - -# Usage: IncrementVersion -IncrementVersion() { - local version=$1 - local minorVersion=$(echo $version | sed 's/[0-9][0-9]*\.[0-9][0-9]*\.\([0-9][0-9]\)*/\1/') - local nextMinorVersion=$((minorVersion+1)) - echo $version | sed "s/\([0-9][0-9]*\.[0-9][0-9]*\)\.[0-9][0-9]*/\1.$nextMinorVersion/" + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." } [ $# -ne 1 ] && [ $# -ne 2 ] && DieUsage @@ -38,48 +30,28 @@ EchoGreen '===== RELEASE SETUP SCRIPT =====' VERSION=$1 CheckVersion ${VERSION} - if [ -n "$2" ]; then - NEXT_VERSION=$2 - CheckVersion ${NEXT_VERSION} -else - NEXT_VERSION=$(IncrementVersion $VERSION) - CheckVersion ${NEXT_VERSION} + POST_RELEASE_VERSION=$2 + CheckVersion ${POST_RELEASE_VERSION} fi if [[ $(git status -uno --porcelain) ]]; then - Die 'There are uncommitted changes.' + Die 'There are uncommitted changes.' fi # Runs integration tests. -./mvnw -e -PintegrationTest verify +./gradlew jib-maven-plugin:integrationTest --info --stacktrace # Checks out a new branch for this version release (eg. 1.5.7). BRANCH=maven_release_v${VERSION} git checkout -b ${BRANCH} -# Updates the pom.xml with the version to release. -mvn versions:set versions:commit -DnewVersion=${VERSION} - -# Tags a new commit for this release. -TAG=v${VERSION}-maven -git commit -am "preparing release ${VERSION}" -git tag ${TAG} - -# Updates the pom.xml with the next snapshot version. -# For example, when releasing 1.5.7, the next snapshot version would be 1.5.8-SNAPSHOT. -NEXT_SNAPSHOT=${NEXT_VERSION} -if [[ "${NEXT_SNAPSHOT}" != *-SNAPSHOT ]]; then - NEXT_SNAPSHOT=${NEXT_SNAPSHOT}-SNAPSHOT -fi -mvn versions:set versions:commit -DnewVersion=${NEXT_SNAPSHOT} - -# Commits this next snapshot version. -git commit -am "${NEXT_SNAPSHOT}" +# Changes the version for release and creates the commits/tags. +echo | ./gradlew jib-maven-plugin:release -Prelease.releaseVersion=${VERSION} ${POST_RELEASE_VERSION:+"-Prelease.newVersion=${POST_RELEASE_VERSION}"} -# Pushes the tag and release branch to Github. +# Pushes the release branch and tag to Github. git push origin ${BRANCH} -git push origin ${TAG} +git push origin v${VERSION}-maven # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java similarity index 96% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java rename to jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 61e1b8e88f..f6aec49feb 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -32,17 +32,12 @@ /** Integration tests for {@link BuildDockerMojo}. */ public class BuildDockerMojoIntegrationTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject emptyTestProject = new TestProject(testPlugin, "empty"); + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); @ClassRule - public static final TestProject defaultTargetTestProject = - new TestProject(testPlugin, "default-target"); + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); private static void buildToDockerDaemon(Path projectRoot, String imageReference, String pomXml) throws VerificationException, DigestException, IOException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java similarity index 97% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java rename to jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 3fab07385e..9f1fc25e17 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -56,23 +56,16 @@ public class BuildImageMojoIntegrationTest { public static final LocalRegistry localRegistry2 = new LocalRegistry(6000, "testuser2", "testpassword2"); - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + @ClassRule public static final TestProject emptyTestProject = new TestProject("empty"); - @ClassRule - public static final TestProject emptyTestProject = new TestProject(testPlugin, "empty"); + @ClassRule public static final TestProject skippedTestProject = new TestProject("empty"); @ClassRule - public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + public static final TestProject defaultTargetTestProject = new TestProject("default-target"); - @ClassRule - public static final TestProject defaultTargetTestProject = - new TestProject(testPlugin, "default-target"); - - @ClassRule - public static final TestProject servlet25Project = new TestProject(testPlugin, "war_servlet25"); + @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); private static String getTestImageReference(String label) { String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java similarity index 93% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java rename to jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 03643d68d4..5f13807080 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -29,13 +29,9 @@ public class BuildTarMojoIntegrationTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject skippedTestProject = new TestProject(testPlugin, "empty"); + @ClassRule public static final TestProject skippedTestProject = new TestProject("empty"); @Test public void testExecute_simple() diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java index aa90a7fba6..8d24e2dfa3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java @@ -26,7 +26,6 @@ /** Base class for Skaffold-related goals. */ abstract class SkaffoldBindingMojo extends AbstractMojo { - @Nullable @Parameter(defaultValue = "${plugin}", readonly = true) protected PluginDescriptor descriptor; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index a796fcc415..e881854202 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.maven.JibPluginConfiguration.FromAuthConfiguration; import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -101,7 +102,8 @@ public void testGetters() { Assert.assertEquals( Arrays.asList("--log", "info"), rawConfiguration.getProgramArguments().get()); Assert.assertEquals( - new HashSet<>(Arrays.asList("additional", "tags")), rawConfiguration.getToTags()); + new HashSet<>(Arrays.asList("additional", "tags")), + Sets.newHashSet(rawConfiguration.getToTags())); Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java deleted file mode 100644 index 59396ec0bd..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestPlugin.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import org.apache.maven.it.VerificationException; -import org.apache.maven.it.Verifier; -import org.apache.maven.model.Model; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; -import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -import org.junit.rules.ExternalResource; - -/** Sets up the plugin for testing. */ -public class TestPlugin extends ExternalResource { - - private String pluginVersion; - - String getVersion() { - return pluginVersion; - } - - @Override - protected void before() throws IOException, XmlPullParserException, VerificationException { - // Installs the plugin for use in tests. - Verifier verifier = new Verifier(".", true); - verifier.setAutoclean(false); - verifier.addCliOption("-DskipTests"); - verifier.addCliOption("-Dfmt.skip"); - verifier.addCliOption("-Dcheckstyle.skip"); - verifier.executeGoal("install"); - - // Reads the project version. - MavenXpp3Reader reader = new MavenXpp3Reader(); - Model model = - reader.read(Files.newBufferedReader(Paths.get("pom.xml"), StandardCharsets.UTF_8)); - pluginVersion = model.getVersion(); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index 83cb1e655a..649796e459 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -21,6 +21,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.it.util.ResourceExtractor; @@ -36,14 +38,12 @@ private static boolean isPomXml(Path path) { return filename.startsWith("pom") && filename.endsWith(".xml"); } - private final TestPlugin testPlugin; private final String projectDir; private Path projectRoot; /** Initialize to a specific project directory. */ - public TestProject(TestPlugin testPlugin, String projectDir) { - this.testPlugin = testPlugin; + public TestProject(String projectDir) { this.projectDir = projectDir; } @@ -71,12 +71,17 @@ private void copyProject() throws IOException { .toPath(); // Puts the correct plugin version into the test project pom.xml. + Path gradleProperties = Paths.get("gradle.properties"); + Properties properties = new Properties(); + properties.load(Files.newInputStream(gradleProperties)); + String pluginVersion = properties.getProperty("version"); + try (Stream files = Files.list(projectRoot)) { for (Path pomXml : files.filter(TestProject::isPomXml).collect(Collectors.toList())) { Files.write( pomXml, new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) - .replace("@@PluginVersion@@", testPlugin.getVersion()) + .replace("@@PluginVersion@@", pluginVersion) .getBytes(StandardCharsets.UTF_8)); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java index 2fff0678b7..9181ee1f1c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven.skaffold; import com.google.cloud.tools.jib.maven.MojoCommon; -import com.google.cloud.tools.jib.maven.TestPlugin; import com.google.cloud.tools.jib.maven.TestProject; import java.io.IOException; import org.apache.maven.it.VerificationException; @@ -28,10 +27,8 @@ /** Tests for {@link CheckJibVersionMojo}. */ public class CheckJibVersionMojoTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); @Test public void testIdentifiers() { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index f35601d6ee..3be6104cce 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven.skaffold; -import com.google.cloud.tools.jib.maven.TestPlugin; import com.google.cloud.tools.jib.maven.TestProject; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -36,13 +35,9 @@ /** Tests for {@link FilesMojo}. */ public class FilesMojoTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); private static void verifyFiles(Path projectRoot, String pomXml, String module, List files) throws VerificationException, IOException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index 1fb8c6578c..18cc60cd1b 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven.skaffold; -import com.google.cloud.tools.jib.maven.TestPlugin; import com.google.cloud.tools.jib.maven.TestProject; import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; import com.google.common.base.Strings; @@ -37,13 +36,9 @@ /** Tests for {@link FilesMojoV2}. */ public class FilesMojoV2Test { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); private static void verifyFiles( Path projectRoot, diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java index 71d685b6e3..beb0385ea8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/PackageGoalsMojoTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven.skaffold; -import com.google.cloud.tools.jib.maven.TestPlugin; import com.google.cloud.tools.jib.maven.TestProject; import com.google.common.base.Strings; import java.io.IOException; @@ -35,10 +34,7 @@ /** Tests for {@link PackageGoalsMojo}. */ public class PackageGoalsMojoTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); - - @ClassRule - public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); private void verifyGoals(Path projectRoot, String profilesString, String... expectedGoals) throws VerificationException, IOException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java index 51e050bbcf..341c928e77 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.maven.skaffold; -import com.google.cloud.tools.jib.maven.TestPlugin; import com.google.cloud.tools.jib.maven.TestProject; import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; import java.io.IOException; @@ -38,13 +37,9 @@ /** Tests for {@link SkaffoldInitMojo}. */ public class SkaffoldInitMojoTest { - @ClassRule public static final TestPlugin testPlugin = new TestPlugin(); + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - @ClassRule - public static final TestProject simpleTestProject = new TestProject(testPlugin, "simple"); - - @ClassRule - public static final TestProject multiTestProject = new TestProject(testPlugin, "multi"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); /** * Verifies that the files task succeeded and returns the list of JSON strings printed by the diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java index 41fee95d40..ab7425f896 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/src/test/java/com/lib/LibTest.java @@ -16,18 +16,14 @@ package com.lib; -import static org.junit.Assert.assertTrue; - import org.junit.Assert; import org.junit.Test; -/** - * Unit test for simple App. - */ +/** Unit test for simple App. */ public class LibTest { - /** Rigorous Test :-) */ - @Test - public void testGetThing() { - Assert.assertEquals("thing", new Lib().getThing()); - } + /** Rigorous Test :-) */ + @Test + public void testGetThing() { + Assert.assertEquals("thing", new Lib().getThing()); + } } diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 8ee67d0889..413febd980 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -1,115 +1,17 @@ -plugins { - id 'java' - id 'checkstyle' - id 'com.github.sherter.google-java-format' version '0.8' - id 'net.ltgt.apt' version '0.19' - id 'net.ltgt.errorprone' version '0.6' -} - -group 'com.google.cloud.tools' - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 -compileJava.options.encoding = 'UTF-8' - -repositories { - mavenCentral() -} - -sourceSets { - main { - java.srcDir file('../jib-core/src/main/java') - resources.srcDir file('../jib-core/src/main/resources') - } - test { - java.srcDir file('../jib-core/src/test/java') - resources.srcDir file('../jib-core/src/test/resources') - } -} - dependencies { - // Make sure these are consistent with jib-maven-plugin. - compile('com.google.http-client:google-http-client:1.31.0') { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - compile('com.google.http-client:google-http-client-apache-v2:1.31.0') { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - compile('com.google.auth:google-auth-library-oauth2-http:0.16.2') { + implementation project(':jib-core') + implementation ("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { exclude group: "org.apache.httpcomponents", module: "httpclient" } - compile 'org.apache.httpcomponents:httpclient:4.5.6' - compile 'com.google.guava:guava:28.0-jre' - - compile 'org.apache.commons:commons-compress:1.18' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' - compile 'org.ow2.asm:asm:7.0' - - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.23.4' - testCompile 'org.slf4j:slf4j-api:1.7.25' - testCompile 'com.github.stefanbirkner:system-rules:1.19.0' - - // NullAway errorprone plugin - annotationProcessor 'com.uber.nullaway:nullaway:0.6.4' - errorprone 'com.google.errorprone:error_prone_core:2.3.2' - // Using github.com/google/error-prone-javac is required when running on - // JDK 8. Remove when migrating to JDK 11. - if (System.getProperty("java.version").startsWith("1.8.")) { - errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") - } -} - -test { - testLogging { - showStandardStreams = true - exceptionFormat = 'full' - } -} - -import net.ltgt.gradle.errorprone.CheckSeverity - -// Adds NullAway errorprone checks. -tasks.withType(JavaCompile) { - if (!name.toLowerCase().contains("test")) { - options.errorprone { - check('NullAway', CheckSeverity.ERROR) - option('NullAway:AnnotatedPackages', 'com.google.cloud.tools') - } - } -} + implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" + implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" + implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" -// Fail build on javadoc warnings -tasks.withType(Javadoc) { - options.addBooleanOption('Xwerror', true) + testImplementation "junit:junit:${dependencyVersions.JUNIT}" + testImplementation "org.mockito:mockito-core:${dependencyVersions.MOCKITO_CORE}" + testImplementation "org.slf4j:slf4j-api:${dependencyVersions.SLF4J_API}" + testImplementation "com.github.stefanbirkner:system-rules:${dependencyVersions.SYSTEM_RULES}" + testImplementation project(path:':jib-core', configuration:'tests') } -assemble.dependsOn javadoc -tasks.withType(Test) { - reports.html.setDestination file("${reporting.baseDir}/${name}") -} - -/* GOOGLE JAVA FORMAT */ -googleJavaFormat { - toolVersion = '1.6' -} -check.dependsOn verifyGoogleJavaFormat -/* GOOGLE JAVA FORMAT */ - -/* CHECKSTYLE */ -checkstyle { - toolVersion = '8.18' - - // get the google_checks.xml file from the checkstyle jar and take out the java checks - def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - - config = resources.text.fromString(googleChecksNoJava) - - maxErrors = 0 - maxWarnings = 0 -} -/* CHECKSTYLE */ +sourceSets.test.resources.srcDirs project(':jib-core').sourceSets.test.resources diff --git a/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar b/jib-plugins-common/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKzNUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/jib-plugins-common/settings.gradle b/jib-plugins-common/settings.gradle deleted file mode 100644 index 86f932126b..0000000000 --- a/jib-plugins-common/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'jib-plugins-common' diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java index 5cf1c06d49..4ba44e0189 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JavaContainerBuilderHelperTest.java @@ -108,7 +108,6 @@ public void testFromExplodedWar() Path temporaryExplodedWar = temporaryFolder.getRoot().toPath().resolve("exploded-war"); Files.createDirectories(temporaryExplodedWar.resolve("WEB-INF/classes/empty_dir")); - Paths.get(Resources.getResource("core/layer").toURI()); JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder.from(RegistryImage.named("base")) diff --git a/kokoro/continuous.bat b/kokoro/continuous.bat index 949622bea1..f7e2264453 100755 --- a/kokoro/continuous.bat +++ b/kokoro/continuous.bat @@ -13,9 +13,6 @@ REM Sets the integration testing project. set JIB_INTEGRATION_TESTING_PROJECT=jib-integration-testing REM TODO: Enable integration tests once docker works (b/73345382). -cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -e && ^ -cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace +gradlew.bat clean build --info --stacktrace exit /b %ERRORLEVEL% diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index bc3b5e3f5d..b1e9833dd7 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -30,7 +30,4 @@ if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then while ! docker info > /dev/null 2>&1; do sleep 1; done fi -(cd github/jib/jib-core; ./gradlew clean build integrationTest --info --stacktrace) -(cd github/jib/jib-plugins-common; ./gradlew clean build --info --stacktrace) -(cd github/jib/jib-maven-plugin; ./mvnw clean install -PintegrationTest -B -U -e) -(cd github/jib/jib-gradle-plugin; ./gradlew clean build integrationTest --info --stacktrace) +./gradlew clean build integrationTest --info --stacktrace diff --git a/kokoro/presubmit.bat b/kokoro/presubmit.bat index 7dab7f73e5..4b2280097c 100755 --- a/kokoro/presubmit.bat +++ b/kokoro/presubmit.bat @@ -9,9 +9,6 @@ cd github/jib REM Stops any left-over containers. REM FOR /f "tokens=*" %%i IN ('docker ps -aq') DO docker rm -vf %%i -cd jib-core && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-plugins-common && call gradlew.bat clean build --info --stacktrace && ^ -cd ../jib-maven-plugin && call mvnw.cmd clean install -B -U -e && ^ -cd ../jib-gradle-plugin && call gradlew.bat clean build --info --stacktrace +call gradlew.bat clean build --info --stacktrace exit /b %ERRORLEVEL% diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index f84c885fbe..5fe0edf282 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -28,7 +28,5 @@ cd github/jib # TODO: Follow-up with Kokoro about why this is happening. rm /usr/local/bin/docker-credential-osxkeychain || true -(cd jib-core; ./gradlew clean build integrationTest --info --stacktrace) -(cd jib-plugins-common; ./gradlew clean build --info --stacktrace) -(cd jib-maven-plugin; ./mvnw clean install -B -U -e) -(cd jib-gradle-plugin; ./gradlew clean build --info --stacktrace) +# we only run integration tests on jib-core for presubmit +./gradlew clean build :jib-core:integrationTest --info --stacktrace diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..f8851b6def --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include ":jib-core" +include ":jib-plugins-common" +include ":jib-gradle-plugin" +include ":jib-maven-plugin" From 1f66e3d82acf211315e2e0821ad152c61ce04570 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 27 Aug 2019 15:55:04 -0400 Subject: [PATCH 0701/2020] cd into directory (#1934) --- kokoro/continuous.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index b1e9833dd7..45303a4e09 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -30,4 +30,6 @@ if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then while ! docker info > /dev/null 2>&1; do sleep 1; done fi +cd github/jib + ./gradlew clean build integrationTest --info --stacktrace From 7c7b0ea429a244dc0649a7a5a63cac3e64630d82 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 27 Aug 2019 17:44:29 -0400 Subject: [PATCH 0702/2020] Add local base image support to jib core (#1919) --- jib-core/CHANGELOG.md | 2 + .../tools/jib/api/JibIntegrationTest.java | 114 ++++++++++++++++++ .../resources/core/busybox.tar | Bin 0 -> 1450496 bytes .../src/integration-test/resources/core/hello | 1 + .../tools/jib/api/JavaContainerBuilder.java | 22 ++++ .../com/google/cloud/tools/jib/api/Jib.java | 24 +++- .../tools/jib/api/JibContainerBuilder.java | 38 ++++-- .../tools/jib/builder/steps/StepsRunner.java | 67 ++++++++-- .../jib/configuration/ImageConfiguration.java | 60 ++++++++- .../jib/api/JibContainerBuilderTest.java | 24 ++-- 10 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 jib-core/src/integration-test/resources/core/busybox.tar create mode 100644 jib-core/src/integration-test/resources/core/hello diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 2d18038c34..851a5b87f3 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) + ### Changed - `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)` ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index bc4705d25f..967fa6ed9d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -19,8 +19,12 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.registry.LocalRegistry; import com.google.cloud.tools.jib.registry.ManifestPullerIntegrationTest; +import com.google.common.io.Resources; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -88,6 +92,116 @@ public void testBasic_helloWorld() targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); } + @Test + public void testBasic_dockerDaemonBaseImage() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + localRegistry.pull("busybox"); + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-dockerdaemon"); + JibContainer jibContainer = + Jib.from(DockerDaemonImage.named("busybox")) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.from("username", "password")))) + .setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); + Assert.assertEquals( + "Hello World\n", + pullAndRunBuiltImage( + targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); + } + + @Test + public void testBasic_tarBaseImage_dockerSavedCommand() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + localRegistry.pull("busybox"); + Path path = cacheFolder.getRoot().toPath().resolve("docker-save"); + new Command("docker", "save", "busybox", "-o", path.toString()).run(); + + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-dockersavedtar"); + JibContainer jibContainer = + Jib.from(TarImage.at(path).named("ignored")) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.from("username", "password")))) + .setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); + Assert.assertEquals( + "Hello World\n", + pullAndRunBuiltImage( + targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); + } + + @Test + public void testBasic_tarBaseImage_dockerSavedFile() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException, URISyntaxException { + // tar saved with 'docker save busybox -o busybox.tar' + Path path = Paths.get(Resources.getResource("core/busybox.tar").toURI()); + + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-dockersavedtar"); + JibContainer jibContainer = + Jib.from(TarImage.at(path).named("ignored")) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.from("username", "password")))) + .setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); + Assert.assertEquals( + "Hello World\n", + pullAndRunBuiltImage( + targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); + } + + @Test + public void testBasic_tarBaseImage_jibImage() + throws InvalidImageReferenceException, InterruptedException, ExecutionException, + RegistryException, CacheDirectoryCreationException, IOException, URISyntaxException { + ImageReference targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "jib-base-image"); + Path outputPath = cacheFolder.getRoot().toPath().resolve("jib-image.tar"); + Jib.from("busybox") + .addLayer( + Collections.singletonList(Paths.get(Resources.getResource("core/hello").toURI())), "/") + .containerize( + Containerizer.to(TarImage.at(outputPath).named(targetImageReference)) + .setAllowInsecureRegistries(true)); + + targetImageReference = + ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-jibtar"); + JibContainer jibContainer = + Jib.from(TarImage.at(outputPath).named("ignored")) + .setEntrypoint("cat", "/hello") + .containerize( + Containerizer.to( + RegistryImage.named(targetImageReference) + .addCredentialRetriever( + () -> Optional.of(Credential.from("username", "password")))) + .setAllowInsecureRegistries(true)); + + Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(targetImageReference.toString())); + Assert.assertEquals( + "Hello World\n", + pullAndRunBuiltImage( + targetImageReference.withTag(jibContainer.getDigest().toString()).toString())); + } + @Test public void testScratch() throws IOException, InterruptedException, ExecutionException, RegistryException, diff --git a/jib-core/src/integration-test/resources/core/busybox.tar b/jib-core/src/integration-test/resources/core/busybox.tar new file mode 100644 index 0000000000000000000000000000000000000000..f17c980af93e1eb2eb9fa8a7f66bf9d04d598216 GIT binary patch literal 1450496 zcmeEv34Bvk+V@SH&`?rt3I&Rwgc7kGSsEy%rL8oyl^ac=2nbS8pn#|d480eMY^iCr zx0$Fjqce`n$hf@YHtINvP#|eZ7jQuqQ4|G~?S!x+1=;fbpL5fN8E5AGelzd;d%yRS zpZ4B+_Vb))KhHUtIV5d%W=ZCdj3Kj!4jVFjPTG)Eybd3hHZ#MPKD1;`dS>dJIcdW) zXU(w9o-reJ$ncEWLx-lP+J>jinmwarX4+ukpZ`luO-mgwjm#9pfEi$NZz= zrrJ_d)6>#gLGxFj!spPxT7O&W&~(A_FR#fz8dOW(|L5y(8}#1-YWmEyIYZJjW~U7wHfPSTw2~RaOKihumJFSdkv@FZjG^i2GiD6S zunilYo;qxJ`{wRJ!*lMtH)+JOBxymZJb%W6v;V3Gl+Iu9Ao`Bv;d^2!9|zv-vOJ?? z?s$3Dee;t>%$YH-bauP61&aK)iL)1B3Xzf%FKFy!)>#M4x2S+_|WvBLo+jm&m5XQJTo|IrEm-uxee>_lzmHZO z*#-0EMN1bhxNkn-s+AZ!f0T0HJd8;Dbk3MLdmgGUTd`t5(yRp~56oV4&*IsOO7B~M z$-3`$KOWE%*Ga)kQZbw~RD;?isEcLDlu%pi&JZ*1;IoBGe%|3je(4o!uf2z${6{qHCH|3|_3Z%w52FaEc-__vMG zdj38CPuh@SX~TwU^xtm%ZRtPJ|K9@gzaV8*f%DcVovyXzDB%`?zDD-S2yz`Y_O2$s>d-d|)M zLR2nN@GmMCe+0q8|F%7ik?$Bhsovk~ucche^jp5i9g=5J-T&;rmil*~-aqP(MH*~* z5*+Ct)uA?M`lH88r1h+&w3d9l{`BuB2>b+rpCIrP1b%|RPZ0PC0zX0ECkXrmfuA7o z69oPjKtOUIGqQmuL10E$+>*QA7`SFu_6ZXu^~!ipj8w9tP_38hzKixw4ojZVMzxPr zvPY^rV32gXJsIjPl4qe&Qa@$a!OV7emRpT%b~m)>DKJW&<%vc%iN2zz8AWrR*^{R) zwu|w4>ooh+fK(ZlM@9Ov?a7>mIxCDTtq%2TDI9Rr9W>nLS$k(-UUlt?lh_BF2 z7KKN#t(Kax)ds{qn8C#uN&R8*3ddSqZ&r4@zZ
R9W@IRWzA2a<)q(2t=lT3e7 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

i;p_0@SCAPm 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

Hu_SOl-D?Lu#Qe>+OZ5 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⩔RgoXSQ!ZykNZiL!!t+t%2M%5Tf)S`+B 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�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

N(`C_N|zgPj)QjK(pZ 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

2sNM{E0EWYB%@VowFMMm=?rvPfhwM2kk)jBS7d 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
cbCl63Wvrx(un!xt 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%0D*JpqM_)nPne;K6u2+I5yof${l7C-^`k0;Ii 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 nbIlN2C^b!Nq+>%9%e2g)0|$&b`Xj jvmFlags = new ArrayList<>(); private final LinkedHashSet classpathOrder = new LinkedHashSet<>(4); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index 9f43899636..a0be5a8040 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -45,7 +45,7 @@ public static JibContainerBuilder from(ImageReference baseImageReference) { } /** - * Starts building the container from a base image. + * Starts building the container from a registry base image. * * @param registryImage the {@link RegistryImage} that defines base container registry and * credentials @@ -55,6 +55,28 @@ public static JibContainerBuilder from(RegistryImage registryImage) { return new JibContainerBuilder(registryImage); } + /** + * Starts building the container from a base image stored in the Docker cache. Requires a running + * Docker daemon. + * + * @param dockerDaemonImage the {@link DockerDaemonImage} that defines the base image and Docker + * client + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(DockerDaemonImage dockerDaemonImage) { + return new JibContainerBuilder(dockerDaemonImage); + } + + /** + * Starts building the container from a tarball. + * + * @param tarImage the {@link TarImage} that defines the path to the base image + * @return a new {@link JibContainerBuilder} to continue building the container + */ + public static JibContainerBuilder from(TarImage tarImage) { + return new JibContainerBuilder(tarImage); + } + /** * Starts building the container from an empty base image. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index c190b65dff..162946f19e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -73,19 +74,38 @@ private static String capitalizeFirstLetter(String string) { /** Instantiate with {@link Jib#from}. */ JibContainerBuilder(RegistryImage baseImage) { - this(baseImage, BuildConfiguration.builder()); + this( + ImageConfiguration.builder(baseImage.getImageReference()) + .setCredentialRetrievers(baseImage.getCredentialRetrievers()) + .build(), + BuildConfiguration.builder()); + } + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(DockerDaemonImage baseImage) { + this( + ImageConfiguration.builder(baseImage.getImageReference()) + .setDockerClient( + new DockerClient(baseImage.getDockerExecutable(), baseImage.getDockerEnvironment())) + .build(), + BuildConfiguration.builder()); + } + + /** Instantiate with {@link Jib#from}. */ + JibContainerBuilder(TarImage baseImage) { + // TODO: Cleanup using scratch as placeholder + this( + ImageConfiguration.builder(baseImage.getImageReference().orElse(ImageReference.scratch())) + .setTarPath(baseImage.getPath()) + .build(), + BuildConfiguration.builder()); } @VisibleForTesting JibContainerBuilder( - RegistryImage baseImage, BuildConfiguration.Builder buildConfigurationBuilder) { - this.buildConfigurationBuilder = buildConfigurationBuilder; - - ImageConfiguration imageConfiguration = - ImageConfiguration.builder(baseImage.getImageReference()) - .setCredentialRetrievers(baseImage.getCredentialRetrievers()) - .build(); - buildConfigurationBuilder.setBaseImageConfiguration(imageConfiguration); + ImageConfiguration imageConfiguration, BuildConfiguration.Builder buildConfigurationBuilder) { + this.buildConfigurationBuilder = + buildConfigurationBuilder.setBaseImageConfiguration(imageConfiguration); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 1a73e959c2..42666842e5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -19,8 +19,10 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; @@ -31,6 +33,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -59,6 +62,7 @@ private static Future failedFuture() { new IllegalStateException("invalid usage; required step not configured")); } + private Future tarPath = failedFuture(); private Future baseImageAndAuth = failedFuture(); private Future>> baseImageLayers = failedFuture(); @Nullable private List> applicationLayers; @@ -121,11 +125,10 @@ private StepsRunner( public StepsRunner dockerLoadSteps(DockerClient dockerClient) { rootProgressDescription = "building image to Docker daemon"; - // build and cache - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(() -> obtainBaseImageLayers(true)); // always pull layers for docker builds + addRetrievalSteps(true); // always pull layers for docker builds stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); + // load to Docker stepsToRun.add(() -> loadDocker(dockerClient)); return this; @@ -134,11 +137,10 @@ public StepsRunner dockerLoadSteps(DockerClient dockerClient) { public StepsRunner tarBuildSteps(Path outputPath) { rootProgressDescription = "building image to tar file"; - // build and cache - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(() -> obtainBaseImageLayers(true)); // always pull layers for tar builds + addRetrievalSteps(true); // always pull layers for tar builds stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); + // create a tar stepsToRun.add(() -> writeTarFile(outputPath)); return this; @@ -151,8 +153,7 @@ public StepsRunner registryPushSteps() { stepsToRun.add(this::retrieveTargetRegistryCredentials); stepsToRun.add(this::authenticatePush); - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(() -> obtainBaseImageLayers(layersRequiredLocally)); + addRetrievalSteps(layersRequiredLocally); stepsToRun.add(this::buildAndCacheApplicationLayers); stepsToRun.add(this::buildImage); @@ -164,6 +165,26 @@ public StepsRunner registryPushSteps() { return this; } + private void addRetrievalSteps(boolean layersRequiredLocally) { + ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); + + if (baseImageConfiguration.getTarPath().isPresent()) { + // If tarPath is present, a TarImage was used + results.tarPath = Futures.immediateFuture(baseImageConfiguration.getTarPath().get()); + stepsToRun.add(this::extractTar); + + } else if (baseImageConfiguration.getDockerClient().isPresent()) { + // If dockerClient is present, a DockerDaemonImage was used + stepsToRun.add(this::saveDocker); + stepsToRun.add(this::extractTar); + + } else { + // Otherwise default to RegistryImage + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(() -> obtainBaseImageLayers(layersRequiredLocally)); + } + } + public BuildResult run() throws ExecutionException, InterruptedException { Preconditions.checkNotNull(rootProgressDescription); @@ -208,6 +229,36 @@ private void authenticatePush() { .call()); } + private void saveDocker() { + Optional dockerClient = + buildConfiguration.getBaseImageConfiguration().getDockerClient(); + Preconditions.checkArgument(dockerClient.isPresent()); + + results.tarPath = + executorService.submit(new SaveDockerStep(buildConfiguration, dockerClient.get())); + } + + private void extractTar() { + Future localImageFuture = + executorService.submit( + () -> + new ExtractTarStep( + results.tarPath.get(), Files.createTempDirectory("jib-extract-tar")) + .call()); + results.baseImageAndAuth = + executorService.submit( + () -> new ImageAndAuthorization(localImageFuture.get().baseImage, null)); + results.baseImageLayers = + executorService.submit( + () -> + localImageFuture + .get() + .layers + .stream() + .map(Futures::immediateFuture) + .collect(Collectors.toList())); + } + private void pullBaseImage() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java index d472331244..a83eaddbfe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java @@ -18,11 +18,15 @@ import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.docker.DockerClient; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import java.nio.file.Path; import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; -/** Immutable configuration options for an image reference with credentials. */ +/** Immutable configuration options for a base or target image reference. */ public class ImageConfiguration { /** Builder for instantiating an {@link ImageConfiguration}. */ @@ -30,6 +34,8 @@ public static class Builder { private ImageReference imageReference; private ImmutableList credentialRetrievers = ImmutableList.of(); + @Nullable private DockerClient dockerClient; + @Nullable private Path tarPath; /** * Sets the providers for registry credentials. The order determines the priority in which the @@ -45,13 +51,46 @@ public Builder setCredentialRetrievers(List credentialRetri return this; } + /** + * Sets the Docker client to be used for Docker daemon base images. + * + * @param dockerClient the Docker client + * @return this + */ + public Builder setDockerClient(DockerClient dockerClient) { + this.dockerClient = dockerClient; + return this; + } + + /** + * Sets the path for tarball base images. + * + * @param tarPath the path + * @return this + */ + public Builder setTarPath(Path tarPath) { + this.tarPath = tarPath; + return this; + } + /** * Builds the {@link ImageConfiguration}. * * @return the corresponding {@link ImageConfiguration} */ public ImageConfiguration build() { - return new ImageConfiguration(imageReference, credentialRetrievers); + int numArguments = 0; + if (!credentialRetrievers.isEmpty()) { + numArguments++; + } + if (dockerClient != null) { + numArguments++; + } + if (tarPath != null) { + numArguments++; + } + Preconditions.checkArgument(numArguments <= 1); + return new ImageConfiguration(imageReference, credentialRetrievers, dockerClient, tarPath); } private Builder(ImageReference imageReference) { @@ -71,11 +110,18 @@ public static Builder builder(ImageReference imageReference) { private final ImageReference image; private final ImmutableList credentialRetrievers; + @Nullable private DockerClient dockerClient; + @Nullable private Path tarPath; private ImageConfiguration( - ImageReference image, ImmutableList credentialRetrievers) { + ImageReference image, + ImmutableList credentialRetrievers, + @Nullable DockerClient dockerClient, + @Nullable Path tarPath) { this.image = image; this.credentialRetrievers = credentialRetrievers; + this.dockerClient = dockerClient; + this.tarPath = tarPath; } public ImageReference getImage() { @@ -97,4 +143,12 @@ public String getImageTag() { public ImmutableList getCredentialRetrievers() { return credentialRetrievers; } + + public Optional getDockerClient() { + return Optional.ofNullable(dockerClient); + } + + public Optional getTarPath() { + return Optional.ofNullable(tarPath); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index efe28fb3fa..210c6620a0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -60,8 +60,10 @@ public class JibContainerBuilderTest { @Test public void testToBuildConfiguration_containerConfigurationSet() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -93,8 +95,10 @@ public void testToBuildConfiguration_containerConfigurationSet() @Test public void testToBuildConfiguration_containerConfigurationAdd() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) .setEntrypoint("entry", "point") .setEnvironment(ImmutableMap.of("name", "value")) .addEnvironmentVariable("environment", "variable") @@ -137,10 +141,12 @@ public void testToBuildConfiguration() .setExecutorService(mockExecutorService) .addEventHandler(mockJibEventConsumer); - RegistryImage baseImage = - RegistryImage.named("base/image").addCredentialRetriever(mockCredentialRetriever); + ImageConfiguration baseImageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")) + .setCredentialRetrievers(Collections.singletonList(mockCredentialRetriever)) + .build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImage, spyBuildConfigurationBuilder) + new JibContainerBuilder(baseImageConfiguration, spyBuildConfigurationBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration( @@ -211,8 +217,10 @@ public void testToBuildConfiguration() /** Verify that an internally-created ExecutorService is shutdown. */ @Test public void testContainerize_executorCreated() throws Exception { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -232,8 +240,10 @@ public void testContainerize_executorCreated() throws Exception { /** Verify that a provided ExecutorService is not shutdown. */ @Test public void testContainerize_configuredExecutor() throws Exception { + ImageConfiguration imageConfiguration = + ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(RegistryImage.named("base/image"), spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) From a04064eee6120e41469cafbff48cb0cafcad6de0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 29 Aug 2019 10:47:00 -0400 Subject: [PATCH 0703/2020] Fix maven version (#1941) --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index ced16a7bbb..fd0eb383c9 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.5.1-SNAPSHOT +version = 1.5.2-SNAPSHOT From c4035cacbfb76d36d9fe8543bd9f2ce4568b234c Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 29 Aug 2019 18:22:14 -0400 Subject: [PATCH 0704/2020] Default test runner in idea to gradle, fix checkstyle xml (#1935) --- build.gradle | 11 +++++++++++ jib-maven-plugin/config/google-checks-no-indent.xml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e6aa95d4b4..cbc802143d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,17 @@ plugins { id 'net.researchgate.release' version '2.7.0' apply false id 'com.gradle.plugin-publish' version '0.10.1' apply false id 'io.freefair.maven-plugin' version '3.8.1' apply false + + // apply so we can correctly configure the test runner to be gradle at the project level + id "org.jetbrains.gradle.plugin.idea-ext" version "0.5" +} + +// run tests in intellij using gradle test runner +idea.project.settings { + delegateActions { + delegateBuildRunToGradle = false + testRunner = 'GRADLE' + } } import net.ltgt.gradle.errorprone.CheckSeverity diff --git a/jib-maven-plugin/config/google-checks-no-indent.xml b/jib-maven-plugin/config/google-checks-no-indent.xml index 9efa9a7967..f6faf624c1 100644 --- a/jib-maven-plugin/config/google-checks-no-indent.xml +++ b/jib-maven-plugin/config/google-checks-no-indent.xml @@ -1,7 +1,7 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> d{|EX1^myJ+ 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�O`-|7xNexZU#o0f$+m{i1`rIKtBP| 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႙(z#1FNJ+y=cgHR)`~K_qcmC)9 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[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

ya6iw`owcOt1jVT3R3 z7^f7aV)6PaaS86<_?=)Mt@7Ip6Dc^#9xU5Z3N;@T_0dYZoY!bn8B8zwj?++krl*!n zuYnF(Z(0dBDt5BqS1i7*5Ng_os>@tE?k{R&=hcesd|7V-+e6cO3yByNd6It8Cka1o zq%#r{ewvbU4r=28zDL65!X0p3PqmnAL>p13jC

rf{{P2*5Y6_~|p!Xi)0N1`2?Hxwo6 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 { @@ -123,7 +124,15 @@ public LocalImage call() Path file = destination.resolve(layerFiles.get(index)); // Compress layers if necessary and calculate the digest/size - Blob blob = layersAreCompressed ? Blobs.from(file) : Blobs.compress(Blobs.from(file)); + Blob blob = Blobs.from(file); + if (!layersAreCompressed) { + Path compressedFile = destination.resolve(layerFiles.get(index) + ".compressed"); + try (GZIPOutputStream compressorStream = + new GZIPOutputStream(Files.newOutputStream(compressedFile))) { + blob.writeTo(compressorStream); + } + blob = Blobs.from(compressedFile); + } BlobDescriptor blobDescriptor = blob.writeTo(ByteStreams.nullOutputStream()); // 'manifest' contains the layer files in the same order as the diff ids in 'configuration', From c6d0f9082d4a33cc46c7e59b979ac38706ded924 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 3 Sep 2019 11:01:28 -0400 Subject: [PATCH 0706/2020] Update versions to 1.5.1 (#1948) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 4 ++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 4 ++++ jib-maven-plugin/README.md | 6 +++--- 16 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0cddb60ae..e77d156e37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.5.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.5.2-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.5.0' + // id 'com.google.cloud.tools.jib' version '1.5.1' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 97a26f43eb..5083f95dc2 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.5.0 + 1.5.1 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 21e44e48d0..8aff3def17 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 67cbc8d5f9..74eea89290 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.5.0 + 1.5.1 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index cb9796a8f7..62782fa7ad 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 5f92d888ed..b65a55ef85 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.5.0 + 1.5.1 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 39e8e533e2..3aa825b833 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 7e66712afb..ac0ff1a888 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.5.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.5.1' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 77b4b4a0f3..e95d58ccb0 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.5.0 + 1.5.1 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 0add96cf62..19ea6da2b0 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 6732cd25ae..e51c71a2b4 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.5.0 + 1.5.1 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 2f60ee1c3c..99aeba35f8 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index dbc013a8d7..f531ef6469 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.5.1 + +### Fixed + - Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) ## 1.5.0 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 64d8037f54..0dea7ba2d9 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.5.0' + id 'com.google.cloud.tools.jib' version '1.5.1' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index f64fe90573..7d1ec7adfe 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.5.1 + +### Fixed + - Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) ## 1.5.0 diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index db8a9bf6d4..6818b36d6d 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.1:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.5.0 + 1.5.1 myimage From 68402d1a0b59910fee84c81b6fb4200e12bdf681 Mon Sep 17 00:00:00 2001 From: Tejal Desai Date: Wed, 4 Sep 2019 09:00:17 -0700 Subject: [PATCH 0707/2020] Add `upstreamClient` property to jib which tools using jib to build and push images can use. (#1933) --- .../tools/jib/global/JibSystemProperties.java | 2 ++ .../tools/jib/registry/RegistryClient.java | 6 ++++++ .../tools/jib/registry/RegistryClientTest.java | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index 4a97dcb82b..e6523788af 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -23,6 +23,8 @@ /** Names of system properties defined/used by Jib. */ public class JibSystemProperties { + public static final String UPSTREAM_CLIENT = "_JIB_UPSTREAM_CLIENT"; + @VisibleForTesting public static final String HTTP_TIMEOUT = "jib.httpTimeout"; @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 60a494fa4a..71d9c76d19 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -34,6 +34,7 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimap; import java.io.IOException; @@ -129,6 +130,11 @@ private String makeUserAgent() { if (userAgentSuffix != null) { userAgentBuilder.append(" ").append(userAgentSuffix); } + if (!Strings.isNullOrEmpty(System.getProperty(JibSystemProperties.UPSTREAM_CLIENT))) { + userAgentBuilder + .append(" ") + .append(System.getProperty(JibSystemProperties.UPSTREAM_CLIENT)); + } return userAgentBuilder.toString(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 6ca25274dc..0775f22e68 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -17,10 +17,13 @@ package com.google.cloud.tools.jib.registry; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -42,6 +45,8 @@ public void setUp() { RegistryClient.factory(eventHandlers, "some.server.url", "some image name"); } + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Test public void testGetUserAgent_null() { Assert.assertTrue( @@ -72,6 +77,19 @@ public void testGetUserAgent() { Assert.assertTrue(registryClient.getUserAgent().endsWith(" some user agent suffix")); } + @Test + public void testGetUserAgentWithUpstreamClient() { + System.setProperty(JibSystemProperties.UPSTREAM_CLIENT, "skaffold/0.34.0"); + + RegistryClient registryClient = + testRegistryClientFactory + .setAllowInsecureRegistries(true) + .setUserAgentSuffix("foo") + .newRegistryClient(); + Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); + Assert.assertTrue(registryClient.getUserAgent().endsWith(" skaffold/0.34.0")); + } + @Test public void testGetApiRouteBase() { Assert.assertEquals( From 178f982a7d621b5a34645353299018ef8ea5c5d8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 4 Sep 2019 12:14:11 -0400 Subject: [PATCH 0708/2020] Do not fall back to HTTP for connection timeout (#1949) --- .../tools/jib/builder/steps/StepsRunner.java | 40 +++++++++---------- .../jib/registry/RegistryEndpointCaller.java | 8 ++++ .../registry/RegistryEndpointCallerTest.java | 20 ++++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 42666842e5..647f0cf512 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -165,26 +165,6 @@ public StepsRunner registryPushSteps() { return this; } - private void addRetrievalSteps(boolean layersRequiredLocally) { - ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); - - if (baseImageConfiguration.getTarPath().isPresent()) { - // If tarPath is present, a TarImage was used - results.tarPath = Futures.immediateFuture(baseImageConfiguration.getTarPath().get()); - stepsToRun.add(this::extractTar); - - } else if (baseImageConfiguration.getDockerClient().isPresent()) { - // If dockerClient is present, a DockerDaemonImage was used - stepsToRun.add(this::saveDocker); - stepsToRun.add(this::extractTar); - - } else { - // Otherwise default to RegistryImage - stepsToRun.add(this::pullBaseImage); - stepsToRun.add(() -> obtainBaseImageLayers(layersRequiredLocally)); - } - } - public BuildResult run() throws ExecutionException, InterruptedException { Preconditions.checkNotNull(rootProgressDescription); @@ -205,6 +185,26 @@ public BuildResult run() throws ExecutionException, InterruptedException { } } + private void addRetrievalSteps(boolean layersRequiredLocally) { + ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); + + if (baseImageConfiguration.getTarPath().isPresent()) { + // If tarPath is present, a TarImage was used + results.tarPath = Futures.immediateFuture(baseImageConfiguration.getTarPath().get()); + stepsToRun.add(this::extractTar); + + } else if (baseImageConfiguration.getDockerClient().isPresent()) { + // If dockerClient is present, a DockerDaemonImage was used + stepsToRun.add(this::saveDocker); + stepsToRun.add(this::extractTar); + + } else { + // Otherwise default to RegistryImage + stepsToRun.add(this::pullBaseImage); + stepsToRun.add(() -> obtainBaseImageLayers(layersRequiredLocally)); + } + } + private void retrieveTargetRegistryCredentials() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 0300879379..2cf3529efc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -175,6 +175,14 @@ private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, Reg return handleUnverifiableServerException(url); } catch (ConnectException ex) { + // It is observed that Open/Oracle JDKs sometimes throw SocketTimeoutException but other times + // ConnectException for connection timeout. (Could be a JDK bug.) Note SocketTimeoutException + // does not extend ConnectException (or vice versa), and we want to be consistent to error out + // on timeouts: https://github.com/GoogleContainerTools/jib/issues/1895#issuecomment-527544094 + if (ex.getMessage() != null && ex.getMessage().contains("timed out")) { + throw ex; + } + if (allowInsecureRegistries && isHttpsProtocol(url) && url.getPort() == -1) { // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS // port 443) and we could not connect to 443. It's worth trying port 80. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 77b540b0e9..febd365a10 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -277,6 +277,26 @@ public void testCall_insecureCallerOnNonListeningServerAndPortSpecified() Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); } + @Test + public void testCall_timeoutFromConnectException() throws IOException, RegistryException { + ConnectException mockConnectException = Mockito.mock(ConnectException.class); + Mockito.when(mockConnectException.getMessage()).thenReturn("Connection timed out"); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(mockConnectException) // server times out on 443 + .thenReturn(mockResponse); // respond when connected through 80 + + try { + RegistryEndpointCaller insecureEndpointCaller = + createRegistryEndpointCaller(true, -1); + insecureEndpointCaller.call(); + Assert.fail("Should not fall back to HTTP if timed out even for ConnectionException"); + + } catch (ConnectException ex) { + Assert.assertSame(mockConnectException, ex); + Mockito.verify(mockConnection).send(Mockito.anyString(), Mockito.any()); + } + } + @Test public void testCall_noHttpResponse() throws IOException, RegistryException { NoHttpResponseException mockNoHttpResponseException = From 5a4b6e4f18f0e7b9ddb1efeb0bc4db86b21646ee Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 5 Sep 2019 10:34:36 -0400 Subject: [PATCH 0709/2020] Add plugin configuration for local base images (#1930) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JibIntegrationTest.java | 4 +- .../tools/jib/api/JavaContainerBuilder.java | 6 +- .../com/google/cloud/tools/jib/api/Jib.java | 26 ++- jib-gradle-plugin/CHANGELOG.md | 2 + .../cloud/tools/jib/gradle/JibRunHelper.java | 18 ++ .../gradle/SingleProjectIntegrationTest.java | 32 +++ .../projects/simple/build-local-base.gradle | 35 ++++ jib-maven-plugin/CHANGELOG.md | 2 + .../maven/BuildImageMojoIntegrationTest.java | 63 ++++++ .../maven/projects/simple/pom-localbase.xml | 70 +++++++ .../ConfigurationPropertyValidator.java | 8 +- .../common/PluginConfigurationProcessor.java | 94 +++++---- .../PluginConfigurationProcessorTest.java | 184 +++++++++++------- 14 files changed, 433 insertions(+), 112 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 851a5b87f3..77ecc31684 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) +- `Jib#from(String)` accepts strings prefixed with `docker://`, `tar://`, or `registry://` to specify image type ### Changed diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index 3f593c7a4e..f9ff478c16 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -100,7 +100,7 @@ public void testBasic_dockerDaemonBaseImage() ImageReference targetImageReference = ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-dockerdaemon"); JibContainer jibContainer = - Jib.from(DockerDaemonImage.named("busybox")) + Jib.from("docker://busybox") .setEntrypoint("echo", "Hello World") .containerize( Containerizer.to( @@ -142,7 +142,7 @@ public void testBasic_tarBaseImage_dockerSavedCommand() ImageReference targetImageReference = ImageReference.of("localhost:5000", "jib-core", "basic-helloworld-dockersavedtar"); JibContainer jibContainer = - Jib.from(TarImage.at(path).named("ignored")) + Jib.from("tar://" + path) .setEntrypoint("echo", "Hello World") .containerize( Containerizer.to( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 6a28e393c8..bd263c6227 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -127,7 +127,9 @@ public static JavaContainerBuilder fromDistrolessJetty() { } /** - * Creates a new {@link JavaContainerBuilder} with the specified base image reference. + * Creates a new {@link JavaContainerBuilder} with the specified base image reference. The type of + * base image can be specified using a prefix; see {@link Jib#from(String)} for the accepted + * prefixes. * * @param baseImageReference the base image reference * @return a new {@link JavaContainerBuilder} @@ -135,7 +137,7 @@ public static JavaContainerBuilder fromDistrolessJetty() { */ public static JavaContainerBuilder from(String baseImageReference) throws InvalidImageReferenceException { - return from(RegistryImage.named(baseImageReference)); + return new JavaContainerBuilder(Jib.from(baseImageReference)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java index a0be5a8040..20e70bb638 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java @@ -16,12 +16,25 @@ package com.google.cloud.tools.jib.api; +import java.nio.file.Paths; + /** Build containers with Jib. */ public class Jib { + public static final String REGISTRY_IMAGE_PREFIX = "registry://"; + public static final String DOCKER_DAEMON_IMAGE_PREFIX = "docker://"; + public static final String TAR_IMAGE_PREFIX = "tar://"; + /** - * Starts building the container from a base image. The base image should be publicly-available. - * For a base image that requires credentials, use {@link #from(RegistryImage)}. + * Starts building the container from a base image. The type of base image can be specified using + * a prefix, e.g. {@code docker://gcr.io/project/image}. The available prefixes are described + * below: + * + *

    + *
  • No prefix, or {@code registry://}: uses a registry base image + *
  • {@code docker://}: uses a base image found in the local Docker daemon + *
  • {@code tar://}: uses a tarball base image at the path following the prefix + *
* * @param baseImageReference the base image reference * @return a new {@link JibContainerBuilder} to continue building the container @@ -30,7 +43,14 @@ public class Jib { */ public static JibContainerBuilder from(String baseImageReference) throws InvalidImageReferenceException { - return from(RegistryImage.named(baseImageReference)); + if (baseImageReference.startsWith(DOCKER_DAEMON_IMAGE_PREFIX)) { + return from( + DockerDaemonImage.named(baseImageReference.replaceFirst(DOCKER_DAEMON_IMAGE_PREFIX, ""))); + } + if (baseImageReference.startsWith(TAR_IMAGE_PREFIX)) { + return from(TarImage.at(Paths.get(baseImageReference.replaceFirst(TAR_IMAGE_PREFIX, "")))); + } + return from(RegistryImage.named(baseImageReference.replaceFirst(REGISTRY_IMAGE_PREFIX, ""))); } /** diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index f531ef6469..d858e80b4b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Support for local base images by prefixing `jib.from.image` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) + ### Changed ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 8db0811e48..2bb66ec39b 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -65,6 +65,24 @@ static String buildAndRun( return pullAndRunBuiltImage(imageReference, extraRunArguments); } + static String buildAndRunFromLocalBase(String target, String base) + throws IOException, InterruptedException, DigestException { + BuildResult buildResult = + SingleProjectIntegrationTest.simpleTestProject.build( + "clean", + "jib", + "-Djib.useOnlyProjectCache=true", + "-Djib.console=plain", + "-D_TARGET_IMAGE=" + target, + "-D_BASE_IMAGE=" + base, + "-Djib.allowInsecureRegistries=" + target.startsWith("localhost"), + "-b=" + "build-local-base.gradle"); + assertBuildSuccess(buildResult, "jib", "Built and pushed image as "); + assertImageDigestAndId(SingleProjectIntegrationTest.simpleTestProject.getProjectRoot()); + Assert.assertThat(buildResult.getOutput(), CoreMatchers.containsString(target)); + return pullAndRunBuiltImage(target); + } + static void buildAndRunAdditionalTag( TestProject testProject, String imageReference, String additionalTag, String expectedOutput) throws InvalidImageReferenceException, IOException, InterruptedException, DigestException { diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index a01bc3c9b1..274a9ab92a 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -32,7 +32,9 @@ import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Integration tests for building single project images. */ public class SingleProjectIntegrationTest { @@ -47,6 +49,8 @@ public class SingleProjectIntegrationTest { @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private static boolean isJava11RuntimeOrHigher() { Iterable split = Splitter.on(".").split(System.getProperty("java.version")); return Integer.valueOf(split.iterator().next()) >= 11; @@ -204,6 +208,34 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE assertLayerSize(8, targetImage); } + @Test + public void testBuild_dockerDaemonBase() + throws IOException, InterruptedException, DigestException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithdockerdaemonbase:gradle" + + System.nanoTime(); + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", + JibRunHelper.buildAndRunFromLocalBase( + targetImage, "docker://gcr.io/distroless/java:latest")); + } + + @Test + public void testBuild_tarBase() throws IOException, InterruptedException, DigestException { + Path path = temporaryFolder.getRoot().toPath().resolve("docker-save-distroless"); + new Command("docker", "save", "gcr.io/distroless/java:latest", "-o", path.toString()).run(); + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithtarbase:gradle" + + System.nanoTime(); + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", + JibRunHelper.buildAndRunFromLocalBase(targetImage, "tar://" + path)); + } + @Test public void testBuild_failOffline() { String targetImage = diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle new file mode 100644 index 0000000000..a757ac89fe --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = System.getProperty("_BASE_IMAGE") + } + to { + image = System.getProperty("_TARGET_IMAGE") + credHelper = 'gcr' + } + container { + creationTime = 'EPOCH' + args = ['An argument.'] + ports = ['1000/tcp', '2000-2003/udp'] + labels = [key1:'value1', key2:'value2'] + volumes = ['/var/log', '/var/log2'] + workingDirectory = '/home' + extraClasspath = ['/d1','/d2'] + } + extraDirectories.paths = file('src/main/custom-extra-dir') +} diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7d1ec7adfe..8b2542bb56 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Support for local base images by prefixing `` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) + ### Changed ### Fixed diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 9f1fc25e17..9764e7cec4 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -43,7 +43,9 @@ import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** Integration tests for {@link BuildImageMojo}. */ public class BuildImageMojoIntegrationTest { @@ -67,6 +69,8 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private static String getTestImageReference(String label) { String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; return nameBase + label + System.nanoTime(); @@ -154,6 +158,22 @@ private static String buildAndRun( return output; } + private static String buildAndRunFromLocalBase( + Path projectRoot, String targetImage, String baseImage) + throws VerificationException, IOException, InterruptedException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); + verifier.setSystemProperty("_TARGET_IMAGE", targetImage); + verifier.setSystemProperty("_BASE_IMAGE", baseImage); + verifier.setSystemProperty("jib.allowInsecureRegistries", "true"); + verifier.setAutoclean(false); + verifier.addCliOption("-X"); + verifier.addCliOption("--file=pom-localbase.xml"); + verifier.executeGoals(Arrays.asList("clean", "compile")); + verifier.executeGoal("jib:build"); + return pullAndRunBuiltImage(targetImage); + } + private static String buildAndRunAdditionalTag( Path projectRoot, String imageReference, String additionalTag) throws VerificationException, InvalidImageReferenceException, IOException, @@ -317,6 +337,17 @@ private static void assertLayerSize(int expected, String imageReference) public void setUp() throws IOException, InterruptedException { // Pull distroless to local registry so we can test 'from' credentials localRegistry1.pullAndPushToLocal("gcr.io/distroless/java:latest", "distroless/java"); + + // Make sure resource file has a consistent value at the beginning of each test + // (testExecute_simple overwrites it) + Files.write( + simpleTestProject + .getProjectRoot() + .resolve("src") + .resolve("main") + .resolve("resources") + .resolve("world"), + "world".getBytes(StandardCharsets.UTF_8)); } @After @@ -375,6 +406,38 @@ public void testExecute_simple() assertLayerSize(8, targetImage); } + @Test + public void testBuild_dockerDaemonBase() + throws IOException, InterruptedException, VerificationException { + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithdockerdaemonbase:maven" + + System.nanoTime(); + + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", + buildAndRunFromLocalBase( + simpleTestProject.getProjectRoot(), + targetImage, + "docker://gcr.io/distroless/java:latest")); + } + + @Test + public void testBuild_tarBase() throws IOException, InterruptedException, VerificationException { + Path path = temporaryFolder.getRoot().toPath().resolve("docker-save-distroless"); + new Command("docker", "save", "gcr.io/distroless/java:latest", "-o", path.toString()).run(); + String targetImage = + IntegrationTestingConfiguration.getTestRepositoryLocation() + + "/simplewithtarbase:maven" + + System.nanoTime(); + + Assert.assertEquals( + "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", + buildAndRunFromLocalBase(simpleTestProject.getProjectRoot(), targetImage, "tar://" + path)); + } + @Test public void testExecute_failOffline() throws IOException { String targetImage = getTestImageReference("simpleimageoffline:maven"); diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml new file mode 100644 index 0000000000..bca546479a --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + com.test + hello-world + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_BASE_IMAGE} + + + ${_TARGET_IMAGE} + + + EPOCH + An argument. + + 1000/tcp + 2000-2003/udp + + + value1 + value2 + + + /var/log + /var/log2 + + /home + + ${project.basedir}/src/main/jib-custom + + + + + diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java index e5173f1374..248d3a3fe9 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidator.java @@ -66,11 +66,11 @@ public static Optional getImageCredential( // Warn if a system property is missing String missingProperty = "%s system property is set, but %s is not; attempting other authentication methods."; - if (!commandlinePassword.isEmpty() && commandlineUsername.isEmpty()) { + if (!commandlinePassword.isEmpty()) { logger.accept( LogEvent.warn(String.format(missingProperty, passwordProperty, usernameProperty))); } - if (!commandlineUsername.isEmpty() && commandlinePassword.isEmpty()) { + if (!commandlineUsername.isEmpty()) { logger.accept( LogEvent.warn(String.format(missingProperty, usernameProperty, passwordProperty))); } @@ -81,10 +81,10 @@ public static Optional getImageCredential( } String missingConfig = "%s is missing from build configuration; ignoring auth section."; - if (!Strings.isNullOrEmpty(auth.getPassword()) && Strings.isNullOrEmpty(auth.getUsername())) { + if (!Strings.isNullOrEmpty(auth.getPassword())) { logger.accept(LogEvent.warn(String.format(missingConfig, auth.getUsernameDescriptor()))); } - if (!Strings.isNullOrEmpty(auth.getUsername()) && Strings.isNullOrEmpty(auth.getPassword())) { + if (!Strings.isNullOrEmpty(auth.getUsername())) { logger.accept(LogEvent.warn(String.format(missingConfig, auth.getPasswordDescriptor()))); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 8f1237b16e..977c426b2f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.Ports; @@ -184,9 +185,6 @@ static JibContainerBuilder processCommonConfiguration( JibSystemProperties.checkHttpTimeoutProperty(); JibSystemProperties.checkProxyPortProperty(); - ImageReference baseImageReference = - ImageReference.parse(getBaseImage(rawConfiguration, projectProperties)); - if (JibSystemProperties.sendCredentialsOverHttp()) { projectProperties.log( LogEvent.warn( @@ -194,27 +192,14 @@ static JibContainerBuilder processCommonConfiguration( + "enable this on a public network!")); } - // 1. configure containerizer configureContainerizer(containerizer, rawConfiguration, projectProperties); - RegistryImage baseImage = RegistryImage.named(baseImageReference); - // 2. create and configure credential retrievers for base image - configureCredentialRetrievers( - rawConfiguration, - projectProperties, - baseImage, - baseImageReference, - PropertyNames.FROM_AUTH_USERNAME, - PropertyNames.FROM_AUTH_PASSWORD, - rawConfiguration.getFromAuth(), - inferredAuthProvider, - rawConfiguration.getFromCredHelper().orElse(null)); - - // 3. create and configure JibContainerBuilder + // Create and configure JibContainerBuilder BiFunction modificationTimeProvider = createModificationTimeProvider(rawConfiguration.getFilesModificationTime()); JavaContainerBuilder javaContainerBuilder = - JavaContainerBuilder.from(baseImage) + getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider) .setAppRoot(getAppRootChecked(rawConfiguration, projectProperties)) .setModificationTimeProvider(modificationTimeProvider); JibContainerBuilder jibContainerBuilder = @@ -254,6 +239,60 @@ static JibContainerBuilder processCommonConfiguration( return jibContainerBuilder; } + /** + * Returns a {@link JavaContainerBuilder} with the correctly parsed base image configuration. + * + * @param rawConfiguration contains the base image configuration + * @param projectProperties used for providing additional information + * @param inferredAuthProvider provides inferred auths for registry images + * @return a new {@link JavaContainerBuilder} with the configured base image + * @throws IncompatibleBaseImageJavaVersionException when the Java version in the base image is + * incompatible with the Java version of the application to be containerized + * @throws InvalidImageReferenceException if the base image configuration can't be parsed + * @throws FileNotFoundException if a credential helper can't be found + */ + @VisibleForTesting + static JavaContainerBuilder getJavaContainerBuilderWithBaseImage( + RawConfiguration rawConfiguration, + ProjectProperties projectProperties, + InferredAuthProvider inferredAuthProvider) + throws IncompatibleBaseImageJavaVersionException, InvalidImageReferenceException, + FileNotFoundException { + // Use image configuration as-is if it's a local base image + String baseImageConfig = + rawConfiguration.getFromImage().orElse(getDefaultBaseImage(projectProperties)); + if (baseImageConfig.startsWith(Jib.TAR_IMAGE_PREFIX)) { + return JavaContainerBuilder.from(baseImageConfig); + } + + // Verify Java version is compatible + String prefixRemoved = baseImageConfig.replaceFirst(".*://", ""); + int javaVersion = projectProperties.getMajorJavaVersion(); + if (isKnownDistrolessJava8Image(prefixRemoved) && javaVersion > 8) { + throw new IncompatibleBaseImageJavaVersionException(8, javaVersion); + } + if (isKnownDistrolessJava11Image(prefixRemoved) && javaVersion > 11) { + throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); + } + + if (baseImageConfig.startsWith(Jib.DOCKER_DAEMON_IMAGE_PREFIX)) { + return JavaContainerBuilder.from(baseImageConfig); + } + ImageReference baseImageReference = ImageReference.parse(prefixRemoved); + RegistryImage baseImage = RegistryImage.named(baseImageReference); + configureCredentialRetrievers( + rawConfiguration, + projectProperties, + baseImage, + baseImageReference, + PropertyNames.FROM_AUTH_USERNAME, + PropertyNames.FROM_AUTH_PASSWORD, + rawConfiguration.getFromAuth(), + inferredAuthProvider, + rawConfiguration.getFromCredHelper().orElse(null)); + return JavaContainerBuilder.from(baseImage); + } + /** * Compute the container entrypoint, in this order: * @@ -335,30 +374,15 @@ static List computeEntrypoint( * {@code "gcr.io/distroless/java/jetty"} for WAR projects or {@code "gcr.io/distroless/java"} for * non-WAR. * - * @param rawConfiguration raw configuration data * @param projectProperties used for providing additional information * @return the base image * @throws IncompatibleBaseImageJavaVersionException when the Java version in the base image is * incompatible with the Java version of the application to be containerized */ @VisibleForTesting - static String getBaseImage(RawConfiguration rawConfiguration, ProjectProperties projectProperties) + static String getDefaultBaseImage(ProjectProperties projectProperties) throws IncompatibleBaseImageJavaVersionException { int javaVersion = projectProperties.getMajorJavaVersion(); - - if (rawConfiguration.getFromImage().isPresent()) { - String baseImage = rawConfiguration.getFromImage().get(); - - if (isKnownDistrolessJava8Image(baseImage) && javaVersion > 8) { - throw new IncompatibleBaseImageJavaVersionException(8, javaVersion); - } - if (isKnownDistrolessJava11Image(baseImage) && javaVersion > 11) { - throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); - } - return baseImage; - } - - // Base image not configured; auto-pick Distroless. if (javaVersion <= 8) { return projectProperties.isWarProject() ? "gcr.io/distroless/java/jetty:java8" diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 7623c04421..cda9e21ddd 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -29,9 +29,11 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; @@ -51,6 +53,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; @@ -86,15 +89,17 @@ private static void assertExtractionPathsUnordered( } @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private RawConfiguration rawConfiguration; @Mock private ProjectProperties projectProperties; + @Mock private InferredAuthProvider inferredAuthProvider; @Mock private Containerizer containerizer; @Mock private AuthProperty authProperty; @Mock private Consumer logger; @Before - public void setUp() throws IOException, InvalidImageReferenceException { + public void setUp() throws IOException, InvalidImageReferenceException, InferredAuthException { Mockito.when(rawConfiguration.getFromAuth()).thenReturn(authProperty); Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/app"); @@ -113,6 +118,8 @@ public void setUp() throws IOException, InvalidImageReferenceException { .thenReturn(Jib.from("base")); Mockito.when(projectProperties.isOffline()).thenReturn(false); + Mockito.when(inferredAuthProvider.inferAuth(Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(containerizer.setToolName(Mockito.anyString())).thenReturn(containerizer); Mockito.when(containerizer.setAllowInsecureRegistries(Mockito.anyBoolean())) .thenReturn(containerizer); @@ -207,16 +214,6 @@ public void testPluginConfigurationProcessor_cacheDirectorySystemProperties() Mockito.verify(containerizer).setApplicationLayersCache(Paths.get("/new/application/cache")); } - @Test - public void testGetBaseImage_warProject() - throws IncompatibleBaseImageJavaVersionException, NumberFormatException { - Mockito.when(projectProperties.isWarProject()).thenReturn(true); - - Assert.assertEquals( - "gcr.io/distroless/java/jetty:java8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); - } - @Test public void testEntrypoint() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, @@ -613,89 +610,109 @@ public void testGetWorkingDirectoryChecked_notAbsolute() { } @Test - public void testGetBaseImage_chooseJava8Distroless() + public void testGetDefaultBaseImage_nonWarPackaging() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.isWarProject()).thenReturn(false); + + Assert.assertEquals( + "gcr.io/distroless/java:8", + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); + } + + @Test + public void testGetDefaultBaseImage_warProject() + throws IncompatibleBaseImageJavaVersionException { + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + Assert.assertEquals( + "gcr.io/distroless/java/jetty:java8", + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); + } + + @Test + public void testGetDefaultBaseImage_chooseJava8Distroless() throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(6); Assert.assertEquals( "gcr.io/distroless/java:8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(7); Assert.assertEquals( "gcr.io/distroless/java:8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(8); Assert.assertEquals( "gcr.io/distroless/java:8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); } @Test - public void testGetBaseImage_chooseJava11Distroless() + public void testGetDefaultBaseImage_chooseJava11Distroless() throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(9); Assert.assertEquals( "gcr.io/distroless/java:11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(10); Assert.assertEquals( "gcr.io/distroless/java:11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); Assert.assertEquals( "gcr.io/distroless/java:11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); } @Test - public void testGetBaseImage_chooseJava8JettyDistroless() + public void testGetDefaultBaseImage_chooseJava8JettyDistroless() throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(6); Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( "gcr.io/distroless/java/jetty:java8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(7); Assert.assertEquals( "gcr.io/distroless/java/jetty:java8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(8); Assert.assertEquals( "gcr.io/distroless/java/jetty:java8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); } @Test - public void testGetBaseImage_chooseJava11JettyDistroless() + public void testGetDefaultBaseImage_chooseJava11JettyDistroless() throws IncompatibleBaseImageJavaVersionException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(9); Mockito.when(projectProperties.isWarProject()).thenReturn(true); Assert.assertEquals( "gcr.io/distroless/java/jetty:java11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(10); Assert.assertEquals( "gcr.io/distroless/java/jetty:java11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); Assert.assertEquals( "gcr.io/distroless/java/jetty:java11", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties)); } @Test - public void testGetBaseImage_projectHigherThanJava11() { + public void testGetDefaultBaseImage_projectHigherThanJava11() { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(12); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getDefaultBaseImage(projectProperties); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); @@ -704,13 +721,58 @@ public void testGetBaseImage_projectHigherThanJava11() { } @Test - public void testGetBaseImage_incompatibleJava8BaseImage() { + public void testGetJavaContainerBuilderWithBaseImage_dockerBase() + throws IncompatibleBaseImageJavaVersionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("docker://ima.ge/name")); + ImageConfiguration result = getCommonImageConfiguration(); + Assert.assertEquals("ima.ge/name", result.getImage().toString()); + Assert.assertTrue(result.getDockerClient().isPresent()); + Assert.assertFalse(result.getTarPath().isPresent()); + } + + @Test + public void testGetJavaContainerBuilderWithBaseImage_tarBase() + throws IncompatibleBaseImageJavaVersionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("tar:///path/to.tar")); + ImageConfiguration result = getCommonImageConfiguration(); + Assert.assertEquals(Paths.get("/path/to.tar"), result.getTarPath().get()); + Assert.assertFalse(result.getDockerClient().isPresent()); + } + + @Test + public void testGetJavaContainerBuilderWithBaseImage_registry() + throws IncompatibleBaseImageJavaVersionException, InvalidImageReferenceException, IOException, + CacheDirectoryCreationException { + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("ima.ge/name")); + ImageConfiguration result = getCommonImageConfiguration(); + Assert.assertEquals("ima.ge/name", result.getImage().toString()); + Assert.assertFalse(result.getDockerClient().isPresent()); + Assert.assertFalse(result.getTarPath().isPresent()); + } + + @Test + public void testGetJavaContainerBuilderWithBaseImage_registryWithPrefix() + throws IncompatibleBaseImageJavaVersionException, InvalidImageReferenceException, IOException, + CacheDirectoryCreationException { + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("registry://ima.ge/name")); + ImageConfiguration result = getCommonImageConfiguration(); + Assert.assertEquals("ima.ge/name", result.getImage().toString()); + Assert.assertFalse(result.getDockerClient().isPresent()); + Assert.assertFalse(result.getTarPath().isPresent()); + } + + @Test + public void testGetJavaContainerBuilderWithBaseImage_incompatibleJava8BaseImage() + throws InvalidImageReferenceException, FileNotFoundException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); Mockito.when(rawConfiguration.getFromImage()) .thenReturn(Optional.of("gcr.io/distroless/java:8")); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); @@ -720,7 +782,8 @@ public void testGetBaseImage_incompatibleJava8BaseImage() { Mockito.when(rawConfiguration.getFromImage()) .thenReturn(Optional.of("gcr.io/distroless/java:latest")); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); @@ -729,13 +792,15 @@ public void testGetBaseImage_incompatibleJava8BaseImage() { } @Test - public void testGetBaseImage_incompatibleJava11BaseImage() { + public void testGetJavaContainerBuilderWithBaseImage_incompatibleJava11BaseImage() + throws InvalidImageReferenceException, FileNotFoundException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(15); Mockito.when(rawConfiguration.getFromImage()) .thenReturn(Optional.of("gcr.io/distroless/java:11")); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); @@ -744,13 +809,15 @@ public void testGetBaseImage_incompatibleJava11BaseImage() { } @Test - public void testGetBaseImage_incompatibleJava8JettyBaseImage() { + public void testGetJavaContainerBuilderWithBaseImage_incompatibleJava8JettyBaseImage() + throws InvalidImageReferenceException, FileNotFoundException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(11); Mockito.when(rawConfiguration.getFromImage()) .thenReturn(Optional.of("gcr.io/distroless/java/jetty:java8")); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(8, ex.getBaseImageMajorJavaVersion()); @@ -759,13 +826,15 @@ public void testGetBaseImage_incompatibleJava8JettyBaseImage() { } @Test - public void testGetBaseImage_incompatibleJava11JettyBaseImage() { + public void testGetJavaContainerBuilderWithBaseImage_incompatibleJava11JettyBaseImage() + throws InvalidImageReferenceException, FileNotFoundException { Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(15); Mockito.when(rawConfiguration.getFromImage()) .thenReturn(Optional.of("gcr.io/distroless/java/jetty:java11")); try { - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties); + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); Assert.fail(); } catch (IncompatibleBaseImageJavaVersionException ex) { Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); @@ -773,34 +842,6 @@ public void testGetBaseImage_incompatibleJava11JettyBaseImage() { } } - @Test - public void testGetBaseImage_defaultNonWarPackaging() - throws IncompatibleBaseImageJavaVersionException { - Mockito.when(projectProperties.isWarProject()).thenReturn(false); - - Assert.assertEquals( - "gcr.io/distroless/java:8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); - } - - @Test - public void testGetBaseImage_defaultWarProject() - throws IncompatibleBaseImageJavaVersionException { - Mockito.when(projectProperties.isWarProject()).thenReturn(true); - - Assert.assertEquals( - "gcr.io/distroless/java/jetty:java8", - PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); - } - - @Test - public void testGetBaseImage_nonDefault() throws IncompatibleBaseImageJavaVersionException { - Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("tomcat")); - - Assert.assertEquals( - "tomcat", PluginConfigurationProcessor.getBaseImage(rawConfiguration, projectProperties)); - } - @Test public void testGetValidVolumesList() throws InvalidContainerVolumeException { Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("/some/root")); @@ -890,6 +931,17 @@ public void testGetCreationTime_invalidValue() { } } + private ImageConfiguration getCommonImageConfiguration() + throws IncompatibleBaseImageJavaVersionException, IOException, InvalidImageReferenceException, + CacheDirectoryCreationException { + return getBuildConfiguration( + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider) + .addClasses(temporaryFolder.getRoot().toPath()) + .toContainerBuilder()) + .getBaseImageConfiguration(); + } + private JibContainerBuilder processCommonConfiguration() throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, From 7439c41e6e1362edb0beade4a4ebb98a6dd39d16 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Sep 2019 15:05:22 -0400 Subject: [PATCH 0710/2020] Fix footer display (#1953) --- .../cloud/tools/jib/registry/BlobPusher.java | 2 +- .../common/logging/AnsiLoggerWithFooter.java | 47 +++++++--------- .../logging/AnsiLoggerWithFooterTest.java | 56 +++++++++++++------ 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 3d04288d5b..917e573339 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -217,7 +217,7 @@ RegistryEndpointProvider> initializer() { /** * @param location the upload URL - * @param blobProgressListener the listener for {@link Blob} push progress + * @param writtenByteCountListener the listener for {@link Blob} push progress (written bytes) * @return a {@link RegistryEndpointProvider} for writing the BLOB to an upload location */ RegistryEndpointProvider writer(URL location, Consumer writtenByteCountListener) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index 07e264e6e3..4326b0cbb6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -37,8 +37,8 @@ class AnsiLoggerWithFooter implements ConsoleLogger { */ private static final int MAX_FOOTER_WIDTH = 50; - /** ANSI escape sequence for moving the cursor up one line. */ - private static final String CURSOR_UP_SEQUENCE = "\033[1A"; + /** ANSI escape sequence template for moving the cursor up multiple lines. */ + private static final String CURSOR_UP_SEQUENCE_TEMPLATE = "\033[%dA"; /** ANSI escape sequence for erasing to end of display. */ private static final String ERASE_DISPLAY_BELOW = "\033[0J"; @@ -79,8 +79,9 @@ static List truncateToMaxWidth(List lines) { /** * Creates a new {@link AnsiLoggerWithFooter}. * - * @param messageConsumers map from each {@link Level} to a log message {@link Consumer - * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are logged in a sequential, deterministic order + * @param messageConsumers map from each {@link Level} to a corresponding message logger + * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are + * logged in a sequential, deterministic order */ AnsiLoggerWithFooter( ImmutableMap> messageConsumers, @@ -102,15 +103,15 @@ public void log(Level logLevel, String message) { singleThreadedExecutor.execute( () -> { - boolean didErase = eraseFooter(); - - // If a previous footer was erased, the message needs to go up a line. - String messagePrefix = didErase ? CURSOR_UP_SEQUENCE : ""; - messageConsumer.accept(messagePrefix + message); - - for (String footerLine : footerLines) { - lifecycleConsumer.accept(BOLD + footerLine + UNBOLD); + if (eraseFooter()) { + // If a previous footer was erased, the message needs to go up a line. However, we go up + // two lines (and then down one line by calling "accept()" once) to correct an issue in + // Maven: https://github.com/GoogleContainerTools/jib/issues/1952 + messageConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); } + messageConsumer.accept(message); + + footerLines.forEach(line -> lifecycleConsumer.accept(BOLD + line + UNBOLD)); }); } @@ -132,16 +133,15 @@ public void setFooter(List newFooterLines) { singleThreadedExecutor.execute( () -> { - boolean didErase = eraseFooter(); - - // If a previous footer was erased, the first new footer line needs to go up a line. - String newFooterPrefix = didErase ? CURSOR_UP_SEQUENCE : ""; - - for (String newFooterLine : truncatedNewFooterLines) { - lifecycleConsumer.accept(newFooterPrefix + BOLD + newFooterLine + UNBOLD); - newFooterPrefix = ""; + if (eraseFooter()) { + // If a previous footer was erased, the first new footer line needs to go up a line. + // However, we go up two lines (and then down one line by calling "accept()" once to + // correct an issue in Maven: https://github.com/GoogleContainerTools/jib/issues/1952 + lifecycleConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); } + truncatedNewFooterLines.forEach(line -> lifecycleConsumer.accept(BOLD + line + UNBOLD)); + footerLines = truncatedNewFooterLines; }); } @@ -160,12 +160,7 @@ private boolean eraseFooter() { StringBuilder footerEraserBuilder = new StringBuilder(); // Moves the cursor up to the start of the footer. - // TODO: Optimize to single init. - for (int i = 0; i < footerLines.size(); i++) { - // Moves cursor up. - footerEraserBuilder.append(CURSOR_UP_SEQUENCE); - } - + footerEraserBuilder.append(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, footerLines.size())); // Erases everything below cursor. footerEraserBuilder.append(ERASE_DISPLAY_BELOW); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index 9d1597f88e..84f3965fea 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -132,22 +132,30 @@ public void testLog_sameFooter() { Assert.assertEquals( Arrays.asList( - "\033[1mfooter\033[0m", - "\033[1A\033[0J", - "\033[1Amessage", - "\033[1mfooter\033[0m", - "\033[1A\033[0J", - "\033[1Aanother message", - "\033[1mfooter\033[0m"), + "\033[1mfooter\033[0m", // single-line footer in bold + + // now triggered by logging a message + "\033[1A\033[0J", // cursor up and erase to the end + "\033[2A", // up two lines + "message", + "\033[1mfooter\033[0m", // footer + + // by logging another message + "\033[1A\033[0J", // cursor up and erase + "\033[2A", // up two + "another message", + "\033[1mfooter\033[0m"), // footer messages); Assert.assertEquals( Arrays.asList( Level.LIFECYCLE, Level.LIFECYCLE, Level.INFO, + Level.INFO, Level.LIFECYCLE, Level.LIFECYCLE, Level.INFO, + Level.INFO, Level.LIFECYCLE), levels); } @@ -163,28 +171,40 @@ public void testLog_changingFooter() { Assert.assertEquals( Arrays.asList( - "\033[1mfooter\033[0m", - "\033[1A\033[0J", - "\033[1Amessage", - "\033[1mfooter\033[0m", - "\033[1A\033[0J", - "\033[1A\033[1mtwo line\033[0m", - "\033[1mfooter\033[0m", - "\033[1A\033[1A\033[0J", - "\033[1Aanother message", - "\033[1mtwo line\033[0m", - "\033[1mfooter\033[0m"), + "\033[1mfooter\033[0m", // single-line footer in bold + + // now triggered by logging a warning + "\033[1A\033[0J", // cursor up and erase to the end + "\033[2A", // up two lines + "message", + "\033[1mfooter\033[0m", // footer + + // by setting a two-line footer + "\033[1A\033[0J", // cursor up and erase + "\033[2A", // up two lines + "\033[1mtwo line\033[0m", // footer line 1 + "\033[1mfooter\033[0m", // footer line 2 + + // by logging another warning + "\033[2A\033[0J", // cursor up twice (to erase two-line footer) and erase + "\033[2A", // up two lines + "another message", + "\033[1mtwo line\033[0m", // footer line 1 + "\033[1mfooter\033[0m"), // footer line 2 messages); Assert.assertEquals( Arrays.asList( Level.LIFECYCLE, Level.LIFECYCLE, Level.WARN, + Level.WARN, Level.LIFECYCLE, Level.LIFECYCLE, Level.LIFECYCLE, Level.LIFECYCLE, Level.LIFECYCLE, + Level.LIFECYCLE, + Level.WARN, Level.WARN, Level.LIFECYCLE, Level.LIFECYCLE), From f775f72dfa8055697ca4f542c18430a7dab893e9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Sep 2019 17:48:34 -0400 Subject: [PATCH 0711/2020] Rework I/O error handling (#1950) --- .../registry/RegistryBrokenPipeException.java | 31 ----------- .../jib/registry/RegistryEndpointCaller.java | 31 +++++++---- .../registry/RegistryNoResponseException.java | 27 ---------- .../registry/RegistryEndpointCallerTest.java | 52 ++++++++++++++++++- 4 files changed, 71 insertions(+), 70 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java deleted file mode 100644 index 76508c4184..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryBrokenPipeException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry; - -import com.google.cloud.tools.jib.api.RegistryException; - -/** Thrown when the registry shut down the connection. */ -class RegistryBrokenPipeException extends RegistryException { - - RegistryBrokenPipeException(Throwable cause) { - super( - "I/O error due to broken pipe: the server shut down the connection. " - + "Check the server log if possible. This could also be a proxy issue. For example," - + "a proxy may prevent sending packets that are too large.", - cause); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 2cf3529efc..64aa2127c5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -42,7 +42,6 @@ import java.util.function.Function; import javax.annotation.Nullable; import javax.net.ssl.SSLException; -import org.apache.http.NoHttpResponseException; /** * Makes requests to a registry endpoint. @@ -160,7 +159,22 @@ static boolean isBrokenPipe(IOException original) { * @throws RegistryException for known exceptions when interacting with the registry */ T call() throws IOException, RegistryException { - return callWithAllowInsecureRegistryHandling(initialRequestUrl); + try { + return callWithAllowInsecureRegistryHandling(initialRequestUrl); + + } catch (IOException ex) { + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + logError("I/O error for image [" + registry + "/" + repository + "]:"); + logError(" " + ex.getMessage()); + if (isBrokenPipe(ex)) { + logError( + "broken pipe: the server shut down the connection. Check the server log if possible. " + + "This could also be a proxy issue. For example, a proxy may prevent sending " + + "packets that are too large."); + } + throw ex; + } } private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, RegistryException { @@ -304,14 +318,6 @@ private T call(URL url, Function connectionFactory) throw httpResponseException; } } - } catch (NoHttpResponseException ex) { - throw new RegistryNoResponseException(ex); - - } catch (IOException ex) { - if (isBrokenPipe(ex)) { - throw new RegistryBrokenPipeException(ex); - } - throw ex; } } @@ -339,4 +345,9 @@ RegistryErrorException newRegistryErrorException(HttpResponseException httpRespo return registryErrorExceptionBuilder.build(); } + + /** Logs error message in red. */ + private void logError(String message) { + eventHandlers.dispatch(LogEvent.error("\u001B[31;1m" + message + "\u001B[0m")); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java deleted file mode 100644 index 70513c21ee..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryNoResponseException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry; - -import com.google.cloud.tools.jib.api.RegistryException; - -/** Thrown when a registry did not respond. */ -class RegistryNoResponseException extends RegistryException { - - RegistryNoResponseException(Throwable cause) { - super(cause); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index febd365a10..47779ab5a3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -308,8 +308,8 @@ public void testCall_noHttpResponse() throws IOException, RegistryException { secureEndpointCaller.call(); Assert.fail("Call should have failed"); - } catch (RegistryNoResponseException ex) { - Assert.assertSame(mockNoHttpResponseException, ex.getCause()); + } catch (NoHttpResponseException ex) { + Assert.assertSame(mockNoHttpResponseException, ex); } } @@ -451,6 +451,54 @@ public void testCall_disallowInsecure() throws IOException, RegistryException { } } + @Test + public void testCall_logErrorOnIoExceptions() throws IOException, RegistryException { + IOException ioException = new IOException("detailed exception message"); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(ioException); + + try { + secureEndpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Assert.assertSame(ioException, ex); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m detailed exception message\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } + } + + @Test + public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryException { + IOException ioException = new IOException("this is due to broken pipe"); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) + .thenThrow(ioException); + + try { + secureEndpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Assert.assertSame(ioException, ex); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m this is due to broken pipe\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error( + "\u001B[31;1mbroken pipe: the server shut down the connection. Check the server " + + "log if possible. This could also be a proxy issue. For example, a proxy " + + "may prevent sending packets that are too large.\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } + } + @Test public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); From b3b62e0dcebbc9e8b2d4885385903ff5606f25da Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 6 Sep 2019 12:24:00 -0400 Subject: [PATCH 0712/2020] Dispatch progress events in local base image steps (#1951) --- .../jib/builder/steps/ExtractTarStep.java | 171 +++++++++++------- .../jib/builder/steps/SaveDockerStep.java | 25 ++- .../tools/jib/builder/steps/StepsRunner.java | 12 +- .../cloud/tools/jib/docker/DockerClient.java | 36 +++- .../jib/builder/steps/ExtractTarStepTest.java | 34 +++- .../tools/jib/docker/DockerClientTest.java | 52 ++++-- 6 files changed, 237 insertions(+), 93 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java index efc9a5d981..775e7ba787 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java @@ -21,10 +21,15 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; @@ -76,84 +81,116 @@ static boolean isGzipped(Path path) throws IOException { } } + private final BuildConfiguration buildConfiguration; private final Path tarPath; - private final Path destination; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - ExtractTarStep(Path tarPath, Path destination) { + ExtractTarStep( + BuildConfiguration buildConfiguration, + Path tarPath, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + this.buildConfiguration = buildConfiguration; this.tarPath = tarPath; - this.destination = destination; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; } @Override public LocalImage call() throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException { - Files.createDirectories(destination); - FileOperations.deleteRecursiveOnExit(destination); - TarExtractor.extract(tarPath, destination); - - InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); - DockerManifestEntryTemplate loadManifest = - new ObjectMapper() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) - .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; - manifestStream.close(); - ContainerConfigurationTemplate configurationTemplate = - JsonTemplateMapper.readJsonFromFile( - destination.resolve(loadManifest.getConfig()), ContainerConfigurationTemplate.class); - - List layerFiles = loadManifest.getLayerFiles(); - if (configurationTemplate.getLayerCount() != layerFiles.size()) { - throw new LayerCountMismatchException( - "Invalid base image format: manifest contains " - + layerFiles.size() - + " layers, but container configuration contains " - + configurationTemplate.getLayerCount() - + " layers"); - } + Path destination = Files.createTempDirectory("jib-extract-tar"); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), + "Extracting tar " + tarPath + " into " + destination)) { + FileOperations.deleteRecursiveOnExit(destination); + TarExtractor.extract(tarPath, destination); + + InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); + DockerManifestEntryTemplate loadManifest = + new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; + manifestStream.close(); + ContainerConfigurationTemplate configurationTemplate = + JsonTemplateMapper.readJsonFromFile( + destination.resolve(loadManifest.getConfig()), ContainerConfigurationTemplate.class); + + List layerFiles = loadManifest.getLayerFiles(); + if (configurationTemplate.getLayerCount() != layerFiles.size()) { + throw new LayerCountMismatchException( + "Invalid base image format: manifest contains " + + layerFiles.size() + + " layers, but container configuration contains " + + configurationTemplate.getLayerCount() + + " layers"); + } + + // Check the first layer to see if the layers are compressed already. 'docker save' output is + // uncompressed, but a jib-built tar has compressed layers. + boolean layersAreCompressed = + layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); + + // Process layer blobs + // TODO: Optimize; compressing/calculating layer digests is slow + // e.g. parallelize, cache layers, faster compression method + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "processing base image layers", layerFiles.size())) { + List layers = new ArrayList<>(layerFiles.size()); + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + + List childProgressFactories = new ArrayList<>(); + for (String ignored1 : layerFiles) { + childProgressFactories.add(progressEventDispatcher.newChildProducer()); + } - // Check the first layer to see if the layers are compressed already. 'docker save' output is - // uncompressed, but a jib-built tar has compressed layers. - boolean layersAreCompressed = - layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); - - // Process layer blobs - // TODO: Optimize; compressing/calculating layer digests is slow - List layers = new ArrayList<>(); - V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); - for (int index = 0; index < layerFiles.size(); index++) { - Path file = destination.resolve(layerFiles.get(index)); - - // Compress layers if necessary and calculate the digest/size - Blob blob = Blobs.from(file); - if (!layersAreCompressed) { - Path compressedFile = destination.resolve(layerFiles.get(index) + ".compressed"); - try (GZIPOutputStream compressorStream = - new GZIPOutputStream(Files.newOutputStream(compressedFile))) { - blob.writeTo(compressorStream); + for (int index = 0; index < layerFiles.size(); index++) { + Path file = destination.resolve(layerFiles.get(index)); + + // Compress layers if necessary and calculate the digest/size + Blob blob = Blobs.from(file); + try (ProgressEventDispatcher childDispatcher = + childProgressFactories + .get(index) + .create("compressing " + file, Files.size(file)); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { + if (!layersAreCompressed) { + Path compressedFile = destination.resolve(layerFiles.get(index) + ".compressed"); + try (GZIPOutputStream compressorStream = + new GZIPOutputStream(Files.newOutputStream(compressedFile)); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { + blob.writeTo(notifyingOutputStream); + } + blob = Blobs.from(compressedFile); + } + } + BlobDescriptor blobDescriptor = blob.writeTo(ByteStreams.nullOutputStream()); + + // 'manifest' contains the layer files in the same order as the diff ids in + // 'configuration', so we don't need to recalculate those. + // https://containers.gitbook.io/build-containers-the-hard-way/#docker-load-format + CachedLayer layer = + CachedLayer.builder() + .setLayerBlob(blob) + .setLayerDigest(blobDescriptor.getDigest()) + .setLayerSize(blobDescriptor.getSize()) + .setLayerDiffId(configurationTemplate.getLayerDiffId(index)) + .build(); + + layers.add(new PreparedLayer.Builder(layer).build()); + v22Manifest.addLayer(blobDescriptor.getSize(), blobDescriptor.getDigest()); + progressEventDispatcher.dispatchProgress(1); } - blob = Blobs.from(compressedFile); + + BlobDescriptor configDescriptor = + Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); + v22Manifest.setContainerConfiguration( + configDescriptor.getSize(), configDescriptor.getDigest()); + Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); + return new LocalImage(image, layers); } - BlobDescriptor blobDescriptor = blob.writeTo(ByteStreams.nullOutputStream()); - - // 'manifest' contains the layer files in the same order as the diff ids in 'configuration', - // so we don't need to recalculate those. - // https://containers.gitbook.io/build-containers-the-hard-way/#docker-load-format - CachedLayer layer = - CachedLayer.builder() - .setLayerBlob(blob) - .setLayerDigest(blobDescriptor.getDigest()) - .setLayerSize(blobDescriptor.getSize()) - .setLayerDiffId(configurationTemplate.getLayerDiffId(index)) - .build(); - - layers.add(new PreparedLayer.Builder(layer).build()); - v22Manifest.addLayer(blobDescriptor.getSize(), blobDescriptor.getDigest()); } - - BlobDescriptor configDescriptor = - Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); - v22Manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); - Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); - return new LocalImage(image, layers); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java index 789781aa35..0329c61360 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java @@ -17,8 +17,11 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.filesystem.FileOperations; import java.io.IOException; import java.nio.file.Files; @@ -30,10 +33,15 @@ public class SaveDockerStep implements Callable { private final BuildConfiguration buildConfiguration; private final DockerClient dockerClient; + private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - SaveDockerStep(BuildConfiguration buildConfiguration, DockerClient dockerClient) { + SaveDockerStep( + BuildConfiguration buildConfiguration, + DockerClient dockerClient, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) { this.buildConfiguration = buildConfiguration; this.dockerClient = dockerClient; + this.progressEventDispatcherFactory = progressEventDispatcherFactory; } @Override @@ -42,7 +50,18 @@ public Path call() throws IOException, InterruptedException { FileOperations.deleteRecursiveOnExit(outputDir); Path outputPath = outputDir.resolve("out.tar"); ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); - dockerClient.save(imageReference, outputPath); - return outputPath; + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), + "Saving " + imageReference + " from Docker daemon")) { + long size = dockerClient.sizeOf(imageReference); + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("saving base image " + imageReference, size); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { + dockerClient.save(imageReference, outputPath, throttledProgressReporter); + } + return outputPath; + } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 647f0cf512..a16624d0d1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -33,7 +33,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -230,20 +229,27 @@ private void authenticatePush() { } private void saveDocker() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + Optional dockerClient = buildConfiguration.getBaseImageConfiguration().getDockerClient(); Preconditions.checkArgument(dockerClient.isPresent()); results.tarPath = - executorService.submit(new SaveDockerStep(buildConfiguration, dockerClient.get())); + executorService.submit( + new SaveDockerStep( + buildConfiguration, dockerClient.get(), childProgressDispatcherFactory)); } private void extractTar() { + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); Future localImageFuture = executorService.submit( () -> new ExtractTarStep( - results.tarPath.get(), Files.createTempDirectory("jib-extract-tar")) + buildConfiguration, results.tarPath.get(), childProgressDispatcherFactory) .call()); results.baseImageAndAuth = executorService.submit( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index ddd0b9d238..55e1790040 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -17,19 +17,26 @@ package com.google.cloud.tools.jib.docker; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; /** Calls out to the {@code docker} CLI. */ @@ -165,13 +172,21 @@ public String load(ImageTarball imageTarball) throws InterruptedException, IOExc * href="https://docs.docker.com/engine/reference/commandline/save/">https://docs.docker.com/engine/reference/commandline/save
* @param imageReference the image to save * @param outputPath the destination path to save the output tarball + * @param writtenByteCountListener callback to call when bytes are saved * @throws InterruptedException if the 'docker save' process is interrupted * @throws IOException if creating the tarball fails */ - public void save(ImageReference imageReference, Path outputPath) + public void save( + ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) throws InterruptedException, IOException { - // Runs 'docker save'. - Process dockerProcess = docker("save", imageReference.toString(), "-o", outputPath.toString()); + Process dockerProcess = docker("save", imageReference.toString()); + try (InputStream stdout = new BufferedInputStream(dockerProcess.getInputStream()); + NotifyingOutputStream fileStream = + new NotifyingOutputStream( + new BufferedOutputStream(Files.newOutputStream(outputPath)), + writtenByteCountListener)) { + ByteStreams.copy(stdout, fileStream); + } if (dockerProcess.waitFor() != 0) { try (InputStreamReader stderr = @@ -208,6 +223,21 @@ public void tag(ImageReference originalImageReference, ImageReference newImageRe } } + /** + * Gets the size of an image in the Docker daemon. + * + * @param imageReference the image to find the size of + * @return the size in bytes + * @throws IOException if an I/O exception occurs + */ + public long sizeOf(ImageReference imageReference) throws IOException { + Process sizeProcess = docker("inspect", "-f", "{{.Size}}", imageReference.toString()); + return Long.parseLong( + CharStreams.toString( + new InputStreamReader(sizeProcess.getInputStream(), StandardCharsets.UTF_8)) + .trim()); + } + /** Runs a {@code docker} command. */ private Process docker(String... subCommand) throws IOException { return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java index 664743ab89..729806da5a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java @@ -16,7 +16,10 @@ package com.google.cloud.tools.jib.builder.steps; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; import com.google.common.io.Resources; @@ -25,25 +28,50 @@ import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Assert; +import org.junit.Before; 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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +@RunWith(MockitoJUnitRunner.class) public class ExtractTarStepTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private BuildConfiguration buildConfiguration; + @Mock private EventHandlers eventHandlers; + @Mock private ProgressEventDispatcher.Factory progressEventDispatcherFactory; + @Mock private ProgressEventDispatcher progressEventDispatcher; + @Mock private ProgressEventDispatcher.Factory childFactory; + @Mock private ProgressEventDispatcher childDispatcher; + private static Path getResource(String resource) throws URISyntaxException { return Paths.get(Resources.getResource(resource).toURI()); } + @Before + public void setup() { + Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(eventHandlers); + Mockito.when(progressEventDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(progressEventDispatcher); + Mockito.when(progressEventDispatcher.newChildProducer()).thenReturn(childFactory); + Mockito.when(childFactory.create(Mockito.anyString(), Mockito.anyLong())) + .thenReturn(childDispatcher); + } + @Test public void testCall_validDocker() throws URISyntaxException, LayerCountMismatchException, BadContainerConfigurationFormatException, IOException { Path dockerBuild = getResource("core/extraction/docker-save.tar"); - LocalImage result = new ExtractTarStep(dockerBuild, temporaryFolder.getRoot().toPath()).call(); + LocalImage result = + new ExtractTarStep(buildConfiguration, dockerBuild, progressEventDispatcherFactory).call(); + Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); Assert.assertEquals( "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", @@ -65,8 +93,10 @@ public void testCall_validTar() throws URISyntaxException, LayerCountMismatchException, BadContainerConfigurationFormatException, IOException { Path tarBuild = getResource("core/extraction/jib-image.tar"); - LocalImage result = new ExtractTarStep(tarBuild, temporaryFolder.getRoot().toPath()).call(); + LocalImage result = + new ExtractTarStep(buildConfiguration, tarBuild, progressEventDispatcherFactory).call(); + Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); Assert.assertEquals( "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 2df82e1502..d807392f0e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -56,7 +56,6 @@ public class DockerClientTest { @Before public void setUp() throws IOException { Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); - Mockito.doAnswer( AdditionalAnswers.answerVoid( (VoidAnswer1) @@ -175,32 +174,32 @@ public void testLoad_stdoutFail() throws InterruptedException { @Test public void testSave() throws InterruptedException, IOException { - DockerClient testDockerClient = - new DockerClient( - subcommand -> { - Assert.assertEquals(Arrays.asList("save", "testimage", "-o", "out.tar"), subcommand); - return mockProcessBuilder; - }); + DockerClient testDockerClient = makeDockerSaveClient(); Mockito.when(mockProcess.waitFor()).thenReturn(0); - testDockerClient.save(ImageReference.of(null, "testimage", null), Paths.get("out.tar")); + long[] counter = new long[1]; + testDockerClient.save( + ImageReference.of(null, "testimage", null), + temporaryFolder.getRoot().toPath().resolve("out.tar"), + bytes -> counter[0] += bytes); + + // InputStream writes "jib", so 3 bytes of progress should have been counted. + Assert.assertEquals(3, counter[0]); } @Test public void testSave_fail() throws InterruptedException { - DockerClient testDockerClient = - new DockerClient( - subcommand -> { - Assert.assertEquals(Arrays.asList("save", "testimage", "-o", "out.tar"), subcommand); - return mockProcessBuilder; - }); + DockerClient testDockerClient = makeDockerSaveClient(); Mockito.when(mockProcess.waitFor()).thenReturn(1); Mockito.when(mockProcess.getErrorStream()) .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.save(ImageReference.of(null, "testimage", null), Paths.get("out.tar")); + testDockerClient.save( + ImageReference.of(null, "testimage", null), + temporaryFolder.getRoot().toPath().resolve("out.tar"), + ignored -> {}); Assert.fail("docker save should have failed"); } catch (IOException ex) { @@ -267,4 +266,27 @@ public void testTag_fail() throws InterruptedException, InvalidImageReferenceExc Assert.assertEquals("'docker tag' command failed with error: error", ex.getMessage()); } } + + private DockerClient makeDockerSaveClient() { + return new DockerClient( + subcommand -> { + try { + if (subcommand.contains("{{.Size}}")) { + // It doesn't matter what size is actually returned by 'docker inspect' here, so just + // use 150000 as a placeholder. + Process mockSizeProcess = Mockito.mock(Process.class); + Mockito.when(mockSizeProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("150000".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcessBuilder.start()).thenReturn(mockSizeProcess); + } else { + Assert.assertEquals(Arrays.asList("save", "testimage"), subcommand); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream("jib".getBytes(StandardCharsets.UTF_8))); + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + } + } catch (IOException ignored) { + } + return mockProcessBuilder; + }); + } } From bafd55c5c247b12a4587b4e67c79df1563d10d33 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 6 Sep 2019 17:34:23 -0400 Subject: [PATCH 0713/2020] Add progress events to LoadDockerStep (#1960) --- .../jib/builder/steps/LoadDockerStep.java | 40 ++++++++++++------ .../cloud/tools/jib/docker/DockerClient.java | 42 +++++++++---------- .../cloud/tools/jib/docker/ImageTarball.java | 14 ++++++- .../tools/jib/docker/DockerClientTest.java | 8 ++-- 4 files changed, 65 insertions(+), 39 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 9663669898..beeed1bb24 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -19,10 +19,12 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.image.Image; import java.io.IOException; import java.util.concurrent.Callable; @@ -50,27 +52,39 @@ class LoadDockerStep implements Callable { @Override public BuildResult call() throws InterruptedException, IOException { EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); - eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher(eventHandlers, "Loading to Docker daemon")) { + eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); - try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("loading to Docker daemon", 1)) { ImageReference targetImageReference = buildConfiguration.getTargetImageConfiguration().getImage(); + ImageTarball imageTarball = new ImageTarball(builtImage, targetImageReference); - // Load the image to docker daemon. - eventHandlers.dispatch( - LogEvent.debug(dockerClient.load(new ImageTarball(builtImage, targetImageReference)))); + // Note: The progress reported here is not entirely accurate. The total allocation units is + // the size of the layers, but the progress being reported includes the config and manifest + // as well, so we will always go over the total progress allocation here. + // See https://github.com/GoogleContainerTools/jib/pull/1960#discussion_r321898390 + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "loading to Docker daemon", imageTarball.getTotalLayerSize()); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { + // Load the image to docker daemon. + eventHandlers.dispatch( + LogEvent.debug(dockerClient.load(imageTarball, throttledProgressReporter))); - // Tags the image with all the additional tags, skipping the one 'docker load' already loaded. - for (String tag : buildConfiguration.getAllTargetImageTags()) { - if (tag.equals(targetImageReference.getTag())) { - continue; + // Tags the image with all the additional tags, skipping the one 'docker load' already + // loaded. + for (String tag : buildConfiguration.getAllTargetImageTags()) { + if (tag.equals(targetImageReference.getTag())) { + continue; + } + + dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); } - dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); + return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); } - - return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 55e1790040..c9e3ad2505 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -120,33 +119,34 @@ public DockerClient(Path dockerExecutable, Map dockerEnvironment * * @see https://docs.docker.com/engine/reference/commandline/load - * @param imageTarball the built container tarball. - * @return stdout from {@code docker}. - * @throws InterruptedException if the 'docker load' process is interrupted. - * @throws IOException if streaming the blob to 'docker load' fails. + * @param imageTarball the built container tarball + * @param writtenByteCountListener callback to call when bytes are loaded + * @return stdout from {@code docker} + * @throws InterruptedException if the 'docker load' process is interrupted + * @throws IOException if streaming the blob to 'docker load' fails */ - public String load(ImageTarball imageTarball) throws InterruptedException, IOException { + public String load(ImageTarball imageTarball, Consumer writtenByteCountListener) + throws InterruptedException, IOException { // Runs 'docker load'. Process dockerProcess = docker("load"); - try (OutputStream stdin = dockerProcess.getOutputStream()) { - try { - imageTarball.writeTo(stdin); + try (NotifyingOutputStream stdin = + new NotifyingOutputStream(dockerProcess.getOutputStream(), writtenByteCountListener)) { + imageTarball.writeTo(stdin); - } catch (IOException ex) { - // Tries to read from stderr. - String error; - try (InputStreamReader stderr = - new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { - error = CharStreams.toString(stderr); + } catch (IOException ex) { + // Tries to read from stderr. + String error; + try (InputStreamReader stderr = + new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { + error = CharStreams.toString(stderr); - } catch (IOException ignored) { - // This ignores exceptions from reading stderr and throws the original exception from - // writing to stdin. - throw ex; - } - throw new IOException("'docker load' command failed with error: " + error, ex); + } catch (IOException ignored) { + // This ignores exceptions from reading stderr and throws the original exception from + // writing to stdin. + throw ex; } + throw new IOException("'docker load' command failed with error: " + error, ex); } try (InputStreamReader stdout = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 14b86d54f5..8b250029c7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -41,7 +41,6 @@ public class ImageTarball { private static final String LAYER_FILE_EXTENSION = ".tar.gz"; private final Image image; - private final ImageReference imageReference; /** @@ -83,4 +82,17 @@ public void writeTo(OutputStream out) throws IOException { tarStreamBuilder.writeAsTarArchiveTo(out); } + + /** + * Returns the total size of the image's layers in bytes. + * + * @return the total size of the image's layers in bytes + */ + public long getTotalLayerSize() { + long size = 0; + for (Layer layer : image.getLayers()) { + size += layer.getBlobDescriptor().getSize(); + } + return size; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index d807392f0e..010de52694 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -87,7 +87,7 @@ public void testLoad() throws IOException, InterruptedException { Mockito.when(mockProcess.getInputStream()) .thenReturn(new ByteArrayInputStream("output".getBytes(StandardCharsets.UTF_8))); - String output = testDockerClient.load(imageTarball); + String output = testDockerClient.load(imageTarball, ignored -> {}); Assert.assertEquals( "jib", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); @@ -111,7 +111,7 @@ public void write(int b) throws IOException { .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.load(imageTarball); + testDockerClient.load(imageTarball, ignored -> {}); Assert.fail("Write should have failed"); } catch (IOException ex) { @@ -144,7 +144,7 @@ public int read() throws IOException { }); try { - testDockerClient.load(imageTarball); + testDockerClient.load(imageTarball, ignored -> {}); Assert.fail("Write should have failed"); } catch (IOException ex) { @@ -164,7 +164,7 @@ public void testLoad_stdoutFail() throws InterruptedException { .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.load(imageTarball); + testDockerClient.load(imageTarball, ignored -> {}); Assert.fail("Process should have failed"); } catch (IOException ex) { From 8fbd258dc0d6c5d6cbce948e4b3d2a093089121d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 9 Sep 2019 10:45:33 -0400 Subject: [PATCH 0714/2020] Move Blobs#compress back to test classes (#1961) --- .../google/cloud/tools/jib/blob/Blobs.java | 16 ----------- .../jib/cache/CacheStorageWriterTest.java | 17 +++++++++--- .../cloud/tools/jib/cache/CacheTest.java | 27 +++++++++++++++---- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java index 062f02dc0e..643572f529 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java @@ -23,7 +23,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.zip.GZIPOutputStream; /** Static methods for {@link Blob}. */ public class Blobs { @@ -78,20 +77,5 @@ public static byte[] writeToByteArray(Blob blob) throws IOException { return byteArrayOutputStream.toByteArray(); } - /** - * Gets a {@link Blob} that is {@code blob} compressed. - * - * @param blob the {@link Blob} to compress - * @return the compressed {@link Blob} - */ - public static Blob compress(Blob blob) { - return Blobs.from( - outputStream -> { - try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { - blob.writeTo(compressorStream); - } - }); - } - private Blobs() {} } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index e5bcdb6466..ecf48e2f40 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -35,6 +35,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -48,6 +49,15 @@ private static BlobDescriptor getDigest(Blob blob) throws IOException { return blob.writeTo(ByteStreams.nullOutputStream()); } + private static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }); + } + private static Blob decompress(Blob blob) throws IOException { return Blobs.from(new GZIPInputStream(new ByteArrayInputStream(Blobs.writeToByteArray(blob)))); } @@ -68,8 +78,7 @@ public void testWrite_compressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); CachedLayer cachedLayer = - new CacheStorageWriter(cacheStorageFiles) - .writeCompressed(Blobs.compress(uncompressedLayerBlob)); + new CacheStorageWriter(cacheStorageFiles).writeCompressed(compress(uncompressedLayerBlob)); verifyCachedLayer(cachedLayer, uncompressedLayerBlob); } @@ -77,7 +86,7 @@ public void testWrite_compressed() throws IOException { @Test public void testWrite_uncompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); - DescriptorDigest layerDigest = getDigest(Blobs.compress(uncompressedLayerBlob)).getDigest(); + DescriptorDigest layerDigest = getDigest(compress(uncompressedLayerBlob)).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")).getDigest(); CachedLayer cachedLayer = @@ -150,7 +159,7 @@ public void testWriteMetadata_v22() private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { - BlobDescriptor layerBlobDescriptor = getDigest(Blobs.compress(uncompressedLayerBlob)); + BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); DescriptorDigest layerDiffId = getDigest(uncompressedLayerBlob).getDigest(); // Verifies cachedLayer is correct. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 6d8533ba8c..5c1b22b12b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -32,6 +32,7 @@ import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -41,6 +42,22 @@ /** Tests for {@link Cache}. */ public class CacheTest { + /** + * Gets a {@link Blob} that is {@code blob} compressed. Note that the output stream is closed when + * the blob is written. + * + * @param blob the {@link Blob} to compress + * @return the compressed {@link Blob} + */ + private static Blob compress(Blob blob) { + return Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream)) { + blob.writeTo(compressorStream); + } + }); + } + /** * Gets a {@link Blob} that is {@code blob} decompressed. * @@ -105,9 +122,9 @@ public void setUp() throws IOException { Files.createFile(directory.resolve("another/source/file")); layerBlob1 = Blobs.from("layerBlob1"); - layerDigest1 = digestOf(Blobs.compress(layerBlob1)); + layerDigest1 = digestOf(compress(layerBlob1)); layerDiffId1 = digestOf(layerBlob1); - layerSize1 = sizeOf(Blobs.compress(layerBlob1)); + layerSize1 = sizeOf(compress(layerBlob1)); layerEntries1 = ImmutableList.of( defaultLayerEntry( @@ -117,9 +134,9 @@ public void setUp() throws IOException { AbsoluteUnixPath.get("/another/extraction/path"))); layerBlob2 = Blobs.from("layerBlob2"); - layerDigest2 = digestOf(Blobs.compress(layerBlob2)); + layerDigest2 = digestOf(compress(layerBlob2)); layerDiffId2 = digestOf(layerBlob2); - layerSize2 = sizeOf(Blobs.compress(layerBlob2)); + layerSize2 = sizeOf(compress(layerBlob2)); layerEntries2 = ImmutableList.of(); } @@ -141,7 +158,7 @@ public void testWriteCompressed_retrieveByLayerDigest() throws IOException, CacheCorruptedException { Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - verifyIsLayer1(cache.writeCompressedLayer(Blobs.compress(layerBlob1))); + verifyIsLayer1(cache.writeCompressedLayer(compress(layerBlob1))); verifyIsLayer1(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); } From 3e476ff4711eabef56b3759b51ea62e27b9cd79d Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 9 Sep 2019 13:39:59 -0400 Subject: [PATCH 0715/2020] Reorder priority for retrieving auth from Docker config (~/.docker/config.json) (#1958) * Reorder auth priority for .docker/config.json * Update CHANGELOG --- jib-core/CHANGELOG.md | 2 + .../registry/credentials/DockerConfig.java | 20 +++---- .../DockerConfigCredentialRetriever.java | 28 +++++----- .../json/DockerConfigTemplate.java | 20 ++++--- .../DockerConfigCredentialRetrieverTest.java | 54 +++++-------------- .../credentials/DockerConfigTest.java | 46 ++++++++-------- .../resources/core/json/dockerconfig.json | 9 +--- jib-gradle-plugin/CHANGELOG.md | 3 ++ jib-maven-plugin/CHANGELOG.md | 3 ++ 9 files changed, 77 insertions(+), 108 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 77ecc31684..63e2fd0033 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Changed - `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)` ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java index f613a1a62e..b74d35183a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java @@ -85,10 +85,9 @@ String getAuthFor(String registry) { /** * Determines a {@link DockerCredentialHelper} to use for {@code registry}. * - *

If there exists a matching registry entry (or its aliases) in {@code auths} for {@code - * registry}, the credential helper is {@code credStore}; otherwise, if there exists a matching - * registry entry (or its aliases) in {@code credHelpers}, the corresponding credential helper - * suffix is used. + *

If there exists a matching registry entry (or its aliases) in {@code credHelpers}, returns + * the corresponding credential helper is returned. Otherwise, returns the credential helper + * defined by {@code credStore}. * *

See {@link #getRegistryMatchersFor} for the alias lookup order. * @@ -100,14 +99,6 @@ String getAuthFor(String registry) { DockerCredentialHelper getCredentialHelperFor(String registry) { List> registryMatchers = getRegistryMatchersFor(registry); - Map.Entry firstAuthMatch = - findFirstInMapByKey(dockerConfigTemplate.getAuths(), registryMatchers); - if (firstAuthMatch != null && dockerConfigTemplate.getCredsStore() != null) { - return new DockerCredentialHelper( - firstAuthMatch.getKey(), - Paths.get("docker-credential-" + dockerConfigTemplate.getCredsStore())); - } - Map.Entry firstCredHelperMatch = findFirstInMapByKey(dockerConfigTemplate.getCredHelpers(), registryMatchers); if (firstCredHelperMatch != null) { @@ -116,6 +107,11 @@ DockerCredentialHelper getCredentialHelperFor(String registry) { Paths.get("docker-credential-" + firstCredHelperMatch.getValue())); } + if (dockerConfigTemplate.getCredsStore() != null) { + return new DockerCredentialHelper( + registry, Paths.get("docker-credential-" + dockerConfigTemplate.getCredsStore())); + } + return null; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 968a5126c6..ab15f83d67 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -37,9 +37,9 @@ *

The credentials are searched in the following order (stopping when credentials are found): * *

    + *
  1. The credential helper from {@code credHelpers} defined for a registry, if available. + *
  2. The {@code credsStore} credential helper, if available. *
  3. If there is an {@code auth} defined for a registry. - *
  4. Using the {@code credsStore} credential helper, if available. - *
  5. Using the credential helper from {@code credHelpers}, if available. *
* * @see retrieve(Consumer logger) throws IOExcepti @VisibleForTesting Optional retrieve(DockerConfig dockerConfig, Consumer logger) { for (String registryAlias : RegistryAliasGroup.getAliasesGroup(registry)) { - // First, tries to find defined auth. - String auth = dockerConfig.getAuthFor(registryAlias); - if (auth != null) { - // 'auth' is a basic authentication token that should be parsed back into credentials - String usernameColonPassword = - new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); - String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); - String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); - return Optional.of(Credential.from(username, password)); - } - - // Then, tries to use a defined credHelpers credential helper. + // First, find a credential helper from "credentialHelpers" and "credsStore" in order. DockerCredentialHelper dockerCredentialHelper = dockerConfig.getCredentialHelperFor(registryAlias); if (dockerCredentialHelper != null) { @@ -124,6 +113,17 @@ Optional retrieve(DockerConfig dockerConfig, Consumer logg } } } + + // Lastly, find defined auth. + String auth = dockerConfig.getAuthFor(registryAlias); + if (auth != null) { + // 'auth' is a basic authentication token that should be parsed back into credentials + String usernameColonPassword = + new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); + String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); + String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); + return Optional.of(Credential.from(username, password)); + } } return Optional.empty(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index 2895b18413..2f3d338e1a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -32,12 +32,12 @@ * { * "auths": { * "registry": { - * "auth": "username:password in base64" + * "auth": "username:password string in base64" * }, * "anotherregistry": {}, * ... * }, - * "credsStore": "credential helper name", + * "credsStore": "legacy credential helper config acting as a \"default\" helper", * "credHelpers": { * "registry": "credential helper name", * "anotherregistry": "another credential helper name", @@ -46,17 +46,15 @@ * } * } * - * If an {@code auth} is defined for a registry, that is a valid {@code Basic} authorization to use - * for that registry. - * - *

If {@code credsStore} is defined, is a credential helper that stores authorizations for all - * registries listed under {@code auths}. - * *

Each entry in {@code credHelpers} is a mapping from a registry to a credential helper that - * stores the authorization for that registry. + * stores the authorization for that registry. This takes precedence over {@code credsStore} if + * there exists a match. + * + *

{@code credsStore} is a legacy config that acts to provide a "default" credential helper if + * there is no match in {@code credHelpers}. * - * @see https://www.projectatomic.io/blog/2016/03/docker-credentials-store/ + *

If an {@code auth} is defined for a registry, that is a valid {@code Basic} authorization to + * use for that registry. */ @JsonIgnoreProperties(ignoreUnknown = true) public class DockerConfigTemplate implements JsonTemplate { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 5c4d830ba1..b4c01b9776 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -54,7 +54,7 @@ public void setUp() } @Test - public void testRetrieve_nonexistentDockerConfigFile() throws IOException { + public void testRetrieve_nonExistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); @@ -64,58 +64,30 @@ public void testRetrieve_nonexistentDockerConfigFile() throws IOException { @Test public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", dockerConfigFile); + new DockerConfigCredentialRetriever("some other registry", dockerConfigFile); Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("some", credentials.get().getUsername()); - Assert.assertEquals("auth", credentials.get().getPassword()); - } - - @Test - public void testRetrieve_useCredsStore() { - Mockito.when(mockDockerConfig.getCredentialHelperFor("just registry")) - .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("just registry", dockerConfigFile); - - Assert.assertEquals( - FAKE_CREDENTIAL, - dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockLogger) - .orElseThrow(AssertionError::new)); + Assert.assertEquals("other:auth", credentials.get().getPassword()); } @Test - public void testRetrieve_useCredsStore_withProtocol() { - Mockito.when(mockDockerConfig.getCredentialHelperFor("with.protocol")) + public void testRetrieve_credentialHelperTakesPrecedenceOverAuth() { + Mockito.when(mockDockerConfig.getCredentialHelperFor("some registry")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("with.protocol", dockerConfigFile); - - Assert.assertEquals( - FAKE_CREDENTIAL, - dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockLogger) - .orElseThrow(AssertionError::new)); - } + new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - @Test - public void testRetrieve_useCredHelper() { - Mockito.when(mockDockerConfig.getCredentialHelperFor("another registry")) - .thenReturn(mockDockerCredentialHelper); - DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("another registry", dockerConfigFile); + dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger); Assert.assertEquals( - FAKE_CREDENTIAL, - dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockLogger) - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIAL), + dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger)); } @Test - public void testRetrieve_useCredHelper_warn() + public void testRetrieve_credentialHelper_warn() throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { Mockito.when(mockDockerConfig.getCredentialHelperFor("another registry")) @@ -149,10 +121,8 @@ public void testRetrieve_credentialFromAlias() { new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); Assert.assertEquals( - FAKE_CREDENTIAL, - dockerConfigCredentialRetriever - .retrieve(mockDockerConfig, mockLogger) - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIAL), + dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger)); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java index b971c1d430..804ad1edd6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java @@ -52,21 +52,14 @@ public void test_fromJson() throws URISyntaxException, IOException { Assert.assertNull(dockerConfig.getAuthFor("just registry")); Assert.assertEquals( - Paths.get("docker-credential-some credential store"), + Paths.get("docker-credential-some credential helper"), dockerConfig.getCredentialHelperFor("some registry").getCredentialHelper()); - Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("some other registry").getCredentialHelper()); - Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("just registry").getCredentialHelper()); - Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelper()); Assert.assertEquals( Paths.get("docker-credential-another credential helper"), dockerConfig.getCredentialHelperFor("another registry").getCredentialHelper()); - Assert.assertNull(dockerConfig.getCredentialHelperFor("unknonwn registry")); + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("unknown registry").getCredentialHelper()); } @Test @@ -98,15 +91,16 @@ public void testGetAuthFor_correctSuffixMatching() throws URISyntaxException, IO } @Test - public void testGetCredentialHelperFor() throws URISyntaxException, IOException { + public void testGetCredentialHelperFor_exactMatchInCredHelpers() + throws URISyntaxException, IOException { Path json = Paths.get(Resources.getResource("core/json/dockerconfig.json").toURI()); DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("just registry").getCredentialHelper()); + Paths.get("docker-credential-credHelper for just.registry.in.helpers"), + dockerConfig.getCredentialHelperFor("just.registry.in.helpers").getCredentialHelper()); } @Test @@ -117,8 +111,8 @@ public void testGetCredentialHelperFor_withHttps() throws URISyntaxException, IO new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("with.protocol").getCredentialHelper()); + Paths.get("docker-credential-credHelper for https__with.protocol.in.helpers"), + dockerConfig.getCredentialHelperFor("with.protocol.in.helpers").getCredentialHelper()); } @Test @@ -129,8 +123,8 @@ public void testGetCredentialHelperFor_withSuffix() throws URISyntaxException, I new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("with.suffix").getCredentialHelper()); + Paths.get("docker-credential-credHelper for with.suffix.in.helpers/v2/"), + dockerConfig.getCredentialHelperFor("with.suffix.in.helpers").getCredentialHelper()); } @Test @@ -142,8 +136,11 @@ public void testGetCredentialHelperFor_withProtocolAndSuffix() new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); Assert.assertEquals( - Paths.get("docker-credential-some credential store"), - dockerConfig.getCredentialHelperFor("with.protocol.and.suffix").getCredentialHelper()); + Paths.get( + "docker-credential-credHelper for https__with.protocol.and.suffix.in.helpers/suffix"), + dockerConfig + .getCredentialHelperFor("with.protocol.and.suffix.in.helpers") + .getCredentialHelper()); } @Test @@ -154,7 +151,12 @@ public void testGetCredentialHelperFor_correctSuffixMatching() DockerConfig dockerConfig = new DockerConfig(JsonTemplateMapper.readJsonFromFile(json, DockerConfigTemplate.class)); - Assert.assertNull(dockerConfig.getCredentialHelperFor("example")); - Assert.assertNull(dockerConfig.getCredentialHelperFor("another.example")); + // Should fall back to credsStore + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("example").getCredentialHelper()); + Assert.assertEquals( + Paths.get("docker-credential-some credential store"), + dockerConfig.getCredentialHelperFor("another.example").getCredentialHelper()); } } diff --git a/jib-core/src/test/resources/core/json/dockerconfig.json b/jib-core/src/test/resources/core/json/dockerconfig.json index 56934697b8..00d7673bd1 100644 --- a/jib-core/src/test/resources/core/json/dockerconfig.json +++ b/jib-core/src/test/resources/core/json/dockerconfig.json @@ -4,11 +4,6 @@ "some registry":{"auth":"c29tZTphdXRo","password":"ignored"}, "https://registry":{"auth":"dG9rZW4="}, - "just registry":{}, - "https://with.protocol":{}, - "with.suffix/suffix/":{}, - "https://with.protocol.and.suffix/v10/":{}, - "example.com":{"auth":"should not match example"} }, "credsStore":"some credential store", @@ -18,10 +13,10 @@ "index.docker.io":"index.docker.io credential helper", "just.registry.in.helpers":"credHelper for just.registry.in.helpers", - "https://with.protocol.in.helpers":"credHelper for https://with.protocol.in.helpers", + "https://with.protocol.in.helpers":"credHelper for https__with.protocol.in.helpers", "with.suffix.in.helpers/v2/":"credHelper for with.suffix.in.helpers/v2/", "https://with.protocol.and.suffix.in.helpers/suffix": - "credHelper for https://with.protocol.and.suffix.in.helpers/suffix", + "credHelper for https__with.protocol.and.suffix.in.helpers/suffix", "another.example.com.in.helpers":"should not match example" } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index d858e80b4b..137759aaf5 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to this project will be documented in this file. ### Changed +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) + ### Fixed ## 1.5.1 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 8b2542bb56..b22d7b4b00 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to this project will be documented in this file. ### Changed +- For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) + ### Fixed ## 1.5.1 From 867b04697df4aecf4e467f6efaac8e65d3db8361 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 9 Sep 2019 16:30:43 -0400 Subject: [PATCH 0716/2020] Turn off progress bar when capturing network trace (#1965) --- docs/faq.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 21f4592dea..b195b223e6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -533,12 +533,13 @@ com.google.api.client.http.level=CONFIG And then launch your build tool as follows: ```sh -mvn -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true ... +mvn -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true -Djib.console=plain ... ``` or ```sh -gradle -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true ... +gradle -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true -Djib.console=plain ... ``` +Note there is no dot(.) in `jibSerialize`. ### How do I view debug logs for Jib? From 26d8ba3446f1446d95159b13ce2635b7e2266070 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 10 Sep 2019 17:09:48 -0400 Subject: [PATCH 0717/2020] Fix skaffold init for multimodule maven projects (#1974) --- .../tools/jib/maven/skaffold/SkaffoldInitMojo.java | 12 +++++++++--- .../jib/maven/skaffold/SkaffoldInitMojoTest.java | 10 +++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java index fdd5784847..eb0e06c49c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java @@ -23,6 +23,7 @@ import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; /** * Prints out to.image configuration and project name, used for Jib project detection in Skaffold. @@ -37,12 +38,17 @@ public class SkaffoldInitMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException { checkJibVersion(); + MavenProject project = getProject(); + // Ignore parent projects + if (project.getModules().size() > 0) { + return; + } + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); skaffoldInitOutput.setImage(getTargetImage()); - if (getProject().getParent() != null) { - skaffoldInitOutput.setProject(getProject().getName()); + if (project.getParent() != null && project.getParent().getFile() != null) { + skaffoldInitOutput.setProject(project.getName()); } - System.out.println("\nBEGIN JIB JSON"); try { System.out.println(skaffoldInitOutput.getJsonString()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java index 341c928e77..99906581c8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java @@ -84,21 +84,17 @@ public void testFilesMojo_singleModule() throws IOException, VerificationExcepti @Test public void testFilesMojo_multiModule() throws IOException, VerificationException { List outputs = getJsons(multiTestProject); - Assert.assertEquals(4, outputs.size()); + Assert.assertEquals(3, outputs.size()); SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertNull(skaffoldInitOutput.getProject()); - - skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); - Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); Assert.assertEquals("name-service", skaffoldInitOutput.getProject()); - skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); Assert.assertEquals("lib", skaffoldInitOutput.getProject()); - skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(3)); + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); Assert.assertEquals("service", skaffoldInitOutput.getProject()); } From 770e332bd0ec7c7019c9d61062790b6032d37815 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 10 Sep 2019 17:43:09 -0400 Subject: [PATCH 0718/2020] Update progress and timer messages (#1976) --- .../jib/builder/steps/BuildAndCacheApplicationLayerStep.java | 2 +- .../tools/jib/builder/steps/ObtainBaseImageLayerStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PushImageStep.java | 2 +- .../google/cloud/tools/jib/builder/steps/PushLayerStep.java | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index 10dfc33153..ab40716ce3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -49,7 +49,7 @@ static ImmutableList makeList( try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "preparing application layer builders", layerConfigurations.size()); + "launching application layer builders", layerConfigurations.size()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing application layer builders")) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 02f3b5cbbb..6dff150ad4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -90,7 +90,7 @@ private static ImmutableList makeList( try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "preparing base image layer pullers", baseImageLayers.size()); + "launching base image layer pullers", baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing base image layer pullers")) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index cdd0f4da7f..23f3f4cc3c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -57,7 +57,7 @@ static ImmutableList makeList( new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Preparing manifest pushers"); ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("preparing manifest pushers", tags.size())) { + progressEventDispatcherFactory.create("launching manifest pushers", tags.size())) { // Gets the image manifest to push. BuildableManifestTemplate manifestTemplate = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 31e233bbc1..77251acf7c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -40,10 +40,9 @@ static ImmutableList makeList( List> cachedLayers) { try (TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), "Preparing application layer pushers"); + buildConfiguration.getEventHandlers(), "Preparing layer pushers"); ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - "preparing application layer pushers", cachedLayers.size())) { + progressEventDispatcherFactory.create("launching layer pushers", cachedLayers.size())) { // Constructs a PushBlobStep for each layer. return cachedLayers From 85bb1c58f451a1949f35c8a34bc381c40f8fe047 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 10 Sep 2019 17:44:49 -0400 Subject: [PATCH 0719/2020] Change jibSerialize to jib.serialize (#1968) * Change jibSerialize to jib.serialize --- jib-core/CHANGELOG.md | 1 + .../google/cloud/tools/jib/global/JibSystemProperties.java | 4 ++-- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 63e2fd0033..e2b2f25442 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Changed +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) - `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)` ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index e6523788af..e4a13bddcb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -34,7 +34,7 @@ public class JibSystemProperties { @VisibleForTesting public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; - private static final String SERIALIZE = "jibSerialize"; + private static final String SERIALIZE = "jib.serialize"; private static final String DISABLE_USER_AGENT = "_JIB_DISABLE_USER_AGENT"; @@ -65,7 +65,7 @@ public static boolean useCrossRepositoryBlobMounts() { } /** - * Gets whether or not to serialize Jib's execution. This is defined by the {@code jibSerialize} + * Gets whether or not to serialize Jib's execution. This is defined by the {@code jib.serialize} * system property. * * @return {@code true} if Jib's execution should be serialized, {@code false} if not diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 137759aaf5..34a3f88e2d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changed +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b22d7b4b00..37371ea64e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changed +- To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) From 79aa1491c5685ea5a8ad933f959c0b5f45165013 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 11 Sep 2019 15:55:45 -0400 Subject: [PATCH 0720/2020] fix 0 allocation (#1975) --- .../tools/jib/event/progress/Allocation.java | 4 ++-- .../jib/event/progress/AllocationTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java index ef874dddab..af2911bfc7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java @@ -65,10 +65,10 @@ public static Allocation newRoot(String description, long allocationUnits) { private Allocation(String description, long allocationUnits, @Nullable Allocation parent) { this.description = description; - this.allocationUnits = allocationUnits < 0 ? 0 : allocationUnits; + this.allocationUnits = allocationUnits < 1 ? 1 : allocationUnits; this.parent = parent; - this.fractionOfRoot = (parent == null ? 1.0 : parent.fractionOfRoot) / allocationUnits; + this.fractionOfRoot = (parent == null ? 1.0 : parent.fractionOfRoot) / this.allocationUnits; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java index 28509c573f..bfb060dd1e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java @@ -90,4 +90,21 @@ public void testFractionOfRoot_tree_complete() { + rightDown.getFractionOfRoot() * rightDown.getAllocationUnits(); Assert.assertEquals(1.0, total, DOUBLE_ERROR_MARGIN); } + + @Test + public void testNonPositiveAllocationUnits() { + Allocation root = Allocation.newRoot("ignored", 2); + + Allocation left = root.newChild("ignored", -30); + Allocation right = root.newChild("ignored", 0); + + Assert.assertEquals(0.5, root.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(2, root.getAllocationUnits()); + + Assert.assertEquals(0.5, left.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1, left.getAllocationUnits()); + + Assert.assertEquals(0.5, right.getFractionOfRoot(), DOUBLE_ERROR_MARGIN); + Assert.assertEquals(1, right.getAllocationUnits()); + } } From cc60280a5b0616b3bb7e3eead05acff6686b0f6b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 11 Sep 2019 18:55:25 -0400 Subject: [PATCH 0721/2020] Fix progress bar logger for Gradle (#1971) --- .../jib/gradle/GradleProjectProperties.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../common/logging/AnsiLoggerWithFooter.java | 64 +++++++--- .../common/logging/ConsoleLoggerBuilder.java | 10 +- .../logging/AnsiLoggerWithFooterTest.java | 113 +++++++++++++++--- 5 files changed, 155 insertions(+), 36 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 85f4ac4ce6..5c85871e42 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -112,7 +112,7 @@ private static boolean isProgressFooterEnabled(Project project) { this.logger = logger; ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(project) - ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor, false) : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(logger::lifecycle)) .lifecycle(logger::lifecycle); if (logger.isDebugEnabled()) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 04292e2d3d..2de780c2e3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -163,7 +163,7 @@ static int getVersionFromString(String versionString) { this.session = session; ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(session) - ? ConsoleLoggerBuilder.rich(singleThreadedExecutor) + ? ConsoleLoggerBuilder.rich(singleThreadedExecutor, true) : ConsoleLoggerBuilder.plain(singleThreadedExecutor).progress(log::info)) .lifecycle(log::info); if (log.isDebugEnabled()) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java index 4326b0cbb6..db316e8b30 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooter.java @@ -40,6 +40,9 @@ class AnsiLoggerWithFooter implements ConsoleLogger { /** ANSI escape sequence template for moving the cursor up multiple lines. */ private static final String CURSOR_UP_SEQUENCE_TEMPLATE = "\033[%dA"; + /** ANSI escape sequence for moving the cursor up. */ + private static final String CURSOR_UP_SEQUENCE = String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 1); + /** ANSI escape sequence for erasing to end of display. */ private static final String ERASE_DISPLAY_BELOW = "\033[0J"; @@ -76,22 +79,32 @@ static List truncateToMaxWidth(List lines) { private List footerLines = Collections.emptyList(); + // When a footer is erased, makes the logger go up two lines (and then down one line by calling + // "accept()" once) before printing the next message. This is useful to correct an issue in Maven: + // https://github.com/GoogleContainerTools/jib/issues/1952 + private boolean enableTwoCursorUpJump; + /** * Creates a new {@link AnsiLoggerWithFooter}. * * @param messageConsumers map from each {@link Level} to a corresponding message logger * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are * logged in a sequential, deterministic order + * @param enableTwoCursorUpJump allows the logger to move the cursor up twice at once. Fixes a + * logging issue in Maven (https://github.com/GoogleContainerTools/jib/issues/1952) but causes + * a problem in Gradle (https://github.com/GoogleContainerTools/jib/issues/1963) */ AnsiLoggerWithFooter( ImmutableMap> messageConsumers, - SingleThreadedExecutor singleThreadedExecutor) { + SingleThreadedExecutor singleThreadedExecutor, + boolean enableTwoCursorUpJump) { Preconditions.checkArgument( messageConsumers.containsKey(Level.LIFECYCLE), "Cannot construct AnsiLoggerFooter without LIFECYCLE message consumer"); this.messageConsumers = messageConsumers; - this.lifecycleConsumer = Preconditions.checkNotNull(messageConsumers.get(Level.LIFECYCLE)); + lifecycleConsumer = Preconditions.checkNotNull(messageConsumers.get(Level.LIFECYCLE)); this.singleThreadedExecutor = singleThreadedExecutor; + this.enableTwoCursorUpJump = enableTwoCursorUpJump; } @Override @@ -103,15 +116,20 @@ public void log(Level logLevel, String message) { singleThreadedExecutor.execute( () -> { - if (eraseFooter()) { - // If a previous footer was erased, the message needs to go up a line. However, we go up - // two lines (and then down one line by calling "accept()" once) to correct an issue in - // Maven: https://github.com/GoogleContainerTools/jib/issues/1952 - messageConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); + boolean didErase = eraseFooter(); + // If a previous footer was erased, the message needs to go up a line. + if (didErase) { + if (enableTwoCursorUpJump) { + messageConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); + messageConsumer.accept(message); + } else { + messageConsumer.accept(CURSOR_UP_SEQUENCE + message); + } + } else { + messageConsumer.accept(message); } - messageConsumer.accept(message); - footerLines.forEach(line -> lifecycleConsumer.accept(BOLD + line + UNBOLD)); + printInBold(footerLines); }); } @@ -133,15 +151,19 @@ public void setFooter(List newFooterLines) { singleThreadedExecutor.execute( () -> { - if (eraseFooter()) { - // If a previous footer was erased, the first new footer line needs to go up a line. - // However, we go up two lines (and then down one line by calling "accept()" once to - // correct an issue in Maven: https://github.com/GoogleContainerTools/jib/issues/1952 - lifecycleConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); + boolean didErase = eraseFooter(); + // If a previous footer was erased, the first new footer line needs to go up a line. + if (didErase) { + if (enableTwoCursorUpJump) { + lifecycleConsumer.accept(String.format(CURSOR_UP_SEQUENCE_TEMPLATE, 2)); + printInBold(truncatedNewFooterLines); + } else { + printInBold(truncatedNewFooterLines, CURSOR_UP_SEQUENCE); + } + } else { + printInBold(truncatedNewFooterLines); } - truncatedNewFooterLines.forEach(line -> lifecycleConsumer.accept(BOLD + line + UNBOLD)); - footerLines = truncatedNewFooterLines; }); } @@ -168,4 +190,14 @@ private boolean eraseFooter() { return true; } + + private void printInBold(List lines) { + printInBold(lines, ""); + } + + private void printInBold(List lines, String firstLinePrefix) { + for (int i = 0; i < lines.size(); i++) { + lifecycleConsumer.accept((i == 0 ? firstLinePrefix : "") + BOLD + lines.get(i) + UNBOLD); + } + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java index 4254806dd3..dba05a4d62 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/ConsoleLoggerBuilder.java @@ -39,11 +39,17 @@ interface ConsoleLoggerFactory * * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are * logged in a sequential, deterministic order + * @param enableTwoCursorUpJump allows the logger to move the cursor up twice at once. Fixes a + * logging issue in Maven (https://github.com/GoogleContainerTools/jib/issues/1952) but causes + * a problem in Gradle (https://github.com/GoogleContainerTools/jib/issues/1963) * @return a new {@link ConsoleLoggerBuilder} */ - public static ConsoleLoggerBuilder rich(SingleThreadedExecutor singleThreadedExecutor) { + public static ConsoleLoggerBuilder rich( + SingleThreadedExecutor singleThreadedExecutor, boolean enableTwoCursorUpJump) { return new ConsoleLoggerBuilder( - messageConsumers -> new AnsiLoggerWithFooter(messageConsumers, singleThreadedExecutor)); + messageConsumers -> + new AnsiLoggerWithFooter( + messageConsumers, singleThreadedExecutor, enableTwoCursorUpJump)); } /** diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java index 84f3965fea..d5d917a1fd 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/logging/AnsiLoggerWithFooterTest.java @@ -26,7 +26,6 @@ import java.util.function.Consumer; import java.util.function.Function; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; /** Tests for {@link AnsiLoggerWithFooter}. */ @@ -45,19 +44,6 @@ public class AnsiLoggerWithFooterTest { messages.add(message); }; - private AnsiLoggerWithFooter testAnsiLoggerWithFooter; - - @Before - public void setUp() { - ImmutableMap.Builder> messageConsumers = ImmutableMap.builder(); - for (Level level : Level.values()) { - messageConsumers.put(level, messageConsumerFactory.apply(level)); - } - - testAnsiLoggerWithFooter = - new AnsiLoggerWithFooter(messageConsumers.build(), singleThreadedExecutor); - } - @Test public void testTruncateToMaxWidth() { List lines = @@ -74,7 +60,7 @@ public void testTruncateToMaxWidth() { @Test public void testNoLifecycle() { try { - new AnsiLoggerWithFooter(ImmutableMap.of(), singleThreadedExecutor); + new AnsiLoggerWithFooter(ImmutableMap.of(), singleThreadedExecutor, false); Assert.fail(); } catch (IllegalArgumentException ex) { @@ -85,6 +71,7 @@ public void testNoLifecycle() { @Test public void testLog_noFooter() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = createTestLogger(false); testAnsiLoggerWithFooter.log(Level.LIFECYCLE, "lifecycle"); testAnsiLoggerWithFooter.log(Level.PROGRESS, "progress"); testAnsiLoggerWithFooter.log(Level.INFO, "info"); @@ -107,7 +94,8 @@ public void testLog_ignoreIfNoMessageConsumer() { AnsiLoggerWithFooter testAnsiLoggerWithFooter = new AnsiLoggerWithFooter( ImmutableMap.of(Level.LIFECYCLE, messageConsumerFactory.apply(Level.LIFECYCLE)), - singleThreadedExecutor); + singleThreadedExecutor, + false); testAnsiLoggerWithFooter.log(Level.LIFECYCLE, "lifecycle"); testAnsiLoggerWithFooter.log(Level.PROGRESS, "progress"); @@ -124,6 +112,42 @@ public void testLog_ignoreIfNoMessageConsumer() { @Test public void testLog_sameFooter() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = createTestLogger(false); + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); + testAnsiLoggerWithFooter.log(Level.INFO, "message"); + testAnsiLoggerWithFooter.log(Level.INFO, "another message"); + + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); + + Assert.assertEquals( + Arrays.asList( + "\033[1mfooter\033[0m", // single-line footer in bold + + // now triggered by logging a message + "\033[1A\033[0J", // cursor up and erase to the end + "\033[1Amessage", // cursor up + message + "\033[1mfooter\033[0m", // footer + + // by logging another message + "\033[1A\033[0J", // cursor up and erase + "\033[1Aanother message", // cursor up + message + "\033[1mfooter\033[0m"), // footer + messages); + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.INFO, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.INFO, + Level.LIFECYCLE), + levels); + } + + @Test + public void testLog_sameFooterWithEnableTwoCursorUpJump() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = createTestLogger(true); testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); testAnsiLoggerWithFooter.log(Level.INFO, "message"); testAnsiLoggerWithFooter.log(Level.INFO, "another message"); @@ -162,6 +186,53 @@ public void testLog_sameFooter() { @Test public void testLog_changingFooter() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = createTestLogger(false); + testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); + testAnsiLoggerWithFooter.log(Level.WARN, "message"); + testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); + testAnsiLoggerWithFooter.log(Level.WARN, "another message"); + + singleThreadedExecutor.shutDownAndAwaitTermination(SHUTDOWN_TIMEOUT); + + Assert.assertEquals( + Arrays.asList( + "\033[1mfooter\033[0m", // single-line footer in bold + + // now triggered by logging a warning + "\033[1A\033[0J", // cursor up and erase to the end + "\033[1Amessage", // cursor up + message + "\033[1mfooter\033[0m", // footer + + // by setting a two-line footer + "\033[1A\033[0J", // cursor up and erase + "\033[1A\033[1mtwo line\033[0m", // cursor up + footer line 1 + "\033[1mfooter\033[0m", // footer line 2 + + // by logging another warning + "\033[2A\033[0J", // cursor up twice (to erase two-line footer) and erase + "\033[1Aanother message", // cursor up + message + "\033[1mtwo line\033[0m", // footer line 1 + "\033[1mfooter\033[0m"), // footer line 2 + messages); + Assert.assertEquals( + Arrays.asList( + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.WARN, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.LIFECYCLE, + Level.WARN, + Level.LIFECYCLE, + Level.LIFECYCLE), + levels); + } + + @Test + public void testLog_changingFooterWithEnableTwoCursorUpJump() { + AnsiLoggerWithFooter testAnsiLoggerWithFooter = createTestLogger(true); testAnsiLoggerWithFooter.setFooter(Collections.singletonList("footer")); testAnsiLoggerWithFooter.log(Level.WARN, "message"); testAnsiLoggerWithFooter.setFooter(Arrays.asList("two line", "footer")); @@ -210,4 +281,14 @@ public void testLog_changingFooter() { Level.LIFECYCLE), levels); } + + private AnsiLoggerWithFooter createTestLogger(boolean enableTwoCursorUpJump) { + ImmutableMap.Builder> messageConsumers = ImmutableMap.builder(); + for (Level level : Level.values()) { + messageConsumers.put(level, messageConsumerFactory.apply(level)); + } + + return new AnsiLoggerWithFooter( + messageConsumers.build(), singleThreadedExecutor, enableTwoCursorUpJump); + } } From cdb696fbf959350a401ae60e524e674819bc92c2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 12 Sep 2019 15:16:20 -0400 Subject: [PATCH 0722/2020] Cache compressed local base image layers (#1957) --- .../jib/builder/steps/ExtractTarStep.java | 94 ++++++++++--------- .../google/cloud/tools/jib/cache/Cache.java | 27 ++++++ .../tools/jib/cache/CacheStorageFiles.java | 18 +++- .../tools/jib/cache/CacheStorageReader.java | 42 ++++++++- .../tools/jib/cache/CacheStorageWriter.java | 43 ++++++++- .../jib/builder/steps/ExtractTarStepTest.java | 10 +- .../jib/cache/CacheStorageFilesTest.java | 12 +-- .../jib/cache/CacheStorageReaderTest.java | 45 ++++++++- .../jib/cache/CacheStorageWriterTest.java | 32 ++++++- .../maven/BuildImageMojoIntegrationTest.java | 21 ++++- 10 files changed, 280 insertions(+), 64 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java index 775e7ba787..ee0bc497a6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java @@ -18,12 +18,15 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; @@ -46,6 +49,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -96,7 +100,8 @@ static boolean isGzipped(Path path) throws IOException { @Override public LocalImage call() - throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException { + throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException { Path destination = Files.createTempDirectory("jib-extract-tar"); try (TimerEventDispatcher ignored = new TimerEventDispatcher( @@ -132,56 +137,23 @@ public LocalImage call() // Process layer blobs // TODO: Optimize; compressing/calculating layer digests is slow - // e.g. parallelize, cache layers, faster compression method + // e.g. parallelize, faster compression method try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( "processing base image layers", layerFiles.size())) { List layers = new ArrayList<>(layerFiles.size()); V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); - List childProgressFactories = new ArrayList<>(); - for (String ignored1 : layerFiles) { - childProgressFactories.add(progressEventDispatcher.newChildProducer()); - } - for (int index = 0; index < layerFiles.size(); index++) { - Path file = destination.resolve(layerFiles.get(index)); - - // Compress layers if necessary and calculate the digest/size - Blob blob = Blobs.from(file); - try (ProgressEventDispatcher childDispatcher = - childProgressFactories - .get(index) - .create("compressing " + file, Files.size(file)); - ThrottledAccumulatingConsumer throttledProgressReporter = - new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { - if (!layersAreCompressed) { - Path compressedFile = destination.resolve(layerFiles.get(index) + ".compressed"); - try (GZIPOutputStream compressorStream = - new GZIPOutputStream(Files.newOutputStream(compressedFile)); - NotifyingOutputStream notifyingOutputStream = - new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { - blob.writeTo(notifyingOutputStream); - } - blob = Blobs.from(compressedFile); - } - } - BlobDescriptor blobDescriptor = blob.writeTo(ByteStreams.nullOutputStream()); - - // 'manifest' contains the layer files in the same order as the diff ids in - // 'configuration', so we don't need to recalculate those. - // https://containers.gitbook.io/build-containers-the-hard-way/#docker-load-format + Path layerFile = destination.resolve(layerFiles.get(index)); CachedLayer layer = - CachedLayer.builder() - .setLayerBlob(blob) - .setLayerDigest(blobDescriptor.getDigest()) - .setLayerSize(blobDescriptor.getSize()) - .setLayerDiffId(configurationTemplate.getLayerDiffId(index)) - .build(); - + getCachedTarLayer( + configurationTemplate.getLayerDiffId(index), + layerFile, + layersAreCompressed, + progressEventDispatcher.newChildProducer()); layers.add(new PreparedLayer.Builder(layer).build()); - v22Manifest.addLayer(blobDescriptor.getSize(), blobDescriptor.getDigest()); - progressEventDispatcher.dispatchProgress(1); + v22Manifest.addLayer(layer.getSize(), layer.getDigest()); } BlobDescriptor configDescriptor = @@ -193,4 +165,42 @@ public LocalImage call() } } } + + private CachedLayer getCachedTarLayer( + DescriptorDigest diffId, + Path layerFile, + boolean layersAreCompressed, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) + throws IOException, CacheCorruptedException { + try (ProgressEventDispatcher childDispatcher = + progressEventDispatcherFactory.create( + "compressing layer " + diffId, Files.size(layerFile)); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { + Cache cache = buildConfiguration.getBaseImageLayersCache(); + + // Retrieve pre-compressed layer from cache + Optional optionalLayer = cache.retrieveTarLayer(diffId); + if (optionalLayer.isPresent()) { + return optionalLayer.get(); + } + + // Just write layers that are already compressed + if (layersAreCompressed) { + return cache.writeTarLayer(diffId, Blobs.from(layerFile)); + } + + // Compress uncompressed layers while writing + Blob compressedBlob = + Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { + Blobs.from(layerFile).writeTo(notifyingOutputStream); + } + }); + return cache.writeTarLayer(diffId, compressedBlob); + } + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 6a5c946eed..6bd8532448 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -116,6 +116,20 @@ public CachedLayer writeUncompressedLayer( uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries)); } + /** + * Caches a layer that was extracted from a local base image, and names the file using the + * provided diff id. + * + * @param diffId the diff id + * @param compressedBlob the compressed layer blob + * @return the {@link CachedLayer} for the written layer + * @throws IOException if an I/O exception occurs + */ + public CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) + throws IOException { + return cacheStorageWriter.writeTarLayer(diffId, compressedBlob); + } + /** * Retrieves the cached manifest and container configuration for an image reference. * @@ -160,4 +174,17 @@ public Optional retrieve(DescriptorDigest layerDigest) throws IOException, CacheCorruptedException { return cacheStorageReader.retrieve(layerDigest); } + + /** + * Retrieves a {@link CachedLayer} for a local base image layer with the given diff id. + * + * @param diffId the diff id + * @return the {@link CachedLayer} with the given diff id + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + public Optional retrieveTarLayer(DescriptorDigest diffId) + throws IOException, CacheCorruptedException { + return cacheStorageReader.retrieveTarLayer(diffId); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java index 3296a71da3..e6aa3d3661 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -26,6 +26,7 @@ class CacheStorageFiles { private static final String LAYERS_DIRECTORY = "layers"; + private static final String LOCAL_DIRECTORY = "local"; private static final String IMAGES_DIRECTORY = "images"; private static final String SELECTORS_DIRECTORY = "selectors"; private static final String TEMPORARY_DIRECTORY = "tmp"; @@ -54,14 +55,14 @@ static boolean isLayerFile(Path file) { * @return the diff ID portion of the layer file filename * @throws CacheCorruptedException if no valid diff ID could be parsed */ - DescriptorDigest getDiffId(Path layerFile) throws CacheCorruptedException { + DescriptorDigest getDigestFromFilename(Path layerFile) throws CacheCorruptedException { try { - String diffId = layerFile.getFileName().toString(); - return DescriptorDigest.fromHash(diffId); + String hash = layerFile.getFileName().toString(); + return DescriptorDigest.fromHash(hash); } catch (DigestException | IndexOutOfBoundsException ex) { throw new CacheCorruptedException( - cacheDirectory, "Layer file did not include valid diff ID: " + layerFile, ex); + cacheDirectory, "Layer file did not include valid hash: " + layerFile, ex); } } @@ -125,6 +126,15 @@ Path getLayerDirectory(DescriptorDigest layerDigest) { return getLayersDirectory().resolve(layerDigest.getHash()); } + /** + * Resolves the {@link #LOCAL_DIRECTORY} in the {@link #cacheDirectory}. + * + * @return the directory containing local base image layers + */ + Path getLocalDirectory() { + return cacheDirectory.resolve(LOCAL_DIRECTORY); + } + /** * Gets the directory to store the image manifest and configuration. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index b9b4488836..3fbd217ebf 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -168,7 +168,7 @@ Optional retrieve(DescriptorDigest layerDigest) if (layerFiles.size() != 1) { throw new CacheCorruptedException( cacheStorageFiles.getCacheDirectory(), - "No or multiple layer files found for layer with digest " + "No or multiple layer files found for layer hash " + layerDigest.getHash() + " in directory: " + layerDirectory); @@ -180,7 +180,45 @@ Optional retrieve(DescriptorDigest layerDigest) .setLayerDigest(layerDigest) .setLayerSize(Files.size(layerFile)) .setLayerBlob(Blobs.from(layerFile)) - .setLayerDiffId(cacheStorageFiles.getDiffId(layerFile)) + .setLayerDiffId(cacheStorageFiles.getDigestFromFilename(layerFile)) + .build()); + } + } + + /** + * Retrieves the {@link CachedLayer} for the local base image layer with the given diff ID. + * + * @param diffId the diff ID + * @return the {@link CachedLayer} referenced by the diff ID, if found + * @throws CacheCorruptedException if the cache was found to be corrupted + * @throws IOException if an I/O exception occurs + */ + Optional retrieveTarLayer(DescriptorDigest diffId) + throws IOException, CacheCorruptedException { + Path layerDirectory = cacheStorageFiles.getLocalDirectory().resolve(diffId.getHash()); + if (!Files.exists(layerDirectory)) { + return Optional.empty(); + } + + try (Stream files = Files.list(layerDirectory)) { + List layerFiles = + files.filter(CacheStorageFiles::isLayerFile).collect(Collectors.toList()); + if (layerFiles.size() != 1) { + throw new CacheCorruptedException( + cacheStorageFiles.getCacheDirectory(), + "No or multiple layer files found for layer hash " + + diffId.getHash() + + " in directory: " + + layerDirectory); + } + + Path layerFile = layerFiles.get(0); + return Optional.of( + CachedLayer.builder() + .setLayerDigest(cacheStorageFiles.getDigestFromFilename(layerFile)) + .setLayerSize(Files.size(layerFile)) + .setLayerBlob(Blobs.from(layerFile)) + .setLayerDiffId(diffId) .build()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index ed2f6ac7b8..5704ef4434 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -251,6 +251,47 @@ CachedLayer writeUncompressed(Blob uncompressedLayerBlob, @Nullable DescriptorDi } } + /** + * Saves a local base image layer. + * + * @param diffId the layer blob's diff ID + * @param compressedBlob the blob to save + * @throws IOException if an I/O exception occurs + */ + CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) throws IOException { + Files.createDirectories(cacheStorageFiles.getLocalDirectory()); + Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); + try (TemporaryDirectory temporaryDirectory = + new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { + Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(temporaryLayerDirectory); + + BlobDescriptor layerBlobDescriptor; + try (OutputStream fileOutputStream = + new BufferedOutputStream(Files.newOutputStream(temporaryLayerFile))) { + layerBlobDescriptor = compressedBlob.writeTo(fileOutputStream); + } + + // Renames the temporary layer file to its digest + // (temp/temp -> temp/) + String fileName = layerBlobDescriptor.getDigest().getHash(); + Path digestLayerFile = temporaryLayerDirectory.resolve(fileName); + moveIfDoesNotExist(temporaryLayerFile, digestLayerFile); + + // Moves the temporary directory to directory named with diff ID + // (temp/ -> /) + Path destination = cacheStorageFiles.getLocalDirectory().resolve(diffId.getHash()); + moveIfDoesNotExist(temporaryLayerDirectory, destination); + + return CachedLayer.builder() + .setLayerDigest(layerBlobDescriptor.getDigest()) + .setLayerDiffId(diffId) + .setLayerSize(layerBlobDescriptor.getSize()) + .setLayerBlob(Blobs.from(destination.resolve(fileName))) + .build(); + } + } + /** * Saves the manifest and container configuration for a V2.2 or OCI image. * @@ -286,7 +327,7 @@ void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTe Path imageDirectory = cacheStorageFiles.getImageDirectory(imageReference); Files.createDirectories(imageDirectory); - try (LockFile ignored1 = LockFile.lock(imageDirectory.resolve("lock"))) { + try (LockFile ignored = LockFile.lock(imageDirectory.resolve("lock"))) { writeMetadata(manifestTemplate, imageDirectory.resolve("manifest.json")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java index 729806da5a..4afc6f09f5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java @@ -18,6 +18,8 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -54,7 +56,9 @@ private static Path getResource(String resource) throws URISyntaxException { } @Before - public void setup() { + public void setup() throws IOException { + Mockito.when(buildConfiguration.getBaseImageLayersCache()) + .thenReturn(Cache.withDirectory(temporaryFolder.newFolder().toPath())); Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(eventHandlers); Mockito.when(progressEventDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) .thenReturn(progressEventDispatcher); @@ -66,7 +70,7 @@ public void setup() { @Test public void testCall_validDocker() throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException { + BadContainerConfigurationFormatException, IOException, CacheCorruptedException { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = new ExtractTarStep(buildConfiguration, dockerBuild, progressEventDispatcherFactory).call(); @@ -91,7 +95,7 @@ public void testCall_validDocker() @Test public void testCall_validTar() throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException { + BadContainerConfigurationFormatException, IOException, CacheCorruptedException { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = new ExtractTarStep(buildConfiguration, tarBuild, progressEventDispatcherFactory).call(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java index bf08828f55..fe217f46d7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -48,7 +48,7 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TEST_CACHE_STORAGE_FILES.getDiffId( + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( Paths.get( "layer", "file", @@ -56,25 +56,25 @@ public void testGetDiffId() throws DigestException, CacheCorruptedException { Assert.assertEquals( DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - TEST_CACHE_STORAGE_FILES.getDiffId( + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( Paths.get("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); } @Test public void testGetDiffId_corrupted() { try { - TEST_CACHE_STORAGE_FILES.getDiffId(Paths.get("not long enough")); + TEST_CACHE_STORAGE_FILES.getDigestFromFilename(Paths.get("not long enough")); Assert.fail("Should have thrown CacheCorruptedException"); } catch (CacheCorruptedException ex) { Assert.assertThat( ex.getMessage(), - CoreMatchers.startsWith("Layer file did not include valid diff ID: not long enough")); + CoreMatchers.startsWith("Layer file did not include valid hash: not long enough")); Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } try { - TEST_CACHE_STORAGE_FILES.getDiffId( + TEST_CACHE_STORAGE_FILES.getDigestFromFilename( Paths.get( "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); Assert.fail("Should have thrown CacheCorruptedException"); @@ -83,7 +83,7 @@ public void testGetDiffId_corrupted() { Assert.assertThat( ex.getMessage(), CoreMatchers.startsWith( - "Layer file did not include valid diff ID: " + "Layer file did not include valid hash: " + "not valid hash bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); Assert.assertThat(ex.getCause(), CoreMatchers.instanceOf(DigestException.class)); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 315358f39f..50fc375f25 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -198,13 +198,56 @@ public void testRetrieve() throws IOException, CacheCorruptedException { Assert.assertThat( ex.getMessage(), CoreMatchers.startsWith( - "No or multiple layer files found for layer with digest " + "No or multiple layer files found for layer hash " + layerDigest.getHash() + " in directory: " + cacheStorageFiles.getLayerDirectory(layerDigest))); } } + @Test + public void testRetrieveTarLayer() throws IOException, CacheCorruptedException { + CacheStorageFiles cacheStorageFiles = + new CacheStorageFiles(temporaryFolder.newFolder().toPath()); + + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + + // Creates the test layer directory. + Path localDirectory = cacheStorageFiles.getLocalDirectory(); + DescriptorDigest layerDigest = layerDigest1; + DescriptorDigest layerDiffId = layerDigest2; + Files.createDirectories(localDirectory.resolve(layerDiffId.getHash())); + try (OutputStream out = + Files.newOutputStream( + localDirectory.resolve(layerDiffId.getHash()).resolve(layerDigest.getHash()))) { + out.write("layerBlob".getBytes(StandardCharsets.UTF_8)); + } + + // Checks that the CachedLayer is retrieved correctly. + Optional optionalCachedLayer = cacheStorageReader.retrieveTarLayer(layerDiffId); + Assert.assertTrue(optionalCachedLayer.isPresent()); + Assert.assertEquals(layerDigest, optionalCachedLayer.get().getDigest()); + Assert.assertEquals(layerDiffId, optionalCachedLayer.get().getDiffId()); + Assert.assertEquals("layerBlob".length(), optionalCachedLayer.get().getSize()); + Assert.assertEquals("layerBlob", Blobs.writeToString(optionalCachedLayer.get().getBlob())); + + // Checks that multiple layer files means the cache is corrupted. + Files.createFile(localDirectory.resolve(layerDiffId.getHash()).resolve(layerDiffId.getHash())); + try { + cacheStorageReader.retrieveTarLayer(layerDiffId); + Assert.fail("Should have thrown CacheCorruptedException"); + + } catch (CacheCorruptedException ex) { + Assert.assertThat( + ex.getMessage(), + CoreMatchers.startsWith( + "No or multiple layer files found for layer hash " + + layerDiffId.getHash() + + " in directory: " + + localDirectory.resolve(layerDiffId.getHash()))); + } + } + @Test public void testSelect_invalidLayerDigest() throws IOException { CacheStorageFiles cacheStorageFiles = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index ecf48e2f40..1a24ab542f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -74,7 +74,7 @@ public void setUp() throws IOException { } @Test - public void testWrite_compressed() throws IOException { + public void testWriteCompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); CachedLayer cachedLayer = @@ -84,7 +84,7 @@ public void testWrite_compressed() throws IOException { } @Test - public void testWrite_uncompressed() throws IOException { + public void testWriteUncompressed() throws IOException { Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); DescriptorDigest layerDigest = getDigest(compress(uncompressedLayerBlob)).getDigest(); DescriptorDigest selector = getDigest(Blobs.from("selector")).getDigest(); @@ -101,6 +101,34 @@ public void testWrite_uncompressed() throws IOException { Assert.assertEquals(layerDigest.getHash(), Blobs.writeToString(Blobs.from(selectorFile))); } + @Test + public void testWriteTarLayer() throws IOException { + Blob uncompressedLayerBlob = Blobs.from("uncompressedLayerBlob"); + DescriptorDigest diffId = getDigest(uncompressedLayerBlob).getDigest(); + + CachedLayer cachedLayer = + new CacheStorageWriter(cacheStorageFiles) + .writeTarLayer(diffId, compress(uncompressedLayerBlob)); + + BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); + + // Verifies cachedLayer is correct. + Assert.assertEquals(layerBlobDescriptor.getDigest(), cachedLayer.getDigest()); + Assert.assertEquals(diffId, cachedLayer.getDiffId()); + Assert.assertEquals(layerBlobDescriptor.getSize(), cachedLayer.getSize()); + Assert.assertArrayEquals( + Blobs.writeToByteArray(uncompressedLayerBlob), + Blobs.writeToByteArray(decompress(cachedLayer.getBlob()))); + + // Verifies that the files are present. + Assert.assertTrue( + Files.exists( + cacheStorageFiles + .getLocalDirectory() + .resolve(cachedLayer.getDiffId().getHash()) + .resolve(cachedLayer.getDigest().getHash()))); + } + @Test public void testWriteMetadata_v21() throws IOException, URISyntaxException, InvalidImageReferenceException { diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 9764e7cec4..fa16447c84 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -159,7 +159,7 @@ private static String buildAndRun( } private static String buildAndRunFromLocalBase( - Path projectRoot, String targetImage, String baseImage) + Path projectRoot, String targetImage, String baseImage, boolean buildTwice) throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(projectRoot.toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); @@ -170,7 +170,20 @@ private static String buildAndRunFromLocalBase( verifier.addCliOption("-X"); verifier.addCliOption("--file=pom-localbase.xml"); verifier.executeGoals(Arrays.asList("clean", "compile")); + if (!buildTwice) { + verifier.executeGoal("jib:build"); + return pullAndRunBuiltImage(targetImage); + } + verifier.executeGoal("jib:build"); + float timeOne = getBuildTimeFromVerifierLog(verifier); + + verifier.resetStreams(); + verifier.executeGoal("jib:build"); + float timeTwo = getBuildTimeFromVerifierLog(verifier); + + String failMessage = "First build time (%s) is not greater than second build time (%s)"; + Assert.assertTrue(String.format(failMessage, timeOne, timeTwo), timeOne > timeTwo); return pullAndRunBuiltImage(targetImage); } @@ -420,7 +433,8 @@ public void testBuild_dockerDaemonBase() buildAndRunFromLocalBase( simpleTestProject.getProjectRoot(), targetImage, - "docker://gcr.io/distroless/java:latest")); + "docker://gcr.io/distroless/java:latest", + false)); } @Test @@ -435,7 +449,8 @@ public void testBuild_tarBase() throws IOException, InterruptedException, Verifi Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", - buildAndRunFromLocalBase(simpleTestProject.getProjectRoot(), targetImage, "tar://" + path)); + buildAndRunFromLocalBase( + simpleTestProject.getProjectRoot(), targetImage, "tar://" + path, true)); } @Test From 1deba991529b538ba9eea09fb0a889ee1551aebe Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 12 Sep 2019 16:34:53 -0400 Subject: [PATCH 0723/2020] Revert Google HTTP Client upgrade (#1980) * Revert Google HTTP Client upgrade * Exclude google-http-client from google-auth-library-oauth2-http --- build.gradle | 10 +- jib-core/build.gradle | 10 +- .../cloud/tools/jib/http/Connection.java | 56 ++++-- .../ConnectionWithProxyCredentialsTest.java | 167 ++++++++++++++++++ .../cloud/tools/jib/http/MockConnection.java | 2 +- jib-gradle-plugin/build.gradle | 9 - jib-plugins-common/build.gradle | 5 +- 7 files changed, 217 insertions(+), 42 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java diff --git a/build.gradle b/build.gradle index cbc802143d..983cb4db3c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,16 +46,10 @@ subprojects { // For Google libraries, check , , , // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml // for best compatibility. - GOOGLE_HTTP_CLIENT: '1.31.0', - GOOGLE_HTTP_CLIENT_APACHE_V2: '1.31.0', + GOOGLE_HTTP_CLIENT: '1.27.0', GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.16.2', GUAVA: '28.0-jre', - // TODO: remove once https://github.com/googleapis/google-http-java-client/issues/795 is fixed and released. - // Forcing to downgrade this to 4.5.6 fixes https://github.com/GoogleContainerTools/jib/issues/1914 - // However, #795 and upgrading httpclient alone may not fix #1914. We may need to explicitly disable URI - // normalization as discussed in #795. - APACHE_HTTP_CLIENT_OVERRIDE: '4.5.6', COMMONS_COMPRESS: '1.18', JACKSON_DATABIND: '2.9.9.2', ASM: '7.0', @@ -70,7 +64,7 @@ subprojects { // Use this to ensure we correctly override transitive dependencies // TODO: There might be a plugin that does this task ensureTransitiveDependencyOverrides { - def rules = ["httpclient": dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE] + def rules = ["google-http-client": dependencyVersions.GOOGLE_HTTP_CLIENT] doLast { configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> def dependency = artifact.moduleVersion.id diff --git a/jib-core/build.gradle b/jib-core/build.gradle index dd158cd31f..624a319697 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -4,16 +4,10 @@ plugins { } dependencies { - implementation("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - implementation("com.google.http-client:google-http-client-apache-v2:${dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2}") { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } + implementation "com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}" implementation("com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}") { - exclude group: "org.apache.httpcomponents", module: "httpclient" + exclude group: "com.google.http-client", module: "google-http-client" } - implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" implementation "org.apache.commons:commons-compress:${dependencyVersions.COMMONS_COMPRESS}" implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 834c87779f..6e678cbe21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -22,8 +22,7 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.apache.v2.ApacheHttpTransport; -import com.google.api.client.util.SslUtils; +import com.google.api.client.http.apache.ApacheHttpTransport; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.Closeable; @@ -32,8 +31,9 @@ import java.security.GeneralSecurityException; import java.util.function.Function; import javax.annotation.Nullable; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.DefaultHttpClient; /** * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than @@ -61,7 +61,9 @@ public static Function getConnectionFactory() { // // A new ApacheHttpTransport needs to be created for each connection because otherwise HTTP // connection persistence causes the connection to throw NoHttpResponseException. - return url -> new Connection(url, new ApacheHttpTransport()); + ApacheHttpTransport transport = new ApacheHttpTransport(); + addProxyCredentials(transport); + return url -> new Connection(url, transport); } /** @@ -72,14 +74,44 @@ public static Function getConnectionFactory() { */ public static Function getInsecureConnectionFactory() throws GeneralSecurityException { - HttpClientBuilder httpClientBuilder = - ApacheHttpTransport.newDefaultHttpClientBuilder() - .setSSLSocketFactory(null) // creates new factory with the SSLContext given below - .setSSLContext(SslUtils.trustAllSSLContext()) - .setSSLHostnameVerifier(new NoopHostnameVerifier()); - // Do not use NetHttpTransport. See comments in getConnectionFactory for details. - return url -> new Connection(url, new ApacheHttpTransport(httpClientBuilder.build())); + ApacheHttpTransport transport = + new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); + addProxyCredentials(transport); + return url -> new Connection(url, transport); + } + + /** + * Registers proxy credentials onto transport client, in order to deal with proxies that require + * basic authentication. + * + * @param transport Apache HTTP transport + */ + @VisibleForTesting + static void addProxyCredentials(ApacheHttpTransport transport) { + addProxyCredentials(transport, "https"); + addProxyCredentials(transport, "http"); + } + + private static void addProxyCredentials(ApacheHttpTransport transport, String protocol) { + Preconditions.checkArgument(protocol.equals("http") || protocol.equals("https")); + + String proxyHost = System.getProperty(protocol + ".proxyHost"); + String proxyUser = System.getProperty(protocol + ".proxyUser"); + String proxyPassword = System.getProperty(protocol + ".proxyPassword"); + if (proxyHost == null || proxyUser == null || proxyPassword == null) { + return; + } + + String defaultProxyPort = protocol.equals("http") ? "80" : "443"; + int proxyPort = Integer.parseInt(System.getProperty(protocol + ".proxyPort", defaultProxyPort)); + + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + httpClient + .getCredentialsProvider() + .setCredentials( + new AuthScope(proxyHost, proxyPort), + new UsernamePasswordCredentials(proxyUser, proxyPassword)); } private HttpRequestFactory requestFactory; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java new file mode 100644 index 0000000000..0b86e54a59 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link Connection} with setting proxy credentials. */ +public class ConnectionWithProxyCredentialsTest { + + private static final ImmutableList proxyProperties = + ImmutableList.of( + "http.proxyHost", + "http.proxyPort", + "http.proxyUser", + "http.proxyPassword", + "https.proxyHost", + "https.proxyPort", + "https.proxyUser", + "https.proxyPassword"); + + // HashMap to allow saving null values. + private final HashMap savedProperties = new HashMap<>(); + + private final ApacheHttpTransport transport = new ApacheHttpTransport(); + + @Before + public void setUp() { + proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); + proxyProperties.stream().forEach(key -> System.clearProperty(key)); + } + + @After + public void tearDown() { + Consumer> restoreProperty = + entry -> { + if (entry.getValue() == null) { + System.clearProperty(entry.getKey()); + } else { + System.setProperty(entry.getKey(), entry.getValue()); + } + }; + savedProperties.entrySet().stream().forEach(restoreProperty); + } + + @Test + public void testAddProxyCredentials_undefined() { + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyPort", "1080"); + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyPort", "1443"); + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials httpCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); + Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); + Assert.assertEquals("pass", httpCredentials.getPassword()); + + Credentials httpsCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 1443)); + Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); + Assert.assertEquals("s-pass", httpsCredentials.getPassword()); + } + + @Test + public void testAddProxyCredentials_defaultPorts() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials httpCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); + Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); + Assert.assertEquals("pass", httpCredentials.getPassword()); + + Credentials httpsCredentials = + httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 443)); + Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); + Assert.assertEquals("s-pass", httpsCredentials.getPassword()); + } + + @Test + public void testAddProxyCredentials_hostUndefined() { + System.setProperty("http.proxyUser", "user"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyUser", "s-user"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials_userUndefined() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyPassword", "pass"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyPassword", "s-pass"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } + + @Test + public void testAddProxyCredentials_passwordUndefined() { + System.setProperty("http.proxyHost", "http://localhost"); + System.setProperty("http.proxyUser", "user"); + + System.setProperty("https.proxyHost", "https://host.com"); + System.setProperty("https.proxyUser", "s-user"); + + Connection.addProxyCredentials(transport); + DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); + Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); + Assert.assertNull(credentials); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java index 2ec95c638a..3470ba8d19 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java @@ -17,7 +17,7 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.http.apache.ApacheHttpTransport; import java.io.IOException; import java.util.function.BiFunction; diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index e3c6b8045b..24d81cd15c 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -14,18 +14,9 @@ repositories { } dependencies { - sourceProject project(":jib-core") sourceProject project(":jib-plugins-common") - implementation ("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" - - implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" - implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" - testImplementation "junit:junit:${dependencyVersions.JUNIT}" testImplementation "org.mockito:mockito-core:${dependencyVersions.MOCKITO_CORE}" testImplementation "org.slf4j:slf4j-api:${dependencyVersions.SLF4J_API}" diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 413febd980..e35a0ca220 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -1,9 +1,6 @@ dependencies { implementation project(':jib-core') - implementation ("com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}") { - exclude group: "org.apache.httpcomponents", module: "httpclient" - } - implementation "org.apache.httpcomponents:httpclient:${dependencyVersions.APACHE_HTTP_CLIENT_OVERRIDE}" + implementation "com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}" implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" From 792a9acf5d2d23eed22ba2f6b25be957915afa27 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 12 Sep 2019 17:21:11 -0400 Subject: [PATCH 0724/2020] Do not use guava-HEAD-jre-SNAPSHOT (#1979) --- .../cloud/tools/jib/gradle/FilesTaskTest.java | 13 +++------- .../tools/jib/gradle/FilesTaskV2Test.java | 12 +++------ .../complex-service/build.gradle | 9 +++++-- .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 24 ++++++++++++++++++ .../tiny-test-lib-0.0.1-SNAPSHOT.jar | Bin 0 -> 625 bytes .../tiny-test-lib-0.0.1-SNAPSHOT.pom | 9 +++++++ .../tiny-test-lib/maven-metadata-local.xml | 11 ++++++++ .../jib/maven/skaffold/FilesMojoTest.java | 2 +- .../jib/maven/skaffold/FilesMojoV2Test.java | 2 +- .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 24 ++++++++++++++++++ .../tiny-test-lib-0.0.1-SNAPSHOT.jar | Bin 0 -> 625 bytes .../tiny-test-lib-0.0.1-SNAPSHOT.pom | 9 +++++++ .../tiny-test-lib/maven-metadata-local.xml | 11 ++++++++ .../projects/multi/complex-service/pom.xml | 19 +++++++++++--- 14 files changed, 121 insertions(+), 24 deletions(-) create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java index 678b1136d5..cfdb52efb3 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java @@ -27,7 +27,6 @@ import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; -import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -124,13 +123,9 @@ public void testFilesTask_multiProjectComplexService() throws IOException { complexServiceRoot.resolve("src/main/other-jib"), libRoot.resolve("build.gradle"), libRoot.resolve("src/main/resources"), - libRoot.resolve("src/main/java")); - assertPathListsAreEqual(expected, result.subList(0, 11)); - - // guava jar is in a temporary-looking directory, so don't do a full match here - Assert.assertThat( - result.get(result.size() - 1).toString(), - CoreMatchers.endsWith("guava-HEAD-jre-SNAPSHOT.jar")); - Assert.assertEquals(12, result.size()); + libRoot.resolve("src/main/java"), + complexServiceRoot.resolve( + "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar")); + assertPathListsAreEqual(expected, result); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java index 815c29206f..6aac4681af 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java @@ -132,14 +132,10 @@ public void testFilesTask_multiProjectComplexService() throws IOException { complexServiceRoot.resolve("src/main/java"), complexServiceRoot.resolve("src/main/other-jib"), libRoot.resolve("src/main/resources"), - libRoot.resolve("src/main/java")), - result.getInputs().subList(0, 6)); - // guava jar is in a temporary-looking directory, so we need to do some extra processing to - // match this - Assert.assertThat( - result.getInputs().get(result.getInputs().size() - 1), - CoreMatchers.endsWith("guava-HEAD-jre-SNAPSHOT.jar")); - Assert.assertEquals(7, result.getInputs().size()); + libRoot.resolve("src/main/java"), + complexServiceRoot.resolve( + "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar")), + result.getInputs()); Assert.assertEquals(result.getIgnore().size(), 0); } } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle index eacc676f1c..c4f408f374 100644 --- a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle @@ -8,8 +8,13 @@ targetCompatibility = 1.8 repositories { mavenCentral() + // The local repo contains one tiny test library installed with + // mvn org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file \ + // -Dfile=tiny.jar -DlocalRepositoryPath=local-m2-repo/ \ + // -DgroupId=com.google.cloud.tools -DartifactId=tiny-test-lib \ + // -Dversion=0.0.1-SNAPSHOT -Dpackaging=jar maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' + url "file:" + project.projectDir + "/local-m2-repo" } } @@ -33,5 +38,5 @@ sourceSets { dependencies { compile project(':lib') compile 'org.apache.commons:commons-io:1.3.2' - compile 'com.google.guava:guava:HEAD-jre-SNAPSHOT' + compile 'com.google.cloud.tools:tiny-test-lib:0.0.1-SNAPSHOT' } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..9984010307 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,24 @@ + + + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + + + true + + 20190911205316 + + + jar + 0.0.1-SNAPSHOT + 20190911205316 + + + pom + 0.0.1-SNAPSHOT + 20190911205316 + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..d1517b4150eca0a349f14ef1e0ebb69c97d766b6 GIT binary patch literal 625 zcmWIWW@Zs#;Nak3Fl^KIXFvj+3@i-3t|5-Po_=on|4uP5Ff#;rvvYt{FhP|C;M6Pv zQ~}rQ>*(j{<{BKL=j-;__snS@Z(Y5MyxzK6=gyqp9At3C_`%a6JuhD!Pv48Bt5`TA zUPvC1mXy@Y*s(+{Rqa{kr=pKVj9{zPTqA=mfd+#x!s@rJsP1M5$~x*L=Oh*vr}pjT zIuyXu>i=f`wO_`Ze0+IV59;N0DKt%fv0#VA-B(LvE^1G?_Pt0*riMY@>&)8b=EgZH z?fQ!AzdzF9taG`y!~e~V^o1r1 zMc;|J7m8l=e4!GRzVgz^6FR?Md(V6KG)D5qTs|I=18vR=G#CDijymeT^ZW(Z&i@91 zOp+6>K3#cu>JF>F)-R6n9!xIfo*dvf^K)^j9;d-njn^BwGA zMkWyk)Tn~R11PFc0X*zM5f|W%suejLK@o)jwm>FaD^j!}n*a)LWQT#m8v%9znb6P= T@MdKLNiYN9CLq0%5yS%k_9v-2 literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..ea93ba9238 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + POM was created from install:install-file + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml new file mode 100644 index 0000000000..5be427872a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.google.cloud.tools + tiny-test-lib + + + 0.0.1-SNAPSHOT + + 20190911205316 + + diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java index 3be6104cce..f754855143 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java @@ -137,6 +137,6 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti // this test expects standard .m2 locations Paths.get( System.getProperty("user.home"), - ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar"))); + ".m2/repository/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar"))); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java index 18cc60cd1b..5dc754b9a8 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2Test.java @@ -153,7 +153,7 @@ public void testFilesMojo_multiModuleComplexService() throws VerificationExcepti // this test expects standard .m2 locations Paths.get( System.getProperty("user.home"), - ".m2/repository/com/google/guava/guava/HEAD-jre-SNAPSHOT/guava-HEAD-jre-SNAPSHOT.jar") + ".m2/repository/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar") .toString())); } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 0000000000..9984010307 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,24 @@ + + + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + + + true + + 20190911205316 + + + jar + 0.0.1-SNAPSHOT + 20190911205316 + + + pom + 0.0.1-SNAPSHOT + 20190911205316 + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..d1517b4150eca0a349f14ef1e0ebb69c97d766b6 GIT binary patch literal 625 zcmWIWW@Zs#;Nak3Fl^KIXFvj+3@i-3t|5-Po_=on|4uP5Ff#;rvvYt{FhP|C;M6Pv zQ~}rQ>*(j{<{BKL=j-;__snS@Z(Y5MyxzK6=gyqp9At3C_`%a6JuhD!Pv48Bt5`TA zUPvC1mXy@Y*s(+{Rqa{kr=pKVj9{zPTqA=mfd+#x!s@rJsP1M5$~x*L=Oh*vr}pjT zIuyXu>i=f`wO_`Ze0+IV59;N0DKt%fv0#VA-B(LvE^1G?_Pt0*riMY@>&)8b=EgZH z?fQ!AzdzF9taG`y!~e~V^o1r1 zMc;|J7m8l=e4!GRzVgz^6FR?Md(V6KG)D5qTs|I=18vR=G#CDijymeT^ZW(Z&i@91 zOp+6>K3#cu>JF>F)-R6n9!xIfo*dvf^K)^j9;d-njn^BwGA zMkWyk)Tn~R11PFc0X*zM5f|W%suejLK@o)jwm>FaD^j!}n*a)LWQT#m8v%9znb6P= T@MdKLNiYN9CLq0%5yS%k_9v-2 literal 0 HcmV?d00001 diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom new file mode 100644 index 0000000000..ea93ba9238 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT + POM was created from install:install-file + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml new file mode 100644 index 0000000000..5be427872a --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.google.cloud.tools + tiny-test-lib + + + 0.0.1-SNAPSHOT + + 20190911205316 + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml index 96458e4518..3b4036e300 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml @@ -75,6 +75,19 @@ + + + + local-m2-repo + file:${project.basedir}/local-m2-repo + + + com.jib.test @@ -89,9 +102,9 @@ compile - com.google.guava - guava - HEAD-jre-SNAPSHOT + com.google.cloud.tools + tiny-test-lib + 0.0.1-SNAPSHOT compile From 60101dfea83c9cc56b77a16c0254656b8c9f2b50 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 12 Sep 2019 17:42:45 -0400 Subject: [PATCH 0725/2020] CHANGELOG for 1.6.0 (#1985) --- jib-core/CHANGELOG.md | 2 ++ jib-gradle-plugin/CHANGELOG.md | 3 +++ jib-maven-plugin/CHANGELOG.md | 3 +++ 3 files changed, 8 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index e2b2f25442..72bdc6881b 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -18,6 +18,8 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed an issue interacting with certain registries due to changes to URL handling in the underlying Apache HttpClient library. ([#1924](https://github.com/GoogleContainerTools/jib/issues/1924)) +- Fixed the regression of slow network operations introduced at 0.10.1. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when the `Containerizer` is set to allow insecure registries. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) ## 0.10.1 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 34a3f88e2d..fd796905e1 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -15,6 +15,9 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed the regression of slow network operations introduced at 1.5.0. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when `allowInsecureRegistries` is set. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) + ## 1.5.1 ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 37371ea64e..26d64c3fee 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -15,6 +15,9 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed the regression of slow network operations introduced at 1.5.0. ([#1980](https://github.com/GoogleContainerTools/jib/pull/1980)) +- Fixed an issue where connection timeout sometimes fell back to attempting plain HTTP (non-HTTPS) requests when `` is set. ([#1949](https://github.com/GoogleContainerTools/jib/pull/1949)) + ## 1.5.1 ### Fixed From f27464e06b7b7db79eb32dd8f8ab12105ade5ed6 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 13 Sep 2019 10:29:30 -0400 Subject: [PATCH 0726/2020] Better dependency handling in build (#1984) --- jib-plugins-common/build.gradle | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index e35a0ca220..20a94a39d4 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -1,8 +1,11 @@ dependencies { implementation project(':jib-core') - implementation "com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}" - implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" - implementation "com.fasterxml.jackson.core:jackson-databind:${dependencyVersions.JACKSON_DATABIND}" + // since jib core doesn't export dependencies to a compile scope + // (they are "runtime"), just grab them manually. This means we don't have + // to synchronize dependencies between the two projects -- we don't + // want to use the sourceProject helper because it does things we don't want + // for an unpublished library. + implementation project(':jib-core').configurations.implementation.dependencies testImplementation "junit:junit:${dependencyVersions.JUNIT}" testImplementation "org.mockito:mockito-core:${dependencyVersions.MOCKITO_CORE}" From 92ee431a3636968722cc2fc3ad4e64441d664a91 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 11:09:08 -0400 Subject: [PATCH 0727/2020] Move docker client configuration to all tasks and goals (#1981) --- jib-gradle-plugin/CHANGELOG.md | 2 + .../tools/jib/gradle/BuildDockerTask.java | 33 +++++++++++++--- .../jib/gradle/DockerClientParameters.java | 9 +++-- .../jib/gradle/GradleRawConfiguration.java | 10 +++++ .../cloud/tools/jib/gradle/JibExtension.java | 12 ++++++ .../gradle/GradleRawConfigurationTest.java | 11 ++++++ .../tools/jib/gradle/JibExtensionTest.java | 24 ++++++++++++ jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 38 ------------------- .../jib/maven/JibPluginConfiguration.java | 27 +++++++++++++ .../jib/maven/MavenRawConfiguration.java | 10 +++++ .../jib/maven/JibPluginConfigurationTest.java | 16 ++++++++ .../jib/maven/MavenRawConfigurationTest.java | 8 ++++ .../common/PluginConfigurationProcessor.java | 21 +++++----- .../jib/plugins/common/RawConfiguration.java | 4 ++ 15 files changed, 170 insertions(+), 56 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index fd796905e1..a84c657679 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. - To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- `jib.dockerClient` is now configurable on all tasks, not just `jibDockerBuild`. ([#1932](https://github.com/GoogleContainerTools/jib/issues/1932)) +- `jibDockerBuild.dockerClient` is deprecated in favor of `jib.dockerClient`. ### Fixed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 2803254911..cf347676ad 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -33,6 +33,7 @@ import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; +import java.util.Map; import javax.annotation.Nullable; import org.gradle.api.Action; import org.gradle.api.DefaultTask; @@ -75,10 +76,12 @@ public void setTargetImage(String targetImage) { @Nested @Optional + @Deprecated public DockerClientParameters getDockerClient() { return dockerClientParameters; } + @Deprecated public void dockerClient(Action action) { action.execute(dockerClientParameters); } @@ -87,7 +90,31 @@ public void dockerClient(Action action) { public void buildDocker() throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, MainClassInferenceException { - Path dockerExecutable = dockerClientParameters.getExecutablePath(); + Preconditions.checkNotNull(jibExtension); + + // Check deprecated parameters + Path dockerExecutable = jibExtension.getDockerClient().getExecutablePath(); + Map dockerEnvironment = jibExtension.getDockerClient().getEnvironment(); + if (getDockerClient().getExecutable() != null) { + jibExtension.getDockerClient().setExecutable(getDockerClient().getExecutable()); + getProject() + .getLogger() + .warn( + "'jibDockerBuild.dockerClient.executable' is deprecated; use 'jib.dockerClient.executable' instead."); + } + if (!getDockerClient().getEnvironment().isEmpty()) { + jibExtension.getDockerClient().setEnvironment(getDockerClient().getEnvironment()); + getProject() + .getLogger() + .warn( + "'jibDockerBuild.dockerClient.environment' is deprecated; use 'jib.dockerClient.environment' instead."); + } + if ((getDockerClient().getExecutable() != null && dockerExecutable != null) + || (!getDockerClient().getEnvironment().isEmpty() && !dockerEnvironment.isEmpty())) { + throw new GradleException( + "Cannot configure 'jibDockerBuild.dockerClient' and 'jib.dockerClient' simultaneously"); + } + boolean isDockerInstalled = dockerExecutable == null ? DockerClient.isDefaultDockerInstalled() @@ -97,8 +124,6 @@ public void buildDocker() HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - // Asserts required @Input parameters are not null. - Preconditions.checkNotNull(jibExtension); TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); @@ -109,8 +134,6 @@ public void buildDocker() new GradleRawConfiguration(jibExtension), ignored -> java.util.Optional.empty(), projectProperties, - dockerClientParameters.getExecutablePath(), - dockerClientParameters.getEnvironment(), new GradleHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) .runBuild(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java index d9b06ff3d5..c92892f219 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.Map; import javax.annotation.Nullable; import org.gradle.api.tasks.Input; @@ -33,7 +34,7 @@ public class DockerClientParameters { @Nullable private Path executable; - @Nullable private Map environment; + private Map environment = Collections.emptyMap(); @Input @Nullable @@ -48,6 +49,9 @@ public String getExecutable() { @Internal @Nullable Path getExecutablePath() { + if (System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE) != null) { + return Paths.get(System.getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE)); + } return executable; } @@ -56,7 +60,6 @@ public void setExecutable(String executable) { } @Input - @Nullable @Optional public Map getEnvironment() { if (System.getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT) != null) { @@ -66,7 +69,7 @@ public Map getEnvironment() { return environment; } - public void setEnvironment(@Nullable Map environment) { + public void setEnvironment(Map environment) { this.environment = environment; } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index d5517d7744..6495cf200b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -171,6 +171,16 @@ public Map getExtraDirectoryPermissions() { return TaskCommon.convertPermissionsMap(jibExtension.getExtraDirectories().getPermissions()); } + @Override + public Optional getDockerExecutable() { + return Optional.ofNullable(jibExtension.getDockerClient().getExecutablePath()); + } + + @Override + public Map getDockerEnvironment() { + return jibExtension.getDockerClient().getEnvironment(); + } + @Override public String getContainerizingMode() { return jibExtension.getContainerizingMode(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 5ce850c3dd..aa3676dac6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -71,6 +71,7 @@ public class JibExtension { private final TargetImageParameters to; private final ContainerParameters container; private final ExtraDirectoriesParameters extraDirectories; + private final DockerClientParameters dockerClient; private final Property allowInsecureRegistries; private final Property containerizingMode; @@ -84,6 +85,7 @@ public JibExtension(Project project) { to = objectFactory.newInstance(TargetImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project, this); + dockerClient = objectFactory.newInstance(DockerClientParameters.class); allowInsecureRegistries = objectFactory.property(Boolean.class); containerizingMode = objectFactory.property(String.class); @@ -116,6 +118,10 @@ public void extraDirectories(Action action) action.execute(extraDirectories); } + public void dockerClient(Action action) { + action.execute(dockerClient); + } + @Deprecated // for the deprecated "jib.extraDirectory" config parameter public void setExtraDirectory(File extraDirectory) { @@ -162,6 +168,12 @@ public ExtraDirectoriesParameters getExtraDirectories() { return extraDirectories; } + @Nested + @Optional + public DockerClientParameters getDockerClient() { + return dockerClient; + } + @Input @Optional boolean getAllowInsecureRegistries() { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index 5fcab8e405..8606f509b4 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -37,6 +38,7 @@ public void testGetters() { BaseImageParameters baseImageParameters = Mockito.mock(BaseImageParameters.class); TargetImageParameters targetImageParameters = Mockito.mock(TargetImageParameters.class); ContainerParameters containerParameters = Mockito.mock(ContainerParameters.class); + DockerClientParameters dockerClientParameters = Mockito.mock(DockerClientParameters.class); Mockito.when(authParameters.getUsername()).thenReturn("user"); Mockito.when(authParameters.getPassword()).thenReturn("password"); @@ -47,6 +49,7 @@ public void testGetters() { Mockito.when(jibExtension.getFrom()).thenReturn(baseImageParameters); Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); + Mockito.when(jibExtension.getDockerClient()).thenReturn(dockerClientParameters); Mockito.when(jibExtension.getAllowInsecureRegistries()).thenReturn(true); Mockito.when(baseImageParameters.getCredHelper()).thenReturn("gcr"); @@ -70,6 +73,10 @@ public void testGetters() { Mockito.when(containerParameters.getUser()).thenReturn("admin:wheel"); Mockito.when(containerParameters.getFilesModificationTime()).thenReturn("2011-12-03T22:42:05Z"); + Mockito.when(dockerClientParameters.getExecutablePath()).thenReturn(Paths.get("test")); + Mockito.when(dockerClientParameters.getEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); + GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); AuthProperty fromAuth = rawConfiguration.getFromAuth(); @@ -98,5 +105,9 @@ public void testGetters() { Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); + Assert.assertEquals(Paths.get("test"), rawConfiguration.getDockerExecutable().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("docker", "client")), + rawConfiguration.getDockerEnvironment()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 24cb345a82..069fdc48c2 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -225,6 +225,21 @@ public void testExtraDirectories_fileListForPaths() { testJibExtension.getExtraDirectories().getPaths()); } + @Test + public void testDockerClient() { + testJibExtension.dockerClient( + dockerClient -> { + dockerClient.setExecutable("test-executable"); + dockerClient.setEnvironment(ImmutableMap.of("key1", "val1", "key2", "val2")); + }); + + Assert.assertEquals( + Paths.get("test-executable"), testJibExtension.getDockerClient().getExecutablePath()); + Assert.assertEquals( + ImmutableMap.of("key1", "val1", "key2", "val2"), + testJibExtension.getDockerClient().getEnvironment()); + } + @Test public void testProperties() { System.setProperty("jib.from.image", "fromImage"); @@ -279,6 +294,7 @@ public void testProperties() { "2011-12-03T22:42:05Z", testJibExtension.getContainer().getFilesModificationTime()); System.setProperty("jib.containerizingMode", "packaged"); Assert.assertEquals("packaged", testJibExtension.getContainerizingMode()); + System.setProperty("jib.extraDirectories.paths", "/foo,/bar/baz"); Assert.assertEquals( Arrays.asList(Paths.get("/foo"), Paths.get("/bar/baz")), @@ -287,6 +303,14 @@ public void testProperties() { Assert.assertEquals( ImmutableMap.of("/foo/bar", "707", "/baz", "456"), testJibExtension.getExtraDirectories().getPermissions()); + + System.setProperty("jib.dockerClient.executable", "test-exec"); + Assert.assertEquals( + Paths.get("test-exec"), testJibExtension.getDockerClient().getExecutablePath()); + System.setProperty("jib.dockerClient.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), + testJibExtension.getDockerClient().getEnvironment()); } @Test diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 26d64c3fee..de9546e6ab 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) +- `` is now configurable on all goals, not just `jib:dockerBuild`. ([#1932](https://github.com/GoogleContainerTools/jib/issues/1932)) ### Fixed diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 8b76f8a801..299ade5ea4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; -import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; import com.google.cloud.tools.jib.plugins.common.InvalidAppRootException; @@ -33,16 +32,11 @@ import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; -import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; -import javax.annotation.Nullable; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; /** Builds a container image and exports to the default Docker daemon. */ @@ -51,21 +45,9 @@ requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) public class BuildDockerMojo extends JibPluginConfiguration { - /** - * Object that configures the Docker executable and the additional environment variables to use - * when executing the executable. - */ - public static class DockerClientConfiguration { - - @Nullable @Parameter private File executable; - @Nullable @Parameter private Map environment; - } - @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon failed"; - @Parameter private DockerClientConfiguration dockerClient = new DockerClientConfiguration(); - @Override public void execute() throws MojoExecutionException, MojoFailureException { checkJibVersion(); @@ -108,8 +90,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { new MavenSettingsServerCredentials( getSession().getSettings(), getSettingsDecrypter()), projectProperties, - dockerExecutable, - getDockerClientEnvironment(), new MavenHelpfulSuggestions(HELPFUL_SUGGESTIONS_PREFIX)) .runBuild(); @@ -168,22 +148,4 @@ public void execute() throws MojoExecutionException, MojoFailureException { getLog().info(""); } } - - @Nullable - private Path getDockerClientExecutable() { - String property = getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE); - if (property != null) { - return Paths.get(property); - } - return dockerClient.executable == null ? null : dockerClient.executable.toPath(); - } - - @Nullable - private Map getDockerClientEnvironment() { - String property = getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT); - if (property != null) { - return ConfigurationPropertyValidator.parseMapProperty(property); - } - return dockerClient.environment; - } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 9f1304e4d6..569059ae1c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -225,6 +225,14 @@ public List getPaths() { } } + /** Configuration for the {@code dockerClient} parameter. */ + public static class DockerClientParameters { + + @Nullable @Parameter private File executable; + + @Parameter private Map environment = Collections.emptyMap(); + } + @Nullable @Parameter(defaultValue = "${session}", readonly = true) private MavenSession session; @@ -250,6 +258,8 @@ public List getPaths() { // this parameter is cloned in FilesMojo @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); + @Parameter private DockerClientParameters dockerClient = new DockerClientParameters(); + @Parameter(property = PropertyNames.ALLOW_INSECURE_REGISTRIES) private boolean allowInsecureRegistries; @@ -657,6 +667,23 @@ List getExtraDirectoryPermissions() { : extraDirectory.permissions; } + @Nullable + Path getDockerClientExecutable() { + String property = getProperty(PropertyNames.DOCKER_CLIENT_EXECUTABLE); + if (property != null) { + return Paths.get(property); + } + return dockerClient.executable == null ? null : dockerClient.executable.toPath(); + } + + Map getDockerClientEnvironment() { + String property = getProperty(PropertyNames.DOCKER_CLIENT_ENVIRONMENT); + if (property != null) { + return ConfigurationPropertyValidator.parseMapProperty(property); + } + return dockerClient.environment; + } + boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index d08649986e..7ced9766c0 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -176,6 +176,16 @@ public Map getExtraDirectoryPermissions() { return MojoCommon.convertPermissionsList(jibPluginConfiguration.getExtraDirectoryPermissions()); } + @Override + public Optional getDockerExecutable() { + return Optional.ofNullable(jibPluginConfiguration.getDockerClientExecutable()); + } + + @Override + public Map getDockerEnvironment() { + return jibPluginConfiguration.getDockerClientEnvironment(); + } + @Override public String getContainerizingMode() { return jibPluginConfiguration.getContainerizingMode(); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 14267a1c20..6c2a97154a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -142,6 +142,14 @@ public void testSystemProperties() { Assert.assertEquals("123", permissions.get(0).getMode().get()); Assert.assertEquals("/another/file", permissions.get(1).getFile().get()); Assert.assertEquals("456", permissions.get(1).getMode().get()); + + sessionProperties.put("jib.dockerClient.executable", "test-exec"); + Assert.assertEquals( + Paths.get("test-exec"), testPluginConfiguration.getDockerClientExecutable()); + sessionProperties.put("jib.dockerClient.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), + testPluginConfiguration.getDockerClientEnvironment()); } @Test @@ -217,6 +225,14 @@ public void testPomProperties() { Assert.assertEquals("123", permissions.get(0).getMode().get()); Assert.assertEquals("/another/file", permissions.get(1).getFile().get()); Assert.assertEquals("456", permissions.get(1).getMode().get()); + + project.getProperties().setProperty("jib.dockerClient.executable", "test-exec"); + Assert.assertEquals( + Paths.get("test-exec"), testPluginConfiguration.getDockerClientExecutable()); + project.getProperties().setProperty("jib.dockerClient.environment", "env1=val1,env2=val2"); + Assert.assertEquals( + ImmutableMap.of("env1", "val1", "env2", "val2"), + testPluginConfiguration.getDockerClientEnvironment()); } @Test diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index e881854202..1721bb6d13 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -78,6 +79,9 @@ public void testGetters() { Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); Mockito.when(jibPluginConfiguration.getFilesModificationTime()) .thenReturn("2011-12-03T22:42:05Z"); + Mockito.when(jibPluginConfiguration.getDockerClientExecutable()).thenReturn(Paths.get("test")); + Mockito.when(jibPluginConfiguration.getDockerClientEnvironment()) + .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); @@ -107,6 +111,10 @@ public void testGetters() { Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); + Assert.assertEquals(Paths.get("test"), rawConfiguration.getDockerExecutable().get()); + Assert.assertEquals( + new HashMap<>(ImmutableMap.of("docker", "client")), + rawConfiguration.getDockerEnvironment()); Mockito.verifyNoMoreInteractions(eventHandlers); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 977c426b2f..27c378d86d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -46,7 +46,6 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -62,8 +61,6 @@ public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, ProjectProperties projectProperties, - @Nullable Path dockerExecutable, - @Nullable Map dockerEnvironment, HelpfulSuggestions helpfulSuggestions) throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, @@ -73,12 +70,10 @@ public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); DockerDaemonImage targetImage = DockerDaemonImage.named(targetImageReference); - if (dockerExecutable != null) { - targetImage.setDockerExecutable(dockerExecutable); - } - if (dockerEnvironment != null) { - targetImage.setDockerEnvironment(dockerEnvironment); + if (rawConfiguration.getDockerExecutable().isPresent()) { + targetImage.setDockerExecutable(rawConfiguration.getDockerExecutable().get()); } + targetImage.setDockerEnvironment(rawConfiguration.getDockerEnvironment()); Containerizer containerizer = Containerizer.to(targetImage); JibContainerBuilder jibContainerBuilder = @@ -275,10 +270,16 @@ static JavaContainerBuilder getJavaContainerBuilderWithBaseImage( throw new IncompatibleBaseImageJavaVersionException(11, javaVersion); } + ImageReference baseImageReference = ImageReference.parse(prefixRemoved); if (baseImageConfig.startsWith(Jib.DOCKER_DAEMON_IMAGE_PREFIX)) { - return JavaContainerBuilder.from(baseImageConfig); + DockerDaemonImage dockerDaemonImage = + DockerDaemonImage.named(baseImageReference) + .setDockerEnvironment(rawConfiguration.getDockerEnvironment()); + if (rawConfiguration.getDockerExecutable().isPresent()) { + dockerDaemonImage.setDockerExecutable(rawConfiguration.getDockerExecutable().get()); + } + return JavaContainerBuilder.from(dockerDaemonImage); } - ImageReference baseImageReference = ImageReference.parse(prefixRemoved); RegistryImage baseImage = RegistryImage.named(baseImageReference); configureCredentialRetrievers( rawConfiguration, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 72f308c8e8..0ea66e8ce8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -86,5 +86,9 @@ public interface RawConfiguration { Map getExtraDirectoryPermissions(); + Optional getDockerExecutable(); + + Map getDockerEnvironment(); + String getContainerizingMode(); } From 5b39a34f4f68db57e6b105117100a0d693c15e05 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 13 Sep 2019 12:02:45 -0400 Subject: [PATCH 0728/2020] Fix release by ignoring SNAPSHOT deps on core/plugins-common (#1987) the SNAPSHOT dependencies are a consequence of the monolith, the release plugin just needs to ingore those projects, ideally the plugin wouldn't care that compileOnly dependencies are SNAPSHOTS but whatever, they might have a reason for that. --- jib-gradle-plugin/build.gradle | 1 + jib-maven-plugin/build.gradle | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 24d81cd15c..0404262361 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -33,6 +33,7 @@ dependencies { // Prepare release release { tagTemplate = 'v$version-gradle' + ignoredSnapshotDependencies = ["com.google.cloud.tools:jib-core", "com.google.cloud.tools:jib-plugins-common"] git { requireBranch = /^gradle_release_v\d+.*$/ //regex } diff --git a/jib-maven-plugin/build.gradle b/jib-maven-plugin/build.gradle index a821461150..878e1c18a4 100644 --- a/jib-maven-plugin/build.gradle +++ b/jib-maven-plugin/build.gradle @@ -56,7 +56,8 @@ publishing { release { - tagTemplate = 'v$version-gradle' + tagTemplate = 'v$version-maven' + ignoredSnapshotDependencies = ["com.google.cloud.tools:jib-core", "com.google.cloud.tools:jib-plugins-common"] git { requireBranch = /^maven_release_v\d+.*$/ //regex } From 5102c77de16c67cb1fcb42129e1c352f5009b8c2 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 13:28:19 -0400 Subject: [PATCH 0729/2020] Gradle release v1.6.0 (#1989) * [Gradle Release Plugin] - pre tag commit: 'v1.6.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.6.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index fd0eb383c9..5b487df7ad 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.5.2-SNAPSHOT +version = 1.6.1-SNAPSHOT From df2f8843b5e603a38d0ee87e153cfb75a91dffc8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 13:28:25 -0400 Subject: [PATCH 0730/2020] Core release v0.11.0 (#1988) * [Gradle Release Plugin] - pre tag commit: 'v0.11.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.11.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 3b0f75f558..c17a25ec16 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.10.2-SNAPSHOT +version = 0.11.1-SNAPSHOT From de7b1de61466ade5bc02a86a732e889bf7866506 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 14:46:32 -0400 Subject: [PATCH 0731/2020] Maven release v1.6.0 (#1990) * [Gradle Release Plugin] - pre tag commit: 'v1.6.0-maven'. * [Gradle Release Plugin] - new version commit: 'v1.6.1-SNAPSHOT-maven'. --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index fd0eb383c9..5b487df7ad 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.5.2-SNAPSHOT +version = 1.6.1-SNAPSHOT From 2763a5ad9b916853bf03c7eb2f36dd5c47d97a02 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 13 Sep 2019 14:53:25 -0400 Subject: [PATCH 0732/2020] Update docs for changed jib.serialize property name (#1973) --- docs/faq.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index b195b223e6..454e303d07 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -519,7 +519,7 @@ See more at [Using Google Container Registry (GCR) with Minikube](https://ryanes ### How can I examine network traffic? -It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jibSerialize` property. +It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jib.serialize` property. To see the HTTP traffic, create a `logging.properties` file with the following: ``` @@ -533,16 +533,15 @@ com.google.api.client.http.level=CONFIG And then launch your build tool as follows: ```sh -mvn -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true -Djib.console=plain ... +mvn -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... ``` or ```sh -gradle -Djava.util.logging.config.file=path/to/log.properties -DjibSerialize=true -Djib.console=plain ... +gradle -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... ``` -Note there is no dot(.) in `jibSerialize`. ### How do I view debug logs for Jib? -Maven: use `mvn -X -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. +Maven: use `mvn -X -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. -Gradle: use `grade --debug -DjibSerialize=true` to enable more detailed logging and serialize Jib's actions. +Gradle: use `grade --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. From ce1b90a95781dd560389d414791d7c97f69120c5 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 14:59:40 -0400 Subject: [PATCH 0733/2020] Update version strings for 1.6.0/0.11.0 release (#1991) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 19 files changed, 44 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e77d156e37..976c1ee13b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.5.2-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.6.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.5.1' + // id 'com.google.cloud.tools.jib' version '1.6.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 5083f95dc2..95502c825e 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.5.1 + 1.6.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 8aff3def17..7877a79711 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 74eea89290..763f16de29 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.5.1 + 1.6.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 62782fa7ad..85150c89c5 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index b65a55ef85..e25a48fe2c 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.5.1 + 1.6.0 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 3aa825b833..0397a77fd8 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index ac0ff1a888..1ec017d76c 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.5.1' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.6.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index e95d58ccb0..35f4045b5f 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.5.1 + 1.6.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 19ea6da2b0..7188175898 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index e51c71a2b4..e04a640a92 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.5.1 + 1.6.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 99aeba35f8..0457efdd3b 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 72bdc6881b..6eaf7fcb85 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.11.0 + +### Added + - `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) - `Jib#from(String)` accepts strings prefixed with `docker://`, `tar://`, or `registry://` to specify image type diff --git a/jib-core/README.md b/jib-core/README.md index a6cadff315..b2d96fe4d9 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.10.1 + 0.11.0 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.10.1' + compile 'com.google.cloud.tools:jib-core:0.11.0' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index 30ec935d84..3a92b3b8e6 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.10.1' + classpath 'com.google.cloud.tools:jib-core:0.11.0' } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index a84c657679..d469b29911 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.6.0 + +### Added + - Support for local base images by prefixing `jib.from.image` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) ### Changed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 0dea7ba2d9..c21bca6533 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.5.1' + id 'com.google.cloud.tools.jib' version '1.6.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index de9546e6ab..ba247e1e9c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.6.0 + +### Added + - Support for local base images by prefixing `` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) ### Changed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 6818b36d6d..53465e5a5a 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.5.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.5.1 + 1.6.0 myimage From 89faa43a6ba2204adcabc06cfaff0419547bffc6 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 13 Sep 2019 15:18:26 -0400 Subject: [PATCH 0734/2020] Update 1.6.0 docs for local base images and dockerClient changes (#1938) --- jib-core/README.md | 8 ++++---- jib-gradle-plugin/README.md | 15 +++++++++++++-- jib-maven-plugin/README.md | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/jib-core/README.md b/jib-core/README.md index b2d96fe4d9..131c8692c3 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -65,10 +65,10 @@ See [examples](examples/README.md) for links to more jib-core samples. We welcom [`JibContainer`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/JibContainer.html) - information about the built container -Three `TargetImage` types define the 3 different targets Jib can build to: -- [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) - builds to a container registry -- [`DockerDaemonImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - builds to a Docker daemon -- [`TarImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/TarImage.html) - saves as a tarball archive +Three types define what Jib can accept as either the base image or as the build target: +- [`RegistryImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/RegistryImage.html) - an image on a container registry +- [`DockerDaemonImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/DockerDaemonImage.html) - an image in the Docker daemon +- [`TarImage`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/TarImage.html) - an image saved as a tarball archive on the filesystem Other useful classes: - [`ImageReference`](http://www.javadoc.io/page/com.google.cloud.tools/jib-core/latest/com/google/cloud/tools/jib/api/ImageReference.html) - represents an image reference and has useful methods for parsing and manipulating image references diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index c21bca6533..40a26fa188 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -183,7 +183,7 @@ Field | Type | Default | Description Property | Type | Default | Description --- | --- | --- | --- -`image` | `String` | `gcr.io/distroless/java` | The image reference for the base image. +`image` | `String` | `gcr.io/distroless/java` | The image reference for the base image. The source type can be specified using a [special type prefix](#setting-the-base-image). `auth` | [`auth`](#auth-closure) | *None* | Specify credentials directly (alternative to `credHelper`). `credHelper` | `String` | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). @@ -230,7 +230,7 @@ Property | Type | Default | Description `paths` | `Object` | `(project-dir)/src/main/jib` | Extra directories acceptable by [`Project.files()`](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#files-java.lang.Object...-), such as `String`, `File`, `Path`, `List`, etc. Can be absolute or relative to the project root. `permissions` | `Map` | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. -**(`jibDockerBuild` only)** `dockerClient` is an object that can be configured directly on the `jibDockerBuild` task, and has the following properties: +`dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: Property | Type | Default | Description --- | --- | --- | --- @@ -297,6 +297,17 @@ jib { } ``` +### Setting the Base Image + +There are three different types of base images that Jib accepts: an image from a container registry, an image stored in the Docker daemon, or an image tarball on the local filesystem. You can specify which you would like to use by prepending the `jib.from.image` configuration with a special prefix, listed below: + +Prefix | Example | Type +--- | --- | --- +*None* | `gcr.io/distroless/java` | Pulls the base image from a registry. +`registry://` | `registry://gcr.io/distroless/java` | Pulls the base image from a registry. +`docker://` | `docker://busybox` | Retrieves the base image from the Docker daemon. +`tar://` | `tar:///path/to/file.tar` | Uses an image tarball stored at the specified path as the base image. Also accepts relative paths (e.g. `tar://build/jib-image.tar`). + ### Adding Arbitrary Files to the Image *\* Note: this is an incubating feature and may change in the future.* diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 53465e5a5a..c4f709a9b7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -229,7 +229,7 @@ Field | Type | Default | Description Property | Type | Default | Description --- | --- | --- | --- -`image` | string | `gcr.io/distroless/java` | The image reference for the base image. +`image` | string | `gcr.io/distroless/java` | The image reference for the base image. The source type can be specified using a [special type prefix](#setting-the-base-image). `auth` | [`auth`](#auth-object) | *None* | Specify credentials directly (alternative to `credHelper`). `credHelper` | string | *None* | Specifies a credential helper that can authenticate pulling the base image. This parameter can either be configured as an absolute path to the credential helper executable or as a credential helper suffix (following `docker-credential-`). @@ -276,7 +276,7 @@ Property | Type | Default | Description `paths` | list | `[(project-dir)/src/main/jib]` | List of extra directories. Can be absolute or relative to the project root. `permissions` | list | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. -**(`jib:dockerBuild` only)** `dockerClient` is an object with the following properties: +`dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: Property | Type | Default | Description --- | --- | --- | --- @@ -359,6 +359,17 @@ In this configuration, the image: ``` +### Setting the Base Image + +There are three different types of base images that Jib accepts: an image from a container registry, an image stored in the Docker daemon, or an image tarball on the local filesystem. You can specify which you would like to use by prepending the `` configuration with a special prefix, listed below: + +Prefix | Example | Type +--- | --- | --- +*None* | `gcr.io/distroless/java` | Pulls the base image from a registry. +`registry://` | `registry://gcr.io/distroless/java` | Pulls the base image from a registry. +`docker://` | `docker://busybox` | Retrieves the base image from the Docker daemon. +`tar://` | `tar:///path/to/file.tar` | Uses an image tarball stored at the specified path as the base image. Also accepts relative paths (e.g. `tar://target/jib-image.tar`). + ### Adding Arbitrary Files to the Image *\* Note: this is an incubating feature and may change in the future.* From 2ed151624fe2d420f5259b2ffe3a135578331deb Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 13 Sep 2019 17:42:36 -0400 Subject: [PATCH 0735/2020] Tell to merge after release is complete (#1992) --- jib-core/scripts/prepare_release.sh | 2 ++ jib-gradle-plugin/scripts/prepare_release.sh | 3 ++- jib-maven-plugin/scripts/prepare_release.sh | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jib-core/scripts/prepare_release.sh b/jib-core/scripts/prepare_release.sh index 8fb09d3236..0392327926 100755 --- a/jib-core/scripts/prepare_release.sh +++ b/jib-core/scripts/prepare_release.sh @@ -56,3 +56,5 @@ git push origin v${VERSION}-core # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} + +EchoGreen "Merge the PR after the library is released." diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh index da14f408aa..73af595df3 100755 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ b/jib-gradle-plugin/scripts/prepare_release.sh @@ -57,4 +57,5 @@ git push origin v${VERSION}-gradle EchoGreen 'File a PR for the new release branch:' echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} -EchoGreen "Once approved and merged, checkout the 'v${VERSION}-gradle' tag and run './gradlew jib-gradle-plugin:publishPlugins'." +EchoGreen "Once approved, checkout the 'v${VERSION}-gradle' tag and run './gradlew jib-gradle-plugin:publishPlugins'." +EchoGreen "Merge the PR after the plugin is released." diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index a82daa259e..b020ad173d 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -56,3 +56,5 @@ git push origin v${VERSION}-maven # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} + +EchoGreen "Merge the PR after the plugin is released." From ce66e5740e9f8a361d3e70641db88e9e35b48e1d Mon Sep 17 00:00:00 2001 From: Laurent Pellegrino Date: Sun, 15 Sep 2019 17:15:18 +0200 Subject: [PATCH 0736/2020] Fix typo (#1994) --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 454e303d07..7a12a4ab4d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -544,4 +544,4 @@ gradle -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=tr Maven: use `mvn -X -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. -Gradle: use `grade --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. +Gradle: use `gradle --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. From e0b7a29c030d04eacdc535d2a0355f779a8fa68d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 11:04:00 -0400 Subject: [PATCH 0737/2020] Fix base image java version check (#1998) --- .../common/PluginConfigurationProcessor.java | 4 ++- .../PluginConfigurationProcessorTest.java | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 27c378d86d..5b42887cbc 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -255,7 +255,9 @@ static JavaContainerBuilder getJavaContainerBuilderWithBaseImage( FileNotFoundException { // Use image configuration as-is if it's a local base image String baseImageConfig = - rawConfiguration.getFromImage().orElse(getDefaultBaseImage(projectProperties)); + rawConfiguration.getFromImage().isPresent() + ? rawConfiguration.getFromImage().get() + : getDefaultBaseImage(projectProperties); if (baseImageConfig.startsWith(Jib.TAR_IMAGE_PREFIX)) { return JavaContainerBuilder.from(baseImageConfig); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index cda9e21ddd..aef5c77924 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -842,6 +842,33 @@ public void testGetJavaContainerBuilderWithBaseImage_incompatibleJava11JettyBase } } + // https://github.com/GoogleContainerTools/jib/issues/1995 + @Test + public void testGetJavaContainerBuilderWithBaseImage_java12BaseImage() + throws InvalidImageReferenceException, IOException, IncompatibleBaseImageJavaVersionException, + CacheDirectoryCreationException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(12); + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.of("regis.try/java12image")); + ImageConfiguration imageConfiguration = getCommonImageConfiguration(); + Assert.assertEquals("regis.try", imageConfiguration.getImageRegistry()); + Assert.assertEquals("java12image", imageConfiguration.getImageRepository()); + } + + @Test + public void testGetJavaContainerBuilderWithBaseImage_java12NoBaseImage() + throws InvalidImageReferenceException, IOException { + Mockito.when(projectProperties.getMajorJavaVersion()).thenReturn(12); + Mockito.when(rawConfiguration.getFromImage()).thenReturn(Optional.empty()); + try { + PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( + rawConfiguration, projectProperties, inferredAuthProvider); + Assert.fail(); + } catch (IncompatibleBaseImageJavaVersionException ex) { + Assert.assertEquals(11, ex.getBaseImageMajorJavaVersion()); + Assert.assertEquals(12, ex.getProjectMajorJavaVersion()); + } + } + @Test public void testGetValidVolumesList() throws InvalidContainerVolumeException { Mockito.when(rawConfiguration.getVolumes()).thenReturn(Collections.singletonList("/some/root")); From 1558d31bf85716f988031caaa4d2d3a556aaaf89 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 12:11:52 -0400 Subject: [PATCH 0738/2020] Gradle release v1.6.1 (#2000) * [Gradle Release Plugin] - pre tag commit: 'v1.6.1-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.6.2-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 5b487df7ad..2520234e02 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.6.1-SNAPSHOT +version = 1.6.2-SNAPSHOT From e074713a4c1036d910533fcdb713ccd72c5108eb Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 12:12:27 -0400 Subject: [PATCH 0739/2020] Maven release v1.6.1 (#2001) * [Gradle Release Plugin] - pre tag commit: 'v1.6.1-maven'. * [Gradle Release Plugin] - new version commit: 'v1.6.2-SNAPSHOT-maven'. --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index 5b487df7ad..2520234e02 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.6.1-SNAPSHOT +version = 1.6.2-SNAPSHOT From 8d14465037ca6a45a2ef6d45e3151caafae847da Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 16 Sep 2019 13:01:01 -0400 Subject: [PATCH 0740/2020] Use Github URLs to create PR directly (#1999) --- jib-core/scripts/prepare_release.sh | 2 +- jib-gradle-plugin/scripts/prepare_release.sh | 2 +- jib-maven-plugin/scripts/prepare_release.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jib-core/scripts/prepare_release.sh b/jib-core/scripts/prepare_release.sh index 0392327926..6cd1490eff 100755 --- a/jib-core/scripts/prepare_release.sh +++ b/jib-core/scripts/prepare_release.sh @@ -55,6 +55,6 @@ git push origin v${VERSION}-core # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' -echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} +echo https://github.com/GoogleContainerTools/jib/pull/new/${BRANCH} EchoGreen "Merge the PR after the library is released." diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh index 73af595df3..e0b2a1e90d 100755 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ b/jib-gradle-plugin/scripts/prepare_release.sh @@ -55,7 +55,7 @@ git push origin v${VERSION}-gradle # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' -echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} +echo https://github.com/GoogleContainerTools/jib/pull/new/${BRANCH} EchoGreen "Once approved, checkout the 'v${VERSION}-gradle' tag and run './gradlew jib-gradle-plugin:publishPlugins'." EchoGreen "Merge the PR after the plugin is released." diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index b020ad173d..c74302bacd 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -55,6 +55,6 @@ git push origin v${VERSION}-maven # File a PR on Github for the new branch. Have someone LGTM it, which gives you permission to continue. EchoGreen 'File a PR for the new release branch:' -echo https://github.com/GoogleContainerTools/jib/compare/${BRANCH} +echo https://github.com/GoogleContainerTools/jib/pull/new/${BRANCH} EchoGreen "Merge the PR after the plugin is released." From 8c0d98f881bc5038fb41544432f55a39a571153a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 13:08:27 -0400 Subject: [PATCH 0741/2020] Update version strings for 1.6.1 (#2002) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 6 ++++++ jib-maven-plugin/README.md | 6 +++--- 16 files changed, 29 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 976c1ee13b..693302fc8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.6.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.6.2-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.6.0' + // id 'com.google.cloud.tools.jib' version '1.6.1' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 95502c825e..b64b1e19f4 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.6.0 + 1.6.1 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 7877a79711..db6d92232b 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 763f16de29..2c8bca5dda 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.6.0 + 1.6.1 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 85150c89c5..5a49a8c6d6 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' id 'de.undercouch.download' version '3.4.0' id "com.gorylenko.gradle-git-properties" version "1.5.2" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index e25a48fe2c..a4b09efe92 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.6.0 + 1.6.1 3.8.0 1.4.1 2.2.5 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 0397a77fd8..e38322ef3f 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 1ec017d76c..63b648b38f 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.6.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.6.1' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 35f4045b5f..e90351c665 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.6.0 + 1.6.1 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 7188175898..d060c457ae 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index e04a640a92..12c4dbc546 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.6.0 + 1.6.1 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 0457efdd3b..9e1e7eecb0 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' } repositories { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index d469b29911..9d218c1fb3 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.6.1 + +### Fixed + +- Fixed an issue with using custom base images in Java 12 projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) + ## 1.6.0 ### Added diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 40a26fa188..35411fd54b 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -42,7 +42,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.6.0' + id 'com.google.cloud.tools.jib' version '1.6.1' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ba247e1e9c..2caa1161c0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 1.6.1 + +### Fixed + +- Fixed an issue with using custom base images in Java 12 projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) + ## 1.6.0 ### Added diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index c4f709a9b7..3f393bbb92 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -37,7 +37,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.1:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -45,7 +45,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -63,7 +63,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.6.0 + 1.6.1 myimage From a22bb79327fbe49afe7f9849e3a5faface95a8e0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 13:43:14 -0400 Subject: [PATCH 0742/2020] Add warning for configuring mainClass/extraClasspath/jvmFlags on WAR projects (#2003) --- .../common/PluginConfigurationProcessor.java | 6 ++++++ .../PluginConfigurationProcessorTest.java | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 5b42887cbc..5ac3da5da5 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -339,6 +339,12 @@ static List computeEntrypoint( } if (projectProperties.isWarProject()) { + if (rawConfiguration.getMainClass().isPresent() + || !rawConfiguration.getJvmFlags().isEmpty() + || !rawExtraClasspath.isEmpty()) { + projectProperties.log( + LogEvent.warn("mainClass, extraClasspath, and jvmFlags are ignored for WAR projects")); + } return null; } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index aef5c77924..fe3a772a55 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -410,6 +410,24 @@ public void testEntrypoint_warningOnMainclass() "mainClass, extraClasspath, and jvmFlags are ignored when entrypoint is specified")); } + @Test + public void testEntrypoint_warningOnMainclassForWar() + throws IOException, InvalidCreationTimeException, InvalidImageReferenceException, + IncompatibleBaseImageJavaVersionException, InvalidContainerVolumeException, + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidFilesModificationTimeException, InvalidContainerizingModeException, + CacheDirectoryCreationException { + Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); + Mockito.when(projectProperties.isWarProject()).thenReturn(true); + + BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + + Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Mockito.verify(projectProperties) + .log(LogEvent.warn("mainClass, extraClasspath, and jvmFlags are ignored for WAR projects")); + } + @Test public void testEntrypointClasspath_nonDefaultAppRoot() throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException, From 84b8028ae19345bd740ef231874986d0c38edb28 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 14:23:24 -0400 Subject: [PATCH 0743/2020] Fix base image reproducibility warning (#2004) --- .../jib/configuration/BuildConfiguration.java | 2 +- .../configuration/BuildConfigurationTest.java | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 6e84db8589..34cbca92ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -245,7 +245,7 @@ public BuildConfiguration build() throws IOException { switch (missingFields.size()) { case 0: // No errors Preconditions.checkNotNull(baseImageConfiguration); - if (baseImageConfiguration.getImage().usesDefaultTag() + if (!baseImageConfiguration.getImage().isTagDigest() && !baseImageConfiguration.getImage().isScratch()) { eventHandlers.dispatch( LogEvent.warn( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 0703844de3..37aad46869 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -21,8 +21,11 @@ import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.Port; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -246,4 +249,35 @@ public void testBuilder_missingValues() throws IOException { ex.getMessage()); } } + + @Test + public void testBuilder_digestWarning() throws IOException, InvalidImageReferenceException { + EventHandlers mockEventHandlers = Mockito.mock(EventHandlers.class); + BuildConfiguration.Builder builder = + BuildConfiguration.builder() + .setEventHandlers(mockEventHandlers) + .setTargetImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(MoreExecutors.newDirectExecutorService()); + + builder + .setBaseImageConfiguration( + ImageConfiguration.builder( + ImageReference.parse( + "image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .build()) + .build(); + Mockito.verify(mockEventHandlers, Mockito.never()).dispatch(LogEvent.warn(Mockito.anyString())); + + builder + .setBaseImageConfiguration( + ImageConfiguration.builder(ImageReference.parse("image:tag")).build()) + .build(); + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.warn( + "Base image 'image:tag' does not use a specific image digest - build may not be reproducible")); + } } From 787cc9ccaca9ac3059a31ccad170ae48ff5b9475 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 16 Sep 2019 15:01:34 -0400 Subject: [PATCH 0744/2020] Mark maven goals as threadSafe (#2005) --- .../java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java | 3 ++- .../java/com/google/cloud/tools/jib/maven/BuildImageMojo.java | 3 ++- .../java/com/google/cloud/tools/jib/maven/BuildTarMojo.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 299ade5ea4..f06639c644 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -42,7 +42,8 @@ /** Builds a container image and exports to the default Docker daemon. */ @Mojo( name = BuildDockerMojo.GOAL_NAME, - requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) + requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM, + threadSafe = true) public class BuildDockerMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "dockerBuild"; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 4f9538f18e..3a48b87039 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -43,7 +43,8 @@ /** Builds a container image. */ @Mojo( name = BuildImageMojo.GOAL_NAME, - requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) + requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM, + threadSafe = true) public class BuildImageMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "build"; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index eed89a836e..3d4e742dc6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -42,7 +42,8 @@ */ @Mojo( name = BuildTarMojo.GOAL_NAME, - requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM) + requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM, + threadSafe = true) public class BuildTarMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "buildTar"; From 7cd0c57a56272ab4b5f74733d03c2ac4a9b8e709 Mon Sep 17 00:00:00 2001 From: Markus Heiden Date: Mon, 16 Sep 2019 21:50:19 +0200 Subject: [PATCH 0745/2020] Fix typo (#1996) --- .../google/cloud/tools/jib/gradle/BuildDockerTask.java | 2 +- .../google/cloud/tools/jib/gradle/BuildImageTask.java | 2 +- .../google/cloud/tools/jib/gradle/BuildTarTask.java | 2 +- .../google/cloud/tools/jib/maven/BuildDockerMojo.java | 2 +- .../google/cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../com/google/cloud/tools/jib/maven/BuildTarMojo.java | 2 +- .../tools/jib/plugins/common/HelpfulSuggestions.java | 10 +++++----- .../jib/plugins/common/HelpfulSuggestionsTest.java | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index cf347676ad..d4970c46b4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -172,7 +172,7 @@ public void buildDocker() } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 6e0a19794b..4df4df99b3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -132,7 +132,7 @@ public void buildImage() } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 63c8bff5d0..e9a8b5452b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -148,7 +148,7 @@ public void buildTar() } catch (IncompatibleBaseImageJavaVersionException ex) { throw new GradleException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index f06639c644..8649b8693a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -130,7 +130,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForMaven( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 3a48b87039..c9e1e48d92 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -144,7 +144,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForMaven( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 3d4e742dc6..53130ce95d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -121,7 +121,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } catch (IncompatibleBaseImageJavaVersionException ex) { throw new MojoExecutionException( - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven( + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForMaven( ex.getBaseImageMajorJavaVersion(), ex.getProjectMajorJavaVersion()), ex); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java index 10a985c2c6..1f58e06715 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestions.java @@ -50,9 +50,9 @@ public static String forMainClassNotFound(String messagePrefix, String pluginNam return suggest(messagePrefix, "add a `mainClass` configuration to " + pluginName); } - public static String forIncompatibleBaseImageJavaVesionForGradle( + public static String forIncompatibleBaseImageJavaVersionForGradle( int baseImageMajorJavaVersion, int projectMajorJavaVersion) { - return forIncompatibleBaseImageJavaVesion( + return forIncompatibleBaseImageJavaVersion( baseImageMajorJavaVersion, projectMajorJavaVersion, "using the 'jib.from.image' parameter, or set targetCompatibility = " @@ -60,9 +60,9 @@ public static String forIncompatibleBaseImageJavaVesionForGradle( + " or below"); } - public static String forIncompatibleBaseImageJavaVesionForMaven( + public static String forIncompatibleBaseImageJavaVersionForMaven( int baseImageMajorJavaVersion, int projectMajorJavaVersion) { - return forIncompatibleBaseImageJavaVesion( + return forIncompatibleBaseImageJavaVersion( baseImageMajorJavaVersion, projectMajorJavaVersion, "using the '' parameter, or set maven-compiler-plugin's '' or " @@ -88,7 +88,7 @@ public static String suggest(String messagePrefix, String suggestion) { return messagePrefix + ", perhaps you should " + suggestion; } - private static String forIncompatibleBaseImageJavaVesion( + private static String forIncompatibleBaseImageJavaVersion( int baseImageMajorJavaVersion, int projectMajorJavaVersion, String parameterInstructions) { return suggest( "Your project is using Java " diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java index 30b7db2e2c..2efcc619d8 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/HelpfulSuggestionsTest.java @@ -59,13 +59,13 @@ public void testSuggestions_smoke() { + "configure a Java 11-compatible base image using the 'jib.from.image' " + "parameter, or set targetCompatibility = 8 or below in your build " + "configuration", - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForGradle(8, 11)); + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForGradle(8, 11)); Assert.assertEquals( "Your project is using Java 11 but the base image is for Java 8, perhaps you should " + "configure a Java 11-compatible base image using the '' " + "parameter, or set maven-compiler-plugin's '' or '' version " + "to 8 or below in your build configuration", - HelpfulSuggestions.forIncompatibleBaseImageJavaVesionForMaven(8, 11)); + HelpfulSuggestions.forIncompatibleBaseImageJavaVersionForMaven(8, 11)); Assert.assertEquals( "Invalid image reference gcr.io/invalid_REF, perhaps you should check that the reference " + "is formatted correctly according to https://docs.docker.com/engine/reference/commandline/tag/#extended-description\n" From 0e5dc5da45206154abcb1a7bba99b2095da11f79 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 16 Sep 2019 16:21:34 -0400 Subject: [PATCH 0746/2020] Update CHANGELOGs (#2008) * Add dots to all * more missing dots --- jib-core/CHANGELOG.md | 62 ++++---- jib-gradle-plugin/CHANGELOG.md | 224 ++++++++++++++--------------- jib-maven-plugin/CHANGELOG.md | 252 ++++++++++++++++----------------- 3 files changed, 269 insertions(+), 269 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 6eaf7fcb85..846be6fc94 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -13,13 +13,13 @@ All notable changes to this project will be documented in this file. ### Added -- `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) -- `Jib#from(String)` accepts strings prefixed with `docker://`, `tar://`, or `registry://` to specify image type +- `Jib#from` and `JavaContainerBuilder#from` overloads to allow using a `DockerDaemonImage` or a `TarImage` as the base image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) +- `Jib#from(String)` accepts strings prefixed with `docker://`, `tar://`, or `registry://` to specify image type. ### Changed - To disable parallel execution, the property `jib.serialize` should be used instead of `jibSerialize`. ([#1968](https://github.com/GoogleContainerTools/jib/issues/1968)) -- `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)` ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) +- `TarImage` is constructed using `TarImage.at(...).named(...)` instead of `TarImage.named(...).saveTo(...)`. ([#1918](https://github.com/GoogleContainerTools/jib/issues/1918)) - For retrieving credentials from Docker config (`~/.docker/config.json`), `credHelpers` now takes precedence over `credsStore`, followed by `auths`. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) - The legacy `credsStore` no longer requires defining empty registry entries in `auths` to be used. This now means that if `credsStore` is defined, `auths` will be completely ignored. ([#1958](https://github.com/GoogleContainerTools/jib/pull/1958)) @@ -33,92 +33,92 @@ All notable changes to this project will be documented in this file. ### Added -- `JavaContainerBuilder#setLastModifiedTimeProvider` to set file timestamps ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- `JavaContainerBuilder#setLastModifiedTimeProvider` to set file timestamps. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) ### Changed -- `JibContainerBuilder#addDependencies` is now split into three methods: `addDependencies`, `addSnapshotDependencies`, `addProjectDependencies` ([#1773](https://github.com/GoogleContainerTools/jib/pull/1773)) -- For building and pushing to a registry, Jib now skips downloading and caching base image layers if the layers already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `JibContainerBuilder#addDependencies` is now split into three methods: `addDependencies`, `addSnapshotDependencies`, `addProjectDependencies`. ([#1773](https://github.com/GoogleContainerTools/jib/pull/1773)) +- For building and pushing to a registry, Jib now skips downloading and caching base image layers if the layers already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) ### Fixed -- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) ## 0.10.0 ### Added -- `Containerizer#addEventHandler` for adding event handlers +- `Containerizer#addEventHandler` for adding event handlers. ### Changed -- Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package -- Event handlers are now added directly to the `Containerizer` rather than adding them to an `EventHandlers` object first +- Multiple classes have been moved to the `com.google.cloud.tools.jib.api` package. +- Event handlers are now added directly to the `Containerizer` rather than adding them to an `EventHandlers` object first. - Removed multiple classes to simplify the event system (`JibEventType`, `BuildStepType`, `EventDispatcher`, `DefaultEventDispatcher`, `LayerCountEvent`) -- MainClassFinder now uses a static method instead of requiring instantiation +- MainClassFinder now uses a static method instead of requiring instantiation. ## 0.9.2 ### Added -- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) -- `Containerizer#setOfflineMode` to retrieve the base image from Jib's cache rather than a container registry ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- `Containerizer#setOfflineMode` to retrieve the base image from Jib's cache rather than a container registry. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) ### Fixed -- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) -- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 0.9.1 ### Added -- Overloads for `LayerConfiguration#addEntryRecursive` that take providers to set file permissions and file modification time on a per-file basis ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) +- Overloads for `LayerConfiguration#addEntryRecursive` that take providers to set file permissions and file modification time on a per-file basis. ([#1607](https://github.com/GoogleContainerTools/jib/issues/1607)) ### Changed -- `LayerConfiguration` takes file modification time as an `Instant` instead of a `long` +- `LayerConfiguration` takes file modification time as an `Instant` instead of a `long`. ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) -- Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where the library creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 0.9.0 ### Added -- `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) -- `Jib#fromScratch()` to start building from an empty base image ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) -- Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files +- `JavaContainerBuilder#setAppRoot()` and `JavaContainerBuilder#fromDistrolessJetty()` for building WAR containers. ([#1464](https://github.com/GoogleContainerTools/jib/issues/1464)) +- `Jib#fromScratch()` to start building from an empty base image. ([#1471](https://github.com/GoogleContainerTools/jib/issues/1471)) +- Methods in `JavaContainerBuilder` for setting the destination directories for classes, resources, directories, and additional classpath files. ### Changed -- Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint -- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) +- Allow skipping `JavaContainerBuilder#setMainClass()` to skip setting the entrypoint. +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed -- `ImageReference` assumes `registry-1.docker.io` as the registry if the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) +- `ImageReference` assumes `registry-1.docker.io` as the registry if the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 0.1.2 ### Added -- `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) -- `LayerCountEvent` that is dispatched at the beginning of certain pull/build/push build steps to indicate the number of layers being processed ([#1461](https://github.com/GoogleContainerTools/jib/pull/1461)) +- `ProgressEvent#getBuildStepType` method to get which step in the build process a progress event corresponds to. ([#1449](https://github.com/GoogleContainerTools/jib/pull/1449)) +- `LayerCountEvent` that is dispatched at the beginning of certain pull/build/push build steps to indicate the number of layers being processed. ([#1461](https://github.com/GoogleContainerTools/jib/pull/1461)) ### Changed -- `JibContainerBuilder#containerize()` throws multiple sub-types of `RegistryException` rather than wrapping them in an `ExecutionException` ([#1440](https://github.com/GoogleContainerTools/jib/issues/1440)) +- `JibContainerBuilder#containerize()` throws multiple sub-types of `RegistryException` rather than wrapping them in an `ExecutionException`. ([#1440](https://github.com/GoogleContainerTools/jib/issues/1440)) ### Fixed -- `MainClassFinder` failure when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) +- `MainClassFinder` failure when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) ## 0.1.1 ### Added -- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) -- Adds `JavaContainerBuilder` for building opinionated containers for Java applications ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Adds `JavaContainerBuilder` for building opinionated containers for Java applications. ([#1212](https://github.com/GoogleContainerTools/jib/issues/1212)) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 9d218c1fb3..3eede40c3f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -13,13 +13,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed an issue with using custom base images in Java 12 projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) +- Fixed an issue with using custom base images in Java 12+ projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) ## 1.6.0 ### Added -- Support for local base images by prefixing `jib.from.image` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) +- Support for local base images by prefixing `jib.from.image` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) ### Changed @@ -44,37 +44,37 @@ All notable changes to this project will be documented in this file. ### Added -- Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) -- Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) - For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed -- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) -- `jib.container.useCurrentTimestamp` has been deprecated in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `jib.container.useCurrentTimestamp` has been deprecated in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ## 1.4.0 ### Added -- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'` ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) -- Now automatically supports WAR created by the Spring Boot Gradle Plugin via the `bootWar` task ([#1786](https://github.com/GoogleContainerTools/jib/issues/1786)) +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `jib.containerizingMode = 'packaged'`. ([#1760](https://github.com/GoogleContainerTools/jib/pull/1760/files)) +- Now automatically supports WAR created by the Spring Boot Gradle Plugin via the `bootWar` task. ([#1786](https://github.com/GoogleContainerTools/jib/issues/1786)) - Can now use `jib.from.image = 'scratch'` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) ### Changed -- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies. ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) ### Fixed -- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) -- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) +- Re-enabled cross-repository blob mounts. ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) ## 1.3.0 ### Changed -- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers. ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) ### Fixed @@ -84,41 +84,41 @@ All notable changes to this project will be documented in this file. ### Added -- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) -- Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) -- Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) -- Now supports multiple extra directories with `jib.extraDirectories.{paths|.permissions}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with `jib.container.extraClasspath`. ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline`. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) +- Now supports multiple extra directories with `jib.extraDirectories.{paths|.permissions}`. ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed -- `jib.extraDirectory({.path|.permissions})` are deprecated in favor of the new `jib.extraDirectories.{paths|.permissions}` configurations ([#1671](https://github.com/GoogleContainerTools/jib/pull/1671)) +- `jib.extraDirectory({.path|.permissions})` are deprecated in favor of the new `jib.extraDirectories.{paths|.permissions}` configurations. ([#1671](https://github.com/GoogleContainerTools/jib/pull/1671)) ### Fixed -- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) -- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 1.1.2 ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) ## 1.1.1 ### Fixed -- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 1.1.0 ### Changed -- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed -- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 @@ -140,13 +140,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) ## 1.0.0 ### Changed -- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy. ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) ## 1.0.0-rc2 @@ -162,232 +162,232 @@ All notable changes to this project will be documented in this file. ### Added -- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) -- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories. ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages. ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) ### Changed -- Removed `jib.useOnlyProjectCache` parameter in favor of the `jib.useOnlyProjectCache` system property ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) +- Removed `jib.useOnlyProjectCache` parameter in favor of the `jib.useOnlyProjectCache` system property. ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) ### Fixed -- Builds failing due to dependency JARs with the same name ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) +- Builds failing due to dependency JARs with the same name. ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) ## 0.10.1 ### Added -- Image ID is now written to `build/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) +- Image ID is now written to `build/jib-image.id`. ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `jib.container.entrypoint = 'INHERIT'` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using `jib.container.args`. -- `container.workingDirectory` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) -- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) -- Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) -- Docker health check is now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- `container.workingDirectory` configuration parameter to set the working directory. ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed -- Removed `jibExportDockerContext` task ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) +- Removed `jibExportDockerContext` task. ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) ### Fixed -- NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) +- NullPointerException thrown with incomplete `auth` configuration. ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) ## 0.10.0 ### Added -- Properties for each configuration parameter, allowing any parameter to be set via commandline ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) -- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) -- `container.user` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) -- Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) -- `jib.extraDirectory` closure with a `path` and `permissions` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) +- Properties for each configuration parameter, allowing any parameter to be set via commandline. ([#1083](https://github.com/GoogleContainerTools/jib/issues/1083)) +- `jib.to.credHelper` and `jib.from.credHelper` can be used to specify a credential helper suffix or a full path to a credential helper executable. ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `container.user` configuration parameter to configure the user and group to run the container as. ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects. ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) +- `jib.extraDirectory` closure with a `path` and `permissions` field. ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `jib.extraDirectory.path` configures the extra layer directory (still also configurable via `jib.extraDirectory = file(...)`) - - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string) -- Image digest is now written to `build/jib-image.digest` ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) -- Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) -- `jibDockerBuild.dockerClient.executable` and `jibDockerBuild.dockerClient.environment` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary ([#1214](https://github.com/GoogleContainerTools/jib/pull/1214)) + - `jib.extraDirectory.permissions` is a map from absolute path on container to the file's permission bits (represented as an octal string). +- Image digest is now written to `build/jib-image.digest`. ([#933](https://github.com/GoogleContainerTools/jib/issues/933)) +- Adds the layer type to the layer history as comments. ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) +- `jibDockerBuild.dockerClient.executable` and `jibDockerBuild.dockerClient.environment` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary. ([#1214](https://github.com/GoogleContainerTools/jib/pull/1214)) ### Changed -- Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) -- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) -- Sets tag to "latest" instead of "unspecified" if `jib.to.image` and project version are both unspecified when running `jibDockerBuild` or `jibBuildTar` ([#1096](https://github.com/GoogleContainerTools/jib/issues/1096)) +- Removed deprecated `jib.jvmFlags`, `jib.mainClass`, `jib.args`, and `jib.format` in favor of the equivalents under `jib.container`. ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) +- `jibExportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support. ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- Sets tag to "latest" instead of "unspecified" if `jib.to.image` and project version are both unspecified when running `jibDockerBuild` or `jibBuildTar`. ([#1096](https://github.com/GoogleContainerTools/jib/issues/1096)) ## 0.9.13 ### Fixed -- Adds environment variable configuration to Docker context generator ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) +- Adds environment variable configuration to Docker context generator. ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) ## 0.9.12 ### Fixed -- `Cannot access 'image': it is public in ` error ([#1060](https://github.com/GoogleContainerTools/jib/issues/1060)) +- `Cannot access 'image': it is public in ` error. ([#1060](https://github.com/GoogleContainerTools/jib/issues/1060)) ## 0.9.11 ### Added -- `container.environment` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) -- `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) -- `jib.to.tags` (list) defines additional tags to push to ([#978](https://github.com/GoogleContainerTools/jib/pull/978)) +- `container.environment` configuration parameter to configure environment variables. ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image. ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `jib.to.tags` (list) defines additional tags to push to. ([#978](https://github.com/GoogleContainerTools/jib/pull/978)) ### Fixed -- Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) +- Keep duplicate layers to match container history. ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) ## 0.9.10 ### Added -- `container.labels` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) -- `container.entrypoint` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) -- `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) -- Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) +- `container.labels` configuration parameter for configuring labels. ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `container.entrypoint` configuration parameter to set the entrypoint. ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) +- `history` to layer metadata. ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image. ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) ### Fixed -- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) +- Corrects permissions for directories in the container filesystem. ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) ## 0.9.9 ### Added -- Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) +- Passthrough labels from base image. ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) ### Changed -- Reordered classpath in entrypoint to use _resources_, _classes_, and then _dependencies_, to allow dependency patching - ([#777](https://github.com/GoogleContainerTools/jib/issues/777)). Note that this classpath ordering differs from that used by Gradle's `run` task. -- Changed logging level of missing build output directory message ([#677](https://github.com/GoogleContainerTools/jib/issues/677)) +- Reordered classpath in entrypoint to use _resources_, _classes_, and then _dependencies_, to allow dependency patching. + . ([#777](https://github.com/GoogleContainerTools/jib/issues/777)). Note that this classpath ordering differs from that used by Gradle's `run` task. +- Changed logging level of missing build output directory message. ([#677](https://github.com/GoogleContainerTools/jib/issues/677)) ### Fixed -- Gradle project dependencies have their `assemble` task run before running a jib task ([#815](https://github.com/GoogleContainerTools/jib/issues/815)) +- Gradle project dependencies have their `assemble` task run before running a jib task. ([#815](https://github.com/GoogleContainerTools/jib/issues/815)) ## 0.9.8 ### Added -- Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -- Disable parallel operation by setting the `jibSerialize` system property to `true` ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) +- Docker context generation now includes snapshot dependencies and extra files. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true`. ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) ### Changed -- Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) -- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates) ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) +- Propagates environment variables from the base image. ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates). ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) ### Fixed -- Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) -- Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -- Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) -- Incorrect authentication error message when target and base registry are the same ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) +- Slow image reference parsing. ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth. ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same. ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) ## 0.9.7 ### Added -- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) -- `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) -- `container.useCurrentTimestamp` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) -- Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) -- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) -- Docker context export command-line option `--targetDir` to `--jibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) +- Snapshot dependencies are added as their own layer. ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jibBuildTar` task to build an image tarball at `build/jib-image.tar`, which can be loaded into docker using `docker load`. ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- `container.useCurrentTimestamp` parameter to set the image creation time to the build time. ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) +- Authentication over HTTP using the `sendCredentialsOverHttp` system property. ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property. ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) +- Docker context export command-line option `--targetDir` to `--jibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) ### Changed -- Docker context export command-line option `--targetDir` to `--jibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) +- Docker context export command-line option `--targetDir` to `--jibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) ### Fixed -- Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) -- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) +- Using multi-byte characters in container configuration. ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config. ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) ## 0.9.6 ### Fixed -- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) +- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true`. ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 ### Added -- Incubating feature to build `src/main/jib` as extra layer in image ([#562](https://github.com/GoogleContainerTools/jib/pull/562)) +- Incubating feature to build `src/main/jib` as extra layer in image. ([#562](https://github.com/GoogleContainerTools/jib/pull/562)) ## 0.9.4 ### Fixed -- Fixed handling case-insensitive `Basic` authentication method ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) -- Fixed regression that broke pulling base images from registries that required token authentication ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) +- Fixed handling case-insensitive `Basic` authentication method. ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) +- Fixed regression that broke pulling base images from registries that required token authentication. ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) ## 0.9.3 ### Fixed -- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol) ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) +- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol). ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) ## 0.9.2 ### Added -- Can configure `jibExportDockerContext` output directory with `jibExportDockerContext.targetDir` ([#492](https://github.com/GoogleContainerTools/jib/pull/492)) +- Can configure `jibExportDockerContext` output directory with `jibExportDockerContext.targetDir`. ([#492](https://github.com/GoogleContainerTools/jib/pull/492)) ### Changed ### Fixed -- Set `jibExportDockerContext` output directory with command line option `--targetDir` ([#499](https://github.com/GoogleContainerTools/jib/pull/499)) +- Set `jibExportDockerContext` output directory with command line option `--targetDir`. ([#499](https://github.com/GoogleContainerTools/jib/pull/499)) ## 0.9.1 ### Added -- `container.ports` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`) ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) -- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) +- `container.ports` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`). ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) +- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP. ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) ### Changed -- Fetches credentials from inferred credential helper before Docker config ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) -- Container creation date set to timestamp 0 ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) -- Does not authenticate base image pull unless necessary - reduces build time by about 500ms ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) -- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) -- Warns instead of errors when classes not found ([#462](https://github.com/GoogleContainerTools/jib/pull/462)) +- Fetches credentials from inferred credential helper before Docker config. ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) +- Container creation date set to timestamp 0. ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) +- Does not authenticate base image pull unless necessary - reduces build time by about 500ms. ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) +- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object. ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) +- Warns instead of errors when classes not found. ([#462](https://github.com/GoogleContainerTools/jib/pull/462)) -### Fixed +### Fixed -- Using Azure Container Registry now works - define credentials in `jib.to.auth`/`jib.from.auth` ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) -- Supports `access_token` as alias to `token` in registry authentication ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) -- Docker context export for Groovy project ([#459](https://github.com/GoogleContainerTools/jib/pull/459)) -- Visibility of `jib.to.image` ([#460](https://github.com/GoogleContainerTools/jib/pull/460)) +- Using Azure Container Registry now works - define credentials in `jib.to.auth`/`jib.from.auth`. ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) +- Supports `access_token` as alias to `token` in registry authentication. ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) +- Docker context export for Groovy project. ([#459](https://github.com/GoogleContainerTools/jib/pull/459)) +- Visibility of `jib.to.image`. ([#460](https://github.com/GoogleContainerTools/jib/pull/460)) ## 0.9.0 ### Added -- Export a Docker context (including a Dockerfile) with `jibExportDockerContext` ([#204](https://github.com/google/jib/issues/204)) -- Warns if build may not be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- `jibDockerBuild` gradle task to build straight to Docker daemon ([#265](https://github.com/GoogleContainerTools/jib/pull/265)) -- `mainClass` is inferred by searching through class files if configuration is missing ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) -- All tasks depend on `classes` by default ([#335](https://github.com/GoogleContainerTools/jib/issues/335)) -- Can now specify target image with `--image` ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) -- `args` parameter to define default main arguments ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) +- Export a Docker context (including a Dockerfile) with `jibExportDockerContext`. ([#204](https://github.com/google/jib/issues/204)) +- Warns if build may not be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- `jibDockerBuild` gradle task to build straight to Docker daemon. ([#265](https://github.com/GoogleContainerTools/jib/pull/265)) +- `mainClass` is inferred by searching through class files if configuration is missing. ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) +- All tasks depend on `classes` by default. ([#335](https://github.com/GoogleContainerTools/jib/issues/335)) +- Can now specify target image with `--image`. ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) +- `args` parameter to define default main arguments. ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) ### Changed -- Removed `reproducible` parameter - application layers will always be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- Removed `reproducible` parameter - application layers will always be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) ### Fixed -- Using base images that lack entrypoints ([#284](https://github.com/GoogleContainerTools/jib/pull/284) +- Using base images that lack entrypoints. ([#284](https://github.com/GoogleContainerTools/jib/pull/284) ## 0.1.1 ### Added -- Warns if specified `mainClass` is not a valid Java class ([#206](https://github.com/google/jib/issues/206)) -- Can specify registry credentials to use directly with `from.auth` and `to.auth` ([#215](https://github.com/google/jib/issues/215)) +- Warns if specified `mainClass` is not a valid Java class. ([#206](https://github.com/google/jib/issues/206)) +- Can specify registry credentials to use directly with `from.auth` and `to.auth`. ([#215](https://github.com/google/jib/issues/215)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 2caa1161c0..698869fb5a 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,13 +13,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed an issue with using custom base images in Java 12 projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) +- Fixed an issue with using custom base images in Java 12+ projects. ([#1995](https://github.com/GoogleContainerTools/jib/issues/1995)) ## 1.6.0 ### Added -- Support for local base images by prefixing `` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) +- Support for local base images by prefixing `` with `docker://` to build from a docker daemon image, or `tar://` to build from a tarball image. ([#1468](https://github.com/GoogleContainerTools/jib/issues/1468), [#1905](https://github.com/GoogleContainerTools/jib/issues/1905)) ### Changed @@ -43,36 +43,36 @@ All notable changes to this project will be documented in this file. ### Added -- Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) -- Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z`. ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) +- Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) - For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed -- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true` ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) -- `` has been deprecated in favor of `` with `USE_CURRENT_TIMESTAMP` ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- When building to a registry, Jib now skips downloading and caching base image layers that already exist in the target registry. This feature will be particularly useful in CI/CD environments. However, if you want to force caching base image layers locally, set the system property `-Djib.alwaysCacheBaseImage=true`. ([#1840](https://github.com/GoogleContainerTools/jib/pull/1840)) +- `` has been deprecated in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) ## 1.4.0 ### Added -- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged` ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) +- Can now containerize a JAR artifact instead of putting individual `.class` and resource files with `packaged`. ([#1746](https://github.com/GoogleContainerTools/jib/pull/1746/files)) - Can now use `scratch` to use the scratch (empty) base image for builds. ([#1794](https://github.com/GoogleContainerTools/jib/pull/1794/files)) ### Changed -- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) +- Dependencies are now split into three layers: dependencies, snapshots dependencies, project dependencies. ([#1724](https://github.com/GoogleContainerTools/jib/pull/1724)) ### Fixed -- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) -- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) +- Re-enabled cross-repository blob mounts. ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) +- Manifest lists referenced directly by sha256 are automatically parsed and the first `linux/amd64` manifest is used. ([#1811](https://github.com/GoogleContainerTools/jib/issues/1811)) ## 1.3.0 ### Changed -- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) +- Docker credentials (`~/.docker/config.json`) are now given priority over registry-based inferred credential helpers. ([#1704](https://github.com/GoogleContainerTools/jib/pulls/1704)) ### Fixed @@ -82,31 +82,31 @@ All notable changes to this project will be documented in this file. ### Added -- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) -- Can now prepend paths in the container to the computed classpath with `` ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) -- Can now build in offline mode using `--offline` ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) -- Now supports multiple extra directories with `{|}` ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) +- Container configurations in the base image are now propagated when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1641](https://github.com/GoogleContainerTools/jib/issues/1641)) +- Can now prepend paths in the container to the computed classpath with ``. ([#1642](https://github.com/GoogleContainerTools/jib/pull/1642)) +- Can now build in offline mode using `--offline`. ([#718](https://github.com/GoogleContainerTools/jib/issues/718)) +- Now supports multiple extra directories with `{|}`. ([#1020](https://github.com/GoogleContainerTools/jib/issues/1020)) ### Changed -- `(|)` are deprecated in favor of the new `{|}` configurations ([#1626](https://github.com/GoogleContainerTools/jib/pull/1626)) +- `(|)` are deprecated in favor of the new `{|}` configurations. ([#1626](https://github.com/GoogleContainerTools/jib/pull/1626)) ### Fixed -- Labels in the base image are now propagated ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) -- Fixed an issue with using OCI base images ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) +- Labels in the base image are now propagated. ([#1643](https://github.com/GoogleContainerTools/jib/issues/1643)) +- Fixed an issue with using OCI base images. ([#1683](https://github.com/GoogleContainerTools/jib/issues/1683)) ## 1.1.2 ### Fixed -- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) +- Fixed an issue where automatically generated parent directories in a layer did not get their timestamp configured correctly to epoch + 1s. ([#1648](https://github.com/GoogleContainerTools/jib/issues/1648)) ## 1.1.1 ### Fixed -- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay) ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) +- Fixed an issue where the plugin creates wrong images by adding base image layers in reverse order when registry uses the old V2 image manifest, schema version 1 (such as Quay). ([#1627](https://github.com/GoogleContainerTools/jib/issues/1627)) ## 1.1.0 @@ -116,11 +116,11 @@ All notable changes to this project will be documented in this file. ### Changed -- `os` and `architecture` are taken from base image ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) +- `os` and `architecture` are taken from base image. ([#1564](https://github.com/GoogleContainerTools/jib/pull/1564)) ### Fixed -- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io` ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) +- Fixed an issue where pushing to Docker Hub fails when the host part of an image reference is `docker.io`. ([#1549](https://github.com/GoogleContainerTools/jib/issues/1549)) ## 1.0.2 @@ -143,13 +143,13 @@ All notable changes to this project will be documented in this file. ### Fixed -- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`) ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) +- Failure to infer main class when main method is defined using varargs (i.e. `public static void main(String... args)`). ([#1456](https://github.com/GoogleContainerTools/jib/issues/1456)) ## 1.0.0 ### Changed -- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) +- Shortened progress bar display - make sure console window is at least 50 characters wide or progress bar display can be messy. ([#1361](https://github.com/GoogleContainerTools/jib/issues/1361)) ## 1.0.0-rc2 @@ -158,7 +158,7 @@ All notable changes to this project will be documented in this file. - Setting proxy credentials (via system properties `http(s).proxyUser` and `http(s).proxyPassword`) is now supported. - Maven proxy settings are now supported. - Now checks for system properties in pom as well as commandline. ([#1201](https://github.com/GoogleContainerTools/jib/issues/1201)) -- `` and `` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary ([#468](https://github.com/GoogleContainerTools/jib/issues/468)) +- `` and `` to set Docker client binary path (defaulting to `docker`) and additional environment variables to apply when running the binary. ([#468](https://github.com/GoogleContainerTools/jib/issues/468)) ### Changed @@ -168,99 +168,99 @@ All notable changes to this project will be documented in this file. ### Added -- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) -- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) +- `jib.baseImageCache` and `jib.applicationCache` system properties for setting cache directories. ([#1238](https://github.com/GoogleContainerTools/jib/issues/1238)) +- Build progress shown via a progress bar - set `-Djib.console=plain` to show progress as log messages. ([#1297](https://github.com/GoogleContainerTools/jib/issues/1297)) ### Changed - `gwt-app` packaging type now builds a WAR container. - When building to Docker and no `` is defined, artifact ID is used as an image reference instead of project name. -- Removed `` parameter in favor of the `jib.useOnlyProjectCache` system property ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) +- Removed `` parameter in favor of the `jib.useOnlyProjectCache` system property. ([#1308](https://github.com/GoogleContainerTools/jib/issues/1308)) ### Fixed -- Builds failing due to dependency JARs with the same name ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) +- Builds failing due to dependency JARs with the same name. ([#810](https://github.com/GoogleContainerTools/jib/issues/810)) ## 0.10.1 ### Added -- Image ID is now written to `target/jib-image.id` ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) +- Image ID is now written to `target/jib-image.id`. ([#1204](https://github.com/GoogleContainerTools/jib/issues/1204)) - `INHERIT` allows inheriting `ENTRYPOINT` and `CMD` from the base image. While inheriting `ENTRYPOINT`, you can also override `CMD` using ``. -- `` configuration parameter to set the working directory ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) -- Adds support for configuring volumes ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) -- Exposed ports are now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) -- Docker health check is now propagated from the base image ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- `` configuration parameter to set the working directory. ([#1225](https://github.com/GoogleContainerTools/jib/issues/1225)) +- Adds support for configuring volumes. ([#1121](https://github.com/GoogleContainerTools/jib/issues/1121)) +- Exposed ports are now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) +- Docker health check is now propagated from the base image. ([#595](https://github.com/GoogleContainerTools/jib/issues/595)) ### Changed -- Removed `jib:exportDockerContext` goal ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) +- Removed `jib:exportDockerContext` goal. ([#1219](https://github.com/GoogleContainerTools/jib/issues/1219)) ### Fixed -- NullPointerException thrown with incomplete `auth` configuration ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) +- NullPointerException thrown with incomplete `auth` configuration. ([#1177](https://github.com/GoogleContainerTools/jib/issues/1177)) ## 0.10.0 ### Added -- Properties for each configuration parameter, allowing any parameter to be set via commandline ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) -- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) -- `` configuration parameter to configure the user and group to run the container as ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) -- Preliminary support for building images for WAR projects ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) -- `` object with a `` and `` field ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) +- Properties for each configuration parameter, allowing any parameter to be set via commandline. ([#728](https://github.com/GoogleContainerTools/jib/issues/728)) +- `` and `` can be used to specify a credential helper suffix or a full path to a credential helper executable. ([#925](https://github.com/GoogleContainerTools/jib/issues/925)) +- `` configuration parameter to configure the user and group to run the container as. ([#1029](https://github.com/GoogleContainerTools/jib/issues/1029)) +- Preliminary support for building images for WAR projects. ([#431](https://github.com/GoogleContainerTools/jib/issues/431)) +- `` object with a `` and `` field. ([#794](https://github.com/GoogleContainerTools/jib/issues/794)) - `` configures the extra layer directory (still also configurable via `...`) - `` is a list of `` objects, each with a `` and `` field, used to map a file on the container to the file's permission bits (represented as an octal string) -- Image digest is now written to `target/jib-image.digest` ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) -- Adds the layer type to the layer history as comments ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) +- Image digest is now written to `target/jib-image.digest`. ([#1155](https://github.com/GoogleContainerTools/jib/pull/1155)) +- Adds the layer type to the layer history as comments. ([#1198](https://github.com/GoogleContainerTools/jib/issues/1198)) ### Changed -- Removed deprecated ``, ``, ``, and `` in favor of the equivalents under `` ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) -- `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) -- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) +- Removed deprecated ``, ``, ``, and `` in favor of the equivalents under ``. ([#461](https://github.com/GoogleContainerTools/jib/issues/461)) +- `jib:exportDockerContext` generates different directory layout and `Dockerfile` to enable WAR support. ([#1007](https://github.com/GoogleContainerTools/jib/pull/1007)) +- File timestamps in the built image are set to 1 second since the epoch (hence 1970-01-01T00:00:01Z) to resolve compatibility with applications on Java 6 or below where the epoch means nonexistent or I/O errors; previously they were set to the epoch. ([#1079](https://github.com/GoogleContainerTools/jib/issues/1079)) ## 0.9.13 ### Fixed -- Adds environment variable configuration to Docker context generator ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) +- Adds environment variable configuration to Docker context generator. ([#890 (comment)](https://github.com/GoogleContainerTools/jib/issues/890#issuecomment-430227555)) ## 0.9.11 ### Added -- `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property) ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) -- `` configuration parameter to configure environment variables ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) -- `container.appRoot` configuration parameter to configure app root in the image ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) -- `` (list) defines additional tags to push to ([#1026](https://github.com/GoogleContainerTools/jib/pull/1026)) +- `` configuration parameter to skip Jib execution in multi-module projects (also settable via `jib.skip` property). ([#865](https://github.com/GoogleContainerTools/jib/issues/865)) +- `` configuration parameter to configure environment variables. ([#890](https://github.com/GoogleContainerTools/jib/issues/890)) +- `container.appRoot` configuration parameter to configure app root in the image. ([#984](https://github.com/GoogleContainerTools/jib/pull/984)) +- `` (list) defines additional tags to push to. ([#1026](https://github.com/GoogleContainerTools/jib/pull/1026)) ### Fixed -- Keep duplicate layers to match container history ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) +- Keep duplicate layers to match container history. ([#1017](https://github.com/GoogleContainerTools/jib/pull/1017)) ## 0.9.10 ### Added -- `` configuration parameter for configuring labels ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) -- `` configuration parameter to set the entrypoint ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) -- `history` to layer metadata ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) -- Propagates working directory from the base image ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) +- `` configuration parameter for configuring labels. ([#751](https://github.com/GoogleContainerTools/jib/issues/751)) +- `` configuration parameter to set the entrypoint. ([#579](https://github.com/GoogleContainerTools/jib/issues/579)) +- `history` to layer metadata. ([#875](https://github.com/GoogleContainerTools/jib/issues/875)) +- Propagates working directory from the base image. ([#902](https://github.com/GoogleContainerTools/jib/pull/902)) ### Fixed -- Corrects permissions for directories in the container filesystem ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) +- Corrects permissions for directories in the container filesystem. ([#772](https://github.com/GoogleContainerTools/jib/pull/772)) ## 0.9.9 ### Added -- Passthrough labels from base image ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) +- Passthrough labels from base image. ([#750](https://github.com/GoogleContainerTools/jib/pull/750/files)) ### Changed -- Reordered classpath in entrypoint to allow dependency patching ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) +- Reordered classpath in entrypoint to allow dependency patching. ([#777](https://github.com/GoogleContainerTools/jib/issues/777)) ### Fixed @@ -268,178 +268,178 @@ All notable changes to this project will be documented in this file. ### Added -- `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) -- Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) -- Docker context generation now includes snapshot dependencies and extra files ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -- Disable parallel operation by setting the `jibSerialize` system property to `true` ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) +- `` and `` parameters with `` and `` fields for simple authentication, similar to the Gradle plugin. ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Can set credentials via commandline using `jib.to.auth.username`, `jib.to.auth.password`, `jib.from.auth.username`, and `jib.from.auth.password` system properties. ([#693](https://github.com/GoogleContainerTools/jib/issues/693)) +- Docker context generation now includes snapshot dependencies and extra files. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Disable parallel operation by setting the `jibSerialize` system property to `true`. ([#682](https://github.com/GoogleContainerTools/jib/pull/682)) ### Changed -- Propagates environment variables from the base image ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) -- Skips execution if packaging is `pom` ([#735](https://github.com/GoogleContainerTools/jib/pull/735)) -- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates) ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) +- Propagates environment variables from the base image. ([#716](https://github.com/GoogleContainerTools/jib/pull/716)) +- Skips execution if packaging is `pom`. ([#735](https://github.com/GoogleContainerTools/jib/pull/735)) +- `allowInsecureRegistries` allows connecting to insecure HTTPS registries (for example, registries using self-signed certificates). ([#733](https://github.com/GoogleContainerTools/jib/pull/733)) ### Fixed -- Slow image reference parsing ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) -- Building empty layers ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) -- Duplicate layer entries causing unbounded cache growth ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) -- Incorrect authentication error message when target and base registry are the same ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) +- Slow image reference parsing. ([#680](https://github.com/GoogleContainerTools/jib/pull/680)) +- Building empty layers. ([#516](https://github.com/GoogleContainerTools/jib/pull/516/files)) +- Duplicate layer entries causing unbounded cache growth. ([#721](https://github.com/GoogleContainerTools/jib/issues/721)) +- Incorrect authentication error message when target and base registry are the same. ([#758](https://github.com/GoogleContainerTools/jib/issues/758)) ## 0.9.7 ### Added -- Snapshot dependencies are added as their own layer ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) -- `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load` ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) -- `` parameter to set the image creation time to the build time ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) -- Authentication over HTTP using the `sendCredentialsOverHttp` system property ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) -- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) +- Snapshot dependencies are added as their own layer. ([#584](https://github.com/GoogleContainerTools/jib/pull/584)) +- `jib:buildTar` goal to build an image tarball at `target/jib-image.tar`, which can be loaded into docker using `docker load`. ([#514](https://github.com/GoogleContainerTools/jib/issues/514)) +- `` parameter to set the image creation time to the build time. ([#413](https://github.com/GoogleContainerTools/jib/issues/413)) +- Authentication over HTTP using the `sendCredentialsOverHttp` system property. ([#599](https://github.com/GoogleContainerTools/jib/issues/599)) +- HTTP connection and read timeouts for registry interactions configurable with the `jib.httpTimeout` system property. ([#656](https://github.com/GoogleContainerTools/jib/pull/656)) ### Changed -- Docker context export parameter `-Djib.dockerDir` to `-DjibTargetDir` ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) +- Docker context export parameter `-Djib.dockerDir` to `-DjibTargetDir`. ([#662](https://github.com/GoogleContainerTools/jib/issues/662)) ### Fixed -- Using multi-byte characters in container configuration ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) -- For Docker Hub, also tries registry aliases when getting a credential from the Docker config ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) -- Decrypting credentials from Maven settings ([#592](https://github.com/GoogleContainerTools/jib/issues/592)) +- Using multi-byte characters in container configuration. ([#626](https://github.com/GoogleContainerTools/jib/issues/626)) +- For Docker Hub, also tries registry aliases when getting a credential from the Docker config. ([#605](https://github.com/GoogleContainerTools/jib/pull/605)) +- Decrypting credentials from Maven settings. ([#592](https://github.com/GoogleContainerTools/jib/issues/592)) ## 0.9.6 ### Fixed -- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true` ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) +- Using a private registry that does token authentication with `allowInsecureRegistries` set to `true`. ([#572](https://github.com/GoogleContainerTools/jib/pull/572)) ## 0.9.5 ### Added -- Incubating feature to build `src/main/jib` as extra layer in image ([#565](https://github.com/GoogleContainerTools/jib/pull/565)) +- Incubating feature to build `src/main/jib` as extra layer in image. ([#565](https://github.com/GoogleContainerTools/jib/pull/565)) ## 0.9.4 ### Fixed -- Fixed handling case-insensitive `Basic` authentication method ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) -- Fixed regression that broke pulling base images from registries that required token authentication ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) +- Fixed handling case-insensitive `Basic` authentication method. ([#546](https://github.com/GoogleContainerTools/jib/pull/546)) +- Fixed regression that broke pulling base images from registries that required token authentication. ([#549](https://github.com/GoogleContainerTools/jib/pull/549)) ## 0.9.3 ### Fixed -- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol) ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) +- Using Docker config for finding registry credentials (was not ignoring extra fields and handling `https` protocol). ([#524](https://github.com/GoogleContainerTools/jib/pull/524)) ## 0.9.2 ### Changed -- Minor improvements and issue fixes +- Minor improvements and issue fixes. ## 0.9.1 ### Added -- `` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`) ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) -- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) +- `` parameter to define container's exposed ports (similar to Dockerfile `EXPOSE`). ([#383](https://github.com/GoogleContainerTools/jib/issues/383)) +- Can set `allowInsecureRegistries` parameter to `true` to use registries that only support HTTP. ([#388](https://github.com/GoogleContainerTools/jib/issues/388)) ### Changed -- Fetches credentials from inferred credential helper before Docker config ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) -- Container creation date set to timestamp 0 ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) -- Does not authenticate base image pull unless necessary - reduces build time by about 500ms ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) -- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) +- Fetches credentials from inferred credential helper before Docker config. ([#401](https://github.com/GoogleContainerTools/jib/issues/401)) +- Container creation date set to timestamp 0. ([#341](https://github.com/GoogleContainerTools/jib/issues/341)) +- Does not authenticate base image pull unless necessary - reduces build time by about 500ms. ([#414](https://github.com/GoogleContainerTools/jib/pull/414)) +- `jvmFlags`, `mainClass`, `args`, and `format` are now grouped under `container` configuration object. ([#384](https://github.com/GoogleContainerTools/jib/issues/384)) ### Fixed -- Using Azure Container Registry now works - define credentials in Maven settings ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) -- Supports `access_token` as alias to `token` in registry authentication ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) +- Using Azure Container Registry now works - define credentials in Maven settings. ([#415](https://github.com/GoogleContainerTools/jib/issues/415)) +- Supports `access_token` as alias to `token` in registry authentication. ([#420](https://github.com/GoogleContainerTools/jib/pull/420)) ## 0.9.0 ### Added -- Better feedback for build failures ([#197](https://github.com/google/jib/pull/197)) -- Warns if specified `mainClass` is not a valid Java class ([#206](https://github.com/google/jib/issues/206)) -- Warns if build may not be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- `jib:dockerBuild` maven goal to build straight to Docker daemon ([#266](https://github.com/GoogleContainerTools/jib/pull/266)) -- `mainClass` is inferred by searching through class files if configuration is missing ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) -- Can now specify target image with `-Dimage` ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) -- `args` parameter to define default main args ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) +- Better feedback for build failures. ([#197](https://github.com/google/jib/pull/197)) +- Warns if specified `mainClass` is not a valid Java class. ([#206](https://github.com/google/jib/issues/206)) +- Warns if build may not be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- `jib:dockerBuild` maven goal to build straight to Docker daemon. ([#266](https://github.com/GoogleContainerTools/jib/pull/266)) +- `mainClass` is inferred by searching through class files if configuration is missing. ([#278](https://github.com/GoogleContainerTools/jib/pull/278)) +- Can now specify target image with `-Dimage`. ([#328](https://github.com/GoogleContainerTools/jib/issues/328)) +- `args` parameter to define default main args. ([#346](https://github.com/GoogleContainerTools/jib/issues/346)) ### Changed -- Removed `enableReproducibleBuilds` parameter - application layers will always be reproducible ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) -- Changed configuration schema to be more like configuration for `jib-gradle-plugin` - NOT compatible with prior versions of `jib-maven-plugin` ([#212](https://github.com/GoogleContainerTools/jib/issues/212)) -- `jib:dockercontext` has been changed to `jib:exportDockerContext` ([#350](https://github.com/GoogleContainerTools/jib/issues/350)) +- Removed `enableReproducibleBuilds` parameter - application layers will always be reproducible. ([#245](https://github.com/GoogleContainerTools/jib/pull/245)) +- Changed configuration schema to be more like configuration for `jib-gradle-plugin` - NOT compatible with prior versions of `jib-maven-plugin`. ([#212](https://github.com/GoogleContainerTools/jib/issues/212)) +- `jib:dockercontext` has been changed to `jib:exportDockerContext`. ([#350](https://github.com/GoogleContainerTools/jib/issues/350)) ### Fixed -- Directories in resources are added to classes layer ([#318](https://github.com/GoogleContainerTools/jib/issues/318)) +- Directories in resources are added to classes layer. ([#318](https://github.com/GoogleContainerTools/jib/issues/318)) ## 0.1.7 ### Fixed -- Using base images that lack entrypoints ([#284](https://github.com/GoogleContainerTools/jib/pull/284) +- Using base images that lack entrypoints. ([#284](https://github.com/GoogleContainerTools/jib/pull/284) ## 0.1.6 ### Changed -- Base image layers are now cached on a user-level rather than a project level - disable with `useOnlyProjectCache` configuration ([#29](https://github.com/google/jib/issues/29)) +- Base image layers are now cached on a user-level rather than a project level - disable with `useOnlyProjectCache` configuration. ([#29](https://github.com/google/jib/issues/29)) ### Fixed -- `jib:dockercontext` not building a `Dockerfile` ([#171](https://github.com/google/jib/pull/171)) -- Failure to parse Docker config with `HttpHeaders` field ([#175](https://github.com/google/jib/pull/175)) +- `jib:dockercontext` not building a `Dockerfile`. ([#171](https://github.com/google/jib/pull/171)) +- Failure to parse Docker config with `HttpHeaders` field. ([#175](https://github.com/google/jib/pull/175)) ## 0.1.5 ### Added -- Export a Docker context (including a Dockerfile) with `jib:dockercontext` ([#49](https://github.com/google/jib/issues/49)) +- Export a Docker context (including a Dockerfile) with `jib:dockercontext`. ([#49](https://github.com/google/jib/issues/49)) ## 0.1.4 ### Fixed -- Null tag validation generating NullPointerException ([#125](https://github.com/google/jib/issues/125)) -- Build failure on project with no dependencies ([#126](https://github.com/google/jib/issues/126)) +- Null tag validation generating NullPointerException. ([#125](https://github.com/google/jib/issues/125)) +- Build failure on project with no dependencies. ([#126](https://github.com/google/jib/issues/126)) ## 0.1.3 ### Added -- Build and push OCI container image ([#96](https://github.com/google/jib/issues/96)) +- Build and push OCI container image. ([#96](https://github.com/google/jib/issues/96)) ## 0.1.2 ### Added -- Use credentials from Docker config if none can be found otherwise ([#101](https://github.com/google/jib/issues/101)) -- Reproducible image building ([#7](https://github.com/google/jib/issues/7)) +- Use credentials from Docker config if none can be found otherwise. ([#101](https://github.com/google/jib/issues/101)) +- Reproducible image building. ([#7](https://github.com/google/jib/issues/7)) ## 0.1.1 ### Added -- Simple example `helloworld` project under `examples/` ([#62](https://github.com/google/jib/pull/62)) -- Better error messages when pushing an image manifest ([#63](https://github.com/google/jib/pull/63)) -- Validates target image configuration ([#63](https://github.com/google/jib/pull/63)) -- Configure multiple credential helpers with `credHelpers` ([#68](https://github.com/google/jib/pull/68)) -- Configure registry credentials with Maven settings ([#81](https://github.com/google/jib/pull/81)) +- Simple example `helloworld` project under `examples/`. ([#62](https://github.com/google/jib/pull/62)) +- Better error messages when pushing an image manifest. ([#63](https://github.com/google/jib/pull/63)) +- Validates target image configuration. ([#63](https://github.com/google/jib/pull/63)) +- Configure multiple credential helpers with `credHelpers`. ([#68](https://github.com/google/jib/pull/68)) +- Configure registry credentials with Maven settings. ([#81](https://github.com/google/jib/pull/81)) ### Changed -- Removed configuration `credentialHelperName` ([#68](https://github.com/google/jib/pull/68)) +- Removed configuration `credentialHelperName`. ([#68](https://github.com/google/jib/pull/68)) ### Fixed -- Build failure on Windows ([#74](https://github.com/google/jib/issues/74)) -- Infers common credential helper names (for GCR and ECR) ([#64](https://github.com/google/jib/pull/64)) -- Cannot use private base image ([#68](https://github.com/google/jib/pull/68)) -- Building applications with no resources ([#73](https://github.com/google/jib/pull/73)) -- Pushing to registries like Docker Hub and ACR ([#75](https://github.com/google/jib/issues/75)) -- Cannot build with files having long file names (> 100 chars) ([#91](https://github.com/google/jib/issues/91)) +- Build failure on Windows. ([#74](https://github.com/google/jib/issues/74)) +- Infers common credential helper names (for GCR and ECR). ([#64](https://github.com/google/jib/pull/64)) +- Cannot use private base image. ([#68](https://github.com/google/jib/pull/68)) +- Building applications with no resources. ([#73](https://github.com/google/jib/pull/73)) +- Pushing to registries like Docker Hub and ACR. ([#75](https://github.com/google/jib/issues/75)) +- Cannot build with files having long file names (> 100 chars). ([#91](https://github.com/google/jib/issues/91)) From cdc428020e50a5a64f7645d5126fbcfafa8197b1 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 18 Sep 2019 13:42:13 -0400 Subject: [PATCH 0747/2020] Minor cleanups (#2009) --- .../tools/jib/json/JsonTemplateMapper.java | 18 +++++++++++++++++- .../jib/registry/RegistryAuthenticator.java | 18 +++++------------- .../tools/jib/json/JsonTemplateMapperTest.java | 10 ++++++++++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index 916a3d8540..a3d178eb30 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -66,7 +66,9 @@ public class JsonTemplateMapper { */ public static T readJsonFromFile(Path jsonFile, Class templateClass) throws IOException { - return objectMapper.readValue(Files.newInputStream(jsonFile), templateClass); + try (InputStream fileIn = Files.newInputStream(jsonFile)) { + return objectMapper.readValue(fileIn, templateClass); + } } /** @@ -88,6 +90,20 @@ public static T readJsonFromFileWithLock( } } + /** + * Deserializes a JSON object from a JSON input stream. + * + * @param child type of {@link JsonTemplate} + * @param jsonStream input stream + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonString} + * @throws IOException if an error occurred during parsing the JSON + */ + public static T readJson(InputStream jsonStream, Class templateClass) + throws IOException { + return objectMapper.readValue(jsonStream, templateClass); + } + /** * Deserializes a JSON object from a JSON string. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 2b34e5e080..a0869a42ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.api.client.http.HttpMethods; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.blob.Blobs; @@ -31,13 +32,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.collect.ImmutableMap; -import com.google.common.io.CharStreams; import com.google.common.net.MediaType; import java.io.IOException; -import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; @@ -245,10 +243,7 @@ private Authorization authenticate(@Nullable Credential credential, String scope scope, registryEndpointRequestProperties.getSourceImageName(), "pull"); - Authorization auth = authenticate(credential, scopes); - if (auth != null) { - return auth; - } + return authenticate(credential, scopes); } catch (RegistryAuthenticationFailedException ex) { // Unable to obtain authorization with source image: fallthrough and try without } @@ -279,14 +274,11 @@ private Authorization authenticate( Authorization.fromBasicCredentials(credential.getUsername(), credential.getPassword())); } - Request request = requestBuilder.build(); - Response response = - isOAuth2Auth(credential) ? connection.post(request) : connection.get(request); - String responseString = - CharStreams.toString(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); + String httpMethod = isOAuth2Auth(credential) ? HttpMethods.POST : HttpMethods.GET; + Response response = connection.send(httpMethod, requestBuilder.build()); AuthenticationResponseTemplate responseJson = - JsonTemplateMapper.readJson(responseString, AuthenticationResponseTemplate.class); + JsonTemplateMapper.readJson(response.getBody(), AuthenticationResponseTemplate.class); if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java index 87a586aa66..a72ca64b7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -153,4 +154,13 @@ public void testToBlob_listOfJson() throws IOException, URISyntaxException { Assert.assertEquals(jsonString, JsonTemplateMapper.toUtf8String(listOfJson)); } + + @Test + public void testReadJson_inputStream() throws IOException { + String testJson = "{\"number\":3, \"text\":\"cool\"}"; + ByteArrayInputStream in = new ByteArrayInputStream(testJson.getBytes(StandardCharsets.UTF_8)); + TestJson json = JsonTemplateMapper.readJson(in, TestJson.class); + Assert.assertEquals(3, json.number); + Assert.assertEquals("cool", json.text); + } } From 41057c3fc824ae38a813a4e7a4836d114acfcd20 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 18 Sep 2019 14:24:35 -0400 Subject: [PATCH 0748/2020] Add Kotlin support for Skaffold-Jib integration (#2010) * Add Kotlin support for Skaffold-Jib integration * Exclude test-compile goal --- .../tools/jib/maven/skaffold/FilesMojoV2.java | 47 ++++ .../maven/skaffold/FilesMojoV2KotlinTest.java | 217 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2KotlinTest.java diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index 6985dfa739..60b74d817d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven.skaffold; +import com.google.api.client.util.Strings; import com.google.cloud.tools.jib.maven.MavenProjectProperties; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -27,8 +28,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -67,6 +70,46 @@ public class FilesMojoV2 extends SkaffoldBindingMojo { @VisibleForTesting static final String GOAL_NAME = "_skaffold-files-v2"; + // extracting source directories based on https://kotlinlang.org/docs/reference/using-maven.html + @VisibleForTesting + static Set getKotlinSourceDirectories(MavenProject project) { + Plugin kotlinPlugin = project.getPlugin("org.jetbrains.kotlin:kotlin-maven-plugin"); + if (kotlinPlugin == null) { + return Collections.emptySet(); + } + + Path projectBaseDir = project.getBasedir().toPath(); + + // Extract values from in the plugin . Sample: + // + // + // src/main/kotlin + // ${project.basedir}/src/main/java + // + // + Set kotlinSourceDirectories = + kotlinPlugin + .getExecutions() + .stream() + .filter(execution -> !execution.getGoals().contains("test-compile")) + .map(execution -> (Xpp3Dom) execution.getConfiguration()) + .filter(Objects::nonNull) + .map(configuration -> configuration.getChild("sourceDirs")) + .filter(Objects::nonNull) + .map(sourceDirs -> Arrays.asList(sourceDirs.getChildren())) + .flatMap(Collection::stream) // "array of arrays" into "arrays" + .map(Xpp3Dom::getValue) + .filter(value -> !Strings.isNullOrEmpty(value)) + .map(Paths::get) + .map(path -> path.isAbsolute() ? path : projectBaseDir.resolve(path)) + .collect(Collectors.toSet()); + + Path conventionalDirectory = projectBaseDir.resolve(Paths.get("src", "main", "kotlin")); + kotlinSourceDirectories.add(conventionalDirectory); + + return kotlinSourceDirectories; + } + @Nullable @Parameter(defaultValue = "${session}", required = true, readonly = true) private MavenSession session; @@ -98,6 +141,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { // Add sources directory (resolved by maven to be an absolute path) skaffoldFilesOutput.addInput(Paths.get(project.getBuild().getSourceDirectory())); + for (Path directory : getKotlinSourceDirectories(project)) { + skaffoldFilesOutput.addInput(directory); + } + // Add resources directory (resolved by maven to be an absolute path) project .getBuild() diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2KotlinTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2KotlinTest.java new file mode 100644 index 0000000000..8eba9ce30b --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2KotlinTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Kotlin-related tests for {@link FilesMojoV2}. */ +@RunWith(MockitoJUnitRunner.class) +public class FilesMojoV2KotlinTest { + + private final PluginExecution pluginExecution1 = new PluginExecution(); + private final PluginExecution pluginExecution2 = new PluginExecution(); + private final Xpp3Dom configuration1 = new Xpp3Dom("configuration"); + private final Xpp3Dom configuration2 = new Xpp3Dom("configuration"); + private final Xpp3Dom sourceDirs1 = new Xpp3Dom("sourceDirs"); + private final Xpp3Dom sourceDirs2 = new Xpp3Dom("sourceDirs"); + private final Xpp3Dom sourceDir1 = new Xpp3Dom("sourceDir"); + private final Xpp3Dom sourceDir2 = new Xpp3Dom("sourceDir"); + private final Xpp3Dom sourceDir3 = new Xpp3Dom("sourceDir"); + private final Xpp3Dom sourceDir4 = new Xpp3Dom("sourceDir"); + + @Mock private MavenProject mavenProject; + @Mock private Plugin kotlinPlugin; + + @Before + public void setUp() { + Mockito.when(mavenProject.getPlugin("org.jetbrains.kotlin:kotlin-maven-plugin")) + .thenReturn(kotlinPlugin); + Mockito.when(mavenProject.getBasedir()).thenReturn(new File("/base")); + + pluginExecution1.setConfiguration(configuration1); + pluginExecution2.setConfiguration(configuration2); + } + + @Test + public void getKotlinSourceDirectories_noKotlinPlugin() { + Mockito.when(mavenProject.getPlugin(Mockito.anyString())).thenReturn(null); + Assert.assertEquals(ImmutableSet.of(), FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_noExecutions() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Collections.emptyList()); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_noConfiguration() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + pluginExecution1.setConfiguration(null); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_noSourceDirs() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_noSourceDirsChildren() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + configuration1.addChild(sourceDirs1); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_nullSourceDir() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + configuration1.addChild(sourceDirs1); + sourceDirs1.addChild(sourceDir1); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_emptySourceDir() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + configuration1.addChild(sourceDirs1); + sourceDirs1.addChild(sourceDir1); + sourceDir1.setValue(""); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_relativePath() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + configuration1.addChild(sourceDirs1); + sourceDirs1.addChild(sourceDir1); + sourceDir1.setValue("kotlin/src"); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin"), Paths.get("/base/kotlin/src")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_absolutePath() { + Mockito.when(kotlinPlugin.getExecutions()).thenReturn(Arrays.asList(pluginExecution1)); + configuration1.addChild(sourceDirs1); + sourceDirs1.addChild(sourceDir1); + sourceDir1.setValue("/absolute/src"); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin"), Paths.get("/absolute/src")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_complex() { + Mockito.when(kotlinPlugin.getExecutions()) + .thenReturn(Arrays.asList(pluginExecution1, pluginExecution2)); + configuration1.addChild(sourceDirs1); + configuration2.addChild(sourceDirs2); + sourceDirs1.addChild(sourceDir1); + sourceDirs1.addChild(sourceDir2); + sourceDirs2.addChild(sourceDir3); + sourceDirs2.addChild(sourceDir4); + sourceDir1.setValue("/absolute/src1"); + sourceDir2.setValue("relative/src2"); + sourceDir3.setValue("/absolute/src3"); + sourceDir4.setValue("relative/src4"); + + Assert.assertEquals( + ImmutableSet.of( + Paths.get("/base/src/main/kotlin"), + Paths.get("/absolute/src1"), + Paths.get("/absolute/src3"), + Paths.get("/base/relative/src2"), + Paths.get("/base/relative/src4")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_noDuplicates() { + Mockito.when(kotlinPlugin.getExecutions()) + .thenReturn(Arrays.asList(pluginExecution1, pluginExecution2)); + configuration1.addChild(sourceDirs1); + configuration2.addChild(sourceDirs2); + sourceDirs1.addChild(sourceDir1); + sourceDirs1.addChild(sourceDir2); + sourceDirs2.addChild(sourceDir3); + sourceDirs2.addChild(sourceDir4); + sourceDir1.setValue("src/main/kotlin"); + sourceDir2.setValue("/base/another/src"); + sourceDir3.setValue("another/src"); + sourceDir4.setValue("/base/src/main/kotlin"); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin"), Paths.get("/base/another/src")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } + + @Test + public void getKotlinSourceDirectories_excludeTestCompileGoal() { + Mockito.when(kotlinPlugin.getExecutions()) + .thenReturn(Arrays.asList(pluginExecution1, pluginExecution2)); + pluginExecution1.setGoals(Arrays.asList("compile")); + pluginExecution2.setGoals(Arrays.asList("tomato", "test-compile")); + configuration1.addChild(sourceDirs1); + configuration2.addChild(sourceDirs2); + sourceDirs1.addChild(sourceDir1); + sourceDirs2.addChild(sourceDir2); + sourceDir1.setValue("/included"); + sourceDir2.setValue("/should/not/be/included"); + + Assert.assertEquals( + ImmutableSet.of(Paths.get("/base/src/main/kotlin"), Paths.get("/included")), + FilesMojoV2.getKotlinSourceDirectories(mavenProject)); + } +} From b5a3ee9a661a048119258e886bf269f5d2195b3a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 20 Sep 2019 11:42:48 -0400 Subject: [PATCH 0749/2020] Check exit code for 'docker inspect' (#2012) --- .../cloud/tools/jib/docker/DockerClient.java | 61 +++++++++++-------- .../tools/jib/docker/DockerClientTest.java | 31 ++++++++-- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index c9e3ad2505..0bdcd958a5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -135,16 +136,16 @@ public String load(ImageTarball imageTarball, Consumer writtenByteCountLis imageTarball.writeTo(stdin); } catch (IOException ex) { - // Tries to read from stderr. + // Tries to read from stderr. Not using getStderrOutput(), as we want to show the error + // message from the tarball I/O write failure when reading from stderr fails. String error; try (InputStreamReader stderr = new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { error = CharStreams.toString(stderr); - } catch (IOException ignored) { - // This ignores exceptions from reading stderr and throws the original exception from + // This ignores exceptions from reading stderr and uses the original exception from // writing to stdin. - throw ex; + error = ex.getMessage(); } throw new IOException("'docker load' command failed with error: " + error, ex); } @@ -154,11 +155,8 @@ public String load(ImageTarball imageTarball, Consumer writtenByteCountLis String output = CharStreams.toString(stdout); if (dockerProcess.waitFor() != 0) { - try (InputStreamReader stderr = - new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { - throw new IOException( - "'docker load' command failed with output: " + CharStreams.toString(stderr)); - } + throw new IOException( + "'docker load' command failed with error: " + getStderrOutput(dockerProcess)); } return output; @@ -180,20 +178,17 @@ public void save( ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) throws InterruptedException, IOException { Process dockerProcess = docker("save", imageReference.toString()); + try (InputStream stdout = new BufferedInputStream(dockerProcess.getInputStream()); - NotifyingOutputStream fileStream = - new NotifyingOutputStream( - new BufferedOutputStream(Files.newOutputStream(outputPath)), - writtenByteCountListener)) { - ByteStreams.copy(stdout, fileStream); + OutputStream fileStream = new BufferedOutputStream(Files.newOutputStream(outputPath)); + NotifyingOutputStream notifyingFileStream = + new NotifyingOutputStream(fileStream, writtenByteCountListener)) { + ByteStreams.copy(stdout, notifyingFileStream); } if (dockerProcess.waitFor() != 0) { - try (InputStreamReader stderr = - new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { - throw new IOException( - "'docker save' command failed with output: " + CharStreams.toString(stderr)); - } + throw new IOException( + "'docker save' command failed with error: " + getStderrOutput(dockerProcess)); } } @@ -205,7 +200,7 @@ public void save( * @param newImageReference the new image reference * @see https://docs.docker.com/engine/reference/commandline/tag/ - * @throws InterruptedException if the 'docker tag' process is interrupted. + * @throws InterruptedException if the 'docker tag' process is interrupted * @throws IOException if an I/O exception occurs or {@code docker tag} failed */ public void tag(ImageReference originalImageReference, ImageReference newImageReference) @@ -213,13 +208,9 @@ public void tag(ImageReference originalImageReference, ImageReference newImageRe // Runs 'docker tag'. Process dockerProcess = docker("tag", originalImageReference.toString(), newImageReference.toString()); - if (dockerProcess.waitFor() != 0) { - try (InputStreamReader stderr = - new InputStreamReader(dockerProcess.getErrorStream(), StandardCharsets.UTF_8)) { - throw new IOException( - "'docker tag' command failed with error: " + CharStreams.toString(stderr)); - } + throw new IOException( + "'docker tag' command failed with error: " + getStderrOutput(dockerProcess)); } } @@ -229,9 +220,16 @@ public void tag(ImageReference originalImageReference, ImageReference newImageRe * @param imageReference the image to find the size of * @return the size in bytes * @throws IOException if an I/O exception occurs + * @throws InterruptedException if the 'docker inspect' process is interrupted */ - public long sizeOf(ImageReference imageReference) throws IOException { + public long sizeOf(ImageReference imageReference) throws IOException, InterruptedException { Process sizeProcess = docker("inspect", "-f", "{{.Size}}", imageReference.toString()); + + if (sizeProcess.waitFor() != 0) { + throw new IOException( + "'docker inspect' command failed with error: " + getStderrOutput(sizeProcess)); + } + return Long.parseLong( CharStreams.toString( new InputStreamReader(sizeProcess.getInputStream(), StandardCharsets.UTF_8)) @@ -242,4 +240,13 @@ public long sizeOf(ImageReference imageReference) throws IOException { private Process docker(String... subCommand) throws IOException { return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); } + + private static String getStderrOutput(Process process) { + try (InputStreamReader stderr = + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { + return CharStreams.toString(stderr); + } catch (IOException ex) { + return "unknown (failed to read error message from stderr due to " + ex.getMessage() + ")"; + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 010de52694..bc3e9d19f8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -122,7 +122,6 @@ public void write(int b) throws IOException { @Test public void testLoad_stdinFail_stderrFail() throws InterruptedException { DockerClient testDockerClient = new DockerClient(ignored -> mockProcessBuilder); - IOException expectedIOException = new IOException(); Mockito.when(mockProcess.getOutputStream()) .thenReturn( @@ -130,7 +129,7 @@ public void testLoad_stdinFail_stderrFail() throws InterruptedException { @Override public void write(int b) throws IOException { - throw expectedIOException; + throw new IOException("I/O failed"); } }); Mockito.when(mockProcess.getErrorStream()) @@ -148,7 +147,7 @@ public int read() throws IOException { Assert.fail("Write should have failed"); } catch (IOException ex) { - Assert.assertSame(expectedIOException, ex); + Assert.assertEquals("'docker load' command failed with error: I/O failed", ex.getMessage()); } } @@ -168,7 +167,7 @@ public void testLoad_stdoutFail() throws InterruptedException { Assert.fail("Process should have failed"); } catch (IOException ex) { - Assert.assertEquals("'docker load' command failed with output: error", ex.getMessage()); + Assert.assertEquals("'docker load' command failed with error: error", ex.getMessage()); } } @@ -203,7 +202,7 @@ public void testSave_fail() throws InterruptedException { Assert.fail("docker save should have failed"); } catch (IOException ex) { - Assert.assertEquals("'docker save' command failed with output: error", ex.getMessage()); + Assert.assertEquals("'docker save' command failed with error: error", ex.getMessage()); } } @@ -267,6 +266,28 @@ public void testTag_fail() throws InterruptedException, InvalidImageReferenceExc } } + @Test + public void testSize_fail() throws InterruptedException { + DockerClient testDockerClient = + new DockerClient( + subcommand -> { + Assert.assertEquals(Arrays.asList("inspect", "-f", "{{.Size}}", "image"), subcommand); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + try { + testDockerClient.sizeOf(ImageReference.of(null, "image", null)); + Assert.fail("docker inspect should have failed"); + + } catch (IOException ex) { + Assert.assertEquals("'docker inspect' command failed with error: error", ex.getMessage()); + } + } + private DockerClient makeDockerSaveClient() { return new DockerClient( subcommand -> { From d92f83f0683d9ee669939bf9cbdeb655c3960d8d Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 23 Sep 2019 13:22:17 -0400 Subject: [PATCH 0750/2020] Update ASM to 7.1 (#2017) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 983cb4db3c..f542242094 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ subprojects { COMMONS_COMPRESS: '1.18', JACKSON_DATABIND: '2.9.9.2', - ASM: '7.0', + ASM: '7.1', //test JUNIT: '4.12', From 8d6efa5b9189130c5baa010328e185e352558374 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 24 Sep 2019 13:52:09 -0400 Subject: [PATCH 0751/2020] Fix local base image temp directory cleanup (#2018) --- .../jib/builder/steps/ExtractTarStep.java | 10 +-- .../jib/builder/steps/SaveDockerStep.java | 11 +-- .../tools/jib/builder/steps/StepsRunner.java | 15 +++- .../tools/jib/cache/CacheStorageWriter.java | 20 +++--- .../tools/jib/filesystem/FileOperations.java | 21 ------ .../jib/filesystem/TempDirectoryProvider.java | 71 +++++++++++++++++++ .../jib/filesystem/TemporaryDirectory.java | 65 ----------------- .../jib/builder/steps/ExtractTarStepTest.java | 15 +++- ...st.java => TempDirectoryProviderTest.java} | 33 +++++---- 9 files changed, 138 insertions(+), 123 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java rename jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/{TemporaryDirectoryTest.java => TempDirectoryProviderTest.java} (62%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java index ee0bc497a6..05652ba3bd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java @@ -31,7 +31,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -88,26 +88,28 @@ static boolean isGzipped(Path path) throws IOException { private final BuildConfiguration buildConfiguration; private final Path tarPath; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final TempDirectoryProvider tempDirectoryProvider; ExtractTarStep( BuildConfiguration buildConfiguration, Path tarPath, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + TempDirectoryProvider tempDirectoryProvider) { this.buildConfiguration = buildConfiguration; this.tarPath = tarPath; this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.tempDirectoryProvider = tempDirectoryProvider; } @Override public LocalImage call() throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, CacheCorruptedException { - Path destination = Files.createTempDirectory("jib-extract-tar"); + Path destination = tempDirectoryProvider.newDirectory(); try (TimerEventDispatcher ignored = new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Extracting tar " + tarPath + " into " + destination)) { - FileOperations.deleteRecursiveOnExit(destination); TarExtractor.extract(tarPath, destination); InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java index 0329c61360..9b8548007f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java @@ -22,9 +22,8 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.filesystem.FileOperations; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.Callable; @@ -34,20 +33,22 @@ public class SaveDockerStep implements Callable { private final BuildConfiguration buildConfiguration; private final DockerClient dockerClient; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; + private final TempDirectoryProvider tempDirectoryProvider; SaveDockerStep( BuildConfiguration buildConfiguration, DockerClient dockerClient, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + TempDirectoryProvider tempDirectoryProvider) { this.buildConfiguration = buildConfiguration; this.dockerClient = dockerClient; this.progressEventDispatcherFactory = progressEventDispatcherFactory; + this.tempDirectoryProvider = tempDirectoryProvider; } @Override public Path call() throws IOException, InterruptedException { - Path outputDir = Files.createTempDirectory("jib-docker-save"); - FileOperations.deleteRecursiveOnExit(outputDir); + Path outputDir = tempDirectoryProvider.newDirectory(); Path outputPath = outputDir.resolve("out.tar"); ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); try (TimerEventDispatcher ignored = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index a16624d0d1..bf447eca5b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; @@ -102,6 +103,7 @@ private static List realizeFutures(List> futures) private final ExecutorService executorService; private final BuildConfiguration buildConfiguration; + private final TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); // We save steps to run by wrapping each step into a Runnable, only because of the unfortunate // chicken-and-egg situation arising from using ProgressEventDispatcher. The current @@ -181,6 +183,9 @@ public BuildResult run() throws ExecutionException, InterruptedException { unrolled = (ExecutionException) unrolled.getCause(); } throw unrolled; + + } finally { + tempDirectoryProvider.close(); } } @@ -239,7 +244,10 @@ private void saveDocker() { results.tarPath = executorService.submit( new SaveDockerStep( - buildConfiguration, dockerClient.get(), childProgressDispatcherFactory)); + buildConfiguration, + dockerClient.get(), + childProgressDispatcherFactory, + tempDirectoryProvider)); } private void extractTar() { @@ -249,7 +257,10 @@ private void extractTar() { executorService.submit( () -> new ExtractTarStep( - buildConfiguration, results.tarPath.get(), childProgressDispatcherFactory) + buildConfiguration, + results.tarPath.get(), + childProgressDispatcherFactory, + tempDirectoryProvider) .call()); results.baseImageAndAuth = executorService.submit( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index 5704ef4434..2995f7c5a9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.filesystem.LockFile; -import com.google.cloud.tools.jib.filesystem.TemporaryDirectory; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.hash.CountingDigestOutputStream; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; @@ -171,9 +171,9 @@ CachedLayer writeCompressed(Blob compressedLayerBlob) throws IOException { // Creates the temporary directory. Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); - try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { - Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); // Writes the layer file to the temporary directory. WrittenLayer writtenLayer = @@ -220,9 +220,9 @@ CachedLayer writeUncompressed(Blob uncompressedLayerBlob, @Nullable DescriptorDi // Creates the temporary directory. The temporary directory must be in the same FileStore as the // final location for Files.move to work. Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); - try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { - Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); // Writes the layer file to the temporary directory. WrittenLayer writtenLayer = @@ -261,9 +261,9 @@ CachedLayer writeUncompressed(Blob uncompressedLayerBlob, @Nullable DescriptorDi CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) throws IOException { Files.createDirectories(cacheStorageFiles.getLocalDirectory()); Files.createDirectories(cacheStorageFiles.getTemporaryDirectory()); - try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(cacheStorageFiles.getTemporaryDirectory())) { - Path temporaryLayerDirectory = temporaryDirectory.getDirectory(); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path temporaryLayerDirectory = + tempDirectoryProvider.newDirectory(cacheStorageFiles.getTemporaryDirectory()); Path temporaryLayerFile = cacheStorageFiles.getTemporaryLayerFile(temporaryLayerDirectory); BlobDescriptor layerBlobDescriptor; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java index 5a36dbec78..d4f6e09839 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.filesystem; import com.google.common.collect.ImmutableList; -import com.google.common.io.MoreFiles; -import com.google.common.io.RecursiveDeleteOption; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; @@ -87,24 +85,5 @@ public static OutputStream newLockingOutputStream(Path file) throws IOException return Channels.newOutputStream(channel); } - /** - * Sets up a shutdown hook that tries to delete a file or directory. - * - * @param path the path to the file or directory - */ - public static void deleteRecursiveOnExit(Path path) { - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - if (Files.exists(path)) { - try { - MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); - } catch (IOException ignored) { - } - } - })); - } - private FileOperations() {} } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java new file mode 100644 index 0000000000..6f48528449 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** Creates temporary directories and deletes them all when closed. */ +public class TempDirectoryProvider implements Closeable { + + private final Set directories = Collections.synchronizedSet(new HashSet<>()); + + /** + * Creates a new temporary directory. + * + * @return the path to the temporary directory + * @throws IOException if creating the directory fails + */ + public Path newDirectory() throws IOException { + Path path = Files.createTempDirectory(null); + directories.add(path); + return path; + } + + /** + * Creates a new temporary directory. + * + * @param parentDirectory the directory to create the temp directory inside + * @return the path to the temporary directory + * @throws IOException if creating the directory fails + */ + public Path newDirectory(Path parentDirectory) throws IOException { + Path path = Files.createTempDirectory(parentDirectory, null); + directories.add(path); + return path; + } + + @Override + public void close() { + for (Path path : directories) { + if (Files.exists(path)) { + try { + MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); + } catch (IOException ignored) { + } + } + } + directories.clear(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java deleted file mode 100644 index 99977dddbb..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectory.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.filesystem; - -import com.google.common.io.MoreFiles; -import com.google.common.io.RecursiveDeleteOption; -import java.io.Closeable; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.FileSystemException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * A temporary directory that tries to delete itself upon close. Note that deletion is NOT - * guaranteed. - */ -public class TemporaryDirectory implements Closeable { - - private final Path temporaryDirectory; - - /** - * Creates a new temporary directory under an existing {@code parentDirectory}. - * - * @param parentDirectory the directory to create the temporary directory within - * @throws IOException if an I/O exception occurs - */ - public TemporaryDirectory(Path parentDirectory) throws IOException { - temporaryDirectory = Files.createTempDirectory(parentDirectory, null); - } - - /** - * Gets the temporary directory. - * - * @return the temporary directory. - */ - public Path getDirectory() { - return temporaryDirectory; - } - - @Override - public void close() throws IOException { - if (Files.exists(temporaryDirectory)) { - try { - MoreFiles.deleteRecursively(temporaryDirectory, RecursiveDeleteOption.ALLOW_INSECURE); - } catch (FileNotFoundException | FileSystemException ex) { - // TODO log error; deletion is best-effort - } - } - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java index 4afc6f09f5..cdcc251f13 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; import com.google.common.io.Resources; @@ -73,7 +74,12 @@ public void testCall_validDocker() BadContainerConfigurationFormatException, IOException, CacheCorruptedException { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = - new ExtractTarStep(buildConfiguration, dockerBuild, progressEventDispatcherFactory).call(); + new ExtractTarStep( + buildConfiguration, + dockerBuild, + progressEventDispatcherFactory, + new TempDirectoryProvider()) + .call(); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); @@ -98,7 +104,12 @@ public void testCall_validTar() BadContainerConfigurationFormatException, IOException, CacheCorruptedException { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = - new ExtractTarStep(buildConfiguration, tarBuild, progressEventDispatcherFactory).call(); + new ExtractTarStep( + buildConfiguration, + tarBuild, + progressEventDispatcherFactory, + new TempDirectoryProvider()) + .call(); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java similarity index 62% rename from jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java index af129c2a0c..2c2575a508 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TemporaryDirectoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java @@ -27,8 +27,8 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -/** Tests for {@link TemporaryDirectory}. */ -public class TemporaryDirectoryTest { +/** Tests for {@link TempDirectoryProvider}. */ +public class TempDirectoryProviderTest { private static void createFilesInDirectory(Path directory) throws IOException, URISyntaxException { @@ -41,13 +41,18 @@ private static void createFilesInDirectory(Path directory) @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void testClose_directoryDeleted() throws IOException, URISyntaxException { - try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(temporaryFolder.newFolder().toPath())) { - createFilesInDirectory(temporaryDirectory.getDirectory()); + public void testClose_directoriesDeleted() throws IOException, URISyntaxException { + Path parent = temporaryFolder.newFolder().toPath(); - temporaryDirectory.close(); - Assert.assertFalse(Files.exists(temporaryDirectory.getDirectory())); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path directory1 = tempDirectoryProvider.newDirectory(parent); + createFilesInDirectory(directory1); + Path directory2 = tempDirectoryProvider.newDirectory(parent); + createFilesInDirectory(directory2); + + tempDirectoryProvider.close(); + Assert.assertFalse(Files.exists(directory1)); + Assert.assertFalse(Files.exists(directory2)); } } @@ -55,15 +60,15 @@ public void testClose_directoryDeleted() throws IOException, URISyntaxException public void testClose_directoryNotDeletedIfMoved() throws IOException, URISyntaxException { Path destinationParent = temporaryFolder.newFolder().toPath(); - try (TemporaryDirectory temporaryDirectory = - new TemporaryDirectory(temporaryFolder.newFolder().toPath())) { - createFilesInDirectory(temporaryDirectory.getDirectory()); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path directory = tempDirectoryProvider.newDirectory(destinationParent); + createFilesInDirectory(directory); Assert.assertFalse(Files.exists(destinationParent.resolve("destination"))); - Files.move(temporaryDirectory.getDirectory(), destinationParent.resolve("destination")); + Files.move(directory, destinationParent.resolve("destination")); - temporaryDirectory.close(); - Assert.assertFalse(Files.exists(temporaryDirectory.getDirectory())); + tempDirectoryProvider.close(); + Assert.assertFalse(Files.exists(directory)); Assert.assertTrue(Files.exists(destinationParent.resolve("destination"))); } } From f14e299e3c3572db52a940e2965ae688d2af7b90 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 24 Sep 2019 14:54:14 -0400 Subject: [PATCH 0752/2020] Update changelog for #2016 (#2019) --- jib-core/CHANGELOG.md | 2 ++ jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 846be6fc94..528b3ef581 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) + ## 0.11.0 ### Added diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3eede40c3f..82d0198fe7 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) + ## 1.6.1 ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 698869fb5a..ffd5185d58 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) + ## 1.6.1 ### Fixed From 930d9110d8448fce83e90036ad3d249b347beff7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 25 Sep 2019 14:43:20 -0400 Subject: [PATCH 0753/2020] Parallelize local base image compression (#2022) --- jib-core/CHANGELOG.md | 2 ++ .../jib/builder/steps/ExtractTarStep.java | 32 ++++++++++++++----- .../steps/ObtainBaseImageLayerStep.java | 2 +- .../tools/jib/builder/steps/StepsRunner.java | 1 + .../jib/builder/steps/ExtractTarStepTest.java | 11 +++++-- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 528b3ef581..0cb95d39b6 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java index 05652ba3bd..8e996e292c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java @@ -51,6 +51,9 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -85,16 +88,19 @@ static boolean isGzipped(Path path) throws IOException { } } + private final ExecutorService executorService; private final BuildConfiguration buildConfiguration; private final Path tarPath; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final TempDirectoryProvider tempDirectoryProvider; ExtractTarStep( + ExecutorService executorService, BuildConfiguration buildConfiguration, Path tarPath, ProgressEventDispatcher.Factory progressEventDispatcherFactory, TempDirectoryProvider tempDirectoryProvider) { + this.executorService = executorService; this.buildConfiguration = buildConfiguration; this.tarPath = tarPath; this.progressEventDispatcherFactory = progressEventDispatcherFactory; @@ -104,7 +110,7 @@ static boolean isGzipped(Path path) throws IOException { @Override public LocalImage call() throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, - CacheCorruptedException { + ExecutionException, InterruptedException { Path destination = tempDirectoryProvider.newDirectory(); try (TimerEventDispatcher ignored = new TimerEventDispatcher( @@ -139,21 +145,31 @@ public LocalImage call() // Process layer blobs // TODO: Optimize; compressing/calculating layer digests is slow - // e.g. parallelize, faster compression method + // e.g. faster compression method try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( "processing base image layers", layerFiles.size())) { List layers = new ArrayList<>(layerFiles.size()); V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + // Start compressing layers in parallel + List> cachedLayers = new ArrayList<>(); for (int index = 0; index < layerFiles.size(); index++) { Path layerFile = destination.resolve(layerFiles.get(index)); - CachedLayer layer = - getCachedTarLayer( - configurationTemplate.getLayerDiffId(index), - layerFile, - layersAreCompressed, - progressEventDispatcher.newChildProducer()); + DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); + ProgressEventDispatcher.Factory progressEventDispatcherFactory = + progressEventDispatcher.newChildProducer(); + + cachedLayers.add( + executorService.submit( + () -> + getCachedTarLayer( + diffId, layerFile, layersAreCompressed, progressEventDispatcherFactory))); + } + + // Collect compressed layers and add to manifest + for (Future layerFuture : cachedLayers) { + CachedLayer layer = layerFuture.get(); layers.add(new PreparedLayer.Builder(layer).build()); v22Manifest.addLayer(layer.getSize(), layer.getDigest()); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 6dff150ad4..251f9c298d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -116,7 +116,7 @@ private static ImmutableList makeList( private final @Nullable Authorization pullAuthorization; private final BlobExistenceChecker blobExistenceChecker; - ObtainBaseImageLayerStep( + private ObtainBaseImageLayerStep( BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Layer layer, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index bf447eca5b..a519e0ab0b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -257,6 +257,7 @@ private void extractTar() { executorService.submit( () -> new ExtractTarStep( + executorService, buildConfiguration, results.tarPath.get(), childProgressDispatcherFactory, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java index cdcc251f13..c5cdf42d1a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java @@ -19,17 +19,18 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; import com.google.common.io.Resources; +import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.concurrent.ExecutionException; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -71,10 +72,12 @@ public void setup() throws IOException { @Test public void testCall_validDocker() throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException, CacheCorruptedException { + BadContainerConfigurationFormatException, IOException, ExecutionException, + InterruptedException { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = new ExtractTarStep( + MoreExecutors.newDirectExecutorService(), buildConfiguration, dockerBuild, progressEventDispatcherFactory, @@ -101,10 +104,12 @@ public void testCall_validDocker() @Test public void testCall_validTar() throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException, CacheCorruptedException { + BadContainerConfigurationFormatException, IOException, ExecutionException, + InterruptedException { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = new ExtractTarStep( + MoreExecutors.newDirectExecutorService(), buildConfiguration, tarBuild, progressEventDispatcherFactory, diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 82d0198fe7..48ae0e766e 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ffd5185d58..a518ca19ba 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) From 3f3be49c32a8d09ba6c1bd5f40c049441a3c88a3 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 26 Sep 2019 14:58:21 -0400 Subject: [PATCH 0754/2020] Survey in README.md (/, maven, gradle, faq) (#2020) --- README.md | 5 +++++ docs/faq.md | 4 ++++ jib-gradle-plugin/README.md | 4 ++++ jib-maven-plugin/README.md | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/README.md b/README.md index e33b0df894..bbef7ff2b8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Jib - Containerize your Java applications. + +| â˜‘ï¸ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + ## What is Jib? Jib builds optimized Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications without a Docker daemon - and without deep mastery of Docker best-practices. It is available as plugins for [Maven](jib-maven-plugin) and [Gradle](jib-gradle-plugin) and as a Java library. diff --git a/docs/faq.md b/docs/faq.md index 7a12a4ab4d..e2dffca2a7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,6 +2,10 @@ If a question you have is not answered below, please [submit an issue](/../../issues/new). +| â˜‘ï¸ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + [But, I'm not a Java developer.](#but-im-not-a-java-developer)\ [How do I run the image I built?](#how-do-i-run-the-image-i-built)\ [Where is bash?](#where-is-bash)\ diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 35411fd54b..ca33f5f1d2 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -10,6 +10,10 @@ For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). For information about the project, see the [Jib project README](../README.md). +| â˜‘ï¸ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + ## Table of Contents * [Upcoming Features](#upcoming-features) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 3f393bbb92..e45154f6d7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -10,6 +10,10 @@ For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin) For information about the project, see the [Jib project README](../README.md). +| â˜‘ï¸ Jib User Survey | +| :----- | +| What do you like best about Jib? What needs to be improved? Please tell us by taking a [one-minute survey](https://forms.gle/YRFeamGj51xmgnx28). Your responses will help us understand Jib usage and allow us to serve our customers (you!) better. | + ## Table of Contents * [Upcoming Features](#upcoming-features) From 69877c5e7d55f60d77d5d2d7828039dbe85d6f40 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 27 Sep 2019 14:24:30 -0400 Subject: [PATCH 0755/2020] Add dockerClient to top level of extended usage in READMEs (#2026) --- jib-gradle-plugin/README.md | 1 + jib-maven-plugin/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index ca33f5f1d2..584233e04e 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -180,6 +180,7 @@ Field | Type | Default | Description `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. `extraDirectories` | [`extraDirectories`](#extradirectories-closure) | See [`extraDirectories`](#extradirectories-closure) | Configures the directories used to add arbitrary files to the image. +`dockerClient` | [`dockerClient`](#dockerclient-closure) | See [`dockerClient`](#dockerclient-closure) | Configures Docker for building to/from the Docker daemon. `containerizingMode` | `String` | `exploded` | If set to `packaged`, puts the JAR artifact built by the Gradle Java plugin into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index e45154f6d7..573b2b5dc7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -225,6 +225,7 @@ Field | Type | Default | Description `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. `extraDirectories` | [`extraDirectories`](#extradirectories-object) | See [`extraDirectories`](#extradirectories-object) | Configures the directories used to add arbitrary files to the image. +`dockerClient` | [`dockerClient`](#dockerclient-object) | See [`dockerClient`](#dockerclient-object) | Configures Docker for building to/from the Docker daemon. `containerizingMode` | string | `exploded` | If set to `packaged`, puts the JAR artifact built at `${project.build.directory}/${project.build.finalName}.jar` (the default location where many JAR-buidiling plugins put a JAR registered as a main artifact, such as the Maven JAR Plugin) into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. `skip` | boolean | `false` | If set to true, Jib execution is skipped (useful for multi-module projects). This can also be specified via the `-Djib.skip` command line option. From 491556155169d2656913dc37b3e79093ad11efe0 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 27 Sep 2019 16:39:55 -0400 Subject: [PATCH 0756/2020] Update examples/java-agent (#2023) * update to extraDirectories * use $PORT if provided for cloud-run * plugin versions --- examples/java-agent/build.gradle | 10 +++++----- examples/java-agent/pom.xml | 12 +++++++----- .../java-agent/src/main/java/example/HelloWorld.java | 9 ++++++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 5a49a8c6d6..7ec6d4f3f8 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java' id 'com.google.cloud.tools.jib' version '1.6.1' - id 'de.undercouch.download' version '3.4.0' - id "com.gorylenko.gradle-git-properties" version "1.5.2" + id 'de.undercouch.download' version '4.0.0' + id "com.gorylenko.gradle-git-properties" version "2.2.0" } ext { @@ -24,8 +24,8 @@ repositories { } dependencies { - compile "com.sparkjava:spark-core:2.7.2" - compile "org.slf4j:slf4j-simple:1.7.21" + compile "com.sparkjava:spark-core:2.9.1" + compile "org.slf4j:slf4j-simple:1.7.28" } // Download and extract the Cloud Debugger Java Agent @@ -42,7 +42,7 @@ jib { to { image = 'gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib' } - extraDirectory = file(jibExtraDirectory) + extraDirectories.paths = [file(jibExtraDirectory)] container { ports = ['4567'] jvmFlags = [ diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index a4b09efe92..a59f351098 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -11,8 +11,8 @@ UTF-8 1.6.1 3.8.0 - 1.4.1 - 2.2.5 + 1.4.2 + 3.0.1 ${project.build.directory}/jib-agents @@ -28,12 +28,12 @@ com.sparkjava spark-core - 2.7.2 + 2.9.1 org.slf4j slf4j-simple - 1.7.21 + 1.7.28 @@ -77,7 +77,9 @@ gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib - ${agent-extraction-root} + + ${agent-extraction-root} + 4567 diff --git a/examples/java-agent/src/main/java/example/HelloWorld.java b/examples/java-agent/src/main/java/example/HelloWorld.java index 8cf08fec54..fdc61eb94f 100644 --- a/examples/java-agent/src/main/java/example/HelloWorld.java +++ b/examples/java-agent/src/main/java/example/HelloWorld.java @@ -17,9 +17,16 @@ package example; import static spark.Spark.get; +import static spark.Spark.port; public class HelloWorld { public static void main(String[] args) { - get("/hello", (req, res) -> "Hello World"); + // Allow use with Cloud Run which requires listening on the value in PORT + String portEnv = System.getenv("PORT"); + if (portEnv != null) { + port(Integer.parseInt(portEnv)); + } + + get("/", (req, res) -> "Hello World"); } } From 92f1fad5b583e0bf3da7e201cfe7114f6047e4de Mon Sep 17 00:00:00 2001 From: Matthew Hitchens Date: Mon, 30 Sep 2019 13:58:50 -0500 Subject: [PATCH 0757/2020] Upgrade commons-compress to 1.19 (#2030) There is a denial of service vulnerability in commons-compress 1.15 through 1.18. Apache recommends upgrading to 1.19 to mitigate. For reference, see here: https://lists.apache.org/thread.html/308cc15f1f1dc53e97046fddbac240e6cd16de89a2746cf257be7f5b@%3Cdev.commons.apache.org%3E --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f542242094..36bdb5e4b7 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ subprojects { GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.16.2', GUAVA: '28.0-jre', - COMMONS_COMPRESS: '1.18', + COMMONS_COMPRESS: '1.19', JACKSON_DATABIND: '2.9.9.2', ASM: '7.1', From c1ceb27f16f221d90810ff00ef282f30e8206493 Mon Sep 17 00:00:00 2001 From: Matthew Hitchens Date: Mon, 30 Sep 2019 16:18:14 -0500 Subject: [PATCH 0758/2020] Upgrade jackson-databind to 2.9.10 (#2032) jackson-databind 2.9.9.2 is vulnerable to several deserialization vulnerabilities. The latest stable version of 2.9 (2.9.10) mitigates these. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 36bdb5e4b7..559043559f 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ subprojects { GUAVA: '28.0-jre', COMMONS_COMPRESS: '1.19', - JACKSON_DATABIND: '2.9.9.2', + JACKSON_DATABIND: '2.9.10', ASM: '7.1', //test From e3de1a40c55478076d8b27c00571a3c44faf75e9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 1 Oct 2019 13:33:06 -0400 Subject: [PATCH 0759/2020] Proposal RFC: custom output file locations (#2024) --- proposals/output_files.md | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 proposals/output_files.md diff --git a/proposals/output_files.md b/proposals/output_files.md new file mode 100644 index 0000000000..d709e56327 --- /dev/null +++ b/proposals/output_files.md @@ -0,0 +1,52 @@ +# Proposal: Image ID, Digest, and Tar File Location Configuration + +Relevant issues: [#1561](https://github.com/GoogleContainerTools/jib/issues/1561), [#1921](https://github.com/GoogleContainerTools/jib/pull/1921) + +## Motivation + +Currently Jib hardcodes the location of the files it generates during the build, such as the image ID, digest, +and tar file (for tar builds). For some users, it would be useful to allow configuring the location of these +output files in a way that best fits their workflows. + +## Current Configuration + +Jib currently doesn't allow configuring these locations, and instead it uses hardcoded defaults: + +- Image tar -> `${buildDir}/jib-image.tar` +- Image ID -> `${buildDir}/jib-image.id` +- Image digest -> `${buildDir}/jib-image.digest` + +## Proposed Configuration + +The proposal is to allow users to configure their build with the following rules: +1. Extensions to the filename (like id, digest, tar) will not be automatically appended. +1. Existing files at the specified locations will be overwritten if necessary. +1. Running `clean` will not delete output files created outside of the project's build directory. +1. The configuration will accept both absolute and relative paths. Relative paths are resolved in the build tool's default manner. + +#### Maven (`pom.xml`) +```xml + + + /absolute/location.tar + relative/path/digest + ${project.build.directory}/id + + +``` + +#### Gradle (`build.gradle`) +```groovy +jib { + outputPaths { + tar = "/absolute/location.tar" + digest = file("relative/path/digest") + imageId = file("$buildDir/id") + } +} +``` + +Corresponding system properties will also be added so the outputs can be set via commandline: +* `-Djib.outputPaths.tar` +* `-Djib.outputPaths.digest` +* `-Djib.outputPaths.imageId` \ No newline at end of file From 012b6e944cde3e0c16fbb41b47db003cdbd6cd65 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 2 Oct 2019 13:25:05 -0400 Subject: [PATCH 0760/2020] Don't pass redundant parameter (#2035) * Don't pass redundant parameter * Fix descrepancy in tests * Remove unused constant --- .../tools/jib/registry/RegistryClient.java | 7 --- .../jib/registry/RegistryEndpointCaller.java | 11 +---- .../jib/registry/RegistryClientTest.java | 10 ---- .../registry/RegistryEndpointCallerTest.java | 46 +++++++++---------- 4 files changed, 25 insertions(+), 49 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 71d9c76d19..493975f68a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -443,12 +443,6 @@ static boolean canAttemptBlobMount(@Nullable Authorization authorization, String return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull"); } - /** @return the registry endpoint's API root, without the protocol */ - @VisibleForTesting - String getApiRouteBase() { - return registryEndpointRequestProperties.getServerUrl() + "/v2/"; - } - @VisibleForTesting String getUserAgent() { return userAgent; @@ -466,7 +460,6 @@ private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointP return new RegistryEndpointCaller<>( eventHandlers, userAgent, - getApiRouteBase(), registryEndpointProvider, authorization, registryEndpointRequestProperties, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 64aa2127c5..0456b59f22 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -56,8 +56,6 @@ class RegistryEndpointCaller { */ @VisibleForTesting static final int STATUS_CODE_PERMANENT_REDIRECT = 308; - private static final String DEFAULT_PROTOCOL = "https"; - private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); } @@ -81,7 +79,6 @@ static boolean isBrokenPipe(IOException original) { } private final EventHandlers eventHandlers; - private final URL initialRequestUrl; private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @Nullable private final Authorization authorization; @@ -99,7 +96,6 @@ static boolean isBrokenPipe(IOException original) { * * @param eventHandlers the event dispatcher used for dispatching log events * @param userAgent {@code User-Agent} header to send with the request - * @param apiRouteBase the endpoint's API root, without the protocol * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request @@ -109,7 +105,6 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointCaller( EventHandlers eventHandlers, String userAgent, - String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, @@ -118,7 +113,6 @@ static boolean isBrokenPipe(IOException original) { this( eventHandlers, userAgent, - apiRouteBase, registryEndpointProvider, authorization, registryEndpointRequestProperties, @@ -131,7 +125,6 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointCaller( EventHandlers eventHandlers, String userAgent, - String apiRouteBase, RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, @@ -140,8 +133,6 @@ static boolean isBrokenPipe(IOException original) { @Nullable Function insecureConnectionFactory) throws MalformedURLException { this.eventHandlers = eventHandlers; - this.initialRequestUrl = - registryEndpointProvider.getApiRoute(DEFAULT_PROTOCOL + "://" + apiRouteBase); this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; this.authorization = authorization; @@ -160,6 +151,8 @@ static boolean isBrokenPipe(IOException original) { */ T call() throws IOException, RegistryException { try { + String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; + URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); return callWithAllowInsecureRegistryHandling(initialRequestUrl); } catch (IOException ex) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 0775f22e68..041a0a5dbf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -89,14 +89,4 @@ public void testGetUserAgentWithUpstreamClient() { Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); Assert.assertTrue(registryClient.getUserAgent().endsWith(" skaffold/0.34.0")); } - - @Test - public void testGetApiRouteBase() { - Assert.assertEquals( - "some.server.url/v2/", - testRegistryClientFactory - .setAllowInsecureRegistries(true) - .newRegistryClient() - .getApiRouteBase()); - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 47779ab5a3..7ae726e9df 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -73,7 +73,7 @@ public String getHttpMethod() { @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { - return new URL(apiRouteBase + "/api"); + return new URL(apiRouteBase + "api"); } @Nullable @@ -146,7 +146,7 @@ public void testCall_secureCallerOnUnverifiableServer() throws IOException, Regi Assert.fail("Secure caller should fail if cannot verify server"); } catch (InsecureRegistryException ex) { Assert.assertEquals( - "Failed to verify the server at https://apiRouteBase/api because only secure connections are allowed.", + "Failed to verify the server at https://serverUrl/v2/api because only secure connections are allowed.", ex.getMessage()); } } @@ -163,10 +163,10 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -174,7 +174,7 @@ public void testCall_insecureCallerOnUnverifiableServer() throws IOException, Re Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); + "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); } @Test @@ -190,11 +190,11 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(2)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(2)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -202,11 +202,11 @@ public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryEx Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); + "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); + "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); } @Test @@ -221,8 +221,8 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -230,7 +230,7 @@ public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); + "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); } @Test @@ -248,7 +248,7 @@ public void testCall_secureCallerOnNonListeningServerAndNoPortSpecified() ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -271,7 +271,7 @@ public void testCall_insecureCallerOnNonListeningServerAndPortSpecified() ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase:5000/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("https://serverUrl:5000/v2/api"), urlCaptor.getAllValues().get(0)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -360,12 +360,12 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory, Mockito.times(3)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://apiRouteBase/api"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); Assert.assertEquals(new URL("http://newlocation"), urlCaptor.getAllValues().get(2)); Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://apiRouteBase/api"), urlCaptor.getAllValues().get(3)); + Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(3)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); @@ -373,11 +373,11 @@ public void testCall_credentialsForcedOverHttp() throws IOException, RegistryExc Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Cannot verify server at https://apiRouteBase/api. Attempting again with no TLS verification.")); + "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); Mockito.verify(mockEventHandlers) .dispatch( LogEvent.info( - "Failed to connect to https://apiRouteBase/api over HTTPS. Attempting again with HTTP: http://apiRouteBase/api")); + "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); } @Test @@ -693,7 +693,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(URL.class); Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlArgumentCaptor.capture()); Assert.assertEquals( - new URL("https://apiRouteBase/api"), urlArgumentCaptor.getAllValues().get(0)); + new URL("https://serverUrl/v2/api"), urlArgumentCaptor.getAllValues().get(0)); Assert.assertEquals(new URL("https://newlocation"), urlArgumentCaptor.getAllValues().get(1)); Mockito.verifyNoMoreInteractions(mockConnectionFactory); @@ -705,10 +705,10 @@ private RegistryEndpointCaller createRegistryEndpointCaller( return new RegistryEndpointCaller<>( mockEventHandlers, "userAgent", - (port == -1) ? "apiRouteBase" : ("apiRouteBase:" + port), new TestRegistryEndpointProvider(), Authorization.fromBasicToken("token"), - new RegistryEndpointRequestProperties("serverUrl", "imageName"), + new RegistryEndpointRequestProperties( + (port == -1 ? "serverUrl" : "serverUrl:" + port), "imageName"), allowInsecure, mockConnectionFactory, mockInsecureConnectionFactory); From 7a60abb9bf57dedda32bf72f0cac23c8247faaff Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 2 Oct 2019 16:30:22 -0400 Subject: [PATCH 0761/2020] Add configuration for custom output file locations (#2025) --- .../jib/builder/steps/WriteTarFileStep.java | 4 +- jib-gradle-plugin/CHANGELOG.md | 5 + .../cloud/tools/jib/gradle/JibRunHelper.java | 2 +- .../gradle/SingleProjectIntegrationTest.java | 59 ++++++++++-- .../gradle/projects/simple/build.gradle | 3 + .../projects/simple/complex-build.gradle | 4 + .../cloud/tools/jib/gradle/BuildTarTask.java | 11 +-- .../jib/gradle/DockerClientParameters.java | 4 +- .../jib/gradle/GradleRawConfiguration.java | 15 +++ .../cloud/tools/jib/gradle/JibExtension.java | 17 ++++ .../jib/gradle/OutputPathsParameters.java | 91 +++++++++++++++++++ .../gradle/GradleRawConfigurationTest.java | 9 ++ .../tools/jib/gradle/JibExtensionTest.java | 45 +++++++++ jib-maven-plugin/CHANGELOG.md | 5 + .../maven/BuildDockerMojoIntegrationTest.java | 2 +- .../maven/BuildImageMojoIntegrationTest.java | 34 +++---- .../maven/BuildTarMojoIntegrationTest.java | 5 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 3 +- .../jib/maven/JibPluginConfiguration.java | 41 +++++++++ .../jib/maven/MavenRawConfiguration.java | 15 +++ .../jib/maven/JibPluginConfigurationTest.java | 35 ++++++- .../jib/maven/MavenRawConfigurationTest.java | 6 ++ .../maven/projects/simple/pom-complex.xml | 4 + .../resources/maven/projects/simple/pom.xml | 3 + .../common/PluginConfigurationProcessor.java | 18 ++-- .../jib/plugins/common/PropertyNames.java | 3 + .../jib/plugins/common/RawConfiguration.java | 6 ++ 27 files changed, 395 insertions(+), 54 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 5d1681ed80..4dd97f91f4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -58,7 +58,9 @@ public BuildResult call() throws ExecutionException, InterruptedException, IOExc try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("writing to tar file", 1)) { // Builds the image to a tarball. - Files.createDirectories(outputPath.getParent()); + if (outputPath.getParent() != null) { + Files.createDirectories(outputPath.getParent()); + } try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { new ImageTarball(builtImage, buildConfiguration.getTargetImageConfiguration().getImage()) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 48ae0e766e..9da018e57c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. ### Added +- `jib.outputPaths` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) + - `jib.outputPaths.tar` configures output path of `jibBuildTar` (`build/jib-image.tar` by default) + - `jib.outputPaths.digest` configures the output path of the image digest (`build/jib-image.digest` by default) + - `jib.outputPaths.imageId` configures output path of the image id (`build/jib-image.id` by default) + ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 2bb66ec39b..9ad410e839 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -196,7 +196,7 @@ static void assertImageDigestAndId(Path projectRoot) throws IOException, DigestE * @throws IOException if an I/O exception occurs * @throws InterruptedException if the process was interrupted */ - private static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) + static String pullAndRunBuiltImage(String imageReference, String... extraRunArguments) throws IOException, InterruptedException { new Command("docker", "pull", imageReference).run(); String history = new Command("docker", "history", imageReference).run(); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 274a9ab92a..bddab8eedb 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -18,9 +18,13 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +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.registry.LocalRegistry; import com.google.common.base.Splitter; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.DigestException; @@ -117,6 +121,12 @@ private static void assertDockerInspect(String imageReference) + " }")); } + static String readDigestFile(Path digestPath) throws IOException, DigestException { + Assert.assertTrue(Files.exists(digestPath)); + String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); + return DescriptorDigest.fromDigest(digest).toString(); + } + private static void assertExtraDirectoryDeprecationWarning(String buildFile) throws DigestException, IOException, InterruptedException { String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); @@ -134,7 +144,7 @@ private static void assertExtraDirectoryDeprecationWarning(String buildFile) + "'jib.extraDirectories.permissions'")); } - private static void buildAndRunComplex( + private static String buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) throws IOException, InterruptedException { Path baseCache = simpleTestProject.getProjectRoot().resolve("build/jib-base-cache"); @@ -157,11 +167,14 @@ private static void buildAndRunComplex( assertDockerInspect(imageReference); String history = new Command("docker", "history", imageReference).run(); Assert.assertThat(history, CoreMatchers.containsString("jib-gradle-plugin")); + + String output = new Command("docker", "run", "--rm", imageReference).run(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - new Command("docker", "run", "--rm", imageReference).run()); + output); + return output; } @Before @@ -171,7 +184,8 @@ public void setup() throws IOException, InterruptedException { } @Test - public void testBuild_simple() throws IOException, InterruptedException, DigestException { + public void testBuild_simple() + throws IOException, InterruptedException, DigestException, InvalidImageReferenceException { String targetImage = IntegrationTestingConfiguration.getTestRepositoryLocation() + "/simpleimage:gradle" @@ -195,10 +209,22 @@ public void testBuild_simple() throws IOException, InterruptedException, DigestE "No classes files were found - did you compile your project?")); } + String output = JibRunHelper.buildAndRun(simpleTestProject, targetImage); + Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", - JibRunHelper.buildAndRun(simpleTestProject, targetImage)); + output); + + String digest = + readDigestFile(simpleTestProject.getProjectRoot().resolve("build/jib-image.digest")); + String imageReferenceWithDigest = ImageReference.parse(targetImage).withTag(digest).toString(); + Assert.assertEquals(output, JibRunHelper.pullAndRunBuiltImage(imageReferenceWithDigest)); + + String id = readDigestFile(simpleTestProject.getProjectRoot().resolve("build/jib-image.id")); + Assert.assertNotEquals(digest, id); + Assert.assertEquals(output, new Command("docker", "run", "--rm", id).run()); + assertDockerInspect(targetImage); JibRunHelper.assertSimpleCreationTimeIsEqual(Instant.EPOCH, targetImage); assertWorkingDirectory("/home", targetImage); @@ -335,10 +361,25 @@ public void testDockerDaemon_simple_multipleExtraDirectoriesWithAlternativeConfi } @Test - public void testBuild_complex() throws IOException, InterruptedException { + public void testBuild_complex() + throws IOException, InterruptedException, DigestException, InvalidImageReferenceException { String targetImage = "localhost:6000/compleximage:gradle" + System.nanoTime(); Instant beforeBuild = Instant.now(); - buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); + String output = buildAndRunComplex(targetImage, "testuser2", "testpassword2", localRegistry2); + + String digest = + readDigestFile( + simpleTestProject.getProjectRoot().resolve("build/different-jib-image.digest")); + String imageReferenceWithDigest = ImageReference.parse(targetImage).withTag(digest).toString(); + localRegistry2.pull(imageReferenceWithDigest); + Assert.assertEquals( + output, new Command("docker", "run", "--rm", imageReferenceWithDigest).run()); + + String id = + readDigestFile(simpleTestProject.getProjectRoot().resolve("different-jib-image.id")); + Assert.assertNotEquals(digest, id); + Assert.assertEquals(output, new Command("docker", "run", "--rm", id).run()); + JibRunHelper.assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); assertWorkingDirectory("", targetImage); } @@ -464,7 +505,11 @@ public void testBuildTar_simple() throws IOException, InterruptedException { String targetImage = "simpleimage:gradle" + System.nanoTime(); String outputPath = - simpleTestProject.getProjectRoot().resolve("build").resolve("jib-image.tar").toString(); + simpleTestProject + .getProjectRoot() + .resolve("build") + .resolve("different-jib-image.tar") + .toString(); BuildResult buildResult = simpleTestProject.build( "clean", diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle index 4c9914c010..5da82dbf88 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle @@ -28,5 +28,8 @@ jib { workingDirectory = '/home' extraClasspath = ['/d1','/d2'] } + outputPaths { + tar = file("$buildDir/different-jib-image.tar") + } extraDirectories.paths = file('src/main/custom-extra-dir') } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle index 4d7ad8c00c..715e294ca9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle @@ -43,5 +43,9 @@ jib { paths = file('src/main/custom-extra-dir') permissions = ['/foo':'755', '/bar/cat':'777'] } + outputPaths { + digest = file("$buildDir/different-jib-image.digest") + imageId = file("different-jib-image.id") + } allowInsecureRegistries = true } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index e9a8b5452b..0943bee943 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -91,7 +91,7 @@ public FileCollection getInputFiles() { */ @OutputFile public String getOutputFile() { - return getTargetPath().toString(); + return Preconditions.checkNotNull(jibExtension).getOutputPaths().getTarPath().toString(); } @TaskAction @@ -166,13 +166,4 @@ public BuildTarTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } - - /** - * Returns the output directory for the tarball. By default, it is {@code build/jib-image.tar}. - * - * @return the output directory - */ - private Path getTargetPath() { - return getProject().getBuildDir().toPath().resolve("jib-image.tar"); - } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java index c92892f219..90af55c2c0 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java @@ -28,8 +28,8 @@ import org.gradle.api.tasks.Optional; /** - * Object in {@link BuildDockerTask} that configures the Docker executable and the additional - * environment variables to use when executing the executable. + * Object that configures the Docker executable and the additional environment variables to use when + * executing the executable. */ public class DockerClientParameters { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 6495cf200b..b741e21152 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -185,4 +185,19 @@ public Map getDockerEnvironment() { public String getContainerizingMode() { return jibExtension.getContainerizingMode(); } + + @Override + public Path getTarOutputPath() { + return jibExtension.getOutputPaths().getTarPath(); + } + + @Override + public Path getDigestOutputPath() { + return jibExtension.getOutputPaths().getDigestPath(); + } + + @Override + public Path getImageIdOutputPath() { + return jibExtension.getOutputPaths().getImageIdPath(); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index aa3676dac6..8030f5b73d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -56,6 +56,11 @@ * '/path/on/container/file2': 123 * ] * } + * outputPaths { + * tar = file('reative/to/project/root/jib-image.tar') + * digest = file('/absolute/path/jib-image.digest') + * imageId = file("$buildDir/jib-image.id") + * } * allowInsecureRegistries = false * containerizingMode = 'exploded' * } @@ -72,6 +77,7 @@ public class JibExtension { private final ContainerParameters container; private final ExtraDirectoriesParameters extraDirectories; private final DockerClientParameters dockerClient; + private final OutputPathsParameters outputPaths; private final Property allowInsecureRegistries; private final Property containerizingMode; @@ -86,6 +92,7 @@ public JibExtension(Project project) { container = objectFactory.newInstance(ContainerParameters.class); extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project, this); dockerClient = objectFactory.newInstance(DockerClientParameters.class); + outputPaths = objectFactory.newInstance(OutputPathsParameters.class, project); allowInsecureRegistries = objectFactory.property(Boolean.class); containerizingMode = objectFactory.property(String.class); @@ -122,6 +129,10 @@ public void dockerClient(Action action) { action.execute(dockerClient); } + public void outputPaths(Action action) { + action.execute(outputPaths); + } + @Deprecated // for the deprecated "jib.extraDirectory" config parameter public void setExtraDirectory(File extraDirectory) { @@ -174,6 +185,12 @@ public DockerClientParameters getDockerClient() { return dockerClient; } + @Nested + @Optional + public OutputPathsParameters getOutputPaths() { + return outputPaths; + } + @Input @Optional boolean getAllowInsecureRegistries() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java new file mode 100644 index 0000000000..34f862ca5d --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.inject.Inject; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; + +/** Object that configures where Jib should create its build output files. */ +public class OutputPathsParameters { + + private final Project project; + + private Path digest; + private Path tar; + private Path imageId; + + @Inject + public OutputPathsParameters(Project project) { + this.project = project; + digest = project.getBuildDir().toPath().resolve("jib-image.digest"); + imageId = project.getBuildDir().toPath().resolve("jib-image.id"); + tar = project.getBuildDir().toPath().resolve("jib-image.tar"); + } + + @Input + public String getDigest() { + return getRelativeToProjectRoot(digest, PropertyNames.OUTPUT_PATHS_DIGEST).toString(); + } + + @Internal + Path getDigestPath() { + return getRelativeToProjectRoot(digest, PropertyNames.OUTPUT_PATHS_DIGEST); + } + + public void setDigest(String digest) { + this.digest = Paths.get(digest); + } + + @Input + public String getImageId() { + return getRelativeToProjectRoot(imageId, PropertyNames.OUTPUT_PATHS_IMAGE_ID).toString(); + } + + @Internal + Path getImageIdPath() { + return getRelativeToProjectRoot(imageId, PropertyNames.OUTPUT_PATHS_IMAGE_ID); + } + + public void setImageId(String id) { + this.imageId = Paths.get(id); + } + + @Input + public String getTar() { + return getRelativeToProjectRoot(tar, PropertyNames.OUTPUT_PATHS_TAR).toString(); + } + + @Internal + Path getTarPath() { + return getRelativeToProjectRoot(tar, PropertyNames.OUTPUT_PATHS_TAR); + } + + public void setTar(String tar) { + this.tar = Paths.get(tar); + } + + private Path getRelativeToProjectRoot(Path configuration, String propertyName) { + String property = System.getProperty(propertyName); + Path path = property != null ? Paths.get(property) : configuration; + return path.isAbsolute() ? path : project.getProjectDir().toPath().resolve(path); + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index 8606f509b4..8a0d16e2f7 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -39,6 +39,7 @@ public void testGetters() { TargetImageParameters targetImageParameters = Mockito.mock(TargetImageParameters.class); ContainerParameters containerParameters = Mockito.mock(ContainerParameters.class); DockerClientParameters dockerClientParameters = Mockito.mock(DockerClientParameters.class); + OutputPathsParameters outputPathsParameters = Mockito.mock(OutputPathsParameters.class); Mockito.when(authParameters.getUsername()).thenReturn("user"); Mockito.when(authParameters.getPassword()).thenReturn("password"); @@ -50,6 +51,7 @@ public void testGetters() { Mockito.when(jibExtension.getTo()).thenReturn(targetImageParameters); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); Mockito.when(jibExtension.getDockerClient()).thenReturn(dockerClientParameters); + Mockito.when(jibExtension.getOutputPaths()).thenReturn(outputPathsParameters); Mockito.when(jibExtension.getAllowInsecureRegistries()).thenReturn(true); Mockito.when(baseImageParameters.getCredHelper()).thenReturn("gcr"); @@ -77,6 +79,10 @@ public void testGetters() { Mockito.when(dockerClientParameters.getEnvironment()) .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); + Mockito.when(outputPathsParameters.getDigestPath()).thenReturn(Paths.get("digest/path")); + Mockito.when(outputPathsParameters.getImageIdPath()).thenReturn(Paths.get("id/path")); + Mockito.when(outputPathsParameters.getTarPath()).thenReturn(Paths.get("tar/path")); + GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); AuthProperty fromAuth = rawConfiguration.getFromAuth(); @@ -109,5 +115,8 @@ public void testGetters() { Assert.assertEquals( new HashMap<>(ImmutableMap.of("docker", "client")), rawConfiguration.getDockerEnvironment()); + Assert.assertEquals(Paths.get("digest/path"), rawConfiguration.getDigestOutputPath()); + Assert.assertEquals(Paths.get("id/path"), rawConfiguration.getImageIdOutputPath()); + Assert.assertEquals(Paths.get("tar/path"), rawConfiguration.getTarOutputPath()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 069fdc48c2..bb1430e521 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -240,6 +240,26 @@ public void testDockerClient() { testJibExtension.getDockerClient().getEnvironment()); } + @Test + public void testOutputFiles() { + testJibExtension.outputPaths( + outputFiles -> { + outputFiles.setDigest("/path/to/digest"); + outputFiles.setImageId("/path/to/id"); + outputFiles.setTar("path/to/tar"); + }); + + Assert.assertEquals( + Paths.get("/path/to/digest").toAbsolutePath(), + testJibExtension.getOutputPaths().getDigestPath()); + Assert.assertEquals( + Paths.get("/path/to/id").toAbsolutePath(), + testJibExtension.getOutputPaths().getImageIdPath()); + Assert.assertEquals( + fakeProject.getProjectDir().toPath().resolve(Paths.get("path/to/tar")), + testJibExtension.getOutputPaths().getTarPath()); + } + @Test public void testProperties() { System.setProperty("jib.from.image", "fromImage"); @@ -311,6 +331,31 @@ public void testProperties() { Assert.assertEquals( ImmutableMap.of("env1", "val1", "env2", "val2"), testJibExtension.getDockerClient().getEnvironment()); + + // Absolute paths + System.setProperty("jib.outputPaths.digest", "/digest/path"); + Assert.assertEquals( + Paths.get("/digest/path").toAbsolutePath(), + testJibExtension.getOutputPaths().getDigestPath()); + System.setProperty("jib.outputPaths.imageId", "/id/path"); + Assert.assertEquals( + Paths.get("/id/path").toAbsolutePath(), testJibExtension.getOutputPaths().getImageIdPath()); + System.setProperty("jib.outputPaths.tar", "/tar/path"); + Assert.assertEquals( + Paths.get("/tar/path").toAbsolutePath(), testJibExtension.getOutputPaths().getTarPath()); + // Relative paths + System.setProperty("jib.outputPaths.digest", "digest/path"); + Assert.assertEquals( + fakeProject.getProjectDir().toPath().resolve(Paths.get("digest/path")), + testJibExtension.getOutputPaths().getDigestPath()); + System.setProperty("jib.outputPaths.imageId", "id/path"); + Assert.assertEquals( + fakeProject.getProjectDir().toPath().resolve(Paths.get("id/path")), + testJibExtension.getOutputPaths().getImageIdPath()); + System.setProperty("jib.outputPaths.tar", "tar/path"); + Assert.assertEquals( + fakeProject.getProjectDir().toPath().resolve(Paths.get("tar/path")), + testJibExtension.getOutputPaths().getTarPath()); } @Test diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a518ca19ba..dd8d8c90fe 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. ### Added +- `` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) + - `` configures output path of `jib:buildTar` (`target/jib-image.tar` by default) + - `` configures the output path of the image digest (`target/jib-image.digest` by default) + - `` configures output path of the image id (`target/jib-image.id` by default) + ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index f6aec49feb..15d4ea3992 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -51,7 +51,7 @@ private static void buildToDockerDaemon(Path projectRoot, String imageReference, verifier.executeGoal("jib:dockerBuild"); verifier.verifyErrorFreeLog(); - BuildImageMojoIntegrationTest.assertImageDigest(projectRoot); + BuildImageMojoIntegrationTest.readDigestFile(projectRoot.resolve("target/jib-image.digest")); } /** diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index fa16447c84..5615924487 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -76,20 +76,12 @@ private static String getTestImageReference(String label) { return nameBase + label + System.nanoTime(); } - static String assertImageDigest(Path projectRoot) throws IOException, DigestException { - Path digestPath = projectRoot.resolve("target/jib-image.digest"); - Assert.assertTrue(Files.exists(digestPath)); + static String readDigestFile(Path digestPath) throws IOException, DigestException { + Assert.assertTrue("File missing: " + digestPath, Files.exists(digestPath)); String digest = new String(Files.readAllBytes(digestPath), StandardCharsets.UTF_8); return DescriptorDigest.fromDigest(digest).toString(); } - static String assertImageId(Path projectRoot) throws IOException, DigestException { - Path idPath = projectRoot.resolve("target/jib-image.id"); - Assert.assertTrue(Files.exists(idPath)); - String id = new String(Files.readAllBytes(idPath), StandardCharsets.UTF_8); - return DescriptorDigest.fromDigest(id).toString(); - } - private static boolean isJava11RuntimeOrHigher() { Iterable split = Splitter.on(".").split(System.getProperty("java.version")); return Integer.valueOf(split.iterator().next()) >= 11; @@ -141,13 +133,13 @@ private static String buildAndRun( try { // Test pulling/running using image digest - String digest = assertImageDigest(projectRoot); + String digest = readDigestFile(projectRoot.resolve("target/jib-image.digest")); String imageReferenceWithDigest = ImageReference.parse(imageReference).withTag(digest).toString(); Assert.assertEquals(output, pullAndRunBuiltImage(imageReferenceWithDigest)); // Test running using image id - String id = assertImageId(projectRoot); + String id = readDigestFile(projectRoot.resolve("target/jib-image.id")); Assert.assertNotEquals(digest, id); Assert.assertEquals(output, new Command("docker", "run", "--rm", id).run()); @@ -210,7 +202,7 @@ private static String buildAndRunAdditionalTag( String additionalOutput = pullAndRunBuiltImage(additionalImageReference); Assert.assertEquals(output, additionalOutput); - String digest = assertImageDigest(projectRoot); + String digest = readDigestFile(projectRoot.resolve("target/jib-image.digest")); String digestImageReference = ImageReference.parse(imageReference).withTag(digest).toString(); String digestOutput = pullAndRunBuiltImage(digestImageReference); Assert.assertEquals(output, digestOutput); @@ -240,8 +232,6 @@ private static String buildAndRunComplex( verifier.executeGoals(Arrays.asList("clean", "compile", "jib:build")); verifier.verifyErrorFreeLog(); - assertImageDigest(simpleTestProject.getProjectRoot()); - // Verify output targetRegistry.pull(imageReference); assertDockerInspectParameters(imageReference); @@ -583,12 +573,22 @@ public void testExecute_complex() throws IOException, InterruptedException, VerificationException, DigestException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Instant before = Instant.now(); + String output = + buildAndRunComplex( + targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml"); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n" + "-Xms512m\n-Xdebug\nenvvalue1\nenvvalue2\n", - buildAndRunComplex( - targetImage, "testuser2", "testpassword2", localRegistry2, "pom-complex.xml")); + output); + String digest = + readDigestFile( + simpleTestProject.getProjectRoot().resolve("target/different-jib-image.digest")); + String id = + readDigestFile(simpleTestProject.getProjectRoot().resolve("different-jib-image.id")); + Assert.assertNotEquals(digest, id); + Assert.assertEquals(output, new Command("docker", "run", "--rm", id).run()); + assertCreationTimeIsAfter(before, targetImage); assertWorkingDirectory("", targetImage); assertEntrypoint( diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java index 5f13807080..761a3291dc 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildTarMojoIntegrationTest.java @@ -47,7 +47,8 @@ public void testExecute_simple() verifier.executeGoal("jib:" + BuildTarMojo.GOAL_NAME); verifier.verifyErrorFreeLog(); - BuildImageMojoIntegrationTest.assertImageDigest(simpleTestProject.getProjectRoot()); + BuildImageMojoIntegrationTest.readDigestFile( + simpleTestProject.getProjectRoot().resolve("target/jib-image.digest")); new Command( "docker", @@ -56,7 +57,7 @@ public void testExecute_simple() simpleTestProject .getProjectRoot() .resolve("target") - .resolve("jib-image.tar") + .resolve("different-jib-image.tar") .toString()) .run(); Assert.assertEquals( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 53130ce95d..f3b5636a25 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -38,7 +38,8 @@ import org.apache.maven.plugins.annotations.ResolutionScope; /** - * Builds a container image and exports to disk at {@code ${project.build.directory}/jib-image.tar}. + * Builds a container image and exports to disk at the configured location ({@code + * ${project.build.directory}/jib-image.tar} by default). */ @Mojo( name = BuildTarMojo.GOAL_NAME, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 569059ae1c..f0df5992c6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -233,6 +233,15 @@ public static class DockerClientParameters { @Parameter private Map environment = Collections.emptyMap(); } + public static class OutputPathsParameters { + + @Nullable @Parameter private File tar; + + @Nullable @Parameter private File digest; + + @Nullable @Parameter private File imageId; + } + @Nullable @Parameter(defaultValue = "${session}", readonly = true) private MavenSession session; @@ -260,6 +269,8 @@ public static class DockerClientParameters { @Parameter private DockerClientParameters dockerClient = new DockerClientParameters(); + @Parameter private OutputPathsParameters outputPaths = new OutputPathsParameters(); + @Parameter(property = PropertyNames.ALLOW_INSECURE_REGISTRIES) private boolean allowInsecureRegistries; @@ -684,6 +695,36 @@ Map getDockerClientEnvironment() { return dockerClient.environment; } + Path getTarOutputPath() { + Path configuredPath = + outputPaths.tar == null + ? Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.tar") + : outputPaths.tar.toPath(); + return getRelativeToProjectRoot(configuredPath, PropertyNames.OUTPUT_PATHS_TAR); + } + + Path getDigestOutputPath() { + Path configuredPath = + outputPaths.digest == null + ? Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.digest") + : outputPaths.digest.toPath(); + return getRelativeToProjectRoot(configuredPath, PropertyNames.OUTPUT_PATHS_DIGEST); + } + + Path getImageIdOutputPath() { + Path configuredPath = + outputPaths.imageId == null + ? Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.id") + : outputPaths.imageId.toPath(); + return getRelativeToProjectRoot(configuredPath, PropertyNames.OUTPUT_PATHS_IMAGE_ID); + } + + private Path getRelativeToProjectRoot(Path configuration, String propertyName) { + String property = getProperty(propertyName); + Path path = property != null ? Paths.get(property) : configuration; + return path.isAbsolute() ? path : getProject().getBasedir().toPath().resolve(path); + } + boolean getAllowInsecureRegistries() { return allowInsecureRegistries; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 7ced9766c0..fd2ef3c94c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -190,4 +190,19 @@ public Map getDockerEnvironment() { public String getContainerizingMode() { return jibPluginConfiguration.getContainerizingMode(); } + + @Override + public Path getTarOutputPath() { + return jibPluginConfiguration.getTarOutputPath(); + } + + @Override + public Path getDigestOutputPath() { + return jibPluginConfiguration.getDigestOutputPath(); + } + + @Override + public Path getImageIdOutputPath() { + return jibPluginConfiguration.getImageIdOutputPath(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 6c2a97154a..0a335c31d7 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Properties; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Build; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.junit.Assert; @@ -44,11 +45,13 @@ public class JibPluginConfigurationTest { private final Properties sessionProperties = new Properties(); @Mock private MavenSession session; @Mock private Log log; + @Mock private Build build; private JibPluginConfiguration testPluginConfiguration; @Before public void setup() { Mockito.when(session.getSystemProperties()).thenReturn(sessionProperties); + Mockito.when(build.getDirectory()).thenReturn("/test/directory"); testPluginConfiguration = new JibPluginConfiguration() { @Override @@ -59,6 +62,8 @@ public Log getLog() { return log; } }; + project.setBuild(build); + project.setFile(new File("/repository/project/pom.xml")); // sets baseDir testPluginConfiguration.setProject(project); testPluginConfiguration.setSession(session); } @@ -150,6 +155,25 @@ public void testSystemProperties() { Assert.assertEquals( ImmutableMap.of("env1", "val1", "env2", "val2"), testPluginConfiguration.getDockerClientEnvironment()); + + // Absolute paths + sessionProperties.put("jib.outputPaths.digest", "/digest/path"); + Assert.assertEquals(Paths.get("/digest/path"), testPluginConfiguration.getDigestOutputPath()); + sessionProperties.put("jib.outputPaths.imageId", "/id/path"); + Assert.assertEquals(Paths.get("/id/path"), testPluginConfiguration.getImageIdOutputPath()); + sessionProperties.put("jib.outputPaths.tar", "/tar/path"); + Assert.assertEquals(Paths.get("/tar/path"), testPluginConfiguration.getTarOutputPath()); + // Relative paths + sessionProperties.put("jib.outputPaths.digest", "digest/path"); + Assert.assertEquals( + Paths.get("/repository/project/digest/path"), + testPluginConfiguration.getDigestOutputPath()); + sessionProperties.put("jib.outputPaths.imageId", "id/path"); + Assert.assertEquals( + Paths.get("/repository/project/id/path"), testPluginConfiguration.getImageIdOutputPath()); + sessionProperties.put("jib.outputPaths.tar", "tar/path"); + Assert.assertEquals( + Paths.get("/repository/project/tar/path"), testPluginConfiguration.getTarOutputPath()); } @Test @@ -233,6 +257,14 @@ public void testPomProperties() { Assert.assertEquals( ImmutableMap.of("env1", "val1", "env2", "val2"), testPluginConfiguration.getDockerClientEnvironment()); + + project.getProperties().setProperty("jib.outputPaths.digest", "/digest/path"); + Assert.assertEquals(Paths.get("/digest/path"), testPluginConfiguration.getDigestOutputPath()); + project.getProperties().setProperty("jib.outputPaths.imageId", "/id/path"); + Assert.assertEquals(Paths.get("/id/path"), testPluginConfiguration.getImageIdOutputPath()); + project.getProperties().setProperty("jib.outputPaths.tar", "tar/path"); + Assert.assertEquals( + Paths.get("/repository/project/tar/path"), testPluginConfiguration.getTarOutputPath()); } @Test @@ -369,7 +401,6 @@ public void testIsContainerizable_noProperty() { public void testIsContainerizable_artifactId() { project.setGroupId("group"); project.setArtifactId("artifact"); - project.setFile(new File("/repository/project/pom.xml")); // sets baseDir Properties projectProperties = project.getProperties(); projectProperties.setProperty("jib.containerize", ":artifact"); @@ -383,7 +414,6 @@ public void testIsContainerizable_artifactId() { public void testIsContainerizable_groupAndArtifactId() { project.setGroupId("group"); project.setArtifactId("artifact"); - project.setFile(new File("/repository/project/pom.xml")); // sets baseDir Properties projectProperties = project.getProperties(); projectProperties.setProperty("jib.containerize", "group:artifact"); @@ -397,7 +427,6 @@ public void testIsContainerizable_groupAndArtifactId() { public void testIsContainerizable_directory() { project.setGroupId("group"); project.setArtifactId("artifact"); - project.setFile(new File("/repository/project/pom.xml")); // sets baseDir Properties projectProperties = project.getProperties(); projectProperties.setProperty("jib.containerize", "project"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index 1721bb6d13..c18cca1b29 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -82,6 +82,9 @@ public void testGetters() { Mockito.when(jibPluginConfiguration.getDockerClientExecutable()).thenReturn(Paths.get("test")); Mockito.when(jibPluginConfiguration.getDockerClientEnvironment()) .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); + Mockito.when(jibPluginConfiguration.getDigestOutputPath()).thenReturn(Paths.get("digest/path")); + Mockito.when(jibPluginConfiguration.getImageIdOutputPath()).thenReturn(Paths.get("id/path")); + Mockito.when(jibPluginConfiguration.getTarOutputPath()).thenReturn(Paths.get("tar/path")); MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); @@ -115,6 +118,9 @@ public void testGetters() { Assert.assertEquals( new HashMap<>(ImmutableMap.of("docker", "client")), rawConfiguration.getDockerEnvironment()); + Assert.assertEquals(Paths.get("digest/path"), jibPluginConfiguration.getDigestOutputPath()); + Assert.assertEquals(Paths.get("id/path"), jibPluginConfiguration.getImageIdOutputPath()); + Assert.assertEquals(Paths.get("tar/path"), jibPluginConfiguration.getTarOutputPath()); Mockito.verifyNoMoreInteractions(eventHandlers); } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml index 82610e9c8e..4853e833c4 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml @@ -95,6 +95,10 @@ + + ${project.build.directory}/different-jib-image.digest + different-jib-image.id + true diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml index 9847100fa3..96992bee67 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml @@ -60,6 +60,9 @@ /home ${project.basedir}/src/main/jib-custom + + ${project.build.directory}/different-jib-image.tar + diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 5ac3da5da5..cbb4d44dda 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -87,8 +87,8 @@ public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( helpfulSuggestions, targetImageReference, rawConfiguration.getToTags()) - .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) - .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); + .writeImageDigest(rawConfiguration.getDigestOutputPath()) + .writeImageId(rawConfiguration.getImageIdOutputPath()); } public static JibBuildRunner createJibBuildRunnerForTarImage( @@ -101,10 +101,10 @@ public static JibBuildRunner createJibBuildRunnerForTarImage( IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { - Path tarImagePath = projectProperties.getOutputDirectory().resolve("jib-image.tar"); ImageReference targetImageReference = getGeneratedTargetDockerTag(rawConfiguration, projectProperties, helpfulSuggestions); - TarImage targetImage = TarImage.at(tarImagePath).named(targetImageReference); + TarImage targetImage = + TarImage.at(rawConfiguration.getTarOutputPath()).named(targetImageReference); Containerizer containerizer = Containerizer.to(targetImage); JibContainerBuilder jibContainerBuilder = @@ -116,9 +116,9 @@ public static JibBuildRunner createJibBuildRunnerForTarImage( containerizer, projectProperties::log, helpfulSuggestions, - tarImagePath) - .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) - .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); + rawConfiguration.getTarOutputPath()) + .writeImageDigest(rawConfiguration.getDigestOutputPath()) + .writeImageId(rawConfiguration.getImageIdOutputPath()); } public static JibBuildRunner createJibBuildRunnerForRegistryImage( @@ -162,8 +162,8 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( helpfulSuggestions, targetImageReference, rawConfiguration.getToTags()) - .writeImageDigest(projectProperties.getOutputDirectory().resolve("jib-image.digest")) - .writeImageId(projectProperties.getOutputDirectory().resolve("jib-image.id")); + .writeImageDigest(rawConfiguration.getDigestOutputPath()) + .writeImageId(rawConfiguration.getImageIdOutputPath()); } @VisibleForTesting diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 132ed6a0ec..9b125bd4f1 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -53,6 +53,9 @@ public class PropertyNames { public static final String EXTRA_DIRECTORIES_PERMISSIONS = "jib.extraDirectories.permissions"; public static final String DOCKER_CLIENT_EXECUTABLE = "jib.dockerClient.executable"; public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; + public static final String OUTPUT_PATHS_DIGEST = "jib.outputPaths.digest"; + public static final String OUTPUT_PATHS_IMAGE_ID = "jib.outputPaths.imageId"; + public static final String OUTPUT_PATHS_TAR = "jib.outputPaths.tar"; public static final String CONTAINERIZING_MODE = "jib.containerizingMode"; public static final String SKIP = "jib.skip"; public static final String CONSOLE = "jib.console"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 0ea66e8ce8..804f1477d6 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -91,4 +91,10 @@ public interface RawConfiguration { Map getDockerEnvironment(); String getContainerizingMode(); + + Path getTarOutputPath(); + + Path getDigestOutputPath(); + + Path getImageIdOutputPath(); } From 50eb5fe8a9710ba1a74758a66379a8025f50e6fc Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 3 Oct 2019 15:29:00 -0400 Subject: [PATCH 0762/2020] Upgrade ASM to 7.2 (#2036) --- build.gradle | 2 +- jib-core/CHANGELOG.md | 2 ++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 559043559f..2f1410b744 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ subprojects { COMMONS_COMPRESS: '1.19', JACKSON_DATABIND: '2.9.10', - ASM: '7.1', + ASM: '7.2', //test JUNIT: '4.12', diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 0cb95d39b6..729eb32632 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) + ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 9da018e57c..7bb7f5970f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `jib.outputPaths.tar` configures output path of `jibBuildTar` (`build/jib-image.tar` by default) - `jib.outputPaths.digest` configures the output path of the image digest (`build/jib-image.digest` by default) - `jib.outputPaths.imageId` configures output path of the image id (`build/jib-image.id` by default) +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index dd8d8c90fe..84d2df8384 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - `` configures output path of `jib:buildTar` (`target/jib-image.tar` by default) - `` configures the output path of the image digest (`target/jib-image.digest` by default) - `` configures output path of the image id (`target/jib-image.id` by default) +- Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) ### Changed From c116d7e6ca23da9c3222cbb92a734a16dfd0a564 Mon Sep 17 00:00:00 2001 From: sullis Date: Thu, 3 Oct 2019 15:44:05 -0700 Subject: [PATCH 0763/2020] mockito 3.1.0 (#2033) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2f1410b744..f0be4cc39c 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ subprojects { //test JUNIT: '4.12', - MOCKITO_CORE: '2.23.4', + MOCKITO_CORE: '3.1.0', SLF4J_API: '1.7.25', SYSTEM_RULES: '1.19.0', ] From 03b15bc131ef027c0299db8540bed759cedbb0c9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 4 Oct 2019 11:33:54 -0400 Subject: [PATCH 0764/2020] Check base image cache if referenced by digest (#2039) --- .../jib/builder/steps/PullBaseImageStep.java | 43 +++--- .../builder/steps/PullBaseImageStepTest.java | 125 ++++++++++++++++++ 2 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 40a3f1f1c4..265c4a0588 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -29,7 +29,6 @@ import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; @@ -100,27 +99,29 @@ public ImageAndAuthorization call() CacheCorruptedException, CredentialRetrievalException { EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); // Skip this step if this is a scratch image - ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); - if (baseImageConfiguration.getImage().isScratch()) { + ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); + if (imageReference.isScratch()) { eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); return new ImageAndAuthorization( Image.builder(buildConfiguration.getTargetFormat()).build(), null); } - eventHandlers.dispatch( - LogEvent.progress( - "Getting base image " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "...")); + eventHandlers.dispatch(LogEvent.progress("Getting base image " + imageReference + "...")); - if (buildConfiguration.isOffline()) { - return new ImageAndAuthorization(pullBaseImageOffline(), null); + if (buildConfiguration.isOffline() || imageReference.isTagDigest()) { + Optional image = getCachedBaseImage(); + if (image.isPresent()) { + return new ImageAndAuthorization(image.get(), null); + } + if (buildConfiguration.isOffline()) { + throw new IOException( + "Cannot run Jib in offline mode; " + imageReference + " not found in local Jib cache"); + } } try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pulling base image manifest", 2); - TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { + TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) { // First, try with no credentials. try { return new ImageAndAuthorization(pullBaseImage(null, progressEventDispatcher), null); @@ -128,9 +129,7 @@ public ImageAndAuthorization call() } catch (RegistryUnauthorizedException ex) { eventHandlers.dispatch( LogEvent.lifecycle( - "The base image requires auth. Trying again for " - + buildConfiguration.getBaseImageConfiguration().getImage() - + "...")); + "The base image requires auth. Trying again for " + imageReference + "...")); // If failed, then, retrieve base registry credentials and try with retrieved credentials. // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to @@ -294,32 +293,32 @@ private ManifestTemplate obtainPlatformSpecificImageManifest( /** * Retrieves the cached base image. * - * @return the cached image + * @return the cached image, if found * @throws IOException when an I/O exception occurs * @throws CacheCorruptedException if the cache is corrupted * @throws LayerPropertyNotFoundException if adding image layers fails * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ - private Image pullBaseImageOffline() + private Optional getCachedBaseImage() throws IOException, CacheCorruptedException, BadContainerConfigurationFormatException, LayerCountMismatchException { ImageReference baseImage = buildConfiguration.getBaseImageConfiguration().getImage(); Optional metadata = buildConfiguration.getBaseImageLayersCache().retrieveMetadata(baseImage); if (!metadata.isPresent()) { - throw new IOException( - "Cannot run Jib in offline mode; " + baseImage + " not found in local Jib cache"); + return Optional.empty(); } ManifestTemplate manifestTemplate = metadata.get().getManifest(); if (manifestTemplate instanceof V21ManifestTemplate) { - return JsonToImageTranslator.toImage((V21ManifestTemplate) manifestTemplate); + return Optional.of(JsonToImageTranslator.toImage((V21ManifestTemplate) manifestTemplate)); } ContainerConfigurationTemplate configurationTemplate = metadata.get().getConfig().orElseThrow(IllegalStateException::new); - return JsonToImageTranslator.toImage( - (BuildableManifestTemplate) manifestTemplate, configurationTemplate); + return Optional.of( + JsonToImageTranslator.toImage( + (BuildableManifestTemplate) manifestTemplate, configurationTemplate)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java new file mode 100644 index 0000000000..38e88f9ba6 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.ManifestAndConfig; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import java.io.IOException; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link PullBaseImageStep}. */ +@RunWith(MockitoJUnitRunner.class) +public class PullBaseImageStepTest { + + private final ContainerConfigurationTemplate containerConfig = + new ContainerConfigurationTemplate(); + private final ManifestAndConfig manifestAndConfig = + new ManifestAndConfig(new V22ManifestTemplate(), containerConfig); + + @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; + @Mock private BuildConfiguration buildConfiguration; + @Mock private ImageConfiguration imageConfiguration; + @Mock private Cache cache; + + private PullBaseImageStep pullBaseImageStep; + + @Before + public void setUp() { + containerConfig.setOs("fat system"); + + Mockito.when(buildConfiguration.getBaseImageConfiguration()).thenReturn(imageConfiguration); + Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(buildConfiguration.getBaseImageLayersCache()).thenReturn(cache); + + pullBaseImageStep = new PullBaseImageStep(buildConfiguration, progressDispatcherFactory); + } + + @Test + public void testCall_digestBaseImage() + throws LayerPropertyNotFoundException, IOException, RegistryException, + LayerCountMismatchException, BadContainerConfigurationFormatException, + CacheCorruptedException, CredentialRetrievalException, InvalidImageReferenceException { + ImageReference imageReference = + ImageReference.parse( + "awesome@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + Assert.assertTrue(imageReference.isTagDigest()); + Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(manifestAndConfig)); + + ImageAndAuthorization result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.getImage().getOs()); + Assert.assertNull(result.getAuthorization()); + + Mockito.verify(buildConfiguration, Mockito.never()).newBaseImageRegistryClientFactory(); + } + + @Test + public void testCall_offlineMode_notCached() + throws LayerPropertyNotFoundException, RegistryException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException, InvalidImageReferenceException { + Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.parse("cat")); + Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + + try { + pullBaseImageStep.call(); + Assert.fail(); + } catch (IOException ex) { + Assert.assertEquals( + "Cannot run Jib in offline mode; cat not found in local Jib cache", ex.getMessage()); + } + } + + @Test + public void testCall_offlineMode_cached() + throws LayerPropertyNotFoundException, RegistryException, LayerCountMismatchException, + BadContainerConfigurationFormatException, CacheCorruptedException, + CredentialRetrievalException, InvalidImageReferenceException, IOException { + ImageReference imageReference = ImageReference.parse("cat"); + Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); + Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(manifestAndConfig)); + + ImageAndAuthorization result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.getImage().getOs()); + Assert.assertNull(result.getAuthorization()); + + Mockito.verify(buildConfiguration, Mockito.never()).newBaseImageRegistryClientFactory(); + } +} From 2d163f96a5157c44171dc6d009a688fc76c2d3ed Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 4 Oct 2019 13:12:57 -0400 Subject: [PATCH 0765/2020] Update changelog for #2039 (#2040) --- jib-core/CHANGELOG.md | 1 + jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 3 files changed, 3 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 729eb32632..9e73aa2c00 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) ### Fixed diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7bb7f5970f..6a2d92f413 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 84d2df8384..5e0925d389 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) ### Fixed From 428498fd26f25dae732e6818227e19d7490eec4f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 4 Oct 2019 14:22:36 -0400 Subject: [PATCH 0766/2020] Add cache methods for writing/reading local base image container config (#2038) --- .../google/cloud/tools/jib/cache/Cache.java | 38 +++++++++++++++++++ .../tools/jib/cache/CacheStorageReader.java | 20 ++++++++++ .../tools/jib/cache/CacheStorageWriter.java | 15 ++++++++ .../jib/cache/CacheStorageReaderTest.java | 29 ++++++++++++++ .../jib/cache/CacheStorageWriterTest.java | 27 +++++++++++++ 5 files changed, 129 insertions(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index 6bd8532448..f8014a2873 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -130,6 +130,25 @@ public CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) return cacheStorageWriter.writeTarLayer(diffId, compressedBlob); } + /** + * Writes a container configuration to {@code (cache directory)/local/config/(image id)}. An image + * ID is a SHA hash of a container configuration JSON. The value is also shown as IMAGE ID in + * {@code docker images}. + * + *

Note: the {@code imageID} to the {@code containerConfiguration} is a one-way relationship; + * there is no guarantee that {@code containerConfiguration}'s SHA will be {@code imageID}, since + * the original container configuration is being rewritten here rather than being moved. + * + * @param imageId the ID of the image to store the container configuration for + * @param containerConfiguration the container configuration + * @throws IOException if an I/O exception occurs + */ + void writeLocalConfig( + DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) + throws IOException { + cacheStorageWriter.writeLocalConfig(imageId, containerConfiguration); + } + /** * Retrieves the cached manifest and container configuration for an image reference. * @@ -187,4 +206,23 @@ public Optional retrieveTarLayer(DescriptorDigest diffId) throws IOException, CacheCorruptedException { return cacheStorageReader.retrieveTarLayer(diffId); } + + /** + * Retrieves the {@link ContainerConfigurationTemplate} for the image saved from the given image + * ID. An image ID is a SHA hash of a container configuration JSON. The value is also shown as + * IMAGE ID in {@code docker images}. + * + *

Note: the {@code imageID} is only used to find the {@code containerConfiguration}, and is + * not necessarily the actual SHA of {@code containerConfiguration}. There is no guarantee that + * {@code containerConfiguration}'s SHA will be {@code imageID}, since the saved container + * configuration is not a direct copy of the base image's original configuration. + * + * @param imageId the image ID + * @return the {@link ContainerConfigurationTemplate} referenced by the image ID, if found + * @throws IOException if an I/O exception occurs + */ + public Optional retrieveLocalConfig(DescriptorDigest imageId) + throws IOException { + return cacheStorageReader.retrieveLocalConfig(imageId); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index 3fbd217ebf..b921b9fac0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -223,6 +223,26 @@ Optional retrieveTarLayer(DescriptorDigest diffId) } } + /** + * Retrieves the {@link ContainerConfigurationTemplate} for the image with the given image ID. + * + * @param imageId the image ID + * @return the {@link ContainerConfigurationTemplate} referenced by the image ID, if found + * @throws IOException if an I/O exception occurs + */ + Optional retrieveLocalConfig(DescriptorDigest imageId) + throws IOException { + Path configPath = + cacheStorageFiles.getLocalDirectory().resolve("config").resolve(imageId.getHash()); + if (!Files.exists(configPath)) { + return Optional.empty(); + } + + ContainerConfigurationTemplate config = + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + return Optional.of(config); + } + /** * Retrieves the layer digest selected by the {@code selector}. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index 2995f7c5a9..fbcc8d20ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -332,6 +332,21 @@ void writeMetadata(ImageReference imageReference, V21ManifestTemplate manifestTe } } + /** + * Writes a container configuration to {@code (cache directory)/local/config/(image id)}. + * + * @param imageId the ID of the image to store the container configuration for + * @param containerConfiguration the container configuration + * @throws IOException if an I/O exception occurs + */ + void writeLocalConfig( + DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) + throws IOException { + Path configDirectory = cacheStorageFiles.getLocalDirectory().resolve("config"); + Files.createDirectories(configDirectory); + writeMetadata(containerConfiguration, configDirectory.resolve(imageId.getHash())); + } + /** * Writes a compressed {@code layerBlob} to the {@code layerDirectory}. * diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index 50fc375f25..ef2c2a0047 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -248,6 +248,35 @@ public void testRetrieveTarLayer() throws IOException, CacheCorruptedException { } } + @Test + public void testRetrieveLocalConfig() throws IOException, URISyntaxException, DigestException { + Path cacheDirectory = temporaryFolder.newFolder().toPath(); + Path configDirectory = cacheDirectory.resolve("local").resolve("config"); + Files.createDirectories(configDirectory); + Files.copy( + Paths.get(Resources.getResource("core/json/containerconfig.json").toURI()), + configDirectory.resolve( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + + CacheStorageFiles cacheStorageFiles = new CacheStorageFiles(cacheDirectory); + CacheStorageReader cacheStorageReader = new CacheStorageReader(cacheStorageFiles); + + ContainerConfigurationTemplate configurationTemplate = + cacheStorageReader + .retrieveLocalConfig( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .get(); + Assert.assertEquals("wasm", configurationTemplate.getArchitecture()); + Assert.assertEquals("js", configurationTemplate.getOs()); + + Optional missingConfigurationTemplate = + cacheStorageReader.retrieveLocalConfig( + DescriptorDigest.fromHash( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + Assert.assertFalse(missingConfigurationTemplate.isPresent()); + } + @Test public void testSelect_invalidLayerDigest() throws IOException { CacheStorageFiles cacheStorageFiles = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index 1a24ab542f..c5bdfb5f63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -34,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.DigestException; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.junit.Assert; @@ -185,6 +186,32 @@ public void testWriteMetadata_v22() Assert.assertEquals("wasm", savedContainerConfig.getArchitecture()); } + @Test + public void testWriteLocalConfig() throws IOException, URISyntaxException, DigestException { + Path containerConfigurationJsonFile = + Paths.get( + getClass().getClassLoader().getResource("core/json/containerconfig.json").toURI()); + ContainerConfigurationTemplate containerConfigurationTemplate = + JsonTemplateMapper.readJsonFromFile( + containerConfigurationJsonFile, ContainerConfigurationTemplate.class); + + new CacheStorageWriter(cacheStorageFiles) + .writeLocalConfig( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + containerConfigurationTemplate); + + Path savedConfigPath = + cacheStorageFiles + .getLocalDirectory() + .resolve("config") + .resolve("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + Assert.assertTrue(Files.exists(savedConfigPath)); + ContainerConfigurationTemplate savedContainerConfig = + JsonTemplateMapper.readJsonFromFile(savedConfigPath, ContainerConfigurationTemplate.class); + Assert.assertEquals("wasm", savedContainerConfig.getArchitecture()); + } + private void verifyCachedLayer(CachedLayer cachedLayer, Blob uncompressedLayerBlob) throws IOException { BlobDescriptor layerBlobDescriptor = getDigest(compress(uncompressedLayerBlob)); From f6d2f4aea9f3e811074d7396d3f62b6a5f2f603e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 4 Oct 2019 18:13:27 -0400 Subject: [PATCH 0767/2020] Clarify keystore (keystore.jks) vs truststore (cacerts.jsk) distinction (#2037) --- docs/self_sign_cert.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/self_sign_cert.md b/docs/self_sign_cert.md index fadea5b3cd..8c1df10a79 100644 --- a/docs/self_sign_cert.md +++ b/docs/self_sign_cert.md @@ -2,7 +2,7 @@ Jib relies on the Java Runtime Environment's list of approved _Certification Authority Certificates_ for validating SSL certificates, and will hence fail when connecting to a docker registry that uses a self-signed `https` certificate. This document describes two approaches for handling registries with self-signed certificates. Both approaches configure the JRE's list of approved CA Certificates. -These CA Certificates are managed through a _keystore_ file. The easiest way to manipulate keystores is using the [KeyStore Explorer](http://keystore-explorer.org/), an open source GUI replacement for the Java command-line `keytool` and `jarsigner` utilities. Download and install KeyStore Explorer from the [official website](http://keystore-explorer.org/downloads.html). +These CA Certificates for JRE are managed through a type of a keystore file called _truststore_. An easy way to manipulate truststores is using the [KeyStore Explorer](http://keystore-explorer.org/), an open source GUI replacement for the Java command-line `keytool` and `jarsigner` utilities. Download and install KeyStore Explorer from the [official website](http://keystore-explorer.org/downloads.html). ## Step 1. Identify Java runtime used by build tool @@ -73,20 +73,20 @@ Otherwise use _Examine > Examine SSL_ to connect to your service and click the _ ## 4. Save the CA Certificates -Now we save the updated keystore. We can either save to a new keystore and configure our build's JVM to use this new keystore as a _trusted keystore_, or modify the JRE's list of CA Certificates. +Now we save the updated truststore. We can either save to a new truststore and configure our build's JVM to use this new truststore, or modify the JRE's list of CA Certificates. -#### Option 1: Create a New Trusted Keystore +#### Option 1: Create a New Truststore -This option creates a _new_ list of CA Certificates and configures your build tool to use ths new list as the JRE's list of approved CA certificates, called the _trust store_. +This option creates a _new_ list of CA Certificates and configures your build tool to use this new list as the JRE's list of approved CA certificates. -Within _KeyStore Explorer_, select _File > Save As..._ and save the new keystore file as a _JKS_ file within your project location. You will be prompted for a password; we use `password` in the examples below. +Within _KeyStore Explorer_, select _File > Save As..._ and save the new truststore file as a _JKS_ file within your project location. You will be prompted for a password; we use `password` in the examples below. ##### Maven -The following snippet shows how to configure Maven to use this new keystore file: +The following snippet shows how to configure Maven to use this new truststore file: ```shell -$ ./mvnw -Djavax.net.ssl.trustStore=path/to/keystore.jks \ +$ ./mvnw -Djavax.net.ssl.trustStore=path/to/truststore.jks \ -Djavax.net.ssl.trustStorePassword=password \ -Dimage=:/ jib:build ``` @@ -95,11 +95,11 @@ You may choose to configure your registry credentials with [the `~/.m2/settings. ##### Gradle -The following snippet shows how to configure Gradle to use this new keystore file: +The following snippet shows how to configure Gradle to use this new truststore file: ```shell $ ./gradlew jib \ - -Djavax.net.ssl.trustStore=path/to/keystore.jks \ + -Djavax.net.ssl.trustStore=path/to/truststore.jks \ -Djavax.net.ssl.trustStorePassword=password ``` From 4935511e558d5a896fe02d7c2816cd7f62a52098 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 7 Oct 2019 16:46:05 -0400 Subject: [PATCH 0768/2020] Get size, image ID, and diff IDs from "docker inspect" (#2041) --- .../jib/builder/steps/SaveDockerStep.java | 5 +- .../cloud/tools/jib/docker/DockerClient.java | 92 ++++++++++++++----- .../tools/jib/docker/DockerClientTest.java | 32 ++++++- 3 files changed, 103 insertions(+), 26 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java index 9b8548007f..40132c3982 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import java.io.IOException; import java.nio.file.Path; +import java.security.DigestException; import java.util.concurrent.Callable; /** Saves an image from the docker daemon. */ @@ -47,7 +48,7 @@ public class SaveDockerStep implements Callable { } @Override - public Path call() throws IOException, InterruptedException { + public Path call() throws IOException, InterruptedException, DigestException { Path outputDir = tempDirectoryProvider.newDirectory(); Path outputPath = outputDir.resolve("out.tar"); ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); @@ -55,7 +56,7 @@ public Path call() throws IOException, InterruptedException { new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Saving " + imageReference + " from Docker daemon")) { - long size = dockerClient.sizeOf(imageReference); + long size = dockerClient.inspect(imageReference).getSize(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("saving base image " + imageReference, size); ThrottledAccumulatingConsumer throttledProgressReporter = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 0bdcd958a5..c990b883e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -16,8 +16,11 @@ package com.google.cloud.tools.jib.docker; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.http.NotifyingOutputStream; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; @@ -32,8 +35,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.DigestException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -42,6 +47,31 @@ /** Calls out to the {@code docker} CLI. */ public class DockerClient { + /** + * Contains the size, image ID, and diff IDs of an image inspected with {@code docker inspect}. + */ + public static class DockerImageDetails implements JsonTemplate { + private long size; + private String imageId = ""; + private List diffIds = Collections.emptyList(); + + public long getSize() { + return size; + } + + public DescriptorDigest getImageId() throws DigestException { + return DescriptorDigest.fromDigest(imageId); + } + + public List getDiffIds() throws DigestException { + List processedDiffIds = new ArrayList<>(diffIds.size()); + for (String diffId : diffIds) { + processedDiffIds.add(DescriptorDigest.fromDigest(diffId.trim())); + } + return processedDiffIds; + } + } + /** Default path to the docker executable. */ public static final Path DEFAULT_DOCKER_CLIENT = Paths.get("docker"); @@ -95,6 +125,27 @@ static Function, ProcessBuilder> defaultProcessBuilderFactory( }; } + /** + * Parses the results of {@code docker inspect} into an {@link DockerImageDetails}. + * + * @param inspectOutput the output of the {@code docker inspect} command containing the size, + * image ID, and diff IDs + * @return the {@link DockerImageDetails} + */ + @VisibleForTesting + static DockerImageDetails parseInspectResults(String inspectOutput) throws IOException { + return JsonTemplateMapper.readJson(inspectOutput, DockerImageDetails.class); + } + + private static String getStderrOutput(Process process) { + try (InputStreamReader stderr = + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { + return CharStreams.toString(stderr); + } catch (IOException ex) { + return "unknown (failed to read error message from stderr due to " + ex.getMessage() + ")"; + } + } + /** Factory for generating the {@link ProcessBuilder} for running {@code docker} commands. */ private final Function, ProcessBuilder> processBuilderFactory; @@ -215,24 +266,30 @@ public void tag(ImageReference originalImageReference, ImageReference newImageRe } /** - * Gets the size of an image in the Docker daemon. + * Gets the size, image ID, and diff IDs of an image in the Docker daemon. * - * @param imageReference the image to find the size of - * @return the size in bytes - * @throws IOException if an I/O exception occurs - * @throws InterruptedException if the 'docker inspect' process is interrupted + * @param imageReference the image to inspect + * @return the size, image ID, and diff IDs of the image + * @throws IOException if an I/O exception occurs or {@code docker inspect} failed + * @throws InterruptedException if the {@code docker inspect} process was interrupted */ - public long sizeOf(ImageReference imageReference) throws IOException, InterruptedException { - Process sizeProcess = docker("inspect", "-f", "{{.Size}}", imageReference.toString()); - - if (sizeProcess.waitFor() != 0) { + public DockerImageDetails inspect(ImageReference imageReference) + throws IOException, InterruptedException { + Process inspectProcess = + docker( + "inspect", + "-f", + "{\"size\":{{.Size}},\"imageId\":\"{{.Id}}\",\"diffIds\":{{json .RootFS.Layers}}}", + "--type", + "image", + imageReference.toString()); + if (inspectProcess.waitFor() != 0) { throw new IOException( - "'docker inspect' command failed with error: " + getStderrOutput(sizeProcess)); + "'docker inspect' command failed with error: " + getStderrOutput(inspectProcess)); } - - return Long.parseLong( + return parseInspectResults( CharStreams.toString( - new InputStreamReader(sizeProcess.getInputStream(), StandardCharsets.UTF_8)) + new InputStreamReader(inspectProcess.getInputStream(), StandardCharsets.UTF_8)) .trim()); } @@ -240,13 +297,4 @@ public long sizeOf(ImageReference imageReference) throws IOException, Interrupte private Process docker(String... subCommand) throws IOException { return processBuilderFactory.apply(Arrays.asList(subCommand)).start(); } - - private static String getStderrOutput(Process process) { - try (InputStreamReader stderr = - new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { - return CharStreams.toString(stderr); - } catch (IOException ex) { - return "unknown (failed to read error message from stderr due to " + ex.getMessage() + ")"; - } - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index bc3e9d19f8..8d110776cd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -16,8 +16,10 @@ package com.google.cloud.tools.jib.docker; +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.docker.DockerClient.DockerImageDetails; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; @@ -27,6 +29,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import java.security.DigestException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -271,7 +274,7 @@ public void testSize_fail() throws InterruptedException { DockerClient testDockerClient = new DockerClient( subcommand -> { - Assert.assertEquals(Arrays.asList("inspect", "-f", "{{.Size}}", "image"), subcommand); + Assert.assertEquals("inspect", subcommand.get(0)); return mockProcessBuilder; }); Mockito.when(mockProcess.waitFor()).thenReturn(1); @@ -280,7 +283,7 @@ public void testSize_fail() throws InterruptedException { .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); try { - testDockerClient.sizeOf(ImageReference.of(null, "image", null)); + testDockerClient.inspect(ImageReference.of(null, "image", null)); Assert.fail("docker inspect should have failed"); } catch (IOException ex) { @@ -288,6 +291,31 @@ public void testSize_fail() throws InterruptedException { } } + @Test + public void testParseInspectResults() throws DigestException, IOException { + String output = + "{\"size\":488118507," + + "\"imageId\":\"sha256:e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd\"," + + "\"diffIds\":[\"sha256:55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1\"," + + "\"sha256:26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b\"," + + "\"sha256:8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a\"]}\n"; + DockerImageDetails results = DockerClient.parseInspectResults(output); + Assert.assertEquals(488118507, results.getSize()); + Assert.assertEquals( + DescriptorDigest.fromHash( + "e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd"), + results.getImageId()); + Assert.assertEquals( + Arrays.asList( + DescriptorDigest.fromHash( + "55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1"), + DescriptorDigest.fromHash( + "26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b"), + DescriptorDigest.fromHash( + "8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a")), + results.getDiffIds()); + } + private DockerClient makeDockerSaveClient() { return new DockerClient( subcommand -> { From 7d3c12ba051e28d7741b902db121b333fcd8e6a5 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 9 Oct 2019 15:06:13 -0400 Subject: [PATCH 0769/2020] Request source image access only if distinct from the base image (#2048) Co-Authored-By: Tad Cordle --- .../jib/registry/RegistryAuthenticator.java | 2 +- .../RegistryEndpointRequestProperties.java | 5 ++ .../registry/RegistryAuthenticatorTest.java | 47 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index a0869a42ca..7bea466a50 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -235,7 +235,7 @@ private Authorization authenticate(@Nullable Credential credential, String scope throws RegistryAuthenticationFailedException { // try authorizing against both the main repository and the source repository too // to enable cross-repository mounts on pushes - if (registryEndpointRequestProperties.getSourceImageName() != null) { + if (registryEndpointRequestProperties.hasDistinctSourceImageName()) { try { Map scopes = ImmutableMap.of( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index 2e125ec847..37ea2682e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -57,4 +57,9 @@ String getImageName() { String getSourceImageName() { return sourceImageName; } + + /** Return {@code true} if we have a source image and it is different from the base image name. */ + public boolean hasDistinctSourceImageName() { + return sourceImageName != null && !imageName.equals(sourceImageName); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 59ca1c8df9..b73e7ba342 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -214,4 +214,51 @@ public void testUserAgent() server.getInputRead(), CoreMatchers.containsString("User-Agent: Competent-Agent")); } } + + @Test + public void testSourceImage_differentSourceRepository() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(false)) { + try { + RegistryEndpointRequestProperties registryEndpointRequestProperties = + new RegistryEndpointRequestProperties("someserver", "someimage", "anotherimage"); + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"" + server.getEndpoint() + "\"", + registryEndpointRequestProperties, + "Competent-Agent") + .get(); + authenticator.authenticatePush(null); + } catch (RegistryAuthenticationFailedException ex) { + // Doesn't matter if auth fails. We only examine what we sent. + } + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull")); + } + } + + @Test + public void testSourceImage_sameSourceRepository() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + try (TestWebServer server = new TestWebServer(false)) { + try { + RegistryEndpointRequestProperties registryEndpointRequestProperties = + new RegistryEndpointRequestProperties("someserver", "someimage", "someimage"); + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"" + server.getEndpoint() + "\"", + registryEndpointRequestProperties, + "Competent-Agent") + .get(); + authenticator.authenticatePush(null); + } catch (RegistryAuthenticationFailedException ex) { + // Doesn't matter if auth fails. We only examine what we sent. + } + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("service=someserver&scope=repository:someimage:pull,push ")); + } + } } From c3ae62a5bbba771048d794083cf467c87066f1e0 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 11 Oct 2019 15:55:32 -0400 Subject: [PATCH 0770/2020] Cleanup code (#2057) --- .../cloud/tools/jib/registry/BlobChecker.java | 3 +-- .../jib/registry/RegistryAuthenticator.java | 22 +++++----------- .../jib/registry/RegistryEndpointCaller.java | 8 ++---- .../RegistryEndpointRequestProperties.java | 5 ---- .../json/DockerConfigTemplate.java | 21 --------------- .../jib/registry/json/ErrorEntryTemplate.java | 1 + .../CredentialRetrieverFactoryTest.java | 2 +- .../registry/RegistryEndpointCallerTest.java | 2 +- .../RegistryErrorExceptionBuilderTest.java | 26 +++++++++---------- .../PluginConfigurationProcessorTest.java | 4 +-- 10 files changed, 28 insertions(+), 66 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index e2b61300a9..3c64aea09a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -61,8 +61,7 @@ public Optional handleResponse(Response response) throws Registr @Override public Optional handleHttpResponseException( - HttpResponseException httpResponseException) - throws RegistryErrorException, HttpResponseException { + HttpResponseException httpResponseException) throws HttpResponseException { if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw httpResponseException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 7bea466a50..4564571beb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -146,7 +146,7 @@ private String getToken() { private final String service; private final String userAgent; - RegistryAuthenticator( + private RegistryAuthenticator( String realm, String service, RegistryEndpointRequestProperties registryEndpointRequestProperties, @@ -181,8 +181,7 @@ public Authorization authenticatePush(@Nullable Credential credential) return authenticate(credential, "pull,push"); } - @VisibleForTesting - String getServiceScopeRequestParameters(Map repositoryScopes) { + private String getServiceScopeRequestParameters(Map repositoryScopes) { StringBuilder parameters = new StringBuilder("service=").append(service); for (Entry pair : repositoryScopes.entrySet()) { parameters @@ -235,23 +234,16 @@ private Authorization authenticate(@Nullable Credential credential, String scope throws RegistryAuthenticationFailedException { // try authorizing against both the main repository and the source repository too // to enable cross-repository mounts on pushes - if (registryEndpointRequestProperties.hasDistinctSourceImageName()) { + String sourceImageName = registryEndpointRequestProperties.getSourceImageName(); + String imageName = registryEndpointRequestProperties.getImageName(); + if (sourceImageName != null && !sourceImageName.equals(imageName)) { try { - Map scopes = - ImmutableMap.of( - registryEndpointRequestProperties.getImageName(), - scope, - registryEndpointRequestProperties.getSourceImageName(), - "pull"); - return authenticate(credential, scopes); + return authenticate(credential, ImmutableMap.of(imageName, scope, sourceImageName, "pull")); } catch (RegistryAuthenticationFailedException ex) { // Unable to obtain authorization with source image: fallthrough and try without } } - Map repositoryScopes = - ImmutableMap.of(registryEndpointRequestProperties.getImageName(), scope); - Authorization auth = authenticate(credential, repositoryScopes); - return auth; + return authenticate(credential, ImmutableMap.of(imageName, scope)); } private Authorization authenticate( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 0456b59f22..7c0382bff1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -35,7 +35,6 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.ConnectException; -import java.net.MalformedURLException; import java.net.URL; import java.security.GeneralSecurityException; import java.util.Locale; @@ -100,7 +99,6 @@ static boolean isBrokenPipe(IOException original) { * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed - * @throws MalformedURLException if the URL generated for the endpoint is malformed */ RegistryEndpointCaller( EventHandlers eventHandlers, @@ -108,8 +106,7 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowInsecureRegistries) - throws MalformedURLException { + boolean allowInsecureRegistries) { this( eventHandlers, userAgent, @@ -130,8 +127,7 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointRequestProperties registryEndpointRequestProperties, boolean allowInsecureRegistries, Function connectionFactory, - @Nullable Function insecureConnectionFactory) - throws MalformedURLException { + @Nullable Function insecureConnectionFactory) { this.eventHandlers = eventHandlers; this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index 37ea2682e2..2e125ec847 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -57,9 +57,4 @@ String getImageName() { String getSourceImageName() { return sourceImageName; } - - /** Return {@code true} if we have a source image and it is different from the base image name. */ - public boolean hasDistinctSourceImageName() { - return sourceImageName != null && !imageName.equals(sourceImageName); - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index 2f3d338e1a..12bc6529fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.common.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -91,24 +90,4 @@ public String getCredsStore() { public Map getCredHelpers() { return credHelpers; } - - @VisibleForTesting - DockerConfigTemplate addAuth(String registry, @Nullable String auth) { - AuthTemplate authTemplate = new AuthTemplate(); - authTemplate.auth = auth; - auths.put(registry, authTemplate); - return this; - } - - @VisibleForTesting - DockerConfigTemplate setCredsStore(String credsStore) { - this.credsStore = credsStore; - return this; - } - - @VisibleForTesting - DockerConfigTemplate addCredHelper(String registry, String credHelper) { - credHelpers.put(registry, credHelper); - return this; - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java index 09257600ad..151a64f336 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java @@ -32,6 +32,7 @@ public ErrorEntryTemplate(String code, String message) { this.message = message; } + /** Necessary for Jackson to create from JSON. */ private ErrorEntryTemplate() {} @Nullable diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index c01b3f4aef..24de724303 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -144,7 +144,7 @@ public void testGoogleApplicationDefaultCredentials_notGoogleContainerRegistry() Assert.assertFalse( credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); - Mockito.verifyZeroInteractions(mockLogger); + Mockito.verifyNoInteractions(mockLogger); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 7ae726e9df..528fb07aee 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -701,7 +701,7 @@ private void verifyRetriesWithNewLocation(int httpStatusCode) } private RegistryEndpointCaller createRegistryEndpointCaller( - boolean allowInsecure, int port) throws MalformedURLException { + boolean allowInsecure, int port) { return new RegistryEndpointCaller<>( mockEventHandlers, "userAgent", diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java index f32311e3ad..64107096fc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java @@ -33,19 +33,19 @@ public class RegistryErrorExceptionBuilderTest { @Test public void testAddErrorEntry() { RegistryErrorExceptionBuilder builder = - new RegistryErrorExceptionBuilder("do something", mockHttpResponseException); - - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_INVALID.name(), "manifest invalid")); - builder.addReason(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "blob unknown")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "manifest unknown")); - builder.addReason(new ErrorEntryTemplate(ErrorCodes.TAG_INVALID.name(), "tag invalid")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNVERIFIED.name(), "manifest unverified")); - builder.addReason( - new ErrorEntryTemplate(ErrorCodes.UNSUPPORTED.name(), "some other error happened")); - builder.addReason(new ErrorEntryTemplate("unknown", "some unknown error happened")); + new RegistryErrorExceptionBuilder("do something", mockHttpResponseException) + .addReason( + new ErrorEntryTemplate(ErrorCodes.MANIFEST_INVALID.name(), "manifest invalid")) + .addReason(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "blob unknown")) + .addReason( + new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "manifest unknown")) + .addReason(new ErrorEntryTemplate(ErrorCodes.TAG_INVALID.name(), "tag invalid")) + .addReason( + new ErrorEntryTemplate( + ErrorCodes.MANIFEST_UNVERIFIED.name(), "manifest unverified")) + .addReason( + new ErrorEntryTemplate(ErrorCodes.UNSUPPORTED.name(), "some other error happened")) + .addReason(new ErrorEntryTemplate("unknown", "some unknown error happened")); try { throw builder.build(); diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index fe3a772a55..292c4fc392 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -230,7 +230,7 @@ public void testEntrypoint() Assert.assertEquals( Arrays.asList("custom", "entrypoint"), buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(logger); + Mockito.verifyNoInteractions(logger); } @Test @@ -288,7 +288,7 @@ public void testEntrypoint_defaultWarPackaging() Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - Mockito.verifyZeroInteractions(logger); + Mockito.verifyNoInteractions(logger); } @Test From 756d5e40fdad2198ea72d6e915e690d5694bd167 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 14 Oct 2019 14:57:50 -0400 Subject: [PATCH 0771/2020] Clarify log messages for skipped layers (#2062) --- .../jib/builder/steps/ObtainBaseImageLayerStep.java | 10 ++++++++-- .../tools/jib/builder/steps/PullBaseImageStep.java | 3 ++- .../cloud/tools/jib/builder/steps/PushBlobStep.java | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 251f9c298d..7a4b411298 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -26,6 +27,7 @@ import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.registry.RegistryClient; @@ -131,15 +133,19 @@ private ObtainBaseImageLayerStep( @Override public PreparedLayer call() throws IOException, CacheCorruptedException, RegistryException { + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, layerDigest))) { + new TimerEventDispatcher(eventHandlers, String.format(DESCRIPTION, layerDigest))) { StateInTarget stateInTarget = blobExistenceChecker.check(layerDigest); if (stateInTarget == StateInTarget.EXISTING) { + eventHandlers.dispatch( + LogEvent.info( + "Skipping pull; BLOB already exists on target registry : " + + layer.getBlobDescriptor())); return new PreparedLayer.Builder(layer).setStateInTarget(stateInTarget).build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 265c4a0588..74243ff30c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -106,7 +106,8 @@ public ImageAndAuthorization call() Image.builder(buildConfiguration.getTargetFormat()).build(), null); } - eventHandlers.dispatch(LogEvent.progress("Getting base image " + imageReference + "...")); + eventHandlers.dispatch( + LogEvent.progress("Getting manifest for base image " + imageReference + "...")); if (buildConfiguration.isOffline() || imageReference.isTagDigest()) { Optional image = getCachedBaseImage(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 12bba2e04e..534de20198 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -80,7 +80,8 @@ public BlobDescriptor call() throws IOException, RegistryException { // check if the BLOB is available if (!forcePush && registryClient.checkBlob(blobDigest).isPresent()) { eventHandlers.dispatch( - LogEvent.info("BLOB : " + blobDescriptor + " already exists on registry")); + LogEvent.info( + "Skipping push; BLOB already exists on target registry : " + blobDescriptor)); return blobDescriptor; } From d3d35f34777f2cf003a80c9349e77315d211dab8 Mon Sep 17 00:00:00 2001 From: Tejas Kasetty Date: Tue, 15 Oct 2019 01:43:48 +0530 Subject: [PATCH 0772/2020] Return both manifest and digest for pull manifest (#2034) (#2052) --- .../registry/BlobCheckerIntegrationTest.java | 2 +- .../registry/BlobPullerIntegrationTest.java | 2 +- .../ManifestPullerIntegrationTest.java | 11 ++-- .../ManifestPusherIntegrationTest.java | 8 +-- .../jib/builder/steps/PullBaseImageStep.java | 11 ++-- .../tools/jib/registry/ManifestAndDigest.java | 50 +++++++++++++++++++ .../tools/jib/registry/ManifestPuller.java | 22 +++++--- .../tools/jib/registry/RegistryClient.java | 11 ++-- .../jib/registry/ManifestPullerTest.java | 37 +++++++++++--- 9 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index b94e078a72..721c00c05b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -44,7 +44,7 @@ public void testCheck_exists() throws IOException, RegistryException { .setAllowInsecureRegistries(true) .newRegistryClient(); V22ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V22ManifestTemplate.class); + registryClient.pullManifest("latest", V22ManifestTemplate.class).getManifest(); DescriptorDigest blobDigest = manifestTemplate.getLayers().get(0).getDigest(); Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).get().getDigest()); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index da187049de..1f1f909144 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -52,7 +52,7 @@ public void testPull() throws IOException, RegistryException { .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V21ManifestTemplate.class); + registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); DescriptorDigest realDigest = manifestTemplate.getLayerDigests().get(0); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 3c3ee29754..067a00bcdf 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -51,7 +51,7 @@ public void testPull_v21() throws IOException, RegistryException { .setAllowInsecureRegistries(true) .newRegistryClient(); V21ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V21ManifestTemplate.class); + registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); Assert.assertEquals(1, manifestTemplate.getSchemaVersion()); Assert.assertTrue(manifestTemplate.getFsLayers().size() > 0); @@ -61,7 +61,7 @@ public void testPull_v21() throws IOException, RegistryException { public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); - ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); + ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); V22ManifestTemplate v22ManifestTemplate = (V22ManifestTemplate) manifestTemplate; @@ -78,12 +78,12 @@ public void testPull_v22ManifestList() throws IOException, RegistryException { // Ensure 11-jre-slim is a manifest list V22ManifestListTemplate manifestListTemplate = - registryClient.pullManifest("11-jre-slim", V22ManifestListTemplate.class); + registryClient.pullManifest("11-jre-slim", V22ManifestListTemplate.class).getManifest(); Assert.assertEquals(2, manifestListTemplate.getSchemaVersion()); Assert.assertTrue(manifestListTemplate.getManifests().size() > 0); // Generic call to 11-jre-slim should NOT pull a manifest list (delegate to registry default) - ManifestTemplate manifestTemplate = registryClient.pullManifest("11-jre-slim"); + ManifestTemplate manifestTemplate = registryClient.pullManifest("11-jre-slim").getManifest(); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestTemplate.class)); @@ -96,7 +96,8 @@ public void testPull_v22ManifestList() throws IOException, RegistryException { } // Referencing a manifest list by sha256, should return a manifest list - ManifestTemplate sha256ManifestList = registryClient.pullManifest(KNOWN_MANIFEST_LIST_SHA); + ManifestTemplate sha256ManifestList = + registryClient.pullManifest(KNOWN_MANIFEST_LIST_SHA).getManifest(); Assert.assertEquals(2, sha256ManifestList.getSchemaVersion()); Assert.assertThat(sha256ManifestList, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); Assert.assertTrue(((V22ManifestListTemplate) sha256ManifestList).getManifests().size() > 0); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 5765a3ca99..88e8e7f982 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -47,7 +47,7 @@ public static void setUp() throws IOException, InterruptedException { public void testPush_missingBlobs() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); - ManifestTemplate manifestTemplate = registryClient.pullManifest("latest"); + ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); registryClient = RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") @@ -101,7 +101,7 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pulls the manifest. V22ManifestTemplate manifestTemplate = - registryClient.pullManifest("latest", V22ManifestTemplate.class); + registryClient.pullManifest("latest", V22ManifestTemplate.class).getManifest(); Assert.assertEquals(1, manifestTemplate.getLayers().size()); Assert.assertEquals(testLayerBlobDigest, manifestTemplate.getLayers().get(0).getDigest()); Assert.assertNotNull(manifestTemplate.getContainerConfiguration()); @@ -111,7 +111,9 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pulls the manifest by digest. V22ManifestTemplate manifestTemplateByDigest = - registryClient.pullManifest(imageDigest.toString(), V22ManifestTemplate.class); + registryClient + .pullManifest(imageDigest.toString(), V22ManifestTemplate.class) + .getManifest(); Assert.assertEquals( Digests.computeJsonDigest(manifestTemplate), Digests.computeJsonDigest(manifestTemplateByDigest)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 74243ff30c..4306c91e71 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -45,6 +45,7 @@ import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.registry.ManifestAndDigest; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -207,7 +208,9 @@ private Image pullBaseImage( .newRegistryClient(); ManifestTemplate manifestTemplate = - registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); + registryClient + .pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()) + .getManifest(); // special handling if we happen upon a manifest list, redirect to a manifest and continue // handling it normally @@ -219,9 +222,9 @@ private Image pullBaseImage( "The base image reference is manifest list, searching for linux/amd64")); manifestTemplate = obtainPlatformSpecificImageManifest( - registryClient, (V22ManifestListTemplate) manifestTemplate); + registryClient, (V22ManifestListTemplate) manifestTemplate) + .getManifest(); } - switch (manifestTemplate.getSchemaVersion()) { case 1: V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; @@ -276,7 +279,7 @@ private Image pullBaseImage( * Looks through a manifest list for any amd64/linux manifest and downloads and returns the first * manifest it finds. */ - private ManifestTemplate obtainPlatformSpecificImageManifest( + private ManifestAndDigest obtainPlatformSpecificImageManifest( RegistryClient registryClient, V22ManifestListTemplate manifestListTemplate) throws RegistryException, IOException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java new file mode 100644 index 0000000000..a95bbd1dcf --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.image.json.ManifestTemplate; + +/** Stores a manifest and digest. */ +public class ManifestAndDigest { + + private final T manifest; + private final DescriptorDigest digest; + + public ManifestAndDigest(T manifest, DescriptorDigest digest) { + this.manifest = manifest; + this.digest = digest; + } + + /** + * Gets the manifest. + * + * @return the manifest + */ + public T getManifest() { + return manifest; + } + + /** + * Gets the digest. + * + * @return the digest + */ + public DescriptorDigest getDigest() { + return digest; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index 84918eb0ed..b8343d72d8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.api.client.http.HttpMethods; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; @@ -28,9 +30,8 @@ import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.io.CharStreams; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -40,7 +41,8 @@ import javax.annotation.Nullable; /** Pulls an image's manifest. */ -class ManifestPuller implements RegistryEndpointProvider { +class ManifestPuller + implements RegistryEndpointProvider> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String imageTag; @@ -85,12 +87,16 @@ public List getAccept() { V21ManifestTemplate.MEDIA_TYPE); } - /** Parses the response body into a {@link ManifestTemplate}. */ + /** Parses the response body into a {@link ManifestAndDigest}. */ @Override - public T handleResponse(Response response) throws IOException, UnknownManifestFormatException { - String jsonString = - CharStreams.toString(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8)); - return getManifestTemplateFromJson(jsonString); + public ManifestAndDigest handleResponse(Response response) + throws IOException, UnknownManifestFormatException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DescriptorDigest digest = + Digests.computeDigest(response.getBody(), byteArrayOutputStream).getDigest(); + String jsonString = byteArrayOutputStream.toString(StandardCharsets.UTF_8.name()); + T manifestTemplate = getManifestTemplateFromJson(jsonString); + return new ManifestAndDigest<>(manifestTemplate, digest); } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 493975f68a..490262b676 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -171,6 +171,7 @@ public static Factory factory( */ @JsonIgnoreProperties(ignoreUnknown = true) private static class TokenPayloadTemplate implements JsonTemplate { + @Nullable private List access; } @@ -181,6 +182,7 @@ private static class TokenPayloadTemplate implements JsonTemplate { */ @JsonIgnoreProperties(ignoreUnknown = true) private static class AccessClaim implements JsonTemplate { + @Nullable private String type; @Nullable private String name; @Nullable private List actions; @@ -279,25 +281,26 @@ public Optional getRegistryAuthenticator() } /** - * Pulls the image manifest for a specific tag. + * Pulls the image manifest and digest for a specific tag. * * @param child type of ManifestTemplate * @param imageTag the tag to pull on * @param manifestTemplateClass the specific version of manifest template to pull, or {@link * ManifestTemplate} to pull predefined subclasses; see: {@link * ManifestPuller#handleResponse(Response)} - * @return the manifest template + * @return the {@link ManifestAndDigest} * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ - public T pullManifest( + public ManifestAndDigest pullManifest( String imageTag, Class manifestTemplateClass) throws IOException, RegistryException { ManifestPuller manifestPuller = new ManifestPuller<>(registryEndpointRequestProperties, imageTag, manifestTemplateClass); return callRegistryEndpoint(manifestPuller); } - public ManifestTemplate pullManifest(String imageTag) throws IOException, RegistryException { + public ManifestAndDigest pullManifest(String imageTag) + throws IOException, RegistryException { return pullManifest(imageTag, ManifestTemplate.class); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index cd583c5381..fe0bdab4e4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -52,27 +54,32 @@ private static InputStream stringToInputStreamUtf8(String string) { return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); } - @Mock private Response mockResponse; - private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private final ManifestPuller testManifestPuller = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class); + @Mock private Response mockResponse; + @Test public void testHandleResponse_v21() throws URISyntaxException, IOException, UnknownManifestFormatException { Path v21ManifestFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); InputStream v21Manifest = new ByteArrayInputStream(Files.readAllBytes(v21ManifestFile)); + DescriptorDigest expectedDigest = Digests.computeDigest(v21Manifest).getDigest(); + v21Manifest.reset(); + Mockito.when(mockResponse.getBody()).thenReturn(v21Manifest); - ManifestTemplate manifestTemplate = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .handleResponse(mockResponse); - Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V21ManifestTemplate.class)); + Assert.assertThat( + manifestAndDigest.getManifest(), CoreMatchers.instanceOf(V21ManifestTemplate.class)); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test @@ -81,13 +88,18 @@ public void testHandleResponse_v22() Path v22ManifestFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); InputStream v22Manifest = new ByteArrayInputStream(Files.readAllBytes(v22ManifestFile)); + DescriptorDigest expectedDigest = Digests.computeDigest(v22Manifest).getDigest(); + v22Manifest.reset(); + Mockito.when(mockResponse.getBody()).thenReturn(v22Manifest); - ManifestTemplate manifestTemplate = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) .handleResponse(mockResponse); - Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestTemplate.class)); + Assert.assertThat( + manifestAndDigest.getManifest(), CoreMatchers.instanceOf(V22ManifestTemplate.class)); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test @@ -114,15 +126,19 @@ public void testHandleResponse_v22ManifestListFromParentType() Path v22ManifestListFile = Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + DescriptorDigest expectedDigest = Digests.computeDigest(v22ManifestList).getDigest(); + v22ManifestList.reset(); Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); - ManifestTemplate manifestTemplate = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) .handleResponse(mockResponse); + ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); Assert.assertTrue(((V22ManifestListTemplate) manifestTemplate).getManifests().size() > 0); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test @@ -132,16 +148,21 @@ public void testHandleResponse_v22ManifestList() Paths.get(Resources.getResource("core/json/v22manifest_list.json").toURI()); InputStream v22ManifestList = new ByteArrayInputStream(Files.readAllBytes(v22ManifestListFile)); + DescriptorDigest expectedDigest = Digests.computeDigest(v22ManifestList).getDigest(); + v22ManifestList.reset(); + Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); - V22ManifestListTemplate manifestTemplate = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestListTemplate.class) .handleResponse(mockResponse); + V22ManifestListTemplate manifestTemplate = manifestAndDigest.getManifest(); Assert.assertThat(manifestTemplate, CoreMatchers.instanceOf(V22ManifestListTemplate.class)); Assert.assertTrue(manifestTemplate.getManifests().size() > 0); + Assert.assertEquals(expectedDigest, manifestAndDigest.getDigest()); } @Test From 3193be52739c9f4d8932181b9e56887806933a24 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 14 Oct 2019 16:14:17 -0400 Subject: [PATCH 0773/2020] Add extra tags to tar image (#2054) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/ReproducibleImageTest.java | 29 +++++++-------- .../jib/builder/steps/LoadDockerStep.java | 20 +++-------- .../jib/builder/steps/WriteTarFileStep.java | 8 +++-- .../cloud/tools/jib/docker/DockerClient.java | 22 ------------ .../cloud/tools/jib/docker/ImageTarball.java | 14 ++++++-- .../json/DockerManifestEntryTemplate.java | 13 ++++--- .../tools/jib/docker/DockerClientTest.java | 36 ------------------- .../tools/jib/docker/ImageTarballTest.java | 15 ++++++-- ...a => DockerManifestEntryTemplateTest.java} | 5 ++- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 12 files changed, 59 insertions(+), 106 deletions(-) rename jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/{DockerManifestTemplateTest.java => DockerManifestEntryTemplateTest.java} (94%) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 9e73aa2c00..6241a39575 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) ## 0.11.0 diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 28af05adaf..31345a37bf 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -23,7 +23,6 @@ import com.google.common.io.CharStreams; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -111,26 +110,22 @@ public void testTarballStructure() throws IOException { @Test public void testManifest() throws IOException { - try (InputStream input = Files.newInputStream(imageTar.toPath())) { - String exectedManifest = - "[{\"config\":\"config.json\",\"repoTags\":[\"jib-core/reproducible:latest\"]," - + "\"layers\":[\"c46572ef74f58d95e44dd36c1fbdfebd3752e8b56a794a13c11cfed35a1a6e1c.tar.gz\",\"6d2763b0f3940d324ea6b55386429e5b173899608abf7d1bff62e25dd2e4dcea.tar.gz\",\"530c1954a2b087d0b989895ea56435c9dc739a973f2d2b6cb9bb98e55bbea7ac.tar.gz\"]}]"; - String generatedManifest = extractFromTarFileAsString(imageTar, "manifest.json"); - Assert.assertEquals(exectedManifest, generatedManifest); - } + String exectedManifest = + "[{\"config\":\"config.json\",\"repoTags\":[\"jib-core/reproducible:latest\"]," + + "\"layers\":[\"c46572ef74f58d95e44dd36c1fbdfebd3752e8b56a794a13c11cfed35a1a6e1c.tar.gz\",\"6d2763b0f3940d324ea6b55386429e5b173899608abf7d1bff62e25dd2e4dcea.tar.gz\",\"530c1954a2b087d0b989895ea56435c9dc739a973f2d2b6cb9bb98e55bbea7ac.tar.gz\"]}]"; + String generatedManifest = extractFromTarFileAsString(imageTar, "manifest.json"); + Assert.assertEquals(exectedManifest, generatedManifest); } @Test public void testConfiguration() throws IOException { - try (InputStream input = Files.newInputStream(imageTar.toPath())) { - String exectedConfig = - "{\"created\":\"1970-01-01T00:00:00Z\",\"architecture\":\"amd64\",\"os\":\"linux\"," - + "\"config\":{\"Env\":[],\"Entrypoint\":[\"echo\",\"Hello World\"],\"ExposedPorts\":{},\"Labels\":{},\"Volumes\":{}}," - + "\"history\":[{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"}]," - + "\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:18e4f44e6d1835bd968339b166057bd17ab7d4cbb56dc7262a5cafea7cf8d405\",\"sha256:13369c34f073f2b9c1fa6431e23d925f1a8eac65b1726c8cc8fcc2596c69b414\",\"sha256:4f92c507112d7880ca0f504ef8272b7fdee107263270125036a260a741565923\"]}}"; - String generatedConfig = extractFromTarFileAsString(imageTar, "config.json"); - Assert.assertEquals(exectedConfig, generatedConfig); - } + String exectedConfig = + "{\"created\":\"1970-01-01T00:00:00Z\",\"architecture\":\"amd64\",\"os\":\"linux\"," + + "\"config\":{\"Env\":[],\"Entrypoint\":[\"echo\",\"Hello World\"],\"ExposedPorts\":{},\"Labels\":{},\"Volumes\":{}}," + + "\"history\":[{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"},{\"created\":\"1970-01-01T00:00:00Z\",\"author\":\"Jib\",\"created_by\":\"jib-core:null\",\"comment\":\"\"}]," + + "\"rootfs\":{\"type\":\"layers\",\"diff_ids\":[\"sha256:18e4f44e6d1835bd968339b166057bd17ab7d4cbb56dc7262a5cafea7cf8d405\",\"sha256:13369c34f073f2b9c1fa6431e23d925f1a8eac65b1726c8cc8fcc2596c69b414\",\"sha256:4f92c507112d7880ca0f504ef8272b7fdee107263270125036a260a741565923\"]}}"; + String generatedConfig = extractFromTarFileAsString(imageTar, "config.json"); + Assert.assertEquals(exectedConfig, generatedConfig); } @Test diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index beeed1bb24..b1ab290b3d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -55,10 +54,11 @@ public BuildResult call() throws InterruptedException, IOException { try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, "Loading to Docker daemon")) { eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); - - ImageReference targetImageReference = - buildConfiguration.getTargetImageConfiguration().getImage(); - ImageTarball imageTarball = new ImageTarball(builtImage, targetImageReference); + ImageTarball imageTarball = + new ImageTarball( + builtImage, + buildConfiguration.getTargetImageConfiguration().getImage(), + buildConfiguration.getAllTargetImageTags()); // Note: The progress reported here is not entirely accurate. The total allocation units is // the size of the layers, but the progress being reported includes the config and manifest @@ -73,16 +73,6 @@ public BuildResult call() throws InterruptedException, IOException { eventHandlers.dispatch( LogEvent.debug(dockerClient.load(imageTarball, throttledProgressReporter))); - // Tags the image with all the additional tags, skipping the one 'docker load' already - // loaded. - for (String tag : buildConfiguration.getAllTargetImageTags()) { - if (tag.equals(targetImageReference.getTag())) { - continue; - } - - dockerClient.tag(targetImageReference, targetImageReference.withTag(tag)); - } - return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 4dd97f91f4..59011904db 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -28,7 +28,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; public class WriteTarFileStep implements Callable { @@ -50,7 +49,7 @@ public class WriteTarFileStep implements Callable { } @Override - public BuildResult call() throws ExecutionException, InterruptedException, IOException { + public BuildResult call() throws IOException { buildConfiguration .getEventHandlers() .dispatch(LogEvent.progress("Building image to tar file...")); @@ -63,7 +62,10 @@ public BuildResult call() throws ExecutionException, InterruptedException, IOExc } try (OutputStream outputStream = new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { - new ImageTarball(builtImage, buildConfiguration.getTargetImageConfiguration().getImage()) + new ImageTarball( + builtImage, + buildConfiguration.getTargetImageConfiguration().getImage(), + buildConfiguration.getAllTargetImageTags()) .writeTo(outputStream); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index c990b883e2..b23060c870 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -243,28 +243,6 @@ public void save( } } - /** - * Tags the image referenced by {@code originalImageReference} with a new image reference {@code - * newImageReference}. - * - * @param originalImageReference the existing image reference on the Docker daemon - * @param newImageReference the new image reference - * @see https://docs.docker.com/engine/reference/commandline/tag/ - * @throws InterruptedException if the 'docker tag' process is interrupted - * @throws IOException if an I/O exception occurs or {@code docker tag} failed - */ - public void tag(ImageReference originalImageReference, ImageReference newImageReference) - throws IOException, InterruptedException { - // Runs 'docker tag'. - Process dockerProcess = - docker("tag", originalImageReference.toString(), newImageReference.toString()); - if (dockerProcess.waitFor() != 0) { - throw new IOException( - "'docker tag' command failed with error: " + getStderrOutput(dockerProcess)); - } - } - /** * Gets the size, image ID, and diff IDs of an image in the Docker daemon. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java index 8b250029c7..fd23ec4721 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java @@ -24,6 +24,7 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; @@ -42,16 +43,21 @@ public class ImageTarball { private final Image image; private final ImageReference imageReference; + private final ImmutableSet allTargetImageTags; /** * Instantiate with an {@link Image}. * * @param image the image to convert into a tarball - * @param imageReference image reference to set in the manifest + * @param imageReference image reference to set in the manifest (note that the tag portion of the + * image reference is ignored) + * @param allTargetImageTags the tags to tag the image with */ - public ImageTarball(Image image, ImageReference imageReference) { + public ImageTarball( + Image image, ImageReference imageReference, ImmutableSet allTargetImageTags) { this.image = image; this.imageReference = imageReference; + this.allTargetImageTags = allTargetImageTags; } public void writeTo(OutputStream out) throws IOException { @@ -75,7 +81,9 @@ public void writeTo(OutputStream out) throws IOException { CONTAINER_CONFIGURATION_JSON_FILE_NAME); // Adds the manifest to tarball. - manifestTemplate.setRepoTags(imageReference.toStringWithTag()); + for (String tag : allTargetImageTags) { + manifestTemplate.addRepoTag(imageReference.withTag(tag).toStringWithTag()); + } tarStreamBuilder.addByteEntry( JsonTemplateMapper.toByteArray(Collections.singletonList(manifestTemplate)), MANIFEST_JSON_FILE_NAME); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java index b131321a9b..ea93f95d64 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java @@ -17,8 +17,8 @@ package com.google.cloud.tools.jib.docker.json; import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -48,15 +48,15 @@ public class DockerManifestEntryTemplate implements JsonTemplate { private String config = "config.json"; - private List repoTags = Collections.singletonList(null); + private final List repoTags = new ArrayList<>(); private final List layers = new ArrayList<>(); public void setConfig(String config) { this.config = config; } - public void setRepoTags(String repoTags) { - this.repoTags = Collections.singletonList(repoTags); + public void addRepoTag(String repoTag) { + repoTags.add(repoTag); } public void addLayerFile(String layer) { @@ -70,4 +70,9 @@ public String getConfig() { public List getLayerFiles() { return layers; } + + @VisibleForTesting + public List getRepoTags() { + return repoTags; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 8d110776cd..b255ccf63c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -18,7 +18,6 @@ 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.docker.DockerClient.DockerImageDetails; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; @@ -209,19 +208,6 @@ public void testSave_fail() throws InterruptedException { } } - @Test - public void testTag() throws InterruptedException, IOException, InvalidImageReferenceException { - DockerClient testDockerClient = - new DockerClient( - subcommand -> { - Assert.assertEquals(Arrays.asList("tag", "original", "new"), subcommand); - return mockProcessBuilder; - }); - Mockito.when(mockProcess.waitFor()).thenReturn(0); - - testDockerClient.tag(ImageReference.of(null, "original", null), ImageReference.parse("new")); - } - @Test public void testDefaultProcessorBuilderFactory_customExecutable() { ProcessBuilder processBuilder = @@ -247,28 +233,6 @@ public void testDefaultProcessorBuilderFactory_customEnvironment() { Assert.assertEquals(expectedEnvironment, processBuilder.environment()); } - @Test - public void testTag_fail() throws InterruptedException, InvalidImageReferenceException { - DockerClient testDockerClient = - new DockerClient( - subcommand -> { - Assert.assertEquals(Arrays.asList("tag", "original", "new"), subcommand); - return mockProcessBuilder; - }); - Mockito.when(mockProcess.waitFor()).thenReturn(1); - - Mockito.when(mockProcess.getErrorStream()) - .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); - - try { - testDockerClient.tag(ImageReference.of(null, "original", null), ImageReference.parse("new")); - Assert.fail("docker tag should have failed"); - - } catch (IOException ex) { - Assert.assertEquals("'docker tag' command failed with error: error", ex.getMessage()); - } - } - @Test public void testSize_fail() throws InterruptedException { DockerClient testDockerClient = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java index 630fd6988e..6455d73e97 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java @@ -28,6 +28,8 @@ import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.io.CharStreams; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; @@ -82,8 +84,11 @@ public void testWriteTo() Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); Image testImage = Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); - - ImageTarball imageToTarball = new ImageTarball(testImage, ImageReference.parse("my/image:tag")); + ImageTarball imageToTarball = + new ImageTarball( + testImage, + ImageReference.parse("my/image:tag"), + ImmutableSet.of("tag", "another-tag", "tag3")); ByteArrayOutputStream out = new ByteArrayOutputStream(); imageToTarball.writeTo(out); @@ -120,7 +125,11 @@ public void testWriteTo() String manifestJson = CharStreams.toString( new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); - JsonTemplateMapper.readListOfJson(manifestJson, DockerManifestEntryTemplate.class); + DockerManifestEntryTemplate manifest = + JsonTemplateMapper.readListOfJson(manifestJson, DockerManifestEntryTemplate.class).get(0); + Assert.assertEquals( + ImmutableList.of("my/image:tag", "my/image:another-tag", "my/image:tag3"), + manifest.getRepoTags()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java similarity index 94% rename from jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java index e338cfbb3d..cdebbc02f3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java @@ -33,7 +33,7 @@ import org.junit.Test; /** Tests for {@link DockerManifestEntryTemplate}. */ -public class DockerManifestTemplateTest { +public class DockerManifestEntryTemplateTest { @Test public void testToJson() throws URISyntaxException, IOException { @@ -42,8 +42,7 @@ public void testToJson() throws URISyntaxException, IOException { String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); DockerManifestEntryTemplate template = new DockerManifestEntryTemplate(); - template.setRepoTags( - ImageReference.of("testregistry", "testrepo", "testtag").toStringWithTag()); + template.addRepoTag(ImageReference.of("testregistry", "testrepo", "testtag").toStringWithTag()); template.addLayerFile("layer1.tar.gz"); template.addLayerFile("layer2.tar.gz"); template.addLayerFile("layer3.tar.gz"); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 6a2d92f413..025e706b2b 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) ## 1.6.1 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 5e0925d389..ce39d41eca 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) +- Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) ## 1.6.1 From 286e3d9a7e9877f26aef43968c4a9c386895c9bc Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 14 Oct 2019 18:17:14 -0400 Subject: [PATCH 0774/2020] Combine local base image steps (#2044) --- .../jib/builder/steps/ExtractTarStep.java | 224 --------------- .../builder/steps/LocalBaseImageSteps.java | 258 ++++++++++++++++++ .../jib/builder/steps/SaveDockerStep.java | 69 ----- .../tools/jib/builder/steps/StepsRunner.java | 39 ++- ...Test.java => LocalBaseImageStepsTest.java} | 48 ++-- 5 files changed, 294 insertions(+), 344 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java rename jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/{ExtractTarStepTest.java => LocalBaseImageStepsTest.java} (75%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java deleted file mode 100644 index 8e996e292c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStep.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.blob.Blob; -import com.google.cloud.tools.jib.blob.BlobDescriptor; -import com.google.cloud.tools.jib.blob.Blobs; -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; -import com.google.cloud.tools.jib.cache.Cache; -import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; -import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; -import com.google.cloud.tools.jib.http.NotifyingOutputStream; -import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.LayerCountMismatchException; -import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; -import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; -import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; -import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.cloud.tools.jib.tar.TarExtractor; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.ByteStreams; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** Extracts a tar file base image. */ -public class ExtractTarStep implements Callable { - - /** Contains an {@link Image} and its layers. * */ - static class LocalImage { - final Image baseImage; - final List layers; - - LocalImage(Image baseImage, List layers) { - this.baseImage = baseImage; - this.layers = layers; - } - } - - /** - * Checks the first two bytes of a file to see if it has been gzipped. - * - * @param path the file to check - * @return {@code true} if the file is gzipped, {@code false} if not - * @throws IOException if reading the file fails - * @see GZIP file format - */ - @VisibleForTesting - static boolean isGzipped(Path path) throws IOException { - try (InputStream inputStream = Files.newInputStream(path)) { - inputStream.mark(2); - int magic = (inputStream.read() & 0xff) | ((inputStream.read() << 8) & 0xff00); - return magic == GZIPInputStream.GZIP_MAGIC; - } - } - - private final ExecutorService executorService; - private final BuildConfiguration buildConfiguration; - private final Path tarPath; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final TempDirectoryProvider tempDirectoryProvider; - - ExtractTarStep( - ExecutorService executorService, - BuildConfiguration buildConfiguration, - Path tarPath, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - TempDirectoryProvider tempDirectoryProvider) { - this.executorService = executorService; - this.buildConfiguration = buildConfiguration; - this.tarPath = tarPath; - this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.tempDirectoryProvider = tempDirectoryProvider; - } - - @Override - public LocalImage call() - throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, - ExecutionException, InterruptedException { - Path destination = tempDirectoryProvider.newDirectory(); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), - "Extracting tar " + tarPath + " into " + destination)) { - TarExtractor.extract(tarPath, destination); - - InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); - DockerManifestEntryTemplate loadManifest = - new ObjectMapper() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) - .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; - manifestStream.close(); - ContainerConfigurationTemplate configurationTemplate = - JsonTemplateMapper.readJsonFromFile( - destination.resolve(loadManifest.getConfig()), ContainerConfigurationTemplate.class); - - List layerFiles = loadManifest.getLayerFiles(); - if (configurationTemplate.getLayerCount() != layerFiles.size()) { - throw new LayerCountMismatchException( - "Invalid base image format: manifest contains " - + layerFiles.size() - + " layers, but container configuration contains " - + configurationTemplate.getLayerCount() - + " layers"); - } - - // Check the first layer to see if the layers are compressed already. 'docker save' output is - // uncompressed, but a jib-built tar has compressed layers. - boolean layersAreCompressed = - layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); - - // Process layer blobs - // TODO: Optimize; compressing/calculating layer digests is slow - // e.g. faster compression method - try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - "processing base image layers", layerFiles.size())) { - List layers = new ArrayList<>(layerFiles.size()); - V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); - - // Start compressing layers in parallel - List> cachedLayers = new ArrayList<>(); - for (int index = 0; index < layerFiles.size(); index++) { - Path layerFile = destination.resolve(layerFiles.get(index)); - DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); - ProgressEventDispatcher.Factory progressEventDispatcherFactory = - progressEventDispatcher.newChildProducer(); - - cachedLayers.add( - executorService.submit( - () -> - getCachedTarLayer( - diffId, layerFile, layersAreCompressed, progressEventDispatcherFactory))); - } - - // Collect compressed layers and add to manifest - for (Future layerFuture : cachedLayers) { - CachedLayer layer = layerFuture.get(); - layers.add(new PreparedLayer.Builder(layer).build()); - v22Manifest.addLayer(layer.getSize(), layer.getDigest()); - } - - BlobDescriptor configDescriptor = - Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); - v22Manifest.setContainerConfiguration( - configDescriptor.getSize(), configDescriptor.getDigest()); - Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); - return new LocalImage(image, layers); - } - } - } - - private CachedLayer getCachedTarLayer( - DescriptorDigest diffId, - Path layerFile, - boolean layersAreCompressed, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) - throws IOException, CacheCorruptedException { - try (ProgressEventDispatcher childDispatcher = - progressEventDispatcherFactory.create( - "compressing layer " + diffId, Files.size(layerFile)); - ThrottledAccumulatingConsumer throttledProgressReporter = - new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { - Cache cache = buildConfiguration.getBaseImageLayersCache(); - - // Retrieve pre-compressed layer from cache - Optional optionalLayer = cache.retrieveTarLayer(diffId); - if (optionalLayer.isPresent()) { - return optionalLayer.get(); - } - - // Just write layers that are already compressed - if (layersAreCompressed) { - return cache.writeTarLayer(diffId, Blobs.from(layerFile)); - } - - // Compress uncompressed layers while writing - Blob compressedBlob = - Blobs.from( - outputStream -> { - try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream); - NotifyingOutputStream notifyingOutputStream = - new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { - Blobs.from(layerFile).writeTo(notifyingOutputStream); - } - }); - return cache.writeTarLayer(diffId, compressedBlob); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java new file mode 100644 index 0000000000..f1615672fc --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -0,0 +1,258 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.Blob; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.blob.Blobs; +import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; +import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; +import com.google.cloud.tools.jib.cache.CachedLayer; +import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; +import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.http.NotifyingOutputStream; +import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; +import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.cloud.tools.jib.tar.TarExtractor; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** Extracts a tar file base image. */ +public class LocalBaseImageSteps { + + /** Contains an {@link Image} and its layers. * */ + static class LocalImage { + final Image baseImage; + final List layers; + + LocalImage(Image baseImage, List layers) { + this.baseImage = baseImage; + this.layers = layers; + } + } + + /** + * Checks the first two bytes of a file to see if it has been gzipped. + * + * @param path the file to check + * @return {@code true} if the file is gzipped, {@code false} if not + * @throws IOException if reading the file fails + * @see GZIP file format + */ + @VisibleForTesting + static boolean isGzipped(Path path) throws IOException { + try (InputStream inputStream = Files.newInputStream(path)) { + inputStream.mark(2); + int magic = (inputStream.read() & 0xff) | ((inputStream.read() << 8) & 0xff00); + return magic == GZIPInputStream.GZIP_MAGIC; + } + } + + static Callable retrieveDockerDaemonImageStep( + ExecutorService executorService, + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + DockerClient dockerClient) { + return () -> { + ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create("processing base image " + imageReference, 2); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), + "Saving " + imageReference + " from Docker daemon")) { + Path tarPath = tempDirectoryProvider.newDirectory().resolve("out.tar"); + long size = dockerClient.inspect(imageReference).getSize(); + try (ProgressEventDispatcher dockerProgress = + progressEventDispatcher + .newChildProducer() + .create("saving base image " + imageReference, size); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(dockerProgress::dispatchProgress)) { + dockerClient.save(imageReference, tarPath, throttledProgressReporter); + } + + return cacheDockerImageTar( + buildConfiguration, + executorService, + tarPath, + progressEventDispatcher.newChildProducer()); + } + }; + } + + static Callable retrieveTarImageStep( + ExecutorService executorService, + BuildConfiguration buildConfiguration, + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + Path tarPath) { + return () -> + cacheDockerImageTar( + buildConfiguration, executorService, tarPath, progressEventDispatcherFactory); + } + + @VisibleForTesting + static LocalImage cacheDockerImageTar( + BuildConfiguration buildConfiguration, + ExecutorService executorService, + Path tarPath, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) + throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, + ExecutionException, InterruptedException { + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + Path destination = tempDirectoryProvider.newDirectory(); + + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), + "Extracting tar " + tarPath + " into " + destination)) { + TarExtractor.extract(tarPath, destination); + + InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); + DockerManifestEntryTemplate loadManifest = + new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; + manifestStream.close(); + ContainerConfigurationTemplate configurationTemplate = + JsonTemplateMapper.readJsonFromFile( + destination.resolve(loadManifest.getConfig()), + ContainerConfigurationTemplate.class); + + List layerFiles = loadManifest.getLayerFiles(); + if (configurationTemplate.getLayerCount() != layerFiles.size()) { + throw new LayerCountMismatchException( + "Invalid base image format: manifest contains " + + layerFiles.size() + + " layers, but container configuration contains " + + configurationTemplate.getLayerCount() + + " layers"); + } + + // Check the first layer to see if the layers are compressed already. 'docker save' output + // is uncompressed, but a jib-built tar has compressed layers. + boolean layersAreCompressed = + layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); + + // Process layer blobs + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "processing base image layers", layerFiles.size())) { + List layers = new ArrayList<>(layerFiles.size()); + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + + // Start compressing layers in parallel + List> cachedLayers = new ArrayList<>(); + for (int index = 0; index < layerFiles.size(); index++) { + Path layerFile = destination.resolve(layerFiles.get(index)); + DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); + ProgressEventDispatcher.Factory layerProgressDispatcherFactory = + progressEventDispatcher.newChildProducer(); + cachedLayers.add( + executorService.submit( + () -> + compressAndCacheTarLayer( + buildConfiguration.getBaseImageLayersCache(), + diffId, + layerFile, + layersAreCompressed, + layerProgressDispatcherFactory))); + } + + // Collect compressed layers and add to manifest + for (Future layerFuture : cachedLayers) { + CachedLayer layer = layerFuture.get(); + layers.add(new PreparedLayer.Builder(layer).build()); + v22Manifest.addLayer(layer.getSize(), layer.getDigest()); + } + + BlobDescriptor configDescriptor = + Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); + v22Manifest.setContainerConfiguration( + configDescriptor.getSize(), configDescriptor.getDigest()); + Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); + return new LocalImage(image, layers); + } + } + } + } + + private static CachedLayer compressAndCacheTarLayer( + Cache cache, + DescriptorDigest diffId, + Path layerFile, + boolean layersAreCompressed, + ProgressEventDispatcher.Factory progressEventDispatcherFactory) + throws IOException, CacheCorruptedException { + try (ProgressEventDispatcher childDispatcher = + progressEventDispatcherFactory.create( + "compressing layer " + diffId, Files.size(layerFile)); + ThrottledAccumulatingConsumer throttledProgressReporter = + new ThrottledAccumulatingConsumer(childDispatcher::dispatchProgress)) { + // Retrieve pre-compressed layer from cache + Optional optionalLayer = cache.retrieveTarLayer(diffId); + if (optionalLayer.isPresent()) { + return optionalLayer.get(); + } + + // Just write layers that are already compressed + if (layersAreCompressed) { + return cache.writeTarLayer(diffId, Blobs.from(layerFile)); + } + + // Compress uncompressed layers while writing + Blob compressedBlob = + Blobs.from( + outputStream -> { + try (GZIPOutputStream compressorStream = new GZIPOutputStream(outputStream); + NotifyingOutputStream notifyingOutputStream = + new NotifyingOutputStream(compressorStream, throttledProgressReporter)) { + Blobs.from(layerFile).writeTo(notifyingOutputStream); + } + }); + return cache.writeTarLayer(diffId, compressedBlob); + } + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java deleted file mode 100644 index 40132c3982..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/SaveDockerStep.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2019 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; -import java.io.IOException; -import java.nio.file.Path; -import java.security.DigestException; -import java.util.concurrent.Callable; - -/** Saves an image from the docker daemon. */ -public class SaveDockerStep implements Callable { - - private final BuildConfiguration buildConfiguration; - private final DockerClient dockerClient; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - private final TempDirectoryProvider tempDirectoryProvider; - - SaveDockerStep( - BuildConfiguration buildConfiguration, - DockerClient dockerClient, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - TempDirectoryProvider tempDirectoryProvider) { - this.buildConfiguration = buildConfiguration; - this.dockerClient = dockerClient; - this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.tempDirectoryProvider = tempDirectoryProvider; - } - - @Override - public Path call() throws IOException, InterruptedException, DigestException { - Path outputDir = tempDirectoryProvider.newDirectory(); - Path outputPath = outputDir.resolve("out.tar"); - ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); - try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), - "Saving " + imageReference + " from Docker daemon")) { - long size = dockerClient.inspect(imageReference).getSize(); - try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create("saving base image " + imageReference, size); - ThrottledAccumulatingConsumer throttledProgressReporter = - new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { - dockerClient.save(imageReference, outputPath, throttledProgressReporter); - } - return outputPath; - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index a519e0ab0b..2b9877076b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; @@ -62,7 +62,6 @@ private static Future failedFuture() { new IllegalStateException("invalid usage; required step not configured")); } - private Future tarPath = failedFuture(); private Future baseImageAndAuth = failedFuture(); private Future>> baseImageLayers = failedFuture(); @Nullable private List> applicationLayers; @@ -194,13 +193,11 @@ private void addRetrievalSteps(boolean layersRequiredLocally) { if (baseImageConfiguration.getTarPath().isPresent()) { // If tarPath is present, a TarImage was used - results.tarPath = Futures.immediateFuture(baseImageConfiguration.getTarPath().get()); stepsToRun.add(this::extractTar); } else if (baseImageConfiguration.getDockerClient().isPresent()) { // If dockerClient is present, a DockerDaemonImage was used stepsToRun.add(this::saveDocker); - stepsToRun.add(this::extractTar); } else { // Otherwise default to RegistryImage @@ -234,35 +231,35 @@ private void authenticatePush() { } private void saveDocker() { - ProgressEventDispatcher.Factory childProgressDispatcherFactory = - Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); - Optional dockerClient = buildConfiguration.getBaseImageConfiguration().getDockerClient(); Preconditions.checkArgument(dockerClient.isPresent()); - - results.tarPath = + ProgressEventDispatcher.Factory childProgressDispatcherFactory = + Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); + assignLocalImageResult( executorService.submit( - new SaveDockerStep( + LocalBaseImageSteps.retrieveDockerDaemonImageStep( + executorService, buildConfiguration, - dockerClient.get(), childProgressDispatcherFactory, - tempDirectoryProvider)); + dockerClient.get()))); } private void extractTar() { + Optional tarPath = buildConfiguration.getBaseImageConfiguration().getTarPath(); + Preconditions.checkArgument(tarPath.isPresent()); ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); - Future localImageFuture = + assignLocalImageResult( executorService.submit( - () -> - new ExtractTarStep( - executorService, - buildConfiguration, - results.tarPath.get(), - childProgressDispatcherFactory, - tempDirectoryProvider) - .call()); + LocalBaseImageSteps.retrieveTarImageStep( + executorService, + buildConfiguration, + childProgressDispatcherFactory, + tarPath.get()))); + } + + private void assignLocalImageResult(Future localImageFuture) { results.baseImageAndAuth = executorService.submit( () -> new ImageAndAuthorization(localImageFuture.get().baseImage, null)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java similarity index 75% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index c5cdf42d1a..1741e31b8c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ExtractTarStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -17,20 +17,16 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.ExtractTarStep.LocalImage; +import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; -import com.google.cloud.tools.jib.image.LayerCountMismatchException; -import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.ExecutionException; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -42,7 +38,7 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class ExtractTarStepTest { +public class LocalBaseImageStepsTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -70,19 +66,14 @@ public void setup() throws IOException { } @Test - public void testCall_validDocker() - throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException, ExecutionException, - InterruptedException { + public void testCacheDockerImageTar_validDocker() throws Exception { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = - new ExtractTarStep( - MoreExecutors.newDirectExecutorService(), - buildConfiguration, - dockerBuild, - progressEventDispatcherFactory, - new TempDirectoryProvider()) - .call(); + LocalBaseImageSteps.cacheDockerImageTar( + buildConfiguration, + MoreExecutors.newDirectExecutorService(), + dockerBuild, + progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); @@ -102,19 +93,14 @@ public void testCall_validDocker() } @Test - public void testCall_validTar() - throws URISyntaxException, LayerCountMismatchException, - BadContainerConfigurationFormatException, IOException, ExecutionException, - InterruptedException { + public void testCacheDockerImageTar_validTar() throws Exception { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = - new ExtractTarStep( - MoreExecutors.newDirectExecutorService(), - buildConfiguration, - tarBuild, - progressEventDispatcherFactory, - new TempDirectoryProvider()) - .call(); + LocalBaseImageSteps.cacheDockerImageTar( + buildConfiguration, + MoreExecutors.newDirectExecutorService(), + tarBuild, + progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); @@ -135,7 +121,9 @@ public void testCall_validTar() @Test public void testIsGzipped() throws URISyntaxException, IOException { - Assert.assertTrue(ExtractTarStep.isGzipped(getResource("core/extraction/compressed.tar.gz"))); - Assert.assertFalse(ExtractTarStep.isGzipped(getResource("core/extraction/not-compressed.tar"))); + Assert.assertTrue( + LocalBaseImageSteps.isGzipped(getResource("core/extraction/compressed.tar.gz"))); + Assert.assertFalse( + LocalBaseImageSteps.isGzipped(getResource("core/extraction/not-compressed.tar"))); } } From 14627630a1e3e8010b9a76d29532abcccc65c8c0 Mon Sep 17 00:00:00 2001 From: Tejas Kasetty Date: Wed, 16 Oct 2019 00:18:24 +0530 Subject: [PATCH 0775/2020] Report base image digest (#1884) (#2063) Base image digest is reported/logged only when the manifest schema version is 2. --- .../jib/builder/steps/PullBaseImageStep.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 4306c91e71..59df8659c8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -201,30 +201,29 @@ private Image pullBaseImage( ProgressEventDispatcher progressEventDispatcher) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { + EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); RegistryClient registryClient = buildConfiguration .newBaseImageRegistryClientFactory() .setAuthorization(registryAuthorization) .newRegistryClient(); - ManifestTemplate manifestTemplate = - registryClient - .pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()) - .getManifest(); + ManifestAndDigest manifestAndDigest = + registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); + ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); // special handling if we happen upon a manifest list, redirect to a manifest and continue // handling it normally if (manifestTemplate instanceof V22ManifestListTemplate) { - buildConfiguration - .getEventHandlers() - .dispatch( - LogEvent.lifecycle( - "The base image reference is manifest list, searching for linux/amd64")); - manifestTemplate = + eventHandlers.dispatch( + LogEvent.lifecycle( + "The base image reference is manifest list, searching for linux/amd64")); + manifestAndDigest = obtainPlatformSpecificImageManifest( - registryClient, (V22ManifestListTemplate) manifestTemplate) - .getManifest(); + registryClient, (V22ManifestListTemplate) manifestTemplate); + manifestTemplate = manifestAndDigest.getManifest(); } + switch (manifestTemplate.getSchemaVersion()) { case 1: V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; @@ -235,6 +234,8 @@ private Image pullBaseImage( return JsonToImageTranslator.toImage(v21ManifestTemplate); case 2: + eventHandlers.dispatch( + LogEvent.lifecycle("Using base image with digest: " + manifestAndDigest.getDigest())); BuildableManifestTemplate buildableManifestTemplate = (BuildableManifestTemplate) manifestTemplate; if (buildableManifestTemplate.getContainerConfiguration() == null From d92ddc7cb6fe05698064343392e7b09de54069cc Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Oct 2019 12:09:02 -0400 Subject: [PATCH 0776/2020] Fix tar base image failing with missing explicit directory entries (#2068) --- jib-core/CHANGELOG.md | 1 + .../cloud/tools/jib/tar/TarExtractor.java | 3 +++ .../cloud/tools/jib/tar/TarExtractorTest.java | 16 ++++++++++++++++ .../resources/core/extract-missing-dirs.tar | Bin 0 -> 3072 bytes jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 6 files changed, 22 insertions(+) create mode 100644 jib-core/src/test/resources/core/extract-missing-dirs.tar diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 6241a39575..64b602336e 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) - Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) +- Fixed `TarImage` base image failing if tar does not contain explicit directory entries. ([#2067](https://github.com/GoogleContainerTools/jib/issues/2067)) ## 0.11.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java index 71261a4c94..6e3708b388 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java @@ -58,6 +58,9 @@ public static void extract(Path source, Path destination) throws IOException { if (entry.isDirectory()) { Files.createDirectories(entryPath); } else { + if (entryPath.getParent() != null) { + Files.createDirectories(entryPath.getParent()); + } try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { ByteStreams.copy(tarArchiveInputStream, out); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java index cfa67a3fbc..6a806abe20 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java @@ -50,4 +50,20 @@ public void testExtract() throws URISyntaxException, IOException { Assert.assertEquals("Hello", contents); } } + + @Test + public void testExtract_missingDirectoryEntries() throws URISyntaxException, IOException { + Path source = Paths.get(Resources.getResource("core/extract-missing-dirs.tar").toURI()); + Path destination = temporaryFolder.getRoot().toPath(); + TarExtractor.extract(source, destination); + + Assert.assertTrue(Files.exists(destination.resolve("world"))); + Assert.assertTrue( + Files.exists(destination.resolve("a").resolve("b").resolve("c").resolve("world"))); + + try (Stream lines = Files.lines(destination.resolve("world"))) { + String contents = lines.collect(Collectors.joining()); + Assert.assertEquals("world", contents); + } + } } diff --git a/jib-core/src/test/resources/core/extract-missing-dirs.tar b/jib-core/src/test/resources/core/extract-missing-dirs.tar new file mode 100644 index 0000000000000000000000000000000000000000..4fb03f1c9a3a165f117bc2e045eda562e19ed739 GIT binary patch literal 3072 zcmeH`F%Ezr3`IE#H=sYI+y`|uF+q*j*XTmnh=DM)3#0?T|9u+W Date: Wed, 16 Oct 2019 15:26:04 -0400 Subject: [PATCH 0777/2020] Skip docker save if docker daemon base image is cached (#2049) --- jib-core/CHANGELOG.md | 1 + .../builder/steps/LocalBaseImageSteps.java | 54 ++++++++++++++- .../google/cloud/tools/jib/cache/Cache.java | 2 +- .../cloud/tools/jib/docker/DockerClient.java | 10 +++ .../steps/LocalBaseImageStepsTest.java | 62 ++++++++++++++++++ ...5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f | Bin 0 -> 1417 bytes ...abcfc3165abe13fb0a157640fa8cb7af81077670c0 | 1 + ...5666721cff7b86ce978c203a958d1fc86ee6c19f94 | Bin 0 -> 2319 bytes jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 10 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f create mode 100644 jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 create mode 100644 jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 64b602336e..94e93dabb7 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) - The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `DockerDaemonImage` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java index f1615672fc..8c8798f045 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -30,9 +30,11 @@ import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -48,6 +50,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.security.DigestException; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -103,6 +106,13 @@ static Callable retrieveDockerDaemonImageStep( new TimerEventDispatcher( buildConfiguration.getEventHandlers(), "Saving " + imageReference + " from Docker daemon")) { + DockerClient.DockerImageDetails dockerImageDetails = dockerClient.inspect(imageReference); + Optional cachedImage = + getCachedDockerImage(buildConfiguration.getBaseImageLayersCache(), dockerImageDetails); + if (cachedImage.isPresent()) { + return cachedImage.get(); + } + Path tarPath = tempDirectoryProvider.newDirectory().resolve("out.tar"); long size = dockerClient.inspect(imageReference).getSize(); try (ProgressEventDispatcher dockerProgress = @@ -133,6 +143,39 @@ static Callable retrieveTarImageStep( buildConfiguration, executorService, tarPath, progressEventDispatcherFactory); } + @VisibleForTesting + static Optional getCachedDockerImage( + Cache cache, DockerImageDetails dockerImageDetails) + throws DigestException, IOException, CacheCorruptedException, LayerCountMismatchException, + BadContainerConfigurationFormatException { + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + List cachedLayers = new ArrayList<>(); + + // Get config + Optional cachedConfig = + cache.retrieveLocalConfig(dockerImageDetails.getImageId()); + if (!cachedConfig.isPresent()) { + return Optional.empty(); + } + + // Get layers + for (DescriptorDigest diffId : dockerImageDetails.getDiffIds()) { + Optional cachedLayer = cache.retrieveTarLayer(diffId); + if (!cachedLayer.isPresent()) { + return Optional.empty(); + } + CachedLayer layer = cachedLayer.get(); + cachedLayers.add(new PreparedLayer.Builder(layer).build()); + v22Manifest.addLayer(layer.getSize(), layer.getDigest()); + } + + // Create manifest + BlobDescriptor configDescriptor = Digests.computeDigest(cachedConfig.get()); + v22Manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); + Image image = JsonToImageTranslator.toImage(v22Manifest, cachedConfig.get()); + return Optional.of(new LocalImage(image, cachedLayers)); + } + @VisibleForTesting static LocalImage cacheDockerImageTar( BuildConfiguration buildConfiguration, @@ -156,10 +199,12 @@ static LocalImage cacheDockerImageTar( .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; manifestStream.close(); + + Path configPath = destination.resolve(loadManifest.getConfig()); ContainerConfigurationTemplate configurationTemplate = - JsonTemplateMapper.readJsonFromFile( - destination.resolve(loadManifest.getConfig()), - ContainerConfigurationTemplate.class); + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + BlobDescriptor originalConfigDescriptor = + Blobs.from(configPath).writeTo(ByteStreams.nullOutputStream()); List layerFiles = loadManifest.getLayerFiles(); if (configurationTemplate.getLayerCount() != layerFiles.size()) { @@ -170,6 +215,9 @@ static LocalImage cacheDockerImageTar( + configurationTemplate.getLayerCount() + " layers"); } + buildConfiguration + .getBaseImageLayersCache() + .writeLocalConfig(originalConfigDescriptor.getDigest(), configurationTemplate); // Check the first layer to see if the layers are compressed already. 'docker save' output // is uncompressed, but a jib-built tar has compressed layers. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java index f8014a2873..b243d7a255 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java @@ -143,7 +143,7 @@ public CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) * @param containerConfiguration the container configuration * @throws IOException if an I/O exception occurs */ - void writeLocalConfig( + public void writeLocalConfig( DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) throws IOException { cacheStorageWriter.writeLocalConfig(imageId, containerConfiguration); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index b23060c870..2ae05e43ed 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -55,6 +55,16 @@ public static class DockerImageDetails implements JsonTemplate { private String imageId = ""; private List diffIds = Collections.emptyList(); + // Required for JSON + public DockerImageDetails() {} + + @VisibleForTesting + public DockerImageDetails(long size, String imageId, List diffIds) { + this.size = size; + this.imageId = imageId; + this.diffIds = diffIds; + } + public long getSize() { return size; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index 1741e31b8c..b7c878c7ad 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -19,14 +19,23 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; import com.google.cloud.tools.jib.cache.Cache; +import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.image.LayerCountMismatchException; +import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.DigestException; +import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -119,6 +128,59 @@ public void testCacheDockerImageTar_validTar() throws Exception { Assert.assertEquals("value1", result.baseImage.getLabels().get("label1")); } + @Test + public void testGetCachedDockerImage() + throws IOException, DigestException, BadContainerConfigurationFormatException, + CacheCorruptedException, LayerCountMismatchException, URISyntaxException { + DockerImageDetails dockerImageDetails = + new DockerImageDetails( + 0, + "sha256:066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0", + ImmutableList.of( + "sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", + "sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e")); + Path cachePath = temporaryFolder.newFolder("cache").toPath(); + Files.createDirectories(cachePath.resolve("local/config")); + Cache cache = Cache.withDirectory(cachePath); + + // Image not in cache + Optional localImage = + LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // Config in cache, but not layers + String configHash = "066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0"; + Files.copy( + getResource("core/extraction/test-cache/local/config/" + configHash), + cachePath.resolve("local/config/" + configHash)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // One layer missing + String diffId = "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd"; + String digest = "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f"; + Files.createDirectories(cachePath.resolve("local").resolve(diffId)); + Files.copy( + getResource("core/extraction/test-cache/local/" + diffId + "/" + digest), + cachePath.resolve("local").resolve(diffId).resolve(digest)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertFalse(localImage.isPresent()); + + // Image fully in cache + diffId = "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e"; + digest = "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94"; + Files.createDirectories(cachePath.resolve("local").resolve(diffId)); + Files.copy( + getResource("core/extraction/test-cache/local/" + diffId + "/" + digest), + cachePath.resolve("local").resolve(diffId).resolve(digest)); + localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); + Assert.assertTrue(localImage.isPresent()); + LocalImage image = localImage.get(); + Assert.assertEquals( + ImmutableMap.of("label1", "value1", "label2", "value2"), image.baseImage.getLabels()); + Assert.assertEquals(2, image.layers.size()); + } + @Test public void testIsGzipped() throws URISyntaxException, IOException { Assert.assertTrue( diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f b/jib-core/src/test/resources/core/extraction/test-cache/local/5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f new file mode 100644 index 0000000000000000000000000000000000000000..571cfb2d046b6c7e6a7b56812baa7c27e39cfc69 GIT binary patch literal 1417 zcmV;41$O!$iwFP!000000PR=XQxjJh|1BYGSXRY&!3%1<)CPnkK#*3nRZt|F03{S} zy)DTxEGE0@?ukP0_lv!^wjE#k%CnsjLpyc)+}BPYYu`Hk4?5Eu{mw?1Bt)y@<)s{E z_Ut+5`+nc~e!uUWELt`j#651IP&m}p+35=vT6tgg-sTIng*rOg;p@ED8xt*ObIqbH z-YHIba(7{C_oHc~ej_Ki^!XE{rb~Jb>O>9K<0ReF|LY_297R`#p$={)mF3 zh$@I-pbmo=s>5R#u0tFn;_gq9xFEM1JRPge7B-IgFno%-&kiHxQj64v@pMJKegS}?5{sbHL2 zx{>M@@HG=>GY71cfLpXKp^8mPSW{Th&0u8k&}4#Txo#S?=qvJw7@S#FF3VZ6*rZys zQnZ~_6@p7xX5|d7XISib57TImJBo}lnxUmwh8a9KTx=!W;#^CdiD_9UWf`3^3{sIK zkzvZn646Ad1SM|ftOV=R1ushByo&%TcH%)9&#JIMduu4)Sx-(+Cz&Na~h~xA3-vx_FwgEcpZi0Tu6HO2YCWXEtv=O=-2Z zwRgyPSH*d}Ct;lv#~_)+T9E4H%%G!#jQ3T1fDZ|-&&(M~p8%kgKElT`J`pK>N)lDj znDjHv(`5OXNcw_`&+&zX%{NzFg4^M(B=Nzq36|hRQHF>JPINGhDt_C4>rVCQ80EO0 z4n`=d#!ah~+bNqf0&|%;UR<+9k*IiT=r@2FNeQj~3g=b+nfu%gi#1Pl*(6 zg>rIpJ50`3%R)sDX?&cLaS^D5?h0U)uDIx8jP6Cq;*KEX8a7Ep&`Z0&lG~~1jZRp% zprSz}YufsxKpSFKMz?KRBe?yy4Oc{doCzjdN^Lbx>5b($K~;>n-Cu@Kk!OW=9Amak zrBrg1M#ky1&>3;t;`Skt{YRQcu@yV$S5veqNa7Bqr=Lba>gj5PyZH*FOU`l=J!_oF z^=O8;HXQ@(LJRe^XhoplEd7jX`UC#@8dcxIb>a%Dz3v&*G++G+wau>fX}G;Ijc4GQ zL2VvNbk{8EB+NpU5NnymG6@&aKrhQBe2*0agwslv({sNTA=VRucV!;l1tePZuA0GW z?;6*4SdqtC>KgJ`cM$_ce(Qx_sl|poHZGt{!xt+u-$V;7GuXVaz8YKd*g8FTp@P-+ z1*=QM*m+z=p1uuCT%*5j^aUa~>_I&}HxliQG~R@MY^INqEf^*-OqxAMGMpzKFB7wQ zvX~{del@E<1b`ytb8h>&Lqt&LWy$VR+%&>Ja0R^p$KCj5xq=pua{ zqR~Eh=CA>hjE7MrV~>pPIia^0y9#3&d;i8d8T({B@&_6le@DNJM;qy}pDYhJ1<`Z= XKJu2@Qd??EtwQ@3Upd(E02BZKU9+_n literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 b/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 new file mode 100644 index 0000000000..601a2227c6 --- /dev/null +++ b/jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 @@ -0,0 +1 @@ +{"created":"1970-01-01T00:00:01Z","architecture":"amd64","os":"linux","config":{"Env":[],"Entrypoint":["java","-cp","/app/resources:/app/classes:/app/libs/*","Hello"],"ExposedPorts":{},"Labels":{"label1":"value1","label2":"value2"},"Volumes":{}},"history":[{"created":"1970-01-01T00:00:01Z","author":"Jib","created_by":"jib-gradle-plugin:1.4.1-SNAPSHOT","comment":"classes"},{"created":"1970-01-01T00:00:01Z","author":"Jib","created_by":"jib-gradle-plugin:1.4.1-SNAPSHOT","comment":"extra files"}],"rootfs":{"type":"layers","diff_ids":["sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd","sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e"]}} \ No newline at end of file diff --git a/jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 b/jib-core/src/test/resources/core/extraction/test-cache/local/f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 new file mode 100644 index 0000000000000000000000000000000000000000..7e13f3f70b8febb46bf7659be13619356bdec139 GIT binary patch literal 2319 zcmV+q3GntGiwFP!000000PUK4I92H$$9ES^w#q4#OKEGUl*_uWwM7%LbMJ~xnr&TO z(gnM0PYsR1sJ5ahCx)WiM8nh&PAQMdr9p92l!}~F(S>d_>B!kV=6PmL(@mo1IkP{1 zto{DA_Pf^i_x-)U-+EuEpMQvdh&gW{0RRNRQB(l*^6Kv^V^9EqAb_Gg0Xm?w`i(Uq zQZiUE#s1#s)rb2CQyUDVz4}Z2*Z_ZuCTTzRxWm?@#} z`Xd;i^#7pnX8jdPfG|XmG|F%+%rGnh&^V2<01E)LkYGRphDn%YNfx3w3d0DR6S9;L zLum*S(m3~f0QFga2m@eTssCVLX1;)w*|Q|0klajc-Wv{*KZJv%Hyk7%9u7PJ1ON;{ z-l#u>p#Q5stcVzhCy))s3;2uc4`QfbH7WJ88o)$=vbf?F5X8VRf^#IRNCpJWpcKvl zm=K~d0^?`~z$t*mIYvk$BnY7##ZVN>&;Y_P{7?Oo{*M_9+27%R2m`@a@;?rOAWzV5 zPreU-asN-=Jo2~EeUFC48Xnp`rUrpx_YzsLT0r525mAP7;@xxI78oY;j5JrBFg**J zOg-M(w$Y~j>E;vpd6OhvzvQHjojYqy*VQ`L$;%rpy45Xe^}5fAb{g6q`q$0<6S>ZN z!rRZo8=r+2wWpYPVR~n*R;Di?E?m6as;1=?Ch5F>OYLa1g~gQ{cMI$EG)#R%s`i*( zH+BsR4kedA-Rr+EVoKI!ET`Rb($$kMRxCSyJn>1<Y8siEjPyt z$kNI1Nm+U<;m-I6sD>~6*^k>dEX_4b-{j&bUM#Mu@?joSGm-2+R_f-q%zPB9o#N?k zR^j<`{XVapMY*YNT9Hm-?&hd$U-3Pedad8qS)1fga{7L8jE*JY9cXm5+1n{)qrc}g zRwor2wJ5T#aqUaHEyCejI+cR>j=$M&V}OjJ3_yI_EPF&sn#8dZXuj*ZI?u z(DvHq+M%IPi4x+nxB<3c68K`PtX%w$<{b?!mwA+g6;2~lHOd4@XT0L?+^kI7`;Gjm+xiXas@dl&bsTo{ z(zee&RR+x-Yy6etIJfGZPvELr=2?<`SLCMi52K^1aF1n?4L7n39~K``O>DAT7!y0v zRSLw-2yckE>)O$(eY0TK7}501QkO&98;rk)hlV5xzx2+rOE@SCXu&t=IH+d5;NK=| zE_8Sa=BeYiFD|_=3A7&{>~8B=>t46y)PbD59BS8*=(^da)}9Ha9mb^<|7`Re_hf1r zZS+he58P6laZXh&GpM~VXRc&PzEH=^JT+ajL*>i5M56Ms>9&b=_l96+QyW?hwPJ6K zax^wR=yGXI?%xe6DxuBuH)^ho4h{~P<$TVg!p2VBe|^ffps@#|+W0;@vI|-$d7E8% z^A^FsbukCqYNz8x+Zr-j;#|e~X?v^v(1EP*Ut85;IG4 zK0RS3)M%?_3fC7NKB>F(QN4v#XnN$uPL)GWXX?YeMY~RpcJ|&|wBm`%6ylz>l@&hj zejIW=Dm?JO)&x`Ssi1SR)jTL=n!dS4PT0>ojq)OeGa zjrUc}igV8_7Zt9l)-#^t-n=`0>VIOdwM_X+e^1S9*{#Dvw#mO;Xhd(7w+YkpwU6^@ zopn8>7CO%v%SWH->P5^elF!p?J+j+GolvO>>6!f~@yep50Zv&q&nHNP^$&TPpDh2s zdHqLefB{$tMGzs*LNq`FD2Q_?L((Klpo-FrVJL{k5ss!voCXL?QMEBFg9t&CV-$s3 zzxqEAQ1*X=!h83BA07^U@Bbhi$3SKOHyFG}|JUzCfFL*s!xRjX1i~pgJA@>04nYZm z!*B*>Fc=~sl4AjgA~Bq!ggC*V03}2LgcQO;7F70s1IJ*<{x1K62>hr1Phm)9|2GKy z-v7;XFg2@jsi<_gh$0Y|Hu~kN4Zz?+U;|I8J5nBBAHyFJ6T@*aqxd6`V0Hd z3P`Irlb&B#?Ei$f(RjLm8hG-Z@5fVa3iUApjpa=w{Xc#9bRES{|rAXnu^xgj`{&N3U>HmSJzg7OH pp1+d*@f!b2SFHZHXTUz-{12!+|D{BU5+wu6e*ynks6zlQ006-^j?Dl7 literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 31572e6358..bf9169b289 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) - The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `docker://` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) ### Fixed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index ddd8535777..878d98e35f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) - The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) +- Docker daemon base images are now cached more effectively, speeding up builds using `docker://` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) ### Fixed From 21c36b7e398e61a31836a968a6ffffea1c93192a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 16 Oct 2019 16:13:17 -0400 Subject: [PATCH 0778/2020] Update FAQ with layering information (#2066) --- docs/faq.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index e2dffca2a7..e42cd37c26 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,6 +12,7 @@ If a question you have is not answered below, please [submit an issue](/../../is [What image format does Jib use?](#what-image-format-does-jib-use)\ [Why is my image created 48+ years ago?](#why-is-my-image-created-48-years-ago)\ [Where is the application in the container filesystem?](#where-is-the-application-in-the-container-filesystem)\ +[How are Jib applications layered?](#how-are-jib-applications-layered)\ [Can I learn more about container images?](#can-i-learn-more-about-container-images) **How-Tos**\ @@ -151,6 +152,17 @@ Jib packages your Java application into the following paths on the image: * `/app/classes/` contains all the classes files * the contents of the extra directory (default `src/main/jib`) are placed relative to the container's root directory (`/`) +### How are Jib applications layered? + +Jib makes use of [layering](https://containers.gitbook.io/build-containers-the-hard-way/#layers) to allow for fast rebuilds - it will only rebuild the layers containing files that changed since the previous build and will reuse cached layers containing files that didn't change. Jib organizes files in a way that groups frequently changing files separately from large, rarely changing files. For example, `SNAPSHOT` dependencies are placed in a separate layer from other dependencies, so that a frequently changing `SNAPSHOT` will not force the entire dependency layer to rebuild itself. + +Jib applications are split into the following layers: +* Classes +* Resources +* Project dependencies +* Snapshot dependencies +* All other dependencies +* Each extra directory (`jib.extraDirectories` in Gradle, `` in Maven) builds to its own layer ### Can I learn more about container images? @@ -426,6 +438,7 @@ FROM gcr.io/distroless/java:latest # Multiple copy statements are used to break the app into layers, allowing for faster rebuilds after small changes COPY dependencyJars /app/libs COPY snapshotDependencyJars /app/libs +COPY projectDependencyJars /app/libs COPY resources /app/resources COPY classFiles /app/classes From 4b7bd8f10eeb15d0a5a2b2d1e32c7e9940065fe4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 16 Oct 2019 17:47:27 -0400 Subject: [PATCH 0779/2020] Maven: explode final WAR instead of using intermediate exploded WAR directory (#2069) --- jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/BuildDockerMojo.java | 6 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 6 +- .../cloud/tools/jib/maven/BuildTarMojo.java | 6 +- .../jib/maven/MavenProjectProperties.java | 26 +++- .../jib/maven/MavenProjectPropertiesTest.java | 126 +++++++++++------- .../tools/jib/plugins/common/ZipUtil.java | 3 + 7 files changed, 121 insertions(+), 53 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 878d98e35f..30a637fc5b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file. - Fixed temporary directory cleanup during builds using local base images. ([#2016](https://github.com/GoogleContainerTools/jib/issues/2016)) - Fixed additional tags being ignored when building to a tarball. ([#2043](https://github.com/GoogleContainerTools/jib/issues/2043)) - Fixed `tar://` base image failing if tar does not contain explicit directory entries. ([#2067](https://github.com/GoogleContainerTools/jib/issues/2067)) +- Fixed an issue for WAR projects where Jib used an intermediate exploded WAR directory instead of exploding the final WAR file. ([#1091](https://github.com/GoogleContainerTools/jib/issues/1091)) ## 1.6.1 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 8649b8693a..d98977b03d 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -83,8 +84,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + MavenProjectProperties.getForProject( + getProject(), getSession(), getLog(), tempDirectoryProvider); try { PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( new MavenRawConfiguration(this), @@ -145,6 +148,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c9e1e48d92..4132ae562f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.ImageFormat; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -97,8 +98,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + MavenProjectProperties.getForProject( + getProject(), getSession(), getLog(), tempDirectoryProvider); try { PluginConfigurationProcessor.createJibBuildRunnerForRegistryImage( new MavenRawConfiguration(this), @@ -159,6 +162,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index f3b5636a25..44da660bcd 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -75,8 +76,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); MavenProjectProperties projectProperties = - MavenProjectProperties.getForProject(getProject(), getSession(), getLog()); + MavenProjectProperties.getForProject( + getProject(), getSession(), getLog(), tempDirectoryProvider); try { PluginConfigurationProcessor.createJibBuildRunnerForTarImage( new MavenRawConfiguration(this), @@ -137,6 +140,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoExecutionException(ex.getMessage(), ex.getCause()); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 2de780c2e3..9abad2b322 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -25,11 +25,13 @@ import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; @@ -50,6 +52,7 @@ import javax.annotation.Nullable; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; @@ -77,10 +80,15 @@ public class MavenProjectProperties implements ProjectProperties { * @param project the {@link MavenProject} for the plugin. * @param session the {@link MavenSession} for the plugin. * @param log the Maven {@link Log} to log messages during Jib execution + * @param tempDirectoryProvider temporary directory provider * @return a MavenProjectProperties from the given project and logger. */ - static MavenProjectProperties getForProject(MavenProject project, MavenSession session, Log log) { - return new MavenProjectProperties(project, session, log); + static MavenProjectProperties getForProject( + MavenProject project, + MavenSession session, + Log log, + TempDirectoryProvider tempDirectoryProvider) { + return new MavenProjectProperties(project, session, log, tempDirectoryProvider); } /** @@ -156,11 +164,17 @@ static int getVersionFromString(String versionString) { private final MavenSession session; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final ConsoleLogger consoleLogger; + private final TempDirectoryProvider tempDirectoryProvider; @VisibleForTesting - MavenProjectProperties(MavenProject project, MavenSession session, Log log) { + MavenProjectProperties( + MavenProject project, + MavenSession session, + Log log, + TempDirectoryProvider tempDirectoryProvider) { this.project = project; this.session = session; + this.tempDirectoryProvider = tempDirectoryProvider; ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(session) ? ConsoleLoggerBuilder.rich(singleThreadedExecutor, true) @@ -187,8 +201,10 @@ public JibContainerBuilder createJibContainerBuilder( throws IOException { try { if (isWarProject()) { - Path explodedWarPath = - Paths.get(project.getBuild().getDirectory(), project.getBuild().getFinalName()); + Build build = project.getBuild(); + Path war = Paths.get(build.getDirectory(), build.getFinalName() + ".war"); + Path explodedWarPath = tempDirectoryProvider.newDirectory(); + ZipUtil.unzip(war, explodedWarPath); return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 433e260104..99f9931353 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -28,13 +28,17 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; @@ -46,6 +50,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.StringJoiner; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; @@ -56,6 +61,8 @@ import org.apache.maven.model.Plugin; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.archiver.zip.ZipEntry; +import org.codehaus.plexus.archiver.zip.ZipOutputStream; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.junit.Assert; import org.junit.Before; @@ -168,6 +175,28 @@ private static Path getResource(String path) throws URISyntaxException { return Paths.get(Resources.getResource(path).toURI()); } + private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOException { + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(targetZip))) { + for (Path source : new DirectoryWalker(sourceRoot).filterRoot().walk()) { + + StringJoiner pathJoiner = new StringJoiner("/", "", ""); + sourceRoot.relativize(source).forEach(element -> pathJoiner.add(element.toString())); + String zipEntryPath = + Files.isDirectory(source) ? pathJoiner.toString() + '/' : pathJoiner.toString(); + + ZipEntry entry = new ZipEntry(zipEntryPath); + zipOut.putNextEntry(entry); + if (!Files.isDirectory(source)) { + try (InputStream in = Files.newInputStream(source)) { + ByteStreams.copy(in, zipOut); + } + } + zipOut.closeEntry(); + } + } + return targetZip; + } + @Rule public final TestRepository testRepository = new TestRepository(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -179,6 +208,7 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private Plugin mockJarPlugin; @Mock private Plugin mockCompilerPlugin; @Mock private Log mockLog; + @Mock private TempDirectoryProvider mockTempDirectoryProvider; private Xpp3Dom jarPluginConfiguration; private Xpp3Dom archive; @@ -195,7 +225,8 @@ private static Path getResource(String path) throws URISyntaxException { public void setUp() throws IOException, URISyntaxException { Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = - new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog); + new MavenProjectProperties( + mockMavenProject, mockMavenSession, mockLog, mockTempDirectoryProvider); jarPluginConfiguration = new Xpp3Dom(""); archive = new Xpp3Dom("archive"); manifest = new Xpp3Dom("manifest"); @@ -209,11 +240,11 @@ public void setUp() throws IOException, URISyntaxException { Set artifacts = ImmutableSet.of( - makeArtifact(dependenciesPath.resolve("library.jarC.jar")), - makeArtifact(dependenciesPath.resolve("libraryB.jar")), - makeArtifact(dependenciesPath.resolve("libraryA.jar")), - makeArtifact(dependenciesPath.resolve("more/dependency-1.0.0.jar")), - makeArtifact(dependenciesPath.resolve("another/one/dependency-1.0.0.jar")), + newArtifact(dependenciesPath.resolve("library.jarC.jar")), + newArtifact(dependenciesPath.resolve("libraryB.jar")), + newArtifact(dependenciesPath.resolve("libraryA.jar")), + newArtifact(dependenciesPath.resolve("more/dependency-1.0.0.jar")), + newArtifact(dependenciesPath.resolve("another/one/dependency-1.0.0.jar")), // Maven reads and populates "Artifacts" with its own processing, so read some from a // repository testRepository.findArtifact("com.test", "dependency", "1.0.0"), @@ -227,12 +258,6 @@ public void setUp() throws IOException, URISyntaxException { Mockito.when(mockMavenProject.getProperties()).thenReturn(mockMavenProperties); } - private Artifact makeArtifact(Path path) { - Artifact artifact = Mockito.mock(Artifact.class); - Mockito.when(artifact.getFile()).thenReturn(path.toFile()); - return artifact; - } - @Test public void testGetMainClassFromJar_success() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) @@ -486,40 +511,36 @@ public void testCreateContainerBuilder_packagedMode() public void testCreateContainerBuilder_warNonDefaultAppRoot() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - Path outputPath = getResource("maven/webapp"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()).thenReturn(outputPath.toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + Path unzipTarget = setUpWar(getResource("maven/webapp/final-name")); BuildConfiguration configuration = setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertSourcePathsUnordered( - ImmutableList.of(outputPath.resolve("final-name/WEB-INF/lib/dependency-1.0.0.jar")), + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), layers.dependenciesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( - ImmutableList.of( - outputPath.resolve("final-name/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), layers.snapshotsLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( - outputPath.resolve("final-name/META-INF"), - outputPath.resolve("final-name/META-INF/context.xml"), - outputPath.resolve("final-name/Test.jsp"), - outputPath.resolve("final-name/WEB-INF"), - outputPath.resolve("final-name/WEB-INF/classes"), - outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), - outputPath.resolve("final-name/WEB-INF/classes/package"), - outputPath.resolve("final-name/WEB-INF/classes/package/test.properties"), - outputPath.resolve("final-name/WEB-INF/lib"), - outputPath.resolve("final-name/WEB-INF/web.xml")), + unzipTarget.resolve("META-INF"), + unzipTarget.resolve("META-INF/context.xml"), + unzipTarget.resolve("Test.jsp"), + unzipTarget.resolve("WEB-INF"), + unzipTarget.resolve("WEB-INF/classes"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/test.properties"), + unzipTarget.resolve("WEB-INF/lib"), + unzipTarget.resolve("WEB-INF/web.xml")), layers.resourcesLayers.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( - outputPath.resolve("final-name/WEB-INF/classes/HelloWorld.class"), - outputPath.resolve("final-name/WEB-INF/classes/empty_dir"), - outputPath.resolve("final-name/WEB-INF/classes/package"), - outputPath.resolve("final-name/WEB-INF/classes/package/Other.class")), + unzipTarget.resolve("WEB-INF/classes/HelloWorld.class"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/Other.class")), layers.classesLayers.get(0).getLayerEntries()); assertExtractionPathsUnordered( @@ -563,10 +584,7 @@ public void testCreateContainerBuilder_jarNonDefaultAppRoot() @Test public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - temporaryFolder.newFolder("final-name"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + setUpWar(temporaryFolder.newFolder("final-name").toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -575,9 +593,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + setUpWar(temporaryFolder.getRoot().toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -586,9 +602,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); - Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); - Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + setUpWar(temporaryFolder.getRoot().toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -646,7 +660,8 @@ public void testClassifyDependencies() { newArtifact("com.test", "projectC", "3.0")); Map> classifyDependencies = - new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) + new MavenProjectProperties( + mockMavenProject, mockMavenSession, mockLog, mockTempDirectoryProvider) .classifyDependencies(artifacts, projectArtifacts); Assert.assertEquals( @@ -677,7 +692,8 @@ private BuildConfiguration setupBuildConfiguration( .setAppRoot(AbsoluteUnixPath.get(appRoot)) .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); JibContainerBuilder jibContainerBuilder = - new MavenProjectProperties(mockMavenProject, mockMavenSession, mockLog) + new MavenProjectProperties( + mockMavenProject, mockMavenSession, mockLog, mockTempDirectoryProvider) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, @@ -685,6 +701,26 @@ private BuildConfiguration setupBuildConfiguration( .setExecutorService(MoreExecutors.newDirectExecutorService())); } + private Path setUpWar(Path explodedWar) throws IOException { + Path fakeMavenBuildDirectory = temporaryFolder.getRoot().toPath(); + Mockito.when(mockBuild.getDirectory()).thenReturn(fakeMavenBuildDirectory.toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); + Mockito.when(mockMavenProject.getPackaging()).thenReturn("war"); + + zipUpDirectory(explodedWar, fakeMavenBuildDirectory.resolve("final-name.war")); + + // Make "MavenProjectProperties" use this folder to explode the WAR into. + Path unzipTarget = temporaryFolder.newFolder("exploded").toPath(); + Mockito.when(mockTempDirectoryProvider.newDirectory()).thenReturn(unzipTarget); + return unzipTarget; + } + + private Artifact newArtifact(Path sourceJar) { + Artifact artifact = Mockito.mock(Artifact.class); + Mockito.when(artifact.getFile()).thenReturn(sourceJar.toFile()); + return artifact; + } + private Artifact newArtifact(String group, String artifactId, String version) { Artifact artifact = new DefaultArtifact(group, artifactId, version, null, "jar", "", null); artifact.setFile(new File("/tmp/" + group + artifactId + version)); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java index e899780992..6ed3585b64 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ZipUtil.java @@ -56,6 +56,9 @@ public static void unzip(Path archive, Path destination) throws IOException { if (entry.isDirectory()) { Files.createDirectories(entryPath); } else { + if (entryPath.getParent() != null) { + Files.createDirectories(entryPath.getParent()); + } try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(entryPath))) { ByteStreams.copy(zipIn, out); } From 137de7e0eb4cb91de325f530c242171a9357bb91 Mon Sep 17 00:00:00 2001 From: Raja Shekar Reddy Date: Thu, 17 Oct 2019 00:09:22 +0200 Subject: [PATCH 0780/2020] Implemented API in jib-core to control base image caching as per the issue #1870 (#2065) --- .../cloud/tools/jib/api/Containerizer.java | 19 ++++++++++++ .../tools/jib/api/JibContainerBuilder.java | 1 + .../tools/jib/builder/steps/StepsRunner.java | 2 +- .../jib/configuration/BuildConfiguration.java | 31 +++++++++++++++++-- .../tools/jib/global/JibSystemProperties.java | 12 ------- .../jib/api/JibContainerBuilderTest.java | 4 ++- .../jib/global/JibSystemPropertiesTest.java | 24 -------------- .../common/PluginConfigurationProcessor.java | 9 +++++- .../jib/plugins/common/PropertyNames.java | 1 + 9 files changed, 62 insertions(+), 41 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 5e8b2e4706..b727341cd4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -130,6 +130,7 @@ public static Containerizer to(TarImage tarImage) { private boolean allowInsecureRegistries = false; private boolean offline = false; private String toolName = DEFAULT_TOOL_NAME; + private boolean alwaysCacheBaseImage = false; /** Instantiate with {@link #to}. */ private Containerizer( @@ -269,6 +270,20 @@ public Containerizer setToolName(String toolName) { return this; } + /** + * Controls the optimization which skips downloading base image layers that exist in a target + * registry. If the user does not set this property, then read as false. + * + * @param alwaysCacheBaseImage if {@code true}, base image layers are always pulled and cached. If + * {@code false}, base image layers will not be pulled/cached if they already exist on the + * target registry. + * @return this + */ + public Containerizer setAlwaysCacheBaseImage(boolean alwaysCacheBaseImage) { + this.alwaysCacheBaseImage = alwaysCacheBaseImage; + return this; + } + Set getAdditionalTags() { return additionalTags; } @@ -311,6 +326,10 @@ String getToolName() { return toolName; } + boolean getAlwaysCacheBaseImage() { + return alwaysCacheBaseImage; + } + String getDescription() { return description; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 162946f19e..524a11250f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -536,6 +536,7 @@ BuildConfiguration toBuildConfiguration( .setToolName(containerizer.getToolName()) .setExecutorService(executorService) .setEventHandlers(containerizer.buildEventHandlers()) + .setAlwaysCacheBaseImage(containerizer.getAlwaysCacheBaseImage()) .build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 2b9877076b..ab316a7713 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -148,7 +148,7 @@ public StepsRunner tarBuildSteps(Path outputPath) { public StepsRunner registryPushSteps() { rootProgressDescription = "building image to registry"; - boolean layersRequiredLocally = JibSystemProperties.alwaysCacheBaseImage(); + boolean layersRequiredLocally = buildConfiguration.getAlwaysCacheBaseImage(); stepsToRun.add(this::retrieveTargetRegistryCredentials); stepsToRun.add(this::authenticatePush); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 34cbca92ce..6e1f011295 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -66,6 +66,7 @@ public static class Builder { private String toolName = DEFAULT_TOOL_NAME; private EventHandlers eventHandlers = EventHandlers.NONE; @Nullable private ExecutorService executorService; + private boolean alwaysCacheBaseImage = false; private Builder() {} @@ -172,6 +173,19 @@ public Builder setOffline(boolean offline) { return this; } + /** + * Controls the optimization which skips downloading base image layers that exist in a target + * registry. If the user does not set this property then read as false. + * + * @param alwaysCacheBaseImage if {@code true}, base image layers are always pulled and cached. + * If {@code false}, base image layers will not be pulled/cached if they already exist on + * the target registry. + * @return this + */ + public Builder setAlwaysCacheBaseImage(boolean alwaysCacheBaseImage) { + this.alwaysCacheBaseImage = alwaysCacheBaseImage; + return this; + } /** * Sets the layers to build. * @@ -267,7 +281,8 @@ public BuildConfiguration build() throws IOException { layerConfigurations, toolName, eventHandlers, - Preconditions.checkNotNull(executorService)); + Preconditions.checkNotNull(executorService), + alwaysCacheBaseImage); case 1: throw new IllegalStateException(missingFields.get(0) + " is required but not set"); @@ -321,6 +336,7 @@ public static Builder builder() { private final String toolName; private final EventHandlers eventHandlers; private final ExecutorService executorService; + private final boolean alwaysCacheBaseImage; /** Instantiate with {@link #builder}. */ private BuildConfiguration( @@ -336,7 +352,8 @@ private BuildConfiguration( ImmutableList layerConfigurations, String toolName, EventHandlers eventHandlers, - ExecutorService executorService) { + ExecutorService executorService, + boolean alwaysCacheBaseImage) { this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; this.additionalTargetImageTags = additionalTargetImageTags; @@ -350,6 +367,7 @@ private BuildConfiguration( this.toolName = toolName; this.eventHandlers = eventHandlers; this.executorService = executorService; + this.alwaysCacheBaseImage = alwaysCacheBaseImage; } public ImageConfiguration getBaseImageConfiguration() { @@ -425,6 +443,15 @@ public boolean isOffline() { return offline; } + /** + * Gets whether or not to force caching the base images. + * + * @return {@code true} if the user wants to force the build to always pull the image layers. + */ + public boolean getAlwaysCacheBaseImage() { + return alwaysCacheBaseImage; + } + /** * Gets the configurations for building the layers. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index e4a13bddcb..329f4b4fcb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -29,8 +29,6 @@ public class JibSystemProperties { @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; - @VisibleForTesting static final String ALWAYS_CACHE_BASE_IMAGE = "jib.alwaysCacheBaseImage"; - @VisibleForTesting public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; @@ -95,16 +93,6 @@ public static boolean isUserAgentEnabled() { return Strings.isNullOrEmpty(System.getProperty(DISABLE_USER_AGENT)); } - /** - * Gets whether to always cache base image layers. Determined from the {@code - * jib.alwaysCacheBaseImage} system property. - * - * @return true if the property is set to {@code always}; false otherwise - */ - public static boolean alwaysCacheBaseImage() { - return Boolean.getBoolean(ALWAYS_CACHE_BASE_IMAGE); - } - /** * Checks the {@code jib.httpTimeout} system property for invalid (non-integer or negative) * values. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 210c6620a0..868149ee7a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -139,7 +139,8 @@ public void testToBuildConfiguration() .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) .setExecutorService(mockExecutorService) - .addEventHandler(mockJibEventConsumer); + .addEventHandler(mockJibEventConsumer) + .setAlwaysCacheBaseImage(false); ImageConfiguration baseImageConfiguration = ImageConfiguration.builder(ImageReference.parse("base/image")) @@ -212,6 +213,7 @@ public void testToBuildConfiguration() Assert.assertEquals( ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals("toolName", buildConfiguration.getToolName()); + Assert.assertFalse(buildConfiguration.getAlwaysCacheBaseImage()); } /** Verify that an internally-created ExecutorService is shutdown. */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index 5fc616a90e..b66fec1e4f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -160,28 +160,4 @@ public void testUseBlobMounts_other() { System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "nonbool"); Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); } - - @Test - public void testAlwaysCacheBaseImage_undefined() { - System.clearProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE); - Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); - } - - @Test - public void testAlwaysCacheBaseImage_true() { - System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "true"); - Assert.assertTrue(JibSystemProperties.alwaysCacheBaseImage()); - } - - @Test - public void testAlwaysCacheBaseImage_false() { - System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "false"); - Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); - } - - @Test - public void testAlwaysCacheBaseImage_other() { - System.setProperty(JibSystemProperties.ALWAYS_CACHE_BASE_IMAGE, "nonbool"); - Assert.assertFalse(JibSystemProperties.alwaysCacheBaseImage()); - } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index cbb4d44dda..6952e149f8 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -147,7 +147,14 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( inferredAuthProvider, rawConfiguration.getToCredHelper().orElse(null)); - Containerizer containerizer = Containerizer.to(targetImage); + boolean alwaysCacheBaseImage = + Boolean.valueOf( + rawConfiguration + .getProperty(PropertyNames.ALWAYS_CACHE_BASE_IMAGE) + .orElse(Boolean.FALSE.toString())); + Containerizer containerizer = + Containerizer.to(targetImage).setAlwaysCacheBaseImage(alwaysCacheBaseImage); + JibContainerBuilder jibContainerBuilder = processCommonConfiguration( rawConfiguration, inferredAuthProvider, projectProperties, containerizer); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 9b125bd4f1..db27429987 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -60,6 +60,7 @@ public class PropertyNames { public static final String SKIP = "jib.skip"; public static final String CONSOLE = "jib.console"; public static final String CONTAINERIZE = "jib.containerize"; + public static final String ALWAYS_CACHE_BASE_IMAGE = "jib.alwaysCacheBaseImage"; @Deprecated public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; From a610e487497ecbbec20a9053c868914707cce5bd Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Oct 2019 11:51:34 -0400 Subject: [PATCH 0781/2020] CHANGELOG for new alwaysCacheBaseImage API (#2071) --- jib-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 94e93dabb7..71d9b31e25 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file. ### Added - Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) +- `Containerizer#setAlwaysCacheBaseImage(boolean)` controls the optimization to skip downloading base image layers that exist in a target registry. ([#1870](https://github.com/GoogleContainerTools/jib/pull/1870)) ### Changed - Local base image layers are now processed in parallel, speeding up builds using large local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) - The base image manifest is no longer pulled from the registry if a digest is provided and the manifest is already cached. ([#1881](https://github.com/GoogleContainerTools/jib/issues/1881)) - Docker daemon base images are now cached more effectively, speeding up builds using `DockerDaemonImage` base images. ([#1912](https://github.com/GoogleContainerTools/jib/issues/1912)) +- Now ignores `jib.alwaysCacheBaseImage` system property. Use `Containerizer#setAlwaysCacheBaseImage(boolean)` instead. ([#1870](https://github.com/GoogleContainerTools/jib/pull/1870)) ### Fixed From 075e03db5bc905b81456bf55fd221917bebce45c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Oct 2019 17:31:39 -0400 Subject: [PATCH 0782/2020] Core release v0.12.0 (#2077) * [Gradle Release Plugin] - pre tag commit: 'v0.12.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.12.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index c17a25ec16..bec2129bb8 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.11.1-SNAPSHOT +version = 0.12.1-SNAPSHOT From 903abbdf8ed605e316aa73415b68a074ee87b9ab Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Oct 2019 17:31:45 -0400 Subject: [PATCH 0783/2020] Maven release v1.7.0 (#2075) * [Gradle Release Plugin] - pre tag commit: 'v1.7.0-maven'. * [Gradle Release Plugin] - new version commit: 'v1.7.1-SNAPSHOT-maven'. --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index 2520234e02..92d8b7c5f9 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.6.2-SNAPSHOT +version = 1.7.1-SNAPSHOT From a87dad62644d723f043d3e9b9dd3392cb99761de Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 17 Oct 2019 17:31:55 -0400 Subject: [PATCH 0784/2020] Gradle release v1.7.0 (#2076) * [Gradle Release Plugin] - pre tag commit: 'v1.7.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.7.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 2520234e02..92d8b7c5f9 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.6.2-SNAPSHOT +version = 1.7.1-SNAPSHOT From 34f837ee20349cd0f9bacf3410e53c100ceccb8c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 18 Oct 2019 11:11:42 -0400 Subject: [PATCH 0785/2020] Update readme for outputPaths (#2027) --- jib-gradle-plugin/README.md | 14 +++++++++++--- jib-maven-plugin/README.md | 12 ++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 584233e04e..4c0e3c6912 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -167,8 +167,7 @@ Then, ```gradle build``` will build and containerize your application. ### Additional Build Artifacts -As part of an image build, Jib also writes out the _image digest_ to -`build/jib-image.digest`, as well as the _image ID_ to `build/jib-image.id` +As part of an image build, Jib also writes out the _image digest_ and the _image ID_. By default, these are written out to `build/jib-image.digest` and `build/jib-image.id` respectively, but the locations can be configured using the `jib.outputFiles.digest` and `jib.outputFiles.imageId` configuration properties. See [Extended Usage](#outputpaths-closure) for more details. ## Extended Usage @@ -180,6 +179,7 @@ Field | Type | Default | Description `from` | [`from`](#from-closure) | See [`from`](#from-closure) | Configures the base image to build your application on top of. `container` | [`container`](#container-closure) | See [`container`](#container-closure) | Configures the container that is run from your built image. `extraDirectories` | [`extraDirectories`](#extradirectories-closure) | See [`extraDirectories`](#extradirectories-closure) | Configures the directories used to add arbitrary files to the image. +`outputPaths` | [`outputPaths`](#outputpaths-closure) | See [`outputPaths`](#outputpaths-closure) | Configures the locations of additional build artifacts generated by Jib. `dockerClient` | [`dockerClient`](#dockerclient-closure) | See [`dockerClient`](#dockerclient-closure) | Configures Docker for building to/from the Docker daemon. `containerizingMode` | `String` | `exploded` | If set to `packaged`, puts the JAR artifact built by the Gradle Java plugin into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | `boolean` | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. @@ -228,13 +228,21 @@ Property | Type | Default | Description `volumes` | `List` | *None* | Specifies a list of mount points on the container. `workingDirectory` | `String` | *None* | The working directory in the container. -`extraDirectories` is an object with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): +`extraDirectories` is a closure with the following properties (see [Adding Arbitrary Files to the Image](#adding-arbitrary-files-to-the-image)): Property | Type | Default | Description --- | --- | --- | --- `paths` | `Object` | `(project-dir)/src/main/jib` | Extra directories acceptable by [`Project.files()`](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#files-java.lang.Object...-), such as `String`, `File`, `Path`, `List`, etc. Can be absolute or relative to the project root. `permissions` | `Map` | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. +`outputPaths` is a closure with the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`tar` | `File` | `(project-dir)/build/jib-image.tar` | The path of the tarball generated by `jib:buildTar`. Relative paths are resolved relative to the project root. +`digest` | `File` | `(project-dir)/build/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. +`imageId` | `File` | `(project-dir)/build/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. + `dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: Property | Type | Default | Description diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 573b2b5dc7..0865e93fe6 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -212,8 +212,7 @@ mvn package ### Additional Build Artifacts -As part of an image build, Jib also writes out the _image digest_ to -`target/jib-image.digest`, as well as the _image ID_ to `target/jib-image.id`. +As part of an image build, Jib also writes out the _image digest_ and the _image ID_. By default, these are written out to `target/jib-image.digest` and `target/jib-image.id` respectively, but the locations can be configured using the `` and `` configuration properties. See [Extended Usage](#outputpaths-object) for more details. ## Extended Usage @@ -225,6 +224,7 @@ Field | Type | Default | Description `from` | [`from`](#from-object) | See [`from`](#from-object) | Configures the base image to build your application on top of. `container` | [`container`](#container-object) | See [`container`](#container-object) | Configures the container that is run from your image. `extraDirectories` | [`extraDirectories`](#extradirectories-object) | See [`extraDirectories`](#extradirectories-object) | Configures the directories used to add arbitrary files to the image. +`outputPaths` | [`outputPaths`](#outputpaths-object) | See [`outputPaths`](#outputpaths-object) | Configures the locations of additional build artifacts generated by Jib. `dockerClient` | [`dockerClient`](#dockerclient-object) | See [`dockerClient`](#dockerclient-object) | Configures Docker for building to/from the Docker daemon. `containerizingMode` | string | `exploded` | If set to `packaged`, puts the JAR artifact built at `${project.build.directory}/${project.build.finalName}.jar` (the default location where many JAR-buidiling plugins put a JAR registered as a main artifact, such as the Maven JAR Plugin) into the final image. If set to `exploded` (default), containerizes individual `.class` files and resources files. `allowInsecureRegistries` | boolean | `false` | If set to true, Jib ignores HTTPS certificate errors and may fall back to HTTP as a last resort. Leaving this parameter set to `false` is strongly recommended, since HTTP communication is unencrypted and visible to others on the network, and insecure HTTPS is no better than plain HTTP. [If accessing a registry with a self-signed certificate, adding the certificate to your Java runtime's trusted keys](https://github.com/GoogleContainerTools/jib/tree/master/docs/self_sign_cert.md) may be an alternative to enabling this option. @@ -281,6 +281,14 @@ Property | Type | Default | Description `paths` | list | `[(project-dir)/src/main/jib]` | List of extra directories. Can be absolute or relative to the project root. `permissions` | list | *None* | Maps file paths on container to Unix permissions. (Effective only for files added from extra directories.) If not configured, permissions default to "755" for directories and "644" for files. +`outputPaths` is an object with the following properties: + +Property | Type | Default | Description +--- | --- | --- | --- +`tar` | string | `(project-dir)/target/jib-image.tar` | The path of the tarball generated by `jib:buildTar`. Relative paths are resolved relative to the project root. +`digest` | string | `(project-dir)/target/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. +`imageId` | string | `(project-dir)/target/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. + `dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: Property | Type | Default | Description From 306fb5f44f1c4f5fa6d935d1735c30305f08ffb8 Mon Sep 17 00:00:00 2001 From: Dimitrios Mavrommatis Date: Fri, 18 Oct 2019 17:57:32 +0200 Subject: [PATCH 0786/2020] Advanced configuration avoidance (jib-gradle-plugin) (#2073) * Advanced configuration avoidance to avoid creating jib tasks (also dependents like jar, assemble, war, etc) when not needed * Fixed format * Updating plugin tests. IMPORTANT change minimum version to 4.9 to support TaskProviders * Mock TaskProvider for named('war') * Return war task as default when plugin is applied --- .../cloud/tools/jib/gradle/JibPlugin.java | 90 +++++++++++-------- .../cloud/tools/jib/gradle/TaskCommon.java | 14 +-- .../gradle/GradleProjectPropertiesTest.java | 13 +-- .../cloud/tools/jib/gradle/JibPluginTest.java | 47 ++++++---- .../tools/jib/gradle/TaskCommonTest.java | 21 ++--- 5 files changed, 106 insertions(+), 79 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 74ce5ed5b8..61ea57fd92 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -30,12 +30,13 @@ import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.War; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { - @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("4.6"); + @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("4.9"); @VisibleForTesting static final String JIB_EXTENSION_NAME = "jib"; @VisibleForTesting static final String BUILD_IMAGE_TASK_NAME = "jib"; @@ -114,71 +115,86 @@ public void apply(Project project) { JibExtension jibExtension = project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); - Task buildImageTask = + TaskProvider buildImageProviderTask = project .getTasks() - .create( + .register( BUILD_IMAGE_TASK_NAME, BuildImageTask.class, task -> { task.setGroup("Jib"); task.setDescription("Builds a container image to a registry."); - }) - .setJibExtension(jibExtension); - Task buildDockerTask = + task.setJibExtension(jibExtension); + }); + + TaskProvider buildDockerProviderTask = project .getTasks() - .create( + .register( BUILD_DOCKER_TASK_NAME, BuildDockerTask.class, task -> { task.setGroup("Jib"); task.setDescription("Builds a container image to a Docker daemon."); - }) - .setJibExtension(jibExtension); - Task buildTarTask = + task.setJibExtension(jibExtension); + }); + + TaskProvider buildTarProviderTask = project .getTasks() - .create( + .register( BUILD_TAR_TASK_NAME, BuildTarTask.class, task -> { task.setGroup("Jib"); task.setDescription("Builds a container image to a tarball."); - }) - .setJibExtension(jibExtension); - project.getTasks().create(FILES_TASK_NAME, FilesTask.class).setJibExtension(jibExtension); - project.getTasks().create(FILES_TASK_V2_NAME, FilesTaskV2.class).setJibExtension(jibExtension); - project.getTasks().create(INIT_TASK_NAME, SkaffoldInitTask.class).setJibExtension(jibExtension); + task.setJibExtension(jibExtension); + }); + + project + .getTasks() + .register(FILES_TASK_NAME, FilesTask.class) + .configure(t -> t.setJibExtension(jibExtension)); + project + .getTasks() + .register(FILES_TASK_V2_NAME, FilesTaskV2.class) + .configure(t -> t.setJibExtension(jibExtension)); + project + .getTasks() + .register(INIT_TASK_NAME, SkaffoldInitTask.class) + .configure(t -> t.setJibExtension(jibExtension)); // A check to catch older versions of Jib. This can be removed once we are certain people // are using Jib 1.3.1 or later. - project.getTasks().create(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); + project.getTasks().register(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); project.afterEvaluate( projectAfterEvaluation -> { try { - War warTask = TaskCommon.getWarTask(project); - Task dependsOnTask; - if (warTask != null) { - ExplodedWarTask explodedWarTask = - project.getTasks().create(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); - explodedWarTask.dependsOn(warTask); - explodedWarTask.setWarFile(warTask.getArchivePath().toPath()); - explodedWarTask.setExplodedWarDirectory( - GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); + TaskProvider warProviderTask = TaskCommon.getWarProviderTask(project); + TaskProvider dependsOnTaskProvider; + if (warProviderTask != null) { + TaskProvider explodedWarProviderTask = + project.getTasks().register(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); + explodedWarProviderTask.configure( + task -> { + task.dependsOn(warProviderTask); + task.setWarFile(((War) warProviderTask.get()).getArchivePath().toPath()); + task.setExplodedWarDirectory( + GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); + }); // Have all tasks depend on the 'jibExplodedWar' task. - dependsOnTask = explodedWarTask; + dependsOnTaskProvider = explodedWarProviderTask; } else if ("packaged".equals(jibExtension.getContainerizingMode())) { // Have all tasks depend on the 'jar' task. - dependsOnTask = projectAfterEvaluation.getTasks().getByPath("jar"); + dependsOnTaskProvider = projectAfterEvaluation.getTasks().named("jar"); } else { // Have all tasks depend on the 'classes' task. - dependsOnTask = projectAfterEvaluation.getTasks().getByPath("classes"); + dependsOnTaskProvider = projectAfterEvaluation.getTasks().named("classes"); } - buildImageTask.dependsOn(dependsOnTask); - buildDockerTask.dependsOn(dependsOnTask); - buildTarTask.dependsOn(dependsOnTask); + buildImageProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); + buildDockerProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); + buildTarProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble @@ -190,11 +206,11 @@ public void apply(Project project) { .withType( BasePlugin.class, unused -> { - Task assembleTask = - dependencyProject.getTasks().getByPath(BasePlugin.ASSEMBLE_TASK_NAME); - buildImageTask.dependsOn(assembleTask); - buildDockerTask.dependsOn(assembleTask); - buildTarTask.dependsOn(assembleTask); + TaskProvider assembleProviderTask = + dependencyProject.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME); + buildImageProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); + buildDockerProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); + buildTarProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); }); } } catch (UnknownTaskException ex) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 27b084dc98..8bc3d22286 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -26,9 +26,10 @@ import javax.annotation.Nullable; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.UnknownTaskException; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.WarPlugin; -import org.gradle.api.tasks.bundling.War; +import org.gradle.api.tasks.TaskProvider; import org.gradle.internal.logging.events.LogEvent; import org.gradle.internal.logging.events.OutputEventListener; import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; @@ -38,20 +39,19 @@ class TaskCommon { @Nullable - static War getWarTask(Project project) { - + static TaskProvider getWarProviderTask(Project project) { if (!project.getPlugins().hasPlugin(WarPlugin.class)) { return null; } if (project.getPlugins().hasPlugin("org.springframework.boot")) { - Task bootWar = project.getTasks().findByName("bootWar"); - if (bootWar != null) { // Spring Boot > 2.0 - return (War) bootWar; + try { + return project.getTasks().named("bootWar"); + } catch (UnknownTaskException ignored) { } } - return (War) project.getTasks().findByName(WarPlugin.WAR_TASK_NAME); + return project.getTasks().named(WarPlugin.WAR_TASK_NAME); } /** Disables annoying Apache HTTP client logging. */ diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index dc2ef244f7..d22be8a34c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -52,6 +52,7 @@ import org.gradle.StartParameter; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; @@ -72,7 +73,7 @@ import org.gradle.api.tasks.SourceSetOutput; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskDependency; -import org.gradle.api.tasks.bundling.War; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; import org.hamcrest.CoreMatchers; import org.junit.Assert; @@ -193,7 +194,7 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private War war; + @Mock private TaskProvider warTaskProvider; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -278,7 +279,7 @@ public void testIsWarProject() throws URISyntaxException { @Test public void testGetWar_warProject() throws URISyntaxException { setUpWarProject(getResource("gradle/webapp")); - Assert.assertNotNull(TaskCommon.getWarTask(mockProject)); + Assert.assertNotNull(TaskCommon.getWarProviderTask(mockProject)); } @Test @@ -286,12 +287,12 @@ public void testGetWar_noWarPlugin() throws URISyntaxException { setUpWarProject(getResource("gradle/webapp")); Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(false); - Assert.assertNull(TaskCommon.getWarTask(mockProject)); + Assert.assertNull(TaskCommon.getWarProviderTask(mockProject)); } @Test public void testGetWar_noWarTask() { - Assert.assertNull(TaskCommon.getWarTask(mockProject)); + Assert.assertNull(TaskCommon.getWarProviderTask(mockProject)); } @Test @@ -598,7 +599,7 @@ private BuildConfiguration setupBuildConfiguration( private void setUpWarProject(Path webAppDirectory) { Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); - Mockito.when(mockTaskContainer.findByName("war")).thenReturn(war); + Mockito.when(mockTaskContainer.named("war")).thenReturn(warTaskProvider); Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index e7f5e1609b..3bf24d4131 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -32,6 +32,7 @@ import org.gradle.api.UnknownTaskException; import org.gradle.api.internal.project.ProjectInternal; import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; @@ -177,7 +178,7 @@ public void testProjectDependencyAssembleTasksAreRun() { rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); // add a custom task that our jib tasks depend on to ensure we do not overwrite this dependsOn - Task dependencyTask = rootProject.getTasks().create("myCustomTask", task -> {}); + TaskProvider dependencyTask = rootProject.getTasks().register("myCustomTask", task -> {}); KNOWN_JIB_TASKS.forEach( taskName -> rootProject.getTasks().getByPath(taskName).dependsOn(dependencyTask)); @@ -192,6 +193,8 @@ public void testProjectDependencyAssembleTasksAreRun() { .getByPath(taskName) .getDependsOn() .stream() + .map(TaskProvider.class::cast) + .map(TaskProvider::get) .map(Task.class::cast) .map(Task::getPath) .collect(Collectors.toSet()))); @@ -214,28 +217,34 @@ public void testWebAppProject() { Assert.assertEquals( explodedWarTask, - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME) - .getDependsOn() - .iterator() - .next()); + ((TaskProvider) + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME) + .getDependsOn() + .iterator() + .next()) + .get()); Assert.assertEquals( explodedWarTask, - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME) - .getDependsOn() - .iterator() - .next()); + ((TaskProvider) + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME) + .getDependsOn() + .iterator() + .next()) + .get()); Assert.assertEquals( explodedWarTask, - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_TAR_TASK_NAME) - .getDependsOn() - .iterator() - .next()); + ((TaskProvider) + rootProject + .getTasks() + .getByPath(JibPlugin.BUILD_TAR_TASK_NAME) + .getDependsOn() + .iterator() + .next()) + .get()); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index a8ab91f92e..84a980c189 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -17,10 +17,11 @@ package com.google.cloud.tools.jib.gradle; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.WarPlugin; -import org.gradle.api.tasks.bundling.War; +import org.gradle.api.tasks.TaskProvider; import org.gradle.testfixtures.ProjectBuilder; import org.junit.Assert; import org.junit.Before; @@ -159,9 +160,9 @@ public void testGetWarTask_normalJavaProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(JavaPlugin.class); - War warTask = TaskCommon.getWarTask(project); + TaskProvider warProviderTask = TaskCommon.getWarProviderTask(project); - Assert.assertNull(warTask); + Assert.assertNull(warProviderTask); } @Test @@ -170,9 +171,9 @@ public void testGetWarTask_bootJavaProject() { project.getPlugins().apply(JavaPlugin.class); project.getPlugins().apply(SpringBootPlugin.class); - War warTask = TaskCommon.getWarTask(project); + TaskProvider warTaskProvider = TaskCommon.getWarProviderTask(project); - Assert.assertNull(warTask); + Assert.assertNull(warTaskProvider); } @Test @@ -180,9 +181,9 @@ public void testGetWarTask_normalWarProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(WarPlugin.class); - War warTask = TaskCommon.getWarTask(project); + TaskProvider warTaskProvider = TaskCommon.getWarProviderTask(project); - Assert.assertNotNull(warTask); + Assert.assertNotNull(warTaskProvider); } @Test @@ -191,9 +192,9 @@ public void testGetWarTask_bootWarProject() { project.getPlugins().apply(WarPlugin.class); project.getPlugins().apply(SpringBootPlugin.class); - War warTask = TaskCommon.getWarTask(project); + TaskProvider warTaskProviderProvider = TaskCommon.getWarProviderTask(project); - Assert.assertNotNull(warTask); - Assert.assertTrue(warTask instanceof BootWar); + Assert.assertNotNull(warTaskProviderProvider); + Assert.assertTrue(warTaskProviderProvider.get() instanceof BootWar); } } From 6cda7a0f6d12749a1b93e1ec4deaa57a0692586e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 18 Oct 2019 12:30:27 -0400 Subject: [PATCH 0787/2020] Update version strings for 1.7.0 (#2079) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 19 files changed, 44 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 693302fc8e..7f69d8f02d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.6.2-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.7.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.6.1' + // id 'com.google.cloud.tools.jib' version '1.7.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index b64b1e19f4..1894360031 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.6.1 + 1.7.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index db6d92232b..570579f688 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 2c8bca5dda..10240f1e2a 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.6.1 + 1.7.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 7ec6d4f3f8..787bbb65bc 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' id 'de.undercouch.download' version '4.0.0' id "com.gorylenko.gradle-git-properties" version "2.2.0" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index a59f351098..1af9961c7c 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.6.1 + 1.7.0 3.8.0 1.4.2 3.0.1 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index e38322ef3f..6fa0695008 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 63b648b38f..a6307e4e08 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.6.1' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.7.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index e90351c665..15dbb90231 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.6.1 + 1.7.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index d060c457ae..ee85226f1f 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 12c4dbc546..6a52f3e838 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.6.1 + 1.7.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 9e1e7eecb0..67941075b7 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 71d9b31e25..203a9a7d52 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.12.0 + +### Added + - Main class inference support for Java 13/14. ([#2015](https://github.com/GoogleContainerTools/jib/issues/2015)) - `Containerizer#setAlwaysCacheBaseImage(boolean)` controls the optimization to skip downloading base image layers that exist in a target registry. ([#1870](https://github.com/GoogleContainerTools/jib/pull/1870)) diff --git a/jib-core/README.md b/jib-core/README.md index 131c8692c3..53a9917f6a 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.11.0 + 0.12.0 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.11.0' + compile 'com.google.cloud.tools:jib-core:0.12.0' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index 3a92b3b8e6..ff33820db7 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.11.0' + classpath 'com.google.cloud.tools:jib-core:0.12.0' } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index bf9169b289..93542c7a98 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.7.0 + +### Added + - `jib.outputPaths` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) - `jib.outputPaths.tar` configures output path of `jibBuildTar` (`build/jib-image.tar` by default) - `jib.outputPaths.digest` configures the output path of the image digest (`build/jib-image.digest` by default) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 4c0e3c6912..661b374aef 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -46,7 +46,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.6.1' + id 'com.google.cloud.tools.jib' version '1.7.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 30a637fc5b..6eaa10b52d 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 1.7.0 + +### Added + - `` object for configuration output file locations ([#1561](https://github.com/GoogleContainerTools/jib/issues/1561)) - `` configures output path of `jib:buildTar` (`target/jib-image.tar` by default) - `` configures the output path of the image digest (`target/jib-image.digest` by default) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 0865e93fe6..80b6d92b96 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -41,7 +41,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.7.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -49,7 +49,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.6.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.7.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -67,7 +67,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.6.1 + 1.7.0 myimage From 14d6fc60e0a9e65c03c08a7ca788ed7b4dd11c2c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 18 Oct 2019 13:16:01 -0400 Subject: [PATCH 0788/2020] Resolve merge conflicts from my own stash (#2080) --- .../cloud/tools/jib/gradle/JibPlugin.java | 121 +++++++++--------- .../cloud/tools/jib/gradle/TaskCommon.java | 4 +- .../gradle/GradleProjectPropertiesTest.java | 6 +- .../cloud/tools/jib/gradle/JibPluginTest.java | 31 ++--- .../tools/jib/gradle/TaskCommonTest.java | 16 +-- 5 files changed, 79 insertions(+), 99 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 61ea57fd92..1c69f330da 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -30,6 +30,7 @@ import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.War; import org.gradle.util.GradleVersion; @@ -115,86 +116,78 @@ public void apply(Project project) { JibExtension jibExtension = project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); - TaskProvider buildImageProviderTask = - project - .getTasks() - .register( - BUILD_IMAGE_TASK_NAME, - BuildImageTask.class, - task -> { - task.setGroup("Jib"); - task.setDescription("Builds a container image to a registry."); - task.setJibExtension(jibExtension); - }); - - TaskProvider buildDockerProviderTask = - project - .getTasks() - .register( - BUILD_DOCKER_TASK_NAME, - BuildDockerTask.class, - task -> { - task.setGroup("Jib"); - task.setDescription("Builds a container image to a Docker daemon."); - task.setJibExtension(jibExtension); - }); - - TaskProvider buildTarProviderTask = - project - .getTasks() - .register( - BUILD_TAR_TASK_NAME, - BuildTarTask.class, - task -> { - task.setGroup("Jib"); - task.setDescription("Builds a container image to a tarball."); - task.setJibExtension(jibExtension); - }); - - project - .getTasks() + TaskContainer tasks = project.getTasks(); + TaskProvider buildImageTask = + tasks.register( + BUILD_IMAGE_TASK_NAME, + BuildImageTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a registry."); + task.setJibExtension(jibExtension); + }); + + TaskProvider buildDockerTask = + tasks.register( + BUILD_DOCKER_TASK_NAME, + BuildDockerTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a Docker daemon."); + task.setJibExtension(jibExtension); + }); + + TaskProvider buildTarTask = + tasks.register( + BUILD_TAR_TASK_NAME, + BuildTarTask.class, + task -> { + task.setGroup("Jib"); + task.setDescription("Builds a container image to a tarball."); + task.setJibExtension(jibExtension); + }); + + tasks .register(FILES_TASK_NAME, FilesTask.class) - .configure(t -> t.setJibExtension(jibExtension)); - project - .getTasks() + .configure(task -> task.setJibExtension(jibExtension)); + tasks .register(FILES_TASK_V2_NAME, FilesTaskV2.class) - .configure(t -> t.setJibExtension(jibExtension)); - project - .getTasks() + .configure(task -> task.setJibExtension(jibExtension)); + tasks .register(INIT_TASK_NAME, SkaffoldInitTask.class) - .configure(t -> t.setJibExtension(jibExtension)); + .configure(task -> task.setJibExtension(jibExtension)); // A check to catch older versions of Jib. This can be removed once we are certain people // are using Jib 1.3.1 or later. - project.getTasks().register(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); + tasks.register(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); project.afterEvaluate( projectAfterEvaluation -> { try { - TaskProvider warProviderTask = TaskCommon.getWarProviderTask(project); - TaskProvider dependsOnTaskProvider; - if (warProviderTask != null) { - TaskProvider explodedWarProviderTask = - project.getTasks().register(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); - explodedWarProviderTask.configure( + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); + TaskProvider dependsOnTask; + if (warTask != null) { + TaskProvider explodedWarTask = + tasks.register(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); + explodedWarTask.configure( task -> { - task.dependsOn(warProviderTask); - task.setWarFile(((War) warProviderTask.get()).getArchivePath().toPath()); + task.dependsOn(warTask); + task.setWarFile(((War) warTask.get()).getArchivePath().toPath()); task.setExplodedWarDirectory( GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); }); // Have all tasks depend on the 'jibExplodedWar' task. - dependsOnTaskProvider = explodedWarProviderTask; + dependsOnTask = explodedWarTask; } else if ("packaged".equals(jibExtension.getContainerizingMode())) { // Have all tasks depend on the 'jar' task. - dependsOnTaskProvider = projectAfterEvaluation.getTasks().named("jar"); + dependsOnTask = projectAfterEvaluation.getTasks().named("jar"); } else { // Have all tasks depend on the 'classes' task. - dependsOnTaskProvider = projectAfterEvaluation.getTasks().named("classes"); + dependsOnTask = projectAfterEvaluation.getTasks().named("classes"); } - buildImageProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); - buildDockerProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); - buildTarProviderTask.configure(t -> t.dependsOn(dependsOnTaskProvider)); + buildImageTask.configure(task -> task.dependsOn(dependsOnTask)); + buildDockerTask.configure(task -> task.dependsOn(dependsOnTask)); + buildTarTask.configure(task -> task.dependsOn(dependsOnTask)); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble @@ -206,11 +199,11 @@ public void apply(Project project) { .withType( BasePlugin.class, unused -> { - TaskProvider assembleProviderTask = + TaskProvider assembleTask = dependencyProject.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME); - buildImageProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); - buildDockerProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); - buildTarProviderTask.configure(t -> t.dependsOn(assembleProviderTask)); + buildImageTask.configure(task -> task.dependsOn(assembleTask)); + buildDockerTask.configure(task -> task.dependsOn(assembleTask)); + buildTarTask.configure(task -> task.dependsOn(assembleTask)); }); } } catch (UnknownTaskException ex) { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 8bc3d22286..bfbf9f601b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -39,7 +39,7 @@ class TaskCommon { @Nullable - static TaskProvider getWarProviderTask(Project project) { + static TaskProvider getWarTaskProvider(Project project) { if (!project.getPlugins().hasPlugin(WarPlugin.class)) { return null; } @@ -47,7 +47,7 @@ static TaskProvider getWarProviderTask(Project project) { if (project.getPlugins().hasPlugin("org.springframework.boot")) { try { return project.getTasks().named("bootWar"); - } catch (UnknownTaskException ignored) { + } catch (UnknownTaskException ignored) { // fall through } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index d22be8a34c..eed1860f7c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -279,7 +279,7 @@ public void testIsWarProject() throws URISyntaxException { @Test public void testGetWar_warProject() throws URISyntaxException { setUpWarProject(getResource("gradle/webapp")); - Assert.assertNotNull(TaskCommon.getWarProviderTask(mockProject)); + Assert.assertNotNull(TaskCommon.getWarTaskProvider(mockProject)); } @Test @@ -287,12 +287,12 @@ public void testGetWar_noWarPlugin() throws URISyntaxException { setUpWarProject(getResource("gradle/webapp")); Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(false); - Assert.assertNull(TaskCommon.getWarProviderTask(mockProject)); + Assert.assertNull(TaskCommon.getWarTaskProvider(mockProject)); } @Test public void testGetWar_noWarTask() { - Assert.assertNull(TaskCommon.getWarProviderTask(mockProject)); + Assert.assertNull(TaskCommon.getWarTaskProvider(mockProject)); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 3bf24d4131..f9b0c83252 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -200,6 +200,7 @@ public void testProjectDependencyAssembleTasksAreRun() { .collect(Collectors.toSet()))); } + @SuppressWarnings("unchecked") @Test public void testWebAppProject() { Project rootProject = @@ -207,43 +208,29 @@ public void testWebAppProject() { rootProject.getPluginManager().apply("java"); rootProject.getPluginManager().apply("war"); rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + ((ProjectInternal) rootProject).evaluate(); - Assert.assertNotNull(rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME)); - ExplodedWarTask explodedWarTask = - (ExplodedWarTask) rootProject.getTasks().getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + TaskContainer tasks = rootProject.getTasks(); + Task explodedWarTask = tasks.getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + Assert.assertNotNull(explodedWarTask); Assert.assertEquals( rootProject.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME), - explodedWarTask.getExplodedWarDirectory().toPath()); + ((ExplodedWarTask) explodedWarTask).getExplodedWarDirectory().toPath()); Assert.assertEquals( explodedWarTask, ((TaskProvider) - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME) - .getDependsOn() - .iterator() - .next()) + tasks.getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME).getDependsOn().iterator().next()) .get()); Assert.assertEquals( explodedWarTask, ((TaskProvider) - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME) - .getDependsOn() - .iterator() - .next()) + tasks.getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME).getDependsOn().iterator().next()) .get()); Assert.assertEquals( explodedWarTask, ((TaskProvider) - rootProject - .getTasks() - .getByPath(JibPlugin.BUILD_TAR_TASK_NAME) - .getDependsOn() - .iterator() - .next()) + tasks.getByPath(JibPlugin.BUILD_TAR_TASK_NAME).getDependsOn().iterator().next()) .get()); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 84a980c189..26891b5fb6 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -160,7 +160,7 @@ public void testGetWarTask_normalJavaProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(JavaPlugin.class); - TaskProvider warProviderTask = TaskCommon.getWarProviderTask(project); + TaskProvider warProviderTask = TaskCommon.getWarTaskProvider(project); Assert.assertNull(warProviderTask); } @@ -171,9 +171,9 @@ public void testGetWarTask_bootJavaProject() { project.getPlugins().apply(JavaPlugin.class); project.getPlugins().apply(SpringBootPlugin.class); - TaskProvider warTaskProvider = TaskCommon.getWarProviderTask(project); + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - Assert.assertNull(warTaskProvider); + Assert.assertNull(warTask); } @Test @@ -181,9 +181,9 @@ public void testGetWarTask_normalWarProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(WarPlugin.class); - TaskProvider warTaskProvider = TaskCommon.getWarProviderTask(project); + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - Assert.assertNotNull(warTaskProvider); + Assert.assertNotNull(warTask); } @Test @@ -192,9 +192,9 @@ public void testGetWarTask_bootWarProject() { project.getPlugins().apply(WarPlugin.class); project.getPlugins().apply(SpringBootPlugin.class); - TaskProvider warTaskProviderProvider = TaskCommon.getWarProviderTask(project); + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - Assert.assertNotNull(warTaskProviderProvider); - Assert.assertTrue(warTaskProviderProvider.get() instanceof BootWar); + Assert.assertNotNull(warTask); + Assert.assertTrue(warTask.get() instanceof BootWar); } } From faba9965d15265939c7578d2c5d8c3aac5991d4b Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 18 Oct 2019 13:33:12 -0400 Subject: [PATCH 0789/2020] Add in syncmap json templates (#2051) * Add in syncmap json templates --- .../common/SkaffoldSyncMapTemplate.java | 94 +++++++++++ .../common/SkaffoldSyncMapTemplateTest.java | 150 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplateTest.java diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java new file mode 100644 index 0000000000..7a36b68731 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java @@ -0,0 +1,94 @@ +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.tools.jib.api.LayerEntry; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Builds a JSON string containing files and directories that Skaffold can use for synchronizing + * files against a remote container. + * + *

Example: + * + *

{@code
+ * {
+ *   "generated": [
+ *      {
+ *        src: "fileX-local",
+ *        dest: "fileX-remote"
+ *      },
+ *      {
+ *        src: "dirX-local",
+ *        dest: "dirX-remote"
+ *      }
+ *   ],
+ *   "direct": [
+ *      {
+ *        src: "fileY-local",
+ *        dest: "fileY-remote"
+ *      },
+ *      {
+ *        src: "dirY-local",
+ *        dest: "dirY-remote"
+ *      },
+ *   ]
+ * }
+ * }
+ */ +public class SkaffoldSyncMapTemplate implements JsonTemplate { + + /** + * A single entry in the skaffold sync map, may be eventually extended to support permissions and + * ownership. + */ + public static class FileTemplate implements JsonTemplate { + private final String src; + private final String dest; + + @JsonCreator + public FileTemplate( + @JsonProperty(value = "src", required = true) String src, + @JsonProperty(value = "dest", required = true) String dest) { + this.src = src; + this.dest = dest; + } + } + + private final List generated = new ArrayList<>(); + private final List direct = new ArrayList<>(); + + @VisibleForTesting + static SkaffoldSyncMapTemplate from(String jsonString) throws IOException { + return new ObjectMapper().readValue(jsonString, SkaffoldSyncMapTemplate.class); + } + + public void addGenerated(LayerEntry layerEntry) { + generated.add( + new FileTemplate( + layerEntry.getSourceFile().toAbsolutePath().toString(), + layerEntry.getExtractionPath().toString())); + } + + public void addDirect(LayerEntry layerEntry) { + direct.add( + new FileTemplate( + layerEntry.getSourceFile().toAbsolutePath().toString(), + layerEntry.getExtractionPath().toString())); + } + + public String getJsonString() throws IOException { + try (OutputStream outputStream = new ByteArrayOutputStream()) { + new ObjectMapper().writeValue(outputStream, this); + return outputStream.toString(); + } + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplateTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplateTest.java new file mode 100644 index 0000000000..bd3b82c091 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplateTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LayerConfiguration; +import com.google.cloud.tools.jib.api.LayerEntry; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link SkaffoldFilesOutput}. */ +public class SkaffoldSyncMapTemplateTest { + + private static final Path GEN_SRC = Paths.get("/gen/src/").toAbsolutePath(); + private static final Path DIR_SRC_1 = Paths.get("/dir/src/").toAbsolutePath(); + private static final Path DIR_SRC_2 = Paths.get("/dir/src/").toAbsolutePath(); + + private static final String TEST_JSON = + "{\"generated\":[{\"src\":\"" + + getPathForJson(GEN_SRC) + + "\",\"dest\":\"/genDest\"}],\"direct\":[{\"src\":\"" + + getPathForJson(DIR_SRC_1) + + "\",\"dest\":\"/dirDest1\"},{\"src\":\"" + + getPathForJson(DIR_SRC_2) + + "\",\"dest\":\"/dirDest2\"}]}"; + private static final String TEST_JSON_EMPTY_GENERATED = + "{\"generated\":[],\"direct\":[{\"src\":\"" + + getPathForJson(DIR_SRC_1) + + "\",\"dest\":\"/dirDest1\"},{\"src\":\"" + + getPathForJson(DIR_SRC_2) + + "\",\"dest\":\"/dirDest2\"}]}"; + private static final String TEST_JSON_NO_GENERATED = + "{\"direct\":[{\"src\":\"" + + getPathForJson(DIR_SRC_1) + + "\",\"dest\":\"/dirDest1\"},{\"src\":\"" + + getPathForJson(DIR_SRC_2) + + "\",\"dest\":\"/dirDest2\"}]}"; + private static final String FAIL_TEST_JSON_MISSING_FIELD = + "{\"generated\":[{\"src\":\"" + + getPathForJson(GEN_SRC) + + "\"}],\"direct\":[{\"src\":\"" + + getPathForJson(DIR_SRC_1) + + "\",\"dest\":\"/dirDest1\"},{\"src\":\"" + + getPathForJson(DIR_SRC_2) + + "\",\"dest\":\"/dirDest2\"}]}"; + private static final String FAIL_TEST_JSON_BAD_PROPERTY_NAME = + "{\"generated\":[{\"jean-luc\":\"picard\", \"src\":\"" + + getPathForJson(GEN_SRC) + + "\",\"dest\":\"/genDest\"}],\"direct\":[{\"src\":\"" + + getPathForJson(DIR_SRC_1) + + "\",\"dest\":\"/dirDest1\"},{\"src\":\"" + + getPathForJson(DIR_SRC_2) + + "\",\"dest\":\"/dirDest2\"}]}"; + + // manually correct "\" that we inject into the strings above for windows paths, this is only + // needed for this test, when json writes the string out in the actual code, it does the right + // thing + private static String getPathForJson(Path path) { + return path.toString().replace("\\", "\\\\"); + } + + @Test + public void testFrom_badPropertyName() throws IOException { + try { + SkaffoldSyncMapTemplate.from(FAIL_TEST_JSON_BAD_PROPERTY_NAME); + Assert.fail(); + } catch (UnrecognizedPropertyException ex) { + Assert.assertTrue(ex.getMessage().contains("Unrecognized field \"jean-luc\"")); + } + } + + @Test + public void testFrom_missingField() throws IOException { + try { + SkaffoldSyncMapTemplate.from(FAIL_TEST_JSON_MISSING_FIELD); + Assert.fail(); + } catch (MismatchedInputException ex) { + Assert.assertTrue(ex.getMessage().contains("Missing required creator property 'dest'")); + } + } + + @Test + public void testFrom_validEmpty() throws Exception { + SkaffoldSyncMapTemplate.from(TEST_JSON_EMPTY_GENERATED); + SkaffoldSyncMapTemplate.from(TEST_JSON_NO_GENERATED); + // pass if no exceptions + } + + @Test + public void testGetJsonString() throws IOException { + SkaffoldSyncMapTemplate ssmt = new SkaffoldSyncMapTemplate(); + ssmt.addGenerated( + new LayerEntry( + GEN_SRC, + AbsoluteUnixPath.get("/genDest"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + LayerConfiguration.DEFAULT_MODIFICATION_TIME)); + ssmt.addDirect( + new LayerEntry( + DIR_SRC_1, + AbsoluteUnixPath.get("/dirDest1"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + LayerConfiguration.DEFAULT_MODIFICATION_TIME)); + ssmt.addDirect( + new LayerEntry( + DIR_SRC_2, + AbsoluteUnixPath.get("/dirDest2"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + LayerConfiguration.DEFAULT_MODIFICATION_TIME)); + Assert.assertEquals(TEST_JSON, ssmt.getJsonString()); + } + + @Test + public void testGetJsonString_emptyGenerated() throws IOException { + SkaffoldSyncMapTemplate ssmt = new SkaffoldSyncMapTemplate(); + ssmt.addDirect( + new LayerEntry( + DIR_SRC_1, + AbsoluteUnixPath.get("/dirDest1"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + LayerConfiguration.DEFAULT_MODIFICATION_TIME)); + ssmt.addDirect( + new LayerEntry( + DIR_SRC_2, + AbsoluteUnixPath.get("/dirDest2"), + FilePermissions.DEFAULT_FILE_PERMISSIONS, + LayerConfiguration.DEFAULT_MODIFICATION_TIME)); + Assert.assertEquals(TEST_JSON_EMPTY_GENERATED, ssmt.getJsonString()); + } +} From 140e3266f4b174ac07780982064e0f0291694468 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 18 Oct 2019 15:15:40 -0400 Subject: [PATCH 0790/2020] Remove unused methods (#2081) --- .../cloud/tools/jib/gradle/GradleProjectProperties.java | 5 ----- .../google/cloud/tools/jib/maven/MavenProjectProperties.java | 5 ----- .../cloud/tools/jib/plugins/common/ProjectProperties.java | 2 -- 3 files changed, 12 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 5c85871e42..870fe9846e 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -340,11 +340,6 @@ public String getVersion() { return project.getVersion().toString(); } - @Override - public Path getOutputDirectory() { - return project.getBuildDir().toPath(); - } - @Override public int getMajorJavaVersion() { JavaVersion version = JavaVersion.current(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 9abad2b322..1409d440a1 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -383,11 +383,6 @@ public String getVersion() { return project.getVersion(); } - @Override - public Path getOutputDirectory() { - return Paths.get(project.getBuild().getDirectory()); - } - @Override public int getMajorJavaVersion() { // Check properties for version diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 258238e681..46aaad508d 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -82,8 +82,6 @@ JibContainerBuilder createJibContainerBuilder( String getVersion(); - Path getOutputDirectory(); - int getMajorJavaVersion(); boolean isOffline(); From 62c8267a6e1a324949e373c9e0cea732b9c48759 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 18 Oct 2019 16:54:50 -0400 Subject: [PATCH 0791/2020] Eliminate generic-related warnings and minor cleanups (#2082) --- .../jib/builder/steps/PullBaseImageStep.java | 4 +-- .../tools/jib/cache/CacheStorageWriter.java | 2 +- .../tools/jib/registry/RegistryClient.java | 3 +- .../RegistryErrorExceptionBuilder.java | 35 +++++++++---------- .../jib/registry/ManifestPullerTest.java | 6 ++-- .../registry/RegistryEndpointCallerTest.java | 4 +-- .../RegistryErrorExceptionBuilderTest.java | 6 +++- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 59df8659c8..a84a7003ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -208,7 +208,7 @@ private Image pullBaseImage( .setAuthorization(registryAuthorization) .newRegistryClient(); - ManifestAndDigest manifestAndDigest = + ManifestAndDigest manifestAndDigest = registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); @@ -280,7 +280,7 @@ private Image pullBaseImage( * Looks through a manifest list for any amd64/linux manifest and downloads and returns the first * manifest it finds. */ - private ManifestAndDigest obtainPlatformSpecificImageManifest( + private ManifestAndDigest obtainPlatformSpecificImageManifest( RegistryClient registryClient, V22ManifestListTemplate manifestListTemplate) throws RegistryException, IOException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index fbcc8d20ca..ba924b5ae5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -144,7 +144,7 @@ private static void writeMetadata(JsonTemplate jsonTemplate, Path destination) StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException ignored2) { + } catch (AtomicMoveNotSupportedException ignored) { Files.move(temporaryFile, destination, StandardCopyOption.REPLACE_EXISTING); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 490262b676..834d040c2a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -299,8 +299,7 @@ public ManifestAndDigest pullManifest( return callRegistryEndpoint(manifestPuller); } - public ManifestAndDigest pullManifest(String imageTag) - throws IOException, RegistryException { + public ManifestAndDigest pullManifest(String imageTag) throws IOException, RegistryException { return pullManifest(imageTag, ManifestTemplate.class); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java index 7126750626..448f2801da 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java @@ -39,29 +39,26 @@ private static String getReason(@Nullable String errorCodeString, @Nullable Stri if (message == null) { message = "no details"; } + if (errorCodeString == null) { + return "unknown: " + message; + } try { - if (errorCodeString == null) { - throw new IllegalArgumentException(); + switch (ErrorCodes.valueOf(errorCodeString)) { + case MANIFEST_INVALID: + case BLOB_UNKNOWN: + return message + " (something went wrong)"; + + case MANIFEST_UNKNOWN: + case TAG_INVALID: + case MANIFEST_UNVERIFIED: + return message; + + default: + return "other: " + message; } - - ErrorCodes errorCode = ErrorCodes.valueOf(errorCodeString); - - if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.BLOB_UNKNOWN) { - return message + " (something went wrong)"; - - } else if (errorCode == ErrorCodes.MANIFEST_UNKNOWN - || errorCode == ErrorCodes.TAG_INVALID - || errorCode == ErrorCodes.MANIFEST_UNVERIFIED) { - return message; - - } else { - return "other: " + message; - } - } catch (IllegalArgumentException ex) { - // Unknown errorCodeString - return "unknown: " + message; + return "unknown error code: " + errorCodeString + " (" + message + ")"; } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index fe0bdab4e4..ba4527d8b9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -72,7 +72,7 @@ public void testHandleResponse_v21() v21Manifest.reset(); Mockito.when(mockResponse.getBody()).thenReturn(v21Manifest); - ManifestAndDigest manifestAndDigest = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .handleResponse(mockResponse); @@ -92,7 +92,7 @@ public void testHandleResponse_v22() v22Manifest.reset(); Mockito.when(mockResponse.getBody()).thenReturn(v22Manifest); - ManifestAndDigest manifestAndDigest = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) .handleResponse(mockResponse); @@ -130,7 +130,7 @@ public void testHandleResponse_v22ManifestListFromParentType() v22ManifestList.reset(); Mockito.when(mockResponse.getBody()).thenReturn(v22ManifestList); - ManifestAndDigest manifestAndDigest = + ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", ManifestTemplate.class) .handleResponse(mockResponse); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 528fb07aee..f04323b79d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -666,8 +666,8 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) } catch (RegistryErrorException ex) { Assert.assertThat( ex.getMessage(), - CoreMatchers.containsString( - "Tried to actionDescription but failed because: unknown: message")); + CoreMatchers.startsWith( + "Tried to actionDescription but failed because: unknown error code: code (message)")); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java index 64107096fc..bbc46b55c5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java @@ -52,7 +52,11 @@ public void testAddErrorEntry() { } catch (RegistryErrorException ex) { Assert.assertEquals( - "Tried to do something but failed because: manifest invalid (something went wrong), blob unknown (something went wrong), manifest unknown, tag invalid, manifest unverified, other: some other error happened, unknown: some unknown error happened | If this is a bug, please file an issue at https://github.com/GoogleContainerTools/jib/issues/new", + "Tried to do something but failed because: manifest invalid (something went wrong), blob " + + "unknown (something went wrong), manifest unknown, tag invalid, manifest " + + "unverified, other: some other error happened, unknown error code: unknown (some " + + "unknown error happened) | If this is a bug, please file an issue at " + + "https://github.com/GoogleContainerTools/jib/issues/new", ex.getMessage()); } } From 4d8efc902d40945fe5ed296d3bf55f0d42119fef Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 18 Oct 2019 16:57:54 -0400 Subject: [PATCH 0792/2020] More minor cleanup (#2085) --- .../google/cloud/tools/jib/gradle/JibPlugin.java | 4 ++-- .../cloud/tools/jib/maven/SettingsFixture.java | 6 +++--- .../cloud/tools/jib/maven/TestRepository.java | 16 ++++++---------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 1c69f330da..816de287f4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -89,7 +89,7 @@ private static void checkGradleVersion() { } /** Check the Jib version matches the required version (if specified). */ - private static void checkJibVersion(Project project) { + private static void checkJibVersion() { // todo: should retrieve from project properties? String requiredVersion = System.getProperty(REQUIRED_VERSION_PROPERTY_NAME); if (requiredVersion == null) { @@ -111,7 +111,7 @@ private static void checkJibVersion(Project project) { @Override public void apply(Project project) { checkGradleVersion(); - checkJibVersion(project); + checkJibVersion(); JibExtension jibExtension = project.getExtensions().create(JIB_EXTENSION_NAME, JibExtension.class, project); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java index 8fe60a6dbe..494fabab97 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/SettingsFixture.java @@ -31,7 +31,7 @@ import org.sonatype.plexus.components.cipher.DefaultPlexusCipher; import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher; -public class SettingsFixture { +class SettingsFixture { /** * Create a new {@link Settings} for testing purposes. @@ -39,7 +39,7 @@ public class SettingsFixture { * @param settingsFile absolute path to settings.xml * @return {@link Settings} built from settingsFile */ - public static Settings newSettings(Path settingsFile) { + static Settings newSettings(Path settingsFile) { Preconditions.checkArgument(Files.isRegularFile(settingsFile)); try { SettingsBuilder settingsBuilder = new DefaultSettingsBuilderFactory().newInstance(); @@ -57,7 +57,7 @@ public static Settings newSettings(Path settingsFile) { * @param settingsSecurityFile absolute path to security-settings.xml * @return {@link SettingsDecrypter} built from settingsSecurityFile */ - public static SettingsDecrypter newSettingsDecrypter(Path settingsSecurityFile) { + static SettingsDecrypter newSettingsDecrypter(Path settingsSecurityFile) { Preconditions.checkArgument(Files.isRegularFile(settingsSecurityFile)); try { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java index 5eb6276526..2d4c175697 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java @@ -24,9 +24,6 @@ public class TestRepository extends ExternalResource { private static final String TEST_M2 = "maven/testM2"; - private MojoRule testHarness; - private ArtifactRepositoryFactory artifactRepositoryFactory; - private ArtifactHandlerManager artifactHandlerManager; private ArtifactRepository testLocalRepo; private ArtifactResolver artifactResolver; private ArtifactHandler jarHandler; @@ -34,12 +31,11 @@ public class TestRepository extends ExternalResource { @Override protected void before() throws ComponentLookupException, URISyntaxException, MalformedURLException { - testHarness = new MojoRule(); - artifactRepositoryFactory = testHarness.lookup(ArtifactRepositoryFactory.class); - artifactHandlerManager = testHarness.lookup(ArtifactHandlerManager.class); + MojoRule testHarness = new MojoRule(); + ArtifactRepositoryFactory artifactRepositoryFactory = + testHarness.lookup(ArtifactRepositoryFactory.class); artifactResolver = testHarness.lookup(ArtifactResolver.class); - jarHandler = artifactHandlerManager.getArtifactHandler("jar"); - + jarHandler = testHarness.lookup(ArtifactHandlerManager.class).getArtifactHandler("jar"); testLocalRepo = artifactRepositoryFactory.createArtifactRepository( "test", @@ -49,7 +45,7 @@ protected void before() null); } - public Artifact findArtifact(String group, String artifact, String version) { + Artifact findArtifact(String group, String artifact, String version) { ArtifactResolutionRequest artifactResolutionRequest = new ArtifactResolutionRequest(); artifactResolutionRequest.setLocalRepository(testLocalRepo); Artifact artifactToFind = @@ -63,7 +59,7 @@ public Artifact findArtifact(String group, String artifact, String version) { return ars.getArtifacts().iterator().next(); } - public Path artifactPathOnDisk(String group, String artifact, String version) { + Path artifactPathOnDisk(String group, String artifact, String version) { return findArtifact(group, artifact, version).getFile().toPath(); } } From 29f90e13ee0fc5687b6a7a23fd72cc4cbda54da8 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 23 Oct 2019 11:11:25 -0400 Subject: [PATCH 0793/2020] Use artifact ID instead of project name in SkaffoldInitMojo (#2090) --- .../google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java index eb0e06c49c..c9ff150bc7 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java @@ -47,7 +47,7 @@ public void execute() throws MojoExecutionException { SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); skaffoldInitOutput.setImage(getTargetImage()); if (project.getParent() != null && project.getParent().getFile() != null) { - skaffoldInitOutput.setProject(project.getName()); + skaffoldInitOutput.setProject(project.getArtifactId()); } System.out.println("\nBEGIN JIB JSON"); try { From 37b7bbdeb54493c8d3c71f9cb89dd4eff379f382 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 23 Oct 2019 12:38:45 -0400 Subject: [PATCH 0794/2020] Fix root build.gradle being reported in skaffold init for multi-module projects (#2091) --- jib-gradle-plugin/CHANGELOG.md | 2 ++ .../cloud/tools/jib/gradle/SkaffoldInitTask.java | 10 ++++++++-- .../cloud/tools/jib/gradle/SkaffoldInitTaskTest.java | 8 ++------ jib-maven-plugin/CHANGELOG.md | 2 ++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 93542c7a98..adee4e8490 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed reporting parent build file when `skaffold init` is run on multi-module projects. + ## 1.7.0 ### Added diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java index 34c6c0e255..c77e1906d2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java @@ -21,6 +21,7 @@ import java.io.IOException; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; +import org.gradle.api.Project; import org.gradle.api.tasks.TaskAction; /** @@ -39,10 +40,15 @@ public SkaffoldInitTask setJibExtension(JibExtension jibExtension) { @TaskAction public void listModulesAndTargets() throws IOException { + Project project = getProject(); + // Ignore parent projects + if (project.getSubprojects().size() > 0) { + return; + } SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); skaffoldInitOutput.setImage(Preconditions.checkNotNull(jibExtension).getTo().getImage()); - if (!getProject().equals(getProject().getRootProject())) { - skaffoldInitOutput.setProject(getProject().getName()); + if (!project.equals(project.getRootProject())) { + skaffoldInitOutput.setProject(project.getName()); } System.out.println("\nBEGIN JIB JSON"); System.out.println(skaffoldInitOutput.getJsonString()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java index 3109137fb2..814128f476 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java @@ -77,17 +77,13 @@ public void testFilesTask_singleProject() throws IOException { @Test public void testFilesTask_multiProject() throws IOException { List outputs = getJsons(multiTestProject); - Assert.assertEquals(3, outputs.size()); + Assert.assertEquals(2, outputs.size()); SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertNull(skaffoldInitOutput.getProject()); - - skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); - Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); Assert.assertEquals("complex-service", skaffoldInitOutput.getProject()); - skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); Assert.assertEquals("simple-service", skaffoldInitOutput.getProject()); } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 6eaa10b52d..4e4522e35a 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed reporting wrong module name when `skaffold init` is run on multi-module projects ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)). + ## 1.7.0 ### Added From 44307a28045d1ee0f3aa57390610f6df66d0ad9c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 23 Oct 2019 16:26:47 -0400 Subject: [PATCH 0795/2020] Enhance TestWebServer and some cleanups (#2092) --- .../ConnectionWithProxyCredentialsTest.java | 23 ++------- .../cloud/tools/jib/http/MockConnection.java | 49 ------------------- .../cloud/tools/jib/http/RequestWrapper.java | 31 ++++++++++++ .../cloud/tools/jib/http/TestWebServer.java | 36 ++++++++++---- .../jib/http/WithServerConnectionTest.java | 2 +- .../registry/RegistryEndpointCallerTest.java | 44 +++++++++-------- 6 files changed, 86 insertions(+), 99 deletions(-) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java index 0b86e54a59..5821f97911 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java @@ -18,16 +18,14 @@ import com.google.api.client.http.apache.ApacheHttpTransport; import com.google.common.collect.ImmutableList; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.impl.client.DefaultHttpClient; -import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link Connection} with setting proxy credentials. */ public class ConnectionWithProxyCredentialsTest { @@ -43,30 +41,15 @@ public class ConnectionWithProxyCredentialsTest { "https.proxyUser", "https.proxyPassword"); - // HashMap to allow saving null values. - private final HashMap savedProperties = new HashMap<>(); + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); private final ApacheHttpTransport transport = new ApacheHttpTransport(); @Before public void setUp() { - proxyProperties.stream().forEach(key -> savedProperties.put(key, System.getProperty(key))); proxyProperties.stream().forEach(key -> System.clearProperty(key)); } - @After - public void tearDown() { - Consumer> restoreProperty = - entry -> { - if (entry.getValue() == null) { - System.clearProperty(entry.getKey()); - } else { - System.setProperty(entry.getKey(), entry.getValue()); - } - }; - savedProperties.entrySet().stream().forEach(restoreProperty); - } - @Test public void testAddProxyCredentials_undefined() { Connection.addProxyCredentials(transport); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java deleted file mode 100644 index 3470ba8d19..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/MockConnection.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.apache.ApacheHttpTransport; -import java.io.IOException; -import java.util.function.BiFunction; - -/** - * Mock {@link Connection} used for testing. Normally, you would use {@link - * org.mockito.Mockito#mock}; this class is intended to examine the {@link Request) object by - * calling its non-public package-protected methods. - */ -public class MockConnection extends Connection { - - private final BiFunction responseSupplier; - private Integer httpTimeout; - - public MockConnection(BiFunction responseSupplier) { - super( - new GenericUrl("ftp://non-exisiting.example.url.ever").toURL(), new ApacheHttpTransport()); - this.responseSupplier = responseSupplier; - } - - @Override - public Response send(String httpMethod, Request request) throws IOException { - httpTimeout = request.getHttpTimeout(); - return responseSupplier.apply(httpMethod, request); - } - - public Integer getRequestedHttpTimeout() { - return httpTimeout; - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java new file mode 100644 index 0000000000..3d03d62aff --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +/** Helper to expose package-private methods. */ +public class RequestWrapper { + + private final Request request; + + public RequestWrapper(Request request) { + this.request = request; + } + + public int getHttpTimeout() { + return request.getHttpTimeout(); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 8fee967e83..d35eb28c87 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.URISyntaxException; @@ -45,24 +46,33 @@ public class TestWebServer implements Closeable { private final boolean https; private final ServerSocket serverSocket; - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - private final Semaphore threadStarted = new Semaphore(0); + private final ExecutorService executorService; + private final Semaphore threadsStarted; private final StringBuilder inputRead = new StringBuilder(); private final List responses; public TestWebServer(boolean https) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!")); + this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"), 1); } - public TestWebServer(boolean https, List responses) + public TestWebServer(boolean https, int numThreads) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"), numThreads); + } + + public TestWebServer(boolean https, List responses, int numThreads) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { this.https = https; this.responses = responses; serverSocket = https ? createHttpsServerSocket() : new ServerSocket(0); - ignoreReturn(executorService.submit(this::serveResponses)); - threadStarted.acquire(); + threadsStarted = new Semaphore(1 - numThreads); + executorService = Executors.newFixedThreadPool(numThreads); + for (int i = 0; i < numThreads; i++) { + ignoreReturn(executorService.submit(this::serveResponses)); + } + threadsStarted.acquire(); } public int getLocalPort() { @@ -99,18 +109,26 @@ private ServerSocket createHttpsServerSocket() } private Void serveResponses() throws IOException { - threadStarted.release(); + threadsStarted.release(); try (Socket socket = serverSocket.accept()) { InputStream in = socket.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + OutputStream out = socket.getOutputStream(); + + int firstByte = in.read(); + if (firstByte != 'G' && firstByte != 'P') { // GET, POST, ... + out.write("HTTP/1.1 400 Bad Request\n\n".getBytes(StandardCharsets.UTF_8)); + return null; + } + inputRead.append((char) firstByte); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); for (String response : responses) { for (String line = reader.readLine(); line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. line = reader.readLine()) { inputRead.append(line + "\n"); } - socket.getOutputStream().write(response.getBytes(StandardCharsets.UTF_8)); + out.write(response.getBytes(StandardCharsets.UTF_8)); socket.getOutputStream().flush(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index d4104633c8..6d843b2c7c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -108,7 +108,7 @@ public void testProxyCredentialProperties() String targetServerResponse = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; try (TestWebServer server = - new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse))) { + new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse), 1)) { System.setProperty("http.proxyHost", "localhost"); System.setProperty("http.proxyPort", String.valueOf(server.getLocalPort())); System.setProperty("http.proxyUser", "user_sys_prop"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index f04323b79d..20e5f542e9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -29,7 +29,8 @@ import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Connection; -import com.google.cloud.tools.jib.http.MockConnection; +import com.google.cloud.tools.jib.http.Request; +import com.google.cloud.tools.jib.http.RequestWrapper; import com.google.cloud.tools.jib.http.Response; import com.google.common.io.CharStreams; import java.io.ByteArrayInputStream; @@ -501,63 +502,66 @@ public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryExceptio @Test public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { - MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + .thenReturn(mockResponse); System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); secureEndpointCaller.call(); // We fall back to the default timeout: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 - Assert.assertEquals(20000, mockConnection.getRequestedHttpTimeout().intValue()); + Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @Test public void testHttpTimeout_stringValue() throws IOException, RegistryException { - MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); secureEndpointCaller.call(); - Assert.assertEquals(20000, mockConnection.getRequestedHttpTimeout().intValue()); + Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @Test public void testHttpTimeout_negativeValue() throws IOException, RegistryException { - MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-1"); secureEndpointCaller.call(); // We let the negative value pass through: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 - Assert.assertEquals(Integer.valueOf(-1), mockConnection.getRequestedHttpTimeout()); + Assert.assertEquals(-1, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @Test public void testHttpTimeout_0accepted() throws IOException, RegistryException { - System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "0"); - - MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + .thenReturn(mockResponse); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "0"); secureEndpointCaller.call(); - Assert.assertEquals(Integer.valueOf(0), mockConnection.getRequestedHttpTimeout()); + Assert.assertEquals(0, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @Test public void testHttpTimeout() throws IOException, RegistryException { - System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "7593"); - - MockConnection mockConnection = new MockConnection((httpMethod, request) -> mockResponse); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + .thenReturn(mockResponse); + System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "7593"); secureEndpointCaller.call(); - Assert.assertEquals(Integer.valueOf(7593), mockConnection.getRequestedHttpTimeout()); + Assert.assertEquals(7593, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @Test From acb651cd853f27efc990c55388b1ff762a2b5d6b Mon Sep 17 00:00:00 2001 From: sullis Date: Thu, 24 Oct 2019 08:57:00 -0700 Subject: [PATCH 0796/2020] dropwizard 1.3.16 (#2093) --- examples/dropwizard/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 1894360031..c91c9cff2c 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -21,7 +21,7 @@ example.JibExampleApplication - 1.3.7 + 1.3.16 dropwizard.yml 1.5.0 From 1e86c61b6708729e1afedeb692edd941c7c8ac3f Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 24 Oct 2019 17:30:00 -0400 Subject: [PATCH 0797/2020] Moving parsing code for containerizing mode (#2098) * Moving parsing code for containerizing mode So it can be used in other places that need to validate the mode --- .../plugins/common/ContainerizingMode.java | 24 ++++++++- .../common/PluginConfigurationProcessor.java | 21 ++------ .../common/ContainerizingModeTest.java | 52 +++++++++++++++++++ .../PluginConfigurationProcessorTest.java | 50 ------------------ 4 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ContainerizingModeTest.java diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java index f50654eb32..0a898ce09b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ContainerizingMode.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.plugins.common; +import java.util.Locale; + /** * Containerizing mode. * @@ -26,5 +28,25 @@ */ public enum ContainerizingMode { EXPLODED, - PACKAGED + PACKAGED; + + /** + * Converts a string representation of ContainerizingMode to Enum. It requires an all lowercase + * string that matches the enum value exactly. + * + * @param rawMode the raw string to parse + * @return the enum equivalent of the mode + * @throws InvalidContainerizingModeException when not lowercase, or cannot match to an values of + * this enum class + */ + public static ContainerizingMode from(String rawMode) throws InvalidContainerizingModeException { + try { + if (!rawMode.toLowerCase(Locale.US).equals(rawMode)) { + throw new InvalidContainerizingModeException(rawMode, rawMode); + } + return ContainerizingMode.valueOf(rawMode.toUpperCase(Locale.US)); + } catch (IllegalArgumentException ex) { + throw new InvalidContainerizingModeException(rawMode, rawMode); + } + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 6952e149f8..ccf39fb73b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -45,7 +45,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -463,25 +462,15 @@ static AbsoluteUnixPath getAppRootChecked( } } - @VisibleForTesting static ContainerizingMode getContainerizingModeChecked( RawConfiguration rawConfiguration, ProjectProperties projectProperties) throws InvalidContainerizingModeException { - String rawMode = rawConfiguration.getContainerizingMode(); - try { - if (!rawMode.toLowerCase(Locale.US).equals(rawMode)) { - throw new InvalidContainerizingModeException(rawMode, rawMode); - } - - ContainerizingMode mode = ContainerizingMode.valueOf(rawMode.toUpperCase(Locale.US)); - if (mode == ContainerizingMode.PACKAGED && projectProperties.isWarProject()) { - throw new UnsupportedOperationException( - "packaged containerizing mode for WAR is not yet supported"); - } - return mode; - } catch (IllegalArgumentException ex) { - throw new InvalidContainerizingModeException(rawMode, rawMode); + ContainerizingMode mode = ContainerizingMode.from(rawConfiguration.getContainerizingMode()); + if (mode == ContainerizingMode.PACKAGED && projectProperties.isWarProject()) { + throw new UnsupportedOperationException( + "packaged containerizing mode for WAR is not yet supported"); } + return mode; } @VisibleForTesting diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ContainerizingModeTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ContainerizingModeTest.java new file mode 100644 index 0000000000..3abd56f1a5 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ContainerizingModeTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link ContainerizingMode}. */ +public class ContainerizingModeTest { + + @Test + public void testFrom_validValues() throws InvalidContainerizingModeException { + Assert.assertEquals(ContainerizingMode.EXPLODED, ContainerizingMode.from("exploded")); + Assert.assertEquals(ContainerizingMode.PACKAGED, ContainerizingMode.from("packaged")); + } + + @Test + public void testFrom_invalidCasing() { + try { + ContainerizingMode.from("PACKAGED"); + Assert.fail(); + } catch (InvalidContainerizingModeException ex) { + Assert.assertEquals("PACKAGED", ex.getInvalidContainerizingMode()); + Assert.assertEquals("PACKAGED", ex.getMessage()); + } + } + + @Test + public void testFrom_invalidValue() { + try { + ContainerizingMode.from("this is wrong"); + Assert.fail(); + } catch (InvalidContainerizingModeException ex) { + Assert.assertEquals("this is wrong", ex.getInvalidContainerizingMode()); + Assert.assertEquals("this is wrong", ex.getMessage()); + } + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 292c4fc392..e89bd1f852 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -530,56 +530,6 @@ public void testGetAppRootChecked_defaultWarProject() throws InvalidAppRootExcep PluginConfigurationProcessor.getAppRootChecked(rawConfiguration, projectProperties)); } - @Test - public void testGetContainerizingModeChecked_exploded() - throws InvalidContainerizingModeException { - Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("exploded"); - - Assert.assertEquals( - ContainerizingMode.EXPLODED, - PluginConfigurationProcessor.getContainerizingModeChecked( - rawConfiguration, projectProperties)); - } - - @Test - public void testGetContainerizingModeChecked_packaged() - throws InvalidContainerizingModeException { - Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("packaged"); - - Assert.assertEquals( - ContainerizingMode.PACKAGED, - PluginConfigurationProcessor.getContainerizingModeChecked( - rawConfiguration, projectProperties)); - } - - @Test - public void testGetContainerizingModeChecked_caseSensitive() { - Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("PACKAGED"); - - try { - PluginConfigurationProcessor.getContainerizingModeChecked( - rawConfiguration, projectProperties); - Assert.fail(); - } catch (InvalidContainerizingModeException ex) { - Assert.assertEquals("PACKAGED", ex.getInvalidContainerizingMode()); - Assert.assertEquals("PACKAGED", ex.getMessage()); - } - } - - @Test - public void testGetContainerizingModeChecked_invalidValue() { - Mockito.when(rawConfiguration.getContainerizingMode()).thenReturn("this is wrong"); - - try { - PluginConfigurationProcessor.getContainerizingModeChecked( - rawConfiguration, projectProperties); - Assert.fail(); - } catch (InvalidContainerizingModeException ex) { - Assert.assertEquals("this is wrong", ex.getInvalidContainerizingMode()); - Assert.assertEquals("this is wrong", ex.getMessage()); - } - } - @Test public void testGetContainerizingModeChecked_packagedWithWar() throws InvalidContainerizingModeException { From ee2e25c79ccc053999efb4de544db3c3aabd409a Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 24 Oct 2019 17:34:02 -0400 Subject: [PATCH 0798/2020] fix test (#2099) --- .../cloud/tools/jib/registry/RegistryAuthenticatorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index b73e7ba342..74fab1eb8c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -218,7 +218,7 @@ public void testUserAgent() @Test public void testSourceImage_differentSourceRepository() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - try (TestWebServer server = new TestWebServer(false)) { + try (TestWebServer server = new TestWebServer(false, 2)) { try { RegistryEndpointRequestProperties registryEndpointRequestProperties = new RegistryEndpointRequestProperties("someserver", "someimage", "anotherimage"); @@ -235,7 +235,7 @@ public void testSourceImage_differentSourceRepository() Assert.assertThat( server.getInputRead(), CoreMatchers.containsString( - "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull")); + "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull ")); } } From c0300a95c79bda957b069d15b503423e2c2adbc0 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 24 Oct 2019 17:55:33 -0400 Subject: [PATCH 0799/2020] Refactoring and renaming (#2095) * Various minor fixes * Don't log error for SSLException * Log broken pipe for SSLException too --- .../google/cloud/tools/jib/http/Request.java | 4 +- .../AuthenticationMethodRetriever.java | 10 +- .../cloud/tools/jib/registry/BlobChecker.java | 12 +-- .../tools/jib/registry/ErrorResponseUtil.java | 10 +- .../tools/jib/registry/ManifestPusher.java | 14 +-- .../jib/registry/RegistryAuthenticator.java | 30 +++--- .../jib/registry/RegistryEndpointCaller.java | 93 +++++++++---------- .../registry/RegistryEndpointProvider.java | 10 +- .../AuthenticationMethodRetrieverTest.java | 28 +++--- .../tools/jib/registry/BlobCheckerTest.java | 46 +++++---- .../jib/registry/ErrorResponseUtilTest.java | 63 ++++++------- .../jib/registry/ManifestPusherTest.java | 49 +++++----- 12 files changed, 168 insertions(+), 201 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java index 88631a5546..9a0ae9d07c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java @@ -50,9 +50,7 @@ public Request build() { * @return this */ public Builder setAuthorization(@Nullable Authorization authorization) { - if (authorization != null) { - headers.setAuthorization(authorization.toString()); - } + headers.setAuthorization(authorization == null ? null : authorization.toString()); return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index 7176376629..eba5a28971 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -81,17 +81,17 @@ public String getActionDescription() { @Override public Optional handleHttpResponseException( - HttpResponseException httpResponseException) + HttpResponseException responseException) throws HttpResponseException, RegistryErrorException { // Only valid for status code of '401 Unauthorized'. - if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { - throw httpResponseException; + if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { + throw responseException; } // Checks if the 'WWW-Authenticate' header is present. - String authenticationMethod = httpResponseException.getHeaders().getAuthenticate(); + String authenticationMethod = responseException.getHeaders().getAuthenticate(); if (authenticationMethod == null) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) + throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException) .addReason("'WWW-Authenticate' header not found") .build(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index 3c64aea09a..eb3b3bb1ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -61,25 +61,25 @@ public Optional handleResponse(Response response) throws Registr @Override public Optional handleHttpResponseException( - HttpResponseException httpResponseException) throws HttpResponseException { - if (httpResponseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { - throw httpResponseException; + HttpResponseException responseException) throws HttpResponseException { + if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { + throw responseException; } // Finds a BLOB_UNKNOWN error response code. - if (httpResponseException.getContent() == null) { + if (responseException.getContent() == null) { // TODO: The Google HTTP client gives null content for HEAD requests. Make the content never // be null, even for HEAD requests. return Optional.empty(); } - ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(responseException); if (errorCode == ErrorCodes.BLOB_UNKNOWN) { return Optional.empty(); } // BLOB_UNKNOWN was not found as a error response code. - throw httpResponseException; + throw responseException; } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java index f6407235d2..6548317f7a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java @@ -30,17 +30,17 @@ public class ErrorResponseUtil { * Extract an {@link ErrorCodes} response from the error object encoded in an {@link * HttpResponseException}. * - * @param httpResponseException the response exception + * @param responseException the response exception * @return the parsed {@link ErrorCodes} if found * @throws HttpResponseException rethrows the original exception if an error object could not be * parsed, if there were multiple error objects, or if the error code is unknown. */ - public static ErrorCodes getErrorCode(HttpResponseException httpResponseException) + public static ErrorCodes getErrorCode(HttpResponseException responseException) throws HttpResponseException { // Obtain the error response code. - String errorContent = httpResponseException.getContent(); + String errorContent = responseException.getContent(); if (errorContent == null) { - throw httpResponseException; + throw responseException; } try { @@ -62,7 +62,7 @@ public static ErrorCodes getErrorCode(HttpResponseException httpResponseExceptio } // rethrow the original exception - throw httpResponseException; + throw responseException; } // not intended to be instantiated diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index 4928408b9e..e0797034b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -92,7 +92,7 @@ public List getAccept() { } @Override - public DescriptorDigest handleHttpResponseException(HttpResponseException httpResponseException) + public DescriptorDigest handleHttpResponseException(HttpResponseException responseException) throws HttpResponseException, RegistryErrorException { // docker registry 2.0 and 2.1 returns: // 400 Bad Request @@ -105,21 +105,21 @@ public DescriptorDigest handleHttpResponseException(HttpResponseException httpRe // {"errors":[{"code":"MANIFEST_INVALID","detail": // {"message":"manifest schema version not supported"},"message":"manifest invalid"}]} - if (httpResponseException.getStatusCode() != HttpStatus.SC_BAD_REQUEST - && httpResponseException.getStatusCode() != HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE) { - throw httpResponseException; + if (responseException.getStatusCode() != HttpStatus.SC_BAD_REQUEST + && responseException.getStatusCode() != HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE) { + throw responseException; } - ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(httpResponseException); + ErrorCodes errorCode = ErrorResponseUtil.getErrorCode(responseException); if (errorCode == ErrorCodes.MANIFEST_INVALID || errorCode == ErrorCodes.TAG_INVALID) { - throw new RegistryErrorExceptionBuilder(getActionDescription(), httpResponseException) + throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException) .addReason( "Registry may not support pushing OCI Manifest or " + "Docker Image Manifest Version 2, Schema 2") .build(); } // rethrow: unhandled error response code. - throw httpResponseException; + throw responseException; } @Override diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 4564571beb..05638595b7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -74,33 +74,26 @@ static Optional fromAuthenticationMethod( return Optional.empty(); } + String registryUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); // Checks that the authentication method starts with 'bearer ' (case insensitive). if (!authenticationMethod.matches("^(?i)(bearer) .*")) { throw newRegistryAuthenticationFailedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - authenticationMethod, - "Bearer"); + registryUrl, imageName, authenticationMethod, "Bearer"); } Pattern realmPattern = Pattern.compile("realm=\"(.*?)\""); Matcher realmMatcher = realmPattern.matcher(authenticationMethod); if (!realmMatcher.find()) { throw newRegistryAuthenticationFailedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - authenticationMethod, - "realm"); + registryUrl, imageName, authenticationMethod, "realm"); } String realm = realmMatcher.group(1); Pattern servicePattern = Pattern.compile("service=\"(.*?)\""); Matcher serviceMatcher = servicePattern.matcher(authenticationMethod); // use the provided registry location when missing service (e.g., for OpenShift) - String service = - serviceMatcher.find() - ? serviceMatcher.group(1) - : registryEndpointRequestProperties.getServerUrl(); + String service = serviceMatcher.find() ? serviceMatcher.group(1) : registryUrl; return Optional.of( new RegistryAuthenticator(realm, service, registryEndpointRequestProperties, userAgent)); @@ -240,7 +233,7 @@ private Authorization authenticate(@Nullable Credential credential, String scope try { return authenticate(credential, ImmutableMap.of(imageName, scope, sourceImageName, "pull")); } catch (RegistryAuthenticationFailedException ex) { - // Unable to obtain authorization with source image: fallthrough and try without + // Unable to obtain authorization with source image: fall through and try without } } return authenticate(credential, ImmutableMap.of(imageName, scope)); @@ -249,6 +242,8 @@ private Authorization authenticate(@Nullable Credential credential, String scope private Authorization authenticate( @Nullable Credential credential, Map repositoryScopes) throws RegistryAuthenticationFailedException { + String registryUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); try (Connection connection = Connection.getConnectionFactory() .apply(getAuthenticationUrl(credential, repositoryScopes))) { @@ -274,8 +269,8 @@ private Authorization authenticate( if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), + registryUrl, + imageName, "Did not get token in authentication response from " + getAuthenticationUrl(credential, repositoryScopes) + "; parameters: " @@ -284,10 +279,7 @@ private Authorization authenticate( return Authorization.fromBearerToken(responseJson.getToken()); } catch (IOException ex) { - throw new RegistryAuthenticationFailedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - ex); + throw new RegistryAuthenticationFailedException(registryUrl, imageName, ex); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 7c0382bff1..c0a40be4ae 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -146,24 +146,9 @@ static boolean isBrokenPipe(IOException original) { * @throws RegistryException for known exceptions when interacting with the registry */ T call() throws IOException, RegistryException { - try { - String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; - URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); - return callWithAllowInsecureRegistryHandling(initialRequestUrl); - - } catch (IOException ex) { - String registry = registryEndpointRequestProperties.getServerUrl(); - String repository = registryEndpointRequestProperties.getImageName(); - logError("I/O error for image [" + registry + "/" + repository + "]:"); - logError(" " + ex.getMessage()); - if (isBrokenPipe(ex)) { - logError( - "broken pipe: the server shut down the connection. Check the server log if possible. " - + "This could also be a proxy issue. For example, a proxy may prevent sending " - + "packets that are too large."); - } - throw ex; - } + String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; + URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); + return callWithAllowInsecureRegistryHandling(initialRequestUrl); } private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, RegistryException { @@ -244,6 +229,8 @@ private T call(URL url, Function connectionFactory) throws IOException, RegistryException { // Only sends authorization if using HTTPS or explicitly forcing over HTTP. boolean sendCredentials = isHttpsProtocol(url) || JibSystemProperties.sendCredentialsOverHttp(); + String serverUrl = registryEndpointRequestProperties.getServerUrl(); + String imageName = registryEndpointRequestProperties.getImageName(); try (Connection connection = connectionFactory.apply(url)) { Request.Builder requestBuilder = @@ -265,70 +252,69 @@ private T call(URL url, Function connectionFactory) try { return registryEndpointProvider.handleHttpResponseException(ex); - } catch (HttpResponseException httpResponseException) { - if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST - || httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND - || httpResponseException.getStatusCode() + } catch (HttpResponseException responseException) { + if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST + || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND + || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_METHOD_NOT_ALLOWED) { // The name or reference was invalid. - throw newRegistryErrorException(httpResponseException); + throw newRegistryErrorException(responseException); - } else if (httpResponseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { - throw new RegistryUnauthorizedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - httpResponseException); + } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); - } else if (httpResponseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { + } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { if (sendCredentials) { // Credentials are either missing or wrong. - throw new RegistryUnauthorizedException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName(), - httpResponseException); + throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } else { - throw new RegistryCredentialsNotSentException( - registryEndpointRequestProperties.getServerUrl(), - registryEndpointRequestProperties.getImageName()); + throw new RegistryCredentialsNotSentException(serverUrl, imageName); } - } else if (httpResponseException.getStatusCode() + } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT - || httpResponseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY - || httpResponseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { + || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY + || responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { // 'Location' header can be relative or absolute. - URL redirectLocation = new URL(url, httpResponseException.getHeaders().getLocation()); + URL redirectLocation = new URL(url, responseException.getHeaders().getLocation()); return callWithAllowInsecureRegistryHandling(redirectLocation); } else { // Unknown - throw httpResponseException; + throw responseException; } } + + } catch (SSLException ex) { + logErrorIfBrokenPipe(ex); + throw ex; + + } catch (IOException ex) { + logError("I/O error for image [" + serverUrl + "/" + imageName + "]:"); + logError(" " + ex.getMessage()); + logErrorIfBrokenPipe(ex); + throw ex; } } @VisibleForTesting - RegistryErrorException newRegistryErrorException(HttpResponseException httpResponseException) { + RegistryErrorException newRegistryErrorException(HttpResponseException responseException) { RegistryErrorExceptionBuilder registryErrorExceptionBuilder = new RegistryErrorExceptionBuilder( - registryEndpointProvider.getActionDescription(), httpResponseException); + registryEndpointProvider.getActionDescription(), responseException); try { ErrorResponseTemplate errorResponse = - JsonTemplateMapper.readJson( - httpResponseException.getContent(), ErrorResponseTemplate.class); + JsonTemplateMapper.readJson(responseException.getContent(), ErrorResponseTemplate.class); for (ErrorEntryTemplate errorEntry : errorResponse.getErrors()) { registryErrorExceptionBuilder.addReason(errorEntry); } } catch (IOException ex) { registryErrorExceptionBuilder.addReason( "registry returned error code " - + httpResponseException.getStatusCode() + + responseException.getStatusCode() + "; possible causes include invalid or wrong reference. Actual error output follows:\n" - + httpResponseException.getContent() + + responseException.getContent() + "\n"); } @@ -339,4 +325,13 @@ RegistryErrorException newRegistryErrorException(HttpResponseException httpRespo private void logError(String message) { eventHandlers.dispatch(LogEvent.error("\u001B[31;1m" + message + "\u001B[0m")); } + + private void logErrorIfBrokenPipe(IOException ex) { + if (isBrokenPipe(ex)) { + logError( + "broken pipe: the server shut down the connection. Check the server log if possible. " + + "This could also be a proxy issue. For example, a proxy may prevent sending " + + "packets that are too large."); + } + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index be409a09e8..0381352dc3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -55,14 +55,14 @@ interface RegistryEndpointProvider { /** * Handles an {@link HttpResponseException} that occurs. * - * @param httpResponseException the {@link HttpResponseException} to handle - * @throws HttpResponseException {@code httpResponseException} if {@code httpResponseException} - * could not be handled + * @param responseException the {@link HttpResponseException} to handle + * @throws HttpResponseException {@code responseException} if {@code responseException} could not + * be handled * @throws RegistryErrorException if there is an error with a remote registry */ - default T handleHttpResponseException(HttpResponseException httpResponseException) + default T handleHttpResponseException(HttpResponseException responseException) throws HttpResponseException, RegistryErrorException { - throw httpResponseException; + throw responseException; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 06bdca6365..0a5bde73fb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -36,7 +36,7 @@ @RunWith(MockitoJUnitRunner.class) public class AuthenticationMethodRetrieverTest { - @Mock private HttpResponseException mockHttpResponseException; + @Mock private HttpResponseException mockResponseException; @Mock private HttpHeaders mockHeaders; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = @@ -81,27 +81,27 @@ public void testGetActionDescription() { @Test public void testHandleHttpResponseException_invalidStatusCode() throws RegistryErrorException { - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); + Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should only handle HTTP 401 Unauthorized errors"); } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + Assert.assertEquals(mockResponseException, ex); } } @Test public void tsetHandleHttpResponseException_noHeader() throws HttpResponseException { - Mockito.when(mockHttpResponseException.getStatusCode()) + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(null); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header is not found"); @@ -116,13 +116,13 @@ public void testHandleHttpResponseException_badAuthenticationMethod() throws HttpResponseException { String authenticationMethod = "bad authentication method"; - Mockito.when(mockHttpResponseException.getStatusCode()) + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); try { - testAuthenticationMethodRetriever.handleHttpResponseException(mockHttpResponseException); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header failed to parse"); @@ -140,15 +140,13 @@ public void testHandleHttpResponseException_pass() String authenticationMethod = "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\""; - Mockito.when(mockHttpResponseException.getStatusCode()) + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); - Mockito.when(mockHttpResponseException.getHeaders()).thenReturn(mockHeaders); + Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); RegistryAuthenticator registryAuthenticator = - testAuthenticationMethodRetriever - .handleHttpResponseException(mockHttpResponseException) - .get(); + testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 06ce13e2c0..0e364e79e7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -82,75 +82,73 @@ public void testHandleResponse_noContentLength() { } @Test - public void testHandleHttpResponseException() throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException() throws IOException { + HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")); - Mockito.when(mockHttpResponseException.getContent()) + Mockito.when(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); Assert.assertFalse( - testBlobChecker.handleHttpResponseException(mockHttpResponseException).isPresent()); + testBlobChecker.handleHttpResponseException(mockResponseException).isPresent()); } @Test - public void testHandleHttpResponseException_hasOtherErrors() - throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException_hasOtherErrors() throws IOException { + HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate() .addError(new ErrorEntryTemplate(ErrorCodes.BLOB_UNKNOWN.name(), "some message")) .addError(new ErrorEntryTemplate(ErrorCodes.MANIFEST_UNKNOWN.name(), "some message")); - Mockito.when(mockHttpResponseException.getContent()) + Mockito.when(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + Assert.assertEquals(mockResponseException, ex); } } @Test - public void testHandleHttpResponseException_notBlobUnknown() - throws IOException, RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()) + public void testHandleHttpResponseException_notBlobUnknown() throws IOException { + HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); ErrorResponseTemplate emptyErrorResponseTemplate = new ErrorResponseTemplate(); - Mockito.when(mockHttpResponseException.getContent()) + Mockito.when(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + Assert.assertEquals(mockResponseException, ex); } } @Test - public void testHandleHttpResponseException_invalidStatusCode() throws RegistryErrorException { - HttpResponseException mockHttpResponseException = Mockito.mock(HttpResponseException.class); - Mockito.when(mockHttpResponseException.getStatusCode()).thenReturn(-1); + public void testHandleHttpResponseException_invalidStatusCode() { + HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { - testBlobChecker.handleHttpResponseException(mockHttpResponseException); + testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-404 status codes should not be handled"); } catch (HttpResponseException ex) { - Assert.assertEquals(mockHttpResponseException, ex); + Assert.assertEquals(mockResponseException, ex); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java index fe15dfdd3a..5c8981338a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java @@ -16,78 +16,71 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; -import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Test for {@link ErrorReponseUtil}. */ +@RunWith(MockitoJUnitRunner.class) public class ErrorResponseUtilTest { + @Mock HttpResponseException responseException; + @Test public void testGetErrorCode_knownErrorCode() throws HttpResponseException { - HttpResponseException httpResponseException = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent( - "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}") - .build(); + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}"); Assert.assertSame( - ErrorCodes.MANIFEST_INVALID, ErrorResponseUtil.getErrorCode(httpResponseException)); + ErrorCodes.MANIFEST_INVALID, ErrorResponseUtil.getErrorCode(responseException)); } /** An unknown {@link ErrorCodes} should cause original exception to be rethrown. */ @Test public void testGetErrorCode_unknownErrorCode() { - HttpResponseException httpResponseException = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent( - "{\"errors\":[{\"code\":\"INVALID_ERROR_CODE\",\"message\":\"invalid code\",\"detail\":{}}]}") - .build(); + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"INVALID_ERROR_CODE\",\"message\":\"invalid code\",\"detail\":{}}]}"); try { - ErrorResponseUtil.getErrorCode(httpResponseException); + ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); } catch (HttpResponseException ex) { - Assert.assertSame(httpResponseException, ex); + Assert.assertSame(responseException, ex); } } /** Multiple error objects should cause original exception to be rethrown. */ @Test public void testGetErrorCode_multipleErrors() { - HttpResponseException httpResponseException = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent( - "{\"errors\":[" - + "{\"code\":\"MANIFEST_INVALID\",\"message\":\"message 1\",\"detail\":{}}," - + "{\"code\":\"TAG_INVALID\",\"message\":\"message 2\",\"detail\":{}}" - + "]}") - .build(); + Mockito.when(responseException.getContent()) + .thenReturn( + "{\"errors\":[" + + "{\"code\":\"MANIFEST_INVALID\",\"message\":\"message 1\",\"detail\":{}}," + + "{\"code\":\"TAG_INVALID\",\"message\":\"message 2\",\"detail\":{}}" + + "]}"); try { - ErrorResponseUtil.getErrorCode(httpResponseException); + ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); } catch (HttpResponseException ex) { - Assert.assertSame(httpResponseException, ex); + Assert.assertSame(responseException, ex); } } /** An non-error object should cause original exception to be rethrown. */ @Test public void testGetErrorCode_invalidErrorObject() { - HttpResponseException httpResponseException = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent("{\"type\":\"other\",\"message\":\"some other object\"}") - .build(); + Mockito.when(responseException.getContent()) + .thenReturn("{\"type\":\"other\",\"message\":\"some other object\"}"); try { - ErrorResponseUtil.getErrorCode(httpResponseException); + ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); } catch (HttpResponseException ex) { - Assert.assertSame(httpResponseException, ex); + Assert.assertSame(responseException, ex); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index fba3529cac..87ca1cdb2c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; @@ -159,13 +158,12 @@ public void testGetAccept() { @Test public void testHandleHttpResponseException_dockerRegistry_tagInvalid() throws HttpResponseException { - HttpResponseException exception = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent( - "{\"errors\":[{\"code\":\"TAG_INVALID\"," - + "\"message\":\"manifest tag did not match URI\"}]}") - .build(); + HttpResponseException exception = Mockito.mock(HttpResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"TAG_INVALID\"," + + "\"message\":\"manifest tag did not match URI\"}]}"); try { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); @@ -183,13 +181,12 @@ public void testHandleHttpResponseException_dockerRegistry_tagInvalid() @Test public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() throws HttpResponseException { - HttpResponseException exception = - new HttpResponseException.Builder( - HttpStatus.SC_BAD_REQUEST, "Bad Request", new HttpHeaders()) - .setContent( - "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," - + "\"message\":\"manifest invalid\",\"detail\":{}}]}") - .build(); + HttpResponseException exception = Mockito.mock(HttpResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"message\":\"manifest invalid\",\"detail\":{}}]}"); try { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); @@ -206,14 +203,13 @@ public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() /** Quay.io returns an undocumented 415 / MANIFEST_INVALID. */ @Test public void testHandleHttpResponseException_quayIo() throws HttpResponseException { - HttpResponseException exception = - new HttpResponseException.Builder( - HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED MEDIA TYPE", new HttpHeaders()) - .setContent( - "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," - + "\"detail\":{\"message\":\"manifest schema version not supported\"}," - + "\"message\":\"manifest invalid\"}]}") - .build(); + HttpResponseException exception = Mockito.mock(HttpResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE); + Mockito.when(exception.getContent()) + .thenReturn( + "{\"errors\":[{\"code\":\"MANIFEST_INVALID\"," + + "\"detail\":{\"message\":\"manifest schema version not supported\"}," + + "\"message\":\"manifest invalid\"}]}"); try { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); @@ -229,11 +225,8 @@ public void testHandleHttpResponseException_quayIo() throws HttpResponseExceptio @Test public void testHandleHttpResponseException_otherError() throws RegistryErrorException { - HttpResponseException exception = - new HttpResponseException.Builder( - HttpStatus.SC_UNAUTHORIZED, "Unauthorized", new HttpHeaders()) - .setContent("{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"Unauthorized\"]}}") - .build(); + HttpResponseException exception = Mockito.mock(HttpResponseException.class); + Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED); try { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); From 3fe8d63e2ea5f81877f74d12280f009ffb5c02f8 Mon Sep 17 00:00:00 2001 From: Raja Shekar Reddy Date: Fri, 25 Oct 2019 21:13:10 +0200 Subject: [PATCH 0800/2020] Unzipping WAR file to a temporary folder to solve #1727 (#2101) * Use artifact ID instead of project name in SkaffoldInitMojo (#2090) * Fix root build.gradle being reported in skaffold init for multi-module projects (#2091) * Enhance TestWebServer and some cleanups (#2092) * Unzipping WAR file to a temporary folder to solve #1727 * Creating libs folder during setup, since we cant commit empty folders * Code cleaning to adher to the recommendations * Deleted unused Task. Formatted code. Changed the tests to use webapp name and resolve to parent folder instead of using libs * Fix tests * Clean up * Restore original * Remove debug output * Remove redundant jib-exploded-war --- .../tools/jib/gradle/BuildDockerTask.java | 5 +- .../tools/jib/gradle/BuildImageTask.java | 5 +- .../cloud/tools/jib/gradle/BuildTarTask.java | 5 +- .../tools/jib/gradle/ExplodedWarTask.java | 49 ------ .../jib/gradle/GradleProjectProperties.java | 27 ++- .../cloud/tools/jib/gradle/JibPlugin.java | 14 +- .../gradle/GradleProjectPropertiesTest.java | 158 +++++++++++------- .../cloud/tools/jib/gradle/JibPluginTest.java | 18 +- .../META-INF/context.xml | 0 .../webapp/{jib-exploded-war => }/Test.jsp | 0 .../WEB-INF/classes/HelloWorld.class | 0 .../WEB-INF/classes/package/Other.class | 0 .../WEB-INF/classes/package/test.properties | 0 .../WEB-INF/lib/dependency-1.0.0.jar | Bin .../lib/dependencyX-1.0.0-SNAPSHOT.jar | Bin .../{jib-exploded-war => }/WEB-INF/web.xml | 0 .../jib/plugins/common/ProjectProperties.java | 6 - 17 files changed, 139 insertions(+), 148 deletions(-) delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/META-INF/context.xml (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/Test.jsp (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/classes/HelloWorld.class (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/classes/package/Other.class (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/classes/package/test.properties (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/lib/dependency-1.0.0.jar (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar (100%) rename jib-gradle-plugin/src/test/resources/gradle/webapp/{jib-exploded-war => }/WEB-INF/web.xml (100%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index d4970c46b4..3c7a670970 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.docker.DockerClient; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -126,9 +127,10 @@ public void buildDocker() TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); + GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); try { PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( new GradleRawConfiguration(jibExtension), @@ -181,6 +183,7 @@ public void buildDocker() HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 4df4df99b3..f70c4f1b6b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -77,9 +78,10 @@ public void buildImage() Preconditions.checkNotNull(jibExtension); TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); + GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); try { if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( @@ -141,6 +143,7 @@ public void buildImage() HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 0943bee943..4573ff1cd4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException; import com.google.cloud.tools.jib.plugins.common.HelpfulSuggestions; import com.google.cloud.tools.jib.plugins.common.IncompatibleBaseImageJavaVersionException; @@ -102,9 +103,10 @@ public void buildTar() Preconditions.checkNotNull(jibExtension); TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); + TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); GradleProjectProperties projectProperties = - GradleProjectProperties.getForProject(getProject(), getLogger()); + GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); try { PluginConfigurationProcessor.createJibBuildRunnerForTarImage( new GradleRawConfiguration(jibExtension), @@ -157,6 +159,7 @@ public void buildTar() HelpfulSuggestions.forInvalidImageReference(ex.getInvalidReference()), ex); } finally { + tempDirectoryProvider.close(); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java deleted file mode 100644 index 38fd416fd6..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExplodedWarTask.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle; - -import java.io.File; -import java.nio.file.Path; -import javax.annotation.Nullable; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.Sync; - -/** Gradle task that explodes a WAR file into a directory. */ -public class ExplodedWarTask extends Sync { - - @Nullable private File explodedWarDirectory; - - public void setWarFile(Path warFile) { - from(getProject().zipTree(warFile)); - } - - /** - * Sets the exploded WAR output directory of this {@link Sync} task. - * - * @param explodedWarDirectory the directory where to extract the WAR file - */ - public void setExplodedWarDirectory(Path explodedWarDirectory) { - this.explodedWarDirectory = explodedWarDirectory.toFile(); - into(explodedWarDirectory); - } - - @OutputDirectory - @Nullable - public File getExplodedWarDirectory() { - return explodedWarDirectory; - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 870fe9846e..ee51d36625 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -24,20 +24,24 @@ import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.cloud.tools.jib.plugins.common.JavaContainerBuilderHelper; import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.TimerEventHandler; +import com.google.cloud.tools.jib.plugins.common.ZipUtil; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLogger; import com.google.cloud.tools.jib.plugins.common.logging.ConsoleLoggerBuilder; import com.google.cloud.tools.jib.plugins.common.logging.ProgressDisplayGenerator; import com.google.cloud.tools.jib.plugins.common.logging.SingleThreadedExecutor; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Verify; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -47,6 +51,7 @@ import org.gradle.api.GradleException; import org.gradle.api.JavaVersion; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.file.FileCollection; @@ -55,6 +60,7 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ @@ -75,12 +81,14 @@ class GradleProjectProperties implements ProjectProperties { private static final Duration LOGGING_THREAD_SHUTDOWN_TIMEOUT = Duration.ofSeconds(1); /** @return a GradleProjectProperties from the given project and logger. */ - static GradleProjectProperties getForProject(Project project, Logger logger) { - return new GradleProjectProperties(project, logger); + static GradleProjectProperties getForProject( + Project project, Logger logger, TempDirectoryProvider tempDirectoryProvider) { + return new GradleProjectProperties(project, logger, tempDirectoryProvider); } - static Path getExplodedWarDirectory(Project project) { - return project.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME); + String getWarFilePath() { + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); + return Verify.verifyNotNull(warTask).get().getOutputs().getFiles().getAsPath(); } private static boolean isProgressFooterEnabled(Project project) { @@ -105,11 +113,14 @@ private static boolean isProgressFooterEnabled(Project project) { private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final Logger logger; private final ConsoleLogger consoleLogger; + private final TempDirectoryProvider tempDirectoryProvider; @VisibleForTesting - GradleProjectProperties(Project project, Logger logger) { + GradleProjectProperties( + Project project, Logger logger, TempDirectoryProvider tempDirectoryProvider) { this.project = project; this.logger = logger; + this.tempDirectoryProvider = tempDirectoryProvider; ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(project) ? ConsoleLoggerBuilder.rich(singleThreadedExecutor, false) @@ -135,8 +146,10 @@ public JibContainerBuilder createJibContainerBuilder( JavaContainerBuilder javaContainerBuilder, ContainerizingMode containerizingMode) { try { if (isWarProject()) { - logger.info("WAR project identified, creating WAR image: " + project.getDisplayName()); - Path explodedWarPath = GradleProjectProperties.getExplodedWarDirectory(project); + String warFilePath = getWarFilePath(); + logger.info("WAR project identified, creating WAR image from: " + warFilePath); + Path explodedWarPath = tempDirectoryProvider.newDirectory(); + ZipUtil.unzip(Paths.get(warFilePath), explodedWarPath); return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 816de287f4..fb655b2dd3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -32,7 +32,6 @@ import org.gradle.api.plugins.BasePlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; -import org.gradle.api.tasks.bundling.War; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { @@ -167,17 +166,8 @@ public void apply(Project project) { TaskProvider warTask = TaskCommon.getWarTaskProvider(project); TaskProvider dependsOnTask; if (warTask != null) { - TaskProvider explodedWarTask = - tasks.register(EXPLODED_WAR_TASK_NAME, ExplodedWarTask.class); - explodedWarTask.configure( - task -> { - task.dependsOn(warTask); - task.setWarFile(((War) warTask.get()).getArchivePath().toPath()); - task.setExplodedWarDirectory( - GradleProjectProperties.getExplodedWarDirectory(projectAfterEvaluation)); - }); - // Have all tasks depend on the 'jibExplodedWar' task. - dependsOnTask = explodedWarTask; + // Have all tasks depend on the 'war' task. + dependsOnTask = warTask; } else if ("packaged".equals(jibExtension.getContainerizingMode())) { // Have all tasks depend on the 'jar' task. dependsOnTask = projectAfterEvaluation.getTasks().named("jar"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index eed1860f7c..aa4430315e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -29,14 +29,18 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; @@ -47,8 +51,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.StringJoiner; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.gradle.StartParameter; import org.gradle.api.JavaVersion; import org.gradle.api.Project; @@ -83,6 +90,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -181,6 +189,7 @@ private static Path getResource(String path) throws URISyntaxException { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock private TempDirectoryProvider mockTempDirectoryProvider; @Mock private FileResolver mockFileResolver; @Mock private Jar mockJar; @Mock private Project mockProject; @@ -194,7 +203,9 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; - @Mock private TaskProvider warTaskProvider; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private TaskProvider mockWarTaskProvider; private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -248,13 +259,12 @@ public void setup() throws URISyntaxException, IOException { Mockito.when(mockMainSourceSetOutput.getClassesDirs()).thenReturn(classesFileCollection); Mockito.when(mockMainSourceSetOutput.getResourcesDir()).thenReturn(resourcesOutputDir.toFile()); Mockito.when(mockMainSourceSet.getRuntimeClasspath()).thenReturn(runtimeFileCollection); - // We can't commit an empty directory in Git, so create (if not exist). - Path emptyDirectory = - getResource("gradle/webapp").resolve("jib-exploded-war/WEB-INF/classes/empty_dir"); + Path emptyDirectory = getResource("gradle/webapp").resolve("WEB-INF/classes/empty_dir"); Files.createDirectories(emptyDirectory); - gradleProjectProperties = new GradleProjectProperties(mockProject, mockLogger); + gradleProjectProperties = + new GradleProjectProperties(mockProject, mockLogger, mockTempDirectoryProvider); } @Test @@ -271,22 +281,21 @@ public void testGetMainClassFromJar_missing() { } @Test - public void testIsWarProject() throws URISyntaxException { - setUpWarProject(getResource("gradle/webapp")); + public void testIsWarProject() { + Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); Assert.assertTrue(gradleProjectProperties.isWarProject()); } @Test - public void testGetWar_warProject() throws URISyntaxException { - setUpWarProject(getResource("gradle/webapp")); + public void testGetWar_warProject() { + Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); Assert.assertNotNull(TaskCommon.getWarTaskProvider(mockProject)); } @Test - public void testGetWar_noWarPlugin() throws URISyntaxException { - setUpWarProject(getResource("gradle/webapp")); + public void testGetWar_noWarPlugin() { Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(false); - Assert.assertNull(TaskCommon.getWarTaskProvider(mockProject)); } @@ -457,38 +466,36 @@ public void testCreateContainerBuilder_war() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { Path webAppDirectory = getResource("gradle/webapp"); - setUpWarProject(webAppDirectory); + Path unzipTarget = setUpWarProject(webAppDirectory); BuildConfiguration configuration = setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); assertSourcePathsUnordered( - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar")), + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), layers.dependenciesLayerEntries.get(0).getLayerEntries()); assertSourcePathsUnordered( - ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), layers.snapshotsLayerEntries.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/META-INF"), - webAppDirectory.resolve("jib-exploded-war/META-INF/context.xml"), - webAppDirectory.resolve("jib-exploded-war/Test.jsp"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/test.properties"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/lib"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/web.xml")), + unzipTarget.resolve("META-INF"), + unzipTarget.resolve("META-INF/context.xml"), + unzipTarget.resolve("Test.jsp"), + unzipTarget.resolve("WEB-INF"), + unzipTarget.resolve("WEB-INF/classes"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/test.properties"), + unzipTarget.resolve("WEB-INF/lib"), + unzipTarget.resolve("WEB-INF/web.xml")), layers.resourcesLayerEntries.get(0).getLayerEntries()); assertSourcePathsUnordered( ImmutableList.of( - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/HelloWorld.class"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/empty_dir"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package"), - webAppDirectory.resolve("jib-exploded-war/WEB-INF/classes/package/Other.class")), + unzipTarget.resolve("WEB-INF/classes/HelloWorld.class"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/Other.class")), layers.classesLayerEntries.get(0).getLayerEntries()); assertExtractionPathsUnordered( @@ -523,44 +530,44 @@ public void testCreateContainerBuilder_war() public void testCreateContainerBuilder_defaultWebAppRoot() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - setUpWarProject(getResource("gradle/webapp")); + Path unzipTarget = setUpWarProject(getResource("gradle/webapp")); BuildConfiguration configuration = setupBuildConfiguration( JavaContainerBuilder.DEFAULT_WEB_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); - assertExtractionPathsUnordered( - Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependency-1.0.0.jar"), + assertSourcePathsUnordered( + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), layers.dependenciesLayerEntries.get(0).getLayerEntries()); - assertExtractionPathsUnordered( - Collections.singletonList("/jetty/webapps/ROOT/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar"), + assertSourcePathsUnordered( + ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar")), layers.snapshotsLayerEntries.get(0).getLayerEntries()); - assertExtractionPathsUnordered( - Arrays.asList( - "/jetty/webapps/ROOT/META-INF", - "/jetty/webapps/ROOT/META-INF/context.xml", - "/jetty/webapps/ROOT/Test.jsp", - "/jetty/webapps/ROOT/WEB-INF", - "/jetty/webapps/ROOT/WEB-INF/classes", - "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", - "/jetty/webapps/ROOT/WEB-INF/classes/package", - "/jetty/webapps/ROOT/WEB-INF/classes/package/test.properties", - "/jetty/webapps/ROOT/WEB-INF/lib", - "/jetty/webapps/ROOT/WEB-INF/web.xml"), + assertSourcePathsUnordered( + ImmutableList.of( + unzipTarget.resolve("META-INF"), + unzipTarget.resolve("META-INF/context.xml"), + unzipTarget.resolve("Test.jsp"), + unzipTarget.resolve("WEB-INF"), + unzipTarget.resolve("WEB-INF/classes"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/test.properties"), + unzipTarget.resolve("WEB-INF/lib"), + unzipTarget.resolve("WEB-INF/web.xml")), layers.resourcesLayerEntries.get(0).getLayerEntries()); - assertExtractionPathsUnordered( + assertSourcePathsUnordered( Arrays.asList( - "/jetty/webapps/ROOT/WEB-INF/classes/HelloWorld.class", - "/jetty/webapps/ROOT/WEB-INF/classes/empty_dir", - "/jetty/webapps/ROOT/WEB-INF/classes/package", - "/jetty/webapps/ROOT/WEB-INF/classes/package/Other.class"), + unzipTarget.resolve("WEB-INF/classes/HelloWorld.class"), + unzipTarget.resolve("WEB-INF/classes/empty_dir"), + unzipTarget.resolve("WEB-INF/classes/package"), + unzipTarget.resolve("WEB-INF/classes/package/Other.class")), layers.classesLayerEntries.get(0).getLayerEntries()); } @Test public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "lib"); + temporaryFolder.newFolder("WEB-INF", "lib"); setUpWarProject(temporaryFolder.getRoot().toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -568,7 +575,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() @Test public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - temporaryFolder.newFolder("jib-exploded-war", "WEB-INF", "classes"); + temporaryFolder.newFolder("WEB-INF", "classes"); setUpWarProject(temporaryFolder.getRoot().toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -576,7 +583,6 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() @Test public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - temporaryFolder.newFolder("jib-exploded-war"); setUpWarProject(temporaryFolder.getRoot().toPath()); setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @@ -589,7 +595,7 @@ private BuildConfiguration setupBuildConfiguration( .setAppRoot(AbsoluteUnixPath.get(appRoot)) .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); JibContainerBuilder jibContainerBuilder = - new GradleProjectProperties(mockProject, mockLogger) + new GradleProjectProperties(mockProject, mockLogger, mockTempDirectoryProvider) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( jibContainerBuilder, @@ -597,9 +603,41 @@ private BuildConfiguration setupBuildConfiguration( .setExecutorService(MoreExecutors.newDirectExecutorService())); } - private void setUpWarProject(Path webAppDirectory) { - Mockito.when(mockProject.getBuildDir()).thenReturn(webAppDirectory.toFile()); - Mockito.when(mockTaskContainer.named("war")).thenReturn(warTaskProvider); + private Path setUpWarProject(Path webAppDirectory) throws IOException { + Path targetZip = + zipUpDirectory(webAppDirectory, temporaryFolder.getRoot().toPath().resolve("my-app.war")); + Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); + Mockito.when(mockWarTaskProvider.get().getOutputs().getFiles().getAsPath()) + .thenReturn(targetZip.toString()); + Mockito.when(gradleProjectProperties.getWarFilePath()).thenReturn(targetZip.toString()); + + // Make "GradleProjectProperties" use this folder to explode the WAR into. + Path unzipTarget = temporaryFolder.newFolder("exploded").toPath(); + Mockito.when(mockTempDirectoryProvider.newDirectory()).thenReturn(unzipTarget); + return unzipTarget; + } + + private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOException { + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(targetZip))) { + for (Path source : new DirectoryWalker(sourceRoot).filterRoot().walk()) { + + StringJoiner pathJoiner = new StringJoiner("/", "", ""); + sourceRoot.relativize(source).forEach(element -> pathJoiner.add(element.toString())); + String zipEntryPath = + Files.isDirectory(source) ? pathJoiner.toString() + '/' : pathJoiner.toString(); + + ZipEntry entry = new ZipEntry(zipEntryPath); + zipOut.putNextEntry(entry); + if (!Files.isDirectory(source)) { + try (InputStream in = Files.newInputStream(source)) { + ByteStreams.copy(in, zipOut); + } + } + zipOut.closeEntry(); + } + } + return targetZip; } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index f9b0c83252..f8fc789500 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.gradle; -import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -31,6 +30,7 @@ import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.testfixtures.ProjectBuilder; @@ -211,24 +211,20 @@ public void testWebAppProject() { ((ProjectInternal) rootProject).evaluate(); TaskContainer tasks = rootProject.getTasks(); - Task explodedWarTask = tasks.getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); - Assert.assertNotNull(explodedWarTask); + Task warTask = tasks.getByPath(":" + WarPlugin.WAR_TASK_NAME); + Assert.assertNotNull(warTask); Assert.assertEquals( - rootProject.getBuildDir().toPath().resolve(ProjectProperties.EXPLODED_WAR_DIRECTORY_NAME), - ((ExplodedWarTask) explodedWarTask).getExplodedWarDirectory().toPath()); - - Assert.assertEquals( - explodedWarTask, + warTask, ((TaskProvider) tasks.getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME).getDependsOn().iterator().next()) .get()); Assert.assertEquals( - explodedWarTask, + warTask, ((TaskProvider) tasks.getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME).getDependsOn().iterator().next()) .get()); Assert.assertEquals( - explodedWarTask, + warTask, ((TaskProvider) tasks.getByPath(JibPlugin.BUILD_TAR_TASK_NAME).getDependsOn().iterator().next()) .get()); @@ -243,7 +239,7 @@ public void testNonWebAppProject() { ((ProjectInternal) rootProject).evaluate(); TaskContainer tasks = rootProject.getTasks(); try { - tasks.getByPath(":" + JibPlugin.EXPLODED_WAR_TASK_NAME); + tasks.getByPath(":" + WarPlugin.WAR_TASK_NAME); Assert.fail(); } catch (UnknownTaskException ex) { Assert.assertNotNull(ex.getMessage()); diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/META-INF/context.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/META-INF/context.xml similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/META-INF/context.xml rename to jib-gradle-plugin/src/test/resources/gradle/webapp/META-INF/context.xml diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/Test.jsp b/jib-gradle-plugin/src/test/resources/gradle/webapp/Test.jsp similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/Test.jsp rename to jib-gradle-plugin/src/test/resources/gradle/webapp/Test.jsp diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/HelloWorld.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/HelloWorld.class rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/HelloWorld.class diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/Other.class similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/Other.class rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/Other.class diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/test.properties similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/classes/package/test.properties rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/test.properties diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependency-1.0.0.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependency-1.0.0.jar rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependency-1.0.0.jar diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/lib/dependencyX-1.0.0-SNAPSHOT.jar diff --git a/jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/web.xml similarity index 100% rename from jib-gradle-plugin/src/test/resources/gradle/webapp/jib-exploded-war/WEB-INF/web.xml rename to jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/web.xml diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java index 46aaad508d..b60c5415fa 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ProjectProperties.java @@ -31,12 +31,6 @@ public interface ProjectProperties { /** Directory name for the cache. The directory will be relative to the build output directory. */ String CACHE_DIRECTORY_NAME = "jib-cache"; - /** - * Directory name for the exploded WAR. The directory will be relative to the build output - * directory. - */ - String EXPLODED_WAR_DIRECTORY_NAME = "jib-exploded-war"; - // TODO: Move out of ProjectProperties. void waitForLoggingThread(); From 71fdf835588c2f2aa5fb3b147ec81f289ea1722a Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 25 Oct 2019 16:19:17 -0400 Subject: [PATCH 0801/2020] Move skip execution logic to MojoCommon (#2102) * Move skip execution logic to MojoCommon --- .../tools/jib/maven/BuildDockerMojo.java | 15 +--------- .../cloud/tools/jib/maven/BuildImageMojo.java | 15 +--------- .../cloud/tools/jib/maven/BuildTarMojo.java | 15 +--------- .../cloud/tools/jib/maven/MojoCommon.java | 29 +++++++++++++++++++ 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index d98977b03d..32f6c17998 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -31,7 +31,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; @@ -53,19 +52,7 @@ public class BuildDockerMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { checkJibVersion(); - if (isSkipped()) { - getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); - return; - } else if (!isContainerizable()) { - getLog() - .info( - "Skipping containerization of this module (not specified in " - + PropertyNames.CONTAINERIZE - + ")"); - return; - } - if ("pom".equals(getProject().getPackaging())) { - getLog().info("Skipping containerization because packaging is 'pom'..."); + if (MojoCommon.shouldSkipJibExecution(this)) { return; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 4132ae562f..748dbfa072 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -31,7 +31,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import java.io.IOException; @@ -55,19 +54,7 @@ public class BuildImageMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { checkJibVersion(); - if (isSkipped()) { - getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); - return; - } else if (!isContainerizable()) { - getLog() - .info( - "Skipping containerization of this module (not specified in " - + PropertyNames.CONTAINERIZE - + ")"); - return; - } - if ("pom".equals(getProject().getPackaging())) { - getLog().info("Skipping containerization because packaging is 'pom'..."); + if (MojoCommon.shouldSkipJibExecution(this)) { return; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 44da660bcd..53ff23c4ca 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -30,7 +30,6 @@ import com.google.cloud.tools.jib.plugins.common.InvalidWorkingDirectoryException; import com.google.cloud.tools.jib.plugins.common.MainClassInferenceException; import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; @@ -55,19 +54,7 @@ public class BuildTarMojo extends JibPluginConfiguration { @Override public void execute() throws MojoExecutionException, MojoFailureException { checkJibVersion(); - if (isSkipped()) { - getLog().info("Skipping containerization because jib-maven-plugin: skip = true"); - return; - } else if (!isContainerizable()) { - getLog() - .info( - "Skipping containerization of this module (not specified in " - + PropertyNames.CONTAINERIZE - + ")"); - return; - } - if ("pom".equals(getProject().getPackaging())) { - getLog().info("Skipping containerization because packaging is 'pom'..."); + if (MojoCommon.shouldSkipJibExecution(this)) { return; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 62a820e557..8910322911 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; +import com.google.cloud.tools.jib.plugins.common.PropertyNames; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -30,6 +31,7 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; /** Collection of common methods to share between Maven goals. */ @@ -122,5 +124,32 @@ public static void checkJibVersion(PluginDescriptor descriptor) throws MojoExecu } } + /** + * Determines if Jib goal execution on this project/module should be skipped due to configuration. + * + * @param jibPluginConfiguration usually {@code this}, the Mojo this check is applied in. + * @return {@code true} if Jib should be skipped (should not execute goal), or {@code false} if it + * should continue with execution. + */ + public static boolean shouldSkipJibExecution(JibPluginConfiguration jibPluginConfiguration) { + Log log = jibPluginConfiguration.getLog(); + if (jibPluginConfiguration.isSkipped()) { + log.info("Skipping containerization because jib-maven-plugin: skip = true"); + return true; + } + if (!jibPluginConfiguration.isContainerizable()) { + log.info( + "Skipping containerization of this module (not specified in " + + PropertyNames.CONTAINERIZE + + ")"); + return true; + } + if ("pom".equals(jibPluginConfiguration.getProject().getPackaging())) { + log.info("Skipping containerization because packaging is 'pom'..."); + return true; + } + return false; + } + private MojoCommon() {} } From 25f986a70248193b10c12c7b9d9f76199088a520 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 28 Oct 2019 11:56:39 -0400 Subject: [PATCH 0802/2020] Fix test project naming (#2103) --- .../cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java | 4 ++-- .../resources/maven/projects/multi/complex-service/pom.xml | 4 ++-- .../src/test/resources/maven/projects/multi/lib/pom.xml | 3 +-- .../src/test/resources/maven/projects/multi/pom.xml | 2 +- .../resources/maven/projects/multi/simple-service/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java index 99906581c8..cac2e8bfd7 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java @@ -88,7 +88,7 @@ public void testFilesMojo_multiModule() throws IOException, VerificationExceptio SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertEquals("name-service", skaffoldInitOutput.getProject()); + Assert.assertEquals("simple-service", skaffoldInitOutput.getProject()); skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); @@ -96,6 +96,6 @@ public void testFilesMojo_multiModule() throws IOException, VerificationExceptio skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertEquals("service", skaffoldInitOutput.getProject()); + Assert.assertEquals("complex-service", skaffoldInitOutput.getProject()); } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml index 3b4036e300..bf876aee44 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml @@ -3,11 +3,11 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - service + complex-service 1.0.0.TEST-SNAPSHOT - com.example + com.jib.test multimodule 1.0.0.TEST-SNAPSHOT diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml index 11a689fbd8..41c098642f 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/lib/pom.xml @@ -3,13 +3,12 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.jib.test lib 1.0.0.TEST-SNAPSHOT lib - com.example + com.jib.test multimodule 1.0.0.TEST-SNAPSHOT diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml index 6b1f514b70..0618624466 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml @@ -6,7 +6,7 @@ 4.0.0 - com.example + com.jib.test multimodule pom 1.0.0.TEST-SNAPSHOT diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml index 5a96aec3af..d089c3bcc2 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/simple-service/pom.xml @@ -4,12 +4,12 @@ 4.0.0 - name-service + simple-service 1.0.0.TEST-SNAPSHOT - com.example + com.jib.test multimodule 1.0.0.TEST-SNAPSHOT From 5995d9303ac8dba1e51bc930a9e25acbd88eba85 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 29 Oct 2019 14:42:20 -0400 Subject: [PATCH 0803/2020] Don't use bootWar if disabled (#2107) --- jib-gradle-plugin/CHANGELOG.md | 3 +- .../jib/gradle/GradleProjectProperties.java | 5 + .../cloud/tools/jib/gradle/JibPlugin.java | 19 ++- .../cloud/tools/jib/gradle/TaskCommon.java | 11 +- .../gradle/GradleProjectPropertiesTest.java | 85 ++++++----- .../cloud/tools/jib/gradle/JibPluginTest.java | 137 +++++++++++------- .../tools/jib/gradle/TaskCommonTest.java | 24 +-- 7 files changed, 167 insertions(+), 117 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index adee4e8490..e40f378907 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,7 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed reporting parent build file when `skaffold init` is run on multi-module projects. +- Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) +- Now correctly uses the `war` task if it is enabled and the `bootWar` task is disabled for Spring WAR projects. ([#2096](https://github.com/GoogleContainerTools/jib/issues/2096)) ## 1.7.0 diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index ee51d36625..b5d2f7ce5a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -87,6 +87,11 @@ static GradleProjectProperties getForProject( } String getWarFilePath() { + TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + if (bootWarTask != null && bootWarTask.get().getEnabled()) { + return bootWarTask.get().getOutputs().getFiles().getAsPath(); + } + TaskProvider warTask = TaskCommon.getWarTaskProvider(project); return Verify.verifyNotNull(warTask).get().getOutputs().getFiles().getAsPath(); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index fb655b2dd3..ae19337776 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.gradle.api.GradleException; @@ -164,16 +165,22 @@ public void apply(Project project) { projectAfterEvaluation -> { try { TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - TaskProvider dependsOnTask; - if (warTask != null) { - // Have all tasks depend on the 'war' task. - dependsOnTask = warTask; + TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + List> dependsOnTask = new ArrayList<>(); + if (warTask != null || bootWarTask != null) { + // Have all tasks depend on the 'war' and/or 'bootWar' task. + if (warTask != null) { + dependsOnTask.add(warTask); + } + if (bootWarTask != null) { + dependsOnTask.add(bootWarTask); + } } else if ("packaged".equals(jibExtension.getContainerizingMode())) { // Have all tasks depend on the 'jar' task. - dependsOnTask = projectAfterEvaluation.getTasks().named("jar"); + dependsOnTask.add(projectAfterEvaluation.getTasks().named("jar")); } else { // Have all tasks depend on the 'classes' task. - dependsOnTask = projectAfterEvaluation.getTasks().named("classes"); + dependsOnTask.add(projectAfterEvaluation.getTasks().named("classes")); } buildImageTask.configure(task -> task.dependsOn(dependsOnTask)); buildDockerTask.configure(task -> task.dependsOn(dependsOnTask)); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index bfbf9f601b..ba011a0733 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -40,18 +40,21 @@ class TaskCommon { @Nullable static TaskProvider getWarTaskProvider(Project project) { - if (!project.getPlugins().hasPlugin(WarPlugin.class)) { - return null; + if (project.getPlugins().hasPlugin(WarPlugin.class)) { + return project.getTasks().named(WarPlugin.WAR_TASK_NAME); } + return null; + } + @Nullable + static TaskProvider getBootWarTaskProvider(Project project) { if (project.getPlugins().hasPlugin("org.springframework.boot")) { try { return project.getTasks().named("bootWar"); } catch (UnknownTaskException ignored) { // fall through } } - - return project.getTasks().named(WarPlugin.WAR_TASK_NAME); + return null; } /** Disables annoying Apache HTTP client logging. */ diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index aa4430315e..24e4d051de 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -56,16 +56,13 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.gradle.StartParameter; import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.file.AbstractFileCollection; import org.gradle.api.internal.file.FileResolver; -import org.gradle.api.invocation.Gradle; import org.gradle.api.java.archives.Manifest; import org.gradle.api.java.archives.internal.DefaultManifest; import org.gradle.api.logging.Logger; @@ -73,7 +70,6 @@ import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.plugins.PluginContainer; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -191,22 +187,23 @@ private static Path getResource(String path) throws URISyntaxException { @Mock private TempDirectoryProvider mockTempDirectoryProvider; @Mock private FileResolver mockFileResolver; - @Mock private Jar mockJar; - @Mock private Project mockProject; @Mock private Convention mockConvention; @Mock private TaskContainer mockTaskContainer; - @Mock private PluginContainer mockPluginContainer; @Mock private Logger mockLogger; - @Mock private Gradle mockGradle; - @Mock private StartParameter mockStartParameter; @Mock private JavaPluginConvention mockJavaPluginConvention; @Mock private SourceSetContainer mockSourceSetContainer; @Mock private SourceSet mockMainSourceSet; @Mock private SourceSetOutput mockMainSourceSetOutput; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Project mockProject; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private TaskProvider mockWarTaskProvider; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private TaskProvider mockBootWarTaskProvider; + private Manifest manifest; private GradleProjectProperties gradleProjectProperties; @@ -218,15 +215,10 @@ public void setup() throws URISyntaxException, IOException { .thenReturn(mockJavaPluginConvention); Mockito.when(mockJavaPluginConvention.getSourceSets()).thenReturn(mockSourceSetContainer); Mockito.when(mockProject.getTasks()).thenReturn(mockTaskContainer); - Mockito.when(mockProject.getPlugins()).thenReturn(mockPluginContainer); - Mockito.when(mockJar.getManifest()).thenReturn(manifest); - Mockito.when(mockProject.getGradle()).thenReturn(mockGradle); - Mockito.when(mockGradle.getStartParameter()).thenReturn(mockStartParameter); - Mockito.when(mockStartParameter.getConsoleOutput()).thenReturn(ConsoleOutput.Auto); + Mockito.when(mockProject.getGradle().getStartParameter().getConsoleOutput()) + .thenReturn(ConsoleOutput.Auto); // mocking to complete ignore project dependency resolution - Mockito.when(mockProject.getConfigurations()) - .thenReturn(Mockito.mock(ConfigurationContainer.class, Mockito.RETURNS_DEEP_STUBS)); Mockito.when( mockProject .getConfigurations() @@ -270,6 +262,8 @@ public void setup() throws URISyntaxException, IOException { @Test public void testGetMainClassFromJar_success() { manifest.attributes(ImmutableMap.of("Main-Class", "some.main.class")); + Jar mockJar = Mockito.mock(Jar.class); + Mockito.when(mockJar.getManifest()).thenReturn(manifest); Mockito.when(mockTaskContainer.findByName("jar")).thenReturn(mockJar); Assert.assertEquals("some.main.class", gradleProjectProperties.getMainClassFromJar()); } @@ -282,28 +276,10 @@ public void testGetMainClassFromJar_missing() { @Test public void testIsWarProject() { - Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockProject.getPlugins().hasPlugin(WarPlugin.class)).thenReturn(true); Assert.assertTrue(gradleProjectProperties.isWarProject()); } - @Test - public void testGetWar_warProject() { - Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); - Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); - Assert.assertNotNull(TaskCommon.getWarTaskProvider(mockProject)); - } - - @Test - public void testGetWar_noWarPlugin() { - Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(false); - Assert.assertNull(TaskCommon.getWarTaskProvider(mockProject)); - } - - @Test - public void testGetWar_noWarTask() { - Assert.assertNull(TaskCommon.getWarTaskProvider(mockProject)); - } - @Test public void testConvertPermissionsMap() { Assert.assertEquals( @@ -587,6 +563,42 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } + @Test + public void testGetWarFilePath() { + Mockito.when(mockProject.getPlugins().hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); + Mockito.when(mockWarTaskProvider.get().getOutputs().getFiles().getAsPath()) + .thenReturn("/war/file/here.war"); + + Assert.assertEquals("/war/file/here.war", gradleProjectProperties.getWarFilePath()); + } + + @Test + public void testGetWarFilePath_bootWar() { + Mockito.when(mockProject.getPlugins().hasPlugin("org.springframework.boot")).thenReturn(true); + Mockito.when(mockTaskContainer.named("bootWar")).thenReturn(mockBootWarTaskProvider); + Mockito.when(mockBootWarTaskProvider.get().getEnabled()).thenReturn(true); + Mockito.when(mockBootWarTaskProvider.get().getOutputs().getFiles().getAsPath()) + .thenReturn("/boot/war/file.war"); + + Assert.assertEquals("/boot/war/file.war", gradleProjectProperties.getWarFilePath()); + } + + @Test + public void testGetWarFilePath_bootWarDisabled() { + Mockito.when(mockProject.getPlugins().hasPlugin("org.springframework.boot")).thenReturn(true); + Mockito.when(mockTaskContainer.named("bootWar")).thenReturn(mockBootWarTaskProvider); + Mockito.when(mockBootWarTaskProvider.get().getOutputs().getFiles().getAsPath()) + .thenReturn("boot.war"); + + Mockito.when(mockProject.getPlugins().hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); + Mockito.when(mockWarTaskProvider.get().getOutputs().getFiles().getAsPath()) + .thenReturn("war.war"); + + Assert.assertEquals("war.war", gradleProjectProperties.getWarFilePath()); + } + private BuildConfiguration setupBuildConfiguration( String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { @@ -607,11 +619,10 @@ private Path setUpWarProject(Path webAppDirectory) throws IOException { Path targetZip = zipUpDirectory(webAppDirectory, temporaryFolder.getRoot().toPath().resolve("my-app.war")); - Mockito.when(mockPluginContainer.hasPlugin(WarPlugin.class)).thenReturn(true); + Mockito.when(mockProject.getPlugins().hasPlugin(WarPlugin.class)).thenReturn(true); Mockito.when(mockTaskContainer.named("war")).thenReturn(mockWarTaskProvider); Mockito.when(mockWarTaskProvider.get().getOutputs().getFiles().getAsPath()) .thenReturn(targetZip.toString()); - Mockito.when(gradleProjectProperties.getWarFilePath()).thenReturn(targetZip.toString()); // Make "GradleProjectProperties" use this folder to explode the WAR into. Path unzipTarget = temporaryFolder.newFolder("exploded").toPath(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index f8fc789500..0a7fabcaf8 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -23,14 +23,15 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; import org.gradle.api.internal.project.ProjectInternal; -import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.testfixtures.ProjectBuilder; @@ -120,11 +121,10 @@ public void testCheckJibVersionNames() { @Test public void testCheckJibVersionInvoked() { - Project rootProject = - ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + Project project = createProject(); System.setProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME, "10000.0"); // not here yet try { - rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + project.getPluginManager().apply("com.google.cloud.tools.jib"); Assert.fail("should have failed"); } catch (GradleException ex) { // Gradle tests aren't run from a jar and so don't have an identifiable plugin version @@ -134,12 +134,11 @@ public void testCheckJibVersionInvoked() { } } + @SuppressWarnings("unchecked") @Test public void testProjectDependencyAssembleTasksAreRun() { // root project is our jib packaged service - Project rootProject = - ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); - rootProject.getPluginManager().apply("java"); + Project rootProject = createProject("java"); // our service DOES depend on this, and jib should trigger an assemble from this project Project subProject = @@ -177,10 +176,10 @@ public void testProjectDependencyAssembleTasksAreRun() { // check by applying the jib plugin and inspect the task dependencies rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + TaskContainer tasks = rootProject.getTasks(); // add a custom task that our jib tasks depend on to ensure we do not overwrite this dependsOn TaskProvider dependencyTask = rootProject.getTasks().register("myCustomTask", task -> {}); - KNOWN_JIB_TASKS.forEach( - taskName -> rootProject.getTasks().getByPath(taskName).dependsOn(dependencyTask)); + KNOWN_JIB_TASKS.forEach(taskName -> tasks.getByPath(taskName).dependsOn(dependencyTask)); ((ProjectInternal) rootProject).evaluate(); @@ -188,58 +187,90 @@ public void testProjectDependencyAssembleTasksAreRun() { taskName -> Assert.assertEquals( ImmutableSet.of(":sub:assemble", ":classes", ":myCustomTask"), - rootProject - .getTasks() + tasks .getByPath(taskName) .getDependsOn() .stream() - .map(TaskProvider.class::cast) - .map(TaskProvider::get) - .map(Task.class::cast) - .map(Task::getPath) + .map( + object -> + object instanceof List ? object : Collections.singletonList(object)) + .map(List.class::cast) + .flatMap(List::stream) + .map(object -> ((TaskProvider) object).get().getPath()) .collect(Collectors.toSet()))); } @SuppressWarnings("unchecked") @Test public void testWebAppProject() { - Project rootProject = - ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); - rootProject.getPluginManager().apply("java"); - rootProject.getPluginManager().apply("war"); - rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); + Project project = createProject("java", "war", "com.google.cloud.tools.jib"); - ((ProjectInternal) rootProject).evaluate(); - TaskContainer tasks = rootProject.getTasks(); - Task warTask = tasks.getByPath(":" + WarPlugin.WAR_TASK_NAME); + ((ProjectInternal) project).evaluate(); + TaskContainer tasks = project.getTasks(); + Task warTask = tasks.getByPath(":war"); Assert.assertNotNull(warTask); - Assert.assertEquals( - warTask, - ((TaskProvider) - tasks.getByPath(JibPlugin.BUILD_IMAGE_TASK_NAME).getDependsOn().iterator().next()) - .get()); - Assert.assertEquals( - warTask, - ((TaskProvider) - tasks.getByPath(JibPlugin.BUILD_DOCKER_TASK_NAME).getDependsOn().iterator().next()) - .get()); - Assert.assertEquals( - warTask, - ((TaskProvider) - tasks.getByPath(JibPlugin.BUILD_TAR_TASK_NAME).getDependsOn().iterator().next()) - .get()); + + for (String taskName : KNOWN_JIB_TASKS) { + List> taskProviders = + (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); + Assert.assertEquals(1, taskProviders.size()); + Assert.assertEquals(warTask, taskProviders.get(0).get()); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testWebAppProject_bootWar() { + Project project = + createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); + ((ProjectInternal) project).evaluate(); + + TaskContainer tasks = project.getTasks(); + Task warTask = tasks.getByPath(":war"); + Task bootWarTask = tasks.getByPath(":bootWar"); + Assert.assertNotNull(warTask); + Assert.assertNotNull(bootWarTask); + + for (String taskName : KNOWN_JIB_TASKS) { + List> taskProviders = + (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); + Assert.assertEquals( + ImmutableSet.of(warTask, bootWarTask), + taskProviders.stream().map(TaskProvider::get).collect(Collectors.toSet())); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testWebAppProject_bootWarDisabled() { + Project project = + createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); + ((ProjectInternal) project).evaluate(); + + TaskContainer tasks = project.getTasks(); + Task warTask = tasks.getByPath(":war"); + Task bootWarTask = tasks.getByPath(":bootWar"); + Assert.assertNotNull(warTask); + Assert.assertNotNull(bootWarTask); + bootWarTask.setEnabled(false); // should depend on bootWar even if disabled + + for (String taskName : KNOWN_JIB_TASKS) { + List> taskProviders = + (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); + Assert.assertEquals( + ImmutableSet.of(warTask, bootWarTask), + taskProviders.stream().map(TaskProvider::get).collect(Collectors.toSet())); + } } @Test public void testNonWebAppProject() { - Project rootProject = - ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); - rootProject.getPluginManager().apply("java"); - rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); - ((ProjectInternal) rootProject).evaluate(); - TaskContainer tasks = rootProject.getTasks(); + Project project = createProject("java", "com.google.cloud.tools.jib"); + ((ProjectInternal) project).evaluate(); + + TaskContainer tasks = project.getTasks(); try { - tasks.getByPath(":" + WarPlugin.WAR_TASK_NAME); + tasks.getByPath(":war"); Assert.fail(); } catch (UnknownTaskException ex) { Assert.assertNotNull(ex.getMessage()); @@ -248,14 +279,18 @@ public void testNonWebAppProject() { @Test public void testJibTaskGroupIsSet() { - Project rootProject = - ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); - rootProject.getPluginManager().apply("java"); - rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); - ((ProjectInternal) rootProject).evaluate(); - TaskContainer tasks = rootProject.getTasks(); + Project project = createProject("java", "com.google.cloud.tools.jib"); + ((ProjectInternal) project).evaluate(); + TaskContainer tasks = project.getTasks(); KNOWN_JIB_TASKS.forEach( taskName -> Assert.assertEquals(taskName, "Jib", tasks.getByPath(taskName).getGroup())); } + + private Project createProject(String... plugins) { + Project project = + ProjectBuilder.builder().withProjectDir(testProjectRoot.getRoot()).withName("root").build(); + Arrays.asList(plugins).forEach(project.getPluginManager()::apply); + return project; + } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 26891b5fb6..a5b625bd0a 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -22,6 +22,7 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.War; import org.gradle.testfixtures.ProjectBuilder; import org.junit.Assert; import org.junit.Before; @@ -161,40 +162,27 @@ public void testGetWarTask_normalJavaProject() { project.getPlugins().apply(JavaPlugin.class); TaskProvider warProviderTask = TaskCommon.getWarTaskProvider(project); - Assert.assertNull(warProviderTask); } - @Test - public void testGetWarTask_bootJavaProject() { - Project project = ProjectBuilder.builder().build(); - project.getPlugins().apply(JavaPlugin.class); - project.getPlugins().apply(SpringBootPlugin.class); - - TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - - Assert.assertNull(warTask); - } - @Test public void testGetWarTask_normalWarProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(WarPlugin.class); TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - Assert.assertNotNull(warTask); + Assert.assertNotNull(warTask instanceof War); } @Test - public void testGetWarTask_bootWarProject() { + public void testGetBootWarTask_bootWarProject() { Project project = ProjectBuilder.builder().build(); project.getPlugins().apply(WarPlugin.class); project.getPlugins().apply(SpringBootPlugin.class); - TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - - Assert.assertNotNull(warTask); - Assert.assertTrue(warTask.get() instanceof BootWar); + TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + Assert.assertNotNull(bootWarTask); + Assert.assertNotNull(bootWarTask instanceof BootWar); } } From 9d84f949a5419fb89cb809cee67ebdb6223e8648 Mon Sep 17 00:00:00 2001 From: Guillaume Le Floch Date: Thu, 31 Oct 2019 21:46:36 +0100 Subject: [PATCH 0804/2020] remove deprecation warnings (#2113) --- .../com/google/cloud/tools/jib/gradle/ContainerParameters.java | 1 - .../java/com/google/cloud/tools/jib/gradle/JibExtension.java | 1 - 2 files changed, 2 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index 21d1831e28..c9c3615efd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -51,7 +51,6 @@ public class ContainerParameters { private String creationTime = "EPOCH"; @Input - @Optional public boolean getUseCurrentTimestamp() { if (System.getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP) != null) { return Boolean.getBoolean(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 8030f5b73d..8f2d49aa00 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -192,7 +192,6 @@ public OutputPathsParameters getOutputPaths() { } @Input - @Optional boolean getAllowInsecureRegistries() { if (System.getProperty(PropertyNames.ALLOW_INSECURE_REGISTRIES) != null) { return Boolean.getBoolean(PropertyNames.ALLOW_INSECURE_REGISTRIES); From 426ee38fd56d7c3b0b74892e5775cca479c9f3f6 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 31 Oct 2019 21:48:10 -0400 Subject: [PATCH 0805/2020] Add "dev" and "devFull" to build (#2114) * Add "dev" and "devFull" to build `gradlew dev`: formats the code + runs the build + runs tests `gradle devFull`: does all of `dev` + run integration tests --- build.gradle | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/build.gradle b/build.gradle index f0be4cc39c..4a64e88320 100644 --- a/build.gradle +++ b/build.gradle @@ -351,3 +351,25 @@ subprojects { } /* INCLUDED PROJECT DEPENDENCY HELPER */ } + + +tasks.register("dev") { + subprojects.each { subproject -> + subproject.tasks.check.dependsOn subproject.tasks.googleJavaFormat + dependsOn subproject.tasks.check + } +} + +tasks.register("devFull") { + if (!System.getenv('JIB_INTEGRATION_TESTING_PROJECT') + && !System.getenv('JIB_INTEGRATION_TESTING_LOCATION')) { + throw new GradleException( + "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " + + "GCP project to use for integration testing or " + + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); + } + dependsOn tasks.dev + subprojects.each { subproject -> + dependsOn subproject.tasks.integrationTest + } +} From a3d5ab6b8eb47dcd1415d41c4620a87e8c488a4d Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 1 Nov 2019 12:28:54 -0400 Subject: [PATCH 0806/2020] add javadoc check to dev, cleanup spaces (#2116) --- build.gradle | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 4a64e88320..bad6e6b992 100644 --- a/build.gradle +++ b/build.gradle @@ -353,23 +353,26 @@ subprojects { } +/* LOCAL DEVELOPMENT HELPER TASKS */ tasks.register("dev") { subprojects.each { subproject -> subproject.tasks.check.dependsOn subproject.tasks.googleJavaFormat dependsOn subproject.tasks.check + dependsOn subproject.tasks.javadoc } } tasks.register("devFull") { - if (!System.getenv('JIB_INTEGRATION_TESTING_PROJECT') + if (!System.getenv('JIB_INTEGRATION_TESTING_PROJECT') && !System.getenv('JIB_INTEGRATION_TESTING_LOCATION')) { throw new GradleException( - "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " - + "GCP project to use for integration testing or " - + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); + "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " + + "GCP project to use for integration testing or " + + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); } dependsOn tasks.dev subprojects.each { subproject -> dependsOn subproject.tasks.integrationTest } } +/* LOCAL DEVELOPMENT HELPER TASKS */ From 3030cde3f4790c06b87062215a19ad0219442d0f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 1 Nov 2019 15:56:44 -0400 Subject: [PATCH 0807/2020] New HTTP framework to incorporate automatic insecure connection handling (#2100) --- jib-core/CHANGELOG.md | 2 + .../ManifestPusherIntegrationTest.java | 6 +- .../jib/api/InsecureRegistryException.java | 5 +- .../api/RegistryUnauthorizedException.java | 3 +- .../cloud/tools/jib/http/Connection.java | 231 +++++++--- .../google/cloud/tools/jib/http/Response.java | 8 +- .../tools/jib/http/ResponseException.java | 56 +++ .../AuthenticationMethodRetriever.java | 14 +- .../cloud/tools/jib/registry/BlobChecker.java | 6 +- .../tools/jib/registry/ErrorResponseUtil.java | 7 +- .../tools/jib/registry/ManifestPusher.java | 6 +- .../jib/registry/RegistryAuthenticator.java | 66 ++- .../tools/jib/registry/RegistryClient.java | 15 +- .../jib/registry/RegistryEndpointCaller.java | 163 ++----- .../registry/RegistryEndpointProvider.java | 9 +- .../cloud/tools/jib/http/ConnectionTest.java | 353 ++++++++++++--- .../ConnectionWithProxyCredentialsTest.java | 1 + .../cloud/tools/jib/http/ResponseTest.java | 6 +- .../jib/http/WithServerConnectionTest.java | 91 ++-- .../AuthenticationMethodRetrieverTest.java | 18 +- .../tools/jib/registry/BlobCheckerTest.java | 16 +- .../jib/registry/ErrorResponseUtilTest.java | 12 +- .../jib/registry/ManifestPusherTest.java | 19 +- .../registry/RegistryAuthenticatorTest.java | 141 +++--- .../registry/RegistryEndpointCallerTest.java | 407 ++++-------------- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + .../jib/plugins/common/JibBuildRunner.java | 6 +- 28 files changed, 916 insertions(+), 753 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 203a9a7d52..e06e3c9785 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) + ## 0.12.0 ### Added diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index 88e8e7f982..d7c0410b2c 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; @@ -24,6 +23,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; @@ -58,9 +58,9 @@ public void testPush_missingBlobs() throws IOException, RegistryException { Assert.fail("Pushing manifest without its BLOBs should fail"); } catch (RegistryErrorException ex) { - HttpResponseException httpResponseException = (HttpResponseException) ex.getCause(); + ResponseException responseException = (ResponseException) ex.getCause(); Assert.assertEquals( - HttpStatusCodes.STATUS_CODE_BAD_REQUEST, httpResponseException.getStatusCode()); + HttpStatusCodes.STATUS_CODE_BAD_REQUEST, responseException.getStatusCode()); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java index 82722ff557..588e38f3e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java @@ -23,10 +23,11 @@ */ public class InsecureRegistryException extends RegistryException { - public InsecureRegistryException(URL insecureUrl) { + public InsecureRegistryException(URL insecureUrl, Throwable cause) { super( "Failed to verify the server at " + insecureUrl - + " because only secure connections are allowed."); + + " because only secure connections are allowed.", + cause); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java index 4a0132143e..602ec3e9fd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.api; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.http.ResponseException; /** Thrown when a registry request was unauthorized and therefore authentication is needed. */ public class RegistryUnauthorizedException extends RegistryException { @@ -32,7 +33,7 @@ public class RegistryUnauthorizedException extends RegistryException { * @param cause the cause */ public RegistryUnauthorizedException( - String registry, String repository, HttpResponseException cause) { + String registry, String repository, ResponseException cause) { super("Unauthorized for " + registry + "/" + repository, cause); this.registry = registry; this.repository = repository; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java index 6e678cbe21..f3671a6188 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java @@ -17,45 +17,70 @@ package com.google.cloud.tools.jib.http; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import java.io.Closeable; import java.io.IOException; +import java.net.ConnectException; import java.net.URL; import java.security.GeneralSecurityException; -import java.util.function.Function; -import javax.annotation.Nullable; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.net.ssl.SSLException; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.impl.client.DefaultHttpClient; /** - * Sends an HTTP {@link Request} and stores the {@link Response}. Clients should not send more than - * one request. + * Thread-safe HTTP client that can automatically failover from secure HTTPS to insecure HTTPS or + * HTTP. Intended to be created once and shared to be called at multiple places. Callers should + * close the returned {@link Response}. * - *

Example usage: + *

The failover (if enabled) in the following way: * - *

{@code
- * try (Connection connection = new Connection(url)) {
- *   Response response = connection.get(request);
- *   // ... process the response
- * }
- * }
+ *
    + *
  • When a port is provided (for example {@code my-registry:5000/my-repo}): + *
      + *
    1. Attempts secure HTTPS on the specified port. + *
    2. If (1) fails due to {@link SSLException}, re-attempts secure HTTPS on the specified + * port but disabling certificate validation. + *
    3. If (2) fails again due to {@link SSLException}, attempts plain-HTTP on the specified + * port. + *
    + *
  • When a port is not provided (for example {@code my-registry/my-repo}): + *
      + *
    1. Attempts secure HTTPS on port 443 (default HTTPS port). + *
    2. If (1) fails due to {@link SSLException}, re-attempts secure HTTPS on port 443 but + * disabling certificate validation. + *
    3. If (2) fails again due to {@link SSLException}, attempts plain-HTTP on port 80 + * (default HTTP port). + *
    4. Or, if (1) fails due to non-timeout {@link ConnectException}, attempts plain-HTTP on + * port 80. + *
    + *
+ * + * This failover behavior is similar to how the Docker client works: + * https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry */ -public class Connection implements Closeable { +public class Connection { // TODO: rename to TlsFailoverHttpClient - /** - * Returns a factory for {@link Connection}. - * - * @return {@link Connection} factory, a function that generates a {@link Connection} to a URL - */ - public static Function getConnectionFactory() { + private static boolean isHttpsProtocol(URL url) { + return "https".equals(url.getProtocol()); + } + + private static URL toHttp(URL url) { + GenericUrl httpUrl = new GenericUrl(url); + httpUrl.setScheme("http"); + return httpUrl.toURL(); + } + + private static HttpTransport getSecureHttpTransport() { // Do not use NetHttpTransport. It does not process response errors properly. // See https://github.com/google/google-http-java-client/issues/39 // @@ -63,22 +88,18 @@ public static Function getConnectionFactory() { // connection persistence causes the connection to throw NoHttpResponseException. ApacheHttpTransport transport = new ApacheHttpTransport(); addProxyCredentials(transport); - return url -> new Connection(url, transport); + return transport; } - /** - * Returns a factory for {@link Connection} that does not verify TLS peer verification. - * - * @throws GeneralSecurityException if unable to turn off TLS peer verification - * @return {@link Connection} factory, a function that generates a {@link Connection} to a URL - */ - public static Function getInsecureConnectionFactory() - throws GeneralSecurityException { - // Do not use NetHttpTransport. See comments in getConnectionFactory for details. - ApacheHttpTransport transport = - new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); - addProxyCredentials(transport); - return url -> new Connection(url, transport); + private static HttpTransport getInsecureHttpTransport() { + try { + ApacheHttpTransport insecureTransport = + new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); + addProxyCredentials(insecureTransport); + return insecureTransport; + } catch (GeneralSecurityException ex) { + throw new RuntimeException("platform does not support TLS protocol", ex); + } } /** @@ -114,87 +135,157 @@ private static void addProxyCredentials(ApacheHttpTransport transport, String pr new UsernamePasswordCredentials(proxyUser, proxyPassword)); } - private HttpRequestFactory requestFactory; - - @Nullable private HttpResponse httpResponse; - - /** The URL to send the request to. */ - private final GenericUrl url; + private final boolean enableHttpAndInsecureFailover; + private final boolean sendAuthorizationOverHttp; + private final Consumer logger; + private final Supplier secureHttpTransportFactory; + private final Supplier insecureHttpTransportFactory; - /** - * Make sure to wrap with a try-with-resource to ensure that the connection is closed after usage. - * - * @param url the url to send the request to - */ - @VisibleForTesting - Connection(URL url, HttpTransport transport) { - this.url = new GenericUrl(url); - requestFactory = transport.createRequestFactory(); + public Connection( + boolean enableHttpAndInsecureFailover, + boolean sendAuthorizationOverHttp, + Consumer logger) { + this( + enableHttpAndInsecureFailover, + sendAuthorizationOverHttp, + logger, + Connection::getSecureHttpTransport, + Connection::getInsecureHttpTransport); } - @Override - public void close() throws IOException { - if (httpResponse == null) { - return; - } - - httpResponse.disconnect(); + @VisibleForTesting + Connection( + boolean enableHttpAndInsecureFailover, + boolean sendAuthorizationOverHttp, + Consumer logger, + Supplier secureHttpTransportFactory, + Supplier insecureHttpTransportFactory) { + this.enableHttpAndInsecureFailover = enableHttpAndInsecureFailover; + this.sendAuthorizationOverHttp = sendAuthorizationOverHttp; + this.logger = logger; + this.secureHttpTransportFactory = secureHttpTransportFactory; + this.insecureHttpTransportFactory = insecureHttpTransportFactory; } /** * Sends the request with method GET. * + * @param url endpoint URL * @param request the request to send * @return the response to the sent request * @throws IOException if sending the request fails */ - public Response get(Request request) throws IOException { - return send(HttpMethods.GET, request); + public Response get(URL url, Request request) throws IOException { + return call(HttpMethods.GET, url, request); } /** * Sends the request with method POST. * + * @param url endpoint URL * @param request the request to send * @return the response to the sent request * @throws IOException if sending the request fails */ - public Response post(Request request) throws IOException { - return send(HttpMethods.POST, request); + public Response post(URL url, Request request) throws IOException { + return call(HttpMethods.POST, url, request); } /** * Sends the request with method PUT. * + * @param url endpoint URL * @param request the request to send * @return the response to the sent request * @throws IOException if sending the request fails */ - public Response put(Request request) throws IOException { - return send(HttpMethods.PUT, request); + public Response put(URL url, Request request) throws IOException { + return call(HttpMethods.PUT, url, request); } /** * Sends the request. * * @param httpMethod the HTTP request method + * @param url endpoint URL * @param request the request to send * @return the response to the sent request * @throws IOException if building the HTTP request fails. */ - public Response send(String httpMethod, Request request) throws IOException { - Preconditions.checkState(httpResponse == null, "Connection can send only one request"); + public Response call(String httpMethod, URL url, Request request) throws IOException { + if (!isHttpsProtocol(url) && !enableHttpAndInsecureFailover) { + throw new SSLException("insecure HTTP connection not allowed: " + url); + } + + try { + return call(httpMethod, url, request, secureHttpTransportFactory.get()); + + } catch (SSLException ex) { + if (!enableHttpAndInsecureFailover) { + throw ex; + } + + try { + logInsecureHttpsFailover(url); + return call(httpMethod, url, request, insecureHttpTransportFactory.get()); + + } catch (SSLException ignored) { // This is usually when the server is plain-HTTP. + logHttpFailover(url); + return call(httpMethod, toHttp(url), request, secureHttpTransportFactory.get()); + } + + } catch (ConnectException ex) { + // It is observed that Open/Oracle JDKs sometimes throw SocketTimeoutException but other times + // ConnectException for connection timeout. (Could be a JDK bug.) Note SocketTimeoutException + // does not extend ConnectException (or vice versa), and we want to be consistent to error out + // on timeouts: https://github.com/GoogleContainerTools/jib/issues/1895#issuecomment-527544094 + if (ex.getMessage() != null && ex.getMessage().contains("timed out")) { + throw ex; + } + + // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS + // port 443) and we could not connect to 443. It's worth trying port 80. + if (enableHttpAndInsecureFailover && isHttpsProtocol(url) && url.getPort() == -1) { + logHttpFailover(url); + return call(httpMethod, toHttp(url), request, secureHttpTransportFactory.get()); + } + throw ex; + } + } + + private Response call(String httpMethod, URL url, Request request, HttpTransport httpTransport) + throws IOException { + boolean clearAuthorization = !isHttpsProtocol(url) && !sendAuthorizationOverHttp; + + HttpHeaders requestHeaders = + clearAuthorization + ? request.getHeaders().clone().setAuthorization((String) null) // deep clone implemented + : request.getHeaders(); HttpRequest httpRequest = - requestFactory - .buildRequest(httpMethod, url, request.getHttpContent()) - .setHeaders(request.getHeaders()); + httpTransport + .createRequestFactory() + .buildRequest(httpMethod, new GenericUrl(url), request.getHttpContent()) + .setHeaders(requestHeaders); if (request.getHttpTimeout() != null) { httpRequest.setConnectTimeout(request.getHttpTimeout()); httpRequest.setReadTimeout(request.getHttpTimeout()); } - httpResponse = httpRequest.execute(); - return new Response(httpResponse); + try { + return new Response(httpRequest.execute()); + } catch (HttpResponseException ex) { + throw new ResponseException(ex, clearAuthorization); + } + } + + private void logHttpFailover(URL url) { + String log = "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP."; + logger.accept(LogEvent.info(log)); + } + + private void logInsecureHttpsFailover(URL url) { + String log = "Cannot verify server at " + url + ". Attempting again with no TLS verification."; + logger.accept(LogEvent.info(log)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java index 3c7f895586..e8bd74ee1d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java @@ -19,12 +19,13 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpResponse; import com.google.common.net.HttpHeaders; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.List; /** Holds an HTTP response. */ -public class Response { +public class Response implements Closeable { private final HttpResponse httpResponse; @@ -75,4 +76,9 @@ public InputStream getBody() throws IOException { public GenericUrl getRequestUrl() { return httpResponse.getRequest().getUrl(); } + + @Override + public void close() throws IOException { + httpResponse.disconnect(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java new file mode 100644 index 0000000000..d798e01ce0 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.http; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponseException; +import java.io.IOException; + +/** Holds an HTTP response exception. */ +public class ResponseException extends IOException { + + private final HttpResponseException httpResponseException; + private final boolean requestAuthorizationCleared; + + ResponseException( + HttpResponseException httpResponseException, boolean requestAuthorizationCleared) { + super(httpResponseException.getMessage(), httpResponseException); + this.httpResponseException = httpResponseException; + this.requestAuthorizationCleared = requestAuthorizationCleared; + } + + public int getStatusCode() { + return httpResponseException.getStatusCode(); + } + + public String getContent() { + return httpResponseException.getContent(); + } + + public HttpHeaders getHeaders() { + return httpResponseException.getHeaders(); + } + + /** + * Returns whether the {@code Authorization} HTTP header was cleared (and thus not sent). + * + * @return whether the {@code Authorization} HTTP header was cleared + */ + public boolean requestAuthorizationCleared() { + return requestAuthorizationCleared; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index eba5a28971..5176cec504 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -17,11 +17,12 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.http.BlobHttpContent; +import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -35,11 +36,15 @@ class AuthenticationMethodRetriever private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String userAgent; + private final Connection httpClient; AuthenticationMethodRetriever( - RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent) { + RegistryEndpointRequestProperties registryEndpointRequestProperties, + String userAgent, + Connection httpClient) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; + this.httpClient = httpClient; } @Nullable @@ -81,8 +86,7 @@ public String getActionDescription() { @Override public Optional handleHttpResponseException( - HttpResponseException responseException) - throws HttpResponseException, RegistryErrorException { + ResponseException responseException) throws ResponseException, RegistryErrorException { // Only valid for status code of '401 Unauthorized'. if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { throw responseException; @@ -99,7 +103,7 @@ public Optional handleHttpResponseException( // Parses the header to retrieve the components. try { return RegistryAuthenticator.fromAuthenticationMethod( - authenticationMethod, registryEndpointRequestProperties, userAgent); + authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient); } catch (RegistryAuthenticationFailedException ex) { throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java index eb3b3bb1ce..ff4e4c2a4d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java @@ -17,12 +17,12 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -60,8 +60,8 @@ public Optional handleResponse(Response response) throws Registr } @Override - public Optional handleHttpResponseException( - HttpResponseException responseException) throws HttpResponseException { + public Optional handleHttpResponseException(ResponseException responseException) + throws ResponseException { if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw responseException; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java index 6548317f7a..afe63988a0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; @@ -32,11 +33,11 @@ public class ErrorResponseUtil { * * @param responseException the response exception * @return the parsed {@link ErrorCodes} if found - * @throws HttpResponseException rethrows the original exception if an error object could not be + * @throws ResponseException rethrows the original exception if an error object could not be * parsed, if there were multiple error objects, or if the error code is unknown. */ - public static ErrorCodes getErrorCode(HttpResponseException responseException) - throws HttpResponseException { + public static ErrorCodes getErrorCode(ResponseException responseException) + throws ResponseException { // Obtain the error response code. String errorContent = responseException.getContent(); if (errorContent == null) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java index e0797034b4..381fda1337 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.blob.Blobs; @@ -25,6 +24,7 @@ import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import java.io.IOException; import java.net.MalformedURLException; @@ -92,8 +92,8 @@ public List getAccept() { } @Override - public DescriptorDigest handleHttpResponseException(HttpResponseException responseException) - throws HttpResponseException, RegistryErrorException { + public DescriptorDigest handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { // docker registry 2.0 and 2.1 returns: // 400 Bad Request // {"errors":[{"code":"TAG_INVALID","message":"manifest tag did not match URI"}]} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 05638595b7..8cd76441ab 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.blob.Blobs; @@ -27,6 +28,7 @@ import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; @@ -58,6 +60,7 @@ public class RegistryAuthenticator { * @param authenticationMethod the {@code WWW-Authenticate} header value * @param registryEndpointRequestProperties the registry request properties * @param userAgent the {@code User-Agent} header value to use in later authentication calls + * @param httpClient HTTP client * @return a new {@link RegistryAuthenticator} for authenticating with the registry service * @throws RegistryAuthenticationFailedException if authentication fails * @see fromAuthenticationMethod( String authenticationMethod, RegistryEndpointRequestProperties registryEndpointRequestProperties, - String userAgent) + String userAgent, + Connection httpClient) throws RegistryAuthenticationFailedException { // If the authentication method starts with 'basic ' (case insensitive), no registry // authentication is needed. @@ -96,7 +100,8 @@ static Optional fromAuthenticationMethod( String service = serviceMatcher.find() ? serviceMatcher.group(1) : registryUrl; return Optional.of( - new RegistryAuthenticator(realm, service, registryEndpointRequestProperties, userAgent)); + new RegistryAuthenticator( + realm, service, registryEndpointRequestProperties, userAgent, httpClient)); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( @@ -138,16 +143,19 @@ private String getToken() { private final String realm; private final String service; private final String userAgent; + private final Connection httpClient; private RegistryAuthenticator( String realm, String service, RegistryEndpointRequestProperties registryEndpointRequestProperties, - String userAgent) { + String userAgent, + Connection httpClient) { this.realm = realm; this.service = service; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; + this.httpClient = httpClient; } /** @@ -156,9 +164,11 @@ private RegistryAuthenticator( * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the pull * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were + * not sent over plain HTTP */ public Authorization authenticatePull(@Nullable Credential credential) - throws RegistryAuthenticationFailedException { + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { return authenticate(credential, "pull"); } @@ -168,9 +178,11 @@ public Authorization authenticatePull(@Nullable Credential credential) * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the push * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were + * not sent over plain HTTP */ public Authorization authenticatePush(@Nullable Credential credential) - throws RegistryAuthenticationFailedException { + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { return authenticate(credential, "pull,push"); } @@ -220,11 +232,13 @@ boolean isOAuth2Auth(@Nullable Credential credential) { * @param scope the scope of permissions to authenticate for * @return the {@link Authorization} response * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were + * not sent over plain HTTP * @see https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate */ private Authorization authenticate(@Nullable Credential credential, String scope) - throws RegistryAuthenticationFailedException { + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { // try authorizing against both the main repository and the source repository too // to enable cross-repository mounts on pushes String sourceImageName = registryEndpointRequestProperties.getSourceImageName(); @@ -241,12 +255,12 @@ private Authorization authenticate(@Nullable Credential credential, String scope private Authorization authenticate( @Nullable Credential credential, Map repositoryScopes) - throws RegistryAuthenticationFailedException { + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { String registryUrl = registryEndpointRequestProperties.getServerUrl(); String imageName = registryEndpointRequestProperties.getImageName(); - try (Connection connection = - Connection.getConnectionFactory() - .apply(getAuthenticationUrl(credential, repositoryScopes))) { + try { + URL url = getAuthenticationUrl(credential, repositoryScopes); + Request.Builder requestBuilder = Request.builder() .setHttpTimeout(JibSystemProperties.getHttpTimeout()) @@ -262,21 +276,29 @@ private Authorization authenticate( } String httpMethod = isOAuth2Auth(credential) ? HttpMethods.POST : HttpMethods.GET; - Response response = connection.send(httpMethod, requestBuilder.build()); + try (Response response = httpClient.call(httpMethod, url, requestBuilder.build())) { + + AuthenticationResponseTemplate responseJson = + JsonTemplateMapper.readJson(response.getBody(), AuthenticationResponseTemplate.class); - AuthenticationResponseTemplate responseJson = - JsonTemplateMapper.readJson(response.getBody(), AuthenticationResponseTemplate.class); + if (responseJson.getToken() == null) { + throw new RegistryAuthenticationFailedException( + registryUrl, + imageName, + "Did not get token in authentication response from " + + getAuthenticationUrl(credential, repositoryScopes) + + "; parameters: " + + getAuthRequestParameters(credential, repositoryScopes)); + } + return Authorization.fromBearerToken(responseJson.getToken()); + } - if (responseJson.getToken() == null) { - throw new RegistryAuthenticationFailedException( - registryUrl, - imageName, - "Did not get token in authentication response from " - + getAuthenticationUrl(credential, repositoryScopes) - + "; parameters: " - + getAuthRequestParameters(credential, repositoryScopes)); + } catch (ResponseException ex) { + if (ex.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED + && ex.requestAuthorizationCleared()) { + throw new RegistryCredentialsNotSentException(registryUrl, imageName); } - return Authorization.fromBearerToken(responseJson.getToken()); + throw new RegistryAuthenticationFailedException(registryUrl, imageName, ex); } catch (IOException ex) { throw new RegistryAuthenticationFailedException(registryUrl, imageName, ex); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 834d040c2a..1214f6aa3b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -28,6 +28,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; @@ -242,8 +243,8 @@ static Multimap decodeTokenRepositoryGrants(String token) { private final EventHandlers eventHandlers; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowInsecureRegistries; private final String userAgent; + private final Connection httpClient; /** * Instantiate with {@link #factory}. @@ -252,6 +253,7 @@ static Multimap decodeTokenRepositoryGrants(String token) { * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed + * @param userAgent {@code User-Agent} header to send with the request */ private RegistryClient( EventHandlers eventHandlers, @@ -262,8 +264,12 @@ private RegistryClient( this.eventHandlers = eventHandlers; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowInsecureRegistries = allowInsecureRegistries; this.userAgent = userAgent; + this.httpClient = + new Connection( + allowInsecureRegistries, + JibSystemProperties.sendCredentialsOverHttp(), + eventHandlers::dispatch); } /** @@ -277,7 +283,8 @@ public Optional getRegistryAuthenticator() // Gets the WWW-Authenticate header (eg. 'WWW-Authenticate: Bearer // realm="https://gcr.io/v2/token",service="gcr.io"') return callRegistryEndpoint( - new AuthenticationMethodRetriever(registryEndpointRequestProperties, getUserAgent())); + new AuthenticationMethodRetriever( + registryEndpointRequestProperties, getUserAgent(), httpClient)); } /** @@ -465,7 +472,7 @@ private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointP registryEndpointProvider, authorization, registryEndpointRequestProperties, - allowInsecureRegistries) + httpClient) .call(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index c0a40be4ae..49cc444716 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -16,8 +16,6 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.LogEvent; @@ -29,16 +27,14 @@ import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.net.ConnectException; import java.net.URL; -import java.security.GeneralSecurityException; import java.util.Locale; -import java.util.function.Function; import javax.annotation.Nullable; import javax.net.ssl.SSLException; @@ -55,10 +51,6 @@ class RegistryEndpointCaller { */ @VisibleForTesting static final int STATUS_CODE_PERMANENT_REDIRECT = 308; - private static boolean isHttpsProtocol(URL url) { - return "https".equals(url.getProtocol()); - } - // https://github.com/GoogleContainerTools/jib/issues/1316 @VisibleForTesting static boolean isBrokenPipe(IOException original) { @@ -82,13 +74,7 @@ static boolean isBrokenPipe(IOException original) { private final RegistryEndpointProvider registryEndpointProvider; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final boolean allowInsecureRegistries; - - /** Makes a {@link Connection} to the specified {@link URL}. */ - private final Function connectionFactory; - - /** Makes an insecure {@link Connection} to the specified {@link URL}. */ - @Nullable private Function insecureConnectionFactory; + private final Connection httpClient; /** * Constructs with parameters for making the request. @@ -98,26 +84,8 @@ static boolean isBrokenPipe(IOException original) { * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request - * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed + * @param httpClient HTTP client */ - RegistryEndpointCaller( - EventHandlers eventHandlers, - String userAgent, - RegistryEndpointProvider registryEndpointProvider, - @Nullable Authorization authorization, - RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowInsecureRegistries) { - this( - eventHandlers, - userAgent, - registryEndpointProvider, - authorization, - registryEndpointRequestProperties, - allowInsecureRegistries, - Connection.getConnectionFactory(), - null /* might never be used, so create lazily to delay throwing potential GeneralSecurityException */); - } - @VisibleForTesting RegistryEndpointCaller( EventHandlers eventHandlers, @@ -125,17 +93,13 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowInsecureRegistries, - Function connectionFactory, - @Nullable Function insecureConnectionFactory) { + Connection httpClient) { this.eventHandlers = eventHandlers; this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - this.allowInsecureRegistries = allowInsecureRegistries; - this.connectionFactory = connectionFactory; - this.insecureConnectionFactory = insecureConnectionFactory; + this.httpClient = httpClient; } /** @@ -148,73 +112,7 @@ static boolean isBrokenPipe(IOException original) { T call() throws IOException, RegistryException { String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); - return callWithAllowInsecureRegistryHandling(initialRequestUrl); - } - - private T callWithAllowInsecureRegistryHandling(URL url) throws IOException, RegistryException { - if (!isHttpsProtocol(url) && !allowInsecureRegistries) { - throw new InsecureRegistryException(url); - } - - try { - return call(url, connectionFactory); - - } catch (SSLException ex) { - return handleUnverifiableServerException(url); - - } catch (ConnectException ex) { - // It is observed that Open/Oracle JDKs sometimes throw SocketTimeoutException but other times - // ConnectException for connection timeout. (Could be a JDK bug.) Note SocketTimeoutException - // does not extend ConnectException (or vice versa), and we want to be consistent to error out - // on timeouts: https://github.com/GoogleContainerTools/jib/issues/1895#issuecomment-527544094 - if (ex.getMessage() != null && ex.getMessage().contains("timed out")) { - throw ex; - } - - if (allowInsecureRegistries && isHttpsProtocol(url) && url.getPort() == -1) { - // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS - // port 443) and we could not connect to 443. It's worth trying port 80. - return fallBackToHttp(url); - } - throw ex; - } - } - - private T handleUnverifiableServerException(URL url) throws IOException, RegistryException { - if (!allowInsecureRegistries) { - throw new InsecureRegistryException(url); - } - - try { - eventHandlers.dispatch( - LogEvent.info( - "Cannot verify server at " + url + ". Attempting again with no TLS verification.")); - return call(url, getInsecureConnectionFactory()); - - } catch (SSLException ex) { - return fallBackToHttp(url); - } - } - - private T fallBackToHttp(URL url) throws IOException, RegistryException { - GenericUrl httpUrl = new GenericUrl(url); - httpUrl.setScheme("http"); - eventHandlers.dispatch( - LogEvent.info( - "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP: " + httpUrl)); - return call(httpUrl.toURL(), connectionFactory); - } - - private Function getInsecureConnectionFactory() throws RegistryException { - try { - if (insecureConnectionFactory == null) { - insecureConnectionFactory = Connection.getInsecureConnectionFactory(); - } - return insecureConnectionFactory; - - } catch (GeneralSecurityException ex) { - throw new RegistryException("cannot turn off TLS peer verification", ex); - } + return call(initialRequestUrl); } /** @@ -225,34 +123,29 @@ private Function getInsecureConnectionFactory() throws Registry * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ - private T call(URL url, Function connectionFactory) - throws IOException, RegistryException { - // Only sends authorization if using HTTPS or explicitly forcing over HTTP. - boolean sendCredentials = isHttpsProtocol(url) || JibSystemProperties.sendCredentialsOverHttp(); + private T call(URL url) throws IOException, RegistryException { String serverUrl = registryEndpointRequestProperties.getServerUrl(); String imageName = registryEndpointRequestProperties.getImageName(); - try (Connection connection = connectionFactory.apply(url)) { - Request.Builder requestBuilder = - Request.builder() - .setUserAgent(userAgent) - .setHttpTimeout(JibSystemProperties.getHttpTimeout()) - .setAccept(registryEndpointProvider.getAccept()) - .setBody(registryEndpointProvider.getContent()); - if (sendCredentials) { - requestBuilder.setAuthorization(authorization); - } - Response response = - connection.send(registryEndpointProvider.getHttpMethod(), requestBuilder.build()); + Request.Builder requestBuilder = + Request.builder() + .setUserAgent(userAgent) + .setHttpTimeout(JibSystemProperties.getHttpTimeout()) + .setAccept(registryEndpointProvider.getAccept()) + .setBody(registryEndpointProvider.getContent()) + .setAuthorization(authorization); + + try (Response response = + httpClient.call(registryEndpointProvider.getHttpMethod(), url, requestBuilder.build())) { return registryEndpointProvider.handleResponse(response); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { // First, see if the endpoint provider handles an exception as an expected response. try { return registryEndpointProvider.handleHttpResponseException(ex); - } catch (HttpResponseException responseException) { + } catch (ResponseException responseException) { if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND || responseException.getStatusCode() @@ -264,11 +157,11 @@ private T call(URL url, Function connectionFactory) throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { - if (sendCredentials) { + if (responseException.requestAuthorizationCleared()) { + throw new RegistryCredentialsNotSentException(serverUrl, imageName); + } else { // Credentials are either missing or wrong. throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); - } else { - throw new RegistryCredentialsNotSentException(serverUrl, imageName); } } else if (responseException.getStatusCode() @@ -277,7 +170,7 @@ private T call(URL url, Function connectionFactory) || responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { // 'Location' header can be relative or absolute. URL redirectLocation = new URL(url, responseException.getHeaders().getLocation()); - return callWithAllowInsecureRegistryHandling(redirectLocation); + return call(redirectLocation); } else { // Unknown @@ -285,20 +178,20 @@ private T call(URL url, Function connectionFactory) } } - } catch (SSLException ex) { - logErrorIfBrokenPipe(ex); - throw ex; - } catch (IOException ex) { logError("I/O error for image [" + serverUrl + "/" + imageName + "]:"); logError(" " + ex.getMessage()); logErrorIfBrokenPipe(ex); + + if (ex instanceof SSLException) { + throw new InsecureRegistryException(url, ex); + } throw ex; } } @VisibleForTesting - RegistryErrorException newRegistryErrorException(HttpResponseException responseException) { + RegistryErrorException newRegistryErrorException(ResponseException responseException) { RegistryErrorExceptionBuilder registryErrorExceptionBuilder = new RegistryErrorExceptionBuilder( registryEndpointProvider.getActionDescription(), responseException); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index 0381352dc3..d6a2e7c502 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -53,15 +54,15 @@ interface RegistryEndpointProvider { T handleResponse(Response response) throws IOException, RegistryException; /** - * Handles an {@link HttpResponseException} that occurs. + * Handles an {@link ResponseException} that occurs. * - * @param responseException the {@link HttpResponseException} to handle + * @param responseException the {@link ResponseException} to handle * @throws HttpResponseException {@code responseException} if {@code responseException} could not * be handled * @throws RegistryErrorException if there is an error with a remote registry */ - default T handleHttpResponseException(HttpResponseException responseException) - throws HttpResponseException, RegistryErrorException { + default T handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { throw responseException; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java index e68be69f92..6049f28bf8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java @@ -22,73 +22,91 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.blob.Blobs; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link Connection}. */ @RunWith(MockitoJUnitRunner.class) -public class ConnectionTest { +public class ConnectionTest { // TODO: rename to TlsFailoverHttpClient @FunctionalInterface - private interface SendFunction { + private interface CallFunction { - Response send(Connection connection, Request request) throws IOException; + Response call(Connection httpClient, URL url, Request request) throws IOException; } + @Mock private HttpTransport mockHttpTransport; + @Mock private HttpTransport mockInsecureHttpTransport; @Mock private HttpRequestFactory mockHttpRequestFactory; + @Mock private HttpRequestFactory mockInsecureHttpRequestFactory; @Mock private HttpRequest mockHttpRequest; + @Mock private HttpRequest mockInsecureHttpRequest; + @Mock private HttpResponse mockHttpResponse; + @Mock private Consumer logger; - private final ArgumentCaptor httpHeadersArgumentCaptor = - ArgumentCaptor.forClass(HttpHeaders.class); - private final ArgumentCaptor blobHttpContentArgumentCaptor = - ArgumentCaptor.forClass(BlobHttpContent.class); + @Captor private ArgumentCaptor httpHeadersCaptor; + @Captor private ArgumentCaptor blobHttpContentCaptor; + @Captor private ArgumentCaptor urlCaptor; - private final GenericUrl fakeUrl = new GenericUrl("http://crepecake/fake/url"); + private final GenericUrl fakeUrl = new GenericUrl("https://crepecake/fake/url"); private final LongAdder totalByteCount = new LongAdder(); - private Request fakeRequest; - private HttpResponse mockHttpResponse; + @Before + public void setUp() throws IOException { + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[] {'b', 'o', 'd', 'y'}); + Mockito.when(mockHttpResponse.getContent()).thenReturn(inStream); + } - @InjectMocks - private final Connection testConnection = - Connection.getConnectionFactory().apply(fakeUrl.toURL()); + private Connection newHttpClient(boolean insecure, boolean authOverHttp) throws IOException { + setUpMocks(mockHttpTransport, mockHttpRequestFactory, mockHttpRequest); + if (insecure) { + setUpMocks( + mockInsecureHttpTransport, mockInsecureHttpRequestFactory, mockInsecureHttpRequest); + } + return new Connection( + insecure, authOverHttp, logger, () -> mockHttpTransport, () -> mockInsecureHttpTransport); + } @Test public void testGet() throws IOException { - setUpMocksAndFakes(null); - testSend(HttpMethods.GET, Connection::get); + verifyCall(HttpMethods.GET, Connection::get); } @Test public void testPost() throws IOException { - setUpMocksAndFakes(null); - testSend(HttpMethods.POST, Connection::post); + verifyCall(HttpMethods.POST, Connection::post); } @Test public void testPut() throws IOException { - setUpMocksAndFakes(null); - testSend(HttpMethods.PUT, Connection::put); + verifyCall(HttpMethods.PUT, Connection::put); } @Test public void testHttpTimeout_doNotSetByDefault() throws IOException { - setUpMocksAndFakes(null); - try (Connection connection = testConnection) { - connection.send(HttpMethods.GET, fakeRequest); - } + try (Response ignored = newHttpClient(false, false).get(fakeUrl.toURL(), fakeRequest(null))) {} Mockito.verify(mockHttpRequest, Mockito.never()).setConnectTimeout(Mockito.anyInt()); Mockito.verify(mockHttpRequest, Mockito.never()).setReadTimeout(Mockito.anyInt()); @@ -96,66 +114,283 @@ public void testHttpTimeout_doNotSetByDefault() throws IOException { @Test public void testHttpTimeout() throws IOException { - setUpMocksAndFakes(5982); - try (Connection connection = testConnection) { - connection.send(HttpMethods.GET, fakeRequest); - } + Connection httpClient = newHttpClient(false, false); + try (Response ignored = httpClient.get(fakeUrl.toURL(), fakeRequest(5982))) {} Mockito.verify(mockHttpRequest).setConnectTimeout(5982); Mockito.verify(mockHttpRequest).setReadTimeout(5982); } - private void setUpMocksAndFakes(Integer httpTimeout) throws IOException { - fakeRequest = - Request.builder() - .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) - .setUserAgent("fake user agent") - .setBody( - new BlobHttpContent( - Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) - .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) - .setHttpTimeout(httpTimeout) - .build(); + private Request fakeRequest(Integer httpTimeout) { + return Request.builder() + .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) + .setUserAgent("fake user agent") + .setBody( + new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) + .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) + .setHttpTimeout(httpTimeout) + .build(); + } + private void setUpMocks( + HttpTransport mockHttpTransport, + HttpRequestFactory mockHttpRequestFactory, + HttpRequest mockHttpRequest) + throws IOException { + Mockito.when(mockHttpTransport.createRequestFactory()).thenReturn(mockHttpRequestFactory); Mockito.when( - mockHttpRequestFactory.buildRequest( - Mockito.anyString(), Mockito.eq(fakeUrl), Mockito.any(BlobHttpContent.class))) + mockHttpRequestFactory.buildRequest(Mockito.any(), urlCaptor.capture(), Mockito.any())) .thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.setHeaders(Mockito.any(HttpHeaders.class))) + Mockito.when(mockHttpRequest.setHeaders(httpHeadersCaptor.capture())) .thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); - mockHttpResponse = Mockito.mock(HttpResponse.class); Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); } - private void testSend(String httpMethod, SendFunction sendFunction) throws IOException { - try (Connection connection = testConnection) { - sendFunction.send(connection, fakeRequest); - } - - Mockito.verify(mockHttpRequest).setHeaders(httpHeadersArgumentCaptor.capture()); - Mockito.verify(mockHttpResponse).disconnect(); + private void verifyCall(String httpMethod, CallFunction callFunction) throws IOException { + Connection httpClient = newHttpClient(false, false); + try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) {} Assert.assertEquals( - "fake.accept,another.fake.accept", httpHeadersArgumentCaptor.getValue().getAccept()); - Assert.assertEquals("fake user agent", httpHeadersArgumentCaptor.getValue().getUserAgent()); + "fake.accept,another.fake.accept", httpHeadersCaptor.getValue().getAccept()); + Assert.assertEquals("fake user agent", httpHeadersCaptor.getValue().getUserAgent()); // Base64 representation of "fake-username:fake-secret" Assert.assertEquals( "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", - httpHeadersArgumentCaptor.getValue().getAuthorization()); + httpHeadersCaptor.getValue().getAuthorization()); Mockito.verify(mockHttpRequestFactory) - .buildRequest( - Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentArgumentCaptor.capture()); - Assert.assertEquals("fake.content.type", blobHttpContentArgumentCaptor.getValue().getType()); + .buildRequest(Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentCaptor.capture()); + Assert.assertEquals("fake.content.type", blobHttpContentCaptor.getValue().getType()); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - blobHttpContentArgumentCaptor.getValue().writeTo(byteArrayOutputStream); + blobHttpContentCaptor.getValue().writeTo(byteArrayOutputStream); - Assert.assertEquals( - "crepecake", new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8)); + Assert.assertEquals("crepecake", byteArrayOutputStream.toString(StandardCharsets.UTF_8.name())); Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); } + + @Test + public void testGet_nonHttpsServer_insecureConnectionAndFailoverDisabled() + throws MalformedURLException, IOException { + Connection httpClient = newHttpClient(false, false); + try (Response response = httpClient.get(new URL("http://plain.http"), fakeRequest(null))) { + Assert.fail("Should disallow non-HTTP attempt"); + } catch (SSLException ex) { + Assert.assertEquals( + "insecure HTTP connection not allowed: http://plain.http", ex.getMessage()); + } + } + + @Test + public void testCall_secureClientOnUnverifiableServer() throws IOException { + Connection httpClient = newHttpClient(false, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("unverified")); + + try (Response response = httpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Secure caller should fail if cannot verify server"); + } catch (SSLException ex) { + Assert.assertEquals("unverified", ex.getMessage()); + Mockito.verifyNoInteractions(logger); + } + } + + @Test + public void testGet_insecureClientOnUnverifiableServer() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("")); + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + Assert.assertEquals(2, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(1)); + + String log = + "Cannot verify server at https://insecure. Attempting again with no TLS verification."; + Mockito.verify(logger).accept(LogEvent.info(log)); + Mockito.verifyNoMoreInteractions(logger); + } + + @Test + public void testGet_insecureClientOnHttpServer() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) // server is not HTTPS + .thenReturn(mockHttpResponse); + Mockito.when(mockInsecureHttpRequest.execute()) + .thenThrow(new SSLException("")); // server is not HTTPS + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); + + Assert.assertEquals(3, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(1)); + Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(2)); + + String log1 = + "Cannot verify server at https://insecure. Attempting again with no TLS verification."; + String log2 = "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."; + Mockito.verify(logger).accept(LogEvent.info(log1)); + Mockito.verify(logger).accept(LogEvent.info(log2)); + Mockito.verifyNoMoreInteractions(logger); + } + + @Test + public void testGet_insecureClientOnHttpServerAndNoPortSpecified() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) // server is not listening on 443 + .thenReturn(mockHttpResponse); // respond when connected through 80 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + byte[] bytes = new byte[4]; + Assert.assertEquals(4, response.getBody().read(bytes)); + Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); + } + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest); + + Assert.assertEquals(2, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(1)); + + String log = "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."; + Mockito.verify(logger).accept(LogEvent.info(log)); + Mockito.verifyNoMoreInteractions(logger); + } + + @Test + public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws IOException { + Connection httpClient = newHttpClient(false, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException("my exception")); // server not listening on 443 + + try (Response response = httpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); + } catch (ConnectException ex) { + Assert.assertEquals("my exception", ex.getMessage()); + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getValue()); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_insecureClientOnNonListeningServerAndPortSpecified() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException("my exception")); // server is not listening on 5000 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure:5000"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); + } catch (ConnectException ex) { + Assert.assertEquals("my exception", ex.getMessage()); + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure:5000"), urlCaptor.getValue()); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_timeoutFromConnectException() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()).thenThrow(new ConnectException("Connection timed out")); + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + Assert.fail("Should not fall back to HTTP if timed out even for ConnectionException"); + } catch (ConnectException ex) { + Assert.assertEquals("Connection timed out", ex.getMessage()); + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getValue()); + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); + } + } + + @Test + public void testGet_doNotSendCredentialsOverHttp() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + // make it fall back to HTTP + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) // server is not listening on 443 + .thenReturn(mockHttpResponse); // respond when connected through 80 + + try (Response response = + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) {} + + Assert.assertEquals(2, urlCaptor.getAllValues().size()); + Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); + Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(1)); + + Assert.assertEquals(2, httpHeadersCaptor.getAllValues().size()); + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getAllValues().get(0).getAuthorization()); + Assert.assertNull(httpHeadersCaptor.getAllValues().get(1).getAuthorization()); + } + + @Test + public void testGet_sendCredentialsOverHttp() throws IOException { + Connection insecureHttpClient = newHttpClient(true, true); // sendCredentialsOverHttp + + try (Response response = + insecureHttpClient.get(new URL("http://plain.http"), fakeRequest(null))) {} + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getValue().getAuthorization()); + } + + @Test + public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IOException { + Connection insecureHttpClient = newHttpClient(true, false); + + Request request = fakeRequest(null); + try (Response response = insecureHttpClient.get(new URL("http://plain.http"), request)) {} + + Assert.assertEquals(1, urlCaptor.getAllValues().size()); + Assert.assertEquals(1, httpHeadersCaptor.getAllValues().size()); + + Assert.assertNull(httpHeadersCaptor.getValue().getAuthorization()); + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", request.getHeaders().getAuthorization()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java index 5821f97911..385a05b21b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java @@ -28,6 +28,7 @@ import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link Connection} with setting proxy credentials. */ +// TODO: rename to TlsFailoverHttpClientProxyCredentialsTest public class ConnectionWithProxyCredentialsTest { private static final ImmutableList proxyProperties = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java index 0cba195cd0..dd27e0bd7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java @@ -41,8 +41,8 @@ public void testGetContent() throws IOException { Mockito.when(httpResponseMock.getContent()).thenReturn(responseInputStream); - Response response = new Response(httpResponseMock); - - Assert.assertArrayEquals(expectedResponse, ByteStreams.toByteArray(response.getBody())); + try (Response response = new Response(httpResponseMock)) { + Assert.assertArrayEquals(expectedResponse, ByteStreams.toByteArray(response.getBody())); + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java index 6d843b2c7c..ed2cd1063d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.http; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.io.ByteStreams; import java.io.IOException; import java.net.URISyntaxException; @@ -23,76 +24,95 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Arrays; +import java.util.function.Consumer; import javax.net.ssl.SSLException; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link Connection} using an actual local server. */ -public class WithServerConnectionTest { +@RunWith(MockitoJUnitRunner.class) +public class WithServerConnectionTest { // TODO: rename to WithServerTlsFailoverHttpClientTest @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private Consumer logger; + + private final Request request = new Request.Builder().build(); + @Test public void testGet() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false); - Connection connection = - Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { - Response response = connection.send("GET", new Request.Builder().build()); + Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { Assert.assertEquals(200, response.getStatusCode()); Assert.assertArrayEquals( "Hello World!".getBytes(StandardCharsets.UTF_8), ByteStreams.toByteArray(response.getBody())); - } - } - - @Test - public void testErrorOnSecondSend() - throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - try (TestWebServer server = new TestWebServer(false); - Connection connection = - Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { - connection.send("GET", new Request.Builder().build()); - try { - connection.send("GET", new Request.Builder().build()); - Assert.fail("Should fail on the second send"); - } catch (IllegalStateException ex) { - Assert.assertEquals("Connection can send only one request", ex.getMessage()); - } + Mockito.verifyNoInteractions(logger); } } @Test public void testSecureConnectionOnInsecureHttpsServer() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + Connection secureHttpClient = new Connection(false /*secure*/, false, logger); try (TestWebServer server = new TestWebServer(true); - Connection connection = - Connection.getConnectionFactory().apply(new URL(server.getEndpoint()))) { - try { - connection.send("GET", new Request.Builder().build()); - Assert.fail("Should fail if cannot verify peer"); - } catch (SSLException ex) { - Assert.assertNotNull(ex.getMessage()); - } + Response ignored = secureHttpClient.get(new URL(server.getEndpoint()), request)) { + Assert.fail("Should fail if cannot verify peer"); + + } catch (SSLException ex) { + Assert.assertNotNull(ex.getMessage()); } } @Test - public void testInsecureConnection() + public void testInsecureConnection_insecureHttpsFailover() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - try (TestWebServer server = new TestWebServer(true); - Connection connection = - Connection.getInsecureConnectionFactory().apply(new URL(server.getEndpoint()))) { - Response response = connection.send("GET", new Request.Builder().build()); + Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(true, 2); + Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { Assert.assertEquals(200, response.getStatusCode()); Assert.assertArrayEquals( "Hello World!".getBytes(StandardCharsets.UTF_8), ByteStreams.toByteArray(response.getBody())); + + String endpoint = server.getEndpoint(); + String expectedLog = + "Cannot verify server at " + endpoint + ". Attempting again with no TLS verification."; + Mockito.verify(logger).accept(LogEvent.info(expectedLog)); + } + } + + @Test + public void testInsecureConnection_plainHttpFailover() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, 3)) { + String httpsUrl = server.getEndpoint().replace("http://", "https://"); + try (Response response = insecureHttpClient.get(new URL(httpsUrl), request)) { + + Assert.assertEquals(200, response.getStatusCode()); + Assert.assertArrayEquals( + "Hello World!".getBytes(StandardCharsets.UTF_8), + ByteStreams.toByteArray(response.getBody())); + + String expectedLog1 = + "Cannot verify server at " + httpsUrl + ". Attempting again with no TLS verification."; + String expectedLog2 = + "Failed to connect to " + httpsUrl + " over HTTPS. Attempting again with HTTP."; + Mockito.verify(logger).accept(LogEvent.info(expectedLog1)); + Mockito.verify(logger).accept(LogEvent.info(expectedLog2)); + } } } @@ -107,6 +127,7 @@ public void testProxyCredentialProperties() + "Content-Length: 0\n\n"; String targetServerResponse = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + Connection httpClient = new Connection(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse), 1)) { System.setProperty("http.proxyHost", "localhost"); @@ -114,9 +135,7 @@ public void testProxyCredentialProperties() System.setProperty("http.proxyUser", "user_sys_prop"); System.setProperty("http.proxyPassword", "pass_sys_prop"); - try (Connection connection = - Connection.getConnectionFactory().apply(new URL("http://does.not.matter"))) { - Response response = connection.send("GET", new Request.Builder().build()); + try (Response response = httpClient.get(new URL("http://does.not.matter"), request)) { Assert.assertThat( server.getInputRead(), CoreMatchers.containsString( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 0a5bde73fb..9e0ccfc0d6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -18,9 +18,10 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; +import com.google.cloud.tools.jib.http.Connection; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; @@ -36,13 +37,15 @@ @RunWith(MockitoJUnitRunner.class) public class AuthenticationMethodRetrieverTest { - @Mock private HttpResponseException mockResponseException; + @Mock private ResponseException mockResponseException; @Mock private HttpHeaders mockHeaders; + @Mock private Connection httpClient; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private final AuthenticationMethodRetriever testAuthenticationMethodRetriever = - new AuthenticationMethodRetriever(fakeRegistryEndpointRequestProperties, "user-agent"); + new AuthenticationMethodRetriever( + fakeRegistryEndpointRequestProperties, "user-agent", httpClient); @Test public void testGetContent() { @@ -88,13 +91,13 @@ public void testHandleHttpResponseException_invalidStatusCode() throws RegistryE Assert.fail( "Authentication method retriever should only handle HTTP 401 Unauthorized errors"); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test - public void tsetHandleHttpResponseException_noHeader() throws HttpResponseException { + public void tsetHandleHttpResponseException_noHeader() throws ResponseException { Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); @@ -112,8 +115,7 @@ public void tsetHandleHttpResponseException_noHeader() throws HttpResponseExcept } @Test - public void testHandleHttpResponseException_badAuthenticationMethod() - throws HttpResponseException { + public void testHandleHttpResponseException_badAuthenticationMethod() throws ResponseException { String authenticationMethod = "bad authentication method"; Mockito.when(mockResponseException.getStatusCode()) @@ -136,7 +138,7 @@ public void testHandleHttpResponseException_badAuthenticationMethod() @Test public void testHandleHttpResponseException_pass() - throws RegistryErrorException, HttpResponseException, MalformedURLException { + throws RegistryErrorException, ResponseException, MalformedURLException { String authenticationMethod = "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\""; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java index 0e364e79e7..b25f8a5078 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java @@ -16,11 +16,11 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import com.google.cloud.tools.jib.registry.json.ErrorResponseTemplate; @@ -83,7 +83,7 @@ public void testHandleResponse_noContentLength() { @Test public void testHandleHttpResponseException() throws IOException { - HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + ResponseException mockResponseException = Mockito.mock(ResponseException.class); Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); @@ -99,7 +99,7 @@ public void testHandleHttpResponseException() throws IOException { @Test public void testHandleHttpResponseException_hasOtherErrors() throws IOException { - HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + ResponseException mockResponseException = Mockito.mock(ResponseException.class); Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); @@ -114,14 +114,14 @@ public void testHandleHttpResponseException_hasOtherErrors() throws IOException testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test public void testHandleHttpResponseException_notBlobUnknown() throws IOException { - HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + ResponseException mockResponseException = Mockito.mock(ResponseException.class); Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_NOT_FOUND); @@ -133,21 +133,21 @@ public void testHandleHttpResponseException_notBlobUnknown() throws IOException testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test public void testHandleHttpResponseException_invalidStatusCode() { - HttpResponseException mockResponseException = Mockito.mock(HttpResponseException.class); + ResponseException mockResponseException = Mockito.mock(ResponseException.class); Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-404 status codes should not be handled"); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java index 5c8981338a..f440e61a83 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; +import com.google.cloud.tools.jib.http.ResponseException; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,10 +28,10 @@ @RunWith(MockitoJUnitRunner.class) public class ErrorResponseUtilTest { - @Mock HttpResponseException responseException; + @Mock private ResponseException responseException; @Test - public void testGetErrorCode_knownErrorCode() throws HttpResponseException { + public void testGetErrorCode_knownErrorCode() throws ResponseException { Mockito.when(responseException.getContent()) .thenReturn( "{\"errors\":[{\"code\":\"MANIFEST_INVALID\",\"message\":\"manifest invalid\",\"detail\":{}}]}"); @@ -49,7 +49,7 @@ public void testGetErrorCode_unknownErrorCode() { try { ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertSame(responseException, ex); } } @@ -66,7 +66,7 @@ public void testGetErrorCode_multipleErrors() { try { ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertSame(responseException, ex); } } @@ -79,7 +79,7 @@ public void testGetErrorCode_invalidErrorObject() { try { ErrorResponseUtil.getErrorCode(responseException); Assert.fail(); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertSame(responseException, ex); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java index 87ca1cdb2c..e9ff217e7c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java @@ -16,13 +16,13 @@ package com.google.cloud.tools.jib.registry; -import com.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; @@ -156,9 +156,8 @@ public void testGetAccept() { /** Docker Registry 2.0 and 2.1 return 400 / TAG_INVALID. */ @Test - public void testHandleHttpResponseException_dockerRegistry_tagInvalid() - throws HttpResponseException { - HttpResponseException exception = Mockito.mock(HttpResponseException.class); + public void testHandleHttpResponseException_dockerRegistry_tagInvalid() throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); Mockito.when(exception.getContent()) .thenReturn( @@ -180,8 +179,8 @@ public void testHandleHttpResponseException_dockerRegistry_tagInvalid() /** Docker Registry 2.2 returns a 400 / MANIFEST_INVALID. */ @Test public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() - throws HttpResponseException { - HttpResponseException exception = Mockito.mock(HttpResponseException.class); + throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); Mockito.when(exception.getContent()) .thenReturn( @@ -202,8 +201,8 @@ public void testHandleHttpResponseException_dockerRegistry_manifestInvalid() /** Quay.io returns an undocumented 415 / MANIFEST_INVALID. */ @Test - public void testHandleHttpResponseException_quayIo() throws HttpResponseException { - HttpResponseException exception = Mockito.mock(HttpResponseException.class); + public void testHandleHttpResponseException_quayIo() throws ResponseException { + ResponseException exception = Mockito.mock(ResponseException.class); Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE); Mockito.when(exception.getContent()) .thenReturn( @@ -225,13 +224,13 @@ public void testHandleHttpResponseException_quayIo() throws HttpResponseExceptio @Test public void testHandleHttpResponseException_otherError() throws RegistryErrorException { - HttpResponseException exception = Mockito.mock(HttpResponseException.class); + ResponseException exception = Mockito.mock(ResponseException.class); Mockito.when(exception.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED); try { testManifestPusher.handleHttpResponseException(exception); Assert.fail(); - } catch (HttpResponseException ex) { + } catch (ResponseException ex) { Assert.assertSame(exception, ex); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 74fab1eb8c..268dda3475 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -18,33 +18,57 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.http.TestWebServer; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Collections; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link RegistryAuthenticator}. */ +@RunWith(MockitoJUnitRunner.class) public class RegistryAuthenticatorTest { private final RegistryEndpointRequestProperties registryEndpointRequestProperties = new RegistryEndpointRequestProperties("someserver", "someimage"); + @Mock private Connection httpClient; + @Mock private Response response; + + @Captor private ArgumentCaptor urlCaptor; + private RegistryAuthenticator registryAuthenticator; @Before - public void setUp() throws RegistryAuthenticationFailedException { + public void setUp() throws RegistryAuthenticationFailedException, IOException { registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .get(); + + ByteArrayInputStream tokenJson = + new ByteArrayInputStream("{\"token\":\"my_token\"}".getBytes(StandardCharsets.UTF_8)); + Mockito.when(response.getBody()).thenReturn(tokenJson); + Mockito.when(httpClient.call(Mockito.any(), urlCaptor.capture(), Mockito.any())) + .thenReturn(response); } @Test @@ -54,7 +78,8 @@ public void testFromAuthenticationMethod_bearer() RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), @@ -65,7 +90,8 @@ public void testFromAuthenticationMethod_bearer() RegistryAuthenticator.fromAuthenticationMethod( "bEaReR realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), @@ -131,21 +157,24 @@ public void testFromAuthenticationMethod_basic() throws RegistryAuthenticationFa RegistryAuthenticator.fromAuthenticationMethod( "Basic realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .isPresent()); Assert.assertFalse( RegistryAuthenticator.fromAuthenticationMethod( "BASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .isPresent()); Assert.assertFalse( RegistryAuthenticator.fromAuthenticationMethod( "bASIC realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .isPresent()); } @@ -155,7 +184,8 @@ public void testFromAuthenticationMethod_noBearer() { RegistryAuthenticator.fromAuthenticationMethod( "realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"", registryEndpointRequestProperties, - "user-agent"); + "user-agent", + httpClient); Assert.fail("Authentication method without 'Bearer ' or 'Basic ' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -169,7 +199,10 @@ public void testFromAuthenticationMethod_noBearer() { public void testFromAuthenticationMethod_noRealm() { try { RegistryAuthenticator.fromAuthenticationMethod( - "Bearer scope=\"somescope\"", registryEndpointRequestProperties, "user-agent"); + "Bearer scope=\"somescope\"", + registryEndpointRequestProperties, + "user-agent", + httpClient); Assert.fail("Authentication method without 'realm' should fail"); } catch (RegistryAuthenticationFailedException ex) { @@ -186,7 +219,8 @@ public void testFromAuthenticationMethod_noService() RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties, - "user-agent") + "user-agent", + httpClient) .get(); Assert.assertEquals( @@ -197,14 +231,16 @@ public void testFromAuthenticationMethod_noService() @Test public void testUserAgent() - throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryCredentialsNotSentException { try (TestWebServer server = new TestWebServer(false)) { try { RegistryAuthenticator authenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"" + server.getEndpoint() + "\"", registryEndpointRequestProperties, - "Competent-Agent") + "Competent-Agent", + new Connection(true, false, ignored -> {})) .get(); authenticator.authenticatePush(null); } catch (RegistryAuthenticationFailedException ex) { @@ -217,48 +253,53 @@ public void testUserAgent() @Test public void testSourceImage_differentSourceRepository() - throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - try (TestWebServer server = new TestWebServer(false, 2)) { - try { - RegistryEndpointRequestProperties registryEndpointRequestProperties = - new RegistryEndpointRequestProperties("someserver", "someimage", "anotherimage"); - RegistryAuthenticator authenticator = - RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"" + server.getEndpoint() + "\"", - registryEndpointRequestProperties, - "Competent-Agent") - .get(); - authenticator.authenticatePush(null); - } catch (RegistryAuthenticationFailedException ex) { - // Doesn't matter if auth fails. We only examine what we sent. - } - Assert.assertThat( - server.getInputRead(), - CoreMatchers.containsString( - "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull ")); - } + throws RegistryCredentialsNotSentException, RegistryAuthenticationFailedException { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://1.2.3.4:5\"", + new RegistryEndpointRequestProperties("someserver", "someimage", "anotherimage"), + "Competent-Agent", + httpClient) + .get(); + authenticator.authenticatePush(null); + Assert.assertThat( + urlCaptor.getValue().toString(), + CoreMatchers.endsWith( + "scope=repository:someimage:pull,push&scope=repository:anotherimage:pull")); } @Test public void testSourceImage_sameSourceRepository() - throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - try (TestWebServer server = new TestWebServer(false)) { - try { - RegistryEndpointRequestProperties registryEndpointRequestProperties = - new RegistryEndpointRequestProperties("someserver", "someimage", "someimage"); - RegistryAuthenticator authenticator = - RegistryAuthenticator.fromAuthenticationMethod( - "Bearer realm=\"" + server.getEndpoint() + "\"", - registryEndpointRequestProperties, - "Competent-Agent") - .get(); - authenticator.authenticatePush(null); - } catch (RegistryAuthenticationFailedException ex) { - // Doesn't matter if auth fails. We only examine what we sent. - } - Assert.assertThat( - server.getInputRead(), - CoreMatchers.containsString("service=someserver&scope=repository:someimage:pull,push ")); + throws RegistryCredentialsNotSentException, RegistryAuthenticationFailedException { + RegistryAuthenticator authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + "Bearer realm=\"https://1.2.3.4:5\"", + new RegistryEndpointRequestProperties("someserver", "someimage", "someimage"), + "Competent-Agent", + httpClient) + .get(); + authenticator.authenticatePush(null); + Assert.assertThat( + urlCaptor.getValue().toString(), + CoreMatchers.endsWith("service=someserver&scope=repository:someimage:pull,push")); + } + + @Test + public void testAuthorizationCleared() throws RegistryAuthenticationFailedException, IOException { + ResponseException responseException = Mockito.mock(ResponseException.class); + Mockito.when(responseException.getStatusCode()).thenReturn(401); + Mockito.when(responseException.requestAuthorizationCleared()).thenReturn(true); + Mockito.when(httpClient.call(Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(responseException); + + try { + registryAuthenticator.authenticatePush(null); + Assert.fail(); + } catch (RegistryCredentialsNotSentException ex) { + Assert.assertEquals( + "Required credentials for someserver/someimage were not sent because the connection was " + + "over HTTP", + ex.getMessage()); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 20e5f542e9..bfebf7455b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -17,8 +17,6 @@ package com.google.cloud.tools.jib.registry; import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.LogEvent; @@ -32,18 +30,17 @@ import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.RequestWrapper; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.common.io.CharStreams; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.net.ConnectException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; -import java.util.function.Function; import javax.annotation.Nullable; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; @@ -101,216 +98,64 @@ public String getActionDescription() { } } - private static HttpResponse mockHttpResponse(int statusCode, @Nullable HttpHeaders headers) - throws IOException { - HttpResponse mock = Mockito.mock(HttpResponse.class); + private static ResponseException mockResponseException( + int statusCode, @Nullable HttpHeaders headers) { + ResponseException mock = Mockito.mock(ResponseException.class); Mockito.when(mock.getStatusCode()).thenReturn(statusCode); - Mockito.when(mock.parseAsString()).thenReturn(""); Mockito.when(mock.getHeaders()).thenReturn(headers != null ? headers : new HttpHeaders()); return mock; } - private static HttpResponse mockRedirectHttpResponse(String redirectLocation) throws IOException { - int code307 = HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT; - return mockHttpResponse(code307, new HttpHeaders().setLocation(redirectLocation)); - } - @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @Mock private EventHandlers mockEventHandlers; - @Mock private Connection mockConnection; - @Mock private Connection mockInsecureConnection; + @Mock private Connection mockHttpClient; @Mock private Response mockResponse; - @Mock private Function mockConnectionFactory; - @Mock private Function mockInsecureConnectionFactory; - private RegistryEndpointCaller secureEndpointCaller; + private RegistryEndpointCaller endpointCaller; @Before public void setUp() throws IOException { - secureEndpointCaller = createRegistryEndpointCaller(false, -1); + endpointCaller = + new RegistryEndpointCaller<>( + mockEventHandlers, + "userAgent", + new TestRegistryEndpointProvider(), + Authorization.fromBasicToken("token"), + new RegistryEndpointRequestProperties("serverUrl", "imageName"), + mockHttpClient); - Mockito.when(mockConnectionFactory.apply(Mockito.any())).thenReturn(mockConnection); - Mockito.when(mockInsecureConnectionFactory.apply(Mockito.any())) - .thenReturn(mockInsecureConnection); Mockito.when(mockResponse.getBody()) .thenReturn(new ByteArrayInputStream("body".getBytes(StandardCharsets.UTF_8))); } @Test public void testCall_secureCallerOnUnverifiableServer() throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // unverifiable HTTPS server + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); try { - secureEndpointCaller.call(); - Assert.fail("Secure caller should fail if cannot verify server"); + endpointCaller.call(); + Assert.fail("Should throw InsecureRegistryException when getting SSLException"); } catch (InsecureRegistryException ex) { Assert.assertEquals( - "Failed to verify the server at https://serverUrl/v2/api because only secure connections are allowed.", + "Failed to verify the server at https://serverUrl/v2/api because only secure connections " + + "are allowed.", ex.getMessage()); } } - @Test - public void testCall_insecureCallerOnUnverifiableServer() throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // unverifiable HTTPS server - Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenReturn(mockResponse); // OK with non-verifying connection - - RegistryEndpointCaller insecureCaller = createRegistryEndpointCaller(true, -1); - Assert.assertEquals("body", insecureCaller.call()); - - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); - - Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); - } - - @Test - public void testCall_insecureCallerOnHttpServer() throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)) // server is not HTTPS - .thenReturn(mockResponse); - Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); - Assert.assertEquals("body", insecureEndpointCaller.call()); - - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); - - Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(2)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); - } - - @Test - public void testCall_insecureCallerOnHttpServerAndNoPortSpecified() - throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(ConnectException.class)) // server is not listening on 443 - .thenReturn(mockResponse); // respond when connected through 80 - - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); - Assert.assertEquals("body", insecureEndpointCaller.call()); - - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); - } - - @Test - public void testCall_secureCallerOnNonListeningServerAndNoPortSpecified() - throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(ConnectException.class)); // server is not listening on 443 - - try { - secureEndpointCaller.call(); - Assert.fail("Should not fall back to HTTP if not allowInsecureRegistries"); - } catch (ConnectException ex) { - Assert.assertNull(ex.getMessage()); - } - - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - } - - @Test - public void testCall_insecureCallerOnNonListeningServerAndPortSpecified() - throws IOException, RegistryException { - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(ConnectException.class)); // server is not listening on 5000 - - RegistryEndpointCaller insecureEndpointCaller = - createRegistryEndpointCaller(true, 5000); - try { - insecureEndpointCaller.call(); - Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); - } catch (ConnectException ex) { - Assert.assertNull(ex.getMessage()); - } - - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl:5000/v2/api"), urlCaptor.getAllValues().get(0)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - } - - @Test - public void testCall_timeoutFromConnectException() throws IOException, RegistryException { - ConnectException mockConnectException = Mockito.mock(ConnectException.class); - Mockito.when(mockConnectException.getMessage()).thenReturn("Connection timed out"); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(mockConnectException) // server times out on 443 - .thenReturn(mockResponse); // respond when connected through 80 - - try { - RegistryEndpointCaller insecureEndpointCaller = - createRegistryEndpointCaller(true, -1); - insecureEndpointCaller.call(); - Assert.fail("Should not fall back to HTTP if timed out even for ConnectionException"); - - } catch (ConnectException ex) { - Assert.assertSame(mockConnectException, ex); - Mockito.verify(mockConnection).send(Mockito.anyString(), Mockito.any()); - } - } - @Test public void testCall_noHttpResponse() throws IOException, RegistryException { - NoHttpResponseException mockNoHttpResponseException = - Mockito.mock(NoHttpResponseException.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(mockNoHttpResponseException); + NoHttpResponseException mockNoResponseException = Mockito.mock(NoHttpResponseException.class); + setUpRegistryResponse(mockNoResponseException); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (NoHttpResponseException ex) { - Assert.assertSame(mockNoHttpResponseException, ex); + Assert.assertSame(mockNoResponseException, ex); } } @@ -321,20 +166,13 @@ public void testCall_unauthorized() throws IOException, RegistryException { @Test public void testCall_credentialsNotSentOverHttp() throws IOException, RegistryException { - HttpResponse redirectResponse = mockRedirectHttpResponse("http://newlocation"); - HttpResponse unauthroizedResponse = - mockHttpResponse(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, null); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)) // server is not HTTPS - .thenThrow(new HttpResponseException(redirectResponse)) // redirect to HTTP - .thenThrow(new HttpResponseException(unauthroizedResponse)); // final response - Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); + ResponseException unauthorizedException = + mockResponseException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, null); + Mockito.when(unauthorizedException.requestAuthorizationCleared()).thenReturn(true); + setUpRegistryResponse(unauthorizedException); + try { - insecureEndpointCaller.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryCredentialsNotSentException ex) { @@ -346,39 +184,20 @@ public void testCall_credentialsNotSentOverHttp() throws IOException, RegistryEx @Test public void testCall_credentialsForcedOverHttp() throws IOException, RegistryException { - HttpResponse redirectResponse = mockRedirectHttpResponse("http://newlocation"); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)) // server is not HTTPS - .thenThrow(new HttpResponseException(redirectResponse)) // redirect to HTTP - .thenReturn(mockResponse); // final response - Mockito.when(mockInsecureConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(Mockito.mock(SSLPeerUnverifiedException.class)); // server is not HTTPS - + ResponseException unauthorizedException = + mockResponseException(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, null); + setUpRegistryResponse(unauthorizedException); System.setProperty(JibSystemProperties.SEND_CREDENTIALS_OVER_HTTP, "true"); - RegistryEndpointCaller insecureEndpointCaller = createRegistryEndpointCaller(true, -1); - Assert.assertEquals("body", insecureEndpointCaller.call()); - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(3)).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("http://serverUrl/v2/api"), urlCaptor.getAllValues().get(1)); - Assert.assertEquals(new URL("http://newlocation"), urlCaptor.getAllValues().get(2)); - - Mockito.verify(mockInsecureConnectionFactory).apply(urlCaptor.capture()); - Assert.assertEquals(new URL("https://serverUrl/v2/api"), urlCaptor.getAllValues().get(3)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); + try { + endpointCaller.call(); + Assert.fail("Call should have failed"); - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Cannot verify server at https://serverUrl/v2/api. Attempting again with no TLS verification.")); - Mockito.verify(mockEventHandlers) - .dispatch( - LogEvent.info( - "Failed to connect to https://serverUrl/v2/api over HTTPS. Attempting again with HTTP: http://serverUrl/v2/api")); + } catch (RegistryCredentialsNotSentException ex) { + throw new AssertionError("should have sent credentials", ex); + } catch (RegistryUnauthorizedException ex) { + Assert.assertEquals("Unauthorized for serverUrl/imageName", ex.getMessage()); + } } @Test @@ -403,19 +222,16 @@ public void testCall_methodNotAllowed() throws IOException, RegistryException { @Test public void testCall_unknown() throws IOException, RegistryException { - HttpResponse mockHttpResponse = - mockHttpResponse(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, null); - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + ResponseException responseException = + mockResponseException(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, null); + setUpRegistryResponse(responseException); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); - } catch (HttpResponseException ex) { - Assert.assertSame(httpResponseException, ex); + } catch (ResponseException ex) { + Assert.assertSame(responseException, ex); } } @@ -434,32 +250,13 @@ public void testCall_permanentRedirect() throws IOException, RegistryException { verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); } - @Test - public void testCall_disallowInsecure() throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. - HttpResponse redirectResponse = mockRedirectHttpResponse("http://newlocation"); - - HttpResponseException redirectException = new HttpResponseException(redirectResponse); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(redirectException); - - try { - secureEndpointCaller.call(); - Assert.fail("Call should have failed"); - - } catch (InsecureRegistryException ex) { - // pass - } - } - @Test public void testCall_logErrorOnIoExceptions() throws IOException, RegistryException { IOException ioException = new IOException("detailed exception message"); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(ioException); + setUpRegistryResponse(ioException); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail(); } catch (IOException ex) { @@ -476,11 +273,10 @@ public void testCall_logErrorOnIoExceptions() throws IOException, RegistryExcept @Test public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryException { IOException ioException = new IOException("this is due to broken pipe"); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(ioException); + setUpRegistryResponse(ioException); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail(); } catch (IOException ex) { @@ -503,11 +299,11 @@ public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryExceptio @Test public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); - secureEndpointCaller.call(); + endpointCaller.call(); // We fall back to the default timeout: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 @@ -517,11 +313,11 @@ public void testHttpTimeout_propertyNotSet() throws IOException, RegistryExcepti @Test public void testHttpTimeout_stringValue() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "random string"); - secureEndpointCaller.call(); + endpointCaller.call(); Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @@ -529,11 +325,11 @@ public void testHttpTimeout_stringValue() throws IOException, RegistryException @Test public void testHttpTimeout_negativeValue() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "-1"); - secureEndpointCaller.call(); + endpointCaller.call(); // We let the negative value pass through: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 @@ -543,11 +339,11 @@ public void testHttpTimeout_negativeValue() throws IOException, RegistryExceptio @Test public void testHttpTimeout_0accepted() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "0"); - secureEndpointCaller.call(); + endpointCaller.call(); Assert.assertEquals(0, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @@ -555,11 +351,11 @@ public void testHttpTimeout_0accepted() throws IOException, RegistryException { @Test public void testHttpTimeout() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), requestCaptor.capture())) + Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.setProperty(JibSystemProperties.HTTP_TIMEOUT, "7593"); - secureEndpointCaller.call(); + endpointCaller.call(); Assert.assertEquals(7593, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @@ -594,13 +390,13 @@ public void testIsBrokenPipe_terminatesWhenCauseIsOriginal() { @Test public void testNewRegistryErrorException_jsonErrorOutput() { - HttpResponseException httpException = Mockito.mock(HttpResponseException.class); + ResponseException httpException = Mockito.mock(ResponseException.class); Mockito.when(httpException.getContent()) .thenReturn( "{\"errors\": [{\"code\": \"MANIFEST_UNKNOWN\", \"message\": \"manifest unknown\"}]}"); RegistryErrorException registryException = - secureEndpointCaller.newRegistryErrorException(httpException); + endpointCaller.newRegistryErrorException(httpException); Assert.assertSame(httpException, registryException.getCause()); Assert.assertEquals( "Tried to actionDescription but failed because: manifest unknown | If this is a bug, " @@ -610,13 +406,13 @@ public void testNewRegistryErrorException_jsonErrorOutput() { @Test public void testNewRegistryErrorException_nonJsonErrorOutput() { - HttpResponseException httpException = Mockito.mock(HttpResponseException.class); + ResponseException httpException = Mockito.mock(ResponseException.class); // Registry returning non-structured error output Mockito.when(httpException.getContent()).thenReturn(">>>>> (404) page not found <<<<<"); Mockito.when(httpException.getStatusCode()).thenReturn(404); RegistryErrorException registryException = - secureEndpointCaller.newRegistryErrorException(httpException); + endpointCaller.newRegistryErrorException(httpException); Assert.assertSame(httpException, registryException.getCause()); Assert.assertEquals( "Tried to actionDescription but failed because: registry returned error code 404; " @@ -633,19 +429,16 @@ public void testNewRegistryErrorException_nonJsonErrorOutput() { */ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) throws IOException, RegistryException { - HttpResponse mockHttpResponse = mockHttpResponse(httpStatusCode, null); - HttpResponseException httpResponseException = new HttpResponseException(mockHttpResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + ResponseException responseException = mockResponseException(httpStatusCode, null); + setUpRegistryResponse(responseException); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryUnauthorizedException ex) { Assert.assertEquals("serverUrl/imageName", ex.getImageReference()); - Assert.assertSame(httpResponseException, ex.getHttpResponseException()); + Assert.assertSame(responseException, ex.getCause()); } } @@ -655,16 +448,13 @@ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) */ private void verifyThrowsRegistryErrorException(int httpStatusCode) throws IOException, RegistryException { - HttpResponse errorResponse = mockHttpResponse(httpStatusCode, null); - Mockito.when(errorResponse.parseAsString()) + ResponseException errorResponse = mockResponseException(httpStatusCode, null); + Mockito.when(errorResponse.getContent()) .thenReturn("{\"errors\":[{\"code\":\"code\",\"message\":\"message\"}]}"); - HttpResponseException httpResponseException = new HttpResponseException(errorResponse); - - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(httpResponseException); + setUpRegistryResponse(errorResponse); try { - secureEndpointCaller.call(); + endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryErrorException ex) { @@ -682,39 +472,28 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) private void verifyRetriesWithNewLocation(int httpStatusCode) throws IOException, RegistryException { // Mocks a response for temporary redirect to a new location. - HttpResponse redirectResponse = - mockHttpResponse(httpStatusCode, new HttpHeaders().setLocation("https://newlocation")); - - // Has mockConnection.send throw first, then succeed. - HttpResponseException redirectException = new HttpResponseException(redirectResponse); - Mockito.when(mockConnection.send(Mockito.eq("httpMethod"), Mockito.any())) - .thenThrow(redirectException) + ResponseException redirectException = + mockResponseException(httpStatusCode, new HttpHeaders().setLocation("https://newlocation")); + + // Make httpClient.call() throw first, then succeed. + setUpRegistryResponse(redirectException); + Mockito.when( + mockHttpClient.call( + Mockito.eq("httpMethod"), + Mockito.eq(new URL("https://newlocation")), + Mockito.any())) .thenReturn(mockResponse); - Assert.assertEquals("body", secureEndpointCaller.call()); + Assert.assertEquals("body", endpointCaller.call()); + } - // Checks that the URL was changed to the new location. - ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(URL.class); - Mockito.verify(mockConnectionFactory, Mockito.times(2)).apply(urlArgumentCaptor.capture()); - Assert.assertEquals( - new URL("https://serverUrl/v2/api"), urlArgumentCaptor.getAllValues().get(0)); - Assert.assertEquals(new URL("https://newlocation"), urlArgumentCaptor.getAllValues().get(1)); - - Mockito.verifyNoMoreInteractions(mockConnectionFactory); - Mockito.verifyNoMoreInteractions(mockInsecureConnectionFactory); - } - - private RegistryEndpointCaller createRegistryEndpointCaller( - boolean allowInsecure, int port) { - return new RegistryEndpointCaller<>( - mockEventHandlers, - "userAgent", - new TestRegistryEndpointProvider(), - Authorization.fromBasicToken("token"), - new RegistryEndpointRequestProperties( - (port == -1 ? "serverUrl" : "serverUrl:" + port), "imageName"), - allowInsecure, - mockConnectionFactory, - mockInsecureConnectionFactory); + private void setUpRegistryResponse(Exception exceptionToThrow) + throws MalformedURLException, IOException { + Mockito.when( + mockHttpClient.call( + Mockito.eq("httpMethod"), + Mockito.eq(new URL("https://serverUrl/v2/api")), + Mockito.any())) + .thenThrow(exceptionToThrow); } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e40f378907..4b346e1db8 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) - Now correctly uses the `war` task if it is enabled and the `bootWar` task is disabled for Spring WAR projects. ([#2096](https://github.com/GoogleContainerTools/jib/issues/2096)) +- `allowInsecureRegistries` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) ## 1.7.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4e4522e35a..0b1bb415a3 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed reporting wrong module name when `skaffold init` is run on multi-module projects ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)). +- `` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) ## 1.7.0 diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index a913c1e264..0ca73689ad 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -16,7 +16,6 @@ package com.google.cloud.tools.jib.plugins.common; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; @@ -28,6 +27,7 @@ import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; @@ -251,10 +251,10 @@ public JibContainer runBuild() throw new BuildStepsExecutionException(helpfulSuggestions.forCredentialsNotSent(), ex); } catch (RegistryAuthenticationFailedException ex) { - if (ex.getCause() instanceof HttpResponseException) { + if (ex.getCause() instanceof ResponseException) { handleRegistryUnauthorizedException( new RegistryUnauthorizedException( - ex.getServerUrl(), ex.getImageName(), (HttpResponseException) ex.getCause()), + ex.getServerUrl(), ex.getImageName(), (ResponseException) ex.getCause()), helpfulSuggestions); } else { // Unknown cause From 5ee3281fc9560c7c40649a036e41d7abb1fe732a Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 1 Nov 2019 16:16:23 -0400 Subject: [PATCH 0808/2020] Make intermediate build config available (#2115) * Make intermediate build config available --- jib-core/CHANGELOG.md | 2 + .../cloud/tools/jib/api/FilePermissions.java | 6 ++- .../tools/jib/api/JibContainerBuilder.java | 10 ++++ .../jib/api/JibContainerDescription.java | 45 +++++++++++++++++ .../cloud/tools/jib/api/LayerEntry.java | 2 + .../common/PluginConfigurationProcessor.java | 48 ++++++++++++------- 6 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index e06e3c9785..00150da1c3 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- New method: `JibContainerBuilder#describeContainer` which returns new class: `JibContainerDescription`, containing a selection of information used for the Jib build. ([#2115](https://github.com/GoogleContainerTools/jib/issues/2115)) + ### Changed ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java index c206666d5d..7820dc10de 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/FilePermissions.java @@ -22,7 +22,11 @@ import java.nio.file.attribute.PosixFilePermission; import java.util.Set; -/** Represents read/write/execute file permissions for owner, group, and others. */ +/** + * Represents read/write/execute file permissions for owner, group, and others. + * + *

This class is immutable and thread-safe. + */ public class FilePermissions { /** Default permissions for files added to the container. */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 524a11250f..1390e0a045 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -477,6 +477,16 @@ public JibContainer containerize(Containerizer containerizer) return containerize(containerizer, Executors::newCachedThreadPool); } + /** + * Describes the container contents and configuration without actually physically building a + * container. + * + * @return a description of the container being built + */ + public JibContainerDescription describeContainer() { + return new JibContainerDescription(layerConfigurations); + } + @VisibleForTesting JibContainer containerize( Containerizer containerizer, Supplier defaultExecutorServiceFactory) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java new file mode 100644 index 0000000000..3862fcefd8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.cloud.tools.jib.api; + +import com.google.cloud.tools.jib.configuration.ContainerConfiguration; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** + * A class containing the representation of the contents of a container. Currently only exposes + * "layers", but can be extended to expose {@link ContainerConfiguration}, {@link ImageReference} of + * the base image, or other informational classes. + * + *

This class is immutable and thread-safe. + */ +public class JibContainerDescription { + + private final ImmutableList layers; + + JibContainerDescription(List layers) { + this.layers = ImmutableList.copyOf(layers); + } + + /** + * Returns a list of "user configured" layers, does *not* include base layer information. + * + * @return An {@link ImmutableList} of {@link LayerConfiguration}s + */ + public List getLayers() { + return layers; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java index d1de767b80..a3c9ca1aa4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java @@ -23,6 +23,8 @@ /** * Represents an entry in the layer. A layer consists of many entries that can be converted into tar * archive entries. + * + *

This class is immutable and thread-safe. */ public class LayerEntry { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index ccf39fb73b..35486e8d1e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -176,24 +176,12 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( static JibContainerBuilder processCommonConfiguration( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, - ProjectProperties projectProperties, - Containerizer containerizer) - throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, - IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, - IncompatibleBaseImageJavaVersionException, NumberFormatException, - InvalidContainerizingModeException, InvalidFilesModificationTimeException, + ProjectProperties projectProperties) + throws InvalidFilesModificationTimeException, InvalidAppRootException, + IncompatibleBaseImageJavaVersionException, IOException, InvalidImageReferenceException, + InvalidContainerizingModeException, MainClassInferenceException, + InvalidContainerVolumeException, InvalidWorkingDirectoryException, InvalidCreationTimeException { - JibSystemProperties.checkHttpTimeoutProperty(); - JibSystemProperties.checkProxyPortProperty(); - - if (JibSystemProperties.sendCredentialsOverHttp()) { - projectProperties.log( - LogEvent.warn( - "Authentication over HTTP is enabled. It is strongly recommended that you do not " - + "enable this on a public network!")); - } - - configureContainerizer(containerizer, rawConfiguration, projectProperties); // Create and configure JibContainerBuilder BiFunction modificationTimeProvider = @@ -240,6 +228,32 @@ static JibContainerBuilder processCommonConfiguration( return jibContainerBuilder; } + @VisibleForTesting + static JibContainerBuilder processCommonConfiguration( + RawConfiguration rawConfiguration, + InferredAuthProvider inferredAuthProvider, + ProjectProperties projectProperties, + Containerizer containerizer) + throws InvalidImageReferenceException, MainClassInferenceException, InvalidAppRootException, + IOException, InvalidWorkingDirectoryException, InvalidContainerVolumeException, + IncompatibleBaseImageJavaVersionException, NumberFormatException, + InvalidContainerizingModeException, InvalidFilesModificationTimeException, + InvalidCreationTimeException { + JibSystemProperties.checkHttpTimeoutProperty(); + JibSystemProperties.checkProxyPortProperty(); + + if (JibSystemProperties.sendCredentialsOverHttp()) { + projectProperties.log( + LogEvent.warn( + "Authentication over HTTP is enabled. It is strongly recommended that you do not " + + "enable this on a public network!")); + } + + configureContainerizer(containerizer, rawConfiguration, projectProperties); + + return processCommonConfiguration(rawConfiguration, inferredAuthProvider, projectProperties); + } + /** * Returns a {@link JavaContainerBuilder} with the correctly parsed base image configuration. * From 2c8badd0841eb3fca89088db5e678df65f095554 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 4 Nov 2019 11:31:24 -0500 Subject: [PATCH 0809/2020] Finalize new HTTP framework changes (#2118) * Rename Connection to FailoverHttpClient * Remove "default" interface implementation --- ...onnection.java => FailoverHttpClient.java} | 10 +- .../AuthenticationMethodRetriever.java | 6 +- .../cloud/tools/jib/registry/BlobPuller.java | 7 + .../cloud/tools/jib/registry/BlobPusher.java | 19 +++ .../tools/jib/registry/ManifestPuller.java | 7 + .../jib/registry/RegistryAuthenticator.java | 8 +- .../tools/jib/registry/RegistryClient.java | 6 +- .../jib/registry/RegistryEndpointCaller.java | 6 +- .../registry/RegistryEndpointProvider.java | 9 +- ...iloverHttpClientProxyCredentialsTest.java} | 17 +- ...nTest.java => FailoverHttpClientTest.java} | 159 +++++++++--------- ... => WithServerFailoverHttpClientTest.java} | 17 +- .../AuthenticationMethodRetrieverTest.java | 4 +- .../registry/RegistryAuthenticatorTest.java | 6 +- .../registry/RegistryEndpointCallerTest.java | 10 +- 15 files changed, 166 insertions(+), 125 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/http/{Connection.java => FailoverHttpClient.java} (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/http/{ConnectionWithProxyCredentialsTest.java => FailoverHttpClientProxyCredentialsTest.java} (92%) rename jib-core/src/test/java/com/google/cloud/tools/jib/http/{ConnectionTest.java => FailoverHttpClientTest.java} (91%) rename jib-core/src/test/java/com/google/cloud/tools/jib/http/{WithServerConnectionTest.java => WithServerFailoverHttpClientTest.java} (89%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index f3671a6188..46bf2615ee 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Connection.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -68,7 +68,7 @@ * This failover behavior is similar to how the Docker client works: * https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry */ -public class Connection { // TODO: rename to TlsFailoverHttpClient +public class FailoverHttpClient { private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); @@ -141,7 +141,7 @@ private static void addProxyCredentials(ApacheHttpTransport transport, String pr private final Supplier secureHttpTransportFactory; private final Supplier insecureHttpTransportFactory; - public Connection( + public FailoverHttpClient( boolean enableHttpAndInsecureFailover, boolean sendAuthorizationOverHttp, Consumer logger) { @@ -149,12 +149,12 @@ public Connection( enableHttpAndInsecureFailover, sendAuthorizationOverHttp, logger, - Connection::getSecureHttpTransport, - Connection::getInsecureHttpTransport); + FailoverHttpClient::getSecureHttpTransport, + FailoverHttpClient::getInsecureHttpTransport); } @VisibleForTesting - Connection( + FailoverHttpClient( boolean enableHttpAndInsecureFailover, boolean sendAuthorizationOverHttp, Consumer logger, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java index 5176cec504..fa2adfb432 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java @@ -20,7 +20,7 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; @@ -36,12 +36,12 @@ class AuthenticationMethodRetriever private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String userAgent; - private final Connection httpClient; + private final FailoverHttpClient httpClient; AuthenticationMethodRetriever( RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent, - Connection httpClient) { + FailoverHttpClient httpClient) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; this.httpClient = httpClient; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java index 04dd6b4bc3..64bf0ff1d6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; @@ -114,4 +115,10 @@ public String getActionDescription() { + " with digest " + blobDigest; } + + @Override + public Void handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java index 917e573339..dd16cca6cc 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.common.net.MediaType; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -107,6 +108,12 @@ public String getHttpMethod() { public String getActionDescription() { return BlobPusher.this.getActionDescription(); } + + @Override + public Optional handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } /** Writes the BLOB content to the upload location. */ @@ -152,6 +159,12 @@ private Writer(URL location, Consumer writtenByteCountListener) { this.location = location; this.writtenByteCountListener = writtenByteCountListener; } + + @Override + public URL handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } /** Commits the written BLOB. */ @@ -194,6 +207,12 @@ public String getActionDescription() { private Committer(URL location) { this.location = location; } + + @Override + public Void handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } BlobPusher( diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index b8343d72d8..c8438ee257 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.BlobHttpContent; import com.google.cloud.tools.jib.http.Response; +import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; @@ -160,4 +161,10 @@ private T getManifestTemplateFromJson(String jsonString) throw new UnknownManifestFormatException( "Unknown schemaVersion: " + schemaVersion + " - only 1 and 2 are supported"); } + + @Override + public ManifestAndDigest handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 8cd76441ab..7640f866ef 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; @@ -70,7 +70,7 @@ static Optional fromAuthenticationMethod( String authenticationMethod, RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent, - Connection httpClient) + FailoverHttpClient httpClient) throws RegistryAuthenticationFailedException { // If the authentication method starts with 'basic ' (case insensitive), no registry // authentication is needed. @@ -143,14 +143,14 @@ private String getToken() { private final String realm; private final String service; private final String userAgent; - private final Connection httpClient; + private final FailoverHttpClient httpClient; private RegistryAuthenticator( String realm, String service, RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent, - Connection httpClient) { + FailoverHttpClient httpClient) { this.realm = realm; this.service = service; this.registryEndpointRequestProperties = registryEndpointRequestProperties; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 1214f6aa3b..cdcda32702 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -28,7 +28,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ManifestTemplate; @@ -244,7 +244,7 @@ static Multimap decodeTokenRepositoryGrants(String token) { @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String userAgent; - private final Connection httpClient; + private final FailoverHttpClient httpClient; /** * Instantiate with {@link #factory}. @@ -266,7 +266,7 @@ private RegistryClient( this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; this.httpClient = - new Connection( + new FailoverHttpClient( allowInsecureRegistries, JibSystemProperties.sendCredentialsOverHttp(), eventHandlers::dispatch); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 49cc444716..eb5d0fd98a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; @@ -74,7 +74,7 @@ static boolean isBrokenPipe(IOException original) { private final RegistryEndpointProvider registryEndpointProvider; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; - private final Connection httpClient; + private final FailoverHttpClient httpClient; /** * Constructs with parameters for making the request. @@ -93,7 +93,7 @@ static boolean isBrokenPipe(IOException original) { RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - Connection httpClient) { + FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java index d6a2e7c502..6ae0842519 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java @@ -54,17 +54,16 @@ interface RegistryEndpointProvider { T handleResponse(Response response) throws IOException, RegistryException; /** - * Handles an {@link ResponseException} that occurs. + * Handles an {@link ResponseException} that occurs. Implementation must re-throw the given + * exception if it did not conclusively handled the response exception. * * @param responseException the {@link ResponseException} to handle * @throws HttpResponseException {@code responseException} if {@code responseException} could not * be handled * @throws RegistryErrorException if there is an error with a remote registry */ - default T handleHttpResponseException(ResponseException responseException) - throws ResponseException, RegistryErrorException { - throw responseException; - } + T handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException; /** * @return a description of the registry action performed, used in error messages to describe the diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java similarity index 92% rename from jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java index 385a05b21b..615f3f0cfb 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionWithProxyCredentialsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java @@ -27,9 +27,8 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; -/** Tests for {@link Connection} with setting proxy credentials. */ -// TODO: rename to TlsFailoverHttpClientProxyCredentialsTest -public class ConnectionWithProxyCredentialsTest { +/** Tests for {@link FailoverHttpClient} with setting proxy credentials. */ +public class FailoverHttpClientProxyCredentialsTest { private static final ImmutableList proxyProperties = ImmutableList.of( @@ -53,7 +52,7 @@ public void setUp() { @Test public void testAddProxyCredentials_undefined() { - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); Assert.assertNull(credentials); @@ -71,7 +70,7 @@ public void testAddProxyCredentials() { System.setProperty("https.proxyUser", "s-user"); System.setProperty("https.proxyPassword", "s-pass"); - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials httpCredentials = httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); @@ -94,7 +93,7 @@ public void testAddProxyCredentials_defaultPorts() { System.setProperty("https.proxyUser", "s-user"); System.setProperty("https.proxyPassword", "s-pass"); - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials httpCredentials = httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); @@ -115,7 +114,7 @@ public void testAddProxyCredentials_hostUndefined() { System.setProperty("https.proxyUser", "s-user"); System.setProperty("https.proxyPassword", "s-pass"); - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); Assert.assertNull(credentials); @@ -129,7 +128,7 @@ public void testAddProxyCredentials_userUndefined() { System.setProperty("https.proxyHost", "https://host.com"); System.setProperty("https.proxyPassword", "s-pass"); - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); Assert.assertNull(credentials); @@ -143,7 +142,7 @@ public void testAddProxyCredentials_passwordUndefined() { System.setProperty("https.proxyHost", "https://host.com"); System.setProperty("https.proxyUser", "s-user"); - Connection.addProxyCredentials(transport); + FailoverHttpClient.addProxyCredentials(transport); DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); Assert.assertNull(credentials); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java similarity index 91% rename from jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java index 6049f28bf8..f796dd6633 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/ConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -47,14 +47,14 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link Connection}. */ +/** Tests for {@link FailoverHttpClient}. */ @RunWith(MockitoJUnitRunner.class) -public class ConnectionTest { // TODO: rename to TlsFailoverHttpClient +public class FailoverHttpClientTest { @FunctionalInterface private interface CallFunction { - Response call(Connection httpClient, URL url, Request request) throws IOException; + Response call(FailoverHttpClient httpClient, URL url, Request request) throws IOException; } @Mock private HttpTransport mockHttpTransport; @@ -79,29 +79,19 @@ public void setUp() throws IOException { Mockito.when(mockHttpResponse.getContent()).thenReturn(inStream); } - private Connection newHttpClient(boolean insecure, boolean authOverHttp) throws IOException { - setUpMocks(mockHttpTransport, mockHttpRequestFactory, mockHttpRequest); - if (insecure) { - setUpMocks( - mockInsecureHttpTransport, mockInsecureHttpRequestFactory, mockInsecureHttpRequest); - } - return new Connection( - insecure, authOverHttp, logger, () -> mockHttpTransport, () -> mockInsecureHttpTransport); - } - @Test public void testGet() throws IOException { - verifyCall(HttpMethods.GET, Connection::get); + verifyCall(HttpMethods.GET, FailoverHttpClient::get); } @Test public void testPost() throws IOException { - verifyCall(HttpMethods.POST, Connection::post); + verifyCall(HttpMethods.POST, FailoverHttpClient::post); } @Test public void testPut() throws IOException { - verifyCall(HttpMethods.PUT, Connection::put); + verifyCall(HttpMethods.PUT, FailoverHttpClient::put); } @Test @@ -114,68 +104,17 @@ public void testHttpTimeout_doNotSetByDefault() throws IOException { @Test public void testHttpTimeout() throws IOException { - Connection httpClient = newHttpClient(false, false); + FailoverHttpClient httpClient = newHttpClient(false, false); try (Response ignored = httpClient.get(fakeUrl.toURL(), fakeRequest(5982))) {} Mockito.verify(mockHttpRequest).setConnectTimeout(5982); Mockito.verify(mockHttpRequest).setReadTimeout(5982); } - private Request fakeRequest(Integer httpTimeout) { - return Request.builder() - .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) - .setUserAgent("fake user agent") - .setBody( - new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) - .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) - .setHttpTimeout(httpTimeout) - .build(); - } - - private void setUpMocks( - HttpTransport mockHttpTransport, - HttpRequestFactory mockHttpRequestFactory, - HttpRequest mockHttpRequest) - throws IOException { - Mockito.when(mockHttpTransport.createRequestFactory()).thenReturn(mockHttpRequestFactory); - Mockito.when( - mockHttpRequestFactory.buildRequest(Mockito.any(), urlCaptor.capture(), Mockito.any())) - .thenReturn(mockHttpRequest); - - Mockito.when(mockHttpRequest.setHeaders(httpHeadersCaptor.capture())) - .thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); - Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); - } - - private void verifyCall(String httpMethod, CallFunction callFunction) throws IOException { - Connection httpClient = newHttpClient(false, false); - try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) {} - - Assert.assertEquals( - "fake.accept,another.fake.accept", httpHeadersCaptor.getValue().getAccept()); - Assert.assertEquals("fake user agent", httpHeadersCaptor.getValue().getUserAgent()); - // Base64 representation of "fake-username:fake-secret" - Assert.assertEquals( - "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", - httpHeadersCaptor.getValue().getAuthorization()); - - Mockito.verify(mockHttpRequestFactory) - .buildRequest(Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentCaptor.capture()); - Assert.assertEquals("fake.content.type", blobHttpContentCaptor.getValue().getType()); - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - blobHttpContentCaptor.getValue().writeTo(byteArrayOutputStream); - - Assert.assertEquals("crepecake", byteArrayOutputStream.toString(StandardCharsets.UTF_8.name())); - Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); - } - @Test public void testGet_nonHttpsServer_insecureConnectionAndFailoverDisabled() throws MalformedURLException, IOException { - Connection httpClient = newHttpClient(false, false); + FailoverHttpClient httpClient = newHttpClient(false, false); try (Response response = httpClient.get(new URL("http://plain.http"), fakeRequest(null))) { Assert.fail("Should disallow non-HTTP attempt"); } catch (SSLException ex) { @@ -186,7 +125,7 @@ public void testGet_nonHttpsServer_insecureConnectionAndFailoverDisabled() @Test public void testCall_secureClientOnUnverifiableServer() throws IOException { - Connection httpClient = newHttpClient(false, false); + FailoverHttpClient httpClient = newHttpClient(false, false); Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("unverified")); @@ -200,7 +139,7 @@ public void testCall_secureClientOnUnverifiableServer() throws IOException { @Test public void testGet_insecureClientOnUnverifiableServer() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Mockito.when(mockHttpRequest.execute()).thenThrow(new SSLPeerUnverifiedException("")); @@ -223,7 +162,7 @@ public void testGet_insecureClientOnUnverifiableServer() throws IOException { @Test public void testGet_insecureClientOnHttpServer() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Mockito.when(mockHttpRequest.execute()) .thenThrow(new SSLException("")) // server is not HTTPS @@ -256,7 +195,7 @@ public void testGet_insecureClientOnHttpServer() throws IOException { @Test public void testGet_insecureClientOnHttpServerAndNoPortSpecified() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Mockito.when(mockHttpRequest.execute()) .thenThrow(new ConnectException()) // server is not listening on 443 @@ -283,7 +222,7 @@ public void testGet_insecureClientOnHttpServerAndNoPortSpecified() throws IOExce @Test public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws IOException { - Connection httpClient = newHttpClient(false, false); + FailoverHttpClient httpClient = newHttpClient(false, false); Mockito.when(mockHttpRequest.execute()) .thenThrow(new ConnectException("my exception")); // server not listening on 443 @@ -303,7 +242,7 @@ public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws @Test public void testGet_insecureClientOnNonListeningServerAndPortSpecified() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Mockito.when(mockHttpRequest.execute()) .thenThrow(new ConnectException("my exception")); // server is not listening on 5000 @@ -324,7 +263,7 @@ public void testGet_insecureClientOnNonListeningServerAndPortSpecified() throws @Test public void testGet_timeoutFromConnectException() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Mockito.when(mockHttpRequest.execute()).thenThrow(new ConnectException("Connection timed out")); @@ -344,7 +283,7 @@ public void testGet_timeoutFromConnectException() throws IOException { @Test public void testGet_doNotSendCredentialsOverHttp() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); // make it fall back to HTTP Mockito.when(mockHttpRequest.execute()) @@ -367,7 +306,7 @@ public void testGet_doNotSendCredentialsOverHttp() throws IOException { @Test public void testGet_sendCredentialsOverHttp() throws IOException { - Connection insecureHttpClient = newHttpClient(true, true); // sendCredentialsOverHttp + FailoverHttpClient insecureHttpClient = newHttpClient(true, true); // sendCredentialsOverHttp try (Response response = insecureHttpClient.get(new URL("http://plain.http"), fakeRequest(null))) {} @@ -381,7 +320,7 @@ public void testGet_sendCredentialsOverHttp() throws IOException { @Test public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IOException { - Connection insecureHttpClient = newHttpClient(true, false); + FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Request request = fakeRequest(null); try (Response response = insecureHttpClient.get(new URL("http://plain.http"), request)) {} @@ -393,4 +332,66 @@ public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IO Assert.assertEquals( "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", request.getHeaders().getAuthorization()); } + + private void setUpMocks( + HttpTransport mockHttpTransport, + HttpRequestFactory mockHttpRequestFactory, + HttpRequest mockHttpRequest) + throws IOException { + Mockito.when(mockHttpTransport.createRequestFactory()).thenReturn(mockHttpRequestFactory); + Mockito.when( + mockHttpRequestFactory.buildRequest(Mockito.any(), urlCaptor.capture(), Mockito.any())) + .thenReturn(mockHttpRequest); + + Mockito.when(mockHttpRequest.setHeaders(httpHeadersCaptor.capture())) + .thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setReadTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.execute()).thenReturn(mockHttpResponse); + } + + private FailoverHttpClient newHttpClient(boolean insecure, boolean authOverHttp) + throws IOException { + setUpMocks(mockHttpTransport, mockHttpRequestFactory, mockHttpRequest); + if (insecure) { + setUpMocks( + mockInsecureHttpTransport, mockInsecureHttpRequestFactory, mockInsecureHttpRequest); + } + return new FailoverHttpClient( + insecure, authOverHttp, logger, () -> mockHttpTransport, () -> mockInsecureHttpTransport); + } + + private Request fakeRequest(Integer httpTimeout) { + return Request.builder() + .setAccept(Arrays.asList("fake.accept", "another.fake.accept")) + .setUserAgent("fake user agent") + .setBody( + new BlobHttpContent(Blobs.from("crepecake"), "fake.content.type", totalByteCount::add)) + .setAuthorization(Authorization.fromBasicCredentials("fake-username", "fake-secret")) + .setHttpTimeout(httpTimeout) + .build(); + } + + private void verifyCall(String httpMethod, CallFunction callFunction) throws IOException { + FailoverHttpClient httpClient = newHttpClient(false, false); + try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) {} + + Assert.assertEquals( + "fake.accept,another.fake.accept", httpHeadersCaptor.getValue().getAccept()); + Assert.assertEquals("fake user agent", httpHeadersCaptor.getValue().getUserAgent()); + // Base64 representation of "fake-username:fake-secret" + Assert.assertEquals( + "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", + httpHeadersCaptor.getValue().getAuthorization()); + + Mockito.verify(mockHttpRequestFactory) + .buildRequest(Mockito.eq(httpMethod), Mockito.eq(fakeUrl), blobHttpContentCaptor.capture()); + Assert.assertEquals("fake.content.type", blobHttpContentCaptor.getValue().getType()); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + blobHttpContentCaptor.getValue().writeTo(byteArrayOutputStream); + + Assert.assertEquals("crepecake", byteArrayOutputStream.toString(StandardCharsets.UTF_8.name())); + Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java similarity index 89% rename from jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index ed2cd1063d..471b74002a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerConnectionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -36,9 +36,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link Connection} using an actual local server. */ +/** Tests for {@link FailoverHttpClient} using an actual local server. */ @RunWith(MockitoJUnitRunner.class) -public class WithServerConnectionTest { // TODO: rename to WithServerTlsFailoverHttpClientTest +public class WithServerFailoverHttpClientTest { @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @@ -49,7 +49,8 @@ public class WithServerConnectionTest { // TODO: rename to WithServerTlsFailover @Test public void testGet() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false); Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { @@ -64,7 +65,7 @@ public void testGet() @Test public void testSecureConnectionOnInsecureHttpsServer() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - Connection secureHttpClient = new Connection(false /*secure*/, false, logger); + FailoverHttpClient secureHttpClient = new FailoverHttpClient(false /*secure*/, false, logger); try (TestWebServer server = new TestWebServer(true); Response ignored = secureHttpClient.get(new URL(server.getEndpoint()), request)) { Assert.fail("Should fail if cannot verify peer"); @@ -77,7 +78,8 @@ public void testSecureConnectionOnInsecureHttpsServer() @Test public void testInsecureConnection_insecureHttpsFailover() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(true, 2); Response response = insecureHttpClient.get(new URL(server.getEndpoint()), request)) { @@ -96,7 +98,8 @@ public void testInsecureConnection_insecureHttpsFailover() @Test public void testInsecureConnection_plainHttpFailover() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { - Connection insecureHttpClient = new Connection(true /*insecure*/, false, logger); + FailoverHttpClient insecureHttpClient = + new FailoverHttpClient(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false, 3)) { String httpsUrl = server.getEndpoint().replace("http://", "https://"); try (Response response = insecureHttpClient.get(new URL(httpsUrl), request)) { @@ -127,7 +130,7 @@ public void testProxyCredentialProperties() + "Content-Length: 0\n\n"; String targetServerResponse = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; - Connection httpClient = new Connection(true /*insecure*/, false, logger); + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false, Arrays.asList(proxyResponse, targetServerResponse), 1)) { System.setProperty("http.proxyHost", "localhost"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 9e0ccfc0d6..a6cc88924a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -19,7 +19,7 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpStatusCodes; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; import java.net.MalformedURLException; @@ -39,7 +39,7 @@ public class AuthenticationMethodRetrieverTest { @Mock private ResponseException mockResponseException; @Mock private HttpHeaders mockHeaders; - @Mock private Connection httpClient; + @Mock private FailoverHttpClient httpClient; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 268dda3475..45431056fc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.http.TestWebServer; @@ -47,7 +47,7 @@ public class RegistryAuthenticatorTest { private final RegistryEndpointRequestProperties registryEndpointRequestProperties = new RegistryEndpointRequestProperties("someserver", "someimage"); - @Mock private Connection httpClient; + @Mock private FailoverHttpClient httpClient; @Mock private Response response; @Captor private ArgumentCaptor urlCaptor; @@ -240,7 +240,7 @@ public void testUserAgent() "Bearer realm=\"" + server.getEndpoint() + "\"", registryEndpointRequestProperties, "Competent-Agent", - new Connection(true, false, ignored -> {})) + new FailoverHttpClient(true, false, ignored -> {})) .get(); authenticator.authenticatePush(null); } catch (RegistryAuthenticationFailedException ex) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index bfebf7455b..2179cd5b2b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -26,7 +26,7 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.BlobHttpContent; -import com.google.cloud.tools.jib.http.Connection; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.Request; import com.google.cloud.tools.jib.http.RequestWrapper; import com.google.cloud.tools.jib.http.Response; @@ -96,6 +96,12 @@ public String handleResponse(Response response) throws IOException { public String getActionDescription() { return "actionDescription"; } + + @Override + public String handleHttpResponseException(ResponseException responseException) + throws ResponseException, RegistryErrorException { + throw responseException; + } } private static ResponseException mockResponseException( @@ -109,7 +115,7 @@ private static ResponseException mockResponseException( @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @Mock private EventHandlers mockEventHandlers; - @Mock private Connection mockHttpClient; + @Mock private FailoverHttpClient mockHttpClient; @Mock private Response mockResponse; private RegistryEndpointCaller endpointCaller; From 13e081412e7193aff001bf169d5a6d4559678c62 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 4 Nov 2019 15:34:28 -0500 Subject: [PATCH 0810/2020] Propagate InsecureRegistryException (#2121) --- .../builder/steps/AuthenticatePushStep.java | 3 -- .../jib/builder/steps/PullBaseImageStep.java | 35 ++++++++----------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 783fbad8ca..55b56200a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.builder.steps; import com.google.cloud.tools.jib.api.Credential; -import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; @@ -68,8 +67,6 @@ public Optional call() throws IOException, RegistryException { if (registryAuthenticator.isPresent()) { return Optional.of(registryAuthenticator.get().authenticatePush(registryCredential)); } - } catch (InsecureRegistryException ex) { - // Cannot skip certificate validation or use HTTP; fall through. } return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index a84a7003ef..cc249c3743 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.api.InsecureRegistryException; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; @@ -123,12 +122,12 @@ public ImageAndAuthorization call() try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pulling base image manifest", 2); - TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) { + TimerEventDispatcher ignored1 = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) { // First, try with no credentials. try { return new ImageAndAuthorization(pullBaseImage(null, progressEventDispatcher), null); - } catch (RegistryUnauthorizedException ex) { + } catch (RegistryUnauthorizedException ignored2) { eventHandlers.dispatch( LogEvent.lifecycle( "The base image requires auth. Trying again for " + imageReference + "...")); @@ -155,26 +154,22 @@ public ImageAndAuthorization call() } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. // See https://docs.docker.com/registry/spec/auth/token - try { - Optional registryAuthenticator = - buildConfiguration - .newBaseImageRegistryClientFactory() - .newRegistryClient() - .getRegistryAuthenticator(); - if (registryAuthenticator.isPresent()) { - Authorization pullAuthorization = - registryAuthenticator.get().authenticatePull(registryCredential); - - return new ImageAndAuthorization( - pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); - } - - } catch (InsecureRegistryException insecureRegistryException) { - // Cannot skip certificate validation or use HTTP; fall through. + Optional registryAuthenticator = + buildConfiguration + .newBaseImageRegistryClientFactory() + .newRegistryClient() + .getRegistryAuthenticator(); + if (registryAuthenticator.isPresent()) { + Authorization pullAuthorization = + registryAuthenticator.get().authenticatePull(registryCredential); + + return new ImageAndAuthorization( + pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); } eventHandlers.dispatch( LogEvent.error( - "Failed to retrieve authentication challenge for registry that required token authentication")); + "Failed to retrieve authentication challenge for registry that required token " + + "authentication")); throw registryUnauthorizedException; } } From 75e626c88dc42c0ec917ee3582313a4801448a0c Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 4 Nov 2019 15:52:30 -0500 Subject: [PATCH 0811/2020] set test project to 1.8 source/target (#2122) --- .../src/test/resources/maven/projects/multi/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml index 0618624466..4196d3dff1 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/pom.xml @@ -12,6 +12,11 @@ 1.0.0.TEST-SNAPSHOT multimodule + + 1.8 + 1.8 + + simple-service From 78fafb07c5a985eda5952439a9b54e79aabf43dc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 4 Nov 2019 16:42:52 -0500 Subject: [PATCH 0812/2020] Upgrade Google HTTP Client to 1.33.0 (Apache v2) (#2120) * Upgrade google-http-client to 1.33.0 (v2) * Remove "excludes" from "google-auth-library-oauth2-http" --- build.gradle | 12 +- jib-core/build.gradle | 5 +- .../tools/jib/http/FailoverHttpClient.java | 57 ++----- .../jib/registry/RegistryEndpointCaller.java | 9 +- ...ailoverHttpClientProxyCredentialsTest.java | 150 ------------------ .../WithServerFailoverHttpClientTest.java | 42 +++++ .../registry/RegistryEndpointCallerTest.java | 46 ++---- 7 files changed, 83 insertions(+), 238 deletions(-) delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java diff --git a/build.gradle b/build.gradle index bad6e6b992..5340839127 100644 --- a/build.gradle +++ b/build.gradle @@ -46,9 +46,10 @@ subprojects { // For Google libraries, check , , , // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml // for best compatibility. - GOOGLE_HTTP_CLIENT: '1.27.0', - GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.16.2', - GUAVA: '28.0-jre', + GOOGLE_HTTP_CLIENT: '1.33.0', + GOOGLE_HTTP_CLIENT_APACHE_V2: '1.33.0', + GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.18.0', + GUAVA: '28.1-jre', COMMONS_COMPRESS: '1.19', JACKSON_DATABIND: '2.9.10', @@ -64,7 +65,10 @@ subprojects { // Use this to ensure we correctly override transitive dependencies // TODO: There might be a plugin that does this task ensureTransitiveDependencyOverrides { - def rules = ["google-http-client": dependencyVersions.GOOGLE_HTTP_CLIENT] + def rules = [ + "google-http-client": dependencyVersions.GOOGLE_HTTP_CLIENT, + "google-http-client-apache-v2": dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2, + ] doLast { configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> def dependency = artifact.moduleVersion.id diff --git a/jib-core/build.gradle b/jib-core/build.gradle index 624a319697..672ab9f3ba 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -5,9 +5,8 @@ plugins { dependencies { implementation "com.google.http-client:google-http-client:${dependencyVersions.GOOGLE_HTTP_CLIENT}" - implementation("com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}") { - exclude group: "com.google.http-client", module: "google-http-client" - } + implementation "com.google.http-client:google-http-client-apache-v2:${dependencyVersions.GOOGLE_HTTP_CLIENT_APACHE_V2}" + implementation "com.google.auth:google-auth-library-oauth2-http:${dependencyVersions.GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP}" implementation "org.apache.commons:commons-compress:${dependencyVersions.COMMONS_COMPRESS}" implementation "com.google.guava:guava:${dependencyVersions.GUAVA}" diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index 46bf2615ee..78e073c5ca 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -22,10 +22,10 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.apache.ApacheHttpTransport; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.util.SslUtils; import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import java.io.IOException; import java.net.ConnectException; import java.net.URL; @@ -33,9 +33,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.net.ssl.SSLException; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.HttpClientBuilder; /** * Thread-safe HTTP client that can automatically failover from secure HTTPS to insecure HTTPS or @@ -86,55 +85,23 @@ private static HttpTransport getSecureHttpTransport() { // // A new ApacheHttpTransport needs to be created for each connection because otherwise HTTP // connection persistence causes the connection to throw NoHttpResponseException. - ApacheHttpTransport transport = new ApacheHttpTransport(); - addProxyCredentials(transport); - return transport; + return new ApacheHttpTransport(); } private static HttpTransport getInsecureHttpTransport() { try { - ApacheHttpTransport insecureTransport = - new ApacheHttpTransport.Builder().doNotValidateCertificate().build(); - addProxyCredentials(insecureTransport); - return insecureTransport; + HttpClientBuilder httpClientBuilder = + ApacheHttpTransport.newDefaultHttpClientBuilder() + .setSSLSocketFactory(null) // creates new factory with the SSLContext given below + .setSSLContext(SslUtils.trustAllSSLContext()) + .setSSLHostnameVerifier(new NoopHostnameVerifier()); + // Do not use NetHttpTransport. See comments in getConnectionFactory for details. + return new ApacheHttpTransport(httpClientBuilder.build()); } catch (GeneralSecurityException ex) { throw new RuntimeException("platform does not support TLS protocol", ex); } } - /** - * Registers proxy credentials onto transport client, in order to deal with proxies that require - * basic authentication. - * - * @param transport Apache HTTP transport - */ - @VisibleForTesting - static void addProxyCredentials(ApacheHttpTransport transport) { - addProxyCredentials(transport, "https"); - addProxyCredentials(transport, "http"); - } - - private static void addProxyCredentials(ApacheHttpTransport transport, String protocol) { - Preconditions.checkArgument(protocol.equals("http") || protocol.equals("https")); - - String proxyHost = System.getProperty(protocol + ".proxyHost"); - String proxyUser = System.getProperty(protocol + ".proxyUser"); - String proxyPassword = System.getProperty(protocol + ".proxyPassword"); - if (proxyHost == null || proxyUser == null || proxyPassword == null) { - return; - } - - String defaultProxyPort = protocol.equals("http") ? "80" : "443"; - int proxyPort = Integer.parseInt(System.getProperty(protocol + ".proxyPort", defaultProxyPort)); - - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - httpClient - .getCredentialsProvider() - .setCredentials( - new AuthScope(proxyHost, proxyPort), - new UsernamePasswordCredentials(proxyUser, proxyPassword)); - } - private final boolean enableHttpAndInsecureFailover; private final boolean sendAuthorizationOverHttp; private final Consumer logger; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index eb5d0fd98a..debfbd4737 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -164,10 +164,11 @@ private T call(URL url) throws IOException, RegistryException { throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } - } else if (responseException.getStatusCode() - == HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT - || responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY - || responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { + // 301 (Moved Permanently), 302 (Found), 303 (See Other), and 307 (Temporary Redirect) are + // automatically followed by Google HTTP Client (setFollowRedirects(true)), but 308 isn't. + // https://github.com/googleapis/google-http-java-client/issues/873 + // TODO: remove this when the bug is fixed. + } else if (responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { // 'Location' header can be relative or absolute. URL redirectLocation = new URL(url, responseException.getHeaders().getLocation()); return call(redirectLocation); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java deleted file mode 100644 index 615f3f0cfb..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientProxyCredentialsTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.http; - -import com.google.api.client.http.apache.ApacheHttpTransport; -import com.google.common.collect.ImmutableList; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.Credentials; -import org.apache.http.impl.client.DefaultHttpClient; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.RestoreSystemProperties; - -/** Tests for {@link FailoverHttpClient} with setting proxy credentials. */ -public class FailoverHttpClientProxyCredentialsTest { - - private static final ImmutableList proxyProperties = - ImmutableList.of( - "http.proxyHost", - "http.proxyPort", - "http.proxyUser", - "http.proxyPassword", - "https.proxyHost", - "https.proxyPort", - "https.proxyUser", - "https.proxyPassword"); - - @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); - - private final ApacheHttpTransport transport = new ApacheHttpTransport(); - - @Before - public void setUp() { - proxyProperties.stream().forEach(key -> System.clearProperty(key)); - } - - @Test - public void testAddProxyCredentials_undefined() { - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPort", "1080"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPort", "1443"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 1080)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 1443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_defaultPorts() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials httpCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("http://localhost", 80)); - Assert.assertEquals("user", httpCredentials.getUserPrincipal().getName()); - Assert.assertEquals("pass", httpCredentials.getPassword()); - - Credentials httpsCredentials = - httpClient.getCredentialsProvider().getCredentials(new AuthScope("https://host.com", 443)); - Assert.assertEquals("s-user", httpsCredentials.getUserPrincipal().getName()); - Assert.assertEquals("s-pass", httpsCredentials.getPassword()); - } - - @Test - public void testAddProxyCredentials_hostUndefined() { - System.setProperty("http.proxyUser", "user"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyUser", "s-user"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_userUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyPassword", "pass"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyPassword", "s-pass"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } - - @Test - public void testAddProxyCredentials_passwordUndefined() { - System.setProperty("http.proxyHost", "http://localhost"); - System.setProperty("http.proxyUser", "user"); - - System.setProperty("https.proxyHost", "https://host.com"); - System.setProperty("https.proxyUser", "s-user"); - - FailoverHttpClient.addProxyCredentials(transport); - DefaultHttpClient httpClient = (DefaultHttpClient) transport.getHttpClient(); - Credentials credentials = httpClient.getCredentialsProvider().getCredentials(AuthScope.ANY); - Assert.assertNull(credentials); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index 471b74002a..72e8baa979 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import javax.net.ssl.SSLException; import org.hamcrest.CoreMatchers; @@ -149,4 +150,45 @@ public void testProxyCredentialProperties() } } } + + @Test + public void testRedirectionUrls() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + // Sample query strings from + // https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104 + String url1 = "?id=301&_auth_=exp=1572285389~hmac=f0a387f0"; + String url2 = "?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + String url3 = "?id=303&_auth_=exp=1572285389~hmac=f0a387f0"; + String url4 = "?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + + String redirect301 = + "HTTP/1.1 301 Moved Permanently\nLocation: " + url1 + "\nContent-Length: 0\n\n"; + String redirect302 = "HTTP/1.1 302 Found\nLocation: " + url2 + "\nContent-Length: 0\n\n"; + String redirect303 = "HTTP/1.1 303 See Other\nLocation: " + url3 + "\nContent-Length: 0\n\n"; + String redirect307 = + "HTTP/1.1 307 Temporary Redirect\nLocation: " + url4 + "\nContent-Length: 0\n\n"; + String ok200 = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; + List responses = + Arrays.asList(redirect301, redirect302, redirect303, redirect307, ok200); + + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, responses, 1)) { + httpClient.get(new URL(server.getEndpoint()), request); + + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=301&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=302&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=303&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString( + "GET /?id=307&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 2179cd5b2b..3487df0b58 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -242,18 +242,22 @@ public void testCall_unknown() throws IOException, RegistryException { } @Test - public void testCall_temporaryRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_TEMPORARY_REDIRECT); - } + public void testCall_permanentRedirect() throws IOException, RegistryException { + ResponseException redirectException = + mockResponseException( + RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT, + new HttpHeaders().setLocation("https://newlocation")); - @Test - public void testCall_movedPermanently() throws IOException, RegistryException { - verifyRetriesWithNewLocation(HttpStatusCodes.STATUS_CODE_MOVED_PERMANENTLY); - } + // Make httpClient.call() throw first, then succeed. + setUpRegistryResponse(redirectException); + Mockito.when( + mockHttpClient.call( + Mockito.eq("httpMethod"), + Mockito.eq(new URL("https://newlocation")), + Mockito.any())) + .thenReturn(mockResponse); - @Test - public void testCall_permanentRedirect() throws IOException, RegistryException { - verifyRetriesWithNewLocation(RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT); + Assert.assertEquals("body", endpointCaller.call()); } @Test @@ -471,28 +475,6 @@ private void verifyThrowsRegistryErrorException(int httpStatusCode) } } - /** - * Verifies that a response with {@code httpStatusCode} retries the request with the {@code - * Location} header. - */ - private void verifyRetriesWithNewLocation(int httpStatusCode) - throws IOException, RegistryException { - // Mocks a response for temporary redirect to a new location. - ResponseException redirectException = - mockResponseException(httpStatusCode, new HttpHeaders().setLocation("https://newlocation")); - - // Make httpClient.call() throw first, then succeed. - setUpRegistryResponse(redirectException); - Mockito.when( - mockHttpClient.call( - Mockito.eq("httpMethod"), - Mockito.eq(new URL("https://newlocation")), - Mockito.any())) - .thenReturn(mockResponse); - - Assert.assertEquals("body", endpointCaller.call()); - } - private void setUpRegistryResponse(Exception exceptionToThrow) throws MalformedURLException, IOException { Mockito.when( From 3b9daa4c4b4117ad6ea4345c839517beaa6283e9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 5 Nov 2019 15:41:22 -0500 Subject: [PATCH 0813/2020] Add shutdown() to FailoverHttpClient (#2119) --- .../tools/jib/http/FailoverHttpClient.java | 52 +++++++++++++++++-- .../jib/http/FailoverHttpClientTest.java | 14 ++++- .../WithServerFailoverHttpClientTest.java | 15 ++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index 78e073c5ca..3f73847437 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -30,6 +30,8 @@ import java.net.ConnectException; import java.net.URL; import java.security.GeneralSecurityException; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.function.Consumer; import java.util.function.Supplier; import javax.net.ssl.SSLException; @@ -108,6 +110,9 @@ private static HttpTransport getInsecureHttpTransport() { private final Supplier secureHttpTransportFactory; private final Supplier insecureHttpTransportFactory; + private final Deque transportsCreated = new ArrayDeque<>(); + private final Deque responsesCreated = new ArrayDeque<>(); + public FailoverHttpClient( boolean enableHttpAndInsecureFailover, boolean sendAuthorizationOverHttp, @@ -134,6 +139,30 @@ public FailoverHttpClient( this.insecureHttpTransportFactory = insecureHttpTransportFactory; } + /** + * Closes all connections and allocated resources, whether they are currently used or not. + * + *

If an I/O error occurs, shutdown attempts stop immediately, resulting in partial resource + * release up to that point. The method can be called again later to re-attempt releasing all + * resources. + * + * @throws IOException when I/O error shutting down resources + */ + public void shutDown() throws IOException { + synchronized (transportsCreated) { + while (!transportsCreated.isEmpty()) { + transportsCreated.peekFirst().shutdown(); + transportsCreated.removeFirst(); + } + } + synchronized (responsesCreated) { + while (!responsesCreated.isEmpty()) { + responsesCreated.peekFirst().close(); + responsesCreated.removeFirst(); + } + } + } + /** * Sends the request with method GET. * @@ -185,7 +214,7 @@ public Response call(String httpMethod, URL url, Request request) throws IOExcep } try { - return call(httpMethod, url, request, secureHttpTransportFactory.get()); + return call(httpMethod, url, request, getHttpTransport(true)); } catch (SSLException ex) { if (!enableHttpAndInsecureFailover) { @@ -194,11 +223,11 @@ public Response call(String httpMethod, URL url, Request request) throws IOExcep try { logInsecureHttpsFailover(url); - return call(httpMethod, url, request, insecureHttpTransportFactory.get()); + return call(httpMethod, url, request, getHttpTransport(false)); } catch (SSLException ignored) { // This is usually when the server is plain-HTTP. logHttpFailover(url); - return call(httpMethod, toHttp(url), request, secureHttpTransportFactory.get()); + return call(httpMethod, toHttp(url), request, getHttpTransport(true)); } } catch (ConnectException ex) { @@ -214,7 +243,7 @@ public Response call(String httpMethod, URL url, Request request) throws IOExcep // port 443) and we could not connect to 443. It's worth trying port 80. if (enableHttpAndInsecureFailover && isHttpsProtocol(url) && url.getPort() == -1) { logHttpFailover(url); - return call(httpMethod, toHttp(url), request, secureHttpTransportFactory.get()); + return call(httpMethod, toHttp(url), request, getHttpTransport(true)); } throw ex; } @@ -240,12 +269,25 @@ private Response call(String httpMethod, URL url, Request request, HttpTransport } try { - return new Response(httpRequest.execute()); + Response response = new Response(httpRequest.execute()); + synchronized (responsesCreated) { + responsesCreated.addLast(response); + } + return response; } catch (HttpResponseException ex) { throw new ResponseException(ex, clearAuthorization); } } + private HttpTransport getHttpTransport(boolean secureTransport) { + HttpTransport transport = + secureTransport ? secureHttpTransportFactory.get() : insecureHttpTransportFactory.get(); + synchronized (transportsCreated) { + transportsCreated.addLast(transport); + } + return transport; + } + private void logHttpFailover(URL url) { String log = "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP."; logger.accept(LogEvent.info(log)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java index f796dd6633..eb2901ab96 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -228,7 +228,7 @@ public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws .thenThrow(new ConnectException("my exception")); // server not listening on 443 try (Response response = httpClient.get(new URL("https://insecure"), fakeRequest(null))) { - Assert.fail("Should not fall back to HTTP if port was explicitly given and cannot connect"); + Assert.fail("Should not fall back to HTTP if secure client"); } catch (ConnectException ex) { Assert.assertEquals("my exception", ex.getMessage()); @@ -333,6 +333,18 @@ public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IO "Basic ZmFrZS11c2VybmFtZTpmYWtlLXNlY3JldA==", request.getHeaders().getAuthorization()); } + @Test + public void testShutDown() throws IOException { + FailoverHttpClient secureHttpClient = newHttpClient(false, false); + + try (Response response = secureHttpClient.get(fakeUrl.toURL(), fakeRequest(null))) { + secureHttpClient.shutDown(); + secureHttpClient.shutDown(); + Mockito.verify(mockHttpTransport, Mockito.times(1)).shutdown(); + Mockito.verify(mockHttpResponse, Mockito.times(1)).disconnect(); + } + } + private void setUpMocks( HttpTransport mockHttpTransport, HttpRequestFactory mockHttpRequestFactory, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index 72e8baa979..f315e1113e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -151,6 +151,21 @@ public void testProxyCredentialProperties() } } + @Test + public void testClosingResourcesMultipleTimes_noErrors() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); + try (TestWebServer server = new TestWebServer(false, 2); + Response ignored1 = httpClient.get(new URL(server.getEndpoint()), request); + Response ignored2 = httpClient.get(new URL(server.getEndpoint()), request)) { + ignored1.close(); + ignored2.close(); + } finally { + httpClient.shutDown(); + httpClient.shutDown(); // test should complete with no error + } + } + @Test public void testRedirectionUrls() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { From 94af0d6ca3f6d0ca71de98bc24328d806119c82c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 6 Nov 2019 13:08:34 -0500 Subject: [PATCH 0814/2020] No need to pass around ExecutorService (#2126) --- .../jib/builder/steps/LocalBaseImageSteps.java | 13 +++---------- .../cloud/tools/jib/builder/steps/StepsRunner.java | 10 ++-------- .../jib/builder/steps/LocalBaseImageStepsTest.java | 12 ++++-------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java index 8c8798f045..95a2668a21 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -93,7 +93,6 @@ static boolean isGzipped(Path path) throws IOException { } static Callable retrieveDockerDaemonImageStep( - ExecutorService executorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient) { @@ -125,22 +124,16 @@ static Callable retrieveDockerDaemonImageStep( } return cacheDockerImageTar( - buildConfiguration, - executorService, - tarPath, - progressEventDispatcher.newChildProducer()); + buildConfiguration, tarPath, progressEventDispatcher.newChildProducer()); } }; } static Callable retrieveTarImageStep( - ExecutorService executorService, BuildConfiguration buildConfiguration, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Path tarPath) { - return () -> - cacheDockerImageTar( - buildConfiguration, executorService, tarPath, progressEventDispatcherFactory); + return () -> cacheDockerImageTar(buildConfiguration, tarPath, progressEventDispatcherFactory); } @VisibleForTesting @@ -179,11 +172,11 @@ static Optional getCachedDockerImage( @VisibleForTesting static LocalImage cacheDockerImageTar( BuildConfiguration buildConfiguration, - ExecutorService executorService, Path tarPath, ProgressEventDispatcher.Factory progressEventDispatcherFactory) throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, ExecutionException, InterruptedException { + ExecutorService executorService = buildConfiguration.getExecutorService(); try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { Path destination = tempDirectoryProvider.newDirectory(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index ab316a7713..50dcccba82 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -239,10 +239,7 @@ private void saveDocker() { assignLocalImageResult( executorService.submit( LocalBaseImageSteps.retrieveDockerDaemonImageStep( - executorService, - buildConfiguration, - childProgressDispatcherFactory, - dockerClient.get()))); + buildConfiguration, childProgressDispatcherFactory, dockerClient.get()))); } private void extractTar() { @@ -253,10 +250,7 @@ private void extractTar() { assignLocalImageResult( executorService.submit( LocalBaseImageSteps.retrieveTarImageStep( - executorService, - buildConfiguration, - childProgressDispatcherFactory, - tarPath.get()))); + buildConfiguration, childProgressDispatcherFactory, tarPath.get()))); } private void assignLocalImageResult(Future localImageFuture) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index b7c878c7ad..a7f67e1e79 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -64,6 +64,8 @@ private static Path getResource(String resource) throws URISyntaxException { @Before public void setup() throws IOException { + Mockito.when(buildConfiguration.getExecutorService()) + .thenReturn(MoreExecutors.newDirectExecutorService()); Mockito.when(buildConfiguration.getBaseImageLayersCache()) .thenReturn(Cache.withDirectory(temporaryFolder.newFolder().toPath())); Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(eventHandlers); @@ -79,10 +81,7 @@ public void testCacheDockerImageTar_validDocker() throws Exception { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildConfiguration, - MoreExecutors.newDirectExecutorService(), - dockerBuild, - progressEventDispatcherFactory); + buildConfiguration, dockerBuild, progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); @@ -106,10 +105,7 @@ public void testCacheDockerImageTar_validTar() throws Exception { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildConfiguration, - MoreExecutors.newDirectExecutorService(), - tarBuild, - progressEventDispatcherFactory); + buildConfiguration, tarBuild, progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); From 9ea9033a8c82532303746565f5a9c02934252cca Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 6 Nov 2019 15:24:55 -0500 Subject: [PATCH 0815/2020] Add note in style guide about jib-core api (#2127) --- STYLE_GUIDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 099f7aa47c..f5e28467ef 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -23,6 +23,8 @@ Class members should be in the following order, in decreasing priority: User-facing methods (such as those in Jib Core) should not have types in their signature that are not standard JDK classes. For example, a parameter should take type `List` rather than Guava's `ImmutableList`. +Jib Core's formal API should not expose internal Jib types. In other words, public classes in the `com.google.cloud.tools.jib.api` package should not contain any public methods that have internal types (Jib classes outside of the `api` package) in the method signature. This includes return types, parameters, thrown types, and javadoc links on public methods. + ### Package hierarchy Packages should depend on each other without cycles. From c4c0c9f6635c03e3862cf39bc6c169d33263d0ac Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 6 Nov 2019 16:50:56 -0500 Subject: [PATCH 0816/2020] Fix class cast exception (#2128) --- .../cloud/tools/jib/api/RegistryUnauthorizedException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java index 602ec3e9fd..fa0e92daf2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java @@ -44,6 +44,6 @@ public String getImageReference() { } public HttpResponseException getHttpResponseException() { - return (HttpResponseException) getCause(); + return (HttpResponseException) getCause().getCause(); } } From 526ea502839df1fec215f21f626cbd3d6a4e797c Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 7 Nov 2019 15:05:43 -0500 Subject: [PATCH 0817/2020] Add copyright checkstyle check (#2129) * Add copyright checkstyle check This does a few more modifications to the provided google_checks.xml using xml apis from groovy instead of simple string replacement. Required because checkstyle doesn't let us run multiple passes of the checkstyle tool. (presumably you could write a custom set of tasks to do this?) Also changing file permissions at the "Checker" module means no java checks would run anywhere, so we have to move where we limit the checks to just xml, properties to the submodules that use them. --- build.gradle | 35 ++- .../checkstyle/copyright-java.header | 4 +- .../tools/jib/api/ReproducibleImageTest.java | 18 +- .../google/cloud/tools/jib/api/JibEvent.java | 2 +- .../tools/jib/builder/steps/BuildResult.java | 18 +- .../jib/cache/CacheCorruptedException.java | 2 +- .../tools/jib/cache/CacheStorageFiles.java | 2 +- .../tools/jib/cache/CacheStorageReader.java | 2 +- .../tools/jib/cache/CacheStorageWriter.java | 2 +- .../google/cloud/tools/jib/cache/Retry.java | 18 +- .../cloud/tools/jib/event/EventHandlers.java | 2 +- .../tools/jib/registry/ErrorResponseUtil.java | 18 +- .../cloud/tools/jib/api/JibContainerTest.java | 18 +- .../jib/builder/steps/BuildResultTest.java | 18 +- .../jib/cache/CacheStorageFilesTest.java | 2 +- .../jib/cache/CacheStorageReaderTest.java | 2 +- .../jib/cache/CacheStorageWriterTest.java | 2 +- .../cloud/tools/jib/cache/CacheTest.java | 2 +- .../cloud/tools/jib/cache/RetryTest.java | 18 +- .../tools/jib/event/EventHandlersTest.java | 2 +- .../DockerRegistryBearerTokenTest.java | 18 +- .../jib/registry/ErrorResponseUtilTest.java | 18 +- ...stryAuthenticationFailedExceptionTest.java | 18 +- .../tools/jib/gradle/CheckJibVersionTask.java | 18 +- .../cloud/tools/jib/gradle/JibTask.java | 2 +- jib-maven-plugin/config/checkstyle-header.xml | 10 - .../config/google-checks-no-indent.xml | 227 ------------------ .../maven/skaffold/CheckJibVersionMojo.java | 18 +- .../maven/skaffold/SkaffoldBindingMojo.java | 18 +- .../cloud/tools/jib/maven/TestRepository.java | 16 ++ .../skaffold/CheckJibVersionMojoTest.java | 18 +- .../common/SkaffoldSyncMapTemplate.java | 16 ++ .../jib/plugins/common/VersionChecker.java | 18 +- .../api/JibContainerBuilderTestHelper.java | 2 +- .../plugins/common/VersionCheckerTest.java | 18 +- 35 files changed, 220 insertions(+), 402 deletions(-) rename jib-maven-plugin/config/copyright.header => config/checkstyle/copyright-java.header (90%) delete mode 100644 jib-maven-plugin/config/checkstyle-header.xml delete mode 100644 jib-maven-plugin/config/google-checks-no-indent.xml diff --git a/build.gradle b/build.gradle index 5340839127..75027408d3 100644 --- a/build.gradle +++ b/build.gradle @@ -119,14 +119,37 @@ subprojects { checkstyle { toolVersion = '8.18' - // get the google_checks.xml file from the checkstyle jar and take out the java checks + // use the google checks packaged with the jar def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - def fileExtensionsBefore = '' - def fileExtensionsAfter = '' - def googleChecksNoJava = googleChecks.replace(fileExtensionsBefore, fileExtensionsAfter) - assert !googleChecks.equals(googleChecksNoJava) - config = resources.text.fromString(googleChecksNoJava) + // preserve these, the parser erases them + def xmlVersionNode = googleChecks.find(/<\?xml version.*>/) + def doctypeNode = googleChecks.find(/(?s)]*>/) + + def parser = new XmlParser() + parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) + parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) + def rootModule = parser.parseText(googleChecks) + + // force each module within the root checker to only work on "properties, xml", ignore "java" + def fileExtensionNode = parser.parseText('') + rootModule.module*.append(fileExtensionNode) + + // add in copyright header check on only java files + def copyrightNode = parser.parseText(''' + + + + + + ''') + rootModule.append(copyrightNode) + + def xmlOutput = new StringWriter() + new XmlNodePrinter(new PrintWriter(xmlOutput)).print(rootModule) + + // this is the actual checkstyle config + config = resources.text.fromString(xmlVersionNode + doctypeNode + xmlOutput.toString()) maxErrors = 0 maxWarnings = 0 diff --git a/jib-maven-plugin/config/copyright.header b/config/checkstyle/copyright-java.header similarity index 90% rename from jib-maven-plugin/config/copyright.header rename to config/checkstyle/copyright-java.header index c864889627..956f87d465 100644 --- a/jib-maven-plugin/config/copyright.header +++ b/config/checkstyle/copyright-java.header @@ -1,9 +1,9 @@ ^/\*$ - * Copyright 2018 Google LLC. +^ \* Copyright 20(17|18|19) Google LLC\.$ ^ \*$ ^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$ ^ \* use this file except in compliance with the License\. You may obtain a copy of$ -^ \* the License at$ +^ \* the License at ^ \*$ ^ \* http://www\.apache\.org/licenses/LICENSE-2\.0$ ^ \*$ diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java index 31345a37bf..b73f35aee0 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.api; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java index 9adc4192c8..ea9a035546 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java index 29f2669fa0..ffc3f32232 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.builder.steps; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java index b178523454..78e0b5e6dd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java index e6aa3d3661..4d1d16cd60 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index b921b9fac0..01ecde9fce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index ba924b5ae5..fc62a641a1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java index 211a5df445..68697fd7c1 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.cache; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java index e9c736990d..d594410d70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java index afe63988a0..abd3479ca9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.registry; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java index 5c02e2e852..52393ab304 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.api; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java index 46aa04b3f9..5397c93787 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.builder.steps; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java index fe217f46d7..bb9fa9c46f 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java index ef2c2a0047..9c313a84e1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java index c5bdfb5f63..40453dd1e4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java index 5c1b22b12b..44130322d9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java index 3659d0ceb0..1d138226ab 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.cache; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java index 686e8e26d1..efcc910b4b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java index 451d8ba1da..097267f21c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.registry; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java index f440e61a83..d5dd5ae007 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.registry; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java index 49e98abfe4..cba6cd769a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.registry; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java index a62bee3320..66efa60080 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.gradle; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java index bb398da9d6..e742988c7f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-maven-plugin/config/checkstyle-header.xml b/jib-maven-plugin/config/checkstyle-header.xml deleted file mode 100644 index 3fb2952123..0000000000 --- a/jib-maven-plugin/config/checkstyle-header.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/jib-maven-plugin/config/google-checks-no-indent.xml b/jib-maven-plugin/config/google-checks-no-indent.xml deleted file mode 100644 index f6faf624c1..0000000000 --- a/jib-maven-plugin/config/google-checks-no-indent.xml +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java index ef1306eeea..87c008182a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojo.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.maven.skaffold; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java index 8d24e2dfa3..380d6aab2f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldBindingMojo.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.maven.skaffold; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java index 2d4c175697..3aec2f06ba 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestRepository.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + package com.google.cloud.tools.jib.maven; import com.google.common.io.Resources; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java index 9181ee1f1c..579d9b919a 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/CheckJibVersionMojoTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.maven.skaffold; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java index 7a36b68731..6b2b669997 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + package com.google.cloud.tools.jib.plugins.common; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java index c7ec80f4f4..9515bafcbf 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.plugins.common; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java index 9db8fdf1cb..510612e310 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC. All rights reserved. + * Copyright 2018 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java index c2552ac276..19cde16bc1 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java @@ -1,17 +1,17 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package com.google.cloud.tools.jib.plugins.common; From 76841cdc32c4b68645134f0ba9c72a59bd53c15d Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 7 Nov 2019 15:58:56 -0500 Subject: [PATCH 0818/2020] Remove env check so intellij sync works (#2131) This really only provided fail fast in the first place, I don't think the user experience is too degraded without it. --- build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 75027408d3..14cd8f0772 100644 --- a/build.gradle +++ b/build.gradle @@ -212,6 +212,7 @@ subprojects { classpath = sourceSets.integrationTest.runtimeClasspath systemProperty '_JIB_DISABLE_USER_AGENT', true } + integrationTest.dependsOn test task integrationTestJar(type: Jar) { @@ -390,13 +391,6 @@ tasks.register("dev") { } tasks.register("devFull") { - if (!System.getenv('JIB_INTEGRATION_TESTING_PROJECT') - && !System.getenv('JIB_INTEGRATION_TESTING_LOCATION')) { - throw new GradleException( - "Must set environment variable JIB_INTEGRATION_TESTING_PROJECT to the " - + "GCP project to use for integration testing or " - + "JIB_INTEGRATION_TESTING_LOCATION to a suitable registry/repository location."); - } dependsOn tasks.dev subprojects.each { subproject -> dependsOn subproject.tasks.integrationTest From 2dd84e6431512cd7181643d6718c94789909f176 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 7 Nov 2019 18:53:34 -0500 Subject: [PATCH 0819/2020] Instantiate a singleton stateful HttpClient instance (#2125) * Instantiate a singleton FailoverHttpClient * Put HttpClient into BuildConfiguration * Update Javadoc --- .../jib/api/ContainerizerIntegrationTest.java | 10 -- .../tools/jib/api/JibIntegrationTest.java | 12 +- ...icationMethodRetrieverIntegrationTest.java | 9 +- .../registry/BlobCheckerIntegrationTest.java | 9 +- .../registry/BlobPullerIntegrationTest.java | 9 +- .../registry/BlobPusherIntegrationTest.java | 6 +- .../ManifestPullerIntegrationTest.java | 15 ++- .../ManifestPusherIntegrationTest.java | 12 +- .../RegistryAuthenticatorIntegrationTest.java | 6 +- .../tools/jib/api/JibContainerBuilder.java | 58 +++------- .../jib/configuration/BuildConfiguration.java | 66 ++++++----- .../tools/jib/registry/RegistryClient.java | 53 +++++---- .../tools/jib/api/ContainerizerTest.java | 12 +- .../jib/api/JavaContainerBuilderTest.java | 17 +-- .../jib/api/JibContainerBuilderTest.java | 106 ++---------------- .../configuration/BuildConfigurationTest.java | 53 +++++++-- .../jib/registry/RegistryClientTest.java | 12 +- .../gradle/GradleProjectPropertiesTest.java | 5 +- .../jib/maven/MavenProjectPropertiesTest.java | 5 +- .../api/JibContainerBuilderTestHelper.java | 5 +- 20 files changed, 194 insertions(+), 286 deletions(-) diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java index 09f5a5a6fe..1145d56bf8 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java @@ -33,11 +33,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Stream; import org.hamcrest.CoreMatchers; -import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -74,7 +71,6 @@ private void checkCompletion() { @ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000); - private static final ExecutorService executorService = Executors.newCachedThreadPool(); private static final Logger logger = LoggerFactory.getLogger(ContainerizerIntegrationTest.class); private static final String DISTROLESS_DIGEST = "sha256:f488c213f278bc5f9ffe3ddf30c5dbb2303a15a74146b738d12453088e662880"; @@ -91,11 +87,6 @@ public static void setUp() throws URISyntaxException, IOException { makeLayerConfiguration("core/application/classes", "/app/classes/")); } - @AfterClass - public static void cleanUp() { - executorService.shutdown(); - } - /** * Lists the files in the {@code resourcePath} resources directory and builds a {@link * LayerConfiguration} from those files. @@ -346,7 +337,6 @@ private JibContainer buildImage( .setApplicationLayersCache(cacheDirectory) .setAllowInsecureRegistries(true) .setToolName("jib-integration-test") - .setExecutorService(executorService) .addEventHandler(ProgressEvent.class, progressChecker.progressEventHandler); additionalTags.forEach(containerizer::withAdditionalTag); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index f9ff478c16..cebbf42d80 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -337,11 +337,13 @@ public void testProvidedExecutorNotDisposed() .setAllowInsecureRegistries(true); ExecutorService executorService = Executors.newCachedThreadPool(); - containerizer.setExecutorService(executorService); - Jib.from("busybox").setEntrypoint("echo", "Hello World").containerize(containerizer); - Assert.assertFalse(executorService.isShutdown()); - - executorService.shutdown(); + try { + containerizer.setExecutorService(executorService); + Jib.from("busybox").setEntrypoint("echo", "Hello World").containerize(containerizer); + Assert.assertFalse(executorService.isShutdown()); + } finally { + executorService.shutdown(); + } } @Test diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java index 936efa0a0f..9904b30d21 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; import java.util.Optional; import org.junit.Assert; @@ -27,10 +28,13 @@ /** Integration tests for {@link AuthenticationMethodRetriever}. */ public class AuthenticationMethodRetrieverIntegrationTest { + private final FailoverHttpClient httpClient = new FailoverHttpClient(false, false, ignored -> {}); + @Test public void testGetRegistryAuthenticator() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory( + EventHandlers.NONE, "registry.hub.docker.com", "library/busybox", httpClient) .newRegistryClient(); Optional registryAuthenticator = registryClient.getRegistryAuthenticator(); @@ -38,7 +42,8 @@ public void testGetRegistryAuthenticator() throws IOException, RegistryException Authorization authorization = registryAuthenticator.get().authenticatePull(null); RegistryClient authorizedRegistryClient = - RegistryClient.factory(EventHandlers.NONE, "registry.hub.docker.com", "library/busybox") + RegistryClient.factory( + EventHandlers.NONE, "registry.hub.docker.com", "library/busybox", httpClient) .setAuthorization(authorization) .newRegistryClient(); authorizedRegistryClient.pullManifest("latest"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java index 721c00c05b..560d3d741b 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import java.io.IOException; import java.security.DigestException; @@ -37,11 +38,12 @@ public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); } + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); V22ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V22ManifestTemplate.class).getManifest(); @@ -53,8 +55,7 @@ public void testCheck_exists() throws IOException, RegistryException { @Test public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); DescriptorDigest fakeBlobDigest = DescriptorDigest.fromHash( diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java index 1f1f909144..71ab91e9cb 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.common.io.ByteStreams; import java.io.IOException; @@ -45,11 +46,12 @@ public static void setUp() throws IOException, InterruptedException { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testPull() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); V21ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); @@ -79,8 +81,7 @@ public void testPull_unknownBlob() throws IOException, DigestException { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); try { diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java index 6700045517..6c35397189 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; @@ -32,6 +33,8 @@ public class BlobPusherIntegrationTest { @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testPush() throws DigestException, IOException, RegistryException, InterruptedException { @@ -43,8 +46,7 @@ public void testPush() "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage", httpClient) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 067a00bcdf..e0afa8e846 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; @@ -44,11 +45,12 @@ public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); } + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testPull_v21() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); V21ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); @@ -60,7 +62,8 @@ public void testPull_v21() throws IOException, RegistryException { @Test public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java", httpClient) + .newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); Assert.assertEquals(2, manifestTemplate.getSchemaVersion()); @@ -71,7 +74,8 @@ public void testPull_v22() throws IOException, RegistryException { @Test public void testPull_v22ManifestList() throws IOException, RegistryException { RegistryClient.Factory factory = - RegistryClient.factory(EventHandlers.NONE, "registry-1.docker.io", "library/openjdk"); + RegistryClient.factory( + EventHandlers.NONE, "registry-1.docker.io", "library/openjdk", httpClient); Authorization authorization = factory.newRegistryClient().getRegistryAuthenticator().get().authenticatePull(null); RegistryClient registryClient = factory.setAuthorization(authorization).newRegistryClient(); @@ -107,8 +111,7 @@ public void testPull_v22ManifestList() throws IOException, RegistryException { public void testPull_unknownManifest() throws RegistryException, IOException { try { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); Assert.fail("Trying to pull nonexistent image should have errored"); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java index d7c0410b2c..fb229c50d3 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -43,15 +44,17 @@ public static void setUp() throws IOException, InterruptedException { localRegistry.pullAndPushToLocal("busybox", "busybox"); } + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testPush_missingBlobs() throws IOException, RegistryException { RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java").newRegistryClient(); + RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/java", httpClient) + .newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "busybox", httpClient) .newRegistryClient(); try { registryClient.pushManifest((V22ManifestTemplate) manifestTemplate, "latest"); @@ -84,8 +87,7 @@ public void testPush() throws DigestException, IOException, RegistryException { // Pushes the BLOBs. RegistryClient registryClient = - RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage") - .setAllowInsecureRegistries(true) + RegistryClient.factory(EventHandlers.NONE, "localhost:5000", "testimage", httpClient) .newRegistryClient(); Assert.assertFalse( registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java index ed4165bf6a..8307a660a6 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; import java.util.Optional; import org.junit.Assert; @@ -29,6 +30,8 @@ /** Integration tests for {@link RegistryAuthenticator}. */ public class RegistryAuthenticatorIntegrationTest { + private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); + @Test public void testAuthenticate() throws IOException, RegistryException, InvalidImageReferenceException { @@ -37,7 +40,8 @@ public void testAuthenticate() RegistryClient.factory( EventHandlers.NONE, dockerHubImageReference.getRegistry(), - dockerHubImageReference.getRepository()) + dockerHubImageReference.getRepository(), + httpClient) .newRegistryClient() .getRegistryAuthenticator(); Assert.assertTrue(registryAuthenticator.isPresent()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index 1390e0a045..a5454e5d52 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -35,9 +35,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Supplier; import javax.annotation.Nullable; import org.apache.http.conn.HttpHostConnectException; @@ -474,35 +471,12 @@ public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workin public JibContainer containerize(Containerizer containerizer) throws InterruptedException, RegistryException, IOException, CacheDirectoryCreationException, ExecutionException { - return containerize(containerizer, Executors::newCachedThreadPool); - } - - /** - * Describes the container contents and configuration without actually physically building a - * container. - * - * @return a description of the container being built - */ - public JibContainerDescription describeContainer() { - return new JibContainerDescription(layerConfigurations); - } - - @VisibleForTesting - JibContainer containerize( - Containerizer containerizer, Supplier defaultExecutorServiceFactory) - throws IOException, CacheDirectoryCreationException, InterruptedException, RegistryException, - ExecutionException { - boolean shutdownExecutorService = !containerizer.getExecutorService().isPresent(); - ExecutorService executorService = - containerizer.getExecutorService().orElseGet(defaultExecutorServiceFactory); + try (BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer); + TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildConfiguration.getEventHandlers(), containerizer.getDescription())) { + logSources(buildConfiguration.getEventHandlers()); - BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer, executorService); - - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); - logSources(eventHandlers); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher(eventHandlers, containerizer.getDescription())) { BuildResult result = containerizer.run(buildConfiguration); return new JibContainer(result.getImageDigest(), result.getImageId()); @@ -512,27 +486,29 @@ JibContainer containerize( throw (RegistryException) ex.getCause(); } throw ex; - - } finally { - if (shutdownExecutorService) { - executorService.shutdown(); - } } } + /** + * Describes the container contents and configuration without actually physically building a + * container. + * + * @return a description of the container being built + */ + public JibContainerDescription describeContainer() { + return new JibContainerDescription(layerConfigurations); + } + /** * Builds a {@link BuildConfiguration} using this and a {@link Containerizer}. * * @param containerizer the {@link Containerizer} - * @param executorService the {@link ExecutorService} to use, overriding the executor in the - * {@link Containerizer} * @return the {@link BuildConfiguration} * @throws CacheDirectoryCreationException if a cache directory could not be created * @throws IOException if an I/O exception occurs */ @VisibleForTesting - BuildConfiguration toBuildConfiguration( - Containerizer containerizer, ExecutorService executorService) + BuildConfiguration toBuildConfiguration(Containerizer containerizer) throws CacheDirectoryCreationException, IOException { return buildConfigurationBuilder .setTargetImageConfiguration(containerizer.getImageConfiguration()) @@ -544,7 +520,7 @@ BuildConfiguration toBuildConfiguration( .setAllowInsecureRegistries(containerizer.getAllowInsecureRegistries()) .setOffline(containerizer.isOfflineMode()) .setToolName(containerizer.getToolName()) - .setExecutorService(executorService) + .setExecutorService(containerizer.getExecutorService().orElse(null)) .setEventHandlers(containerizer.buildEventHandlers()) .setAlwaysCacheBaseImage(containerizer.getAlwaysCacheBaseImage()) .build(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 6e1f011295..492c90763e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -21,6 +21,8 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -29,6 +31,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; @@ -39,8 +42,14 @@ import java.util.concurrent.Executors; import javax.annotation.Nullable; -/** Immutable configuration options for the builder process. */ -public class BuildConfiguration { +/** + * Build context for the builder process. Includes static build configuration options as well as + * various services for execution (such as event dispatching, thread execution service, and HTTP + * client). Informational instances (particularly configuration options such as {@link + * ContainerConfiguration}, {@link ImageConfiguration}, and {@link LayerConfiguration}) held in are + * immutable. + */ +public class BuildConfiguration implements Closeable { /** The default target format of the container manifest. */ private static final Class DEFAULT_TARGET_FORMAT = @@ -226,7 +235,7 @@ public Builder setEventHandlers(EventHandlers eventHandlers) { * @param executorService the {@link ExecutorService} * @return this */ - public Builder setExecutorService(ExecutorService executorService) { + public Builder setExecutorService(@Nullable ExecutorService executorService) { this.executorService = executorService; return this; } @@ -252,9 +261,6 @@ public BuildConfiguration build() throws IOException { if (applicationLayersCacheDirectory == null) { missingFields.add("application layers cache directory"); } - if (executorService == null) { - missingFields.add("executor service"); - } switch (missingFields.size()) { case 0: // No errors @@ -276,12 +282,17 @@ public BuildConfiguration build() throws IOException { Cache.withDirectory(Preconditions.checkNotNull(baseImageLayersCacheDirectory)), Cache.withDirectory(Preconditions.checkNotNull(applicationLayersCacheDirectory)), targetFormat, - allowInsecureRegistries, offline, layerConfigurations, toolName, eventHandlers, - Preconditions.checkNotNull(executorService), + // TODO: try setting global User-Agent: here + new FailoverHttpClient( + allowInsecureRegistries, + JibSystemProperties.sendCredentialsOverHttp(), + eventHandlers::dispatch), + executorService == null ? Executors.newCachedThreadPool() : executorService, + executorService == null, // shutDownExecutorService alwaysCacheBaseImage); case 1: @@ -330,12 +341,13 @@ public static Builder builder() { private final Cache baseImageLayersCache; private final Cache applicationLayersCache; private Class targetFormat; - private final boolean allowInsecureRegistries; private final boolean offline; private final ImmutableList layerConfigurations; private final String toolName; private final EventHandlers eventHandlers; + private final FailoverHttpClient httpClient; private final ExecutorService executorService; + private final boolean shutDownExecutorService; private final boolean alwaysCacheBaseImage; /** Instantiate with {@link #builder}. */ @@ -347,12 +359,13 @@ private BuildConfiguration( Cache baseImageLayersCache, Cache applicationLayersCache, Class targetFormat, - boolean allowInsecureRegistries, boolean offline, ImmutableList layerConfigurations, String toolName, EventHandlers eventHandlers, + FailoverHttpClient httpClient, ExecutorService executorService, + boolean shutDownExecutorService, boolean alwaysCacheBaseImage) { this.baseImageConfiguration = baseImageConfiguration; this.targetImageConfiguration = targetImageConfiguration; @@ -361,12 +374,13 @@ private BuildConfiguration( this.baseImageLayersCache = baseImageLayersCache; this.applicationLayersCache = applicationLayersCache; this.targetFormat = targetFormat; - this.allowInsecureRegistries = allowInsecureRegistries; this.offline = offline; this.layerConfigurations = layerConfigurations; this.toolName = toolName; this.eventHandlers = eventHandlers; + this.httpClient = httpClient; this.executorService = executorService; + this.shutDownExecutorService = shutDownExecutorService; this.alwaysCacheBaseImage = alwaysCacheBaseImage; } @@ -403,6 +417,10 @@ public EventHandlers getEventHandlers() { return eventHandlers; } + public FailoverHttpClient getHttpClient() { + return httpClient; + } + public ExecutorService getExecutorService() { return executorService; } @@ -424,16 +442,6 @@ public Cache getApplicationLayersCache() { return applicationLayersCache; } - /** - * Gets whether or not to allow insecure registries (ignoring certificate validation failure or - * communicating over HTTP if all else fail). - * - * @return {@code true} if insecure connections will be allowed; {@code false} otherwise - */ - public boolean getAllowInsecureRegistries() { - return allowInsecureRegistries; - } - /** * Gets whether or not to run the build in offline mode. * @@ -486,8 +494,8 @@ public RegistryClient.Factory newTargetImageRegistryClientFactory() { getEventHandlers(), targetImageConfiguration.getImageRegistry(), targetImageConfiguration.getImageRepository(), - baseImageConfiguration.getImageRepository()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()) + baseImageConfiguration.getImageRepository(), + httpClient) .setUserAgentSuffix(getToolName()); } return newRegistryClientFactory(targetImageConfiguration); @@ -497,8 +505,16 @@ private RegistryClient.Factory newRegistryClientFactory(ImageConfiguration image return RegistryClient.factory( getEventHandlers(), imageConfiguration.getImageRegistry(), - imageConfiguration.getImageRepository()) - .setAllowInsecureRegistries(getAllowInsecureRegistries()) + imageConfiguration.getImageRepository(), + httpClient) .setUserAgentSuffix(getToolName()); } + + @Override + public void close() throws IOException { + if (shutDownExecutorService) { + executorService.shutdown(); + } + httpClient.shutDown(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index cdcda32702..7c0795c64a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -54,28 +54,18 @@ public static class Factory { private final EventHandlers eventHandlers; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; + private final FailoverHttpClient httpClient; - private boolean allowInsecureRegistries = false; @Nullable private String userAgentSuffix; @Nullable private Authorization authorization; private Factory( EventHandlers eventHandlers, - RegistryEndpointRequestProperties registryEndpointRequestProperties) { + RegistryEndpointRequestProperties registryEndpointRequestProperties, + FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.registryEndpointRequestProperties = registryEndpointRequestProperties; - } - - /** - * Sets whether or not to allow insecure registries (ignoring certificate validation failure or - * communicating over HTTP if all else fail). - * - * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed - * @return this - */ - public Factory setAllowInsecureRegistries(boolean allowInsecureRegistries) { - this.allowInsecureRegistries = allowInsecureRegistries; - return this; + this.httpClient = httpClient; } /** @@ -110,8 +100,8 @@ public RegistryClient newRegistryClient() { eventHandlers, authorization, registryEndpointRequestProperties, - allowInsecureRegistries, - makeUserAgent()); + makeUserAgent(), + httpClient); } /** @@ -146,17 +136,28 @@ private String makeUserAgent() { * @param eventHandlers the event handlers used for dispatching log events * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) + * @param httpClient HTTP client * @return the new {@link Factory} */ - public static Factory factory(EventHandlers eventHandlers, String serverUrl, String imageName) { - return new Factory(eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName)); + public static Factory factory( + EventHandlers eventHandlers, + String serverUrl, + String imageName, + FailoverHttpClient httpClient) { + return new Factory( + eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName), httpClient); } public static Factory factory( - EventHandlers eventHandlers, String serverUrl, String imageName, String sourceImageName) { + EventHandlers eventHandlers, + String serverUrl, + String imageName, + String sourceImageName, + FailoverHttpClient httpClient) { return new Factory( eventHandlers, - new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName)); + new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName), + httpClient); } /** @@ -252,24 +253,20 @@ static Multimap decodeTokenRepositoryGrants(String token) { * @param eventHandlers the event handlers used for dispatching log events * @param authorization the {@link Authorization} to access the registry/repository * @param registryEndpointRequestProperties properties of registry endpoint requests - * @param allowInsecureRegistries if {@code true}, insecure connections will be allowed * @param userAgent {@code User-Agent} header to send with the request + * @param httpClient HTTP client */ private RegistryClient( EventHandlers eventHandlers, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, - boolean allowInsecureRegistries, - String userAgent) { + String userAgent, + FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; - this.httpClient = - new FailoverHttpClient( - allowInsecureRegistries, - JibSystemProperties.sendCredentialsOverHttp(), - eventHandlers::dispatch); + this.httpClient = httpClient; } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java index f439570efb..9a7473117e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java @@ -18,22 +18,17 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; import java.nio.file.Paths; import java.util.Arrays; import java.util.concurrent.ExecutorService; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link Containerizer}. */ -@RunWith(MockitoJUnitRunner.class) public class ContainerizerTest { - @Mock private ExecutorService mockExecutorService; - @Test public void testTo() throws CacheDirectoryCreationException { RegistryImage registryImage = RegistryImage.named(ImageReference.of(null, "repository", null)); @@ -59,10 +54,11 @@ private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreation Assert.assertFalse(containerizer.getAllowInsecureRegistries()); Assert.assertEquals("jib-core", containerizer.getToolName()); + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); containerizer .withAdditionalTag("tag1") .withAdditionalTag("tag2") - .setExecutorService(mockExecutorService) + .setExecutorService(executorService) .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) .setAllowInsecureRegistries(true) @@ -70,7 +66,7 @@ private void verifyTo(Containerizer containerizer) throws CacheDirectoryCreation Assert.assertEquals(ImmutableSet.of("tag1", "tag2"), containerizer.getAdditionalTags()); Assert.assertTrue(containerizer.getExecutorService().isPresent()); - Assert.assertEquals(mockExecutorService, containerizer.getExecutorService().get()); + Assert.assertSame(executorService, containerizer.getExecutorService().get()); Assert.assertEquals( Paths.get("base/image/layers"), containerizer.getBaseImageLayersCacheDirectory()); Assert.assertEquals( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 0221580c87..93ddcb8171 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; -import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; @@ -81,9 +80,7 @@ public void testToJibContainerBuilder_all() .addJvmFlags("-xflag1", "-xflag2") .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("hello")), - MoreExecutors.newDirectExecutorService()); + .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); @@ -159,9 +156,7 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() .addClasses(getResource("core/class-finder-tests/extension")) .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("hello")), - MoreExecutors.newDirectExecutorService()); + .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); @@ -215,9 +210,7 @@ public void testToJibContainerBuilder_setAppRootLate() .setAppRoot("/different") .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("hello")), - MoreExecutors.newDirectExecutorService()); + .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); @@ -267,9 +260,7 @@ public void testToJibContainerBuilder_mainClassNull() JavaContainerBuilder.fromDistroless() .addClasses(getResource("core/application/classes/")) .toContainerBuilder() - .toBuildConfiguration( - Containerizer.to(RegistryImage.named("hello")), - MoreExecutors.newDirectExecutorService()); + .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 868149ee7a..aa68f2afa5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -16,11 +16,9 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; @@ -29,12 +27,9 @@ import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.nio.file.Paths; -import java.security.DigestException; import java.time.Instant; import java.util.Arrays; import java.util.Collections; -import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import org.junit.Assert; @@ -53,7 +48,6 @@ public class JibContainerBuilderTest { @Mock private LayerConfiguration mockLayerConfiguration1; @Mock private LayerConfiguration mockLayerConfiguration2; @Mock private CredentialRetriever mockCredentialRetriever; - @Mock private ExecutorService mockExecutorService; @Mock private Consumer mockJibEventConsumer; @Mock private JibEvent mockJibEvent; @@ -75,8 +69,7 @@ public void testToBuildConfiguration_containerConfigurationSet() BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration( - Containerizer.to(RegistryImage.named("target/image")), - MoreExecutors.newDirectExecutorService()); + Containerizer.to(RegistryImage.named("target/image"))); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -110,8 +103,7 @@ public void testToBuildConfiguration_containerConfigurationAdd() BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration( - Containerizer.to(RegistryImage.named("target/image")), - MoreExecutors.newDirectExecutorService()); + Containerizer.to(RegistryImage.named("target/image"))); ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( @@ -131,6 +123,7 @@ public void testToBuildConfiguration_containerConfigurationAdd() public void testToBuildConfiguration() throws InvalidImageReferenceException, CredentialRetrievalException, IOException, CacheDirectoryCreationException { + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); RegistryImage targetImage = RegistryImage.named(ImageReference.of("gcr.io", "my-project/my-app", null)) .addCredential("username", "password"); @@ -138,7 +131,7 @@ public void testToBuildConfiguration() Containerizer.to(targetImage) .setBaseImageLayersCache(Paths.get("base/image/layers")) .setApplicationLayersCache(Paths.get("application/layers")) - .setExecutorService(mockExecutorService) + .setExecutorService(executorService) .addEventHandler(mockJibEventConsumer) .setAlwaysCacheBaseImage(false); @@ -149,9 +142,7 @@ public void testToBuildConfiguration() JibContainerBuilder jibContainerBuilder = new JibContainerBuilder(baseImageConfiguration, spyBuildConfigurationBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); - BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration( - containerizer, containerizer.getExecutorService().get()); + BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(containerizer); Assert.assertEquals( spyBuildConfigurationBuilder.build().getContainerConfiguration(), @@ -188,7 +179,7 @@ public void testToBuildConfiguration() Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2), buildConfiguration.getLayerConfigurations()); - Assert.assertEquals(mockExecutorService, buildConfiguration.getExecutorService()); + Assert.assertSame(executorService, buildConfiguration.getExecutorService()); buildConfiguration.getEventHandlers().dispatch(mockJibEvent); Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); @@ -207,94 +198,11 @@ public void testToBuildConfiguration() containerizer .withAdditionalTag("tag1") .withAdditionalTag("tag2") - .setToolName("toolName"), - MoreExecutors.newDirectExecutorService()); + .setToolName("toolName")); Assert.assertSame(OCIManifestTemplate.class, buildConfiguration.getTargetFormat()); Assert.assertEquals( ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); Assert.assertEquals("toolName", buildConfiguration.getToolName()); Assert.assertFalse(buildConfiguration.getAlwaysCacheBaseImage()); } - - /** Verify that an internally-created ExecutorService is shutdown. */ - @Test - public void testContainerize_executorCreated() throws Exception { - ImageConfiguration imageConfiguration = - ImageConfiguration.builder(ImageReference.parse("base/image")).build(); - JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) - .setEntrypoint(Arrays.asList("entry", "point")) - .setEnvironment(ImmutableMap.of("name", "value")) - .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) - .setLabels(ImmutableMap.of("key", "value")) - .setProgramArguments(Arrays.asList("program", "arguments")) - .setCreationTime(Instant.ofEpochMilli(1000)) - .setUser("user") - .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); - - Containerizer mockContainerizer = createMockContainerizer(); - - jibContainerBuilder.containerize(mockContainerizer, () -> mockExecutorService); - - Mockito.verify(mockExecutorService).shutdown(); - } - - /** Verify that a provided ExecutorService is not shutdown. */ - @Test - public void testContainerize_configuredExecutor() throws Exception { - ImageConfiguration imageConfiguration = - ImageConfiguration.builder(ImageReference.parse("base/image")).build(); - JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) - .setEntrypoint(Arrays.asList("entry", "point")) - .setEnvironment(ImmutableMap.of("name", "value")) - .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) - .setLabels(ImmutableMap.of("key", "value")) - .setProgramArguments(Arrays.asList("program", "arguments")) - .setCreationTime(Instant.ofEpochMilli(1000)) - .setUser("user") - .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); - Containerizer mockContainerizer = createMockContainerizer(); - Mockito.when(mockContainerizer.getExecutorService()) - .thenReturn(Optional.of(mockExecutorService)); - - jibContainerBuilder.containerize( - mockContainerizer, - () -> { - throw new AssertionError(); - }); - - Mockito.verify(mockExecutorService, Mockito.never()).shutdown(); - } - - private Containerizer createMockContainerizer() - throws CacheDirectoryCreationException, InvalidImageReferenceException, InterruptedException, - ExecutionException, DigestException { - - ImageReference targetImage = ImageReference.parse("target-image"); - Containerizer mockContainerizer = Mockito.mock(Containerizer.class); - BuildResult mockBuildResult = Mockito.mock(BuildResult.class); - - Mockito.when(mockContainerizer.getImageConfiguration()) - .thenReturn(ImageConfiguration.builder(targetImage).build()); - Mockito.when(mockContainerizer.run(Mockito.any(BuildConfiguration.class))) - .thenReturn(mockBuildResult); - Mockito.when(mockBuildResult.getImageDigest()) - .thenReturn( - DescriptorDigest.fromHash( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); - Mockito.when(mockBuildResult.getImageId()) - .thenReturn( - DescriptorDigest.fromHash( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); - - Mockito.when(mockContainerizer.getAdditionalTags()).thenReturn(Collections.emptySet()); - Mockito.when(mockContainerizer.getBaseImageLayersCacheDirectory()).thenReturn(Paths.get("/")); - Mockito.when(mockContainerizer.getApplicationLayersCacheDirectory()).thenReturn(Paths.get("/")); - Mockito.when(mockContainerizer.getAllowInsecureRegistries()).thenReturn(false); - Mockito.when(mockContainerizer.getToolName()).thenReturn("mocktool"); - Mockito.when(mockContainerizer.getExecutorService()).thenReturn(Optional.empty()); - Mockito.when(mockContainerizer.buildEventHandlers()).thenReturn(EventHandlers.NONE); - return mockContainerizer; - } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java index 37aad46869..751c557292 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -108,8 +109,7 @@ public void testBuilder() throws Exception { .setTargetFormat(ImageFormat.OCI) .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) - .setToolName(expectedCreatedBy) - .setExecutorService(MoreExecutors.newDirectExecutorService()); + .setToolName(expectedCreatedBy); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); @@ -154,7 +154,6 @@ public void testBuilder() throws Exception { Assert.assertEquals( expectedBaseImageLayersCacheDirectory, buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); - Assert.assertTrue(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); Assert.assertEquals( expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); @@ -187,8 +186,7 @@ public void testBuilder_default() throws IOException { .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) - .setApplicationLayersCacheDirectory(Paths.get("ignored")) - .setExecutorService(MoreExecutors.newDirectExecutorService()); + .setApplicationLayersCacheDirectory(Paths.get("ignored")); BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); @@ -200,7 +198,6 @@ public void testBuilder_default() throws IOException { Assert.assertEquals( Paths.get("ignored"), buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); Assert.assertNull(buildConfiguration.getContainerConfiguration()); - Assert.assertFalse(buildConfiguration.getAllowInsecureRegistries()); Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); Assert.assertEquals("jib", buildConfiguration.getToolName()); } @@ -214,7 +211,6 @@ public void testBuilder_missingValues() throws IOException { ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) - .setExecutorService(MoreExecutors.newDirectExecutorService()) .build(); Assert.fail("Build configuration should not be built with missing values"); @@ -227,7 +223,6 @@ public void testBuilder_missingValues() throws IOException { BuildConfiguration.builder() .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) - .setExecutorService(MoreExecutors.newDirectExecutorService()) .build(); Assert.fail("Build configuration should not be built with missing values"); @@ -244,8 +239,8 @@ public void testBuilder_missingValues() throws IOException { } catch (IllegalStateException ex) { Assert.assertEquals( - "base image configuration, target image configuration, base image layers cache directory, " - + "application layers cache directory, and executor service are required but not set", + "base image configuration, target image configuration, base image layers cache " + + "directory, and application layers cache directory are required but not set", ex.getMessage()); } } @@ -259,8 +254,7 @@ public void testBuilder_digestWarning() throws IOException, InvalidImageReferenc .setTargetImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) - .setApplicationLayersCacheDirectory(Paths.get("ignored")) - .setExecutorService(MoreExecutors.newDirectExecutorService()); + .setApplicationLayersCacheDirectory(Paths.get("ignored")); builder .setBaseImageConfiguration( @@ -280,4 +274,39 @@ public void testBuilder_digestWarning() throws IOException, InvalidImageReferenc LogEvent.warn( "Base image 'image:tag' does not use a specific image digest - build may not be reproducible")); } + + @Test + public void testClose_shutDownInternalExecutorService() throws IOException { + BuildConfiguration buildConfiguration = + BuildConfiguration.builder() + .setBaseImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setTargetImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .build(); + buildConfiguration.close(); + + Assert.assertTrue(buildConfiguration.getExecutorService().isShutdown()); + } + + @Test + public void testClose_doNotShutDownProvidedExecutorService() throws IOException { + ExecutorService executorService = MoreExecutors.newDirectExecutorService(); + BuildConfiguration buildConfiguration = + BuildConfiguration.builder() + .setBaseImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setTargetImageConfiguration( + ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) + .setBaseImageLayersCacheDirectory(Paths.get("ignored")) + .setApplicationLayersCacheDirectory(Paths.get("ignored")) + .setExecutorService(executorService) + .build(); + buildConfiguration.close(); + + Assert.assertSame(executorService, buildConfiguration.getExecutorService()); + Assert.assertFalse(buildConfiguration.getExecutorService().isShutdown()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 041a0a5dbf..059edbf7f1 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -42,7 +42,7 @@ public class RegistryClientTest { @Before public void setUp() { testRegistryClientFactory = - RegistryClient.factory(eventHandlers, "some.server.url", "some image name"); + RegistryClient.factory(eventHandlers, "some.server.url", "some image name", null); } @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); @@ -68,10 +68,7 @@ public void testGetUserAgent_null() { @Test public void testGetUserAgent() { RegistryClient registryClient = - testRegistryClientFactory - .setAllowInsecureRegistries(true) - .setUserAgentSuffix("some user agent suffix") - .newRegistryClient(); + testRegistryClientFactory.setUserAgentSuffix("some user agent suffix").newRegistryClient(); Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); Assert.assertTrue(registryClient.getUserAgent().endsWith(" some user agent suffix")); @@ -82,10 +79,7 @@ public void testGetUserAgentWithUpstreamClient() { System.setProperty(JibSystemProperties.UPSTREAM_CLIENT, "skaffold/0.34.0"); RegistryClient registryClient = - testRegistryClientFactory - .setAllowInsecureRegistries(true) - .setUserAgentSuffix("foo") - .newRegistryClient(); + testRegistryClientFactory.setUserAgentSuffix("foo").newRegistryClient(); Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); Assert.assertTrue(registryClient.getUserAgent().endsWith(" skaffold/0.34.0")); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 24e4d051de..081ce9681b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -37,7 +37,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; -import com.google.common.util.concurrent.MoreExecutors; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -610,9 +609,7 @@ private BuildConfiguration setupBuildConfiguration( new GradleProjectProperties(mockProject, mockLogger, mockTempDirectoryProvider) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( - jibContainerBuilder, - Containerizer.to(RegistryImage.named("to")) - .setExecutorService(MoreExecutors.newDirectExecutorService())); + jibContainerBuilder, Containerizer.to(RegistryImage.named("to"))); } private Path setUpWarProject(Path webAppDirectory) throws IOException { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 99f9931353..f2ecd1535e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -35,7 +35,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; -import com.google.common.util.concurrent.MoreExecutors; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -696,9 +695,7 @@ private BuildConfiguration setupBuildConfiguration( mockMavenProject, mockMavenSession, mockLog, mockTempDirectoryProvider) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); return JibContainerBuilderTestHelper.toBuildConfiguration( - jibContainerBuilder, - Containerizer.to(RegistryImage.named("to")) - .setExecutorService(MoreExecutors.newDirectExecutorService())); + jibContainerBuilder, Containerizer.to(RegistryImage.named("to"))); } private Path setUpWar(Path explodedWar) throws IOException { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java index 510612e310..2903685166 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.BuildConfiguration; -import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; /** Test helper to expose package-private members of {@link JibContainerBuilder}. */ @@ -26,9 +25,7 @@ public class JibContainerBuilderTestHelper { public static BuildConfiguration toBuildConfiguration( JibContainerBuilder jibContainerBuilder, Containerizer containerizer) throws IOException, CacheDirectoryCreationException { - return jibContainerBuilder.toBuildConfiguration( - containerizer, - containerizer.getExecutorService().orElseGet(MoreExecutors::newDirectExecutorService)); + return jibContainerBuilder.toBuildConfiguration(containerizer); } private JibContainerBuilderTestHelper() {} From 21d73838d1b4b697efa1a6e5366d5c05966d7723 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 8 Nov 2019 15:31:45 -0500 Subject: [PATCH 0820/2020] Add in maven sync map generation (#2055) * Add maven syncmap output - with support in plugins-common --- .../jib/maven/JibPluginConfiguration.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 +- .../jib/maven/MavenRawConfiguration.java | 4 +- .../tools/jib/maven/skaffold/SyncMapMojo.java | 89 ++++++++++ .../jib/maven/skaffold/SyncMapMojoTest.java | 160 ++++++++++++++++++ .../common/PluginConfigurationProcessor.java | 49 ++++++ .../common/SkaffoldSyncMapTemplate.java | 23 ++- 7 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java create mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index f0df5992c6..410504a9d5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -282,7 +282,7 @@ public static class OutputPathsParameters { @Component protected SettingsDecrypter settingsDecrypter; - MavenSession getSession() { + protected MavenSession getSession() { return Preconditions.checkNotNull(session); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 1409d440a1..3e137039a9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -83,7 +83,7 @@ public class MavenProjectProperties implements ProjectProperties { * @param tempDirectoryProvider temporary directory provider * @return a MavenProjectProperties from the given project and logger. */ - static MavenProjectProperties getForProject( + public static MavenProjectProperties getForProject( MavenProject project, MavenSession session, Log log, diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index fd2ef3c94c..842831db38 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -28,7 +28,7 @@ import java.util.Set; /** Maven-specific adapter for providing raw configuration parameter values. */ -class MavenRawConfiguration implements RawConfiguration { +public class MavenRawConfiguration implements RawConfiguration { private final JibPluginConfiguration jibPluginConfiguration; @@ -37,7 +37,7 @@ class MavenRawConfiguration implements RawConfiguration { * * @param jibPluginConfiguration the Jib plugin configuration */ - MavenRawConfiguration(JibPluginConfiguration jibPluginConfiguration) { + public MavenRawConfiguration(JibPluginConfiguration jibPluginConfiguration) { this.jibPluginConfiguration = jibPluginConfiguration; } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java new file mode 100644 index 0000000000..1152d4b82e --- /dev/null +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.maven.JibPluginConfiguration; +import com.google.cloud.tools.jib.maven.MavenProjectProperties; +import com.google.cloud.tools.jib.maven.MavenRawConfiguration; +import com.google.cloud.tools.jib.maven.MojoCommon; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.common.annotations.VisibleForTesting; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +@Mojo( + name = SyncMapMojo.GOAL_NAME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class SyncMapMojo extends JibPluginConfiguration { + + @VisibleForTesting static final String GOAL_NAME = "_skaffold-sync-map"; + + @Override + public void execute() throws MojoExecutionException { + checkJibVersion(); + if (MojoCommon.shouldSkipJibExecution(this)) { + return; + } + + // add check that means this is only for jars + if (!"jar".equals(getProject().getPackaging())) { + throw new MojoExecutionException( + "Skaffold sync is currently only available for 'jar' style Jib projects, but the packaging of " + + getProject().getArtifactId() + + " is '" + + getProject().getPackaging() + + "'"); + } + // add check for exploded containerization + try { + if (!ContainerizingMode.EXPLODED.equals(ContainerizingMode.from(getContainerizingMode()))) { + throw new MojoExecutionException( + "Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of " + + getProject().getArtifactId() + + " is '" + + getContainerizingMode() + + "'"); + } + } catch (InvalidContainerizingModeException ex) { + throw new MojoExecutionException("Invalid containerizing mode", ex); + } + + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + MavenProjectProperties projectProperties = + MavenProjectProperties.getForProject( + getProject(), getSession(), getLog(), tempDirectoryProvider); + + MavenRawConfiguration configuration = new MavenRawConfiguration(this); + + try { + String syncMapJson = + PluginConfigurationProcessor.getSkaffoldSyncMap(configuration, projectProperties); + + System.out.println("\nBEGIN JIB JSON"); + System.out.println(syncMapJson); + + } catch (Exception ex) { + throw new MojoExecutionException( + "Failed to generate a Jib file map for sync with Skaffold", ex); + } + } + } +} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java new file mode 100644 index 0000000000..d76412731a --- /dev/null +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.maven.skaffold; + +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.maven.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate.FileTemplate; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link SyncMapMojo}. */ +public class SyncMapMojoTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); + @ClassRule public static final TestProject warProject = new TestProject("war_servlet25"); + + private static Path runBuild(Path projectRoot, String module, String pomXml) + throws VerificationException { + Verifier verifier = new Verifier(projectRoot.toString()); + verifier.setAutoclean(false); + verifier.addCliOption("-q"); + if (pomXml != null) { + verifier.addCliOption("--file=" + pomXml); + } + if (!Strings.isNullOrEmpty(module)) { + verifier.addCliOption("-pl"); + verifier.addCliOption(module); + verifier.addCliOption("-am"); + verifier.addCliOption("-Djib.containerize=com.jib.test:" + module); + } + verifier.addCliOption("-DskipTests"); + verifier.executeGoals(ImmutableList.of("package", "jib:" + SyncMapMojo.GOAL_NAME)); + + return Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); + } + + private static String getSyncMapJson(Path projectRoot, String module) + throws VerificationException, IOException { + Path logFile = runBuild(projectRoot, module, null); + List outputLines = Files.readAllLines(logFile, StandardCharsets.UTF_8); + Assert.assertEquals(3, outputLines.size()); // we expect ["\n", "BEGIN JIB JSON", ""] + return outputLines.get(2); // this is the JSON output + } + + private static void assertFilePaths(Path src, AbsoluteUnixPath dest, FileTemplate template) { + Assert.assertEquals(src.toString(), template.getSrc()); + Assert.assertEquals(dest.toString(), template.getDest()); + } + + @Test + public void testSyncMapMojo_simpleTestProjectOutput() throws IOException, VerificationException { + Path projectRoot = simpleTestProject.getProjectRoot(); + String json = getSyncMapJson(projectRoot, null); + SkaffoldSyncMapTemplate parsed = SkaffoldSyncMapTemplate.from(json); + + List generated = parsed.getGenerated(); + Assert.assertEquals(2, generated.size()); + assertFilePaths( + projectRoot.resolve("target/classes/world"), + AbsoluteUnixPath.get("/app/resources/world"), + generated.get(0)); + assertFilePaths( + projectRoot.resolve("target/classes/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(1)); + + List direct = parsed.getDirect(); + Assert.assertEquals(2, direct.size()); + assertFilePaths( + projectRoot.resolve("src/main/jib-custom/bar/cat"), + AbsoluteUnixPath.get("/bar/cat"), + direct.get(0)); + assertFilePaths( + projectRoot.resolve("src/main/jib-custom/foo"), + AbsoluteUnixPath.get("/foo"), + direct.get(1)); + } + + @Test + public void testSyncMapMojo_multiProjectOutput() throws IOException, VerificationException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path m2 = Paths.get(System.getProperty("user.home")).resolve(".m2").resolve("repository"); + String json = getSyncMapJson(projectRoot, "complex-service"); + SkaffoldSyncMapTemplate parsed = SkaffoldSyncMapTemplate.from(json); + + List generated = parsed.getGenerated(); + Assert.assertEquals(2, generated.size()); + assertFilePaths( + projectRoot.resolve("lib/target/lib-1.0.0.TEST-SNAPSHOT.jar"), + AbsoluteUnixPath.get("/app/libs/lib-1.0.0.TEST-SNAPSHOT.jar"), + generated.get(0)); + assertFilePaths( + projectRoot.resolve("complex-service/target/classes/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(1)); + + List direct = parsed.getDirect(); + Assert.assertEquals(1, direct.size()); + assertFilePaths( + m2.resolve( + "com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + AbsoluteUnixPath.get("/app/libs/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + direct.get(0)); + } + + @Test + public void testSyncMapMojo_failIfPackagingNotJar() throws IOException { + Path projectRoot = warProject.getProjectRoot(); + try { + runBuild(projectRoot, null, null); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "org.apache.maven.plugin.MojoExecutionException: Skaffold sync is currently only available for 'jar' style Jib projects, but the packaging of servlet25 is 'war'")); + } + } + + @Test + public void testSyncMapMojo_failIfJarContainerizationMode() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + try { + runBuild(projectRoot, null, "pom-jar-containerization.xml"); + Assert.fail(); + } catch (VerificationException ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "org.apache.maven.plugin.MojoExecutionException: Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of hello-world is 'packaged'")); + } + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 35486e8d1e..68210eb67c 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -23,8 +23,10 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; +import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainerBuilder; +import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.Ports; import com.google.cloud.tools.jib.api.RegistryImage; @@ -34,6 +36,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; @@ -56,6 +59,19 @@ */ public class PluginConfigurationProcessor { + // Known "generated" dependencies -- these require that the underlying system run a build step + // before they are available for sync'ing + private static final ImmutableList GENERATED_LAYERS = + ImmutableList.of( + LayerType.PROJECT_DEPENDENCIES.getName(), + LayerType.RESOURCES.getName(), + LayerType.CLASSES.getName()); + + // Known "constant" layers -- changes to these layers require a change to the build definition, + // which we consider non-syncable. These should not be included in the sync-map. + private static final ImmutableList CONST_LAYERS = + ImmutableList.of(LayerType.DEPENDENCIES.getName()); + public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( RawConfiguration rawConfiguration, InferredAuthProvider inferredAuthProvider, @@ -172,6 +188,39 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( .writeImageId(rawConfiguration.getImageIdOutputPath()); } + public static String getSkaffoldSyncMap( + RawConfiguration rawConfiguration, ProjectProperties projectProperties) + throws IOException, InvalidCreationTimeException, InvalidImageReferenceException, + IncompatibleBaseImageJavaVersionException, InvalidContainerVolumeException, + MainClassInferenceException, InvalidAppRootException, InvalidWorkingDirectoryException, + InvalidFilesModificationTimeException, InvalidContainerizingModeException { + JibContainerBuilder jibContainerBuilder = + processCommonConfiguration( + rawConfiguration, ignored -> Optional.empty(), projectProperties); + SkaffoldSyncMapTemplate syncMap = new SkaffoldSyncMapTemplate(); + // since jib has already expanded out directories after processing everything, we just + // ignore directories and provide only files to watch + for (LayerConfiguration layer : jibContainerBuilder.describeContainer().getLayers()) { + if (CONST_LAYERS.contains(layer.getName())) { + continue; + } + if (GENERATED_LAYERS.contains(layer.getName())) { + layer + .getLayerEntries() + .stream() + .filter(layerEntry -> Files.isRegularFile(layerEntry.getSourceFile())) + .forEach(syncMap::addGenerated); + } else { // this is a direct layer + layer + .getLayerEntries() + .stream() + .filter(layerEntry -> Files.isRegularFile(layerEntry.getSourceFile())) + .forEach(syncMap::addDirect); + } + } + return syncMap.getJsonString(); + } + @VisibleForTesting static JibContainerBuilder processCommonConfiguration( RawConfiguration rawConfiguration, diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java index 6b2b669997..d96c400682 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/SkaffoldSyncMapTemplate.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -77,13 +78,23 @@ public FileTemplate( this.src = src; this.dest = dest; } + + @VisibleForTesting + public String getSrc() { + return src; + } + + @VisibleForTesting + public String getDest() { + return dest; + } } private final List generated = new ArrayList<>(); private final List direct = new ArrayList<>(); @VisibleForTesting - static SkaffoldSyncMapTemplate from(String jsonString) throws IOException { + public static SkaffoldSyncMapTemplate from(String jsonString) throws IOException { return new ObjectMapper().readValue(jsonString, SkaffoldSyncMapTemplate.class); } @@ -107,4 +118,14 @@ public String getJsonString() throws IOException { return outputStream.toString(); } } + + @VisibleForTesting + public List getGenerated() { + return ImmutableList.copyOf(generated); + } + + @VisibleForTesting + public List getDirect() { + return ImmutableList.copyOf(direct); + } } From 6b040875489209a4e4701574d43bbfd294bb9d2b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 8 Nov 2019 16:01:20 -0500 Subject: [PATCH 0821/2020] Make TestWebServer thread-safe (#2133) --- .../google/cloud/tools/jib/http/TestWebServer.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index d35eb28c87..fc8ce61716 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -48,7 +48,7 @@ public class TestWebServer implements Closeable { private final ServerSocket serverSocket; private final ExecutorService executorService; private final Semaphore threadsStarted; - private final StringBuilder inputRead = new StringBuilder(); + private final StringBuffer inputRead = new StringBuffer(); // StringBuilder not thread-safe private final List responses; @@ -119,14 +119,18 @@ private Void serveResponses() throws IOException { out.write("HTTP/1.1 400 Bad Request\n\n".getBytes(StandardCharsets.UTF_8)); return null; } - inputRead.append((char) firstByte); BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); for (String response : responses) { for (String line = reader.readLine(); line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. line = reader.readLine()) { - inputRead.append(line + "\n"); + if (firstByte == -1) { + inputRead.append(line + "\n"); + } else { + inputRead.append(((char) firstByte) + line + "\n"); + firstByte = -1; + } } out.write(response.getBytes(StandardCharsets.UTF_8)); socket.getOutputStream().flush(); @@ -141,6 +145,10 @@ private void ignoreReturn(Future future) { // do nothing; to make Error Prone happy } + /** + * Returns input read. Note if there were concurrent connections, input lines from different + * connections can be intermixed. However, no lines will ever be broken in the middle. + */ public String getInputRead() { return inputRead.toString(); } From 230909703f70e64221455e8ee0d9ff1a4b36c757 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 8 Nov 2019 16:27:17 -0500 Subject: [PATCH 0822/2020] clarify repository behavior (#2136) --- .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 0 .../0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar | Bin .../0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom | 0 .../tools/tiny-test-lib/maven-metadata-local.xml | 0 .../maven/projects/multi/complex-service/pom.xml | 11 +++++++---- 5 files changed, 7 insertions(+), 4 deletions(-) rename jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/{local-m2-repo => fake-remote-repo}/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml (100%) rename jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/{local-m2-repo => fake-remote-repo}/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar (100%) rename jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/{local-m2-repo => fake-remote-repo}/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom (100%) rename jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/{local-m2-repo => fake-remote-repo}/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml (100%) diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/maven-metadata-local.xml diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar similarity index 100% rename from jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom similarity index 100% rename from jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.pom diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml similarity index 100% rename from jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/local-m2-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml rename to jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/fake-remote-repo/com/google/cloud/tools/tiny-test-lib/maven-metadata-local.xml diff --git a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml index bf876aee44..193654786e 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/multi/complex-service/pom.xml @@ -75,16 +75,19 @@ - - local-m2-repo - file:${project.basedir}/local-m2-repo + fake-remote-repo + file:${project.basedir}/fake-remote-repo From ecc378d517734224e5660addf606aea37ab8eacc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 8 Nov 2019 16:51:36 -0500 Subject: [PATCH 0823/2020] standard way of server listening (#2137) --- .../cloud/tools/jib/http/TestWebServer.java | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index fc8ce61716..f4b0facf63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -45,12 +45,13 @@ public class TestWebServer implements Closeable { private final boolean https; + private final int numThreads; + private final List responses; + private final ServerSocket serverSocket; private final ExecutorService executorService; - private final Semaphore threadsStarted; - private final StringBuffer inputRead = new StringBuffer(); // StringBuilder not thread-safe - - private final List responses; + private final Semaphore serverStarted = new Semaphore(1); + private final StringBuilder inputRead = new StringBuilder(); public TestWebServer(boolean https) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { @@ -66,13 +67,11 @@ public TestWebServer(boolean https, List responses, int numThreads) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { this.https = https; this.responses = responses; + this.numThreads = numThreads; serverSocket = https ? createHttpsServerSocket() : new ServerSocket(0); - threadsStarted = new Semaphore(1 - numThreads); - executorService = Executors.newFixedThreadPool(numThreads); - for (int i = 0; i < numThreads; i++) { - ignoreReturn(executorService.submit(this::serveResponses)); - } - threadsStarted.acquire(); + executorService = Executors.newFixedThreadPool(numThreads + 1); + ignoreReturn(executorService.submit(this::listen)); + serverStarted.acquire(); } public int getLocalPort() { @@ -108,9 +107,17 @@ private ServerSocket createHttpsServerSocket() return sslContext.getServerSocketFactory().createServerSocket(0); } - private Void serveResponses() throws IOException { - threadsStarted.release(); - try (Socket socket = serverSocket.accept()) { + private Void listen() throws IOException { + serverStarted.release(); + for (int i = 0; i < numThreads; i++) { + Socket socket = serverSocket.accept(); + ignoreReturn(executorService.submit(() -> serveResponses(socket))); + } + return null; + } + + private Void serveResponses(Socket socket) throws IOException { + try (Socket toClose = socket) { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); @@ -125,11 +132,12 @@ private Void serveResponses() throws IOException { for (String line = reader.readLine(); line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. line = reader.readLine()) { - if (firstByte == -1) { - inputRead.append(line + "\n"); - } else { - inputRead.append(((char) firstByte) + line + "\n"); - firstByte = -1; + synchronized (inputRead) { + if (firstByte != -1) { + inputRead.append((char) firstByte); + firstByte = -1; + } + inputRead.append(line).append('\n'); } } out.write(response.getBytes(StandardCharsets.UTF_8)); From 679fa4feab8aada423b377d23a73f217566f1f1c Mon Sep 17 00:00:00 2001 From: damienhollis Date: Tue, 12 Nov 2019 09:39:46 +1300 Subject: [PATCH 0824/2020] Corrected dropwizard-jib-example pom to use jvmFlag rather than jmxFlag. (#2138) --- examples/dropwizard/pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index c91c9cff2c..be056cefa8 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -72,14 +72,14 @@ - -server - -Djava.awt.headless=true - -XX:InitialRAMFraction=2 - -XX:MinRAMFraction=2 - -XX:MaxRAMFraction=2 - -XX:+UseG1GC - -XX:MaxGCPauseMillis=100 - -XX:+UseStringDeduplication + -server + -Djava.awt.headless=true + -XX:InitialRAMFraction=2 + -XX:MinRAMFraction=2 + -XX:MaxRAMFraction=2 + -XX:+UseG1GC + -XX:MaxGCPauseMillis=100 + -XX:+UseStringDeduplication From 93c08cafab629290532a64e0a555ab3fb96fc87c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 11 Nov 2019 15:43:04 -0500 Subject: [PATCH 0825/2020] Set Gradle version to run in integration tests (#2139) --- .../com/google/cloud/tools/jib/gradle/TestProject.java | 6 +++++- .../tools/jib/maven/BuildImageMojoIntegrationTest.java | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index c07b2c258f..51d4ec06ab 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -75,7 +75,11 @@ protected void before() throws Throwable { projectRoot = newFolder().toPath(); copyProject(testProjectName, projectRoot); - gradleRunner = GradleRunner.create().withProjectDir(projectRoot.toFile()).withPluginClasspath(); + gradleRunner = + GradleRunner.create() + .withGradleVersion(JibPlugin.GRADLE_MIN_VERSION.getVersion()) + .withProjectDir(projectRoot.toFile()) + .withPluginClasspath(); } BuildResult build(String... gradleArguments) { diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 5615924487..4bc74a52f1 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -219,7 +219,7 @@ private static String buildAndRunComplex( String password, LocalRegistry targetRegistry, String pomFile) - throws VerificationException, IOException, InterruptedException, DigestException { + throws VerificationException, IOException, InterruptedException { Verifier verifier = new Verifier(simpleTestProject.getProjectRoot().toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", imageReference); @@ -599,7 +599,7 @@ public void testExecute_complex() @Test public void testExecute_timestampCustom() - throws IOException, InterruptedException, VerificationException, DigestException { + throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:6000/simpleimage:maven" + System.nanoTime(); String pom = "pom-timestamps-custom.xml"; Assert.assertEquals( @@ -642,7 +642,7 @@ public void testDockerDaemon_timestampFail() throws IOException { @Test public void testExecute_complex_sameFromAndToRegistry() - throws IOException, InterruptedException, VerificationException, DigestException { + throws IOException, InterruptedException, VerificationException { String targetImage = "localhost:5000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" @@ -655,7 +655,7 @@ public void testExecute_complex_sameFromAndToRegistry() @Test public void testExecute_complexProperties() - throws InterruptedException, DigestException, VerificationException, IOException { + throws InterruptedException, VerificationException, IOException { String targetImage = "localhost:6000/compleximage:maven" + System.nanoTime(); Assert.assertEquals( "Hello, world. An argument.\n1970-01-01T00:00:01Z\nrwxr-xr-x\nrwxrwxrwx\nfoo\ncat\n" From 6161920962c3b0a56760b81b20cb9fdf964b06fc Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 11 Nov 2019 16:47:44 -0500 Subject: [PATCH 0826/2020] Put skaffold classes in skaffold package (#2141) --- .../cloud/tools/jib/gradle/JibPlugin.java | 34 ++++++++++--------- .../{ => skaffold}/CheckJibVersionTask.java | 5 +-- .../jib/gradle/{ => skaffold}/FilesTask.java | 3 +- .../gradle/{ => skaffold}/FilesTaskV2.java | 3 +- .../InitTask.java} | 7 ++-- .../cloud/tools/jib/gradle/JibPluginTest.java | 3 +- .../cloud/tools/jib/gradle/TestProject.java | 8 ++--- .../gradle/{ => skaffold}/FilesTaskTest.java | 6 ++-- .../{ => skaffold}/FilesTaskV2Test.java | 6 ++-- .../InitTaskTest.java} | 12 ++++--- 10 files changed, 50 insertions(+), 37 deletions(-) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{ => skaffold}/CheckJibVersionTask.java (91%) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{ => skaffold}/FilesTask.java (98%) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{ => skaffold}/FilesTaskV2.java (98%) rename jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/{SkaffoldInitTask.java => skaffold/InitTask.java} (89%) rename jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/{ => skaffold}/FilesTaskTest.java (96%) rename jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/{ => skaffold}/FilesTaskV2Test.java (96%) rename jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/{SkaffoldInitTaskTest.java => skaffold/InitTaskTest.java} (88%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index ae19337776..40fcca78b4 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -17,6 +17,10 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.gradle.skaffold.CheckJibVersionTask; +import com.google.cloud.tools.jib.gradle.skaffold.FilesTask; +import com.google.cloud.tools.jib.gradle.skaffold.FilesTaskV2; +import com.google.cloud.tools.jib.gradle.skaffold.InitTask; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; @@ -39,19 +43,17 @@ public class JibPlugin implements Plugin { @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("4.9"); - @VisibleForTesting static final String JIB_EXTENSION_NAME = "jib"; - @VisibleForTesting static final String BUILD_IMAGE_TASK_NAME = "jib"; - @VisibleForTesting static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; - @VisibleForTesting static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; - @VisibleForTesting static final String FILES_TASK_NAME = "_jibSkaffoldFiles"; - @VisibleForTesting static final String FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; - @VisibleForTesting static final String INIT_TASK_NAME = "_jibSkaffoldInit"; + public static final String JIB_EXTENSION_NAME = "jib"; + public static final String BUILD_IMAGE_TASK_NAME = "jib"; + public static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; + public static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; + public static final String SKAFFOLD_FILES_TASK_NAME = "_jibSkaffoldFiles"; + public static final String SKAFFOLD_FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; + public static final String SKAFFOLD_INIT_TASK_NAME = "_jibSkaffoldInit"; + public static final String SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME = + "_skaffoldFailIfJibOutOfDate"; - @VisibleForTesting static final String EXPLODED_WAR_TASK_NAME = "jibExplodedWar"; - - static final String CHECK_REQUIRED_VERSION_TASK_NAME = "_skaffoldFailIfJibOutOfDate"; - - static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; + public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; /** * Collects all project dependencies of the style "compile project(':mylib')" for any kind of @@ -148,18 +150,18 @@ public void apply(Project project) { }); tasks - .register(FILES_TASK_NAME, FilesTask.class) + .register(SKAFFOLD_FILES_TASK_NAME, FilesTask.class) .configure(task -> task.setJibExtension(jibExtension)); tasks - .register(FILES_TASK_V2_NAME, FilesTaskV2.class) + .register(SKAFFOLD_FILES_TASK_V2_NAME, FilesTaskV2.class) .configure(task -> task.setJibExtension(jibExtension)); tasks - .register(INIT_TASK_NAME, SkaffoldInitTask.class) + .register(SKAFFOLD_INIT_TASK_NAME, InitTask.class) .configure(task -> task.setJibExtension(jibExtension)); // A check to catch older versions of Jib. This can be removed once we are certain people // are using Jib 1.3.1 or later. - tasks.register(CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); + tasks.register(SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); project.afterEvaluate( projectAfterEvaluation -> { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java similarity index 91% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java index 66efa60080..a0bca7638c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CheckJibVersionTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibPlugin; import com.google.common.base.Strings; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -36,7 +37,7 @@ public class CheckJibVersionTask extends DefaultTask { public void checkVersion() { if (Strings.isNullOrEmpty(System.getProperty(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME))) { throw new GradleException( - JibPlugin.CHECK_REQUIRED_VERSION_TASK_NAME + JibPlugin.SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME + " requires " + JibPlugin.REQUIRED_VERSION_PROPERTY_NAME + " to be set"); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java similarity index 98% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java index e32ad05486..ae5e5adeb2 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibExtension; import com.google.common.base.Preconditions; import java.io.File; import java.nio.file.Files; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java similarity index 98% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java index 6c5b2938a4..201afd3623 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibExtension; import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; import com.google.common.base.Preconditions; import java.io.File; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java similarity index 89% rename from jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java rename to jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java index c77e1906d2..c077c10644 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java @@ -14,8 +14,9 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibExtension; import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; import com.google.common.base.Preconditions; import java.io.IOException; @@ -29,11 +30,11 @@ * *

Expected use: {@code ./gradlew _jibSkaffoldInit -q} */ -public class SkaffoldInitTask extends DefaultTask { +public class InitTask extends DefaultTask { @Nullable private JibExtension jibExtension; - public SkaffoldInitTask setJibExtension(JibExtension jibExtension) { + public InitTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 0a7fabcaf8..6faad5db92 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -116,7 +116,8 @@ public void testCheckGradleVersion_fail() throws IOException { public void testCheckJibVersionNames() { // These identifiers will be baked into Skaffold and should not be changed Assert.assertEquals(JibPlugin.REQUIRED_VERSION_PROPERTY_NAME, "jib.requiredVersion"); - Assert.assertEquals(JibPlugin.CHECK_REQUIRED_VERSION_TASK_NAME, "_skaffoldFailIfJibOutOfDate"); + Assert.assertEquals( + JibPlugin.SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME, "_skaffoldFailIfJibOutOfDate"); } @Test diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index 51d4ec06ab..84594be2f5 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -30,7 +30,7 @@ // TODO: Consolidate with TestProject in jib-maven-plugin. /** Works with the test Gradle projects in the {@code resources/projects} directory. */ -class TestProject extends TemporaryFolder implements Closeable { +public class TestProject extends TemporaryFolder implements Closeable { private static final String PROJECTS_PATH_IN_RESOURCES = "gradle/projects/"; @@ -59,7 +59,7 @@ private static void copyProject(String projectName, Path destination) private Path projectRoot; /** Initialize with a specific project directory. */ - TestProject(String testProjectName) { + public TestProject(String testProjectName) { this.testProjectName = testProjectName; } @@ -82,11 +82,11 @@ protected void before() throws Throwable { .withPluginClasspath(); } - BuildResult build(String... gradleArguments) { + public BuildResult build(String... gradleArguments) { return gradleRunner.withArguments(gradleArguments).build(); } - Path getProjectRoot() { + public Path getProjectRoot() { return projectRoot; } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java similarity index 96% rename from jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java index cfdb52efb3..4ca2413141 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java @@ -14,8 +14,10 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -47,7 +49,7 @@ public class FilesTaskTest { */ private static List verifyTaskSuccess(TestProject project, @Nullable String moduleName) { String taskName = - ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.FILES_TASK_NAME; + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_FILES_TASK_NAME; BuildResult buildResult = project.build(taskName, "-q"); BuildTask jibTask = buildResult.task(taskName); Assert.assertNotNull(jibTask); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java similarity index 96% rename from jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java index 6aac4681af..9913d2fd92 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/FilesTaskV2Test.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java @@ -14,8 +14,10 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; import com.google.cloud.tools.jib.plugins.common.SkaffoldFilesOutput; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -47,7 +49,7 @@ public class FilesTaskV2Test { */ private static String verifyTaskSuccess(TestProject project, @Nullable String moduleName) { String taskName = - ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.FILES_TASK_V2_NAME; + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_FILES_TASK_V2_NAME; BuildResult buildResult = project.build(taskName, "-q"); BuildTask jibTask = buildResult.task(taskName); Assert.assertNotNull(jibTask); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java similarity index 88% rename from jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java rename to jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java index 814128f476..d4f61bc4d1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/SkaffoldInitTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java @@ -14,8 +14,10 @@ * the License. */ -package com.google.cloud.tools.jib.gradle; +package com.google.cloud.tools.jib.gradle.skaffold; +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; import com.google.cloud.tools.jib.plugins.common.SkaffoldInitOutput; import java.io.IOException; import java.util.ArrayList; @@ -30,8 +32,8 @@ import org.junit.ClassRule; import org.junit.Test; -/** Tests for {@link SkaffoldInitTask}. */ -public class SkaffoldInitTaskTest { +/** Tests for {@link InitTask}. */ +public class InitTaskTest { @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); @@ -46,8 +48,8 @@ public class SkaffoldInitTaskTest { */ private static List getJsons(TestProject project) { BuildResult buildResult = - project.build(JibPlugin.INIT_TASK_NAME, "-q", "-D_TARGET_IMAGE=testimage"); - BuildTask jibTask = buildResult.task(":" + JibPlugin.INIT_TASK_NAME); + project.build(JibPlugin.SKAFFOLD_INIT_TASK_NAME, "-q", "-D_TARGET_IMAGE=testimage"); + BuildTask jibTask = buildResult.task(":" + JibPlugin.SKAFFOLD_INIT_TASK_NAME); Assert.assertNotNull(jibTask); Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); String output = buildResult.getOutput().trim(); From 19cabacfc0f6a19476f8083fd0bb05c480ed520c Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 13 Nov 2019 10:05:12 -0500 Subject: [PATCH 0827/2020] Rename BuildConfiguration to BuildContext (#2146) --- .../cloud/tools/jib/api/Containerizer.java | 24 ++-- .../tools/jib/api/JibContainerBuilder.java | 33 +++-- .../builder/steps/AuthenticatePushStep.java | 14 +-- .../BuildAndCacheApplicationLayerStep.java | 21 ++-- .../jib/builder/steps/BuildImageStep.java | 23 ++-- .../jib/builder/steps/LoadDockerStep.java | 16 +-- .../builder/steps/LocalBaseImageSteps.java | 26 ++-- .../steps/ObtainBaseImageLayerStep.java | 35 +++--- .../jib/builder/steps/PullBaseImageStep.java | 46 ++++--- .../tools/jib/builder/steps/PushBlobStep.java | 18 +-- .../steps/PushContainerConfigurationStep.java | 12 +- .../jib/builder/steps/PushImageStep.java | 22 ++-- .../jib/builder/steps/PushLayerStep.java | 17 ++- .../RetrieveRegistryCredentialsStep.java | 28 ++--- .../tools/jib/builder/steps/StepsRunner.java | 58 +++++---- .../jib/builder/steps/WriteTarFileStep.java | 18 ++- ...ldConfiguration.java => BuildContext.java} | 16 +-- .../jib/api/JavaContainerBuilderTest.java | 72 +++++------ .../jib/api/JibContainerBuilderTest.java | 76 ++++++------ ...BuildAndCacheApplicationLayerStepTest.java | 16 ++- .../jib/builder/steps/BuildImageStepTest.java | 24 ++-- .../steps/LocalBaseImageStepsTest.java | 14 +-- .../steps/ObtainBaseImageLayerStepTest.java | 16 +-- .../builder/steps/PullBaseImageStepTest.java | 20 +-- .../jib/builder/steps/PushBlobStepTest.java | 11 +- .../RetrieveRegistryCredentialsStepTest.java | 28 ++--- ...urationTest.java => BuildContextTest.java} | 114 +++++++++--------- .../gradle/GradleProjectPropertiesTest.java | 57 ++++----- .../jib/maven/MavenProjectPropertiesTest.java | 59 +++++---- .../api/JibContainerBuilderTestHelper.java | 6 +- .../JavaContainerBuilderHelperTest.java | 18 +-- .../PluginConfigurationProcessorTest.java | 92 +++++++------- 32 files changed, 504 insertions(+), 546 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/configuration/{BuildConfiguration.java => BuildContext.java} (97%) rename jib-core/src/test/java/com/google/cloud/tools/jib/configuration/{BuildConfigurationTest.java => BuildContextTest.java} (72%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index b727341cd4..cbd05e7318 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.builder.steps.BuildResult; import com.google.cloud.tools.jib.builder.steps.StepsRunner; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventHandlers; @@ -66,8 +66,8 @@ public static Containerizer to(RegistryImage registryImage) { .setCredentialRetrievers(registryImage.getCredentialRetrievers()) .build(); - Function stepsRunnerFactory = - buildConfiguration -> StepsRunner.begin(buildConfiguration).registryPushSteps(); + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).registryPushSteps(); return new Containerizer( DESCRIPTION_FOR_DOCKER_REGISTRY, imageConfiguration, stepsRunnerFactory, true); @@ -86,8 +86,8 @@ public static Containerizer to(DockerDaemonImage dockerDaemonImage) { DockerClient dockerClient = new DockerClient( dockerDaemonImage.getDockerExecutable(), dockerDaemonImage.getDockerEnvironment()); - Function stepsRunnerFactory = - buildConfiguration -> StepsRunner.begin(buildConfiguration).dockerLoadSteps(dockerClient); + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).dockerLoadSteps(dockerClient); return new Containerizer( DESCRIPTION_FOR_DOCKER_DAEMON, imageConfiguration, stepsRunnerFactory, false); @@ -109,9 +109,8 @@ public static Containerizer to(TarImage tarImage) { ImageConfiguration imageConfiguration = ImageConfiguration.builder(tarImage.getImageReference().get()).build(); - Function stepsRunnerFactory = - buildConfiguration -> - StepsRunner.begin(buildConfiguration).tarBuildSteps(tarImage.getPath()); + Function stepsRunnerFactory = + buildContext -> StepsRunner.begin(buildContext).tarBuildSteps(tarImage.getPath()); return new Containerizer( DESCRIPTION_FOR_TARBALL, imageConfiguration, stepsRunnerFactory, false); @@ -119,7 +118,7 @@ public static Containerizer to(TarImage tarImage) { private final String description; private final ImageConfiguration imageConfiguration; - private final Function stepsRunnerFactory; + private final Function stepsRunnerFactory; private final boolean mustBeOnline; private final Set additionalTags = new HashSet<>(); private final EventHandlers.Builder eventHandlersBuilder = EventHandlers.builder(); @@ -136,7 +135,7 @@ public static Containerizer to(TarImage tarImage) { private Containerizer( String description, ImageConfiguration imageConfiguration, - Function stepsRunnerFactory, + Function stepsRunnerFactory, boolean mustBeOnline) { this.description = description; this.imageConfiguration = imageConfiguration; @@ -338,8 +337,7 @@ ImageConfiguration getImageConfiguration() { return imageConfiguration; } - BuildResult run(BuildConfiguration buildConfiguration) - throws ExecutionException, InterruptedException { - return stepsRunnerFactory.apply(buildConfiguration).run(); + BuildResult run(BuildContext buildContext) throws ExecutionException, InterruptedException { + return stepsRunnerFactory.apply(buildContext).run(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index a5454e5d52..af058fe109 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.BuildResult; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; @@ -65,7 +65,7 @@ private static String capitalizeFirstLetter(String string) { private final ContainerConfiguration.Builder containerConfigurationBuilder = ContainerConfiguration.builder(); - private final BuildConfiguration.Builder buildConfigurationBuilder; + private final BuildContext.Builder buildContextBuilder; private List layerConfigurations = new ArrayList<>(); @@ -75,7 +75,7 @@ private static String capitalizeFirstLetter(String string) { ImageConfiguration.builder(baseImage.getImageReference()) .setCredentialRetrievers(baseImage.getCredentialRetrievers()) .build(), - BuildConfiguration.builder()); + BuildContext.builder()); } /** Instantiate with {@link Jib#from}. */ @@ -85,7 +85,7 @@ private static String capitalizeFirstLetter(String string) { .setDockerClient( new DockerClient(baseImage.getDockerExecutable(), baseImage.getDockerEnvironment())) .build(), - BuildConfiguration.builder()); + BuildContext.builder()); } /** Instantiate with {@link Jib#from}. */ @@ -95,14 +95,13 @@ private static String capitalizeFirstLetter(String string) { ImageConfiguration.builder(baseImage.getImageReference().orElse(ImageReference.scratch())) .setTarPath(baseImage.getPath()) .build(), - BuildConfiguration.builder()); + BuildContext.builder()); } @VisibleForTesting JibContainerBuilder( - ImageConfiguration imageConfiguration, BuildConfiguration.Builder buildConfigurationBuilder) { - this.buildConfigurationBuilder = - buildConfigurationBuilder.setBaseImageConfiguration(imageConfiguration); + ImageConfiguration imageConfiguration, BuildContext.Builder buildContextBuilder) { + this.buildContextBuilder = buildContextBuilder.setBaseImageConfiguration(imageConfiguration); } /** @@ -400,7 +399,7 @@ public JibContainerBuilder addLabel(String key, String value) { * @return this */ public JibContainerBuilder setFormat(ImageFormat imageFormat) { - buildConfigurationBuilder.setTargetFormat(imageFormat); + buildContextBuilder.setTargetFormat(imageFormat); return this; } @@ -471,13 +470,13 @@ public JibContainerBuilder setWorkingDirectory(@Nullable AbsoluteUnixPath workin public JibContainer containerize(Containerizer containerizer) throws InterruptedException, RegistryException, IOException, CacheDirectoryCreationException, ExecutionException { - try (BuildConfiguration buildConfiguration = toBuildConfiguration(containerizer); + try (BuildContext buildContext = toBuildContext(containerizer); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), containerizer.getDescription())) { - logSources(buildConfiguration.getEventHandlers()); + buildContext.getEventHandlers(), containerizer.getDescription())) { + logSources(buildContext.getEventHandlers()); - BuildResult result = containerizer.run(buildConfiguration); + BuildResult result = containerizer.run(buildContext); return new JibContainer(result.getImageDigest(), result.getImageId()); } catch (ExecutionException ex) { @@ -500,17 +499,17 @@ public JibContainerDescription describeContainer() { } /** - * Builds a {@link BuildConfiguration} using this and a {@link Containerizer}. + * Builds a {@link BuildContext} using this and a {@link Containerizer}. * * @param containerizer the {@link Containerizer} - * @return the {@link BuildConfiguration} + * @return the {@link BuildContext} * @throws CacheDirectoryCreationException if a cache directory could not be created * @throws IOException if an I/O exception occurs */ @VisibleForTesting - BuildConfiguration toBuildConfiguration(Containerizer containerizer) + BuildContext toBuildContext(Containerizer containerizer) throws CacheDirectoryCreationException, IOException { - return buildConfigurationBuilder + return buildContextBuilder .setTargetImageConfiguration(containerizer.getImageConfiguration()) .setAdditionalTargetImageTags(containerizer.getAdditionalTags()) .setBaseImageLayersCacheDirectory(containerizer.getBaseImageLayersCacheDirectory()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index 55b56200a1..eed8b5b7b2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import java.io.IOException; @@ -38,29 +38,29 @@ class AuthenticatePushStep implements Callable> { private static final String DESCRIPTION = "Authenticating push to %s"; - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Credential registryCredential; AuthenticatePushStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Credential registryCredential) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registryCredential = registryCredential; } @Override public Optional call() throws IOException, RegistryException { - String registry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + String registry = buildContext.getTargetImageConfiguration().getImageRegistry(); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("authenticating push to " + registry, 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), String.format(DESCRIPTION, registry))) { + buildContext.getEventHandlers(), String.format(DESCRIPTION, registry))) { Optional registryAuthenticator = - buildConfiguration + buildContext .newTargetImageRegistryClientFactory() .newRegistryClient() .getRegistryAuthenticator(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java index ab40716ce3..70f575f86d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.ReproducibleLayerBuilder; import com.google.common.collect.ImmutableList; @@ -43,16 +43,15 @@ class BuildAndCacheApplicationLayerStep implements Callable { * classes layers. Optionally adds an extra layer if configured to do so. */ static ImmutableList makeList( - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { - List layerConfigurations = buildConfiguration.getLayerConfigurations(); + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + List layerConfigurations = buildContext.getLayerConfigurations(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( "launching application layer builders", layerConfigurations.size()); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), "Preparing application layer builders")) { + buildContext.getEventHandlers(), "Preparing application layer builders")) { return layerConfigurations .stream() // Skips the layer if empty. @@ -60,7 +59,7 @@ static ImmutableList makeList( .map( layerConfiguration -> new BuildAndCacheApplicationLayerStep( - buildConfiguration, + buildContext, progressEventDispatcher.newChildProducer(), layerConfiguration.getName(), layerConfiguration)) @@ -68,18 +67,18 @@ static ImmutableList makeList( } } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String layerName; private final LayerConfiguration layerConfiguration; private BuildAndCacheApplicationLayerStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String layerName, LayerConfiguration layerConfiguration) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layerName = layerName; this.layerConfiguration = layerConfiguration; @@ -89,13 +88,13 @@ private BuildAndCacheApplicationLayerStep( public PreparedLayer call() throws IOException, CacheCorruptedException { String description = String.format(DESCRIPTION, layerName); - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); eventHandlers.dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("building " + layerName + " layer", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { - Cache cache = buildConfiguration.getApplicationLayersCache(); + Cache cache = buildContext.getApplicationLayersCache(); // Don't build the layer if it exists already. Optional optionalCachedLayer = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index e221317557..b16656917d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -36,19 +36,19 @@ class BuildImageStep implements Callable { private static final String DESCRIPTION = "Building container configuration"; - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Image baseImage; private final List baseImageLayers; private final List applicationLayers; BuildImageStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Image baseImage, List baseImageLayers, List applicationLayers) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.baseImage = baseImage; this.baseImageLayers = baseImageLayers; @@ -60,11 +60,10 @@ public Image call() throws LayerPropertyNotFoundException { try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("building image format", 1); TimerEventDispatcher ignored2 = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { + new TimerEventDispatcher(buildContext.getEventHandlers(), DESCRIPTION)) { // Constructs the image. - Image.Builder imageBuilder = Image.builder(buildConfiguration.getTargetFormat()); - ContainerConfiguration containerConfiguration = - buildConfiguration.getContainerConfiguration(); + Image.Builder imageBuilder = Image.builder(buildContext.getTargetFormat()); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); // Base image layers baseImageLayers.stream().forEach(imageBuilder::addLayer); @@ -108,7 +107,7 @@ public Image call() throws LayerPropertyNotFoundException { HistoryEntry.builder() .setCreationTimestamp(layerCreationTime) .setAuthor("Jib") - .setCreatedBy(buildConfiguration.getToolName() + ":" + ProjectInfo.VERSION) + .setCreatedBy(buildContext.getToolName() + ":" + ProjectInfo.VERSION) .setComment(applicationLayer.getName()) .build()); } @@ -153,8 +152,8 @@ private ImmutableList computeEntrypoint( if (entrypointToUse != null) { String logSuffix = shouldInherit ? " (inherited from base image)" : ""; String message = "Container entrypoint set to " + entrypointToUse + logSuffix; - buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle("")); - buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle(message)); + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle("")); + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle(message)); } return entrypointToUse; @@ -187,7 +186,7 @@ private ImmutableList computeProgramArguments( if (programArgumentsToUse != null) { String logSuffix = shouldInherit ? " (inherited from base image)" : ""; String message = "Container program arguments set to " + programArgumentsToUse + logSuffix; - buildConfiguration.getEventHandlers().dispatch(LogEvent.lifecycle(message)); + buildContext.getEventHandlers().dispatch(LogEvent.lifecycle(message)); } return programArgumentsToUse; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index b1ab290b3d..38c202989f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.EventHandlers; @@ -31,18 +31,18 @@ /** Adds image layers to a tarball and loads into Docker daemon. */ class LoadDockerStep implements Callable { - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final DockerClient dockerClient; private final Image builtImage; LoadDockerStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient, Image builtImage) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.dockerClient = dockerClient; this.builtImage = builtImage; @@ -50,15 +50,15 @@ class LoadDockerStep implements Callable { @Override public BuildResult call() throws InterruptedException, IOException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, "Loading to Docker daemon")) { eventHandlers.dispatch(LogEvent.progress("Loading to Docker daemon...")); ImageTarball imageTarball = new ImageTarball( builtImage, - buildConfiguration.getTargetImageConfiguration().getImage(), - buildConfiguration.getAllTargetImageTags()); + buildContext.getTargetImageConfiguration().getImage(), + buildContext.getAllTargetImageTags()); // Note: The progress reported here is not entirely accurate. The total allocation units is // the size of the layers, but the progress being reported includes the config and manifest @@ -73,7 +73,7 @@ public BuildResult call() throws InterruptedException, IOException { eventHandlers.dispatch( LogEvent.debug(dockerClient.load(imageTarball, throttledProgressReporter))); - return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); + return BuildResult.fromImage(builtImage, buildContext.getTargetFormat()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java index 95a2668a21..ab58dfc9d8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -28,7 +28,7 @@ import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; @@ -93,21 +93,21 @@ static boolean isGzipped(Path path) throws IOException { } static Callable retrieveDockerDaemonImageStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient) { return () -> { - ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); + ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("processing base image " + imageReference, 2); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), + buildContext.getEventHandlers(), "Saving " + imageReference + " from Docker daemon")) { DockerClient.DockerImageDetails dockerImageDetails = dockerClient.inspect(imageReference); Optional cachedImage = - getCachedDockerImage(buildConfiguration.getBaseImageLayersCache(), dockerImageDetails); + getCachedDockerImage(buildContext.getBaseImageLayersCache(), dockerImageDetails); if (cachedImage.isPresent()) { return cachedImage.get(); } @@ -124,16 +124,16 @@ static Callable retrieveDockerDaemonImageStep( } return cacheDockerImageTar( - buildConfiguration, tarPath, progressEventDispatcher.newChildProducer()); + buildContext, tarPath, progressEventDispatcher.newChildProducer()); } }; } static Callable retrieveTarImageStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Path tarPath) { - return () -> cacheDockerImageTar(buildConfiguration, tarPath, progressEventDispatcherFactory); + return () -> cacheDockerImageTar(buildContext, tarPath, progressEventDispatcherFactory); } @VisibleForTesting @@ -171,18 +171,18 @@ static Optional getCachedDockerImage( @VisibleForTesting static LocalImage cacheDockerImageTar( - BuildConfiguration buildConfiguration, + BuildContext buildContext, Path tarPath, ProgressEventDispatcher.Factory progressEventDispatcherFactory) throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, ExecutionException, InterruptedException { - ExecutorService executorService = buildConfiguration.getExecutorService(); + ExecutorService executorService = buildContext.getExecutorService(); try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { Path destination = tempDirectoryProvider.newDirectory(); try (TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), + buildContext.getEventHandlers(), "Extracting tar " + tarPath + " into " + destination)) { TarExtractor.extract(tarPath, destination); @@ -208,7 +208,7 @@ static LocalImage cacheDockerImageTar( + configurationTemplate.getLayerCount() + " layers"); } - buildConfiguration + buildContext .getBaseImageLayersCache() .writeLocalConfig(originalConfigDescriptor.getDigest(), configurationTemplate); @@ -235,7 +235,7 @@ static LocalImage cacheDockerImageTar( executorService.submit( () -> compressAndCacheTarLayer( - buildConfiguration.getBaseImageLayersCache(), + buildContext.getBaseImageLayersCache(), diffId, layerFile, layersAreCompressed, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 7a4b411298..8aa5e8ce5b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -26,7 +26,7 @@ import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Layer; @@ -52,23 +52,22 @@ private interface BlobExistenceChecker { } static ImmutableList makeListForForcedDownload( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, ImageAndAuthorization baseImageAndAuth) { BlobExistenceChecker noOpChecker = ignored -> StateInTarget.UNKNOWN; - return makeList( - buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, noOpChecker); + return makeList(buildContext, progressEventDispatcherFactory, baseImageAndAuth, noOpChecker); } static ImmutableList makeListForSelectiveDownload( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, ImageAndAuthorization baseImageAndAuth, Authorization pushAuthorization) { - Verify.verify(!buildConfiguration.isOffline()); + Verify.verify(!buildContext.isOffline()); RegistryClient targetRegistryClient = - buildConfiguration + buildContext .newTargetImageRegistryClientFactory() .setAuthorization(pushAuthorization) .newRegistryClient(); @@ -80,11 +79,11 @@ static ImmutableList makeListForSelectiveDownload( : StateInTarget.MISSING; return makeList( - buildConfiguration, progressEventDispatcherFactory, baseImageAndAuth, blobExistenceChecker); + buildContext, progressEventDispatcherFactory, baseImageAndAuth, blobExistenceChecker); } private static ImmutableList makeList( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, ImageAndAuthorization baseImageAndAuth, BlobExistenceChecker blobExistenceChecker) { @@ -95,13 +94,13 @@ private static ImmutableList makeList( "launching base image layer pullers", baseImageLayers.size()); TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), "Preparing base image layer pullers")) { + buildContext.getEventHandlers(), "Preparing base image layer pullers")) { List layerPullers = new ArrayList<>(); for (Layer layer : baseImageLayers) { layerPullers.add( new ObtainBaseImageLayerStep( - buildConfiguration, + buildContext, progressEventDispatcher.newChildProducer(), layer, baseImageAndAuth.getAuthorization(), @@ -111,7 +110,7 @@ private static ImmutableList makeList( } } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Layer layer; @@ -119,12 +118,12 @@ private static ImmutableList makeList( private final BlobExistenceChecker blobExistenceChecker; private ObtainBaseImageLayerStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Layer layer, @Nullable Authorization pullAuthorization, BlobExistenceChecker blobExistenceChecker) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layer = layer; this.pullAuthorization = pullAuthorization; @@ -133,7 +132,7 @@ private ObtainBaseImageLayerStep( @Override public PreparedLayer call() throws IOException, CacheCorruptedException, RegistryException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); DescriptorDigest layerDigest = layer.getBlobDescriptor().getDigest(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("checking base image layer " + layerDigest, 1); @@ -149,14 +148,14 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr return new PreparedLayer.Builder(layer).setStateInTarget(stateInTarget).build(); } - Cache cache = buildConfiguration.getBaseImageLayersCache(); + Cache cache = buildContext.getBaseImageLayersCache(); // Checks if the layer already exists in the cache. Optional optionalCachedLayer = cache.retrieve(layerDigest); if (optionalCachedLayer.isPresent()) { CachedLayer cachedLayer = optionalCachedLayer.get(); return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); - } else if (buildConfiguration.isOffline()) { + } else if (buildContext.isOffline()) { throw new IOException( "Cannot run Jib in offline mode; local Jib cache for base image is missing image layer " + layerDigest @@ -165,7 +164,7 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr } RegistryClient registryClient = - buildConfiguration + buildContext .newBaseImageRegistryClientFactory() .setAuthorization(pullAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index cc249c3743..f776137775 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -27,7 +27,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.http.Authorization; @@ -82,13 +82,12 @@ Authorization getAuthorization() { } } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; PullBaseImageStep( - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { - this.buildConfiguration = buildConfiguration; + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; } @@ -97,24 +96,23 @@ public ImageAndAuthorization call() throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException, CacheCorruptedException, CredentialRetrievalException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); // Skip this step if this is a scratch image - ImageReference imageReference = buildConfiguration.getBaseImageConfiguration().getImage(); + ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); if (imageReference.isScratch()) { eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); - return new ImageAndAuthorization( - Image.builder(buildConfiguration.getTargetFormat()).build(), null); + return new ImageAndAuthorization(Image.builder(buildContext.getTargetFormat()).build(), null); } eventHandlers.dispatch( LogEvent.progress("Getting manifest for base image " + imageReference + "...")); - if (buildConfiguration.isOffline() || imageReference.isTagDigest()) { + if (buildContext.isOffline() || imageReference.isTagDigest()) { Optional image = getCachedBaseImage(); if (image.isPresent()) { return new ImageAndAuthorization(image.get(), null); } - if (buildConfiguration.isOffline()) { + if (buildContext.isOffline()) { throw new IOException( "Cannot run Jib in offline mode; " + imageReference + " not found in local Jib cache"); } @@ -137,7 +135,7 @@ public ImageAndAuthorization call() // registry.credentials.RegistryCredentialsRetriever. Credential registryCredential = RetrieveRegistryCredentialsStep.forBaseImage( - buildConfiguration, progressEventDispatcher.newChildProducer()) + buildContext, progressEventDispatcher.newChildProducer()) .call() .orElse(null); @@ -155,7 +153,7 @@ public ImageAndAuthorization call() // The registry requires us to authenticate using the Docker Token Authentication. // See https://docs.docker.com/registry/spec/auth/token Optional registryAuthenticator = - buildConfiguration + buildContext .newBaseImageRegistryClientFactory() .newRegistryClient() .getRegistryAuthenticator(); @@ -196,15 +194,15 @@ private Image pullBaseImage( ProgressEventDispatcher progressEventDispatcher) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); RegistryClient registryClient = - buildConfiguration + buildContext .newBaseImageRegistryClientFactory() .setAuthorization(registryAuthorization) .newRegistryClient(); ManifestAndDigest manifestAndDigest = - registryClient.pullManifest(buildConfiguration.getBaseImageConfiguration().getImageTag()); + registryClient.pullManifest(buildContext.getBaseImageConfiguration().getImageTag()); ManifestTemplate manifestTemplate = manifestAndDigest.getManifest(); // special handling if we happen upon a manifest list, redirect to a manifest and continue @@ -222,10 +220,10 @@ private Image pullBaseImage( switch (manifestTemplate.getSchemaVersion()) { case 1: V21ManifestTemplate v21ManifestTemplate = (V21ManifestTemplate) manifestTemplate; - buildConfiguration + buildContext .getBaseImageLayersCache() .writeMetadata( - buildConfiguration.getBaseImageConfiguration().getImage(), v21ManifestTemplate); + buildContext.getBaseImageConfiguration().getImage(), v21ManifestTemplate); return JsonToImageTranslator.toImage(v21ManifestTemplate); case 2: @@ -257,10 +255,10 @@ private Image pullBaseImage( ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJson( containerConfigurationString, ContainerConfigurationTemplate.class); - buildConfiguration + buildContext .getBaseImageLayersCache() .writeMetadata( - buildConfiguration.getBaseImageConfiguration().getImage(), + buildContext.getBaseImageConfiguration().getImage(), buildableManifestTemplate, containerConfigurationTemplate); return JsonToImageTranslator.toImage( @@ -283,8 +281,8 @@ private ManifestAndDigest obtainPlatformSpecificImageManifest( if (digests.size() == 0) { String errorMessage = "Unable to find amd64/linux manifest in manifest list at: " - + buildConfiguration.getBaseImageConfiguration().getImage(); - buildConfiguration.getEventHandlers().dispatch(LogEvent.error(errorMessage)); + + buildContext.getBaseImageConfiguration().getImage(); + buildContext.getEventHandlers().dispatch(LogEvent.error(errorMessage)); throw new RegistryException(errorMessage); } return registryClient.pullManifest(digests.get(0)); @@ -303,9 +301,9 @@ private ManifestAndDigest obtainPlatformSpecificImageManifest( private Optional getCachedBaseImage() throws IOException, CacheCorruptedException, BadContainerConfigurationFormatException, LayerCountMismatchException { - ImageReference baseImage = buildConfiguration.getBaseImageConfiguration().getImage(); + ImageReference baseImage = buildContext.getBaseImageConfiguration().getImage(); Optional metadata = - buildConfiguration.getBaseImageLayersCache().retrieveMetadata(baseImage); + buildContext.getBaseImageLayersCache().retrieveMetadata(baseImage); if (!metadata.isPresent()) { return Optional.empty(); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 534de20198..07e2cdd1b5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.http.Authorization; @@ -37,7 +37,7 @@ class PushBlobStep implements Callable { private static final String DESCRIPTION = "Pushing BLOB "; - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Authorization authorization; @@ -46,13 +46,13 @@ class PushBlobStep implements Callable { private final boolean forcePush; PushBlobStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization authorization, BlobDescriptor blobDescriptor, Blob blob, boolean forcePush) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.authorization = authorization; this.blobDescriptor = blobDescriptor; @@ -62,7 +62,7 @@ class PushBlobStep implements Callable { @Override public BlobDescriptor call() throws IOException, RegistryException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); DescriptorDigest blobDigest = blobDescriptor.getDigest(); try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( @@ -72,7 +72,7 @@ public BlobDescriptor call() throws IOException, RegistryException { ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { RegistryClient registryClient = - buildConfiguration + buildContext .newTargetImageRegistryClientFactory() .setAuthorization(authorization) .newRegistryClient(); @@ -89,9 +89,9 @@ public BlobDescriptor call() throws IOException, RegistryException { // BLOB from the base image repository to the target image repository and possibly avoid // having to push the BLOB. See // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. - String baseRegistry = buildConfiguration.getBaseImageConfiguration().getImageRegistry(); - String baseRepository = buildConfiguration.getBaseImageConfiguration().getImageRepository(); - String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + String baseRegistry = buildContext.getBaseImageConfiguration().getImageRegistry(); + String baseRepository = buildContext.getBaseImageConfiguration().getImageRepository(); + String targetRegistry = buildContext.getTargetImageConfiguration().getImageRegistry(); String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; registryClient.pushBlob(blobDigest, blob, sourceRepository, throttledProgressReporter); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index 54049feaa7..cba1111883 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; @@ -36,18 +36,18 @@ class PushContainerConfigurationStep implements Callable { private static final String DESCRIPTION = "Pushing container configuration"; - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Authorization pushAuthorization; private final Image builtImage; PushContainerConfigurationStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization authenticatePushStep, Image builtImage) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pushAuthorization = authenticatePushStep; this.builtImage = builtImage; @@ -58,12 +58,12 @@ public BlobDescriptor call() throws IOException, RegistryException { try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pushing container configuration", 1); TimerEventDispatcher ignored = - new TimerEventDispatcher(buildConfiguration.getEventHandlers(), DESCRIPTION)) { + new TimerEventDispatcher(buildContext.getEventHandlers(), DESCRIPTION)) { JsonTemplate containerConfiguration = new ImageToJsonTranslator(builtImage).getContainerConfiguration(); return new PushBlobStep( - buildConfiguration, + buildContext, progressEventDispatcher.newChildProducer(), pushAuthorization, Digests.computeDigest(containerConfiguration), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index 23f3f4cc3c..a4c0e60a81 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Authorization; @@ -45,17 +45,17 @@ class PushImageStep implements Callable { private static final String DESCRIPTION = "Pushing manifest"; static ImmutableList makeList( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Authorization pushAuthorization, BlobDescriptor containerConfigurationDigestAndSize, Image builtImage) throws IOException { - Set tags = buildConfiguration.getAllTargetImageTags(); + Set tags = buildContext.getAllTargetImageTags(); try (TimerEventDispatcher ignored = new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), "Preparing manifest pushers"); + buildContext.getEventHandlers(), "Preparing manifest pushers"); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("launching manifest pushers", tags.size())) { @@ -63,7 +63,7 @@ static ImmutableList makeList( BuildableManifestTemplate manifestTemplate = new ImageToJsonTranslator(builtImage) .getManifestTemplate( - buildConfiguration.getTargetFormat(), containerConfigurationDigestAndSize); + buildContext.getTargetFormat(), containerConfigurationDigestAndSize); DescriptorDigest manifestDigest = Digests.computeJsonDigest(manifestTemplate); @@ -71,7 +71,7 @@ static ImmutableList makeList( .map( tag -> new PushImageStep( - buildConfiguration, + buildContext, progressEventDispatcher.newChildProducer(), pushAuthorization, manifestTemplate, @@ -82,7 +82,7 @@ static ImmutableList makeList( } } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final BuildableManifestTemplate manifestTemplate; @@ -92,14 +92,14 @@ static ImmutableList makeList( private final DescriptorDigest imageId; PushImageStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, BuildableManifestTemplate manifestTemplate, String tag, DescriptorDigest imageDigest, DescriptorDigest imageId) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pushAuthorization = pushAuthorization; this.manifestTemplate = manifestTemplate; @@ -110,14 +110,14 @@ static ImmutableList makeList( @Override public BuildResult call() throws IOException, RegistryException { - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); try (TimerEventDispatcher ignored = new TimerEventDispatcher(eventHandlers, DESCRIPTION); ProgressEventDispatcher ignored2 = progressEventDispatcherFactory.create("pushing manifest for " + tag, 1)) { eventHandlers.dispatch(LogEvent.info("Pushing manifest for " + tag + "...")); RegistryClient registryClient = - buildConfiguration + buildContext .newTargetImageRegistryClientFactory() .setAuthorization(pushAuthorization) .newRegistryClient(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 77251acf7c..595e088bbd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.http.Authorization; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -34,13 +34,12 @@ class PushLayerStep implements Callable { static ImmutableList makeList( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, List> cachedLayers) { try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildConfiguration.getEventHandlers(), "Preparing layer pushers"); + new TimerEventDispatcher(buildContext.getEventHandlers(), "Preparing layer pushers"); ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("launching layer pushers", cachedLayers.size())) { @@ -50,7 +49,7 @@ static ImmutableList makeList( .map( layer -> new PushLayerStep( - buildConfiguration, + buildContext, progressEventDispatcher.newChildProducer(), pushAuthorization, layer)) @@ -58,18 +57,18 @@ static ImmutableList makeList( } } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Nullable private final Authorization pushAuthorization; private final Future preparedLayer; PushLayerStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, Future preparedLayer) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.pushAuthorization = pushAuthorization; this.preparedLayer = preparedLayer; @@ -86,7 +85,7 @@ public BlobDescriptor call() boolean forcePush = layer.getStateInTarget() == StateInTarget.MISSING; return new PushBlobStep( - buildConfiguration, + buildContext, progressEventDispatcherFactory, pushAuthorization, layer.getBlobDescriptor(), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index b149b213fe..3d4f04bcc6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableList; @@ -33,38 +33,36 @@ class RetrieveRegistryCredentialsStep implements Callable> /** Retrieves credentials for the base image. */ static RetrieveRegistryCredentialsStep forBaseImage( - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( - buildConfiguration, + buildContext, progressEventDispatcherFactory, - buildConfiguration.getBaseImageConfiguration().getImageRegistry(), - buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); + buildContext.getBaseImageConfiguration().getImageRegistry(), + buildContext.getBaseImageConfiguration().getCredentialRetrievers()); } /** Retrieves credentials for the target image. */ static RetrieveRegistryCredentialsStep forTargetImage( - BuildConfiguration buildConfiguration, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) { + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { return new RetrieveRegistryCredentialsStep( - buildConfiguration, + buildContext, progressEventDispatcherFactory, - buildConfiguration.getTargetImageConfiguration().getImageRegistry(), - buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers()); + buildContext.getTargetImageConfiguration().getImageRegistry(), + buildContext.getTargetImageConfiguration().getCredentialRetrievers()); } - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String registry; private final ImmutableList credentialRetrievers; RetrieveRegistryCredentialsStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String registry, ImmutableList credentialRetrievers) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registry = registry; this.credentialRetrievers = credentialRetrievers; @@ -73,7 +71,7 @@ static RetrieveRegistryCredentialsStep forTargetImage( @Override public Optional call() throws CredentialRetrievalException { String description = "Retrieving registry credentials for " + registry; - EventHandlers eventHandlers = buildConfiguration.getEventHandlers(); + EventHandlers eventHandlers = buildContext.getEventHandlers(); eventHandlers.dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 50dcccba82..45a01ee8fb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; @@ -77,16 +77,16 @@ private static Future failedFuture() { /** * Starts building the steps to run. * - * @param buildConfiguration the {@link BuildConfiguration} + * @param buildContext the {@link BuildContext} * @return a new {@link StepsRunner} */ - public static StepsRunner begin(BuildConfiguration buildConfiguration) { + public static StepsRunner begin(BuildContext buildContext) { ExecutorService executorService = JibSystemProperties.serializeExecution() ? MoreExecutors.newDirectExecutorService() - : buildConfiguration.getExecutorService(); + : buildContext.getExecutorService(); - return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildConfiguration); + return new StepsRunner(MoreExecutors.listeningDecorator(executorService), buildContext); } private static List realizeFutures(List> futures) @@ -101,7 +101,7 @@ private static List realizeFutures(List> futures) private final StepResults results = new StepResults(); private final ExecutorService executorService; - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); // We save steps to run by wrapping each step into a Runnable, only because of the unfortunate @@ -116,10 +116,9 @@ private static List realizeFutures(List> futures) @Nullable private String rootProgressDescription; @Nullable private ProgressEventDispatcher rootProgressDispatcher; - private StepsRunner( - ListeningExecutorService executorService, BuildConfiguration buildConfiguration) { + private StepsRunner(ListeningExecutorService executorService, BuildContext buildContext) { this.executorService = executorService; - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; } public StepsRunner dockerLoadSteps(DockerClient dockerClient) { @@ -148,7 +147,7 @@ public StepsRunner tarBuildSteps(Path outputPath) { public StepsRunner registryPushSteps() { rootProgressDescription = "building image to registry"; - boolean layersRequiredLocally = buildConfiguration.getAlwaysCacheBaseImage(); + boolean layersRequiredLocally = buildContext.getAlwaysCacheBaseImage(); stepsToRun.add(this::retrieveTargetRegistryCredentials); stepsToRun.add(this::authenticatePush); @@ -170,7 +169,7 @@ public BuildResult run() throws ExecutionException, InterruptedException { try (ProgressEventDispatcher progressEventDispatcher = ProgressEventDispatcher.newRoot( - buildConfiguration.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { + buildContext.getEventHandlers(), rootProgressDescription, stepsToRun.size())) { rootProgressDispatcher = progressEventDispatcher; stepsToRun.forEach(Runnable::run); @@ -189,7 +188,7 @@ public BuildResult run() throws ExecutionException, InterruptedException { } private void addRetrievalSteps(boolean layersRequiredLocally) { - ImageConfiguration baseImageConfiguration = buildConfiguration.getBaseImageConfiguration(); + ImageConfiguration baseImageConfiguration = buildContext.getBaseImageConfiguration(); if (baseImageConfiguration.getTarPath().isPresent()) { // If tarPath is present, a TarImage was used @@ -213,7 +212,7 @@ private void retrieveTargetRegistryCredentials() { results.targetRegistryCredentials = executorService.submit( RetrieveRegistryCredentialsStep.forTargetImage( - buildConfiguration, childProgressDispatcherFactory)); + buildContext, childProgressDispatcherFactory)); } private void authenticatePush() { @@ -224,7 +223,7 @@ private void authenticatePush() { executorService.submit( () -> new AuthenticatePushStep( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.targetRegistryCredentials.get().orElse(null)) .call()); @@ -232,25 +231,25 @@ private void authenticatePush() { private void saveDocker() { Optional dockerClient = - buildConfiguration.getBaseImageConfiguration().getDockerClient(); + buildContext.getBaseImageConfiguration().getDockerClient(); Preconditions.checkArgument(dockerClient.isPresent()); ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); assignLocalImageResult( executorService.submit( LocalBaseImageSteps.retrieveDockerDaemonImageStep( - buildConfiguration, childProgressDispatcherFactory, dockerClient.get()))); + buildContext, childProgressDispatcherFactory, dockerClient.get()))); } private void extractTar() { - Optional tarPath = buildConfiguration.getBaseImageConfiguration().getTarPath(); + Optional tarPath = buildContext.getBaseImageConfiguration().getTarPath(); Preconditions.checkArgument(tarPath.isPresent()); ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); assignLocalImageResult( executorService.submit( LocalBaseImageSteps.retrieveTarImageStep( - buildConfiguration, childProgressDispatcherFactory, tarPath.get()))); + buildContext, childProgressDispatcherFactory, tarPath.get()))); } private void assignLocalImageResult(Future localImageFuture) { @@ -273,8 +272,7 @@ private void pullBaseImage() { Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); results.baseImageAndAuth = - executorService.submit( - new PullBaseImageStep(buildConfiguration, childProgressDispatcherFactory)); + executorService.submit(new PullBaseImageStep(buildContext, childProgressDispatcherFactory)); } private void obtainBaseImageLayers(boolean layersRequiredLocally) { @@ -287,11 +285,11 @@ private void obtainBaseImageLayers(boolean layersRequiredLocally) { scheduleCallables( layersRequiredLocally ? ObtainBaseImageLayerStep.makeListForForcedDownload( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.baseImageAndAuth.get()) : ObtainBaseImageLayerStep.makeListForSelectiveDownload( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.baseImageAndAuth.get(), results.pushAuthorization.get().orElse(null)))); @@ -306,7 +304,7 @@ private void pushBaseImageLayers() { () -> scheduleCallables( PushLayerStep.makeList( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.pushAuthorization.get().orElse(null), results.baseImageLayers.get()))); @@ -319,7 +317,7 @@ private void buildAndCacheApplicationLayers() { results.applicationLayers = scheduleCallables( BuildAndCacheApplicationLayerStep.makeList( - buildConfiguration, childProgressDispatcherFactory)); + buildContext, childProgressDispatcherFactory)); } private void buildImage() { @@ -330,7 +328,7 @@ private void buildImage() { executorService.submit( () -> new BuildImageStep( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.baseImageAndAuth.get().getImage(), realizeFutures(results.baseImageLayers.get()), @@ -346,7 +344,7 @@ private void pushContainerConfiguration() { executorService.submit( () -> new PushContainerConfigurationStep( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.pushAuthorization.get().orElse(null), results.builtImage.get()) @@ -362,7 +360,7 @@ private void pushApplicationLayers() { () -> scheduleCallables( PushLayerStep.makeList( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.pushAuthorization.get().orElse(null), Verify.verifyNotNull(results.applicationLayers)))); @@ -381,7 +379,7 @@ private void pushImages() { List> manifestPushResults = scheduleCallables( PushImageStep.makeList( - buildConfiguration, + buildContext, childProgressDispatcherFactory, results.pushAuthorization.get().orElse(null), results.containerConfigurationPushResult.get(), @@ -400,7 +398,7 @@ private void loadDocker(DockerClient dockerClient) { executorService.submit( () -> new LoadDockerStep( - buildConfiguration, + buildContext, childProgressDispatcherFactory, dockerClient, results.builtImage.get()) @@ -415,7 +413,7 @@ private void writeTarFile(Path outputPath) { executorService.submit( () -> new WriteTarFileStep( - buildConfiguration, + buildContext, childProgressDispatcherFactory, outputPath, results.builtImage.get()) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 59011904db..7e224ccb70 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -18,7 +18,7 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; @@ -31,18 +31,18 @@ public class WriteTarFileStep implements Callable { - private final BuildConfiguration buildConfiguration; + private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Path outputPath; private final Image builtImage; WriteTarFileStep( - BuildConfiguration buildConfiguration, + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Path outputPath, Image builtImage) { - this.buildConfiguration = buildConfiguration; + this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.outputPath = outputPath; this.builtImage = builtImage; @@ -50,9 +50,7 @@ public class WriteTarFileStep implements Callable { @Override public BuildResult call() throws IOException { - buildConfiguration - .getEventHandlers() - .dispatch(LogEvent.progress("Building image to tar file...")); + buildContext.getEventHandlers().dispatch(LogEvent.progress("Building image to tar file...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("writing to tar file", 1)) { @@ -64,12 +62,12 @@ public BuildResult call() throws IOException { new BufferedOutputStream(FileOperations.newLockingOutputStream(outputPath))) { new ImageTarball( builtImage, - buildConfiguration.getTargetImageConfiguration().getImage(), - buildConfiguration.getAllTargetImageTags()) + buildContext.getTargetImageConfiguration().getImage(), + buildContext.getAllTargetImageTags()) .writeTo(outputStream); } - return BuildResult.fromImage(builtImage, buildConfiguration.getTargetFormat()); + return BuildResult.fromImage(builtImage, buildContext.getTargetFormat()); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java similarity index 97% rename from jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java index 492c90763e..186138f527 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java @@ -49,7 +49,7 @@ * ContainerConfiguration}, {@link ImageConfiguration}, and {@link LayerConfiguration}) held in are * immutable. */ -public class BuildConfiguration implements Closeable { +public class BuildContext implements Closeable { /** The default target format of the container manifest. */ private static final Class DEFAULT_TARGET_FORMAT = @@ -58,7 +58,7 @@ public class BuildConfiguration implements Closeable { /** The default tool identifier. */ private static final String DEFAULT_TOOL_NAME = "jib"; - /** Builds an immutable {@link BuildConfiguration}. Instantiate with {@link #builder}. */ + /** Builds an immutable {@link BuildContext}. Instantiate with {@link #builder}. */ public static class Builder { // All the parameters below are set to their default values. @@ -241,12 +241,12 @@ public Builder setExecutorService(@Nullable ExecutorService executorService) { } /** - * Builds a new {@link BuildConfiguration} using the parameters passed into the builder. + * Builds a new {@link BuildContext} using the parameters passed into the builder. * - * @return the corresponding build configuration + * @return the corresponding build context * @throws IOException if an I/O exception occurs */ - public BuildConfiguration build() throws IOException { + public BuildContext build() throws IOException { // Validates the parameters. List missingFields = new ArrayList<>(); if (baseImageConfiguration == null) { @@ -274,7 +274,7 @@ public BuildConfiguration build() throws IOException { + "' does not use a specific image digest - build may not be reproducible")); } - return new BuildConfiguration( + return new BuildContext( baseImageConfiguration, Preconditions.checkNotNull(targetImageConfiguration), additionalTargetImageTags, @@ -326,7 +326,7 @@ Path getApplicationLayersCacheDirectory() { } /** - * Creates a new {@link Builder} to build a {@link BuildConfiguration}. + * Creates a new {@link Builder} to build a {@link BuildContext}. * * @return a new {@link Builder} */ @@ -351,7 +351,7 @@ public static Builder builder() { private final boolean alwaysCacheBaseImage; /** Instantiate with {@link #builder}. */ - private BuildConfiguration( + private BuildContext( ImageConfiguration baseImageConfiguration, ImageConfiguration targetImageConfiguration, ImmutableSet additionalTargetImageTags, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index 93ddcb8171..f8362bf062 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; @@ -37,10 +37,10 @@ private static Path getResource(String directory) throws URISyntaxException { return Paths.get(Resources.getResource(directory).toURI()); } - /** Gets the extraction paths in the specified layer of a give {@link BuildConfiguration}. */ + /** Gets the extraction paths in the specified layer of a give {@link BuildContext}. */ private static List getExtractionPaths( - BuildConfiguration buildConfiguration, String layerName) { - return buildConfiguration + BuildContext buildContext, String layerName) { + return buildContext .getLayerConfigurations() .stream() .filter(layerConfiguration -> layerConfiguration.getName().equals(layerName)) @@ -59,7 +59,7 @@ private static List getExtractionPaths( public void testToJibContainerBuilder_all() throws InvalidImageReferenceException, URISyntaxException, IOException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = + BuildContext buildContext = JavaContainerBuilder.fromDistroless() .setAppRoot("/hello") .addResources(getResource("core/application/resources")) @@ -80,10 +80,10 @@ public void testToJibContainerBuilder_all() .addJvmFlags("-xflag1", "-xflag2") .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint - ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); Assert.assertNotNull(containerConfiguration); Assert.assertEquals( ImmutableList.of( @@ -100,24 +100,21 @@ public void testToJibContainerBuilder_all() ImmutableList.of( AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-770.jar"), AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar")); - Assert.assertEquals( - expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); // Check snapshots List expectedSnapshotDependencies = ImmutableList.of( AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-SNAPSHOT.jar")); Assert.assertEquals( - expectedSnapshotDependencies, - getExtractionPaths(buildConfiguration, "snapshot dependencies")); + expectedSnapshotDependencies, getExtractionPaths(buildContext, "snapshot dependencies")); List expectedProjectDependencies = ImmutableList.of( AbsoluteUnixPath.get("/hello/different-libs/libraryA.jar"), AbsoluteUnixPath.get("/hello/different-libs/libraryB.jar")); Assert.assertEquals( - expectedProjectDependencies, - getExtractionPaths(buildConfiguration, "project dependencies")); + expectedProjectDependencies, getExtractionPaths(buildContext, "project dependencies")); // Check resources List expectedResources = @@ -125,28 +122,28 @@ public void testToJibContainerBuilder_all() AbsoluteUnixPath.get("/hello/different-resources/resourceA"), AbsoluteUnixPath.get("/hello/different-resources/resourceB"), AbsoluteUnixPath.get("/hello/different-resources/world")); - Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildContext, "resources")); // Check classes List expectedClasses = ImmutableList.of( AbsoluteUnixPath.get("/hello/different-classes/HelloWorld.class"), AbsoluteUnixPath.get("/hello/different-classes/some.class")); - Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); // Check additional classpath files List expectedOthers = ImmutableList.of( AbsoluteUnixPath.get("/hello/different-classpath/fileA"), AbsoluteUnixPath.get("/hello/different-classpath/fileB")); - Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildContext, "extra files")); } @Test public void testToJibContainerBuilder_missingAndMultipleAdds() throws InvalidImageReferenceException, URISyntaxException, IOException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = + BuildContext buildContext = JavaContainerBuilder.fromDistroless() .addDependencies(getResource("core/application/dependencies/libraryA.jar")) .addDependencies(getResource("core/application/dependencies/libraryB.jar")) @@ -156,10 +153,10 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() .addClasses(getResource("core/class-finder-tests/extension")) .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint - ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); Assert.assertNotNull(containerConfiguration); Assert.assertEquals( ImmutableList.of("java", "-cp", "/app/libs/*:/app/classes", "HelloWorld"), @@ -170,15 +167,13 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() ImmutableList.of( AbsoluteUnixPath.get("/app/libs/libraryA.jar"), AbsoluteUnixPath.get("/app/libs/libraryB.jar")); - Assert.assertEquals( - expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); // Check snapshots List expectedSnapshotDependencies = ImmutableList.of(AbsoluteUnixPath.get("/app/libs/dependency-1.0.0-SNAPSHOT.jar")); Assert.assertEquals( - expectedSnapshotDependencies, - getExtractionPaths(buildConfiguration, "snapshot dependencies")); + expectedSnapshotDependencies, getExtractionPaths(buildContext, "snapshot dependencies")); // Check classes List expectedClasses = @@ -190,18 +185,18 @@ public void testToJibContainerBuilder_missingAndMultipleAdds() AbsoluteUnixPath.get("/app/classes/pack/"), AbsoluteUnixPath.get("/app/classes/pack/Apple.class"), AbsoluteUnixPath.get("/app/classes/pack/Orange.class")); - Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); // Check empty layers - Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildConfiguration, "resources")); - Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildConfiguration, "extra files")); + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildContext, "resources")); + Assert.assertEquals(ImmutableList.of(), getExtractionPaths(buildContext, "extra files")); } @Test public void testToJibContainerBuilder_setAppRootLate() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration buildConfiguration = + BuildContext buildContext = JavaContainerBuilder.fromDistroless() .addClasses(getResource("core/application/classes")) .addResources(getResource("core/application/resources")) @@ -210,10 +205,10 @@ public void testToJibContainerBuilder_setAppRootLate() .setAppRoot("/different") .setMainClass("HelloWorld") .toContainerBuilder() - .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); // Check entrypoint - ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); Assert.assertNotNull(containerConfiguration); Assert.assertEquals( ImmutableList.of( @@ -228,7 +223,7 @@ public void testToJibContainerBuilder_setAppRootLate() ImmutableList.of( AbsoluteUnixPath.get("/different/classes/HelloWorld.class"), AbsoluteUnixPath.get("/different/classes/some.class")); - Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); // Check resources List expectedResources = @@ -236,33 +231,32 @@ public void testToJibContainerBuilder_setAppRootLate() AbsoluteUnixPath.get("/different/resources/resourceA"), AbsoluteUnixPath.get("/different/resources/resourceB"), AbsoluteUnixPath.get("/different/resources/world")); - Assert.assertEquals(expectedResources, getExtractionPaths(buildConfiguration, "resources")); + Assert.assertEquals(expectedResources, getExtractionPaths(buildContext, "resources")); // Check dependencies List expectedDependencies = ImmutableList.of(AbsoluteUnixPath.get("/different/libs/libraryA.jar")); - Assert.assertEquals( - expectedDependencies, getExtractionPaths(buildConfiguration, "dependencies")); + Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); - Assert.assertEquals(expectedClasses, getExtractionPaths(buildConfiguration, "classes")); + Assert.assertEquals(expectedClasses, getExtractionPaths(buildContext, "classes")); // Check additional classpath files List expectedOthers = ImmutableList.of(AbsoluteUnixPath.get("/different/classpath/fileA")); - Assert.assertEquals(expectedOthers, getExtractionPaths(buildConfiguration, "extra files")); + Assert.assertEquals(expectedOthers, getExtractionPaths(buildContext, "extra files")); } @Test public void testToJibContainerBuilder_mainClassNull() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException, URISyntaxException { - BuildConfiguration buildConfiguration = + BuildContext buildContext = JavaContainerBuilder.fromDistroless() .addClasses(getResource("core/application/classes/")) .toContainerBuilder() - .toBuildConfiguration(Containerizer.to(RegistryImage.named("hello"))); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + .toBuildContext(Containerizer.to(RegistryImage.named("hello"))); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNull(buildContext.getContainerConfiguration().getEntrypoint()); try { JavaContainerBuilder.fromDistroless().addJvmFlags("-flag1", "-flag2").toContainerBuilder(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index aa68f2afa5..3a41704f5b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -16,7 +16,7 @@ package com.google.cloud.tools.jib.api; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; @@ -44,7 +44,7 @@ @RunWith(MockitoJUnitRunner.class) public class JibContainerBuilderTest { - @Spy private BuildConfiguration.Builder spyBuildConfigurationBuilder; + @Spy private BuildContext.Builder spyBuildContextBuilder; @Mock private LayerConfiguration mockLayerConfiguration1; @Mock private LayerConfiguration mockLayerConfiguration2; @Mock private CredentialRetriever mockCredentialRetriever; @@ -52,12 +52,12 @@ public class JibContainerBuilderTest { @Mock private JibEvent mockJibEvent; @Test - public void testToBuildConfiguration_containerConfigurationSet() + public void testToBuildContext_containerConfigurationSet() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { ImageConfiguration imageConfiguration = ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) .setEntrypoint(Arrays.asList("entry", "point")) .setEnvironment(ImmutableMap.of("name", "value")) .setExposedPorts(ImmutableSet.of(Port.tcp(1234), Port.udp(5678))) @@ -67,10 +67,9 @@ public void testToBuildConfiguration_containerConfigurationSet() .setUser("user") .setWorkingDirectory(AbsoluteUnixPath.get("/working/directory")); - BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration( - Containerizer.to(RegistryImage.named("target/image"))); - ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + BuildContext buildContext = + jibContainerBuilder.toBuildContext(Containerizer.to(RegistryImage.named("target/image"))); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( ImmutableMap.of("name", "value"), containerConfiguration.getEnvironmentMap()); @@ -86,12 +85,12 @@ public void testToBuildConfiguration_containerConfigurationSet() } @Test - public void testToBuildConfiguration_containerConfigurationAdd() + public void testToBuildContext_containerConfigurationAdd() throws InvalidImageReferenceException, CacheDirectoryCreationException, IOException { ImageConfiguration imageConfiguration = ImageConfiguration.builder(ImageReference.parse("base/image")).build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(imageConfiguration, spyBuildConfigurationBuilder) + new JibContainerBuilder(imageConfiguration, spyBuildContextBuilder) .setEntrypoint("entry", "point") .setEnvironment(ImmutableMap.of("name", "value")) .addEnvironmentVariable("environment", "variable") @@ -101,10 +100,9 @@ public void testToBuildConfiguration_containerConfigurationAdd() .addLabel("added", "label") .setProgramArguments("program", "arguments"); - BuildConfiguration buildConfiguration = - jibContainerBuilder.toBuildConfiguration( - Containerizer.to(RegistryImage.named("target/image"))); - ContainerConfiguration containerConfiguration = buildConfiguration.getContainerConfiguration(); + BuildContext buildContext = + jibContainerBuilder.toBuildContext(Containerizer.to(RegistryImage.named("target/image"))); + ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); Assert.assertEquals(Arrays.asList("entry", "point"), containerConfiguration.getEntrypoint()); Assert.assertEquals( ImmutableMap.of("name", "value", "environment", "variable"), @@ -120,7 +118,7 @@ public void testToBuildConfiguration_containerConfigurationAdd() } @Test - public void testToBuildConfiguration() + public void testToBuildContext() throws InvalidImageReferenceException, CredentialRetrievalException, IOException, CacheDirectoryCreationException { ExecutorService executorService = MoreExecutors.newDirectExecutorService(); @@ -140,69 +138,69 @@ public void testToBuildConfiguration() .setCredentialRetrievers(Collections.singletonList(mockCredentialRetriever)) .build(); JibContainerBuilder jibContainerBuilder = - new JibContainerBuilder(baseImageConfiguration, spyBuildConfigurationBuilder) + new JibContainerBuilder(baseImageConfiguration, spyBuildContextBuilder) .setLayers(Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2)); - BuildConfiguration buildConfiguration = jibContainerBuilder.toBuildConfiguration(containerizer); + BuildContext buildContext = jibContainerBuilder.toBuildContext(containerizer); Assert.assertEquals( - spyBuildConfigurationBuilder.build().getContainerConfiguration(), - buildConfiguration.getContainerConfiguration()); + spyBuildContextBuilder.build().getContainerConfiguration(), + buildContext.getContainerConfiguration()); Assert.assertEquals( - "base/image", buildConfiguration.getBaseImageConfiguration().getImage().toString()); + "base/image", buildContext.getBaseImageConfiguration().getImage().toString()); Assert.assertEquals( Arrays.asList(mockCredentialRetriever), - buildConfiguration.getBaseImageConfiguration().getCredentialRetrievers()); + buildContext.getBaseImageConfiguration().getCredentialRetrievers()); Assert.assertEquals( "gcr.io/my-project/my-app", - buildConfiguration.getTargetImageConfiguration().getImage().toString()); + buildContext.getTargetImageConfiguration().getImage().toString()); Assert.assertEquals( - 1, buildConfiguration.getTargetImageConfiguration().getCredentialRetrievers().size()); + 1, buildContext.getTargetImageConfiguration().getCredentialRetrievers().size()); Assert.assertEquals( Credential.from("username", "password"), - buildConfiguration + buildContext .getTargetImageConfiguration() .getCredentialRetrievers() .get(0) .retrieve() .orElseThrow(AssertionError::new)); - Assert.assertEquals(ImmutableSet.of("latest"), buildConfiguration.getAllTargetImageTags()); + Assert.assertEquals(ImmutableSet.of("latest"), buildContext.getAllTargetImageTags()); - Mockito.verify(spyBuildConfigurationBuilder) + Mockito.verify(spyBuildContextBuilder) .setBaseImageLayersCacheDirectory(Paths.get("base/image/layers")); - Mockito.verify(spyBuildConfigurationBuilder) + Mockito.verify(spyBuildContextBuilder) .setApplicationLayersCacheDirectory(Paths.get("application/layers")); Assert.assertEquals( Arrays.asList(mockLayerConfiguration1, mockLayerConfiguration2), - buildConfiguration.getLayerConfigurations()); + buildContext.getLayerConfigurations()); - Assert.assertSame(executorService, buildConfiguration.getExecutorService()); + Assert.assertSame(executorService, buildContext.getExecutorService()); - buildConfiguration.getEventHandlers().dispatch(mockJibEvent); + buildContext.getEventHandlers().dispatch(mockJibEvent); Mockito.verify(mockJibEventConsumer).accept(mockJibEvent); - Assert.assertEquals("jib-core", buildConfiguration.getToolName()); + Assert.assertEquals("jib-core", buildContext.getToolName()); - Assert.assertSame(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); + Assert.assertSame(V22ManifestTemplate.class, buildContext.getTargetFormat()); - Assert.assertEquals("jib-core", buildConfiguration.getToolName()); + Assert.assertEquals("jib-core", buildContext.getToolName()); // Changes jibContainerBuilder. - buildConfiguration = + buildContext = jibContainerBuilder .setFormat(ImageFormat.OCI) - .toBuildConfiguration( + .toBuildContext( containerizer .withAdditionalTag("tag1") .withAdditionalTag("tag2") .setToolName("toolName")); - Assert.assertSame(OCIManifestTemplate.class, buildConfiguration.getTargetFormat()); + Assert.assertSame(OCIManifestTemplate.class, buildContext.getTargetFormat()); Assert.assertEquals( - ImmutableSet.of("latest", "tag1", "tag2"), buildConfiguration.getAllTargetImageTags()); - Assert.assertEquals("toolName", buildConfiguration.getToolName()); - Assert.assertFalse(buildConfiguration.getAlwaysCacheBaseImage()); + ImmutableSet.of("latest", "tag1", "tag2"), buildContext.getAllTargetImageTags()); + Assert.assertEquals("toolName", buildContext.getToolName()); + Assert.assertFalse(buildContext.getAlwaysCacheBaseImage()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java index cc7d3c9bf9..5ce9970387 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -83,7 +83,7 @@ private static void assertBlobsEqual(Blob expectedBlob, Blob blob) throws IOExce @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private BuildContext mockBuildContext; private Cache cache; @@ -120,8 +120,8 @@ public void setUp() throws IOException, URISyntaxException { cache = Cache.withDirectory(temporaryFolder.newFolder().toPath()); - Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); - Mockito.when(mockBuildConfiguration.getApplicationLayersCache()).thenReturn(cache); + Mockito.when(mockBuildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(mockBuildContext.getApplicationLayersCache()).thenReturn(cache); } private List buildFakeLayersToCache() @@ -130,7 +130,7 @@ private List buildFakeLayersToCache() ImmutableList buildAndCacheApplicationLayerSteps = BuildAndCacheApplicationLayerStep.makeList( - mockBuildConfiguration, + mockBuildContext, ProgressEventDispatcher.newRoot(EventHandlers.NONE, "ignored", 1).newChildProducer()); for (BuildAndCacheApplicationLayerStep buildAndCacheApplicationLayerStep : @@ -151,8 +151,7 @@ public void testRun() fakeResourcesLayerConfiguration, fakeClassesLayerConfiguration, fakeExtraFilesLayerConfiguration); - Mockito.when(mockBuildConfiguration.getLayerConfigurations()) - .thenReturn(fakeLayerConfigurations); + Mockito.when(mockBuildContext.getLayerConfigurations()).thenReturn(fakeLayerConfigurations); // Populates the cache. List applicationLayers = buildFakeLayersToCache(); @@ -212,8 +211,7 @@ public void testRun_emptyLayersIgnored() throws IOException, CacheCorruptedExcep fakeResourcesLayerConfiguration, fakeClassesLayerConfiguration, emptyLayerConfiguration); - Mockito.when(mockBuildConfiguration.getLayerConfigurations()) - .thenReturn(fakeLayerConfigurations); + Mockito.when(mockBuildContext.getLayerConfigurations()).thenReturn(fakeLayerConfigurations); // Populates the cache. List applicationLayers = buildFakeLayersToCache(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java index c0d7040c5e..975f0c499e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.DockerHealthCheck; import com.google.cloud.tools.jib.event.EventHandlers; @@ -50,7 +50,7 @@ public class BuildImageStepTest { @Mock private ProgressEventDispatcher.Factory mockProgressEventDispatcherFactory; - @Mock private BuildConfiguration mockBuildConfiguration; + @Mock private BuildContext mockBuildContext; @Mock private ContainerConfiguration mockContainerConfiguration; @Mock private CachedLayer mockCachedLayer; @@ -68,10 +68,10 @@ public void setUp() throws DigestException { DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - Mockito.when(mockBuildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); - Mockito.when(mockBuildConfiguration.getContainerConfiguration()) + Mockito.when(mockBuildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(mockBuildContext.getContainerConfiguration()) .thenReturn(mockContainerConfiguration); - Mockito.when(mockBuildConfiguration.getToolName()).thenReturn("jib"); + Mockito.when(mockBuildContext.getToolName()).thenReturn("jib"); Mockito.when(mockContainerConfiguration.getCreationTime()).thenReturn(Instant.EPOCH); Mockito.when(mockContainerConfiguration.getEnvironmentMap()).thenReturn(ImmutableMap.of()); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(ImmutableList.of()); @@ -137,7 +137,7 @@ public void setUp() throws DigestException { public void test_basicCase() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -161,7 +161,7 @@ public void test_propagateBaseImageConfiguration() { AbsoluteUnixPath.get("/new/path1"), AbsoluteUnixPath.get("/new/path2"))); Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -220,7 +220,7 @@ public void testOverrideWorkingDirectory() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -238,7 +238,7 @@ public void test_inheritedEntrypoint() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -256,7 +256,7 @@ public void test_inheritedEntrypointAndProgramArguments() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -275,7 +275,7 @@ public void test_notInheritedProgramArguments() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, @@ -290,7 +290,7 @@ public void test_notInheritedProgramArguments() { public void test_generateHistoryObjects() { Image image = new BuildImageStep( - mockBuildConfiguration, + mockBuildContext, mockProgressEventDispatcherFactory, baseImage, baseImageLayers, diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index a7f67e1e79..eb23a1b7fd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -51,7 +51,7 @@ public class LocalBaseImageStepsTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Mock private BuildConfiguration buildConfiguration; + @Mock private BuildContext buildContext; @Mock private EventHandlers eventHandlers; @Mock private ProgressEventDispatcher.Factory progressEventDispatcherFactory; @Mock private ProgressEventDispatcher progressEventDispatcher; @@ -64,11 +64,11 @@ private static Path getResource(String resource) throws URISyntaxException { @Before public void setup() throws IOException { - Mockito.when(buildConfiguration.getExecutorService()) + Mockito.when(buildContext.getExecutorService()) .thenReturn(MoreExecutors.newDirectExecutorService()); - Mockito.when(buildConfiguration.getBaseImageLayersCache()) + Mockito.when(buildContext.getBaseImageLayersCache()) .thenReturn(Cache.withDirectory(temporaryFolder.newFolder().toPath())); - Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(eventHandlers); + Mockito.when(buildContext.getEventHandlers()).thenReturn(eventHandlers); Mockito.when(progressEventDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) .thenReturn(progressEventDispatcher); Mockito.when(progressEventDispatcher.newChildProducer()).thenReturn(childFactory); @@ -81,7 +81,7 @@ public void testCacheDockerImageTar_validDocker() throws Exception { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildConfiguration, dockerBuild, progressEventDispatcherFactory); + buildContext, dockerBuild, progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); @@ -105,7 +105,7 @@ public void testCacheDockerImageTar_validTar() throws Exception { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildConfiguration, tarBuild, progressEventDispatcherFactory); + buildContext, tarBuild, progressEventDispatcherFactory); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java index c03516e9b8..cca6b8d8b7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.image.ReferenceLayer; @@ -58,7 +58,7 @@ public class ObtainBaseImageLayerStepTest { @Mock private RegistryClient registryClient; @Mock(answer = Answers.RETURNS_MOCKS) - private BuildConfiguration buildConfiguration; + private BuildContext buildContext; @Mock(answer = Answers.RETURNS_MOCKS) private ProgressEventDispatcher.Factory progressDispatcherFactory; @@ -88,9 +88,9 @@ public void setUp() throws IOException, RegistryException, DigestException { Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); Mockito.lenient() - .when(buildConfiguration.newBaseImageRegistryClientFactory()) + .when(buildContext.newBaseImageRegistryClientFactory()) .thenReturn(registryClientFactory); - Mockito.when(buildConfiguration.newTargetImageRegistryClientFactory()) + Mockito.when(buildContext.newTargetImageRegistryClientFactory()) .thenReturn(registryClientFactory); // necessary to prevent error from classes dealing with progress report @@ -108,7 +108,7 @@ public void testMakeListForSelectiveDownload() throws IOException, CacheCorruptedException, RegistryException { ImmutableList pullers = ObtainBaseImageLayerStep.makeListForSelectiveDownload( - buildConfiguration, progressDispatcherFactory, baseImageAndAuth, null); + buildContext, progressDispatcherFactory, baseImageAndAuth, null); Assert.assertEquals(2, pullers.size()); PreparedLayer preparedExistingLayer = pullers.get(0).call(); @@ -133,7 +133,7 @@ public void testMakeListForForcedDownload() throws IOException, CacheCorruptedException, RegistryException { ImmutableList pullers = ObtainBaseImageLayerStep.makeListForForcedDownload( - buildConfiguration, progressDispatcherFactory, baseImageAndAuth); + buildContext, progressDispatcherFactory, baseImageAndAuth); Assert.assertEquals(2, pullers.size()); PreparedLayer preparedExistingLayer = pullers.get(0).call(); @@ -157,11 +157,11 @@ public void testMakeListForForcedDownload() @Test public void testLayerMissingInCacheInOfflineMode() throws CacheCorruptedException, RegistryException { - Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + Mockito.when(buildContext.isOffline()).thenReturn(true); ImmutableList pullers = ObtainBaseImageLayerStep.makeListForForcedDownload( - buildConfiguration, progressDispatcherFactory, baseImageAndAuth); + buildContext, progressDispatcherFactory, baseImageAndAuth); try { pullers.get(1).call(); Assert.fail(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java index 38e88f9ba6..4a266c67d3 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java @@ -23,7 +23,7 @@ import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.LayerCountMismatchException; @@ -53,7 +53,7 @@ public class PullBaseImageStepTest { new ManifestAndConfig(new V22ManifestTemplate(), containerConfig); @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; - @Mock private BuildConfiguration buildConfiguration; + @Mock private BuildContext buildContext; @Mock private ImageConfiguration imageConfiguration; @Mock private Cache cache; @@ -63,11 +63,11 @@ public class PullBaseImageStepTest { public void setUp() { containerConfig.setOs("fat system"); - Mockito.when(buildConfiguration.getBaseImageConfiguration()).thenReturn(imageConfiguration); - Mockito.when(buildConfiguration.getEventHandlers()).thenReturn(EventHandlers.NONE); - Mockito.when(buildConfiguration.getBaseImageLayersCache()).thenReturn(cache); + Mockito.when(buildContext.getBaseImageConfiguration()).thenReturn(imageConfiguration); + Mockito.when(buildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); + Mockito.when(buildContext.getBaseImageLayersCache()).thenReturn(cache); - pullBaseImageStep = new PullBaseImageStep(buildConfiguration, progressDispatcherFactory); + pullBaseImageStep = new PullBaseImageStep(buildContext, progressDispatcherFactory); } @Test @@ -86,7 +86,7 @@ public void testCall_digestBaseImage() Assert.assertEquals("fat system", result.getImage().getOs()); Assert.assertNull(result.getAuthorization()); - Mockito.verify(buildConfiguration, Mockito.never()).newBaseImageRegistryClientFactory(); + Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(); } @Test @@ -95,7 +95,7 @@ public void testCall_offlineMode_notCached() BadContainerConfigurationFormatException, CacheCorruptedException, CredentialRetrievalException, InvalidImageReferenceException { Mockito.when(imageConfiguration.getImage()).thenReturn(ImageReference.parse("cat")); - Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + Mockito.when(buildContext.isOffline()).thenReturn(true); try { pullBaseImageStep.call(); @@ -113,13 +113,13 @@ public void testCall_offlineMode_cached() CredentialRetrievalException, InvalidImageReferenceException, IOException { ImageReference imageReference = ImageReference.parse("cat"); Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); - Mockito.when(buildConfiguration.isOffline()).thenReturn(true); + Mockito.when(buildContext.isOffline()).thenReturn(true); Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(manifestAndConfig)); ImageAndAuthorization result = pullBaseImageStep.call(); Assert.assertEquals("fat system", result.getImage().getOs()); Assert.assertNull(result.getAuthorization()); - Mockito.verify(buildConfiguration, Mockito.never()).newBaseImageRegistryClientFactory(); + Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java index 514cc4b674..d87e857f26 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; @@ -44,7 +44,7 @@ public class PushBlobStepTest { private ProgressEventDispatcher.Factory progressDispatcherFactory; @Mock(answer = Answers.RETURNS_MOCKS) - private BuildConfiguration buildConfiguration; + private BuildContext buildContext; @Before public void setUp() { @@ -52,9 +52,9 @@ public void setUp() { Mockito.mock(RegistryClient.Factory.class, Answers.RETURNS_SELF); Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); - Mockito.when(buildConfiguration.newTargetImageRegistryClientFactory()) + Mockito.when(buildContext.newTargetImageRegistryClientFactory()) .thenReturn(registryClientFactory); - Mockito.when(buildConfiguration.getTargetImageConfiguration()) + Mockito.when(buildContext.getTargetImageConfiguration()) .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).build()); } @@ -90,8 +90,7 @@ public void testCall_forcePushWithNoBlobCheck() throws IOException, RegistryExce } private void call(boolean forcePush) throws IOException, RegistryException { - new PushBlobStep( - buildConfiguration, progressDispatcherFactory, null, blobDescriptor, null, forcePush) + new PushBlobStep(buildContext, progressDispatcherFactory, null, blobDescriptor, null, forcePush) .call(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java index 66a4049cb0..978b192479 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java @@ -21,7 +21,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; @@ -48,8 +48,8 @@ public class RetrieveRegistryCredentialsStepTest { @Test public void testCall_retrieved() throws CredentialRetrievalException, IOException { - BuildConfiguration buildConfiguration = - makeFakeBuildConfiguration( + BuildContext buildContext = + makeFakeBuildContext( Arrays.asList( Optional::empty, () -> Optional.of(Credential.from("baseusername", "basepassword"))), @@ -60,25 +60,25 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio Assert.assertEquals( Optional.of(Credential.from("baseusername", "basepassword")), RetrieveRegistryCredentialsStep.forBaseImage( - buildConfiguration, + buildContext, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); Assert.assertEquals( Optional.of(Credential.from("targetusername", "targetpassword")), RetrieveRegistryCredentialsStep.forTargetImage( - buildConfiguration, + buildContext, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call()); } @Test public void testCall_none() throws CredentialRetrievalException, IOException { - BuildConfiguration buildConfiguration = - makeFakeBuildConfiguration( + BuildContext buildContext = + makeFakeBuildContext( Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertFalse( RetrieveRegistryCredentialsStep.forBaseImage( - buildConfiguration, + buildContext, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call() .isPresent()); @@ -90,7 +90,7 @@ public void testCall_none() throws CredentialRetrievalException, IOException { Assert.assertFalse( RetrieveRegistryCredentialsStep.forTargetImage( - buildConfiguration, + buildContext, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call() .isPresent()); @@ -103,8 +103,8 @@ public void testCall_none() throws CredentialRetrievalException, IOException { public void testCall_exception() throws IOException { CredentialRetrievalException credentialRetrievalException = Mockito.mock(CredentialRetrievalException.class); - BuildConfiguration buildConfiguration = - makeFakeBuildConfiguration( + BuildContext buildContext = + makeFakeBuildContext( Collections.singletonList( () -> { throw credentialRetrievalException; @@ -112,7 +112,7 @@ public void testCall_exception() throws IOException { Collections.emptyList()); try { RetrieveRegistryCredentialsStep.forBaseImage( - buildConfiguration, + buildContext, ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) .call(); Assert.fail("Should have thrown exception"); @@ -122,13 +122,13 @@ public void testCall_exception() throws IOException { } } - private BuildConfiguration makeFakeBuildConfiguration( + private BuildContext makeFakeBuildContext( List baseCredentialRetrievers, List targetCredentialRetrievers) throws IOException { ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); - return BuildConfiguration.builder() + return BuildContext.builder() .setEventHandlers(mockEventHandlers) .setBaseImageConfiguration( ImageConfiguration.builder(baseImage) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java similarity index 72% rename from jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java index 751c557292..3dbb4ddb36 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildConfigurationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java @@ -47,8 +47,8 @@ import org.junit.Test; import org.mockito.Mockito; -/** Tests for {@link BuildConfiguration}. */ -public class BuildConfigurationTest { +/** Tests for {@link BuildContext}. */ +public class BuildContextTest { @Test public void testBuilder() throws Exception { @@ -98,8 +98,8 @@ public void testBuilder() throws Exception { .setExposedPorts(expectedExposedPorts) .setLabels(expectedLabels) .build(); - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder() + BuildContext.Builder buildContextBuilder = + BuildContext.builder() .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setAdditionalTargetImageTags(additionalTargetImageTags) @@ -110,55 +110,51 @@ public void testBuilder() throws Exception { .setAllowInsecureRegistries(true) .setLayerConfigurations(expectedLayerConfigurations) .setToolName(expectedCreatedBy); - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + BuildContext buildContext = buildContextBuilder.build(); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( - expectedCreationTime, buildConfiguration.getContainerConfiguration().getCreationTime()); + expectedCreationTime, buildContext.getContainerConfiguration().getCreationTime()); Assert.assertEquals( - expectedBaseImageServerUrl, - buildConfiguration.getBaseImageConfiguration().getImageRegistry()); + expectedBaseImageServerUrl, buildContext.getBaseImageConfiguration().getImageRegistry()); Assert.assertEquals( - expectedBaseImageName, buildConfiguration.getBaseImageConfiguration().getImageRepository()); + expectedBaseImageName, buildContext.getBaseImageConfiguration().getImageRepository()); Assert.assertEquals( - expectedBaseImageTag, buildConfiguration.getBaseImageConfiguration().getImageTag()); + expectedBaseImageTag, buildContext.getBaseImageConfiguration().getImageTag()); Assert.assertEquals( - expectedTargetServerUrl, - buildConfiguration.getTargetImageConfiguration().getImageRegistry()); + expectedTargetServerUrl, buildContext.getTargetImageConfiguration().getImageRegistry()); Assert.assertEquals( - expectedTargetImageName, - buildConfiguration.getTargetImageConfiguration().getImageRepository()); + expectedTargetImageName, buildContext.getTargetImageConfiguration().getImageRepository()); Assert.assertEquals( - expectedTargetTag, buildConfiguration.getTargetImageConfiguration().getImageTag()); - Assert.assertEquals(expectedTargetImageTags, buildConfiguration.getAllTargetImageTags()); + expectedTargetTag, buildContext.getTargetImageConfiguration().getImageTag()); + Assert.assertEquals(expectedTargetImageTags, buildContext.getAllTargetImageTags()); Assert.assertEquals( Credential.from("username", "password"), - buildConfiguration + buildContext .getTargetImageConfiguration() .getCredentialRetrievers() .get(0) .retrieve() .orElseThrow(AssertionError::new)); Assert.assertEquals( - expectedProgramArguments, - buildConfiguration.getContainerConfiguration().getProgramArguments()); + expectedProgramArguments, buildContext.getContainerConfiguration().getProgramArguments()); Assert.assertEquals( - expectedEnvironment, buildConfiguration.getContainerConfiguration().getEnvironmentMap()); + expectedEnvironment, buildContext.getContainerConfiguration().getEnvironmentMap()); Assert.assertEquals( - expectedExposedPorts, buildConfiguration.getContainerConfiguration().getExposedPorts()); - Assert.assertEquals(expectedLabels, buildConfiguration.getContainerConfiguration().getLabels()); - Assert.assertEquals(expectedTargetFormat, buildConfiguration.getTargetFormat()); + expectedExposedPorts, buildContext.getContainerConfiguration().getExposedPorts()); + Assert.assertEquals(expectedLabels, buildContext.getContainerConfiguration().getLabels()); + Assert.assertEquals(expectedTargetFormat, buildContext.getTargetFormat()); Assert.assertEquals( expectedApplicationLayersCacheDirectory, - buildConfigurationBuilder.getApplicationLayersCacheDirectory()); + buildContextBuilder.getApplicationLayersCacheDirectory()); Assert.assertEquals( expectedBaseImageLayersCacheDirectory, - buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); - Assert.assertEquals(expectedLayerConfigurations, buildConfiguration.getLayerConfigurations()); + buildContextBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertEquals(expectedLayerConfigurations, buildContext.getLayerConfigurations()); Assert.assertEquals( - expectedEntrypoint, buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertEquals(expectedCreatedBy, buildConfiguration.getToolName()); - Assert.assertNotNull(buildConfiguration.getExecutorService()); + expectedEntrypoint, buildContext.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals(expectedCreatedBy, buildContext.getToolName()); + Assert.assertNotNull(buildContext.getExecutorService()); } @Test @@ -181,38 +177,38 @@ public void testBuilder_default() throws IOException { ImageReference.of( expectedTargetServerUrl, expectedTargetImageName, expectedTargetTag)) .build(); - BuildConfiguration.Builder buildConfigurationBuilder = - BuildConfiguration.builder() + BuildContext.Builder buildContextBuilder = + BuildContext.builder() .setBaseImageConfiguration(baseImageConfiguration) .setTargetImageConfiguration(targetImageConfiguration) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")); - BuildConfiguration buildConfiguration = buildConfigurationBuilder.build(); + BuildContext buildContext = buildContextBuilder.build(); - Assert.assertEquals(ImmutableSet.of("targettag"), buildConfiguration.getAllTargetImageTags()); - Assert.assertEquals(V22ManifestTemplate.class, buildConfiguration.getTargetFormat()); - Assert.assertNotNull(buildConfigurationBuilder.getApplicationLayersCacheDirectory()); + Assert.assertEquals(ImmutableSet.of("targettag"), buildContext.getAllTargetImageTags()); + Assert.assertEquals(V22ManifestTemplate.class, buildContext.getTargetFormat()); + Assert.assertNotNull(buildContextBuilder.getApplicationLayersCacheDirectory()); Assert.assertEquals( - Paths.get("ignored"), buildConfigurationBuilder.getApplicationLayersCacheDirectory()); - Assert.assertNotNull(buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); + Paths.get("ignored"), buildContextBuilder.getApplicationLayersCacheDirectory()); + Assert.assertNotNull(buildContextBuilder.getBaseImageLayersCacheDirectory()); Assert.assertEquals( - Paths.get("ignored"), buildConfigurationBuilder.getBaseImageLayersCacheDirectory()); - Assert.assertNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals(Collections.emptyList(), buildConfiguration.getLayerConfigurations()); - Assert.assertEquals("jib", buildConfiguration.getToolName()); + Paths.get("ignored"), buildContextBuilder.getBaseImageLayersCacheDirectory()); + Assert.assertNull(buildContext.getContainerConfiguration()); + Assert.assertEquals(Collections.emptyList(), buildContext.getLayerConfigurations()); + Assert.assertEquals("jib", buildContext.getToolName()); } @Test public void testBuilder_missingValues() throws IOException { // Target image is missing try { - BuildConfiguration.builder() + BuildContext.builder() .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) .build(); - Assert.fail("Build configuration should not be built with missing values"); + Assert.fail("BuildContext should not be built with missing values"); } catch (IllegalStateException ex) { Assert.assertEquals("target image configuration is required but not set", ex.getMessage()); @@ -220,11 +216,11 @@ public void testBuilder_missingValues() throws IOException { // Two required fields missing try { - BuildConfiguration.builder() + BuildContext.builder() .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) .build(); - Assert.fail("Build configuration should not be built with missing values"); + Assert.fail("BuildContext should not be built with missing values"); } catch (IllegalStateException ex) { Assert.assertEquals( @@ -234,8 +230,8 @@ public void testBuilder_missingValues() throws IOException { // All required fields missing try { - BuildConfiguration.builder().build(); - Assert.fail("Build configuration should not be built with missing values"); + BuildContext.builder().build(); + Assert.fail("BuildContext should not be built with missing values"); } catch (IllegalStateException ex) { Assert.assertEquals( @@ -248,8 +244,8 @@ public void testBuilder_missingValues() throws IOException { @Test public void testBuilder_digestWarning() throws IOException, InvalidImageReferenceException { EventHandlers mockEventHandlers = Mockito.mock(EventHandlers.class); - BuildConfiguration.Builder builder = - BuildConfiguration.builder() + BuildContext.Builder builder = + BuildContext.builder() .setEventHandlers(mockEventHandlers) .setTargetImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) @@ -277,8 +273,8 @@ public void testBuilder_digestWarning() throws IOException, InvalidImageReferenc @Test public void testClose_shutDownInternalExecutorService() throws IOException { - BuildConfiguration buildConfiguration = - BuildConfiguration.builder() + BuildContext buildContext = + BuildContext.builder() .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setTargetImageConfiguration( @@ -286,16 +282,16 @@ public void testClose_shutDownInternalExecutorService() throws IOException { .setBaseImageLayersCacheDirectory(Paths.get("ignored")) .setApplicationLayersCacheDirectory(Paths.get("ignored")) .build(); - buildConfiguration.close(); + buildContext.close(); - Assert.assertTrue(buildConfiguration.getExecutorService().isShutdown()); + Assert.assertTrue(buildContext.getExecutorService().isShutdown()); } @Test public void testClose_doNotShutDownProvidedExecutorService() throws IOException { ExecutorService executorService = MoreExecutors.newDirectExecutorService(); - BuildConfiguration buildConfiguration = - BuildConfiguration.builder() + BuildContext buildContext = + BuildContext.builder() .setBaseImageConfiguration( ImageConfiguration.builder(Mockito.mock(ImageReference.class)).build()) .setTargetImageConfiguration( @@ -304,9 +300,9 @@ public void testClose_doNotShutDownProvidedExecutorService() throws IOException .setApplicationLayersCacheDirectory(Paths.get("ignored")) .setExecutorService(executorService) .build(); - buildConfiguration.close(); + buildContext.close(); - Assert.assertSame(executorService, buildConfiguration.getExecutorService()); - Assert.assertFalse(buildConfiguration.getExecutorService().isShutdown()); + Assert.assertSame(executorService, buildContext.getExecutorService()); + Assert.assertFalse(buildContext.getExecutorService().isShutdown()); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 081ce9681b..01fddeaa60 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -28,7 +28,7 @@ import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; @@ -122,7 +122,7 @@ public TaskDependency getBuildDependencies() { } } - /** Helper for reading back layers in a {@code BuildConfiguration}. */ + /** Helper for reading back layers in a {@link buildContext}. */ private static class ContainerBuilderLayers { private final List resourcesLayerEntries; @@ -130,21 +130,20 @@ private static class ContainerBuilderLayers { private final List dependenciesLayerEntries; private final List snapshotsLayerEntries; - private ContainerBuilderLayers(BuildConfiguration configuration) { + private ContainerBuilderLayers(BuildContext buildContext) { resourcesLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); - classesLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.RESOURCES.getName()); + classesLayerEntries = getLayerConfigurationsByName(buildContext, LayerType.CLASSES.getName()); dependenciesLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.DEPENDENCIES.getName()); snapshotsLayerEntries = - getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.SNAPSHOT_DEPENDENCIES.getName()); } } private static List getLayerConfigurationsByName( - BuildConfiguration buildConfiguration, String name) { - return buildConfiguration + BuildContext buildContext, String name) { + return buildContext .getLayerConfigurations() .stream() .filter(layer -> layer.getName().equals(name)) @@ -334,8 +333,8 @@ public void testGetMajorJavaVersion_jvm11() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = setupBuildConfiguration("/app", DEFAULT_CONTAINERIZING_MODE); - ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + BuildContext buildContext = setupBuildContext("/app", DEFAULT_CONTAINERIZING_MODE); + ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); Path applicationDirectory = getResource("gradle/application"); assertSourcePathsUnordered( @@ -382,9 +381,8 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = - setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); - ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertExtractionPathsUnordered( Arrays.asList( @@ -412,9 +410,9 @@ public void testCreateContainerBuilder_nonDefaultAppRoot() @Test public void testCreateContainerBuilder_defaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildConfiguration configuration = - setupBuildConfiguration(JavaContainerBuilder.DEFAULT_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); - ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + BuildContext buildContext = + setupBuildContext(JavaContainerBuilder.DEFAULT_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); + ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertExtractionPathsUnordered( Arrays.asList( "/app/libs/dependency-1.0.0-770.jar", @@ -443,9 +441,8 @@ public void testCreateContainerBuilder_war() Path webAppDirectory = getResource("gradle/webapp"); Path unzipTarget = setUpWarProject(webAppDirectory); - BuildConfiguration configuration = - setupBuildConfiguration("/my/app", DEFAULT_CONTAINERIZING_MODE); - ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertSourcePathsUnordered( ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), layers.dependenciesLayerEntries.get(0).getLayerEntries()); @@ -507,10 +504,9 @@ public void testCreateContainerBuilder_defaultWebAppRoot() CacheDirectoryCreationException { Path unzipTarget = setUpWarProject(getResource("gradle/webapp")); - BuildConfiguration configuration = - setupBuildConfiguration( - JavaContainerBuilder.DEFAULT_WEB_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); - ContainerBuilderLayers layers = new ContainerBuilderLayers(configuration); + BuildContext buildContext = + setupBuildContext(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); + ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertSourcePathsUnordered( ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), layers.dependenciesLayerEntries.get(0).getLayerEntries()); @@ -544,7 +540,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("WEB-INF", "lib"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -552,14 +548,14 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("WEB-INF", "classes"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildConfiguration("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -598,8 +594,7 @@ public void testGetWarFilePath_bootWarDisabled() { Assert.assertEquals("war.war", gradleProjectProperties.getWarFilePath()); } - private BuildConfiguration setupBuildConfiguration( - String appRoot, ContainerizingMode containerizingMode) + private BuildContext setupBuildContext(String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder.from(RegistryImage.named("base")) @@ -608,7 +603,7 @@ private BuildConfiguration setupBuildConfiguration( JibContainerBuilder jibContainerBuilder = new GradleProjectProperties(mockProject, mockLogger, mockTempDirectoryProvider) .createJibContainerBuilder(javaContainerBuilder, containerizingMode); - return JibContainerBuilderTestHelper.toBuildConfiguration( + return JibContainerBuilderTestHelper.toBuildContext( jibContainerBuilder, Containerizer.to(RegistryImage.named("to"))); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index f2ecd1535e..49a3c4d293 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -27,7 +27,7 @@ import com.google.cloud.tools.jib.api.LayerConfiguration; import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; @@ -80,7 +80,7 @@ public class MavenProjectPropertiesTest { private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; private static final Instant SAMPLE_FILE_MODIFICATION_TIME = Instant.ofEpochSecond(32); - /** Helper for reading back layers in a {@code BuildConfiguration}. */ + /** Helper for reading back layers in a {@link buildContext}. */ private static class ContainerBuilderLayers { private final List resourcesLayers; @@ -89,21 +89,21 @@ private static class ContainerBuilderLayers { private final List snapshotsLayers; private final List extraFilesLayers; - private ContainerBuilderLayers(BuildConfiguration configuration) { - resourcesLayers = getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); - classesLayers = getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + private ContainerBuilderLayers(BuildContext buildContext) { + resourcesLayers = getLayerConfigurationsByName(buildContext, LayerType.RESOURCES.getName()); + classesLayers = getLayerConfigurationsByName(buildContext, LayerType.CLASSES.getName()); dependenciesLayers = - getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.DEPENDENCIES.getName()); snapshotsLayers = - getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.SNAPSHOT_DEPENDENCIES.getName()); extraFilesLayers = - getLayerConfigurationsByName(configuration, LayerType.EXTRA_FILES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.EXTRA_FILES.getName()); } } private static List getLayerConfigurationsByName( - BuildConfiguration buildConfiguration, String name) { - return buildConfiguration + BuildContext buildContext, String name) { + return buildContext .getLayerConfigurations() .stream() .filter(layer -> layer.getName().equals(name)) @@ -137,8 +137,8 @@ private static void assertModificationTime(Instant instant, List getLayerConfigurationsByName( - BuildConfiguration buildConfiguration, String name) { - return buildConfiguration + BuildContext buildContext, String name) { + return buildContext .getLayerConfigurations() .stream() .filter(layer -> layer.getName().equals(name)) @@ -114,20 +114,20 @@ public void testFromExplodedWar() .setAppRoot(AbsoluteUnixPath.get("/my/app")); JibContainerBuilder jibContainerBuilder = JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, temporaryExplodedWar); - BuildConfiguration configuration = - JibContainerBuilderTestHelper.toBuildConfiguration( + BuildContext buildContext = + JibContainerBuilderTestHelper.toBuildContext( jibContainerBuilder, Containerizer.to(RegistryImage.named("target")) .setExecutorService(MoreExecutors.newDirectExecutorService())); List resourcesLayerConfigurations = - getLayerConfigurationsByName(configuration, LayerType.RESOURCES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.RESOURCES.getName()); List classesLayerConfigurations = - getLayerConfigurationsByName(configuration, LayerType.CLASSES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.CLASSES.getName()); List dependenciesLayerConfigurations = - getLayerConfigurationsByName(configuration, LayerType.DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.DEPENDENCIES.getName()); List snapshotsLayerConfigurations = - getLayerConfigurationsByName(configuration, LayerType.SNAPSHOT_DEPENDENCIES.getName()); + getLayerConfigurationsByName(buildContext, LayerType.SNAPSHOT_DEPENDENCIES.getName()); assertSourcePathsUnordered( Collections.singletonList(temporaryExplodedWar.resolve("WEB-INF/lib/dependency-1.0.0.jar")), diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index e89bd1f852..34f18a2dae 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -28,7 +28,7 @@ import com.google.cloud.tools.jib.api.LayerEntry; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryImage; -import com.google.cloud.tools.jib.configuration.BuildConfiguration; +import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -64,9 +64,9 @@ @RunWith(MockitoJUnitRunner.class) public class PluginConfigurationProcessorTest { - private static BuildConfiguration getBuildConfiguration(JibContainerBuilder jibContainerBuilder) + private static BuildContext getBuildContext(JibContainerBuilder jibContainerBuilder) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { - return JibContainerBuilderTestHelper.toBuildConfiguration( + return JibContainerBuilderTestHelper.toBuildContext( jibContainerBuilder, Containerizer.to(RegistryImage.named("ignored"))); } @@ -137,12 +137,12 @@ public void testPluginConfigurationProcessor_defaults() InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verify(containerizer) .setBaseImageLayersCache(Containerizer.DEFAULT_BASE_CACHE_DIRECTORY); @@ -166,9 +166,9 @@ public void testPluginConfigurationProcessor_extraDirectory() .thenReturn( ImmutableMap.of(AbsoluteUnixPath.get("/foo"), FilePermissions.fromOctalString("123"))); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); List extraFiles = - buildConfiguration + buildContext .getLayerConfigurations() .stream() .filter(layer -> layer.getName().equals("extra files")) @@ -224,12 +224,12 @@ public void testEntrypoint() Mockito.when(rawConfiguration.getEntrypoint()) .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verifyNoInteractions(logger); } @@ -284,10 +284,10 @@ public void testEntrypoint_defaultWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(true); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNull(buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verifyNoInteractions(logger); } @@ -301,12 +301,12 @@ public void testEntrypoint_defaultNonWarPackaging() Mockito.when(rawConfiguration.getEntrypoint()).thenReturn(Optional.empty()); Mockito.when(projectProperties.isWarProject()).thenReturn(false); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); @@ -324,13 +324,13 @@ public void testEntrypoint_extraClasspathNonWarPackaging() .thenReturn(Collections.singletonList("/foo")); Mockito.when(projectProperties.isWarProject()).thenReturn(false); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList( "java", "-cp", "/foo:/app/resources:/app/classes:/app/libs/*", "java.lang.Object"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); ArgumentMatcher isLogWarn = logEvent -> logEvent.getLevel() == LogEvent.Level.WARN; Mockito.verify(logger, Mockito.never()).accept(Mockito.argThat(isLogWarn)); @@ -345,10 +345,10 @@ public void testUser() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getUser()).thenReturn(Optional.of("customUser")); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertEquals("customUser", buildConfiguration.getContainerConfiguration().getUser()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertEquals("customUser", buildContext.getContainerConfiguration().getUser()); } @Test @@ -358,10 +358,10 @@ public void testUser_null() InvalidContainerVolumeException, IncompatibleBaseImageJavaVersionException, NumberFormatException, InvalidContainerizingModeException, InvalidFilesModificationTimeException, InvalidCreationTimeException { - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getUser()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNull(buildContext.getContainerConfiguration().getUser()); } @Test @@ -375,12 +375,12 @@ public void testEntrypoint_warningOnJvmFlags() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getJvmFlags()).thenReturn(Collections.singletonList("jvmFlag")); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verify(projectProperties) .log( LogEvent.warn( @@ -398,12 +398,12 @@ public void testEntrypoint_warningOnMainclass() .thenReturn(Optional.of(Arrays.asList("custom", "entrypoint"))); Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); Assert.assertEquals( Arrays.asList("custom", "entrypoint"), - buildConfiguration.getContainerConfiguration().getEntrypoint()); + buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verify(projectProperties) .log( LogEvent.warn( @@ -420,10 +420,10 @@ public void testEntrypoint_warningOnMainclassForWar() Mockito.when(rawConfiguration.getMainClass()).thenReturn(Optional.of("java.util.Object")); Mockito.when(projectProperties.isWarProject()).thenReturn(true); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNull(buildContext.getContainerConfiguration().getEntrypoint()); Mockito.verify(projectProperties) .log(LogEvent.warn("mainClass, extraClasspath, and jvmFlags are ignored for WAR projects")); } @@ -437,17 +437,15 @@ public void testEntrypointClasspath_nonDefaultAppRoot() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(rawConfiguration.getAppRoot()).thenReturn("/my/app"); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); - Assert.assertEquals( - "java", buildConfiguration.getContainerConfiguration().getEntrypoint().get(0)); - Assert.assertEquals( - "-cp", buildConfiguration.getContainerConfiguration().getEntrypoint().get(1)); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNotNull(buildContext.getContainerConfiguration().getEntrypoint()); + Assert.assertEquals("java", buildContext.getContainerConfiguration().getEntrypoint().get(0)); + Assert.assertEquals("-cp", buildContext.getContainerConfiguration().getEntrypoint().get(1)); Assert.assertEquals( "/my/app/resources:/my/app/classes:/my/app/libs/*", - buildConfiguration.getContainerConfiguration().getEntrypoint().get(2)); + buildContext.getContainerConfiguration().getEntrypoint().get(2)); } @Test @@ -459,10 +457,10 @@ public void testWebAppEntrypoint_inheritedFromBaseImage() InvalidFilesModificationTimeException, InvalidCreationTimeException { Mockito.when(projectProperties.isWarProject()).thenReturn(true); - BuildConfiguration buildConfiguration = getBuildConfiguration(processCommonConfiguration()); + BuildContext buildContext = getBuildContext(processCommonConfiguration()); - Assert.assertNotNull(buildConfiguration.getContainerConfiguration()); - Assert.assertNull(buildConfiguration.getContainerConfiguration().getEntrypoint()); + Assert.assertNotNull(buildContext.getContainerConfiguration()); + Assert.assertNull(buildContext.getContainerConfiguration().getEntrypoint()); } @Test @@ -929,7 +927,7 @@ public void testGetCreationTime_invalidValue() { private ImageConfiguration getCommonImageConfiguration() throws IncompatibleBaseImageJavaVersionException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - return getBuildConfiguration( + return getBuildContext( PluginConfigurationProcessor.getJavaContainerBuilderWithBaseImage( rawConfiguration, projectProperties, inferredAuthProvider) .addClasses(temporaryFolder.getRoot().toPath()) From 129cf6bcc7315fc9c7ddad510bddfc6b195b8d21 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 13 Nov 2019 14:41:11 -0500 Subject: [PATCH 0828/2020] Allow pushing local base image layers immediately after compressing (#2144) --- jib-core/CHANGELOG.md | 2 + .../builder/steps/LocalBaseImageSteps.java | 212 +++++++++--------- .../jib/builder/steps/PushLayerStep.java | 2 +- .../tools/jib/builder/steps/StepsRunner.java | 30 +-- .../steps/LocalBaseImageStepsTest.java | 42 ++-- jib-gradle-plugin/CHANGELOG.md | 2 + jib-maven-plugin/CHANGELOG.md | 2 + 7 files changed, 152 insertions(+), 140 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 00150da1c3..dce7cf991e 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Each local base image layer is pushed immediately after being compressed, rather than waiting for all layers to finish compressing before starting to push. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java index ab58dfc9d8..0ea9216c5f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; @@ -34,11 +35,9 @@ import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; -import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; -import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.JsonToImageTranslator; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; @@ -46,6 +45,7 @@ import com.google.cloud.tools.jib.tar.TarExtractor; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; +import com.google.common.util.concurrent.Futures; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -55,7 +55,6 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.zip.GZIPInputStream; @@ -66,12 +65,13 @@ public class LocalBaseImageSteps { /** Contains an {@link Image} and its layers. * */ static class LocalImage { - final Image baseImage; - final List layers; + final List> layers; + final ContainerConfigurationTemplate configurationTemplate; - LocalImage(Image baseImage, List layers) { - this.baseImage = baseImage; + LocalImage( + List> layers, ContainerConfigurationTemplate configurationTemplate) { this.layers = layers; + this.configurationTemplate = configurationTemplate; } } @@ -92,14 +92,14 @@ static boolean isGzipped(Path path) throws IOException { } } - static Callable retrieveDockerDaemonImageStep( + static Callable retrieveDockerDaemonLayersStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - DockerClient dockerClient) { + DockerClient dockerClient, + TempDirectoryProvider tempDirectoryProvider) { return () -> { ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); - try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); - ProgressEventDispatcher progressEventDispatcher = + try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("processing base image " + imageReference, 2); TimerEventDispatcher ignored = new TimerEventDispatcher( @@ -124,26 +124,47 @@ static Callable retrieveDockerDaemonImageStep( } return cacheDockerImageTar( - buildContext, tarPath, progressEventDispatcher.newChildProducer()); + buildContext, + tarPath, + progressEventDispatcher.newChildProducer(), + tempDirectoryProvider); } }; } - static Callable retrieveTarImageStep( + static Callable retrieveTarLayersStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Path tarPath) { - return () -> cacheDockerImageTar(buildContext, tarPath, progressEventDispatcherFactory); + Path tarPath, + TempDirectoryProvider tempDirectoryProvider) { + return () -> + cacheDockerImageTar( + buildContext, tarPath, progressEventDispatcherFactory, tempDirectoryProvider); + } + + static Callable returnImageAndAuthorizationStep( + List layers, ContainerConfigurationTemplate configurationTemplate) { + return () -> { + // Collect compressed layers and add to manifest + V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + for (PreparedLayer layer : layers) { + BlobDescriptor descriptor = layer.getBlobDescriptor(); + v22Manifest.addLayer(descriptor.getSize(), descriptor.getDigest()); + } + + BlobDescriptor configDescriptor = + Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); + v22Manifest.setContainerConfiguration( + configDescriptor.getSize(), configDescriptor.getDigest()); + return new ImageAndAuthorization( + JsonToImageTranslator.toImage(v22Manifest, configurationTemplate), null); + }; } @VisibleForTesting static Optional getCachedDockerImage( Cache cache, DockerImageDetails dockerImageDetails) - throws DigestException, IOException, CacheCorruptedException, LayerCountMismatchException, - BadContainerConfigurationFormatException { - V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); - List cachedLayers = new ArrayList<>(); - + throws DigestException, IOException, CacheCorruptedException { // Get config Optional cachedConfig = cache.retrieveLocalConfig(dockerImageDetails.getImageId()); @@ -152,115 +173,93 @@ static Optional getCachedDockerImage( } // Get layers + List> cachedLayers = new ArrayList<>(); for (DescriptorDigest diffId : dockerImageDetails.getDiffIds()) { Optional cachedLayer = cache.retrieveTarLayer(diffId); if (!cachedLayer.isPresent()) { return Optional.empty(); } CachedLayer layer = cachedLayer.get(); - cachedLayers.add(new PreparedLayer.Builder(layer).build()); - v22Manifest.addLayer(layer.getSize(), layer.getDigest()); + cachedLayers.add(Futures.immediateFuture(new PreparedLayer.Builder(layer).build())); } - // Create manifest - BlobDescriptor configDescriptor = Digests.computeDigest(cachedConfig.get()); - v22Manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); - Image image = JsonToImageTranslator.toImage(v22Manifest, cachedConfig.get()); - return Optional.of(new LocalImage(image, cachedLayers)); + return Optional.of(new LocalImage(cachedLayers, cachedConfig.get())); } @VisibleForTesting static LocalImage cacheDockerImageTar( BuildContext buildContext, Path tarPath, - ProgressEventDispatcher.Factory progressEventDispatcherFactory) - throws IOException, LayerCountMismatchException, BadContainerConfigurationFormatException, - ExecutionException, InterruptedException { + ProgressEventDispatcher.Factory progressEventDispatcherFactory, + TempDirectoryProvider tempDirectoryProvider) + throws IOException, LayerCountMismatchException { ExecutorService executorService = buildContext.getExecutorService(); - try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { - Path destination = tempDirectoryProvider.newDirectory(); - - try (TimerEventDispatcher ignored = - new TimerEventDispatcher( - buildContext.getEventHandlers(), - "Extracting tar " + tarPath + " into " + destination)) { - TarExtractor.extract(tarPath, destination); - - InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); - DockerManifestEntryTemplate loadManifest = - new ObjectMapper() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) - .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; - manifestStream.close(); + Path destination = tempDirectoryProvider.newDirectory(); - Path configPath = destination.resolve(loadManifest.getConfig()); - ContainerConfigurationTemplate configurationTemplate = - JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); - BlobDescriptor originalConfigDescriptor = - Blobs.from(configPath).writeTo(ByteStreams.nullOutputStream()); + try (TimerEventDispatcher ignored = + new TimerEventDispatcher( + buildContext.getEventHandlers(), + "Extracting tar " + tarPath + " into " + destination)) { + TarExtractor.extract(tarPath, destination); - List layerFiles = loadManifest.getLayerFiles(); - if (configurationTemplate.getLayerCount() != layerFiles.size()) { - throw new LayerCountMismatchException( - "Invalid base image format: manifest contains " - + layerFiles.size() - + " layers, but container configuration contains " - + configurationTemplate.getLayerCount() - + " layers"); - } - buildContext - .getBaseImageLayersCache() - .writeLocalConfig(originalConfigDescriptor.getDigest(), configurationTemplate); - - // Check the first layer to see if the layers are compressed already. 'docker save' output - // is uncompressed, but a jib-built tar has compressed layers. - boolean layersAreCompressed = - layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); + InputStream manifestStream = Files.newInputStream(destination.resolve("manifest.json")); + DockerManifestEntryTemplate loadManifest = + new ObjectMapper() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .readValue(manifestStream, DockerManifestEntryTemplate[].class)[0]; + manifestStream.close(); - // Process layer blobs - try (ProgressEventDispatcher progressEventDispatcher = - progressEventDispatcherFactory.create( - "processing base image layers", layerFiles.size())) { - List layers = new ArrayList<>(layerFiles.size()); - V22ManifestTemplate v22Manifest = new V22ManifestTemplate(); + Path configPath = destination.resolve(loadManifest.getConfig()); + ContainerConfigurationTemplate configurationTemplate = + JsonTemplateMapper.readJsonFromFile(configPath, ContainerConfigurationTemplate.class); + BlobDescriptor originalConfigDescriptor = + Blobs.from(configPath).writeTo(ByteStreams.nullOutputStream()); - // Start compressing layers in parallel - List> cachedLayers = new ArrayList<>(); - for (int index = 0; index < layerFiles.size(); index++) { - Path layerFile = destination.resolve(layerFiles.get(index)); - DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); - ProgressEventDispatcher.Factory layerProgressDispatcherFactory = - progressEventDispatcher.newChildProducer(); - cachedLayers.add( - executorService.submit( - () -> - compressAndCacheTarLayer( - buildContext.getBaseImageLayersCache(), - diffId, - layerFile, - layersAreCompressed, - layerProgressDispatcherFactory))); - } + List layerFiles = loadManifest.getLayerFiles(); + if (configurationTemplate.getLayerCount() != layerFiles.size()) { + throw new LayerCountMismatchException( + "Invalid base image format: manifest contains " + + layerFiles.size() + + " layers, but container configuration contains " + + configurationTemplate.getLayerCount() + + " layers"); + } + buildContext + .getBaseImageLayersCache() + .writeLocalConfig(originalConfigDescriptor.getDigest(), configurationTemplate); - // Collect compressed layers and add to manifest - for (Future layerFuture : cachedLayers) { - CachedLayer layer = layerFuture.get(); - layers.add(new PreparedLayer.Builder(layer).build()); - v22Manifest.addLayer(layer.getSize(), layer.getDigest()); - } + // Check the first layer to see if the layers are compressed already. 'docker save' output + // is uncompressed, but a jib-built tar has compressed layers. + boolean layersAreCompressed = + layerFiles.size() > 0 && isGzipped(destination.resolve(layerFiles.get(0))); - BlobDescriptor configDescriptor = - Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); - v22Manifest.setContainerConfiguration( - configDescriptor.getSize(), configDescriptor.getDigest()); - Image image = JsonToImageTranslator.toImage(v22Manifest, configurationTemplate); - return new LocalImage(image, layers); + // Process layer blobs + try (ProgressEventDispatcher progressEventDispatcher = + progressEventDispatcherFactory.create( + "processing base image layers", layerFiles.size())) { + // Start compressing layers in parallel + List> preparedLayers = new ArrayList<>(); + for (int index = 0; index < layerFiles.size(); index++) { + Path layerFile = destination.resolve(layerFiles.get(index)); + DescriptorDigest diffId = configurationTemplate.getLayerDiffId(index); + ProgressEventDispatcher.Factory layerProgressDispatcherFactory = + progressEventDispatcher.newChildProducer(); + preparedLayers.add( + executorService.submit( + () -> + compressAndCacheTarLayer( + buildContext.getBaseImageLayersCache(), + diffId, + layerFile, + layersAreCompressed, + layerProgressDispatcherFactory))); } + return new LocalImage(preparedLayers, configurationTemplate); } } } - private static CachedLayer compressAndCacheTarLayer( + private static PreparedLayer compressAndCacheTarLayer( Cache cache, DescriptorDigest diffId, Path layerFile, @@ -275,12 +274,13 @@ private static CachedLayer compressAndCacheTarLayer( // Retrieve pre-compressed layer from cache Optional optionalLayer = cache.retrieveTarLayer(diffId); if (optionalLayer.isPresent()) { - return optionalLayer.get(); + return new PreparedLayer.Builder(optionalLayer.get()).build(); } // Just write layers that are already compressed if (layersAreCompressed) { - return cache.writeTarLayer(diffId, Blobs.from(layerFile)); + return new PreparedLayer.Builder(cache.writeTarLayer(diffId, Blobs.from(layerFile))) + .build(); } // Compress uncompressed layers while writing @@ -293,7 +293,7 @@ private static CachedLayer compressAndCacheTarLayer( Blobs.from(layerFile).writeTo(notifyingOutputStream); } }); - return cache.writeTarLayer(diffId, compressedBlob); + return new PreparedLayer.Builder(cache.writeTarLayer(diffId, compressedBlob)).build(); } } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 595e088bbd..197e8ed4b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -63,7 +63,7 @@ static ImmutableList makeList( @Nullable private final Authorization pushAuthorization; private final Future preparedLayer; - PushLayerStep( + private PushLayerStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, @Nullable Authorization pushAuthorization, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 45a01ee8fb..68e616d212 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -237,8 +237,11 @@ private void saveDocker() { Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); assignLocalImageResult( executorService.submit( - LocalBaseImageSteps.retrieveDockerDaemonImageStep( - buildContext, childProgressDispatcherFactory, dockerClient.get()))); + LocalBaseImageSteps.retrieveDockerDaemonLayersStep( + buildContext, + childProgressDispatcherFactory, + dockerClient.get(), + tempDirectoryProvider))); } private void extractTar() { @@ -248,23 +251,22 @@ private void extractTar() { Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); assignLocalImageResult( executorService.submit( - LocalBaseImageSteps.retrieveTarImageStep( - buildContext, childProgressDispatcherFactory, tarPath.get()))); + LocalBaseImageSteps.retrieveTarLayersStep( + buildContext, + childProgressDispatcherFactory, + tarPath.get(), + tempDirectoryProvider))); } - private void assignLocalImageResult(Future localImageFuture) { + private void assignLocalImageResult(Future localImage) { + results.baseImageLayers = executorService.submit(() -> localImage.get().layers); results.baseImageAndAuth = - executorService.submit( - () -> new ImageAndAuthorization(localImageFuture.get().baseImage, null)); - results.baseImageLayers = executorService.submit( () -> - localImageFuture - .get() - .layers - .stream() - .map(Futures::immediateFuture) - .collect(Collectors.toList())); + LocalBaseImageSteps.returnImageAndAuthorizationStep( + realizeFutures(results.baseImageLayers.get()), + localImage.get().configurationTemplate) + .call()); } private void pullBaseImage() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index eb23a1b7fd..f9f33e7c89 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -23,10 +23,8 @@ import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.image.LayerCountMismatchException; -import com.google.cloud.tools.jib.image.json.BadContainerConfigurationFormatException; +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; @@ -36,6 +34,7 @@ import java.nio.file.Paths; import java.security.DigestException; import java.util.Optional; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -51,6 +50,8 @@ public class LocalBaseImageStepsTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private final TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); + @Mock private BuildContext buildContext; @Mock private EventHandlers eventHandlers; @Mock private ProgressEventDispatcher.Factory progressEventDispatcherFactory; @@ -76,28 +77,33 @@ public void setup() throws IOException { .thenReturn(childDispatcher); } + @After + public void tearDown() { + tempDirectoryProvider.close(); + } + @Test public void testCacheDockerImageTar_validDocker() throws Exception { Path dockerBuild = getResource("core/extraction/docker-save.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildContext, dockerBuild, progressEventDispatcherFactory); + buildContext, dockerBuild, progressEventDispatcherFactory, tempDirectoryProvider); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); Assert.assertEquals( "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", - result.layers.get(0).getDiffId().getHash()); + result.layers.get(0).get().getDiffId().getHash()); Assert.assertEquals( "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", - result.layers.get(0).getBlobDescriptor().getDigest().getHash()); + result.layers.get(0).get().getBlobDescriptor().getDigest().getHash()); Assert.assertEquals( "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", - result.layers.get(1).getDiffId().getHash()); + result.layers.get(1).get().getDiffId().getHash()); Assert.assertEquals( "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", - result.layers.get(1).getBlobDescriptor().getDigest().getHash()); - Assert.assertEquals("value1", result.baseImage.getLabels().get("label1")); + result.layers.get(1).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals(2, result.configurationTemplate.getLayerCount()); } @Test @@ -105,29 +111,28 @@ public void testCacheDockerImageTar_validTar() throws Exception { Path tarBuild = getResource("core/extraction/jib-image.tar"); LocalImage result = LocalBaseImageSteps.cacheDockerImageTar( - buildContext, tarBuild, progressEventDispatcherFactory); + buildContext, tarBuild, progressEventDispatcherFactory, tempDirectoryProvider); Mockito.verify(progressEventDispatcher, Mockito.times(2)).newChildProducer(); Assert.assertEquals(2, result.layers.size()); Assert.assertEquals( "5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", - result.layers.get(0).getDiffId().getHash()); + result.layers.get(0).get().getDiffId().getHash()); Assert.assertEquals( "0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f", - result.layers.get(0).getBlobDescriptor().getDigest().getHash()); + result.layers.get(0).get().getBlobDescriptor().getDigest().getHash()); Assert.assertEquals( "f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e", - result.layers.get(1).getDiffId().getHash()); + result.layers.get(1).get().getDiffId().getHash()); Assert.assertEquals( "c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94", - result.layers.get(1).getBlobDescriptor().getDigest().getHash()); - Assert.assertEquals("value1", result.baseImage.getLabels().get("label1")); + result.layers.get(1).get().getBlobDescriptor().getDigest().getHash()); + Assert.assertEquals(2, result.configurationTemplate.getLayerCount()); } @Test public void testGetCachedDockerImage() - throws IOException, DigestException, BadContainerConfigurationFormatException, - CacheCorruptedException, LayerCountMismatchException, URISyntaxException { + throws IOException, DigestException, CacheCorruptedException, URISyntaxException { DockerImageDetails dockerImageDetails = new DockerImageDetails( 0, @@ -172,8 +177,7 @@ public void testGetCachedDockerImage() localImage = LocalBaseImageSteps.getCachedDockerImage(cache, dockerImageDetails); Assert.assertTrue(localImage.isPresent()); LocalImage image = localImage.get(); - Assert.assertEquals( - ImmutableMap.of("label1", "value1", "label2", "value2"), image.baseImage.getLabels()); + Assert.assertEquals(2, image.configurationTemplate.getLayerCount()); Assert.assertEquals(2, image.layers.size()); } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4b346e1db8..724823f07a 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 0b1bb415a3..885edcd61c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) + ### Fixed - Fixed reporting wrong module name when `skaffold init` is run on multi-module projects ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)). From f9764863bb31b40ee524cc475b0dd25c1addff4b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 14 Nov 2019 15:01:38 -0500 Subject: [PATCH 0829/2020] Optimize insecure registry failover by caching failover history (#2132) --- jib-core/CHANGELOG.md | 1 + .../tools/jib/configuration/BuildContext.java | 4 - .../tools/jib/http/FailoverHttpClient.java | 69 ++++++--- .../jib/http/FailoverHttpClientTest.java | 136 +++++++++++++----- .../WithServerFailoverHttpClientTest.java | 6 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 7 files changed, 160 insertions(+), 58 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index dce7cf991e..f3498b04f6 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. ### Fixed - `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- Fixed inefficient communications when interacting with insecure registries and servers (when `Containerizer#setAllowInsecureRegistries(boolean)` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 0.12.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java index 186138f527..f252e1a379 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java @@ -417,10 +417,6 @@ public EventHandlers getEventHandlers() { return eventHandlers; } - public FailoverHttpClient getHttpClient() { - return httpClient; - } - public ExecutorService getExecutorService() { return executorService; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index 3f73847437..e86abdfdb9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -26,12 +26,15 @@ import com.google.api.client.util.SslUtils; import com.google.cloud.tools.jib.api.LogEvent; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import java.io.IOException; import java.net.ConnectException; import java.net.URL; import java.security.GeneralSecurityException; import java.util.ArrayDeque; import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; import javax.net.ssl.SSLException; @@ -71,6 +74,13 @@ */ public class FailoverHttpClient { + /** Represents failover actions taken. To be recorded in the failover history. */ + private static enum Failover { + NONE, // no failover (secure HTTPS) + INSECURE_HTTPS, // HTTPS with certificate validation disabled + HTTP // plain HTTP + } + private static boolean isHttpsProtocol(URL url) { return "https".equals(url.getProtocol()); } @@ -110,6 +120,8 @@ private static HttpTransport getInsecureHttpTransport() { private final Supplier secureHttpTransportFactory; private final Supplier insecureHttpTransportFactory; + private final ConcurrentHashMap failoverHistory = new ConcurrentHashMap<>(); + private final Deque transportsCreated = new ArrayDeque<>(); private final Deque responsesCreated = new ArrayDeque<>(); @@ -209,10 +221,18 @@ public Response put(URL url, Request request) throws IOException { * @throws IOException if building the HTTP request fails. */ public Response call(String httpMethod, URL url, Request request) throws IOException { - if (!isHttpsProtocol(url) && !enableHttpAndInsecureFailover) { + if (!isHttpsProtocol(url)) { + if (enableHttpAndInsecureFailover) { // HTTP requested. We only care if HTTP is enabled. + return call(httpMethod, url, request, getHttpTransport(true)); + } throw new SSLException("insecure HTTP connection not allowed: " + url); } + Optional fastPathResponse = followFailoverHistory(httpMethod, url, request); + if (fastPathResponse.isPresent()) { + return fastPathResponse.get(); + } + try { return call(httpMethod, url, request, getHttpTransport(true)); @@ -223,11 +243,15 @@ public Response call(String httpMethod, URL url, Request request) throws IOExcep try { logInsecureHttpsFailover(url); - return call(httpMethod, url, request, getHttpTransport(false)); + Response response = call(httpMethod, url, request, getHttpTransport(false)); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.INSECURE_HTTPS); + return response; } catch (SSLException ignored) { // This is usually when the server is plain-HTTP. logHttpFailover(url); - return call(httpMethod, toHttp(url), request, getHttpTransport(true)); + Response response = call(httpMethod, toHttp(url), request, getHttpTransport(true)); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.HTTP); + return response; } } catch (ConnectException ex) { @@ -235,20 +259,33 @@ public Response call(String httpMethod, URL url, Request request) throws IOExcep // ConnectException for connection timeout. (Could be a JDK bug.) Note SocketTimeoutException // does not extend ConnectException (or vice versa), and we want to be consistent to error out // on timeouts: https://github.com/GoogleContainerTools/jib/issues/1895#issuecomment-527544094 - if (ex.getMessage() != null && ex.getMessage().contains("timed out")) { - throw ex; - } - - // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS - // port 443) and we could not connect to 443. It's worth trying port 80. - if (enableHttpAndInsecureFailover && isHttpsProtocol(url) && url.getPort() == -1) { - logHttpFailover(url); - return call(httpMethod, toHttp(url), request, getHttpTransport(true)); + if (ex.getMessage() == null || !ex.getMessage().contains("timed out")) { + // Fall back to HTTP only if "url" had no port specified (i.e., we tried the default HTTPS + // port 443) and we could not connect to 443. It's worth trying port 80. + if (enableHttpAndInsecureFailover && isHttpsProtocol(url) && url.getPort() == -1) { + logHttpFailover(url); + Response response = call(httpMethod, toHttp(url), request, getHttpTransport(true)); + failoverHistory.put(url.getHost() + ":" + url.getPort(), Failover.HTTP); + return response; + } } throw ex; } } + private Optional followFailoverHistory(String httpMethod, URL url, Request request) + throws IOException { + Preconditions.checkArgument(isHttpsProtocol(url)); + switch (failoverHistory.getOrDefault(url.getHost() + ":" + url.getPort(), Failover.NONE)) { + case HTTP: + return Optional.of(call(httpMethod, toHttp(url), request, getHttpTransport(true))); + case INSECURE_HTTPS: + return Optional.of(call(httpMethod, url, request, getHttpTransport(false))); + default: + return Optional.empty(); // No history found. Should go for normal execution path. + } + } + private Response call(String httpMethod, URL url, Request request, HttpTransport httpTransport) throws IOException { boolean clearAuthorization = !isHttpsProtocol(url) && !sendAuthorizationOverHttp; @@ -271,7 +308,7 @@ private Response call(String httpMethod, URL url, Request request, HttpTransport try { Response response = new Response(httpRequest.execute()); synchronized (responsesCreated) { - responsesCreated.addLast(response); + responsesCreated.add(response); } return response; } catch (HttpResponseException ex) { @@ -283,18 +320,18 @@ private HttpTransport getHttpTransport(boolean secureTransport) { HttpTransport transport = secureTransport ? secureHttpTransportFactory.get() : insecureHttpTransportFactory.get(); synchronized (transportsCreated) { - transportsCreated.addLast(transport); + transportsCreated.add(transport); } return transport; } private void logHttpFailover(URL url) { String log = "Failed to connect to " + url + " over HTTPS. Attempting again with HTTP."; - logger.accept(LogEvent.info(log)); + logger.accept(LogEvent.warn(log)); } private void logInsecureHttpsFailover(URL url) { String log = "Cannot verify server at " + url + ". Attempting again with no TLS verification."; - logger.accept(LogEvent.info(log)); + logger.accept(LogEvent.warn(log)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java index eb2901ab96..701cb10fbe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -33,8 +33,10 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import org.junit.Assert; @@ -150,14 +152,9 @@ public void testGet_insecureClientOnUnverifiableServer() throws IOException { Assert.assertEquals("body", new String(bytes, StandardCharsets.UTF_8)); } - Assert.assertEquals(2, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(1)); - - String log = - "Cannot verify server at https://insecure. Attempting again with no TLS verification."; - Mockito.verify(logger).accept(LogEvent.info(log)); - Mockito.verifyNoMoreInteractions(logger); + verifyCapturedUrls("https://insecure", "https://insecure"); + verifyWarnings( + "Cannot verify server at https://insecure. Attempting again with no TLS verification."); } @Test @@ -180,17 +177,10 @@ public void testGet_insecureClientOnHttpServer() throws IOException { Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); - Assert.assertEquals(3, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(1)); - Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(2)); - - String log1 = - "Cannot verify server at https://insecure. Attempting again with no TLS verification."; - String log2 = "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."; - Mockito.verify(logger).accept(LogEvent.info(log1)); - Mockito.verify(logger).accept(LogEvent.info(log2)); - Mockito.verifyNoMoreInteractions(logger); + verifyCapturedUrls("https://insecure", "https://insecure", "http://insecure"); + verifyWarnings( + "Cannot verify server at https://insecure. Attempting again with no TLS verification.", + "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."); } @Test @@ -211,13 +201,8 @@ public void testGet_insecureClientOnHttpServerAndNoPortSpecified() throws IOExce Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); Mockito.verifyNoInteractions(mockInsecureHttpRequest); - Assert.assertEquals(2, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(1)); - - String log = "Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."; - Mockito.verify(logger).accept(LogEvent.info(log)); - Mockito.verifyNoMoreInteractions(logger); + verifyCapturedUrls("https://insecure", "http://insecure"); + verifyWarnings("Failed to connect to https://insecure over HTTPS. Attempting again with HTTP."); } @Test @@ -232,8 +217,7 @@ public void testGet_secureClientOnNonListeningServerAndNoPortSpecified() throws } catch (ConnectException ex) { Assert.assertEquals("my exception", ex.getMessage()); - Assert.assertEquals(1, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getValue()); + verifyCapturedUrls("https://insecure"); Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); @@ -253,8 +237,7 @@ public void testGet_insecureClientOnNonListeningServerAndPortSpecified() throws } catch (ConnectException ex) { Assert.assertEquals("my exception", ex.getMessage()); - Assert.assertEquals(1, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure:5000"), urlCaptor.getValue()); + verifyCapturedUrls("https://insecure:5000"); Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); @@ -273,8 +256,7 @@ public void testGet_timeoutFromConnectException() throws IOException { } catch (ConnectException ex) { Assert.assertEquals("Connection timed out", ex.getMessage()); - Assert.assertEquals(1, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getValue()); + verifyCapturedUrls("https://insecure"); Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); Mockito.verifyNoInteractions(mockInsecureHttpRequest, logger); @@ -293,9 +275,7 @@ public void testGet_doNotSendCredentialsOverHttp() throws IOException { try (Response response = insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) {} - Assert.assertEquals(2, urlCaptor.getAllValues().size()); - Assert.assertEquals(new GenericUrl("https://insecure"), urlCaptor.getAllValues().get(0)); - Assert.assertEquals(new GenericUrl("http://insecure"), urlCaptor.getAllValues().get(1)); + verifyCapturedUrls("https://insecure", "http://insecure"); Assert.assertEquals(2, httpHeadersCaptor.getAllValues().size()); Assert.assertEquals( @@ -345,6 +325,79 @@ public void testShutDown() throws IOException { } } + @Test + public void testFollowFailoverHistory_insecureHttps() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) {} + + Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); + verifyCapturedUrls("https://url", "https://url", "https://url"); + verifyWarnings( + "Cannot verify server at https://url. Attempting again with no TLS verification."); + } + + @Test + public void testFollowFailoverHistory_httpFailoverByConnectionError() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new ConnectException()) + .thenReturn(mockHttpResponse); + + try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) {} + + Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); + verifyCapturedUrls("https://url", "http://url", "http://url"); + verifyWarnings("Failed to connect to https://url over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testFollowFailoverHistory_httpFailover() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + Mockito.when(mockInsecureHttpRequest.execute()).thenThrow(new SSLException("")); + + try (Response response1 = httpClient.get(new URL("https://url:123"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url:123"), fakeRequest(null))) {} + + Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); + verifyCapturedUrls("https://url:123", "https://url:123", "http://url:123", "http://url:123"); + verifyWarnings( + "Cannot verify server at https://url:123. Attempting again with no TLS verification.", + "Failed to connect to https://url:123 over HTTPS. Attempting again with HTTP."); + } + + @Test + public void testFollowFailoverHistory_portsDifferent() throws IOException { + FailoverHttpClient httpClient = newHttpClient(true, false); + + Mockito.when(mockHttpRequest.execute()) + .thenThrow(new SSLException("")) + .thenThrow(new SSLException("")) + .thenReturn(mockHttpResponse); + + try (Response response1 = httpClient.get(new URL("https://url:1"), fakeRequest(null)); + Response response2 = httpClient.post(new URL("https://url:2"), fakeRequest(null))) {} + + Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); + Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); + verifyCapturedUrls("https://url:1", "https://url:1", "https://url:2", "https://url:2"); + verifyWarnings( + "Cannot verify server at https://url:1. Attempting again with no TLS verification.", + "Cannot verify server at https://url:2. Attempting again with no TLS verification."); + } + private void setUpMocks( HttpTransport mockHttpTransport, HttpRequestFactory mockHttpRequestFactory, @@ -406,4 +459,17 @@ private void verifyCall(String httpMethod, CallFunction callFunction) throws IOE Assert.assertEquals("crepecake", byteArrayOutputStream.toString(StandardCharsets.UTF_8.name())); Assert.assertEquals("crepecake".length(), totalByteCount.longValue()); } + + private void verifyWarnings(String... logs) { + for (String log : logs) { + Mockito.verify(logger, Mockito.times(1)).accept(LogEvent.warn(log)); + } + Mockito.verifyNoMoreInteractions(logger); + } + + private void verifyCapturedUrls(String... urls) { + List captured = + urlCaptor.getAllValues().stream().map(GenericUrl::toString).collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList(urls), captured); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index f315e1113e..7fb95628fc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -92,7 +92,7 @@ public void testInsecureConnection_insecureHttpsFailover() String endpoint = server.getEndpoint(); String expectedLog = "Cannot verify server at " + endpoint + ". Attempting again with no TLS verification."; - Mockito.verify(logger).accept(LogEvent.info(expectedLog)); + Mockito.verify(logger).accept(LogEvent.warn(expectedLog)); } } @@ -114,8 +114,8 @@ public void testInsecureConnection_plainHttpFailover() "Cannot verify server at " + httpsUrl + ". Attempting again with no TLS verification."; String expectedLog2 = "Failed to connect to " + httpsUrl + " over HTTPS. Attempting again with HTTP."; - Mockito.verify(logger).accept(LogEvent.info(expectedLog1)); - Mockito.verify(logger).accept(LogEvent.info(expectedLog2)); + Mockito.verify(logger).accept(LogEvent.warn(expectedLog1)); + Mockito.verify(logger).accept(LogEvent.warn(expectedLog2)); } } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 724823f07a..eb4ab6d0d1 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) - Now correctly uses the `war` task if it is enabled and the `bootWar` task is disabled for Spring WAR projects. ([#2096](https://github.com/GoogleContainerTools/jib/issues/2096)) - `allowInsecureRegistries` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- Fixed inefficient communications when interacting with insecure registries and servers (when `allowInsecureRegistries` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 1.7.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 885edcd61c..4d7811a187 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - Fixed reporting wrong module name when `skaffold init` is run on multi-module projects ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)). - `` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- Fixed inefficient communications when interacting with insecure registries and servers (when `` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 1.7.0 From c9bdb509f261623c1aea6e61808370b3393856b2 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 15 Nov 2019 11:15:28 -0500 Subject: [PATCH 0830/2020] Add syncmap task for gradle plugin (#2149) * Add syncmap task for gradle plugin --- .../jib/gradle/GradleProjectProperties.java | 13 +- .../jib/gradle/GradleRawConfiguration.java | 4 +- .../cloud/tools/jib/gradle/JibPlugin.java | 8 + .../jib/gradle/skaffold/SyncMapTask.java | 91 +++++++++ .../cloud/tools/jib/gradle/TestProject.java | 5 + .../jib/gradle/skaffold/SyncMapTaskTest.java | 184 ++++++++++++++++++ .../projects/war_servlet25/build.gradle | 23 +++ .../projects/war_servlet25/pom-tomcat.xml | 60 ++++++ .../gradle/projects/war_servlet25/pom.xml | 54 +++++ .../war_servlet25/src/extra_js/bogus.js | 1 + .../war_servlet25/src/extra_static/bogus.html | 1 + .../src/main/java/example/HelloWorld.java | 48 +++++ .../war_servlet25/src/main/resources/world | 1 + .../src/main/webapp/META-INF/MANIFEST.MF | 2 + .../src/main/webapp/WEB-INF/web.xml | 15 ++ .../war_servlet25/src/main/webapp/index.html | 20 ++ .../tools/jib/maven/skaffold/SyncMapMojo.java | 1 + 17 files changed, 526 insertions(+), 5 deletions(-) create mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java create mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index b5d2f7ce5a..1c0d8c24fe 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -64,7 +64,7 @@ import org.gradle.jvm.tasks.Jar; /** Obtains information about a Gradle {@link Project} that uses Jib. */ -class GradleProjectProperties implements ProjectProperties { +public class GradleProjectProperties implements ProjectProperties { /** Used to generate the User-Agent header and history metadata. */ private static final String TOOL_NAME = "jib-gradle-plugin"; @@ -80,8 +80,15 @@ class GradleProjectProperties implements ProjectProperties { private static final Duration LOGGING_THREAD_SHUTDOWN_TIMEOUT = Duration.ofSeconds(1); - /** @return a GradleProjectProperties from the given project and logger. */ - static GradleProjectProperties getForProject( + /** + * Generate an instance for a gradle project. + * + * @param project a gradle project + * @param logger a gradle logging instance to use for logging during the build + * @param tempDirectoryProvider for scratch space during the build + * @return a GradleProjectProperties instance to use in a jib build + */ + public static GradleProjectProperties getForProject( Project project, Logger logger, TempDirectoryProvider tempDirectoryProvider) { return new GradleProjectProperties(project, logger, tempDirectoryProvider); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index b741e21152..221b74adc6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -28,11 +28,11 @@ import java.util.Set; /** Gradle-specific adapter for providing raw configuration parameter values. */ -class GradleRawConfiguration implements RawConfiguration { +public class GradleRawConfiguration implements RawConfiguration { private final JibExtension jibExtension; - GradleRawConfiguration(JibExtension jibExtension) { + public GradleRawConfiguration(JibExtension jibExtension) { this.jibExtension = jibExtension; } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 40fcca78b4..7d26b9e801 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.gradle.skaffold.FilesTask; import com.google.cloud.tools.jib.gradle.skaffold.FilesTaskV2; import com.google.cloud.tools.jib.gradle.skaffold.InitTask; +import com.google.cloud.tools.jib.gradle.skaffold.SyncMapTask; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; @@ -50,6 +51,7 @@ public class JibPlugin implements Plugin { public static final String SKAFFOLD_FILES_TASK_NAME = "_jibSkaffoldFiles"; public static final String SKAFFOLD_FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; public static final String SKAFFOLD_INIT_TASK_NAME = "_jibSkaffoldInit"; + public static final String SKAFFOLD_SYNC_MAP_TASK_NAME = "_jibSkaffoldSyncMap"; public static final String SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME = "_skaffoldFailIfJibOutOfDate"; @@ -158,6 +160,11 @@ public void apply(Project project) { tasks .register(SKAFFOLD_INIT_TASK_NAME, InitTask.class) .configure(task -> task.setJibExtension(jibExtension)); + TaskProvider syncMapTask = + tasks.register( + SKAFFOLD_SYNC_MAP_TASK_NAME, + SyncMapTask.class, + task -> task.setJibExtension(jibExtension)); // A check to catch older versions of Jib. This can be removed once we are certain people // are using Jib 1.3.1 or later. @@ -187,6 +194,7 @@ public void apply(Project project) { buildImageTask.configure(task -> task.dependsOn(dependsOnTask)); buildDockerTask.configure(task -> task.dependsOn(dependsOnTask)); buildTarTask.configure(task -> task.dependsOn(dependsOnTask)); + syncMapTask.configure(task -> task.dependsOn(dependsOnTask)); // Find project dependencies and add a dependency to their assemble task. We make sure // to only add the dependency after BasePlugin is evaluated as otherwise the assemble diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java new file mode 100644 index 0000000000..c580b15c53 --- /dev/null +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; +import com.google.cloud.tools.jib.gradle.GradleProjectProperties; +import com.google.cloud.tools.jib.gradle.GradleRawConfiguration; +import com.google.cloud.tools.jib.gradle.JibExtension; +import com.google.cloud.tools.jib.plugins.common.ContainerizingMode; +import com.google.cloud.tools.jib.plugins.common.InvalidContainerizingModeException; +import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; + +/** + * Prints out a map of local files to their location on the container. + * + *

Expected use: {@code ./gradlew _jibSkaffoldSyncMap -q} or {@code ./gradlew + * ::_jibSkaffoldSyncMap -q} + */ +public class SyncMapTask extends DefaultTask { + + @Nullable private JibExtension jibExtension; + + public SyncMapTask setJibExtension(JibExtension jibExtension) { + this.jibExtension = jibExtension; + return this; + } + + @TaskAction + public void listFilesAndTargets() { + Preconditions.checkNotNull(jibExtension); + + try (TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider()) { + GradleProjectProperties projectProperties = + GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); + + GradleRawConfiguration configuration = new GradleRawConfiguration(jibExtension); + + // TODO: move these shared checks with SyncMapMojo into plugins-common + if (projectProperties.isWarProject()) { + throw new GradleException( + "Skaffold sync is currently only available for 'jar' style Jib projects, but the project " + + getProject().getName() + + " is configured to generate a 'war'"); + } + try { + if (!ContainerizingMode.EXPLODED.equals( + ContainerizingMode.from(jibExtension.getContainerizingMode()))) { + throw new GradleException( + "Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of " + + getProject().getName() + + " is '" + + jibExtension.getContainerizingMode() + + "'"); + } + } catch (InvalidContainerizingModeException ex) { + throw new GradleException("Invalid containerizing mode", ex); + } + + try { + String syncMapJson = + PluginConfigurationProcessor.getSkaffoldSyncMap(configuration, projectProperties); + + System.out.println(); + System.out.println("BEGIN JIB JSON"); + System.out.println(syncMapJson); + + } catch (Exception ex) { + throw new GradleException("Failed to generate a Jib file map for sync with Skaffold", ex); + } + } + } +} diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index 84594be2f5..b724b1e532 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.junit.rules.TemporaryFolder; @@ -86,6 +87,10 @@ public BuildResult build(String... gradleArguments) { return gradleRunner.withArguments(gradleArguments).build(); } + public BuildResult build(List gradleArguments) { + return gradleRunner.withArguments(gradleArguments).build(); + } + public Path getProjectRoot() { return projectRoot; } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java new file mode 100644 index 0000000000..8fd5329712 --- /dev/null +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle.skaffold; + +import com.google.cloud.tools.jib.api.AbsoluteUnixPath; +import com.google.cloud.tools.jib.gradle.JibPlugin; +import com.google.cloud.tools.jib.gradle.TestProject; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate; +import com.google.cloud.tools.jib.plugins.common.SkaffoldSyncMapTemplate.FileTemplate; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.BuildTask; +import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Tests for {@link SyncMapTask}. */ +public class SyncMapTaskTest { + + @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); + @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + @ClassRule public static final TestProject warProject = new TestProject("war_servlet25"); + + /** + * Verifies that the sync map task succeeded and returns the parsed json. + * + * @param project the project to run the task on + * @param moduleName the name of the sub-project, or {@code null} if no sub-project + * @param params extra gradle cli params to use during the build + * @return the list of paths printed by the task + * @throws IOException if the json parser fails + */ + private static SkaffoldSyncMapTemplate generateTemplate( + TestProject project, @Nullable String moduleName, @Nullable List params) + throws IOException { + String taskName = + ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_SYNC_MAP_TASK_NAME; + List buildParams = new ArrayList<>(); + buildParams.add(taskName); + buildParams.add("-q"); + buildParams.add("--stacktrace"); + if (params != null) { + buildParams.addAll(params); + } + BuildResult buildResult = project.build(buildParams); + BuildTask jibTask = buildResult.task(taskName); + Assert.assertNotNull(jibTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); + + List outputLines = + Splitter.on(System.lineSeparator()).omitEmptyStrings().splitToList(buildResult.getOutput()); + Assert.assertEquals(2, outputLines.size()); + Assert.assertEquals("BEGIN JIB JSON", outputLines.get(0)); + return SkaffoldSyncMapTemplate.from(outputLines.get(1)); + } + + private static void assertFilePaths(Path src, AbsoluteUnixPath dest, FileTemplate template) + throws IOException { + Assert.assertEquals(src.toRealPath().toString(), template.getSrc()); + Assert.assertEquals(dest.toString(), template.getDest()); + } + + @Test + public void testSyncMapTask_singleProject() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + SkaffoldSyncMapTemplate parsed = generateTemplate(simpleTestProject, null, null); + + List generated = parsed.getGenerated(); + Assert.assertEquals(2, generated.size()); + assertFilePaths( + projectRoot.resolve("build/resources/main/world"), + AbsoluteUnixPath.get("/app/resources/world"), + generated.get(0)); + assertFilePaths( + projectRoot.resolve("build/classes/java/main/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(1)); + + List direct = parsed.getDirect(); + Assert.assertEquals(2, direct.size()); + assertFilePaths( + projectRoot.resolve("src/main/custom-extra-dir/bar/cat"), + AbsoluteUnixPath.get("/bar/cat"), + direct.get(0)); + assertFilePaths( + projectRoot.resolve("src/main/custom-extra-dir/foo"), + AbsoluteUnixPath.get("/foo"), + direct.get(1)); + } + + @Test + public void testSyncMapTask_multiProjectOutput() throws IOException { + Path projectRoot = multiTestProject.getProjectRoot(); + Path complexServiceRoot = projectRoot.resolve("complex-service"); + Path libRoot = projectRoot.resolve("lib"); + SkaffoldSyncMapTemplate parsed = generateTemplate(multiTestProject, "complex-service", null); + + List generated = parsed.getGenerated(); + Assert.assertEquals(4, generated.size()); + assertFilePaths( + libRoot.resolve("build/libs/lib.jar"), + AbsoluteUnixPath.get("/app/libs/lib.jar"), + generated.get(0)); + assertFilePaths( + complexServiceRoot.resolve("build/resources/main/resource1.txt"), + AbsoluteUnixPath.get("/app/resources/resource1.txt"), + generated.get(1)); + assertFilePaths( + complexServiceRoot.resolve("build/resources/main/resource2.txt"), + AbsoluteUnixPath.get("/app/resources/resource2.txt"), + generated.get(2)); + assertFilePaths( + complexServiceRoot.resolve("build/classes/java/main/com/test/HelloWorld.class"), + AbsoluteUnixPath.get("/app/classes/com/test/HelloWorld.class"), + generated.get(3)); + + List direct = parsed.getDirect(); + Assert.assertEquals(2, direct.size()); + assertFilePaths( + complexServiceRoot.resolve( + "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + AbsoluteUnixPath.get("/app/libs/tiny-test-lib-0.0.1-SNAPSHOT.jar"), + direct.get(0)); + assertFilePaths( + complexServiceRoot.resolve("src/main/other-jib/extra-file"), + AbsoluteUnixPath.get("/extra-file"), + direct.get(1)); + } + + @Test + public void testSyncMapMojo_failIfWar() throws IOException { + Path projectRoot = warProject.getProjectRoot(); + try { + generateTemplate(warProject, null, null); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "org.gradle.api.GradleException: Skaffold sync is currently only available for 'jar' style Jib projects, but the project " + + projectRoot.getFileName() + + " is configured to generate a 'war'")); + } + } + + @Test + public void testSyncMapMojo_failIfJarContainerizationMode() throws IOException { + Path projectRoot = simpleTestProject.getProjectRoot(); + try { + generateTemplate( + simpleTestProject, null, ImmutableList.of("-Djib.containerizingMode=packaged")); + Assert.fail(); + } catch (UnexpectedBuildFailure ex) { + Assert.assertTrue( + ex.getMessage() + .contains( + "Skaffold sync is currently only available for Jib projects in 'exploded' containerizing mode, but the containerizing mode of " + + projectRoot.getFileName() + + " is 'packaged'")); + } + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle new file mode 100644 index 0000000000..45dc2c6710 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'war' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compileOnly "javax.servlet:servlet-api:2.5" + implementation "javax.annotation:javax.annotation-api:1.2" +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml new file mode 100644 index 0000000000..2626a734dc --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.test + servlet25 + 1 + war + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + javax.servlet + servlet-api + 2.5 + provided + + + javax.annotation + javax.annotation-api + 1.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + tomcat:8.5-jre8-alpine + + + ${_TARGET_IMAGE} + + + /usr/local/tomcat/webapps/ROOT + + + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml new file mode 100644 index 0000000000..c51d357f05 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.test + servlet25 + 1 + war + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + javax.servlet + servlet-api + 2.5 + provided + + + javax.annotation + javax.annotation-api + 1.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${_TARGET_IMAGE} + + + + + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js new file mode 100644 index 0000000000..6805a1de3a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js @@ -0,0 +1 @@ +// nothing inside diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html new file mode 100644 index 0000000000..08e803ef88 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html @@ -0,0 +1 @@ +nothing inside diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java new file mode 100644 index 0000000000..05618c6777 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package example; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HelloWorld extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + URL worldFile = getServletContext().getResource("/WEB-INF/classes/world"); + Path path = Paths.get(worldFile.toURI()); + String world = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + response.getWriter().print("Hello " + world); + + } catch (URISyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world new file mode 100644 index 0000000000..04fea06420 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e3c07ab38a --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Class-Path: diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..1699019922 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,15 @@ + + + + + HelloWorld + example.HelloWorld + + + HelloWorld + /hello + + diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html new file mode 100644 index 0000000000..c7f81e3aeb --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html @@ -0,0 +1,20 @@ + + + + + Hello World + + + +

Hello World!

+ + + + + + + + +
Available Servlets:
The HelloWorld servlet
+ + diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java index 1152d4b82e..2298399494 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java @@ -43,6 +43,7 @@ public void execute() throws MojoExecutionException { return; } + // TODO: move these shared checks with SyncMapTask into plugins-common // add check that means this is only for jars if (!"jar".equals(getProject().getPackaging())) { throw new MojoExecutionException( From d84f542127e11d9f4c6f7a4738bfefa513d1bdc8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 15 Nov 2019 13:26:02 -0500 Subject: [PATCH 0831/2020] Gradle release v1.8.0 (#2154) * [Gradle Release Plugin] - pre tag commit: 'v1.8.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v1.8.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 92d8b7c5f9..185c1a7e6a 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.7.1-SNAPSHOT +version = 1.8.1-SNAPSHOT From cbc466d54fdd1ec9fa6a9647abfa62cfd76b77b7 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 15 Nov 2019 13:26:12 -0500 Subject: [PATCH 0832/2020] Maven release v1.8.0 (#2152) * [Gradle Release Plugin] - pre tag commit: 'v1.8.0-maven'. * [Gradle Release Plugin] - new version commit: 'v1.8.1-SNAPSHOT-maven'. --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index 92d8b7c5f9..185c1a7e6a 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.7.1-SNAPSHOT +version = 1.8.1-SNAPSHOT From 605238d812641d03cdfc9c68d8331d08122eff0f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 15 Nov 2019 16:43:26 -0500 Subject: [PATCH 0833/2020] Post release updates (#2156) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 9 ++++++++- jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 10 ++++++++-- jib-maven-plugin/README.md | 6 +++--- 17 files changed, 34 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f69d8f02d..4352c797c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.7.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:1.8.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.7.0' + // id 'com.google.cloud.tools.jib' version '1.8.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index be056cefa8..2b6f14306b 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.7.0 + 1.8.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 570579f688..98f257bf92 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 10240f1e2a..0173513ad2 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.7.0 + 1.8.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 787bbb65bc..995dd1bcec 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' id 'de.undercouch.download' version '4.0.0' id "com.gorylenko.gradle-git-properties" version "2.2.0" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 1af9961c7c..455ce22ca0 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.7.0 + 1.8.0 3.8.0 1.4.2 3.0.1 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 6fa0695008..01d479142d 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' } version '0.1' diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index a6307e4e08..0dc17dbe72 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.7.0' + classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.8.0' } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 15dbb90231..1cc1a91673 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -40,7 +40,7 @@ com.google.cloud.tools jib-maven-plugin - 1.7.0 + 1.8.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index ee85226f1f..f52a9aaa57 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 6a52f3e838..d6c915e619 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.7.0 + 1.8.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 67941075b7..59a5e95c73 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index f3498b04f6..91dd2dbde6 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -13,7 +13,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) - Fixed inefficient communications when interacting with insecure registries and servers (when `Containerizer#setAllowInsecureRegistries(boolean)` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 0.12.0 diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index eb4ab6d0d1..790351d373 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,13 +7,20 @@ All notable changes to this project will be documented in this file. ### Changed +### Fixed + +## 1.8.0 + +### Changed + +- Requires Gradle 4.9 or newer (up from 4.6). - Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) ### Fixed - Fixed reporting parent build file when `skaffold init` is run on multi-module projects. ([#2091](https://github.com/GoogleContainerTools/jib/pull/2091)) - Now correctly uses the `war` task if it is enabled and the `bootWar` task is disabled for Spring WAR projects. ([#2096](https://github.com/GoogleContainerTools/jib/issues/2096)) -- `allowInsecureRegistries` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- `allowInsecureRegistries` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) - Fixed inefficient communications when interacting with insecure registries and servers (when `allowInsecureRegistries` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 1.7.0 diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 661b374aef..bc5cf36e06 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -46,7 +46,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.7.0' + id 'com.google.cloud.tools.jib' version '1.8.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4d7811a187..d2594ce7f9 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,12 +7,18 @@ All notable changes to this project will be documented in this file. ### Changed +### Fixed + +## 1.8.0 + +## Changed + - Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) ### Fixed -- Fixed reporting wrong module name when `skaffold init` is run on multi-module projects ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)). -- `` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074) +- Fixed reporting wrong module name when `skaffold init` is run on multi-module projects. ([#2088](https://github.com/GoogleContainerTools/jib/issues/2088)) +- `` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) - Fixed inefficient communications when interacting with insecure registries and servers (when `` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) ## 1.7.0 diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 80b6d92b96..6875b7b5b7 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -41,7 +41,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.7.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:1.8.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -49,7 +49,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.7.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:1.8.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -67,7 +67,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.7.0 + 1.8.0 myimage From 65a56971ec6ab457409d06c12c287e0a1541da4e Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 15 Nov 2019 17:19:17 -0500 Subject: [PATCH 0834/2020] Update multimodule example with module dependency (#2155) --- examples/multi-module/README.md | 40 +++++++++++++++-- examples/multi-module/build.gradle | 40 +++-------------- .../multi-module/hello-service/build.gradle | 22 ++++++++++ examples/multi-module/maven-build.sh | 7 ++- .../multi-module/name-service/build.gradle | 23 ++++++++++ examples/multi-module/name-service/pom.xml | 9 ++++ .../src/main/java/name/NameController.java | 5 ++- examples/multi-module/pom.xml | 1 + examples/multi-module/settings.gradle | 2 +- .../multi-module/shared-library/build.gradle | 15 +++++++ .../shared-library/gradle.properties | 1 + examples/multi-module/shared-library/pom.xml | 44 +++++++++++++++++++ .../src/main/java/common/SharedUtils.java | 8 ++++ 13 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 examples/multi-module/hello-service/build.gradle create mode 100644 examples/multi-module/name-service/build.gradle create mode 100644 examples/multi-module/shared-library/build.gradle create mode 100644 examples/multi-module/shared-library/gradle.properties create mode 100644 examples/multi-module/shared-library/pom.xml create mode 100644 examples/multi-module/shared-library/src/main/java/common/SharedUtils.java diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index e1fe061f85..d932b4660c 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -4,14 +4,30 @@ This example shows how to build multiple containers for a multi-module project i # How the example is set up -The project consists of two microservices: +The project consists of two microservices and a library: 1. `name-service` - responds with a name +1. `shared-library` - a project dependency used by `name-service` 1. `hello-service` - calls `name-service` and responds with a greeting The **Maven** project is set up with a parent POM ([`pom.xml`](pom.xml)) that defines most of the common build configuration. The module POMs ([`name-service/pom.xml`](name-service/pom.xml) and [`hello-service/pom.xml`](hello-service/pom.xml)) just define inheritance on the parent POM. However, if needed, the module POMs can define custom configuration on `jib-maven-plugin` specific to that module. -The **Gradle** project is set up with a single [`build.gradle`](build.gradle) that configures both subprojects (modules). [`settings.gradle`](settings.gradle) defines which modules to include in the overall build. If needed, the subprojects can have `build.gradle` files to define custom configuration on `jib-gradle-plugin` specific to that module. +The **Gradle** project is set up with a parent [`build.gradle`](build.gradle) that sets some common configuration up for all projects, with each sub-project containing its own `build.gradle` with some custom configuration. [`settings.gradle`](settings.gradle) defines which modules to include in the overall build. + +## Reproducibility of dependency module `shared-library` + +Since dependency module builds happen with the underlying build system +(maven/gradle), we must add some extra configuration to ensure that the +resulting `jar` that is built conforms to our reproducibility expectations. +The module [`shared-library`](shared-library) uses the [Reproducible Build Maven Plugin](https://zlika.github.io/reproducible-build-maven-plugin/) +for maven, and some special `Jar` properties ([`preserveFileTimestamps`](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:preserveFileTimestamps), +[`reproducibleFileOrder`](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:reproducibleFileOrder)) +in gradle to achieve this. This configuration can be seen in the +`shared-library`'s [`pom.xml`](shared-library/pom.xml) and [`build.gradle`](shared-library/build.gradle). + +Care must be taken when adding custom attributes to a `MANIFEST.MF`. +Attributes whose values change on every build can affect reproducibility even +with the modifications outlined aboved. # How to run @@ -24,13 +40,29 @@ export PROJECT_ID=$(gcloud config list --format 'value(core.project)') Run the **Maven** build: ```shell -./mvnw compile jib:build +# build everything +./mvnw package jib:build + +# build just hello-service +./mvnw compile jib:build -pl hello-service + +# build name-service (with dependency on shared-library) +# you must use "package" for jib to correctly package "shared-library" with the +# "name-service" container +./mvnw package jib:build -pl name-service -am ``` Run the **Gradle** build: ```shell +# build everything ./gradlew jib + +# build just hello-service +./gradlew :hello-service:jib + +# build name-service (with dependency on shared-library) +./gradlew :name-service:jib ``` You can also run `./maven-build.sh` or `./gradle-build.sh` as a shorthand. @@ -68,5 +100,5 @@ svc/hello-service LoadBalancer 10.19.243.223 35.237.89.148 80:30196/TCP Visit the IP in your web browser and you should see: ``` -Hello Jib Multimodule +Hello Jib Multimodule: A string from 'shared-library' ``` diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index 0dc17dbe72..efa30a8787 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -1,36 +1,6 @@ -buildscript { - repositories { - mavenCentral() - maven { - url 'https://plugins.gradle.org/m2/' - } - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' - classpath 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' - classpath 'gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:1.8.0' - } -} - -subprojects { - apply plugin: 'java' - apply plugin: 'eclipse' - apply plugin: 'idea' - apply plugin: 'org.springframework.boot' - apply plugin: 'io.spring.dependency-management' - apply plugin: 'com.google.cloud.tools.jib' - - repositories { - mavenCentral() - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - - dependencies { - compile 'org.springframework.boot:spring-boot-starter-web' - } - - // IMPORTANT: Set the environment variable PROJECT_ID to your own Google Cloud Platform project. - jib.to.image = "gcr.io/${System.getenv('PROJECT_ID')}/${project.name}:${version}" +// Define plugin versions here, but only apply them where we need to +plugins { + id 'org.springframework.boot' version '2.0.3.RELEASE' apply false + id 'io.spring.dependency-management' version '1.0.6.RELEASE' apply false + id 'com.google.cloud.tools.jib' version '1.7.0' apply false } diff --git a/examples/multi-module/hello-service/build.gradle b/examples/multi-module/hello-service/build.gradle new file mode 100644 index 0000000000..58fff228a2 --- /dev/null +++ b/examples/multi-module/hello-service/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +// IMPORTANT: Set the environment variable PROJECT_ID to your own Google Cloud Platform project. +jib.to.image = "gcr.io/${System.getenv('PROJECT_ID')}/${project.name}:${version}" diff --git a/examples/multi-module/maven-build.sh b/examples/multi-module/maven-build.sh index 9b70f3ce28..7afa910f66 100755 --- a/examples/multi-module/maven-build.sh +++ b/examples/multi-module/maven-build.sh @@ -3,4 +3,9 @@ set -ex export PROJECT_ID=$(gcloud config list --format 'value(core.project)') -./mvnw compile jib:build +# if there are no intermodule dependencies, compile is enough to complete a jib build. +./mvnw compile jib:build -pl hello-service + +# multi module builds with dependencies on other modules in the build require that the +# "package" phase is executed as part of the build to correctly include module dependencies. +./mvnw package jib:build -pl name-service -am diff --git a/examples/multi-module/name-service/build.gradle b/examples/multi-module/name-service/build.gradle new file mode 100644 index 0000000000..3a10607627 --- /dev/null +++ b/examples/multi-module/name-service/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation project(":shared-library") +} + +// IMPORTANT: Set the environment variable PROJECT_ID to your own Google Cloud Platform project. +jib.to.image = "gcr.io/${System.getenv('PROJECT_ID')}/${project.name}:${version}" diff --git a/examples/multi-module/name-service/pom.xml b/examples/multi-module/name-service/pom.xml index e7b69203bb..8fa9dbe679 100644 --- a/examples/multi-module/name-service/pom.xml +++ b/examples/multi-module/name-service/pom.xml @@ -13,4 +13,13 @@ jib-multimodule 0.1.0 + + + + + com.example + shared-library + 0.1.0 + +
diff --git a/examples/multi-module/name-service/src/main/java/name/NameController.java b/examples/multi-module/name-service/src/main/java/name/NameController.java index 3473fdfde6..0ce97199d7 100644 --- a/examples/multi-module/name-service/src/main/java/name/NameController.java +++ b/examples/multi-module/name-service/src/main/java/name/NameController.java @@ -2,12 +2,13 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; +import common.SharedUtils; @RestController public class NameController { @RequestMapping("/") - public String getName() { - return "Jib Multimodule"; + public String getText() { + return "Jib Multimodule: " + SharedUtils.getText(); } } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 1cc1a91673..1395f43a88 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -23,6 +23,7 @@ name-service hello-service + shared-library diff --git a/examples/multi-module/settings.gradle b/examples/multi-module/settings.gradle index e87702674d..d37f386743 100644 --- a/examples/multi-module/settings.gradle +++ b/examples/multi-module/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'jib-multimodule' -include 'name-service', 'hello-service' +include 'name-service', 'hello-service', 'shared-library' diff --git a/examples/multi-module/shared-library/build.gradle b/examples/multi-module/shared-library/build.gradle new file mode 100644 index 0000000000..990d8c9a27 --- /dev/null +++ b/examples/multi-module/shared-library/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'eclipse' + id 'idea' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +// Since this library is included as a jar in our jib projects, we want the +// jar to built reproducibly. +jar { + preserveFileTimestamps false + reproducibleFileOrder true +} diff --git a/examples/multi-module/shared-library/gradle.properties b/examples/multi-module/shared-library/gradle.properties new file mode 100644 index 0000000000..3f63f85a9b --- /dev/null +++ b/examples/multi-module/shared-library/gradle.properties @@ -0,0 +1 @@ +version = 0.1.0 diff --git a/examples/multi-module/shared-library/pom.xml b/examples/multi-module/shared-library/pom.xml new file mode 100644 index 0000000000..ff5de91880 --- /dev/null +++ b/examples/multi-module/shared-library/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + shared-library + 0.1.0 + + + + com.example + jib-multimodule + 0.1.0 + + + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + io.github.zlika + reproducible-build-maven-plugin + 0.11 + + + run-when-packaged + + strip-jar + + package + + + + + + diff --git a/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java b/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java new file mode 100644 index 0000000000..fd3ad5202f --- /dev/null +++ b/examples/multi-module/shared-library/src/main/java/common/SharedUtils.java @@ -0,0 +1,8 @@ +package common; + +public class SharedUtils { + + public static String getText() { + return "A string from 'shared-library'"; + } +} \ No newline at end of file From 027cc518d190cae256ce277ca40e9c32a938beaf Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 18 Nov 2019 11:30:12 -0500 Subject: [PATCH 0835/2020] Minor fixes (#2151) --- .../cloud/tools/jib/gradle/skaffold/FilesTaskV2.java | 3 ++- .../google/cloud/tools/jib/gradle/skaffold/InitTask.java | 3 ++- .../google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java | 3 ++- .../skaffold/{SkaffoldInitMojo.java => InitMojo.java} | 7 ++++--- .../google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java | 3 ++- .../{SkaffoldInitMojoTest.java => InitMojoTest.java} | 6 +++--- 6 files changed, 15 insertions(+), 10 deletions(-) rename jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/{SkaffoldInitMojo.java => InitMojo.java} (90%) rename jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/{SkaffoldInitMojoTest.java => InitMojoTest.java} (96%) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java index 201afd3623..1a6378415b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -103,7 +103,8 @@ public void listFiles() throws IOException { } // Print files - System.out.println("\nBEGIN JIB JSON"); + System.out.println(); + System.out.println("BEGIN JIB JSON"); System.out.println(skaffoldFilesOutput.getJsonString()); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java index c077c10644..cbd7c6b5e7 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java @@ -51,7 +51,8 @@ public void listModulesAndTargets() throws IOException { if (!project.equals(project.getRootProject())) { skaffoldInitOutput.setProject(project.getName()); } - System.out.println("\nBEGIN JIB JSON"); + System.out.println(); + System.out.println("BEGIN JIB JSON"); System.out.println(skaffoldInitOutput.getJsonString()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index 60b74d817d..1cfa084a0e 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -208,7 +208,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { // Print JSON string - System.out.println("\nBEGIN JIB JSON"); + System.out.println(); + System.out.println("BEGIN JIB JSON"); System.out.println(skaffoldFilesOutput.getJsonString()); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java similarity index 90% rename from jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java rename to jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java index c9ff150bc7..da03dec374 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java @@ -30,8 +30,8 @@ * *

Expected use: {@code ./mvnw jib:_skaffold-init -q} */ -@Mojo(name = SkaffoldInitMojo.GOAL_NAME, requiresDependencyCollection = ResolutionScope.NONE) -public class SkaffoldInitMojo extends JibPluginConfiguration { +@Mojo(name = InitMojo.GOAL_NAME, requiresDependencyCollection = ResolutionScope.NONE) +public class InitMojo extends JibPluginConfiguration { @VisibleForTesting static final String GOAL_NAME = "_skaffold-init"; @@ -49,7 +49,8 @@ public void execute() throws MojoExecutionException { if (project.getParent() != null && project.getParent().getFile() != null) { skaffoldInitOutput.setProject(project.getArtifactId()); } - System.out.println("\nBEGIN JIB JSON"); + System.out.println(); + System.out.println("BEGIN JIB JSON"); try { System.out.println(skaffoldInitOutput.getJsonString()); } catch (IOException ex) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java index 2298399494..908c8a04fc 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java @@ -78,7 +78,8 @@ public void execute() throws MojoExecutionException { String syncMapJson = PluginConfigurationProcessor.getSkaffoldSyncMap(configuration, projectProperties); - System.out.println("\nBEGIN JIB JSON"); + System.out.println(); + System.out.println("BEGIN JIB JSON"); System.out.println(syncMapJson); } catch (Exception ex) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java similarity index 96% rename from jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java rename to jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java index cac2e8bfd7..ccbaf0cecf 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SkaffoldInitMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java @@ -34,8 +34,8 @@ import org.junit.ClassRule; import org.junit.Test; -/** Tests for {@link SkaffoldInitMojo}. */ -public class SkaffoldInitMojoTest { +/** Tests for {@link InitMojo}. */ +public class InitMojoTest { @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); @@ -54,7 +54,7 @@ private static List getJsons(TestProject project) verifier.setAutoclean(false); verifier.addCliOption("-q"); verifier.addCliOption("-Dimage=testimage"); - verifier.executeGoal("jib:" + SkaffoldInitMojo.GOAL_NAME); + verifier.executeGoal("jib:" + InitMojo.GOAL_NAME); verifier.verifyErrorFreeLog(); Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); From 8361d92a91239d8c2624c5542d58880e0f318635 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 18 Nov 2019 11:35:19 -0500 Subject: [PATCH 0836/2020] Remove deprecated extraDirectory config (#2108) --- docs/faq.md | 8 +- jib-gradle-plugin/CHANGELOG.md | 2 + .../gradle/SingleProjectIntegrationTest.java | 35 ------ .../simple/build-extra-dir-deprecated.gradle | 20 ---- .../simple/build-extra-dir-deprecated2.gradle | 18 --- .../simple/build-extra-dir-deprecated3.gradle | 18 --- .../gradle/ExtraDirectoriesParameters.java | 19 +--- .../cloud/tools/jib/gradle/JibExtension.java | 27 +---- .../cloud/tools/jib/gradle/TaskCommon.java | 17 --- .../tools/jib/gradle/JibExtensionTest.java | 32 ------ .../tools/jib/gradle/TaskCommonTest.java | 71 ------------ jib-maven-plugin/CHANGELOG.md | 2 + .../maven/BuildImageMojoIntegrationTest.java | 23 ---- .../jib/maven/JibPluginConfiguration.java | 86 +------------- .../tools/jib/maven/skaffold/FilesMojo.java | 35 ++---- .../tools/jib/maven/skaffold/FilesMojoV2.java | 35 +----- .../jib/maven/JibPluginConfigurationTest.java | 106 ------------------ .../maven/projects/simple/pom-complex.xml | 6 +- .../pom-deprecated-and-new-extra-dir.xml | 52 --------- .../simple/pom-deprecated-extra-dir.xml | 51 --------- .../maven/projects/simple/pom-localbase.xml | 4 +- .../projects/simple/pom-timestamps-custom.xml | 6 +- .../resources/maven/projects/simple/pom.xml | 4 +- .../jib/plugins/common/PropertyNames.java | 5 - 24 files changed, 42 insertions(+), 640 deletions(-) delete mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle delete mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle delete mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle delete mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml delete mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml diff --git a/docs/faq.md b/docs/faq.md index e42cd37c26..0d60fe9db3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -300,7 +300,11 @@ In Maven, you can use the `maven-resources-plugin` to copy files to your extra d jib-maven-plugin ... - ${project.basedir}/target/extra-directory/ + + + ${project.basedir}/target/extra-directory/ + + ... @@ -331,7 +335,7 @@ mvn compile resources:copy-resources jib:build The same can be accomplished in Gradle by using a `Copy` task. In your `build.gradle`: ```groovy -jib.extraDirectory = file('build/extra-directory') +jib.extraDirectories = file('build/extra-directory') task setupExtraDir(type: Copy) { from file('build/generated/files') diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 790351d373..3f3057bad1 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) + ### Fixed ## 1.8.0 diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index bddab8eedb..4695c1047d 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -127,23 +127,6 @@ static String readDigestFile(Path digestPath) throws IOException, DigestExceptio return DescriptorDigest.fromDigest(digest).toString(); } - private static void assertExtraDirectoryDeprecationWarning(String buildFile) - throws DigestException, IOException, InterruptedException { - String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); - BuildResult buildResult = - JibRunHelper.buildToDockerDaemon(simpleTestProject, targetImage, buildFile); - Assert.assertEquals( - "Hello, world. \n1970-01-01T00:00:01Z\nrw-r--r--\nrw-r--r--\nfoo\ncat\n" - + "1970-01-01T00:00:01Z\n1970-01-01T00:00:01Z\n", - new Command("docker", "run", "--rm", targetImage).run()); - Assert.assertThat( - buildResult.getOutput(), - CoreMatchers.containsString( - "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " - + "are deprecated; use 'jib.extraDirectories.paths' and " - + "'jib.extraDirectories.permissions'")); - } - private static String buildAndRunComplex( String imageReference, String username, String password, LocalRegistry targetRegistry) throws IOException, InterruptedException { @@ -318,24 +301,6 @@ public void testDockerDaemon_simpleWithIncompatibleJava11() } } - @Test - public void testDockerDaemon_simple_deprecatedExtraDirectory() - throws DigestException, IOException, InterruptedException { - assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated.gradle"); - } - - @Test - public void testDockerDaemon_simple_deprecatedExtraDirectory2() - throws DigestException, IOException, InterruptedException { - assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated2.gradle"); - } - - @Test - public void testDockerDaemon_simple_deprecatedExtraDirectory3() - throws DigestException, IOException, InterruptedException { - assertExtraDirectoryDeprecationWarning("build-extra-dir-deprecated3.gradle"); - } - @Test public void testDockerDaemon_simple_multipleExtraDirectories() throws DigestException, IOException, InterruptedException { diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle deleted file mode 100644 index f330a8f97a..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib.to.image = System.getProperty("_TARGET_IMAGE") -jib.extraDirectory { - path = file('src/main/custom-extra-dir') -} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle deleted file mode 100644 index 4eeb3e5c56..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated2.gradle +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib.to.image = System.getProperty("_TARGET_IMAGE") -jib.extraDirectory = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle deleted file mode 100644 index 3d0a53203f..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dir-deprecated3.gradle +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib.to.image = System.getProperty("_TARGET_IMAGE") -jib.extraDirectory.path = file('src/main/custom-extra-dir') diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java index 219b52eb94..596a3d1159 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java @@ -34,15 +34,13 @@ public class ExtraDirectoriesParameters { private final Project project; - @Deprecated private final JibExtension jibExtension; private List paths; private Map permissions = Collections.emptyMap(); @Inject - public ExtraDirectoriesParameters(Project project, JibExtension jibExtension) { + public ExtraDirectoriesParameters(Project project) { this.project = project; - this.jibExtension = jibExtension; paths = Collections.singletonList( project.getProjectDir().toPath().resolve("src").resolve("main").resolve("jib")); @@ -59,9 +57,7 @@ public List getPathStrings() { public List getPaths() { // Gradle warns about @Input annotations on File objects, so we have to expose a getter for a // String to make them go away. - String deprecatedProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); - String newProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); - String property = newProperty != null ? newProperty : deprecatedProperty; + String property = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); if (property != null) { List pathStrings = ConfigurationPropertyValidator.parseListProperty(property); return pathStrings.stream().map(Paths::get).collect(Collectors.toList()); @@ -76,17 +72,10 @@ public List getPaths() { * @param paths paths to set. */ public void setPaths(Object paths) { - jibExtension.extraDirectoriesConfigured = true; this.paths = project.files(paths).getFiles().stream().map(File::toPath).collect(Collectors.toList()); } - @Deprecated - public void setPath(File path) { - jibExtension.extraDirectoryConfigured = true; - this.paths = Collections.singletonList(path.toPath()); - } - /** * Gets the permissions for files in the extra layer on the container. Maps from absolute path on * the container to a 3-digit octal string representation of the file permission bits (e.g. {@code @@ -96,9 +85,7 @@ public void setPath(File path) { */ @Input public Map getPermissions() { - String deprecatedProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); - String newProperty = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); - String property = newProperty != null ? newProperty : deprecatedProperty; + String property = System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); if (property != null) { return ConfigurationPropertyValidator.parseMapProperty(property); } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java index 8f2d49aa00..41ed1a5fb1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.gradle; import com.google.cloud.tools.jib.plugins.common.PropertyNames; -import java.io.File; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; @@ -81,16 +80,13 @@ public class JibExtension { private final Property allowInsecureRegistries; private final Property containerizingMode; - @Deprecated boolean extraDirectoryConfigured; - @Deprecated boolean extraDirectoriesConfigured; - public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); from = objectFactory.newInstance(BaseImageParameters.class); to = objectFactory.newInstance(TargetImageParameters.class); container = objectFactory.newInstance(ContainerParameters.class); - extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project, this); + extraDirectories = objectFactory.newInstance(ExtraDirectoriesParameters.class, project); dockerClient = objectFactory.newInstance(DockerClientParameters.class); outputPaths = objectFactory.newInstance(OutputPathsParameters.class, project); @@ -114,14 +110,7 @@ public void container(Action action) { action.execute(container); } - @Deprecated - public void extraDirectory(Action action) { - extraDirectoryConfigured = true; - action.execute(extraDirectories); - } - public void extraDirectories(Action action) { - extraDirectoriesConfigured = true; action.execute(extraDirectories); } @@ -133,13 +122,6 @@ public void outputPaths(Action action) { action.execute(outputPaths); } - @Deprecated - // for the deprecated "jib.extraDirectory" config parameter - public void setExtraDirectory(File extraDirectory) { - extraDirectoryConfigured = true; - extraDirectories.setPath(extraDirectory); - } - public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } @@ -166,13 +148,6 @@ public ContainerParameters getContainer() { return container; } - @Deprecated - @Nested - @Optional - public ExtraDirectoriesParameters getExtraDirectory() { - return extraDirectories; - } - @Nested @Optional public ExtraDirectoriesParameters getExtraDirectories() { diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index ba011a0733..f2a2bc0d9d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -19,7 +19,6 @@ import com.google.api.client.http.HttpTransport; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; -import com.google.cloud.tools.jib.plugins.common.PropertyNames; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -77,22 +76,6 @@ static void disableHttpLogging() { @Deprecated static void checkDeprecatedUsage(JibExtension jibExtension, Logger logger) { - if (jibExtension.extraDirectoryConfigured - || System.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH) != null - || System.getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS) != null) { - logger.warn( - "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " - + "are deprecated; use 'jib.extraDirectories.paths' and " - + "'jib.extraDirectories.permissions'"); - - if (jibExtension.extraDirectoriesConfigured - || System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS) != null - || System.getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS) != null) { - throw new IllegalArgumentException( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); - } - } - if (jibExtension.getContainer().getUseCurrentTimestamp()) { if (!jibExtension.getContainer().getCreationTime().equals("EPOCH")) { throw new IllegalArgumentException( diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index bb1430e521..2901b1e4f0 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -147,24 +147,6 @@ public void testExtraDirectories_default() { Collections.emptyMap(), testJibExtension.getExtraDirectories().getPermissions()); } - @Test - public void testExtraDirectories_deprecatedConfig() { - testJibExtension.extraDirectory( - extraDirectory -> { - extraDirectory.setPath(Paths.get("test", "path").toFile()); - extraDirectory.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); - }); - Assert.assertTrue(testJibExtension.extraDirectoryConfigured); - Assert.assertFalse(testJibExtension.extraDirectoriesConfigured); - - Assert.assertEquals( - Arrays.asList(Paths.get("test", "path")), - testJibExtension.getExtraDirectories().getPaths()); - Assert.assertEquals( - ImmutableMap.of("file1", "123", "file2", "456"), - testJibExtension.getExtraDirectories().getPermissions()); - } - @Test public void testExtraDirectories() { testJibExtension.extraDirectories( @@ -172,8 +154,6 @@ public void testExtraDirectories() { extraDirectories.setPaths("test/path"); extraDirectories.setPermissions(ImmutableMap.of("file1", "123", "file2", "456")); }); - Assert.assertFalse(testJibExtension.extraDirectoryConfigured); - Assert.assertTrue(testJibExtension.extraDirectoriesConfigured); Assert.assertEquals( Arrays.asList(Paths.get(fakeProject.getProjectDir().getPath(), "test", "path")), @@ -357,16 +337,4 @@ public void testProperties() { fakeProject.getProjectDir().toPath().resolve(Paths.get("tar/path")), testJibExtension.getOutputPaths().getTarPath()); } - - @Test - public void testDeprecatedProperties() { - System.setProperty("jib.extraDirectory.path", "/foo,/bar/baz"); - Assert.assertEquals( - Arrays.asList(Paths.get("/foo"), Paths.get("/bar/baz")), - testJibExtension.getExtraDirectories().getPaths()); - System.setProperty("jib.extraDirectory.permissions", "/foo/bar=707,/baz=456"); - Assert.assertEquals( - ImmutableMap.of("/foo/bar", "707", "/baz", "456"), - testJibExtension.getExtraDirectories().getPermissions()); - } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index a5b625bd0a..3cfcc6d57b 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -48,8 +48,6 @@ public class TaskCommonTest { @Before public void setUp() { - System.clearProperty("jib.extraDirectory.path"); - System.clearProperty("jib.extraDirectory.permissions"); System.clearProperty("jib.extraDirectories.paths"); System.clearProperty("jib.extraDirectories.permissions"); Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); @@ -61,75 +59,6 @@ public void testCheckDeprecatedUsage_default() { Mockito.verify(logger, Mockito.never()).warn(Mockito.anyString()); } - @Test - public void testCheckDeprecatedUsage_extraDirectoriesConfigured() { - jibExtension.extraDirectoriesConfigured = true; - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger, Mockito.never()).warn(Mockito.anyString()); - } - - @Test - public void testCheckDeprecatedUsage_extraDirectoryPathPropertySet() { - System.setProperty("jib.extraDirectory.path", "something"); - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger, Mockito.times(1)) - .warn( - "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " - + "are deprecated; use 'jib.extraDirectories.paths' and " - + "'jib.extraDirectories.permissions'"); - } - - @Test - public void testCheckDeprecatedUsage_extraDirectoryPermissionsPropertySet() { - System.setProperty("jib.extraDirectory.permissions", "something"); - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger, Mockito.times(1)) - .warn( - "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " - + "are deprecated; use 'jib.extraDirectories.paths' and " - + "'jib.extraDirectories.permissions'"); - } - - @Test - public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesPropertiesSet() { - System.setProperty("jib.extraDirectory.path", "something"); - System.setProperty("jib.extraDirectories.permissions", "something"); - - try { - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", - ex.getMessage()); - } - } - - @Test - public void testCheckDeprecatedUsage_extraDirectoryConfigured() { - jibExtension.extraDirectoryConfigured = true; - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger, Mockito.times(1)) - .warn( - "'jib.extraDirectory', 'jib.extraDirectory.path', and 'jib.extraDirectory.permissions' " - + "are deprecated; use 'jib.extraDirectories.paths' and " - + "'jib.extraDirectories.permissions'"); - } - - @Test - public void testCheckDeprecatedUsage_extraDirectoryAndExtraDirectoriesConfigured() { - jibExtension.extraDirectoryConfigured = true; - jibExtension.extraDirectoriesConfigured = true; - try { - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", - ex.getMessage()); - } - } - @Test public void testCheckDeprecatedUsage_useCurrentTimestampConfigured() { Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index d2594ce7f9..4a3de952b4 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) + ### Fixed ## 1.8.0 diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 4bc74a52f1..6fa671d057 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -526,29 +526,6 @@ public void testExecute_multipleExtraDirectories() assertLayerSize(9, targetImage); // one more than usual } - @Test - public void testExecute_bothDeprecatedAndNewExtraDirectoryConfigUsed() throws IOException { - try { - build( - simpleTestProject.getProjectRoot(), "foo", "pom-deprecated-and-new-extra-dir.xml", false); - Assert.fail(); - } catch (VerificationException ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "You cannot configure both and ")); - } - } - - @Test - public void testExecute_deprecatedExtraDirectoryConfigUsed() - throws IOException, VerificationException { - String targetImage = getTestImageReference("simpleimage:maven"); - build(simpleTestProject.getProjectRoot(), targetImage, "pom-deprecated-extra-dir.xml", false) - .verifyTextInLog( - " is deprecated; use with "); - } - @Test public void testExecute_defaultTarget() throws IOException { // Test error when 'to' is missing diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 410504a9d5..55d2781eea 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -202,29 +202,6 @@ public List getPaths() { } } - /** Configuration for the {@code extraDirectory} parameter. */ - @Deprecated - public static class ExtraDirectoryParameters { - - // retained for backward-compatibility for ... - @Deprecated @Nullable @Parameter private File path; - - @Deprecated @Parameter - private List permissions = Collections.emptyList(); - - // Allows users to configure a single path using just instead of - // . - @Deprecated - public void set(File path) { - this.path = path; - } - - @Deprecated - public List getPaths() { - return path == null ? Collections.emptyList() : Collections.singletonList(path); - } - } - /** Configuration for the {@code dockerClient} parameter. */ public static class DockerClientParameters { @@ -260,10 +237,6 @@ public static class OutputPathsParameters { @Parameter private ContainerParameters container = new ContainerParameters(); - // this parameter is cloned in FilesMojo - @Deprecated @Parameter - private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); - // this parameter is cloned in FilesMojo @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); @@ -602,38 +575,12 @@ String getCreationTime() { */ List getExtraDirectories() { // TODO: Should inform user about nonexistent directory if using custom directory. - String deprecatedProperty = getProperty(PropertyNames.EXTRA_DIRECTORY_PATH); - String newProperty = getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); - - List deprecatedPaths = extraDirectory.getPaths(); - List newPaths = extraDirectories.getPaths(); - - if (deprecatedProperty != null) { - getLog() - .warn( - "The property 'jib.extraDirectory.path' is deprecated; " - + "use 'jib.extraDirectories.paths' instead"); - } - if (!deprecatedPaths.isEmpty()) { - getLog().warn(" is deprecated; use with "); - } - if (deprecatedProperty != null && newProperty != null) { - throw new IllegalArgumentException( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); - } - if (!deprecatedPaths.isEmpty() && !newPaths.isEmpty()) { - throw new IllegalArgumentException( - "You cannot configure both and "); - } - - String property = newProperty != null ? newProperty : deprecatedProperty; + String property = getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS); if (property != null) { List paths = ConfigurationPropertyValidator.parseListProperty(property); return paths.stream().map(Paths::get).collect(Collectors.toList()); } - - List paths = !newPaths.isEmpty() ? newPaths : deprecatedPaths; - return paths.stream().map(File::toPath).collect(Collectors.toList()); + return extraDirectories.getPaths().stream().map(File::toPath).collect(Collectors.toList()); } /** @@ -642,29 +589,7 @@ List getExtraDirectories() { * @return the configured extra layer file permissions */ List getExtraDirectoryPermissions() { - String deprecatedProperty = getProperty(PropertyNames.EXTRA_DIRECTORY_PERMISSIONS); - String newProperty = getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); - - List deprecatedPermissions = extraDirectory.permissions; - List newPermissions = extraDirectories.permissions; - - if (deprecatedProperty != null) { - getLog() - .warn( - "The property 'jib.extraDirectory.permissions' is deprecated; " - + "use 'jib.extraDirectories.permissions' instead"); - } - if (deprecatedProperty != null && newProperty != null) { - throw new IllegalArgumentException( - "You cannot configure both 'jib.extraDirectory.permissions' and " - + "'jib.extraDirectories.permissions'"); - } - if (!deprecatedPermissions.isEmpty() && !newPermissions.isEmpty()) { - throw new IllegalArgumentException( - "You cannot configure both and "); - } - - String property = newProperty != null ? newProperty : deprecatedProperty; + String property = getProperty(PropertyNames.EXTRA_DIRECTORIES_PERMISSIONS); if (property != null) { return ConfigurationPropertyValidator.parseMapProperty(property) .entrySet() @@ -672,10 +597,7 @@ List getExtraDirectoryPermissions() { .map(entry -> new PermissionConfiguration(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } - - return !extraDirectories.getPaths().isEmpty() - ? extraDirectories.permissions - : extraDirectory.permissions; + return extraDirectories.permissions; } @Nullable diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java index c7d2ce685c..961ba01a0f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojo.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.maven.skaffold; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.ExtraDirectoriesParameters; -import com.google.cloud.tools.jib.maven.JibPluginConfiguration.ExtraDirectoryParameters; import com.google.cloud.tools.jib.maven.MavenProjectProperties; import com.google.cloud.tools.jib.plugins.common.ConfigurationPropertyValidator; import com.google.cloud.tools.jib.plugins.common.PropertyNames; @@ -80,10 +79,6 @@ public class FilesMojo extends SkaffoldBindingMojo { // TODO: This is internal maven, we should find a better way to do this @Nullable @Component private ProjectDependenciesResolver projectDependenciesResolver; - // This parameter is cloned from JibPluginConfiguration - @Deprecated @Parameter - private ExtraDirectoryParameters extraDirectory = new ExtraDirectoryParameters(); - // This parameter is cloned from JibPluginConfiguration @Parameter private ExtraDirectoriesParameters extraDirectories = new ExtraDirectoriesParameters(); @@ -161,38 +156,26 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private List resolveExtraDirectories() throws MojoExecutionException { + private List resolveExtraDirectories() { // TODO: Should inform user about nonexistent directory if using custom directory. - String deprecatedProperty = - MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); - String newProperty = + String property = MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS, project, session); - - List deprecatedPaths = extraDirectory.getPaths(); - List newPaths = extraDirectories.getPaths(); - - if (deprecatedProperty != null && newProperty != null) { - throw new MojoExecutionException( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); - } - if (!deprecatedPaths.isEmpty() && !newPaths.isEmpty()) { - throw new MojoExecutionException( - "You cannot configure both and "); - } - - String property = newProperty != null ? newProperty : deprecatedProperty; if (property != null) { List paths = ConfigurationPropertyValidator.parseListProperty(property); return paths.stream().map(Paths::get).map(Path::toAbsolutePath).collect(Collectors.toList()); } - List paths = !newPaths.isEmpty() ? newPaths : deprecatedPaths; - if (paths.isEmpty()) { + if (extraDirectories.getPaths().isEmpty()) { Path projectBase = Preconditions.checkNotNull(project).getBasedir().getAbsoluteFile().toPath(); Path srcMainJib = Paths.get("src", "main", "jib"); return Collections.singletonList(projectBase.resolve(srcMainJib)); } - return paths.stream().map(File::getAbsoluteFile).map(File::toPath).collect(Collectors.toList()); + return extraDirectories + .getPaths() + .stream() + .map(File::getAbsoluteFile) + .map(File::toPath) + .collect(Collectors.toList()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java index 1cfa084a0e..da1ea59807 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoV2.java @@ -216,26 +216,17 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private List resolveExtraDirectories(MavenProject project) throws MojoExecutionException { + private List resolveExtraDirectories(MavenProject project) { return collectExtraDirectories(project) .stream() .map(path -> path.isAbsolute() ? path : project.getBasedir().toPath().resolve(path)) .collect(Collectors.toList()); } - private List collectExtraDirectories(MavenProject project) throws MojoExecutionException { + private List collectExtraDirectories(MavenProject project) { // Try getting extra directory from project/session properties - String deprecatedProperty = - MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORY_PATH, project, session); - String newProperty = + String property = MavenProjectProperties.getProperty(PropertyNames.EXTRA_DIRECTORIES_PATHS, project, session); - - if (deprecatedProperty != null && newProperty != null) { - throw new MojoExecutionException( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'"); - } - - String property = newProperty != null ? newProperty : deprecatedProperty; if (property != null) { List paths = ConfigurationPropertyValidator.parseListProperty(property); return paths.stream().map(Paths::get).collect(Collectors.toList()); @@ -246,14 +237,7 @@ private List collectExtraDirectories(MavenProject project) throws MojoExec if (jibMavenPlugin != null) { Xpp3Dom pluginConfiguration = (Xpp3Dom) jibMavenPlugin.getConfiguration(); if (pluginConfiguration != null) { - - Xpp3Dom extraDirectoryConfiguration = pluginConfiguration.getChild("extraDirectory"); Xpp3Dom extraDirectoriesConfiguration = pluginConfiguration.getChild("extraDirectories"); - if (extraDirectoryConfiguration != null && extraDirectoriesConfiguration != null) { - throw new MojoExecutionException( - "You cannot configure both and "); - } - if (extraDirectoriesConfiguration != null) { Xpp3Dom child = extraDirectoriesConfiguration.getChild("paths"); if (child != null) { @@ -264,19 +248,6 @@ private List collectExtraDirectories(MavenProject project) throws MojoExec .collect(Collectors.toList()); } } - - if (extraDirectoryConfiguration != null) { - Xpp3Dom child = extraDirectoryConfiguration.getChild("path"); - if (child != null) { - // ... - return Collections.singletonList(Paths.get(child.getValue())); - } - // ... - String value = extraDirectoryConfiguration.getValue(); - if (value != null) { - return Collections.singletonList(Paths.get(extraDirectoryConfiguration.getValue())); - } - } } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 0a335c31d7..2a9f942b49 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -280,112 +280,6 @@ public void testEmptyOrNullTags() { } } - @Test - public void testDeprecatedSystemProperties_extraDirectory() { - sessionProperties.put("jib.extraDirectory.path", "custom-jib"); - Assert.assertEquals( - Arrays.asList(Paths.get("custom-jib")), testPluginConfiguration.getExtraDirectories()); - sessionProperties.put("jib.extraDirectory.permissions", "/test/file13=650,/another/file24=777"); - List permissions = - testPluginConfiguration.getExtraDirectoryPermissions(); - Assert.assertEquals("/test/file13", permissions.get(0).getFile().get()); - Assert.assertEquals("650", permissions.get(0).getMode().get()); - Assert.assertEquals("/another/file24", permissions.get(1).getFile().get()); - Assert.assertEquals("777", permissions.get(1).getMode().get()); - - Mockito.verify(log, Mockito.times(1)) - .warn( - "The property 'jib.extraDirectory.path' is deprecated; " - + "use 'jib.extraDirectories.paths' instead"); - } - - @Test - public void testDeprecatedProperties_extraDirectory() { - Properties projectProperties = project.getProperties(); - - projectProperties.setProperty("jib.extraDirectory.path", "this-is-extra"); - Assert.assertEquals( - Arrays.asList(Paths.get("this-is-extra")), testPluginConfiguration.getExtraDirectories()); - - projectProperties.setProperty( - "jib.extraDirectory.permissions", "/test/file1=654,/dir/file2=321"); - List permissions = - testPluginConfiguration.getExtraDirectoryPermissions(); - Assert.assertEquals("/test/file1", permissions.get(0).getFile().get()); - Assert.assertEquals("654", permissions.get(0).getMode().get()); - Assert.assertEquals("/dir/file2", permissions.get(1).getFile().get()); - Assert.assertEquals("321", permissions.get(1).getMode().get()); - - Mockito.verify(log, Mockito.times(1)) - .warn( - "The property 'jib.extraDirectory.path' is deprecated; " - + "use 'jib.extraDirectories.paths' instead"); - } - - @Test - public void testGetExtraDirectories_bothSystemPropertiesUsed() { - sessionProperties.put("jib.extraDirectory.path", "deprecated-property"); - sessionProperties.put("jib.extraDirectories.paths", "new-property"); - - try { - testPluginConfiguration.getExtraDirectories(); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", - ex.getMessage()); - } - } - - @Test - public void testGetExtraDirectories_bothPropertiesUsed() { - Properties projectProperties = project.getProperties(); - projectProperties.setProperty("jib.extraDirectory.path", "deprecated-property"); - projectProperties.setProperty("jib.extraDirectories.paths", "new-property"); - - try { - testPluginConfiguration.getExtraDirectories(); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.path' and 'jib.extraDirectories.paths'", - ex.getMessage()); - } - } - - @Test - public void testGetExtraDirectoryPermissions_bothSystemPropertiesUsed() { - sessionProperties.put("jib.extraDirectory.permissions", "deprecated-property"); - sessionProperties.put("jib.extraDirectories.permissions", "new-property"); - - try { - testPluginConfiguration.getExtraDirectoryPermissions(); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.permissions' and " - + "'jib.extraDirectories.permissions'", - ex.getMessage()); - } - } - - @Test - public void testGetExtraDirectoryPermissions_bothPropertiesUsed() { - Properties projectProperties = project.getProperties(); - projectProperties.setProperty("jib.extraDirectory.permissions", "deprecated-property"); - projectProperties.setProperty("jib.extraDirectories.permissions", "new-property"); - - try { - testPluginConfiguration.getExtraDirectoryPermissions(); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.extraDirectory.permissions' and " - + "'jib.extraDirectories.permissions'", - ex.getMessage()); - } - } - @Test public void testIsContainerizable_noProperty() { Properties projectProperties = project.getProperties(); diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml index 4853e833c4..54f6d615aa 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex.xml @@ -82,8 +82,8 @@ Docker - - ${project.basedir}/src/main/jib-custom + + ${project.basedir}/src/main/jib-custom /foo @@ -94,7 +94,7 @@ 777 - + ${project.build.directory}/different-jib-image.digest different-jib-image.id diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml deleted file mode 100644 index 19f2919955..0000000000 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-and-new-extra-dir.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - 4.0.0 - - com.test - my-artifact-id - 1 - - - UTF-8 - UTF-8 - @@PluginVersion@@ - ${_TARGET_IMAGE} - An argument. - 1000/tcp,2000-2003/udp - key1=value1,key2=value2 - - - - - com.test - dependency - 1.0.0 - system - ${project.basedir}/libs/dependency-1.0.0.jar - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - deprecated-config - good-config - - - - - diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml deleted file mode 100644 index ec9beeaa69..0000000000 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-deprecated-extra-dir.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - 4.0.0 - - com.test - my-artifact-id - 1 - - - UTF-8 - UTF-8 - @@PluginVersion@@ - ${_TARGET_IMAGE} - An argument. - 1000/tcp,2000-2003/udp - key1=value1,key2=value2 - - - - - com.test - dependency - 1.0.0 - system - ${project.basedir}/libs/dependency-1.0.0.jar - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - deprecated-config - - - - - diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml index bca546479a..f577138f1d 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-localbase.xml @@ -62,7 +62,9 @@ /home - ${project.basedir}/src/main/jib-custom + + ${project.basedir}/src/main/jib-custom + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml index e9928e11d8..92a9d2bc48 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-timestamps-custom.xml @@ -65,9 +65,9 @@ 2019-06-17T16:30:00Z 2013-11-05T06:29:30Z - - src/main/jib-custom - + + src/main/jib-custom + true diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml index 96992bee67..4c59c9ecc2 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom.xml @@ -59,7 +59,9 @@ /home - ${project.basedir}/src/main/jib-custom + + ${project.basedir}/src/main/jib-custom + ${project.build.directory}/different-jib-image.tar diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index db27429987..92a9e0f09b 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -65,10 +65,5 @@ public class PropertyNames { @Deprecated public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; - @Deprecated public static final String EXTRA_DIRECTORY_PATH = "jib.extraDirectory.path"; - - @Deprecated - public static final String EXTRA_DIRECTORY_PERMISSIONS = "jib.extraDirectory.permissions"; - private PropertyNames() {} } From f445c1ab31d33507b100b3465232c8d73eb6bf92 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 18 Nov 2019 12:29:29 -0500 Subject: [PATCH 0837/2020] Remove skaffold files v1 task/goal (#2084) --- .../cloud/tools/jib/gradle/JibPlugin.java | 5 - .../tools/jib/gradle/skaffold/FilesTask.java | 186 ------------------ .../jib/gradle/skaffold/FilesTaskV2.java | 4 +- .../jib/gradle/skaffold/FilesTaskTest.java | 133 ------------- .../jib/maven/skaffold/FilesMojoTest.java | 142 ------------- 5 files changed, 2 insertions(+), 468 deletions(-) delete mode 100644 jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java delete mode 100644 jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java delete mode 100644 jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 7d26b9e801..e186668928 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.gradle.skaffold.CheckJibVersionTask; -import com.google.cloud.tools.jib.gradle.skaffold.FilesTask; import com.google.cloud.tools.jib.gradle.skaffold.FilesTaskV2; import com.google.cloud.tools.jib.gradle.skaffold.InitTask; import com.google.cloud.tools.jib.gradle.skaffold.SyncMapTask; @@ -48,7 +47,6 @@ public class JibPlugin implements Plugin { public static final String BUILD_IMAGE_TASK_NAME = "jib"; public static final String BUILD_TAR_TASK_NAME = "jibBuildTar"; public static final String BUILD_DOCKER_TASK_NAME = "jibDockerBuild"; - public static final String SKAFFOLD_FILES_TASK_NAME = "_jibSkaffoldFiles"; public static final String SKAFFOLD_FILES_TASK_V2_NAME = "_jibSkaffoldFilesV2"; public static final String SKAFFOLD_INIT_TASK_NAME = "_jibSkaffoldInit"; public static final String SKAFFOLD_SYNC_MAP_TASK_NAME = "_jibSkaffoldSyncMap"; @@ -151,9 +149,6 @@ public void apply(Project project) { task.setJibExtension(jibExtension); }); - tasks - .register(SKAFFOLD_FILES_TASK_NAME, FilesTask.class) - .configure(task -> task.setJibExtension(jibExtension)); tasks .register(SKAFFOLD_FILES_TASK_V2_NAME, FilesTaskV2.class) .configure(task -> task.setJibExtension(jibExtension)); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java deleted file mode 100644 index ae5e5adeb2..0000000000 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTask.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle.skaffold; - -import com.google.cloud.tools.jib.gradle.JibExtension; -import com.google.common.base.Preconditions; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import javax.annotation.Nullable; -import org.gradle.api.DefaultTask; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.artifacts.PublishArtifact; -import org.gradle.api.initialization.Settings; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskAction; - -/** - * Print out changing source dependencies on a project. - * - *

Expected use: {@code ./gradlew _jibSkaffoldFiles -q} or {@code ./gradlew - * ::_jibSkaffoldFiles -q} - */ -public class FilesTask extends DefaultTask { - - /** - * Prints the locations of a project's build.gradle, settings.gradle, and gradle.properties. - * - * @param project the project - */ - private static void printGradleFiles(Project project) { - Path projectPath = project.getProjectDir().toPath(); - - // Print build.gradle - System.out.println(project.getBuildFile()); - - // Print settings.gradle - if (project.getGradle().getStartParameter().getSettingsFile() != null) { - System.out.println(project.getGradle().getStartParameter().getSettingsFile()); - } else if (Files.exists(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE))) { - System.out.println(projectPath.resolve(Settings.DEFAULT_SETTINGS_FILE)); - } - - // Print gradle.properties - if (Files.exists(projectPath.resolve("gradle.properties"))) { - System.out.println(projectPath.resolve("gradle.properties")); - } - } - - /** - * Prints build files, sources, and resources associated with a project. - * - * @param project the project - */ - private static void printProjectFiles(Project project) { - JavaPluginConvention javaConvention = - project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = - javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME); - if (mainSourceSet == null) { - return; - } - - // Print build config, settings, etc. - printGradleFiles(project); - - // Print sources + resources - mainSourceSet - .getAllSource() - .getSourceDirectories() - .forEach( - sourceDirectory -> { - if (sourceDirectory.exists()) { - System.out.println(sourceDirectory); - } - }); - } - - /** - * Collects a project's project dependencies, including all transitive project dependencies. - * - * @param project the project to find the project dependencies for - * @return the set of project dependencies - */ - private static Set findProjectDependencies(Project project) { - Set projectDependencies = new HashSet<>(); - Deque projects = new ArrayDeque<>(); - projects.push(project); - - while (!projects.isEmpty()) { - Project currentProject = projects.pop(); - - // Search through all dependencies - for (Configuration configuration : - currentProject.getConfigurations().getByName("runtime").getHierarchy()) { - for (Dependency dependency : configuration.getDependencies()) { - if (dependency instanceof ProjectDependency) { - // If this is a project dependency, save it - ProjectDependency projectDependency = (ProjectDependency) dependency; - if (!projectDependencies.contains(projectDependency)) { - projects.push(projectDependency.getDependencyProject()); - projectDependencies.add(projectDependency); - } - } - } - } - } - return projectDependencies; - } - - @Nullable private JibExtension jibExtension; - - public FilesTask setJibExtension(JibExtension jibExtension) { - this.jibExtension = jibExtension; - return this; - } - - @TaskAction - public void listFiles() { - Preconditions.checkNotNull(jibExtension); - Project project = getProject(); - - // If this is not the root project, print the root project's build.gradle and settings.gradle - if (project != project.getRootProject()) { - printGradleFiles(project.getRootProject()); - } - - printProjectFiles(project); - - // Print extra layer - List extraDirectories = jibExtension.getExtraDirectories().getPaths(); - extraDirectories.stream().filter(Files::exists).forEach(System.out::println); - - // Find project dependencies - Set projectDependencies = findProjectDependencies(project); - - Set projectDependencyJars = new HashSet<>(); - for (ProjectDependency projectDependency : projectDependencies) { - printProjectFiles(projectDependency.getDependencyProject()); - - // Keep track of project dependency jars for filtering out later - String configurationName = projectDependency.getTargetConfiguration(); - if (configurationName == null) { - configurationName = "default"; - } - Project dependencyProject = projectDependency.getDependencyProject(); - for (Configuration targetConfiguration : - dependencyProject.getConfigurations().getByName(configurationName).getHierarchy()) { - for (PublishArtifact artifact : targetConfiguration.getArtifacts()) { - projectDependencyJars.add(artifact.getFile()); - } - } - } - - // Print out SNAPSHOT, non-project dependency jars - for (File file : project.getConfigurations().getByName("runtime")) { - if (!projectDependencyJars.contains(file) && file.toString().contains("SNAPSHOT")) { - System.out.println(file); - projectDependencyJars.add(file); // Add to set to avoid printing the same files twice - } - } - } -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java index 1a6378415b..3cebdaa420 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -43,8 +43,8 @@ /** * Prints out changing source dependencies on a project. * - *

Expected use: {@code ./gradlew _jibSkaffoldFiles -q} or {@code ./gradlew - * ::_jibSkaffoldFiles -q} + *

Expected use: {@code ./gradlew _jibSkaffoldFilesV2 -q} or {@code ./gradlew + * ::_jibSkaffoldFilesV2 -q} */ public class FilesTaskV2 extends DefaultTask { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java deleted file mode 100644 index 4ca2413141..0000000000 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.gradle.skaffold; - -import com.google.cloud.tools.jib.gradle.JibPlugin; -import com.google.cloud.tools.jib.gradle.TestProject; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.BuildTask; -import org.gradle.testkit.runner.TaskOutcome; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Test; - -/** Tests for {@link FilesTask}. */ -public class FilesTaskTest { - - @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - - @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); - - /** - * Verifies that the files task succeeded and returns the list of paths it prints out. - * - * @param project the project to run the task on - * @param moduleName the name of the sub-project, or {@code null} if no sub-project - * @return the list of paths printed by the task - */ - private static List verifyTaskSuccess(TestProject project, @Nullable String moduleName) { - String taskName = - ":" + (moduleName == null ? "" : moduleName + ":") + JibPlugin.SKAFFOLD_FILES_TASK_NAME; - BuildResult buildResult = project.build(taskName, "-q"); - BuildTask jibTask = buildResult.task(taskName); - Assert.assertNotNull(jibTask); - Assert.assertEquals(TaskOutcome.SUCCESS, jibTask.getOutcome()); - - return Splitter.on(System.lineSeparator()) - .omitEmptyStrings() - .splitToList(buildResult.getOutput()) - .stream() - .map(Paths::get) - .collect(Collectors.toList()); - } - - /** - * Asserts that two lists contain the same paths. Required to avoid Mac's /var/ vs. /private/var/ - * symlink issue. - * - * @param expected the expected list of paths - * @param actual the actual list of paths - * @throws IOException if checking if two files are the same fails - */ - private static void assertPathListsAreEqual(List expected, List actual) - throws IOException { - Assert.assertEquals(expected.size(), actual.size()); - for (int index = 0; index < expected.size(); index++) { - Assert.assertEquals(expected.get(index).toRealPath(), actual.get(index).toRealPath()); - } - } - - @Test - public void testFilesTask_singleProject() throws IOException { - Path projectRoot = simpleTestProject.getProjectRoot(); - List result = verifyTaskSuccess(simpleTestProject, null); - List expected = - ImmutableList.of( - projectRoot.resolve("build.gradle"), - projectRoot.resolve("src/main/resources"), - projectRoot.resolve("src/main/java"), - projectRoot.resolve("src/main/custom-extra-dir")); - assertPathListsAreEqual(expected, result); - } - - @Test - public void testFilesTask_multiProjectSimpleService() throws IOException { - Path projectRoot = multiTestProject.getProjectRoot(); - Path simpleServiceRoot = projectRoot.resolve("simple-service"); - List result = verifyTaskSuccess(multiTestProject, "simple-service"); - List expected = - ImmutableList.of( - projectRoot.resolve("build.gradle"), - projectRoot.resolve("settings.gradle"), - projectRoot.resolve("gradle.properties"), - simpleServiceRoot.resolve("build.gradle"), - simpleServiceRoot.resolve("src/main/java")); - assertPathListsAreEqual(expected, result); - } - - @Test - public void testFilesTask_multiProjectComplexService() throws IOException { - Path projectRoot = multiTestProject.getProjectRoot(); - Path complexServiceRoot = projectRoot.resolve("complex-service"); - Path libRoot = projectRoot.resolve("lib"); - List result = verifyTaskSuccess(multiTestProject, "complex-service"); - List expected = - ImmutableList.of( - projectRoot.resolve("build.gradle"), - projectRoot.resolve("settings.gradle"), - projectRoot.resolve("gradle.properties"), - complexServiceRoot.resolve("build.gradle"), - complexServiceRoot.resolve("src/main/extra-resources-1"), - complexServiceRoot.resolve("src/main/extra-resources-2"), - complexServiceRoot.resolve("src/main/java"), - complexServiceRoot.resolve("src/main/other-jib"), - libRoot.resolve("build.gradle"), - libRoot.resolve("src/main/resources"), - libRoot.resolve("src/main/java"), - complexServiceRoot.resolve( - "local-m2-repo/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar")); - assertPathListsAreEqual(expected, result); - } -} diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java deleted file mode 100644 index f754855143..0000000000 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/FilesMojoTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.maven.skaffold; - -import com.google.cloud.tools.jib.maven.TestProject; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import org.apache.maven.it.VerificationException; -import org.apache.maven.it.Verifier; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Test; - -/** Tests for {@link FilesMojo}. */ -public class FilesMojoTest { - - @ClassRule public static final TestProject simpleTestProject = new TestProject("simple"); - - @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); - - private static void verifyFiles(Path projectRoot, String pomXml, String module, List files) - throws VerificationException, IOException { - - Verifier verifier = new Verifier(projectRoot.toString()); - verifier.setAutoclean(false); - verifier.addCliOption("--file=" + pomXml); - verifier.addCliOption("-q"); - if (!Strings.isNullOrEmpty(module)) { - verifier.addCliOption("-pl"); - verifier.addCliOption(module); - verifier.addCliOption("-am"); - } - verifier.executeGoal("jib:" + FilesMojo.GOAL_NAME); - - verifier.verifyErrorFreeLog(); - Path logFile = Paths.get(verifier.getBasedir()).resolve(verifier.getLogFileName()); - List log = Files.readAllLines(logFile, StandardCharsets.UTF_8); - - List expectedResult = - files.stream().map(Path::toAbsolutePath).map(Path::toString).collect(Collectors.toList()); - - Assert.assertEquals(expectedResult, log); - } - - @Test - public void testFilesMojo_singleModule() throws VerificationException, IOException { - Path projectRoot = simpleTestProject.getProjectRoot(); - - verifyFiles( - projectRoot, - "pom.xml", - null, - ImmutableList.of( - projectRoot.resolve("pom.xml"), - projectRoot.resolve("src/main/java"), - projectRoot.resolve("src/main/resources"), - projectRoot.resolve("src/main/jib-custom"))); - } - - @Test - public void testFilesMojo_singleModuleWithMultipleExtraDirectories() - throws VerificationException, IOException { - Path projectRoot = simpleTestProject.getProjectRoot(); - - verifyFiles( - projectRoot, - "pom-extra-dirs.xml", - null, - ImmutableList.of( - projectRoot.resolve("pom-extra-dirs.xml"), - projectRoot.resolve("src/main/java"), - projectRoot.resolve("src/main/resources"), - projectRoot.resolve("src/main/jib-custom"), - projectRoot.resolve("src/main/jib-custom-2"))); - } - - @Test - public void testFilesMojo_multiModuleSimpleService() throws VerificationException, IOException { - Path projectRoot = multiTestProject.getProjectRoot(); - Path simpleServiceRoot = projectRoot.resolve("simple-service"); - - verifyFiles( - projectRoot, - "pom.xml", - "simple-service", - ImmutableList.of( - projectRoot.resolve("pom.xml"), - simpleServiceRoot.resolve("pom.xml"), - simpleServiceRoot.resolve("src/main/java"), - simpleServiceRoot.resolve("src/main/resources"), - simpleServiceRoot.resolve("src/main/jib"))); - } - - @Test - public void testFilesMojo_multiModuleComplexService() throws VerificationException, IOException { - Path projectRoot = multiTestProject.getProjectRoot(); - Path complexServiceRoot = projectRoot.resolve("complex-service"); - Path libRoot = projectRoot.resolve("lib"); - - verifyFiles( - projectRoot, - "pom.xml", - "complex-service", - ImmutableList.of( - projectRoot.resolve("pom.xml"), - libRoot.resolve("pom.xml"), - libRoot.resolve("src/main/java"), - libRoot.resolve("src/main/resources"), - complexServiceRoot.resolve("pom.xml"), - complexServiceRoot.resolve("src/main/java"), - complexServiceRoot.resolve("src/main/resources1"), - complexServiceRoot.resolve("src/main/resources2"), - complexServiceRoot.resolve("src/main/jib1"), - complexServiceRoot.resolve("src/main/jib2"), - Paths.get("/some/random/absolute/path/jib3"), - // this test expects standard .m2 locations - Paths.get( - System.getProperty("user.home"), - ".m2/repository/com/google/cloud/tools/tiny-test-lib/0.0.1-SNAPSHOT/tiny-test-lib-0.0.1-SNAPSHOT.jar"))); - } -} From 4e6f7f5d6aaef689c67d59bfd9ebe843f7320dbc Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 18 Nov 2019 14:45:38 -0500 Subject: [PATCH 0838/2020] Remove deprecated jibDockerBuild.dockerClient (#2109) --- jib-gradle-plugin/CHANGELOG.md | 1 + .../tools/jib/gradle/BuildDockerTask.java | 38 ------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3f3057bad1..3a05a9b68c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Changed +- Removed `jibDockerBuild.dockerClient` in favor of `jib.dockerClient`. ([#1983](https://github.com/GoogleContainerTools/jib/issues/1983)) - Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) ### Fixed diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 3c7a670970..27b3dbd9c6 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -34,13 +34,10 @@ import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; -import java.util.Map; import javax.annotation.Nullable; -import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.tasks.Nested; -import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; @@ -51,8 +48,6 @@ public class BuildDockerTask extends DefaultTask implements JibTask { @Nullable private JibExtension jibExtension; - private final DockerClientParameters dockerClientParameters = new DockerClientParameters(); - /** * This will call the property {@code "jib"} so that it is the same name as the extension. This * way, the user would see error messages for missing configuration with the prefix {@code jib.}. @@ -75,18 +70,6 @@ public void setTargetImage(String targetImage) { Preconditions.checkNotNull(jibExtension).getTo().setImage(targetImage); } - @Nested - @Optional - @Deprecated - public DockerClientParameters getDockerClient() { - return dockerClientParameters; - } - - @Deprecated - public void dockerClient(Action action) { - action.execute(dockerClientParameters); - } - @TaskAction public void buildDocker() throws IOException, BuildStepsExecutionException, CacheDirectoryCreationException, @@ -95,27 +78,6 @@ public void buildDocker() // Check deprecated parameters Path dockerExecutable = jibExtension.getDockerClient().getExecutablePath(); - Map dockerEnvironment = jibExtension.getDockerClient().getEnvironment(); - if (getDockerClient().getExecutable() != null) { - jibExtension.getDockerClient().setExecutable(getDockerClient().getExecutable()); - getProject() - .getLogger() - .warn( - "'jibDockerBuild.dockerClient.executable' is deprecated; use 'jib.dockerClient.executable' instead."); - } - if (!getDockerClient().getEnvironment().isEmpty()) { - jibExtension.getDockerClient().setEnvironment(getDockerClient().getEnvironment()); - getProject() - .getLogger() - .warn( - "'jibDockerBuild.dockerClient.environment' is deprecated; use 'jib.dockerClient.environment' instead."); - } - if ((getDockerClient().getExecutable() != null && dockerExecutable != null) - || (!getDockerClient().getEnvironment().isEmpty() && !dockerEnvironment.isEmpty())) { - throw new GradleException( - "Cannot configure 'jibDockerBuild.dockerClient' and 'jib.dockerClient' simultaneously"); - } - boolean isDockerInstalled = dockerExecutable == null ? DockerClient.isDefaultDockerInstalled() From 14a317435a3cf31076ec5f3e6648ff382b442dee Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 18 Nov 2019 15:52:41 -0500 Subject: [PATCH 0839/2020] Fix dockerClient configuration in integration test (#2158) --- .../gradle/projects/simple/build-dockerclient.gradle | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle index 36d0ff075c..e08a14880a 100644 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle @@ -18,11 +18,8 @@ jib { to { image = System.getProperty("_TARGET_IMAGE") } -} - -jibDockerBuild { dockerClient { executable = file('mock-docker.sh') environment = [envvar1:'value1', envvar2:'value2'] } -} +} \ No newline at end of file From 5ce7ac17403de8cae675d0ee8f76cb519becdcf4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 19 Nov 2019 12:39:50 -0500 Subject: [PATCH 0840/2020] Update Maven CHANGELOG for (#2162) * Update CHANGELOG.md * Update jib-maven-plugin/CHANGELOG.md Co-Authored-By: Tad Cordle --- jib-maven-plugin/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4a3de952b4..58be4538b0 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed `` being ignored if `` are not explicitly defined. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2160)) + ## 1.8.0 ## Changed From ab558c787813bae7e2284bc07b501a55ca77c1c9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 19 Nov 2019 15:11:04 -0500 Subject: [PATCH 0841/2020] Update ISSUE_TEMPLATE.md (#2164) --- .github/ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index fb190d0958..895f0e49b5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,9 +9,9 @@ please check our Frequently Asked Questions before filing an issue: **Environment**: -- *Jib version:* +- *Jib version:* - *Build tool:* -- *OS:* +- *OS:* **Description of the issue**: From 701fc31afce8850bf380d6e38cf5f178991448b9 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 20 Nov 2019 11:22:45 -0500 Subject: [PATCH 0842/2020] Remove deprecated useCurrentTimestamp config (#2110) --- docs/faq.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + .../gradle/SingleProjectIntegrationTest.java | 31 ----------- .../simple/build-usecurrent-deprecated.gradle | 20 ------- .../build-usecurrent-deprecated2.gradle | 21 -------- .../tools/jib/gradle/BuildDockerTask.java | 1 - .../tools/jib/gradle/BuildImageTask.java | 1 - .../cloud/tools/jib/gradle/BuildTarTask.java | 1 - .../tools/jib/gradle/ContainerParameters.java | 13 ----- .../jib/gradle/GradleRawConfiguration.java | 5 -- .../cloud/tools/jib/gradle/TaskCommon.java | 15 ------ .../gradle/GradleRawConfigurationTest.java | 2 - .../tools/jib/gradle/JibExtensionTest.java | 2 - .../tools/jib/gradle/TaskCommonTest.java | 40 -------------- jib-maven-plugin/CHANGELOG.md | 1 + .../maven/BuildImageMojoIntegrationTest.java | 27 ---------- .../tools/jib/maven/BuildDockerMojo.java | 2 - .../cloud/tools/jib/maven/BuildImageMojo.java | 2 - .../cloud/tools/jib/maven/BuildTarMojo.java | 2 - .../jib/maven/JibPluginConfiguration.java | 16 ------ .../jib/maven/MavenRawConfiguration.java | 5 -- .../cloud/tools/jib/maven/MojoCommon.java | 16 ------ .../jib/maven/JibPluginConfigurationTest.java | 4 -- .../jib/maven/MavenRawConfigurationTest.java | 2 - .../simple/pom-complex-properties.xml | 1 - .../simple/pom-usecurrent-deprecated.xml | 52 ------------------ .../simple/pom-usecurrent-deprecated2.xml | 53 ------------------- .../common/PluginConfigurationProcessor.java | 11 +--- .../jib/plugins/common/PropertyNames.java | 3 -- .../jib/plugins/common/RawConfiguration.java | 3 -- 30 files changed, 5 insertions(+), 350 deletions(-) delete mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle delete mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle delete mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml delete mode 100644 jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml diff --git a/docs/faq.md b/docs/faq.md index 0d60fe9db3..f858c43310 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -139,7 +139,7 @@ Note that the modification time of the files in the built image put by Jib will _Reproducible_ means that given the same inputs, a build should produce the same outputs. Container images are uniquely identified by a digest (or a hash) of the image contents and image metadata. Tools and infrastructure such the Docker daemon, Docker Hub, registries, Kubernetes, etc) treat images with different digests as being different. -To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.useCurrentTimestamp=true` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. +To ensure that a Jib build is reproducible — that the rebuilt container image has the same digest — Jib adds files and directories in a consistent order, and sets consistent creation- and modification-times and permissions for all files and directories. Jib also ensures that the image metadata is recorded in a consistent order, and that the container image has a consistent creation time. To ensure consistent times, files and directories are recorded as having a creation and modification time of 1 second past the Unix Epoch (1970-01-01 00:00:01.000 UTC), and the container image is recorded as being created on the Unix Epoch. Setting `container.creationTime` to `USE_CURRENT_TIMESTAMP` and then rebuilding an image will produce a different timestamp for the image creation time, and so the container images will have different digests and appear to be different. For more details see [reproducible-builds.org](https://reproducible-builds.org). diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 3a05a9b68c..912ffd999c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Removed `jibDockerBuild.dockerClient` in favor of `jib.dockerClient`. ([#1983](https://github.com/GoogleContainerTools/jib/issues/1983)) - Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) +- Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) ### Fixed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 4695c1047d..74c58787a3 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -408,37 +408,6 @@ public void testDockerDaemon_timestampCustom() Instant.parse("2013-11-04T21:29:30Z"), targetImage); } - @Test - public void testDockerDaemon_timestampDeprecated() - throws DigestException, IOException, InterruptedException { - Instant beforeBuild = Instant.now(); - String targetImage = "simpleimage:gradle" + System.nanoTime(); - BuildResult buildResult = - JibRunHelper.buildToDockerDaemon( - simpleTestProject, targetImage, "build-usecurrent-deprecated.gradle"); - JibRunHelper.assertSimpleCreationTimeIsAfter(beforeBuild, targetImage); - Assert.assertThat( - buildResult.getOutput(), - CoreMatchers.containsString( - "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' with the value 'USE_CURRENT_TIMESTAMP' instead")); - } - - @Test - public void testDockerDaemon_timestampFail() - throws InterruptedException, IOException, DigestException { - try { - String targetImage = "simpleimage:gradle" + System.nanoTime(); - JibRunHelper.buildToDockerDaemonAndRun( - simpleTestProject, targetImage, "build-usecurrent-deprecated2.gradle"); - Assert.fail(); - } catch (UnexpectedBuildFailure ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "You cannot configure both 'jib.container.useCurrentTimestamp' and 'jib.container.creationTime'")); - } - } - @Test public void testBuild_dockerClient() throws IOException, InterruptedException, DigestException { Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows")); diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle deleted file mode 100644 index 919c215900..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib { - to.image = System.getProperty("_TARGET_IMAGE") - container.useCurrentTimestamp = true -} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle deleted file mode 100644 index 092247c19b..0000000000 --- a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-usecurrent-deprecated2.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'java' - id 'com.google.cloud.tools.jib' -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -dependencies { - compile files('libs/dependency-1.0.0.jar') -} - -jib { - to.image = System.getProperty("_TARGET_IMAGE") - container.useCurrentTimestamp = true - container.creationTime = 'USE_CURRENT_TIMESTAMP' -} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 27b3dbd9c6..5d8f3a7ab9 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -87,7 +87,6 @@ public void buildDocker() HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index f70c4f1b6b..8927310deb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -76,7 +76,6 @@ public void buildImage() MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 4573ff1cd4..3a778b019f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -101,7 +101,6 @@ public void buildTar() MainClassInferenceException { // Asserts required @Input parameters are not null. Preconditions.checkNotNull(jibExtension); - TaskCommon.checkDeprecatedUsage(jibExtension, getLogger()); TaskCommon.disableHttpLogging(); TempDirectoryProvider tempDirectoryProvider = new TempDirectoryProvider(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java index c9c3615efd..b3f96e8993 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java @@ -33,7 +33,6 @@ */ public class ContainerParameters { - private boolean useCurrentTimestamp = false; private List jvmFlags = Collections.emptyList(); private Map environment = Collections.emptyMap(); @Nullable private List entrypoint; @@ -50,18 +49,6 @@ public class ContainerParameters { private String filesModificationTime = "EPOCH_PLUS_SECOND"; private String creationTime = "EPOCH"; - @Input - public boolean getUseCurrentTimestamp() { - if (System.getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP) != null) { - return Boolean.getBoolean(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); - } - return useCurrentTimestamp; - } - - public void setUseCurrentTimestamp(boolean useCurrentTimestamp) { - this.useCurrentTimestamp = useCurrentTimestamp; - } - @Input @Nullable @Optional diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index 221b74adc6..ad1aa28fdf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -131,11 +131,6 @@ public Optional getWorkingDirectory() { return Optional.ofNullable(jibExtension.getContainer().getWorkingDirectory()); } - @Override - public boolean getUseCurrentTimestamp() { - return jibExtension.getContainer().getUseCurrentTimestamp(); - } - @Override public boolean getAllowInsecureRegistries() { return jibExtension.getAllowInsecureRegistries(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index f2a2bc0d9d..c9cb753796 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -26,7 +26,6 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; -import org.gradle.api.logging.Logger; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskProvider; import org.gradle.internal.logging.events.LogEvent; @@ -74,20 +73,6 @@ static void disableHttpLogging() { java.util.logging.Logger.getLogger(HttpTransport.class.getName()).setLevel(Level.OFF); } - @Deprecated - static void checkDeprecatedUsage(JibExtension jibExtension, Logger logger) { - if (jibExtension.getContainer().getUseCurrentTimestamp()) { - if (!jibExtension.getContainer().getCreationTime().equals("EPOCH")) { - throw new IllegalArgumentException( - "You cannot configure both 'jib.container.useCurrentTimestamp' and " - + "'jib.container.creationTime'"); - } - logger.warn( - "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' " - + "with the value 'USE_CURRENT_TIMESTAMP' instead"); - } - } - /** * Validates and converts a {@code String->String} file-path-to-file-permissions map to an * equivalent {@code AbsoluteUnixPath->FilePermission} map. diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index 8a0d16e2f7..eeb01f18f4 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -71,7 +71,6 @@ public void testGetters() { .thenReturn(new HashMap<>(ImmutableMap.of("unit", "cm"))); Mockito.when(containerParameters.getMainClass()).thenReturn("com.example.Main"); Mockito.when(containerParameters.getPorts()).thenReturn(Arrays.asList("80/tcp", "0")); - Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); Mockito.when(containerParameters.getUser()).thenReturn("admin:wheel"); Mockito.when(containerParameters.getFilesModificationTime()).thenReturn("2011-12-03T22:42:05Z"); @@ -108,7 +107,6 @@ public void testGetters() { Assert.assertEquals( new HashSet<>(Arrays.asList("additional", "tags")), Sets.newHashSet(rawConfiguration.getToTags())); - Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); Assert.assertEquals(Paths.get("test"), rawConfiguration.getDockerExecutable().get()); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java index 2901b1e4f0..4720f24496 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java @@ -285,8 +285,6 @@ public void testProperties() { System.setProperty("jib.container.ports", "port1,port2,port3"); Assert.assertEquals( ImmutableList.of("port1", "port2", "port3"), testJibExtension.getContainer().getPorts()); - System.setProperty("jib.container.useCurrentTimestamp", "true"); - Assert.assertTrue(testJibExtension.getContainer().getUseCurrentTimestamp()); System.setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testJibExtension.getContainer().getUser()); System.setProperty("jib.container.filesModificationTime", "2011-12-03T22:42:05Z"); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java index 3cfcc6d57b..092c43117c 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java @@ -18,7 +18,6 @@ import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.logging.Logger; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskProvider; @@ -30,8 +29,6 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.boot.gradle.plugin.SpringBootPlugin; import org.springframework.boot.gradle.tasks.bundling.BootWar; @@ -42,47 +39,10 @@ public class TaskCommonTest { @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); - @Mock private JibExtension jibExtension; - @Mock private ContainerParameters containerParameters; - @Mock private Logger logger; - @Before public void setUp() { System.clearProperty("jib.extraDirectories.paths"); System.clearProperty("jib.extraDirectories.permissions"); - Mockito.when(jibExtension.getContainer()).thenReturn(containerParameters); - } - - @Test - public void testCheckDeprecatedUsage_default() { - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger, Mockito.never()).warn(Mockito.anyString()); - } - - @Test - public void testCheckDeprecatedUsage_useCurrentTimestampConfigured() { - Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); - Mockito.when(containerParameters.getCreationTime()).thenReturn("EPOCH"); - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Mockito.verify(logger) - .warn( - "'jib.container.useCurrentTimestamp' is deprecated; use 'jib.container.creationTime' " - + "with the value 'USE_CURRENT_TIMESTAMP' instead"); - } - - @Test - public void testCheckDeprecatedUsage_useCurrentTimestampAndCreationTimeConfigured() { - Mockito.when(containerParameters.getUseCurrentTimestamp()).thenReturn(true); - Mockito.when(containerParameters.getCreationTime()).thenReturn("USE_CURRENT_TIMESTAMP"); - try { - TaskCommon.checkDeprecatedUsage(jibExtension, logger); - Assert.fail(); - } catch (IllegalArgumentException ex) { - Assert.assertEquals( - "You cannot configure both 'jib.container.useCurrentTimestamp' and " - + "'jib.container.creationTime'", - ex.getMessage()); - } } @Test diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 58be4538b0..77a9aba237 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### Changed - Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) +- Removed deprecated `` configuration in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) ### Fixed diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 6fa671d057..8b0e93b404 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -590,33 +590,6 @@ public void testExecute_timestampCustom() Assert.assertEquals(Instant.parse("2013-11-05T06:29:30Z"), parsed); } - @Test - public void testDockerDaemon_timestampDeprecated() - throws IOException, VerificationException, InterruptedException { - Instant before = Instant.now(); - String targetImage = getTestImageReference("simpleimage:gradle" + System.nanoTime()); - build(simpleTestProject.getProjectRoot(), targetImage, "pom-usecurrent-deprecated.xml", false) - .verifyTextInLog( - " is deprecated; use with the value USE_CURRENT_TIMESTAMP instead"); - new Command("docker", "pull", targetImage).run(); - assertCreationTimeIsAfter(before, targetImage); - } - - @Test - public void testDockerDaemon_timestampFail() throws IOException { - try { - String targetImage = getTestImageReference("simpleimage:gradle" + System.nanoTime()); - build( - simpleTestProject.getProjectRoot(), targetImage, "pom-usecurrent-deprecated2.xml", false); - Assert.fail(); - } catch (VerificationException ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString( - "You cannot configure both and ")); - } - } - @Test public void testExecute_complex_sameFromAndToRegistry() throws IOException, InterruptedException, VerificationException { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 32f6c17998..08d37c6df3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -66,8 +66,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { HelpfulSuggestions.forDockerNotInstalled(HELPFUL_SUGGESTIONS_PREFIX)); } - MojoCommon.checkUseCurrentTimestampDeprecation(this); - MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 748dbfa072..98de714f51 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -58,8 +58,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - MojoCommon.checkUseCurrentTimestampDeprecation(this); - // Validates 'format'. if (Arrays.stream(ImageFormat.values()).noneMatch(value -> value.name().equals(getFormat()))) { throw new MojoFailureException( diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 53ff23c4ca..10ab15b691 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -58,8 +58,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - MojoCommon.checkUseCurrentTimestampDeprecation(this); - MavenSettingsProxyProvider.activateHttpAndHttpsProxies( getSession().getSettings(), getSettingsDecrypter()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 55d2781eea..ee1e1fcf84 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -157,8 +157,6 @@ public static class ContainerParameters { // Note: `entrypoint` and `args` are @Nullable to handle inheriting values from the base image - @Parameter private boolean useCurrentTimestamp = false; - @Nullable @Parameter private List entrypoint; @Parameter private List jvmFlags = Collections.emptyList(); @@ -354,20 +352,6 @@ AuthConfiguration getTargetImageAuth() { return to.auth; } - /** - * Gets whether or not to use the current timestamp for the container build. - * - * @return {@code true} if the build should use the current timestamp, {@code false} if not - */ - @Deprecated - boolean getUseCurrentTimestamp() { - String property = getProperty(PropertyNames.CONTAINER_USE_CURRENT_TIMESTAMP); - if (property != null) { - return Boolean.parseBoolean(property); - } - return container.useCurrentTimestamp; - } - /** * Gets the configured entrypoint. * diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 842831db38..292f51f318 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -136,11 +136,6 @@ public Optional getWorkingDirectory() { return Optional.ofNullable(jibPluginConfiguration.getWorkingDirectory()); } - @Override - public boolean getUseCurrentTimestamp() { - return jibPluginConfiguration.getUseCurrentTimestamp(); - } - @Override public boolean getAllowInsecureRegistries() { return jibPluginConfiguration.getAllowInsecureRegistries(); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index 8910322911..b333779828 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -40,22 +40,6 @@ public class MojoCommon { @VisibleForTesting public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; - @Deprecated - static void checkUseCurrentTimestampDeprecation(JibPluginConfiguration jibPluginConfiguration) { - if (jibPluginConfiguration.getUseCurrentTimestamp()) { - if (!jibPluginConfiguration.getCreationTime().equals("EPOCH")) { - throw new IllegalArgumentException( - "You cannot configure both and " - + ""); - } - jibPluginConfiguration - .getLog() - .warn( - " is deprecated; use with " - + "the value USE_CURRENT_TIMESTAMP instead"); - } - } - /** * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code * (project dir)/src/main/jib} by default if not configured. diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 2a9f942b49..8e676aa9dc 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -121,8 +121,6 @@ public void testSystemProperties() { sessionProperties.put("jib.container.ports", "port1,port2,port3"); Assert.assertEquals( ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); - sessionProperties.put("jib.container.useCurrentTimestamp", "true"); - Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); sessionProperties.put("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); sessionProperties.put("jib.container.workingDirectory", "/working/directory"); @@ -219,8 +217,6 @@ public void testPomProperties() { project.getProperties().setProperty("jib.container.ports", "port1,port2,port3"); Assert.assertEquals( ImmutableList.of("port1", "port2", "port3"), testPluginConfiguration.getExposedPorts()); - project.getProperties().setProperty("jib.container.useCurrentTimestamp", "true"); - Assert.assertTrue(testPluginConfiguration.getUseCurrentTimestamp()); project.getProperties().setProperty("jib.container.user", "myUser"); Assert.assertEquals("myUser", testPluginConfiguration.getUser()); project.getProperties().setProperty("jib.container.workingDirectory", "/working/directory"); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index c18cca1b29..a1e1d4ee8e 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -75,7 +75,6 @@ public void testGetters() { Mockito.when(jibPluginConfiguration.getMainClass()).thenReturn("com.example.Main"); Mockito.when(jibPluginConfiguration.getTargetImageAdditionalTags()) .thenReturn(new HashSet<>(Arrays.asList("additional", "tags"))); - Mockito.when(jibPluginConfiguration.getUseCurrentTimestamp()).thenReturn(true); Mockito.when(jibPluginConfiguration.getUser()).thenReturn("admin:wheel"); Mockito.when(jibPluginConfiguration.getFilesModificationTime()) .thenReturn("2011-12-03T22:42:05Z"); @@ -111,7 +110,6 @@ public void testGetters() { Assert.assertEquals( new HashSet<>(Arrays.asList("additional", "tags")), Sets.newHashSet(rawConfiguration.getToTags())); - Assert.assertTrue(rawConfiguration.getUseCurrentTimestamp()); Assert.assertEquals("admin:wheel", rawConfiguration.getUser().get()); Assert.assertEquals("2011-12-03T22:42:05Z", rawConfiguration.getFilesModificationTime()); Assert.assertEquals(Paths.get("test"), rawConfiguration.getDockerExecutable().get()); diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml index b8c1dc77e3..421392636d 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-complex-properties.xml @@ -18,7 +18,6 @@ ${_TARGET_IMAGE} ${_TARGET_USERNAME} ${_TARGET_PASSWORD} - true An argument. com.test.HelloWorld -Xms512m,-Xdebug diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml deleted file mode 100644 index a666d0bebd..0000000000 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - 4.0.0 - - com.test - hello-world - 1 - - - UTF-8 - UTF-8 - @@PluginVersion@@ - - - - - com.test - dependency - 1.0.0 - system - ${project.basedir}/libs/dependency-1.0.0.jar - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - ${_TARGET_IMAGE} - - - true - - - - - - diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml deleted file mode 100644 index 1220d24bb8..0000000000 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-usecurrent-deprecated2.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - 4.0.0 - - com.test - hello-world - 1 - - - UTF-8 - UTF-8 - @@PluginVersion@@ - - - - - com.test - dependency - 1.0.0 - system - ${project.basedir}/libs/dependency-1.0.0.jar - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - ${_TARGET_IMAGE} - - - true - USE_CURRENT_TIMESTAMP - - - - - - diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 68210eb67c..7938532790 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -254,15 +254,8 @@ static JibContainerBuilder processCommonConfiguration( .setUser(rawConfiguration.getUser().orElse(null)); getWorkingDirectoryChecked(rawConfiguration) .ifPresent(jibContainerBuilder::setWorkingDirectory); - if (rawConfiguration.getUseCurrentTimestamp()) { - projectProperties.log( - LogEvent.warn( - "Setting image creation time to current time; your image may not be reproducible.")); - jibContainerBuilder.setCreationTime(Instant.now()); - } else { - jibContainerBuilder.setCreationTime( - getCreationTime(rawConfiguration.getCreationTime(), projectProperties)); - } + jibContainerBuilder.setCreationTime( + getCreationTime(rawConfiguration.getCreationTime(), projectProperties)); // Adds all the extra files. for (Path directory : rawConfiguration.getExtraDirectories()) { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index 92a9e0f09b..fd15d8f30e 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -62,8 +62,5 @@ public class PropertyNames { public static final String CONTAINERIZE = "jib.containerize"; public static final String ALWAYS_CACHE_BASE_IMAGE = "jib.alwaysCacheBaseImage"; - @Deprecated - public static final String CONTAINER_USE_CURRENT_TIMESTAMP = "jib.container.useCurrentTimestamp"; - private PropertyNames() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 804f1477d6..01a198f135 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -69,9 +69,6 @@ public interface RawConfiguration { Optional getWorkingDirectory(); - @Deprecated - boolean getUseCurrentTimestamp(); - boolean getAllowInsecureRegistries(); ImageFormat getImageFormat(); From fb29e86b99ff124ccf7f64e24b366d2862f95d6c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 20 Nov 2019 12:08:44 -0500 Subject: [PATCH 0843/2020] Small cleanup (#2167) --- .../gradle/GradleProjectPropertiesTest.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 01fddeaa60..67c17fd103 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -94,7 +94,6 @@ @RunWith(MockitoJUnitRunner.class) public class GradleProjectPropertiesTest { - private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; private static final Instant SAMPLE_FILE_MODIFICATION_TIME = Instant.ofEpochSecond(32); /** Implementation of {@link FileCollection} that just holds a set of {@link File}s. */ @@ -122,7 +121,7 @@ public TaskDependency getBuildDependencies() { } } - /** Helper for reading back layers in a {@link buildContext}. */ + /** Helper for reading back layers in a {@link BuildContext}. */ private static class ContainerBuilderLayers { private final List resourcesLayerEntries; @@ -333,7 +332,7 @@ public void testGetMajorJavaVersion_jvm11() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildContext buildContext = setupBuildContext("/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setupBuildContext("/app"); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); Path applicationDirectory = getResource("gradle/application"); @@ -374,14 +373,14 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer Mockito.when(mockMainSourceSetOutput.getClassesDirs()) .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); gradleProjectProperties.createJibContainerBuilder( - JavaContainerBuilder.from(RegistryImage.named("base")), DEFAULT_CONTAINERIZING_MODE); + JavaContainerBuilder.from(RegistryImage.named("base")), ContainerizingMode.EXPLODED); Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setupBuildContext("/my/app"); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertExtractionPathsUnordered( @@ -410,8 +409,7 @@ public void testCreateContainerBuilder_nonDefaultAppRoot() @Test public void testCreateContainerBuilder_defaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildContext buildContext = - setupBuildContext(JavaContainerBuilder.DEFAULT_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setupBuildContext(JavaContainerBuilder.DEFAULT_APP_ROOT); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertExtractionPathsUnordered( Arrays.asList( @@ -441,7 +439,7 @@ public void testCreateContainerBuilder_war() Path webAppDirectory = getResource("gradle/webapp"); Path unzipTarget = setUpWarProject(webAppDirectory); - BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setupBuildContext("/my/app"); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertSourcePathsUnordered( ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), @@ -504,8 +502,7 @@ public void testCreateContainerBuilder_defaultWebAppRoot() CacheDirectoryCreationException { Path unzipTarget = setUpWarProject(getResource("gradle/webapp")); - BuildContext buildContext = - setupBuildContext(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT, DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setupBuildContext(JavaContainerBuilder.DEFAULT_WEB_APP_ROOT); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertSourcePathsUnordered( ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), @@ -540,7 +537,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("WEB-INF", "lib"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything"); // should pass } @Test @@ -548,14 +545,14 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { temporaryFolder.newFolder("WEB-INF", "classes"); setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything"); // should pass } @Test public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { setUpWarProject(temporaryFolder.getRoot().toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setupBuildContext("/anything"); // should pass } @Test @@ -594,7 +591,7 @@ public void testGetWarFilePath_bootWarDisabled() { Assert.assertEquals("war.war", gradleProjectProperties.getWarFilePath()); } - private BuildContext setupBuildContext(String appRoot, ContainerizingMode containerizingMode) + private BuildContext setupBuildContext(String appRoot) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder.from(RegistryImage.named("base")) @@ -602,7 +599,7 @@ private BuildContext setupBuildContext(String appRoot, ContainerizingMode contai .setModificationTimeProvider((ignored1, ignored2) -> SAMPLE_FILE_MODIFICATION_TIME); JibContainerBuilder jibContainerBuilder = new GradleProjectProperties(mockProject, mockLogger, mockTempDirectoryProvider) - .createJibContainerBuilder(javaContainerBuilder, containerizingMode); + .createJibContainerBuilder(javaContainerBuilder, ContainerizingMode.EXPLODED); return JibContainerBuilderTestHelper.toBuildContext( jibContainerBuilder, Containerizer.to(RegistryImage.named("to"))); } From 2d40ebe35d4a2acd0d7d9cfb1282eae3c7cd40d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Youri=20Bonnaff=C3=A9?= Date: Thu, 21 Nov 2019 14:48:53 +0100 Subject: [PATCH 0844/2020] Missing underscore in FAQ for USE_CURRENT_TIMESTAMP (#2169) The special keyword USE_CURRENT_TIMESTAMP is missing an underscore, preventing it to be copied and used. --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index f858c43310..c0702a76c4 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -108,7 +108,7 @@ See [Extended Usage](../jib-gradle-plugin#extended-usage) for the `container.for ### Why is my image created 48+ years ago? -For reproducibility purposes, Jib sets the creation time of the container images to the Unix epoch (00:00:00, January 1st, 1970 in UTC). If you would like to use a different timestamp, set the `jib.container.creationTime` / `` parameter to an ISO 8601 date-time. You may also use the value `USE_CURRENT TIMESTAMP` to set the creation time to the actual build time, but this sacrifices reproducibility since the timestamp will change with every build. +For reproducibility purposes, Jib sets the creation time of the container images to the Unix epoch (00:00:00, January 1st, 1970 in UTC). If you would like to use a different timestamp, set the `jib.container.creationTime` / `` parameter to an ISO 8601 date-time. You may also use the value `USE_CURRENT_TIMESTAMP` to set the creation time to the actual build time, but this sacrifices reproducibility since the timestamp will change with every build.

Setting `creationTime` parameter From 640fdefaf35cf893af41293906d8e00239c12feb Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 22 Nov 2019 13:35:48 -0500 Subject: [PATCH 0845/2020] Minor fixes (#2171) --- .../java/com/google/cloud/tools/jib/http/TestWebServer.java | 4 +++- jib-maven-plugin/CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index f4b0facf63..fa8216fe8a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -158,6 +158,8 @@ private void ignoreReturn(Future future) { * connections can be intermixed. However, no lines will ever be broken in the middle. */ public String getInputRead() { - return inputRead.toString(); + synchronized (inputRead) { + return inputRead.toString(); + } } } diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 77a9aba237..a5a4829390 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to this project will be documented in this file. ## 1.8.0 -## Changed +### Changed - Optimized building to a registry with local base images. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) From 864edac746cc0785e9cd728e67eec049e07ace40 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Mon, 25 Nov 2019 17:45:30 -0500 Subject: [PATCH 0846/2020] Use ConsoleLogger instead of native Gradle logger (#2176) * Fix Gradle logging issue * Wait for logging thread --- .../cloud/tools/jib/gradle/GradleProjectProperties.java | 9 +++++---- .../tools/jib/gradle/GradleProjectPropertiesTest.java | 8 +++++++- .../tools/jib/maven/MavenProjectPropertiesTest.java | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 1c0d8c24fe..6df423e31d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -123,7 +124,6 @@ private static boolean isProgressFooterEnabled(Project project) { private final Project project; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); - private final Logger logger; private final ConsoleLogger consoleLogger; private final TempDirectoryProvider tempDirectoryProvider; @@ -131,7 +131,6 @@ private static boolean isProgressFooterEnabled(Project project) { GradleProjectProperties( Project project, Logger logger, TempDirectoryProvider tempDirectoryProvider) { this.project = project; - this.logger = logger; this.tempDirectoryProvider = tempDirectoryProvider; ConsoleLoggerBuilder consoleLoggerBuilder = (isProgressFooterEnabled(project) @@ -159,7 +158,8 @@ public JibContainerBuilder createJibContainerBuilder( try { if (isWarProject()) { String warFilePath = getWarFilePath(); - logger.info("WAR project identified, creating WAR image from: " + warFilePath); + consoleLogger.log( + Level.INFO, "WAR project identified, creating WAR image from: " + warFilePath); Path explodedWarPath = tempDirectoryProvider.newDirectory(); ZipUtil.unzip(Paths.get(warFilePath), explodedWarPath); return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); @@ -229,7 +229,8 @@ public JibContainerBuilder createJibContainerBuilder( javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); } if (classesOutputDirectories.isEmpty()) { - logger.warn("No classes files were found - did you compile your project?"); + consoleLogger.log( + Level.WARN, "No classes files were found - did you compile your project?"); } break; diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java index 67c17fd103..1c812a5197 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java @@ -205,7 +205,12 @@ private static Path getResource(String path) throws URISyntaxException { private GradleProjectProperties gradleProjectProperties; @Before - public void setup() throws URISyntaxException, IOException { + public void setUp() throws URISyntaxException, IOException { + Mockito.when(mockLogger.isDebugEnabled()).thenReturn(true); + Mockito.when(mockLogger.isInfoEnabled()).thenReturn(true); + Mockito.when(mockLogger.isWarnEnabled()).thenReturn(true); + Mockito.when(mockLogger.isErrorEnabled()).thenReturn(true); + manifest = new DefaultManifest(mockFileResolver); Mockito.when(mockProject.getConvention()).thenReturn(mockConvention); Mockito.when(mockConvention.getPlugin(JavaPluginConvention.class)) @@ -374,6 +379,7 @@ public void testCreateContainerBuilder_noClassesFiles() throws InvalidImageRefer .thenReturn(new TestFileCollection(ImmutableSet.of(nonexistentFile))); gradleProjectProperties.createJibContainerBuilder( JavaContainerBuilder.from(RegistryImage.named("base")), ContainerizingMode.EXPLODED); + gradleProjectProperties.waitForLoggingThread(); Mockito.verify(mockLogger).warn("No classes files were found - did you compile your project?"); } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 49a3c4d293..643b6fd647 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -222,6 +222,10 @@ private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOExc @Before public void setUp() throws IOException, URISyntaxException { + Mockito.when(mockLog.isDebugEnabled()).thenReturn(true); + Mockito.when(mockLog.isWarnEnabled()).thenReturn(true); + Mockito.when(mockLog.isErrorEnabled()).thenReturn(true); + Mockito.when(mockMavenSession.getRequest()).thenReturn(mockMavenRequest); mavenProjectProperties = new MavenProjectProperties( From 04ac1ce443fc5bfc567fef9d4528f013346438a7 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 25 Nov 2019 17:49:06 -0500 Subject: [PATCH 0847/2020] Fix typo (#2177) --- .../cloud/tools/jib/maven/MavenProjectPropertiesTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 643b6fd647..28ec1b84eb 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -80,7 +80,7 @@ public class MavenProjectPropertiesTest { private static final ContainerizingMode DEFAULT_CONTAINERIZING_MODE = ContainerizingMode.EXPLODED; private static final Instant SAMPLE_FILE_MODIFICATION_TIME = Instant.ofEpochSecond(32); - /** Helper for reading back layers in a {@link buildContext}. */ + /** Helper for reading back layers in a {@link BuildContext}. */ private static class ContainerBuilderLayers { private final List resourcesLayers; From 498459d328c2f75ae1e40877eaa0a486e71dc067 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 2 Dec 2019 14:07:24 -0800 Subject: [PATCH 0848/2020] remove draft heading (#2180) Not a draft anymore --- examples/multi-module/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index d932b4660c..dc2248f12c 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -1,4 +1,4 @@ -# Multi-module example (DRAFT) +# Multi-module example This example shows how to build multiple containers for a multi-module project in both **Maven** and **Gradle**. From b78c36215341750dc24aef9c98701008faf4de85 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 2 Dec 2019 14:13:34 -0800 Subject: [PATCH 0849/2020] Reproducible jars proposal (#2089) * Reproducible jars proposal is aborted in favor of docs --- proposals/archives/reproducible_jars.md | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 proposals/archives/reproducible_jars.md diff --git a/proposals/archives/reproducible_jars.md b/proposals/archives/reproducible_jars.md new file mode 100644 index 0000000000..788b45432d --- /dev/null +++ b/proposals/archives/reproducible_jars.md @@ -0,0 +1,81 @@ +# Proposal: Make sub project/module jars included in container reproducible + +Relevant issue: [#1945](https://github.com/GoogleContainerTools/jib/issues/1945) +Relevent PR: [#2070](https://github.com/GoogleContainerTools/jib/pull/2070) + +| ⌠Aborted ⌠| +| :----- | +| This proposal is archived but unimplemented, in favor of documentation in the multimodule example: https://github.com/GoogleContainerTools/jib/tree/master/examples/multi-module | + +## Motivation + +Currently in multimodule builds, jib takes the jars produced by submodule (and subproject) directly from +the build system. This means if the build system (gradle, maven) doesn't produce reproducible jars, and +jib includes them in the final container, the reproducibility guarantees of jib are not satisfied. + +What we want to do is to rewrite these jars to remove any information that could make builds not reproducible. + +## Requirements + +In order to achieve reproducibility for jars, we need to: +1. Remove timestamps and preserve file order. This can be achieved by setting the time to a fixed value + and sorting zip entries alphabetically +2. Remove any changeable values from META-INF/MANIFEST.MF like build time, etc + +## Non-goals + +Signed archives will/should not be touched by this process. While we expect that submodule dependencies will +not be signed, there's no way for us to know what all users are doing eveywhere. A warning should be presented +to the user that signed archives are processed by jib to be reproducible. + +## Current solution + +Currently the user must configure their build to be reproducible. (we will borrow from these two solutions in our own solution with +proper attribution) + +### Gradle +In gradle that means configuring the jar task explicitly: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html + +``` +jar { + preserveFileTimestamps = false + reproducibleFileOrder = true +} +``` +and removing any changeable meta data from the MANIFEST.MF that they may have previously configured. + +### Maven + +In maven, a user must configure an external plugin to do this, one could use: https://github.com/zlika/reproducible-build-maven-plugin +on all submodules and potentially achieve this on their own. + +## Proposed Solution + +Jib should just handle this itself on the `PROJECT_DEPENDENCIES` layer. For all jars included in the layer, Jib needs to: + +1. Inspect the jar's `MANIFEST.MF` and remove values that could change. +2. Force a single timestamp on all files +3. Order the files in the jar(zip) by name (alphabetical) + +Jib *will not* do this for any other layer + +### Configuration + +Jib can make this configurable, but it should be `true` by default. A system property like + +``` +-Djib.projectDependencies.reproducible=true/false +``` + +### Implementation + +1. A utility in jib-core to convert jars to *reproducible* jars. +1. This utility only impacts the `PROJECT_DEPENDENCIES` layer +1. Triggering this utility at one of two places + 1. During layer writing by introducing some new configuration into `LayerConfiguration` -- potentially more performant + 1. At the plugin level to rewrite the jar before presenting it to the Jib Containerizer + +## Potential Issues + +1. Maybe we should just direct users to configure their builds to be reproducible using the gradle/maven mechanism desribed above. +2. Can we *really, truly* know every value thrown into the manifest by the user?? From 3688f567835130eeca7c13b90a5023e485690bf9 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 4 Dec 2019 13:30:19 -0500 Subject: [PATCH 0850/2020] Detect Spring Boot fat-JAR repackaging and use original thin JAR (#2170) * Add Spring Boot fat jar support * CHANGELOG * Fix problem on Windows * Decrease logging level * Add integration test * Copy JAR to have .jar suffix if needed --- jib-maven-plugin/CHANGELOG.md | 2 + .../maven/BuildImageMojoIntegrationTest.java | 29 +- .../jib/maven/MavenProjectProperties.java | 123 ++++-- .../jib/maven/MavenProjectPropertiesTest.java | 400 ++++++++++++++---- .../maven/projects/spring-boot/pom.xml | 46 ++ .../src/main/java/hello/Application.java | 12 + .../src/main/java/hello/HelloController.java | 13 + 7 files changed, 519 insertions(+), 106 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/Application.java create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/HelloController.java diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index a5a4829390..506d225c2f 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed `` being ignored if `` are not explicitly defined. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2160)) +- Now `packaged` works as intended with Spring Boot projects that generate a fat JAR. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) +- Now `packaged` correctly identifies the packaged JAR generated at a non-default location when configured with the Maven Jar Plugin's `` and ``. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) ## 1.8.0 diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index 8b0e93b404..d060d6b128 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -69,6 +69,8 @@ public class BuildImageMojoIntegrationTest { @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); + @ClassRule public static final TestProject springBootProject = new TestProject("spring-boot"); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private static String getTestImageReference(String label) { @@ -667,22 +669,41 @@ public void testExecute_jibRequireVersion_fail() throws IOException { @Test public void testExecute_jettyServlet25() throws VerificationException, IOException, InterruptedException { - buildAndRunWar("jetty-servlet25:maven", "pom.xml"); + buildAndRunWebApp(servlet25Project, "jetty-servlet25:maven", "pom.xml"); HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); } @Test public void testExecute_tomcatServlet25() throws VerificationException, IOException, InterruptedException { - buildAndRunWar("tomcat-servlet25:maven", "pom-tomcat.xml"); + buildAndRunWebApp(servlet25Project, "tomcat-servlet25:maven", "pom-tomcat.xml"); HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080/hello")); } - private void buildAndRunWar(String label, String pomXml) + @Test + public void testExecute_springBootPackaged() + throws VerificationException, IOException, InterruptedException { + buildAndRunWebApp(springBootProject, "spring-boot:maven", "pom.xml"); + + String sizeOutput = + new Command( + "docker", + "exec", + detachedContainerName, + "/busybox/wc", + "-c", + "/app/classpath/spring-boot-0.1.0.original.jar") + .run(); + Assert.assertEquals("2749 /app/classpath/spring-boot-0.1.0.original.jar\n", sizeOutput); + + HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080")); + } + + private void buildAndRunWebApp(TestProject project, String label, String pomXml) throws VerificationException, IOException, InterruptedException { String targetImage = getTestImageReference(label); - Verifier verifier = new Verifier(servlet25Project.getProjectRoot().toString()); + Verifier verifier = new Verifier(project.getProjectRoot().toString()); verifier.setSystemProperty("jib.useOnlyProjectCache", "true"); verifier.setSystemProperty("_TARGET_IMAGE", targetImage); if (targetImage.startsWith("localhost")) { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 3e137039a9..9c62485a97 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -21,6 +21,7 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -39,6 +40,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; @@ -46,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -54,6 +57,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.Os; @@ -160,6 +164,22 @@ static int getVersionFromString(String versionString) { } } + @VisibleForTesting + static Optional getChildValue(@Nullable Xpp3Dom dom, String... childNodePath) { + if (dom == null) { + return Optional.empty(); + } + + Xpp3Dom node = dom; + for (String child : childNodePath) { + node = node.getChild(child); + if (node == null) { + return Optional.empty(); + } + } + return Optional.ofNullable(node.getValue()); + } + private final MavenProject project; private final MavenSession session; private final SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); @@ -330,23 +350,9 @@ public String getPluginName() { public String getMainClassFromJar() { Plugin mavenJarPlugin = project.getPlugin("org.apache.maven.plugins:maven-jar-plugin"); if (mavenJarPlugin != null) { - Xpp3Dom jarConfiguration = (Xpp3Dom) mavenJarPlugin.getConfiguration(); - if (jarConfiguration == null) { - return null; - } - Xpp3Dom archiveObject = jarConfiguration.getChild("archive"); - if (archiveObject == null) { - return null; - } - Xpp3Dom manifestObject = archiveObject.getChild("manifest"); - if (manifestObject == null) { - return null; - } - Xpp3Dom mainClassObject = manifestObject.getChild("mainClass"); - if (mainClassObject == null) { - return null; - } - return mainClassObject.getValue(); + return getChildValue( + (Xpp3Dom) mavenJarPlugin.getConfiguration(), "archive", "manifest", "mainClass") + .orElse(null); } return null; } @@ -398,15 +404,13 @@ public int getMajorJavaVersion() { project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin"); if (mavenCompilerPlugin != null) { Xpp3Dom pluginConfiguration = (Xpp3Dom) mavenCompilerPlugin.getConfiguration(); - if (pluginConfiguration != null) { - Xpp3Dom target = pluginConfiguration.getChild("target"); - if (target != null) { - return getVersionFromString(target.getValue()); - } - Xpp3Dom release = pluginConfiguration.getChild("release"); - if (release != null) { - return getVersionFromString(release.getValue()); - } + Optional target = getChildValue(pluginConfiguration, "target"); + if (target.isPresent()) { + return getVersionFromString(target.get()); + } + Optional release = getChildValue(pluginConfiguration, "release"); + if (release.isPresent()) { + return getVersionFromString(release.get()); } } return 6; // maven-compiler-plugin default is 1.6 @@ -424,12 +428,69 @@ public boolean isOffline() { * https://github.com/apache/maven-jar-plugin/blob/80f58a84aacff6e671f5a601d62a3a3800b507dc/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java#L177 * * @return the path of the JAR + * @throws IOException */ @VisibleForTesting - Path getJarArtifact() { - // TODO: use maven-jar-plugin's and (i.e., - // "/-.jar"). - String jarName = project.getBuild().getFinalName() + ".jar"; - return Paths.get(project.getBuild().getDirectory(), jarName); + Path getJarArtifact() throws IOException { + String classifier = null; + Path buildDirectory = Paths.get(project.getBuild().getDirectory()); + Path outputDirectory = buildDirectory; + + // Read and from maven-jar-plugin. + Plugin jarPlugin = project.getPlugin("org.apache.maven.plugins:maven-jar-plugin"); + if (jarPlugin != null) { + for (PluginExecution execution : jarPlugin.getExecutions()) { + if ("default-jar".equals(execution.getId())) { + Xpp3Dom configuration = (Xpp3Dom) execution.getConfiguration(); + classifier = getChildValue(configuration, "classifier").orElse(null); + Optional directoryString = getChildValue(configuration, "outputDirectory"); + + if (directoryString.isPresent()) { + outputDirectory = project.getBasedir().toPath().resolve(directoryString.get()); + } + } + } + } + + String suffix = ".jar"; + if (jarRepackagedBySpringBoot()) { + consoleLogger.log( + Level.LIFECYCLE, "Spring Boot repackaging (fat JAR) detected; using the original JAR"); + if (outputDirectory.equals(buildDirectory)) { // Spring renames original only when needed + suffix += ".original"; + } + } + + String noSuffixJarName = + project.getBuild().getFinalName() + (classifier == null ? "" : '-' + classifier); + Path jarPath = outputDirectory.resolve(noSuffixJarName + suffix); + consoleLogger.log(Level.DEBUG, "Using JAR: " + jarPath); + + if (".jar".equals(suffix)) { + return jarPath; + } + + // "*" in "java -cp *" doesn't work if JAR doesn't end with ".jar". Copy the JAR with a new name + // ending with ".jar". + Path tempDirectory = tempDirectoryProvider.newDirectory(); + Path newJarPath = tempDirectory.resolve(noSuffixJarName + ".original.jar"); + Files.copy(jarPath, newJarPath); + return newJarPath; + } + + @VisibleForTesting + boolean jarRepackagedBySpringBoot() { + Plugin springBootPlugin = + project.getPlugin("org.springframework.boot:spring-boot-maven-plugin"); + if (springBootPlugin != null) { + for (PluginExecution execution : springBootPlugin.getExecutions()) { + if (execution.getGoals().contains("repackage")) { + Optional skip = getChildValue((Xpp3Dom) execution.getConfiguration(), "skip"); + boolean skipped = "true".equals(skip.orElse("false")); + return !skipped; + } + } + } + return false; } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 28ec1b84eb..6a3a7b8c50 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.StringJoiner; @@ -58,6 +59,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.archiver.zip.ZipEntry; @@ -196,28 +198,46 @@ private static Path zipUpDirectory(Path sourceRoot, Path targetZip) throws IOExc return targetZip; } + private static Artifact newArtifact(Path sourceJar) { + Artifact artifact = Mockito.mock(Artifact.class); + Mockito.when(artifact.getFile()).thenReturn(sourceJar.toFile()); + return artifact; + } + + private static Artifact newArtifact(String group, String artifactId, String version) { + Artifact artifact = new DefaultArtifact(group, artifactId, version, null, "jar", "", null); + artifact.setFile(new File("/tmp/" + group + artifactId + version)); + return artifact; + } + + private static Xpp3Dom newXpp3Dom(String name, String value) { + Xpp3Dom node = new Xpp3Dom(name); + node.setValue(value); + return node; + } + + private static Xpp3Dom addXpp3DomChild(Xpp3Dom parent, String name, String value) { + Xpp3Dom node = new Xpp3Dom(name); + node.setValue(value); + parent.addChild(node); + return node; + } + @Rule public final TestRepository testRepository = new TestRepository(); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private final Xpp3Dom pluginConfiguration = new Xpp3Dom("configuration"); + @Mock private Build mockBuild; @Mock private MavenProject mockMavenProject; @Mock private MavenSession mockMavenSession; @Mock private MavenExecutionRequest mockMavenRequest; @Mock private Properties mockMavenProperties; - @Mock private Plugin mockJarPlugin; - @Mock private Plugin mockCompilerPlugin; + @Mock private Plugin mockPlugin; + @Mock private PluginExecution mockPluginExecution; @Mock private Log mockLog; @Mock private TempDirectoryProvider mockTempDirectoryProvider; - private Xpp3Dom jarPluginConfiguration; - private Xpp3Dom archive; - private Xpp3Dom manifest; - private Xpp3Dom jarPluginMainClass; - - @Mock private Xpp3Dom compilerPluginConfiguration; - @Mock private Xpp3Dom compilerTarget; - @Mock private Xpp3Dom compilerRelease; - private MavenProjectProperties mavenProjectProperties; @Before @@ -230,10 +250,6 @@ public void setUp() throws IOException, URISyntaxException { mavenProjectProperties = new MavenProjectProperties( mockMavenProject, mockMavenSession, mockLog, mockTempDirectoryProvider); - jarPluginConfiguration = new Xpp3Dom(""); - archive = new Xpp3Dom("archive"); - manifest = new Xpp3Dom("manifest"); - jarPluginMainClass = new Xpp3Dom("mainClass"); Path outputPath = getResource("maven/application/output"); Path dependenciesPath = getResource("maven/application/dependencies"); @@ -264,12 +280,13 @@ public void setUp() throws IOException, URISyntaxException { @Test public void testGetMainClassFromJar_success() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) - .thenReturn(mockJarPlugin); - Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); - jarPluginConfiguration.addChild(archive); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); + Xpp3Dom archive = new Xpp3Dom("archive"); + Xpp3Dom manifest = new Xpp3Dom("manifest"); + pluginConfiguration.addChild(archive); archive.addChild(manifest); - manifest.addChild(jarPluginMainClass); - jarPluginMainClass.setValue("some.main.class"); + manifest.addChild(newXpp3Dom("mainClass", "some.main.class")); Assert.assertEquals("some.main.class", mavenProjectProperties.getMainClassFromJar()); } @@ -277,10 +294,11 @@ public void testGetMainClassFromJar_success() { @Test public void testGetMainClassFromJar_missingMainClass() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) - .thenReturn(mockJarPlugin); - Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); - jarPluginConfiguration.addChild(archive); - archive.addChild(manifest); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); + Xpp3Dom archive = new Xpp3Dom("archive"); + archive.addChild(new Xpp3Dom("manifest")); + pluginConfiguration.addChild(archive); Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @@ -288,9 +306,9 @@ public void testGetMainClassFromJar_missingMainClass() { @Test public void testGetMainClassFromJar_missingManifest() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) - .thenReturn(mockJarPlugin); - Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); - jarPluginConfiguration.addChild(archive); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); + pluginConfiguration.addChild(new Xpp3Dom("archive")); Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @@ -298,8 +316,8 @@ public void testGetMainClassFromJar_missingManifest() { @Test public void testGetMainClassFromJar_missingArchive() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) - .thenReturn(mockJarPlugin); - Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(jarPluginConfiguration); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @@ -307,7 +325,7 @@ public void testGetMainClassFromJar_missingArchive() { @Test public void testGetMainClassFromJar_missingConfiguration() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) - .thenReturn(mockJarPlugin); + .thenReturn(mockPlugin); Assert.assertNull(mavenProjectProperties.getMainClassFromJar()); } @@ -367,34 +385,36 @@ public void testValidateBaseImageVersion_releaseProperty() { @Test public void testValidateBaseImageVersion_compilerPluginTarget() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) - .thenReturn(mockCompilerPlugin); - Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); - Mockito.when(compilerPluginConfiguration.getChild("target")).thenReturn(compilerTarget); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); + Xpp3Dom compilerTarget = new Xpp3Dom("target"); + pluginConfiguration.addChild(compilerTarget); - Mockito.when(compilerTarget.getValue()).thenReturn("1.8"); + compilerTarget.setValue("1.8"); Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); - Mockito.when(compilerTarget.getValue()).thenReturn("1.6"); + compilerTarget.setValue("1.6"); Assert.assertEquals(6, mavenProjectProperties.getMajorJavaVersion()); - Mockito.when(compilerTarget.getValue()).thenReturn("13"); + compilerTarget.setValue("13"); Assert.assertEquals(13, mavenProjectProperties.getMajorJavaVersion()); } @Test public void testValidateBaseImageVersion_compilerPluginRelease() { Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-compiler-plugin")) - .thenReturn(mockCompilerPlugin); - Mockito.when(mockCompilerPlugin.getConfiguration()).thenReturn(compilerPluginConfiguration); - Mockito.when(compilerPluginConfiguration.getChild("release")).thenReturn(compilerRelease); + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getConfiguration()).thenReturn(pluginConfiguration); + Xpp3Dom compilerRelease = new Xpp3Dom("release"); + pluginConfiguration.addChild(compilerRelease); - Mockito.when(compilerRelease.getValue()).thenReturn("1.8"); + compilerRelease.setValue("1.8"); Assert.assertEquals(8, mavenProjectProperties.getMajorJavaVersion()); - Mockito.when(compilerRelease.getValue()).thenReturn("10"); + compilerRelease.setValue("10"); Assert.assertEquals(10, mavenProjectProperties.getMajorJavaVersion()); - Mockito.when(compilerRelease.getValue()).thenReturn("13"); + compilerRelease.setValue("13"); Assert.assertEquals(13, mavenProjectProperties.getMajorJavaVersion()); } @@ -408,7 +428,7 @@ public void isProgressFooterEnabled() { public void testCreateContainerBuilder_correctFiles() throws URISyntaxException, IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildContext buildContext = setupBuildContext("/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setUpBuildContext("/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); Path dependenciesPath = getResource("maven/application/dependencies"); @@ -453,7 +473,7 @@ public void testCreateContainerBuilder_correctFiles() @Test public void testCreateContainerBuilder_nonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { - BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setUpBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); assertNonDefaultAppRoot(buildContext); } @@ -465,7 +485,7 @@ public void testCreateContainerBuilder_packagedMode() Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); Mockito.when(mockBuild.getFinalName()).thenReturn("final-name"); - BuildContext buildContext = setupBuildContext("/app-root", ContainerizingMode.PACKAGED); + BuildContext buildContext = setUpBuildContext("/app-root", ContainerizingMode.PACKAGED); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); Assert.assertEquals(1, layers.dependenciesLayers.size()); @@ -514,7 +534,7 @@ public void testCreateContainerBuilder_warNonDefaultAppRoot() CacheDirectoryCreationException { Path unzipTarget = setUpWar(getResource("maven/webapp/final-name")); - BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setUpBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); ContainerBuilderLayers layers = new ContainerBuilderLayers(buildContext); assertSourcePathsUnordered( ImmutableList.of(unzipTarget.resolve("WEB-INF/lib/dependency-1.0.0.jar")), @@ -576,7 +596,7 @@ public void testCreateContainerBuilder_jarNonDefaultAppRoot() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { // Test when the default packaging is set Mockito.when(mockMavenProject.getPackaging()).thenReturn("jar"); - BuildContext buildContext = setupBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); + BuildContext buildContext = setUpBuildContext("/my/app", DEFAULT_CONTAINERIZING_MODE); assertNonDefaultAppRoot(buildContext); } @@ -585,7 +605,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfDoesNotExist() throws IOException, InvalidImageReferenceException, CacheDirectoryCreationException { setUpWar(temporaryFolder.newFolder("final-name").toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setUpBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -594,7 +614,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfLibDoesNotExist() temporaryFolder.newFolder("final-name", "WEB-INF", "classes"); setUpWar(temporaryFolder.getRoot().toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass + setUpBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -603,17 +623,7 @@ public void testCreateContainerBuilder_noErrorIfWebInfClassesDoesNotExist() temporaryFolder.newFolder("final-name", "WEB-INF", "lib"); setUpWar(temporaryFolder.getRoot().toPath()); - setupBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass - } - - @Test - public void testGetJarArtifact() { - Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); - Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); - - Assert.assertEquals( - temporaryFolder.getRoot().toPath().resolve("helloworld-1.jar"), - mavenProjectProperties.getJarArtifact()); + setUpBuildContext("/anything", DEFAULT_CONTAINERIZING_MODE); // should pass } @Test @@ -683,7 +693,258 @@ public void testClassifyDependencies() { newArtifact("com.test", "projectC", "3.0").getFile().toPath())); } - private BuildContext setupBuildContext(String appRoot, ContainerizingMode containerizingMode) + @Test + public void testGetChildValue_null() { + Assert.assertFalse(MavenProjectProperties.getChildValue(null).isPresent()); + Assert.assertFalse(MavenProjectProperties.getChildValue(null, "foo", "bar").isPresent()); + } + + @Test + public void testGetChildValue_noPathGiven() { + Xpp3Dom root = newXpp3Dom("root", "value"); + + Assert.assertEquals(Optional.of("value"), MavenProjectProperties.getChildValue(root)); + } + + @Test + public void testGetChildValue_noChild() { + Xpp3Dom root = newXpp3Dom("root", "value"); + + Assert.assertFalse(MavenProjectProperties.getChildValue(root, "foo").isPresent()); + Assert.assertFalse(MavenProjectProperties.getChildValue(root, "foo", "bar").isPresent()); + } + + @Test + public void testGetChildValue_childPathMatched() { + Xpp3Dom root = newXpp3Dom("root", "value"); + Xpp3Dom foo = addXpp3DomChild(root, "foo", "foo"); + addXpp3DomChild(foo, "bar", "bar"); + + Assert.assertEquals(Optional.of("foo"), MavenProjectProperties.getChildValue(root, "foo")); + Assert.assertEquals( + Optional.of("bar"), MavenProjectProperties.getChildValue(root, "foo", "bar")); + Assert.assertEquals(Optional.of("bar"), MavenProjectProperties.getChildValue(foo, "bar")); + } + + @Test + public void testGetChildValue_notFullyMatched() { + Xpp3Dom root = newXpp3Dom("root", "value"); + Xpp3Dom foo = addXpp3DomChild(root, "foo", "foo"); + + addXpp3DomChild(foo, "bar", "bar"); + Assert.assertFalse(MavenProjectProperties.getChildValue(root, "baz").isPresent()); + Assert.assertFalse(MavenProjectProperties.getChildValue(root, "foo", "baz").isPresent()); + } + + @Test + public void testGetChildValue_nullValue() { + Xpp3Dom root = new Xpp3Dom("root"); + addXpp3DomChild(root, "foo", null); + + Assert.assertFalse(MavenProjectProperties.getChildValue(root).isPresent()); + Assert.assertFalse(MavenProjectProperties.getChildValue(root, "foo").isPresent()); + } + + @Test + public void testJarRepackagedBySpringBoot_pluginNotApplied() { + Assert.assertFalse(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testJarRepackagedBySpringBoot_noExecutions() { + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Collections.emptyList()); + Assert.assertFalse(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testJarRepackagedBySpringBoot_noRepackageGoal() { + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getGoals()).thenReturn(Arrays.asList("goal", "foo", "bar")); + Assert.assertFalse(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testJarRepackagedBySpringBoot_repackageGoal() { + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getGoals()).thenReturn(Arrays.asList("goal", "repackage")); + Assert.assertTrue(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testJarRepackagedBySpringBoot_skipped() { + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getGoals()).thenReturn(Arrays.asList("repackage")); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "skip", "true"); + Assert.assertFalse(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testJarRepackagedBySpringBoot_skipNotTrue() { + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getGoals()).thenReturn(Arrays.asList("repackage")); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "skip", null); + Assert.assertTrue(mavenProjectProperties.jarRepackagedBySpringBoot()); + } + + @Test + public void testGetJarArtifact() throws IOException { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1.jar"), mavenProjectProperties.getJarArtifact()); + } + + @Test + public void testGetJarArtifact_outputDirectoryFromJarPlugin() throws IOException { + Mockito.when(mockMavenProject.getBasedir()).thenReturn(new File("/should/ignore")); + Mockito.when(mockBuild.getDirectory()).thenReturn("/should/ignore"); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-jar"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "outputDirectory", Paths.get("/jar/out").toString()); + + Assert.assertEquals( + Paths.get("/jar/out/helloworld-1.jar"), mavenProjectProperties.getJarArtifact()); + } + + @Test + public void testGetJarArtifact_relativeOutputDirectoryFromJarPlugin() throws IOException { + Mockito.when(mockMavenProject.getBasedir()).thenReturn(new File("/base/dir")); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-jar"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "outputDirectory", Paths.get("relative").toString()); + + Assert.assertEquals( + Paths.get("/base/dir/relative/helloworld-1.jar"), mavenProjectProperties.getJarArtifact()); + } + + @Test + public void testGetJarArtifact_classifier() throws IOException { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-jar"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "classifier", "a-class"); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1-a-class.jar"), mavenProjectProperties.getJarArtifact()); + } + + @Test + public void testGetJarArtifact_executionIdNotMatched() throws IOException { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("no-id-match"); + Mockito.lenient().when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "outputDirectory", "/should/ignore"); + addXpp3DomChild(pluginConfiguration, "classifier", "a-class"); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1.jar"), mavenProjectProperties.getJarArtifact()); + } + + @Test + public void testGetJarArtifact_originalJarCopiedIfSpringBoot() throws IOException { + temporaryFolder.newFile("helloworld-1.jar.original"); + Mockito.when(mockBuild.getDirectory()).thenReturn(temporaryFolder.getRoot().toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + setUpSpringBootFatJar(); + Path tempDirectory = temporaryFolder.newFolder("tmp").toPath(); + Mockito.when(mockTempDirectoryProvider.newDirectory()).thenReturn(tempDirectory); + + Assert.assertEquals( + tempDirectory.resolve("helloworld-1.original.jar"), + mavenProjectProperties.getJarArtifact()); + + mavenProjectProperties.waitForLoggingThread(); + Mockito.verify(mockLog) + .info("Spring Boot repackaging (fat JAR) detected; using the original JAR"); + } + + @Test + public void testGetJarArtifact_originalJarIfSpringBoot_differentDirectories() throws IOException { + Mockito.when(mockMavenProject.getBasedir()).thenReturn(new File("/should/ignore")); + Mockito.when(mockBuild.getDirectory()).thenReturn("/should/ignore"); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-jar"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "outputDirectory", Paths.get("/jar/out").toString()); + + setUpSpringBootFatJar(); + + Assert.assertEquals( + Paths.get("/jar/out/helloworld-1.jar"), mavenProjectProperties.getJarArtifact()); + + mavenProjectProperties.waitForLoggingThread(); + Mockito.verify(mockLog) + .info("Spring Boot repackaging (fat JAR) detected; using the original JAR"); + } + + @Test + public void testGetJarArtifact_originalJarCopiedIfSpringBoot_sameDirectory() throws IOException { + Path buildDirectory = temporaryFolder.newFolder("target").toPath(); + Files.createFile(buildDirectory.resolve("helloworld-1.jar.original")); + Mockito.when(mockMavenProject.getBasedir()).thenReturn(temporaryFolder.getRoot()); + Mockito.when(mockBuild.getDirectory()).thenReturn(buildDirectory.toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-jar-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-jar"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "outputDirectory", "target"); + + setUpSpringBootFatJar(); + Path tempDirectory = temporaryFolder.newFolder("tmp").toPath(); + Mockito.when(mockTempDirectoryProvider.newDirectory()).thenReturn(tempDirectory); + + Assert.assertEquals( + tempDirectory.resolve("helloworld-1.original.jar"), + mavenProjectProperties.getJarArtifact()); + + mavenProjectProperties.waitForLoggingThread(); + Mockito.verify(mockLog) + .info("Spring Boot repackaging (fat JAR) detected; using the original JAR"); + } + + private BuildContext setUpBuildContext(String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder.from(RegistryImage.named("base")) @@ -711,15 +972,12 @@ private Path setUpWar(Path explodedWar) throws IOException { return unzipTarget; } - private Artifact newArtifact(Path sourceJar) { - Artifact artifact = Mockito.mock(Artifact.class); - Mockito.when(artifact.getFile()).thenReturn(sourceJar.toFile()); - return artifact; - } - - private Artifact newArtifact(String group, String artifactId, String version) { - Artifact artifact = new DefaultArtifact(group, artifactId, version, null, "jar", "", null); - artifact.setFile(new File("/tmp/" + group + artifactId + version)); - return artifact; + private void setUpSpringBootFatJar() { + PluginExecution execution = Mockito.mock(PluginExecution.class); + Plugin plugin = Mockito.mock(Plugin.class); + Mockito.when(mockMavenProject.getPlugin("org.springframework.boot:spring-boot-maven-plugin")) + .thenReturn(plugin); + Mockito.when(plugin.getExecutions()).thenReturn(Arrays.asList(execution)); + Mockito.when(execution.getGoals()).thenReturn(Arrays.asList("repackage")); } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml new file mode 100644 index 0000000000..428853cf93 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + example + spring-boot + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.1.6.RELEASE + + + com.google.cloud.tools + jib-maven-plugin + 1.8.1-SNAPSHOT + + gcr.io/distroless/java:debug + ${_TARGET_IMAGE} + packaged + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/Application.java b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/Application.java new file mode 100644 index 0000000000..8c26fec153 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/Application.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/HelloController.java b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..3527662c07 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Hello world"; + } +} From fd4dfaa72e2359835ea3693c1ed7a8f224806fca Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Dec 2019 16:47:45 -0500 Subject: [PATCH 0851/2020] Fix Kokoro on MacOS (#2186) --- kokoro/continuous.sh | 11 +++++++---- kokoro/presubmit.sh | 13 +++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 45303a4e09..ae09b93512 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -3,14 +3,17 @@ set -e set -x -gcloud components install docker-credential-gcr +# On Mac, the default "credsStore" is set to "desktop". However, "desktop" is a +# protected credential store, so "docker login" fails to modify it. +# https://github.com/GoogleContainerTools/jib/issues/2189 +if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then + cat <<< '{"credsStore":"gcr"}' > "${HOME}/.docker/config.json" +fi -# For macOS to find docker-credential-gcr -export PATH=$PATH:/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/ +gcloud components install docker-credential-gcr # docker-credential-gcr uses GOOGLE_APPLICATION_CREDENTIALS as the credentials key file export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_KEYSTORE_DIR}/72743_jib_integration_testing_key -docker-credential-gcr configure-docker # Stops any left-over containers. docker stop $(docker ps --all --quiet) || true diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index 5fe0edf282..d688227a2f 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -3,8 +3,14 @@ set -e set -x +# On Mac, the default "credsStore" is set to "desktop". However, "desktop" is a +# protected credential store, so "docker login" fails to modify it. +# https://github.com/GoogleContainerTools/jib/issues/2189 +if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then + cat <<< '{"credsStore":"gcr"}' > "${HOME}/.docker/config.json" +fi + gcloud components install docker-credential-gcr -export PATH=$PATH:/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/ # Stops any left-over containers. docker stop $(docker ps --all --quiet) || true @@ -23,10 +29,5 @@ fi cd github/jib -# Workaround for issue with calling 'docker login'. It defaults to using docker-credential-osxkeychain and errors with: -# Error saving credentials: error storing credentials - err: exit status 1, out: `User interaction is not allowed.` -# TODO: Follow-up with Kokoro about why this is happening. -rm /usr/local/bin/docker-credential-osxkeychain || true - # we only run integration tests on jib-core for presubmit ./gradlew clean build :jib-core:integrationTest --info --stacktrace From 0e11c5e8dbef0446a27c703e9885ef126651fa2f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 5 Dec 2019 17:01:36 -0500 Subject: [PATCH 0852/2020] Build correct OCI tars (#2181) --- jib-core/CHANGELOG.md | 1 + .../jib/builder/steps/LoadDockerStep.java | 2 +- .../jib/builder/steps/WriteTarFileStep.java | 2 +- .../tools/jib/cache/CacheStorageReader.java | 6 +- .../tools/jib/configuration/BuildContext.java | 4 +- .../cloud/tools/jib/docker/DockerClient.java | 1 + .../jib/{docker => image}/ImageTarball.java | 57 +++++++++- .../image/json/BuildableManifestTemplate.java | 15 ++- .../jib/image/json/OciIndexTemplate.java | 75 +++++++++++++ ...Template.java => OciManifestTemplate.java} | 2 +- .../tools/jib/registry/ManifestPuller.java | 12 +- .../jib/api/JibContainerBuilderTest.java | 4 +- .../jib/configuration/BuildContextTest.java | 4 +- .../tools/jib/docker/DockerClientTest.java | 1 + .../{docker => image}/ImageTarballTest.java | 103 +++++++++++++++--- .../image/json/ImageToJsonTranslatorTest.java | 4 +- .../image/json/JsonToImageTranslatorTest.java | 2 +- .../jib/image/json/OciIndexTemplateTest.java | 76 +++++++++++++ ...Test.java => OciManifestTemplateTest.java} | 10 +- .../jib/registry/ManifestPullerTest.java | 8 +- jib-core/src/test/resources/core/fileA | 2 +- jib-core/src/test/resources/core/fileB | 2 +- .../test/resources/core/json/ociindex.json | 1 + jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + .../common/PluginConfigurationProcessor.java | 5 +- 26 files changed, 350 insertions(+), 51 deletions(-) rename jib-core/src/main/java/com/google/cloud/tools/jib/{docker => image}/ImageTarball.java (61%) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java rename jib-core/src/main/java/com/google/cloud/tools/jib/image/json/{OCIManifestTemplate.java => OciManifestTemplate.java} (98%) rename jib-core/src/test/java/com/google/cloud/tools/jib/{docker => image}/ImageTarballTest.java (56%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java rename jib-core/src/test/java/com/google/cloud/tools/jib/image/json/{OCIManifestTemplateTest.java => OciManifestTemplateTest.java} (91%) create mode 100644 jib-core/src/test/resources/core/json/ociindex.json diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 91dd2dbde6..5c1fd0db13 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) - Fixed inefficient communications when interacting with insecure registries and servers (when `Containerizer#setAllowInsecureRegistries(boolean)` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) +- Building a tarball with `OCI` format now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) ## 0.12.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java index 38c202989f..8e3b941981 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java @@ -21,10 +21,10 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.docker.DockerClient; -import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.ImageTarball; import java.io.IOException; import java.util.concurrent.Callable; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java index 7e224ccb70..88688a1375 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java @@ -19,9 +19,9 @@ import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildContext; -import com.google.cloud.tools.jib.docker.ImageTarball; import com.google.cloud.tools.jib.filesystem.FileOperations; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.image.ImageTarball; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java index 01ecde9fce..6b0ffe1bd0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.ManifestAndConfig; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; @@ -122,9 +122,9 @@ Optional retrieveMetadata(ImageReference imageReference) if (V22ManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { manifestTemplate = JsonTemplateMapper.readJsonFromFile(manifestPath, V22ManifestTemplate.class); - } else if (OCIManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + } else if (OciManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { manifestTemplate = - JsonTemplateMapper.readJsonFromFile(manifestPath, OCIManifestTemplate.class); + JsonTemplateMapper.readJsonFromFile(manifestPath, OciManifestTemplate.class); } else { throw new CacheCorruptedException( cacheStorageFiles.getCacheDirectory(), "Unknown manifest mediaType: " + mediaType); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java index f252e1a379..9f0d3c67ce 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.global.JibSystemProperties; import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.annotations.VisibleForTesting; @@ -156,7 +156,7 @@ public Builder setTargetFormat(ImageFormat targetFormat) { this.targetFormat = targetFormat == ImageFormat.Docker ? V22ManifestTemplate.class - : OCIManifestTemplate.class; + : OciManifestTemplate.class; return this; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index 2ae05e43ed..e5bed9c0c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.http.NotifyingOutputStream; +import com.google.cloud.tools.jib.image.ImageTarball; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java similarity index 61% rename from jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java index fd23ec4721..8b11380ae5 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/ImageTarball.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java @@ -14,19 +14,23 @@ * the License. */ -package com.google.cloud.tools.jib.docker; +package com.google.cloud.tools.jib.image; +import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; -import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.tar.TarStreamBuilder; import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Collections; /** Translates an {@link Image} to a tarball that can be loaded into Docker. */ @@ -61,6 +65,53 @@ public ImageTarball( } public void writeTo(OutputStream out) throws IOException { + if (image.getImageFormat() == OciManifestTemplate.class) { + ociWriteTo(out); + } else { + dockerWriteTo(out); + } + } + + private void ociWriteTo(OutputStream out) throws IOException { + TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); + OciManifestTemplate manifest = new OciManifestTemplate(); + + // Adds all the layers to the tarball and manifest + for (Layer layer : image.getLayers()) { + DescriptorDigest digest = layer.getBlobDescriptor().getDigest(); + long size = layer.getBlobDescriptor().getSize(); + + tarStreamBuilder.addBlobEntry(layer.getBlob(), size, "blobs/sha256/" + digest.getHash()); + manifest.addLayer(size, digest); + } + + // Adds the container configuration to the tarball and manifest + JsonTemplate containerConfiguration = + new ImageToJsonTranslator(image).getContainerConfiguration(); + BlobDescriptor configDescriptor = Digests.computeDigest(containerConfiguration); + manifest.setContainerConfiguration(configDescriptor.getSize(), configDescriptor.getDigest()); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(containerConfiguration), + "blobs/sha256/" + configDescriptor.getDigest().getHash()); + + // Adds the manifest to the tarball + BlobDescriptor manifestDescriptor = Digests.computeDigest(manifest); + tarStreamBuilder.addByteEntry( + JsonTemplateMapper.toByteArray(manifest), + "blobs/sha256/" + manifestDescriptor.getDigest().getHash()); + + // Adds the oci-layout and index.json + tarStreamBuilder.addByteEntry( + "{\"imageLayoutVersion\": \"1.0.0\"}".getBytes(StandardCharsets.UTF_8), "oci-layout"); + OciIndexTemplate index = new OciIndexTemplate(); + // TODO: figure out how to tag with allTargetImageTags + index.addManifest(manifestDescriptor, imageReference.toStringWithTag()); + tarStreamBuilder.addByteEntry(JsonTemplateMapper.toByteArray(index), "index.json"); + + tarStreamBuilder.writeAsTarArchiveTo(out); + } + + private void dockerWriteTo(OutputStream out) throws IOException { TarStreamBuilder tarStreamBuilder = new TarStreamBuilder(); DockerManifestEntryTemplate manifestTemplate = new DockerManifestEntryTemplate(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index 7918ec8fe1..d28fab30d2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -19,14 +19,16 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; /** * Parent class for image manifest JSON templates that can be built. * * @see V22ManifestTemplate Docker V2.2 format - * @see OCIManifestTemplate OCI format + * @see OciManifestTemplate OCI format */ public interface BuildableManifestTemplate extends ManifestTemplate { @@ -43,6 +45,7 @@ class ContentDescriptorTemplate implements JsonTemplate { @Nullable private String mediaType; @Nullable private DescriptorDigest digest; private long size; + @Nullable private Map annotations; ContentDescriptorTemplate(String mediaType, long size, DescriptorDigest digest) { this.mediaType = mediaType; @@ -71,6 +74,16 @@ public DescriptorDigest getDigest() { void setDigest(DescriptorDigest digest) { this.digest = digest; } + + @VisibleForTesting + @Nullable + public Map getAnnotations() { + return annotations; + } + + void setAnnotations(Map annotations) { + this.annotations = ImmutableMap.copyOf(annotations); + } } /** @return the media type for this manifest, specific to the image format */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java new file mode 100644 index 0000000000..af93e5f336 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON template for OCI archive "index.json" file. + * + *

Example manifest JSON: + * + *

{@code
+ * {
+ *   "schemaVersion": 2,
+ *   "manifests": [
+ *     {
+ *       "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ *       "digest": "sha256:e684b1dceef404268f17d4adf7f755fd9912b8ae64864b3954a83ebb8aa628b3",
+ *       "size": 1132,
+ *       "annotations": {
+ *         "org.opencontainers.image.ref.name": "gcr.io/project/image:tag"
+ *       }
+ *     }
+ *   ]
+ * }
+ * }
+ * + * @see OCI Image + * Index Specification + */ +public class OciIndexTemplate implements JsonTemplate { + + private final int schemaVersion = 2; + private final List manifests = + new ArrayList<>(); + + /** + * Adds a manifest reference with the given {@link BlobDescriptor}. + * + * @param descriptor the manifest blob descriptor + * @param imageReferenceName the image reference name + */ + public void addManifest(BlobDescriptor descriptor, String imageReferenceName) { + BuildableManifestTemplate.ContentDescriptorTemplate contentDescriptorTemplate = + new BuildableManifestTemplate.ContentDescriptorTemplate( + OciManifestTemplate.MANIFEST_MEDIA_TYPE, descriptor.getSize(), descriptor.getDigest()); + contentDescriptorTemplate.setAnnotations( + ImmutableMap.of("org.opencontainers.image.ref.name", imageReferenceName)); + manifests.add(contentDescriptorTemplate); + } + + @VisibleForTesting + public List getManifests() { + return manifests; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java similarity index 98% rename from jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java rename to jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java index 6f820a5a39..7639b99a07 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java @@ -54,7 +54,7 @@ * @see OCI Image * Manifest Specification */ -public class OCIManifestTemplate implements BuildableManifestTemplate { +public class OciManifestTemplate implements BuildableManifestTemplate { /** The OCI manifest media type. */ public static final String MANIFEST_MEDIA_TYPE = "application/vnd.oci.image.manifest.v1+json"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java index c8438ee257..1e9756af15 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; @@ -72,8 +72,8 @@ public List getAccept() { if (manifestTemplateClass.equals(V22ManifestTemplate.class)) { return Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE); } - if (manifestTemplateClass.equals(OCIManifestTemplate.class)) { - return Collections.singletonList(OCIManifestTemplate.MANIFEST_MEDIA_TYPE); + if (manifestTemplateClass.equals(OciManifestTemplate.class)) { + return Collections.singletonList(OciManifestTemplate.MANIFEST_MEDIA_TYPE); } if (manifestTemplateClass.equals(V22ManifestListTemplate.class)) { return Collections.singletonList(V22ManifestListTemplate.MANIFEST_MEDIA_TYPE); @@ -83,7 +83,7 @@ public List getAccept() { // it, we only handle it if referenced by sha256 (see getManifestTemplateFromJson) in which // case registries ignore the "accept" directive and just return a manifest list anyway. return Arrays.asList( - OCIManifestTemplate.MANIFEST_MEDIA_TYPE, + OciManifestTemplate.MANIFEST_MEDIA_TYPE, V22ManifestTemplate.MANIFEST_MEDIA_TYPE, V21ManifestTemplate.MEDIA_TYPE); } @@ -148,9 +148,9 @@ private T getManifestTemplateFromJson(String jsonString) return manifestTemplateClass.cast( JsonTemplateMapper.readJson(jsonString, V22ManifestTemplate.class)); } - if (OCIManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { + if (OciManifestTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { return manifestTemplateClass.cast( - JsonTemplateMapper.readJson(jsonString, OCIManifestTemplate.class)); + JsonTemplateMapper.readJson(jsonString, OciManifestTemplate.class)); } if (V22ManifestListTemplate.MANIFEST_MEDIA_TYPE.equals(mediaType)) { return manifestTemplateClass.cast( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index 3a41704f5b..21df4eec9c 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -19,7 +19,7 @@ import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; import com.google.cloud.tools.jib.configuration.ImageConfiguration; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.collect.ImmutableMap; @@ -197,7 +197,7 @@ public void testToBuildContext() .withAdditionalTag("tag1") .withAdditionalTag("tag2") .setToolName("toolName")); - Assert.assertSame(OCIManifestTemplate.class, buildContext.getTargetFormat()); + Assert.assertSame(OciManifestTemplate.class, buildContext.getTargetFormat()); Assert.assertEquals( ImmutableSet.of("latest", "tag1", "tag2"), buildContext.getAllTargetImageTags()); Assert.assertEquals("toolName", buildContext.getToolName()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java index 3dbb4ddb36..4c1dc76e17 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java @@ -27,7 +27,7 @@ import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -68,7 +68,7 @@ public void testBuilder() throws Exception { Map expectedEnvironment = ImmutableMap.of("key", "value"); ImmutableSet expectedExposedPorts = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)); Map expectedLabels = ImmutableMap.of("key1", "value1", "key2", "value2"); - Class expectedTargetFormat = OCIManifestTemplate.class; + Class expectedTargetFormat = OciManifestTemplate.class; Path expectedApplicationLayersCacheDirectory = Paths.get("application/layers"); Path expectedBaseImageLayersCacheDirectory = Paths.get("base/image/layers"); List expectedLayerConfigurations = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index b255ccf63c..6bc1f41dea 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; +import com.google.cloud.tools.jib.image.ImageTarball; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java similarity index 56% rename from jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java index 6455d73e97..0610e0f2bf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java @@ -14,7 +14,7 @@ * the License. */ -package com.google.cloud.tools.jib.docker; +package com.google.cloud.tools.jib.image; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; @@ -22,10 +22,10 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.docker.json.DockerManifestEntryTemplate; -import com.google.cloud.tools.jib.image.Image; -import com.google.cloud.tools.jib.image.Layer; -import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; +import com.google.cloud.tools.jib.image.json.OciIndexTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableList; @@ -45,6 +45,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -55,22 +56,23 @@ @RunWith(MockitoJUnitRunner.class) public class ImageTarballTest { + private Path fileA, fileB; + private DescriptorDigest fakeDigestA, fakeDigestB; + @Mock private Layer mockLayer1; @Mock private Layer mockLayer2; - @Test - public void testWriteTo() - throws InvalidImageReferenceException, IOException, URISyntaxException, - LayerPropertyNotFoundException, DigestException { - Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); - Path fileB = Paths.get(Resources.getResource("core/fileB").toURI()); + @Before + public void setup() throws URISyntaxException, IOException, DigestException { + fileA = Paths.get(Resources.getResource("core/fileA").toURI()); + fileB = Paths.get(Resources.getResource("core/fileB").toURI()); long fileASize = Files.size(fileA); long fileBSize = Files.size(fileB); - DescriptorDigest fakeDigestA = + fakeDigestA = DescriptorDigest.fromHash( "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); - DescriptorDigest fakeDigestB = + fakeDigestB = DescriptorDigest.fromHash( "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc6"); @@ -82,16 +84,21 @@ public void testWriteTo() Mockito.when(mockLayer2.getBlobDescriptor()) .thenReturn(new BlobDescriptor(fileBSize, fakeDigestB)); Mockito.when(mockLayer2.getDiffId()).thenReturn(fakeDigestB); + } + + @Test + public void testWriteTo_docker() + throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException { Image testImage = Image.builder(V22ManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); - ImageTarball imageToTarball = + ImageTarball imageTarball = new ImageTarball( testImage, ImageReference.parse("my/image:tag"), ImmutableSet.of("tag", "another-tag", "tag3")); ByteArrayOutputStream out = new ByteArrayOutputStream(); - imageToTarball.writeTo(out); + imageTarball.writeTo(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { @@ -132,4 +139,72 @@ public void testWriteTo() manifest.getRepoTags()); } } + + @Test + public void testWriteTo_oci() + throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException, + DigestException { + Image testImage = + Image.builder(OciManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); + ImageTarball imageTarball = + new ImageTarball( + testImage, + ImageReference.parse("my/image:tag"), + ImmutableSet.of("tag", "another-tag", "tag3")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + imageTarball.writeTo(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in)) { + + // Verifies layer with fileA was added. + TarArchiveEntry headerFileALayer = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("blobs/sha256/" + fakeDigestA.getHash(), headerFileALayer.getName()); + String fileAString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileA)), fileAString); + + // Verifies layer with fileB was added. + TarArchiveEntry headerFileBLayer = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("blobs/sha256/" + fakeDigestB.getHash(), headerFileBLayer.getName()); + String fileBString = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals(Blobs.writeToString(Blobs.from(fileB)), fileBString); + + // Verifies container configuration was added. + TarArchiveEntry headerContainerConfiguration = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals( + "blobs/sha256/011212cff4d5d6b18c7d3a00a7a2701514a1fdd3ec0d250a03756f84f3d955d4", + headerContainerConfiguration.getName()); + JsonTemplateMapper.readJson(tarArchiveInputStream, ContainerConfigurationTemplate.class); + + // Verifies manifest was added. + TarArchiveEntry headerManifest = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals( + "blobs/sha256/1543d061159a8d6877087938bfd62681cdeff873e1fa3e1fcf12dec358c112a4", + headerManifest.getName()); + JsonTemplateMapper.readJson(tarArchiveInputStream, OciManifestTemplate.class); + + // Verifies oci-layout was added. + TarArchiveEntry headerOciLayout = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("oci-layout", headerOciLayout.getName()); + String ociLayoutJson = + CharStreams.toString( + new InputStreamReader(tarArchiveInputStream, StandardCharsets.UTF_8)); + Assert.assertEquals("{\"imageLayoutVersion\": \"1.0.0\"}", ociLayoutJson); + + // Verifies index.json was added. + TarArchiveEntry headerIndex = tarArchiveInputStream.getNextTarEntry(); + Assert.assertEquals("index.json", headerIndex.getName()); + OciIndexTemplate index = + JsonTemplateMapper.readJson(tarArchiveInputStream, OciIndexTemplate.class); + BuildableManifestTemplate.ContentDescriptorTemplate indexManifest = + index.getManifests().get(0); + Assert.assertEquals( + "1543d061159a8d6877087938bfd62681cdeff873e1fa3e1fcf12dec358c112a4", + indexManifest.getDigest().getHash()); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java index cd99363669..edf9c84197 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java @@ -140,8 +140,8 @@ public void testGetManifest_v22() throws URISyntaxException, IOException, Digest @Test public void testGetManifest_oci() throws URISyntaxException, IOException, DigestException { - setUp(OCIManifestTemplate.class); - testGetManifest(OCIManifestTemplate.class, "core/json/translated_ocimanifest.json"); + setUp(OciManifestTemplate.class); + testGetManifest(OciManifestTemplate.class, "core/json/translated_ocimanifest.json"); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index 7ab492f35c..aada3e0835 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -82,7 +82,7 @@ public void testToImage_v22() public void testToImage_oci() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { - testToImage_buildable("core/json/ocimanifest.json", OCIManifestTemplate.class); + testToImage_buildable("core/json/ocimanifest.json", OciManifestTemplate.class); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java new file mode 100644 index 0000000000..c608979e68 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.image.json; + +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.InvalidImageReferenceException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestException; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link OciIndexTemplate}. */ +public class OciIndexTemplateTest { + + @Test + public void testToJson() + throws DigestException, IOException, URISyntaxException, InvalidImageReferenceException { + // Loads the expected JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex.json").toURI()); + String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + + // Creates the JSON object to serialize. + OciIndexTemplate ociIndexJson = new OciIndexTemplate(); + ociIndexJson.addManifest( + new BlobDescriptor( + 1000, + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")), + "regis.try/repo:tag"); + + // Serializes the JSON object. + Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(ociIndexJson)); + } + + @Test + public void testFromJson() throws IOException, URISyntaxException, DigestException { + // Loads the JSON string. + Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex.json").toURI()); + + // Deserializes into a manifest JSON object. + OciIndexTemplate ociIndexJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciIndexTemplate.class); + BuildableManifestTemplate.ContentDescriptorTemplate manifest = + ociIndexJson.getManifests().get(0); + + Assert.assertEquals( + DescriptorDigest.fromDigest( + "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), + manifest.getDigest()); + Assert.assertEquals( + "regis.try/repo:tag", manifest.getAnnotations().get("org.opencontainers.image.ref.name")); + Assert.assertEquals(1000, manifest.getSize()); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java similarity index 91% rename from jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java index f9a6027705..6eaac9c4bf 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OCIManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java @@ -29,8 +29,8 @@ import org.junit.Assert; import org.junit.Test; -/** Tests for {@link OCIManifestTemplate}. */ -public class OCIManifestTemplateTest { +/** Tests for {@link OciManifestTemplate}. */ +public class OciManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { @@ -39,7 +39,7 @@ public void testToJson() throws DigestException, IOException, URISyntaxException String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Creates the JSON object to serialize. - OCIManifestTemplate manifestJson = new OCIManifestTemplate(); + OciManifestTemplate manifestJson = new OciManifestTemplate(); manifestJson.setContainerConfiguration( 1000, @@ -61,8 +61,8 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); // Deserializes into a manifest JSON object. - OCIManifestTemplate manifestJson = - JsonTemplateMapper.readJsonFromFile(jsonFile, OCIManifestTemplate.class); + OciManifestTemplate manifestJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, OciManifestTemplate.class); Assert.assertEquals( DescriptorDigest.fromDigest( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java index ba4527d8b9..f802499da6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.image.json.ManifestTemplate; -import com.google.cloud.tools.jib.image.json.OCIManifestTemplate; +import com.google.cloud.tools.jib.image.json.OciManifestTemplate; import com.google.cloud.tools.jib.image.json.UnknownManifestFormatException; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; @@ -231,15 +231,15 @@ public void testGetContent() { public void testGetAccept() { Assert.assertEquals( Arrays.asList( - OCIManifestTemplate.MANIFEST_MEDIA_TYPE, + OciManifestTemplate.MANIFEST_MEDIA_TYPE, V22ManifestTemplate.MANIFEST_MEDIA_TYPE, V21ManifestTemplate.MEDIA_TYPE), testManifestPuller.getAccept()); Assert.assertEquals( - Collections.singletonList(OCIManifestTemplate.MANIFEST_MEDIA_TYPE), + Collections.singletonList(OciManifestTemplate.MANIFEST_MEDIA_TYPE), new ManifestPuller<>( - fakeRegistryEndpointRequestProperties, "test-image-tag", OCIManifestTemplate.class) + fakeRegistryEndpointRequestProperties, "test-image-tag", OciManifestTemplate.class) .getAccept()); Assert.assertEquals( Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE), diff --git a/jib-core/src/test/resources/core/fileA b/jib-core/src/test/resources/core/fileA index b29ddafaad..d9ef1a6b36 100644 --- a/jib-core/src/test/resources/core/fileA +++ b/jib-core/src/test/resources/core/fileA @@ -1 +1 @@ -Crepe cakes are good. +Crepe cakes are good. \ No newline at end of file diff --git a/jib-core/src/test/resources/core/fileB b/jib-core/src/test/resources/core/fileB index 710951eef5..c891cde53b 100644 --- a/jib-core/src/test/resources/core/fileB +++ b/jib-core/src/test/resources/core/fileB @@ -1 +1 @@ -Fast image builds are great. +Fast image builds are great. \ No newline at end of file diff --git a/jib-core/src/test/resources/core/json/ociindex.json b/jib-core/src/test/resources/core/json/ociindex.json new file mode 100644 index 0000000000..9ab5e57040 --- /dev/null +++ b/jib-core/src/test/resources/core/json/ociindex.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000,"annotations":{"org.opencontainers.image.ref.name":"regis.try/repo:tag"}}]} \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 912ffd999c..255dd62560 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) ### Fixed +- `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) ## 1.8.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 506d225c2f..58e0e85600 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Fixed `` being ignored if `` are not explicitly defined. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2160)) - Now `packaged` works as intended with Spring Boot projects that generate a fat JAR. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) - Now `packaged` correctly identifies the packaged JAR generated at a non-default location when configured with the Maven Jar Plugin's `` and ``. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) +- `jib:buildTar` with `OCI` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) ## 1.8.0 diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 7938532790..778a839fdd 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -126,6 +126,9 @@ public static JibBuildRunner createJibBuildRunnerForTarImage( processCommonConfiguration( rawConfiguration, inferredAuthProvider, projectProperties, containerizer); + // Note Docker build doesn't set the configured format. + jibContainerBuilder.setFormat(rawConfiguration.getImageFormat()); + return JibBuildRunner.forBuildTar( jibContainerBuilder, containerizer, @@ -174,7 +177,7 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( processCommonConfiguration( rawConfiguration, inferredAuthProvider, projectProperties, containerizer); - // Note Docker and tar builds don't set the configured format. + // Note Docker build doesn't set the configured format. jibContainerBuilder.setFormat(rawConfiguration.getImageFormat()); return JibBuildRunner.forBuildImage( From 894da4343877c398b0163e57389e44a23fdc8be3 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Dec 2019 18:10:19 -0500 Subject: [PATCH 0853/2020] Fix Kokoro Ubuntu (#2191) --- kokoro/continuous.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index ae09b93512..907970fb22 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -14,6 +14,7 @@ gcloud components install docker-credential-gcr # docker-credential-gcr uses GOOGLE_APPLICATION_CREDENTIALS as the credentials key file export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_KEYSTORE_DIR}/72743_jib_integration_testing_key +docker-credential-gcr configure-docker # Stops any left-over containers. docker stop $(docker ps --all --quiet) || true From 78395cb0245c66097f1ff2da2d15522dde51c5a4 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Dec 2019 18:24:40 -0500 Subject: [PATCH 0854/2020] Add Cloud Run button on example Maven projects (#2185) --- examples/dropwizard/README.md | 6 +++++- .../main/java/example/resources/HelloWorldResource.java | 2 +- examples/java-agent/README.md | 4 ++++ examples/spring-boot/README.md | 8 ++++++-- .../spring-boot/src/main/java/hello/HelloController.java | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/dropwizard/README.md b/examples/dropwizard/README.md index d2c6b2e6d4..384c121893 100644 --- a/examples/dropwizard/README.md +++ b/examples/dropwizard/README.md @@ -6,7 +6,7 @@ 1. Start the application - **With Docker**: `docker run --rm -p 8080:8080 dropwizard-jib-example:1` - **Without Docker**: `./mvnw exec:java` -1. Check that your application is running at http://localhost:8080/hello-world +1. Check that your application is running at http://localhost:8080 ## Health Check @@ -36,3 +36,7 @@ The remainder of the archetype code was filled-in following the above guide. Learn [more about Jib](https://github.com/GoogleContainerTools/jib). Learn [more about Dropwizard](https://dropwizard.io). + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/dropwizard) diff --git a/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java index a754cb9caf..987eca2810 100644 --- a/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java +++ b/examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java @@ -31,7 +31,7 @@ import example.api.Saying; import example.config.HelloWorldConfiguration; -@Path("/hello-world") +@Path("/") @Produces(MediaType.APPLICATION_JSON) public class HelloWorldResource { diff --git a/examples/java-agent/README.md b/examples/java-agent/README.md index 7e4b9843aa..672ff2a5d7 100644 --- a/examples/java-agent/README.md +++ b/examples/java-agent/README.md @@ -8,3 +8,7 @@ To build the image: 1. Run `mvn package` or `./gradlew` to build the image. SparkJava listens on port 4567 by default. + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/java-agent) diff --git a/examples/spring-boot/README.md b/examples/spring-boot/README.md index cac5d9f6a4..822a67a761 100644 --- a/examples/spring-boot/README.md +++ b/examples/spring-boot/README.md @@ -35,11 +35,11 @@ IMAGE= kubectl run spring-boot-jib --image=$IMAGE --port=8080 --restart=Never # Wait until pod is running -kubectl port-forward spring-boot-jib 8080 > /dev/null 2>&1 & +kubectl port-forward spring-boot-jib 8080 ``` ```shell curl localhost:8080 -> Greetings from Kubernetes! +> Greetings from Spring Boot and Jib! ``` \* If you are using Gradle, use `./gradlew jib --image=$IMAGE` instead of the `./mvnw` command @@ -50,3 +50,7 @@ Give it a [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style ## More information Learn [more about Jib](https://github.com/GoogleContainerTools/jib). + +## Build and run on Google Cloud + +[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run?git_repo=https://github.com/GoogleContainerTools/jib.git&dir=examples/spring-boot) diff --git a/examples/spring-boot/src/main/java/hello/HelloController.java b/examples/spring-boot/src/main/java/hello/HelloController.java index 8b9ace4914..8052d26866 100644 --- a/examples/spring-boot/src/main/java/hello/HelloController.java +++ b/examples/spring-boot/src/main/java/hello/HelloController.java @@ -8,6 +8,6 @@ public class HelloController { @RequestMapping("/") public String index() { - return "Greetings from Kubernetes!"; + return "Greetings from Spring Boot and Jib!"; } } From 050a1618694cee058ab56758dd4fcfd9a481c33d Mon Sep 17 00:00:00 2001 From: Vladimir Kulev Date: Fri, 6 Dec 2019 01:27:38 +0200 Subject: [PATCH 0855/2020] Use correct configuration for Skaffold dependencies (#2188) --- .../cloud/tools/jib/gradle/skaffold/FilesTaskV2.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java index 3cebdaa420..8b68d17eb1 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -36,6 +36,7 @@ import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.PublishArtifact; import org.gradle.api.initialization.Settings; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskAction; @@ -95,7 +96,8 @@ public void listFiles() throws IOException { } // Add SNAPSHOT, non-project dependency jars - for (File file : project.getConfigurations().getByName("runtime")) { + for (File file : + project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)) { if (!projectDependencyJars.contains(file) && file.toString().contains("SNAPSHOT")) { skaffoldFilesOutput.addInput(file.toPath()); projectDependencyJars.add(file); // Add to set to avoid printing the same files twice @@ -176,7 +178,10 @@ private Set findProjectDependencies(Project project) { // Search through all dependencies for (Configuration configuration : - currentProject.getConfigurations().getByName("runtime").getHierarchy()) { + currentProject + .getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) + .getHierarchy()) { for (Dependency dependency : configuration.getDependencies()) { if (dependency instanceof ProjectDependency) { // If this is a project dependency, save it From 91d591ad553a16a30fc5dc341cca5de1e1715e1b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 5 Dec 2019 18:28:15 -0500 Subject: [PATCH 0856/2020] Update README.md (#2187) --- jib-gradle-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index bc5cf36e06..d5efa3f94e 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -239,7 +239,7 @@ Property | Type | Default | Description Property | Type | Default | Description --- | --- | --- | --- -`tar` | `File` | `(project-dir)/build/jib-image.tar` | The path of the tarball generated by `jib:buildTar`. Relative paths are resolved relative to the project root. +`tar` | `File` | `(project-dir)/build/jib-image.tar` | The path of the tarball generated by `jibBuildTar`. Relative paths are resolved relative to the project root. `digest` | `File` | `(project-dir)/build/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. `imageId` | `File` | `(project-dir)/build/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. From a692b8b0cde1f982f6e28309dd08ca80460950e2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 6 Dec 2019 13:54:16 -0500 Subject: [PATCH 0857/2020] Detect bootJar task (Spring Boot fat JAR) and enable jar task to use the thin JAR (#2178) * Detect bootJar task and enable jar task * CHANGELOG --- jib-gradle-plugin/CHANGELOG.md | 2 + .../cloud/tools/jib/gradle/JibRunHelper.java | 22 ++++++ .../SpringBootProjectIntegrationTest.java | 71 +++++++++++++++++++ .../jib/gradle/WarProjectIntegrationTest.java | 23 +----- .../gradle/projects/spring-boot/build.gradle | 24 +++++++ .../projects/spring-boot/settings.gradle | 1 + .../src/main/java/hello/Application.java | 12 ++++ .../src/main/java/hello/HelloController.java | 13 ++++ .../jib/gradle/GradleProjectProperties.java | 5 +- .../cloud/tools/jib/gradle/JibPlugin.java | 15 +++- .../cloud/tools/jib/gradle/JibPluginTest.java | 23 ++++++ .../jib/maven/MavenProjectProperties.java | 2 +- .../tools/jib/maven/HttpGetVerifier.java | 2 +- 13 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java create mode 100644 jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 255dd62560..c956d8ba2f 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file. ### Fixed - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Now `jib.containerizingMode='packaged'` works as intended with Spring Boot projects that generate a fat JAR. ([#2178](https://github.com/GoogleContainerTools/jib/pull/2178)) + ## 1.8.0 ### Changed diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index 9ad410e839..e8aa743cf0 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -20,7 +20,11 @@ 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.blob.Blobs; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -29,6 +33,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -38,6 +43,23 @@ /** Helper class to run integration tests. */ public class JibRunHelper { + @Nullable + static String getContent(URL url) throws InterruptedException { + for (int i = 0; i < 40; i++) { + Thread.sleep(500); + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream in = connection.getInputStream()) { + return Blobs.writeToString(Blobs.from(in)); + } + } + } catch (IOException ignored) { + } + } + return null; + } + static String buildAndRun(TestProject testProject, String imageReference) throws IOException, InterruptedException, DigestException { return buildAndRun(testProject, imageReference, "build.gradle"); diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java new file mode 100644 index 0000000000..b801ecb109 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.gradle; + +import com.google.cloud.tools.jib.Command; +import com.google.cloud.tools.jib.IntegrationTestingConfiguration; +import java.io.IOException; +import java.net.URL; +import java.security.DigestException; +import javax.annotation.Nullable; +import org.junit.After; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; + +/** Integration tests for building Spring Boot images. */ +public class SpringBootProjectIntegrationTest { + + @ClassRule public static final TestProject springBootProject = new TestProject("spring-boot"); + + @Nullable private String containerName; + + @After + public void tearDown() throws IOException, InterruptedException { + if (containerName != null) { + new Command("docker", "stop", containerName).run(); + } + } + + @Test + public void testBuild_packagedMode() throws IOException, InterruptedException, DigestException { + buildAndRunWebApp(springBootProject, "springboot:gradle", "build.gradle"); + + String output = + new Command( + "docker", + "exec", + containerName, + "/busybox/wc", + "-c", + "/app/classpath/spring-boot-original.jar") + .run(); + Assert.assertEquals("1360 /app/classpath/spring-boot-original.jar\n", output); + + Assert.assertEquals("Hello world", JibRunHelper.getContent(new URL("http://localhost:8080"))); + } + + private void buildAndRunWebApp(TestProject project, String label, String gradleBuildFile) + throws IOException, InterruptedException, DigestException { + String nameBase = IntegrationTestingConfiguration.getTestRepositoryLocation() + '/'; + String targetImage = nameBase + label + System.nanoTime(); + String output = + JibRunHelper.buildAndRun( + springBootProject, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); + containerName = output.trim(); + } +} diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java index 1b161061ce..01fc3dfb08 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java @@ -18,10 +18,7 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.IntegrationTestingConfiguration; -import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.security.DigestException; import javax.annotation.Nullable; @@ -35,23 +32,6 @@ public class WarProjectIntegrationTest { @ClassRule public static final TestProject servlet25Project = new TestProject("war_servlet25"); - @Nullable - private static String getContent(URL url) throws InterruptedException { - for (int i = 0; i < 40; i++) { - Thread.sleep(500); - try { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { - try (InputStream in = connection.getInputStream()) { - return Blobs.writeToString(Blobs.from(in)); - } - } - } catch (IOException ex) { - } - } - return null; - } - @Nullable private String containerName; @After @@ -80,6 +60,7 @@ private void verifyBuildAndRun(TestProject project, String label, String gradleB JibRunHelper.buildAndRun(project, targetImage, gradleBuildFile, "--detach", "-p8080:8080"); containerName = output.trim(); - Assert.assertEquals("Hello world", getContent(new URL("http://localhost:8080/hello"))); + Assert.assertEquals( + "Hello world", JibRunHelper.getContent(new URL("http://localhost:8080/hello"))); } } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle new file mode 100644 index 0000000000..85ec587126 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'org.springframework.boot' version '2.1.6.RELEASE' + id 'io.spring.dependency-management' version '1.0.6.RELEASE' + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +jib { + from.image = 'gcr.io/distroless/java:debug' + to.image = System.getProperty("_TARGET_IMAGE") + to.credHelper = 'gcr' + containerizingMode='packaged' +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle new file mode 100644 index 0000000000..ca13736740 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'spring-boot' \ No newline at end of file diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java new file mode 100644 index 0000000000..8c26fec153 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java @@ -0,0 +1,12 @@ +package hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java new file mode 100644 index 0000000000..3527662c07 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java @@ -0,0 +1,13 @@ +package hello; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Hello world"; + } +} diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 6df423e31d..0c69c3067c 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -237,8 +237,9 @@ public JibContainerBuilder createJibContainerBuilder( case PACKAGED: // Add a JAR Jar jarTask = (Jar) project.getTasks().findByName("jar"); - javaContainerBuilder.addToClasspath( - jarTask.getDestinationDir().toPath().resolve(jarTask.getArchiveName())); + Path jarPath = jarTask.getDestinationDir().toPath().resolve(jarTask.getArchiveName()); + consoleLogger.log(Level.DEBUG, "Using JAR: " + jarPath); + javaContainerBuilder.addToClasspath(jarPath); break; default: diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index e186668928..22cf639978 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -37,6 +37,7 @@ import org.gradle.api.plugins.BasePlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { @@ -168,8 +169,9 @@ public void apply(Project project) { project.afterEvaluate( projectAfterEvaluation -> { try { - TaskProvider warTask = TaskCommon.getWarTaskProvider(project); - TaskProvider bootWarTask = TaskCommon.getBootWarTaskProvider(project); + TaskProvider warTask = TaskCommon.getWarTaskProvider(projectAfterEvaluation); + TaskProvider bootWarTask = + TaskCommon.getBootWarTaskProvider(projectAfterEvaluation); List> dependsOnTask = new ArrayList<>(); if (warTask != null || bootWarTask != null) { // Have all tasks depend on the 'war' and/or 'bootWar' task. @@ -181,7 +183,14 @@ public void apply(Project project) { } } else if ("packaged".equals(jibExtension.getContainerizingMode())) { // Have all tasks depend on the 'jar' task. - dependsOnTask.add(projectAfterEvaluation.getTasks().named("jar")); + TaskProvider jarTask = projectAfterEvaluation.getTasks().named("jar"); + dependsOnTask.add(jarTask); + + if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { + Jar jar = (Jar) jarTask.get(); + jar.setEnabled(true); + jar.setClassifier("original"); + } } else { // Have all tasks depend on the 'classes' task. dependsOnTask.add(projectAfterEvaluation.getTasks().named("classes")); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 6faad5db92..f26ea41f02 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -34,6 +34,7 @@ import org.gradle.api.internal.project.ProjectInternal; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.tasks.Jar; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; @@ -264,6 +265,28 @@ public void testWebAppProject_bootWarDisabled() { } } + @Test + public void testSpringBootJarProject_nonPackagedMode() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + + Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertFalse(jarTask.getEnabled()); + Assert.assertEquals("", jarTask.getClassifier()); + } + + @Test + public void testSpringBootJarProject_packagedMode() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + + Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jarTask.getEnabled()); + Assert.assertEquals("original", jarTask.getClassifier()); + } + @Test public void testNonWebAppProject() { Project project = createProject("java", "com.google.cloud.tools.jib"); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 9c62485a97..ad531bdfc9 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -422,7 +422,7 @@ public boolean isOffline() { } /** - * Gets the path of the JAR that the Maven JAR Plugin would generate. + * Gets the path of the JAR that the Maven JAR Plugin generates. * *

https://maven.apache.org/plugins/maven-jar-plugin/jar-mojo.html * https://github.com/apache/maven-jar-plugin/blob/80f58a84aacff6e671f5a601d62a3a3800b507dc/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java#L177 diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java index 9230a727b9..12dec3d93c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java @@ -46,7 +46,7 @@ private static String getContent(URL url) throws InterruptedException { return Blobs.writeToString(Blobs.from(in)); } } - } catch (IOException ex) { + } catch (IOException ignored) { } } return null; From 2f05348364003b2a0d7d676fe5697009d983b46b Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 11 Dec 2019 16:20:18 -0500 Subject: [PATCH 0858/2020] Minor cleanups (#2196) --- .../cloud/tools/jib/api/JavaContainerBuilder.java | 2 +- .../cloud/tools/jib/api/JibContainerDescription.java | 2 +- .../cloud/tools/jib/image/ImageTarballTest.java | 3 +-- .../tools/jib/image/json/OciIndexTemplateTest.java | 4 +--- .../tools/jib/gradle/GradleProjectProperties.java | 12 ++++-------- jib-maven-plugin/build.gradle | 8 ++++---- .../tools/jib/maven/MavenProjectProperties.java | 9 +++------ 7 files changed, 15 insertions(+), 25 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index bd263c6227..6f21b21a10 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -568,7 +568,7 @@ public JibContainerBuilder toContainerBuilder() throws IOException { appRoot.resolve(resourcesDestination)); } - // Detect duplicate filenames across all layer types + // Detect duplicate filenames across all dependency layer types List duplicates = Streams.concat( addedDependencies.stream(), diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java index 3862fcefd8..1b89c4e6a4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java @@ -35,7 +35,7 @@ public class JibContainerDescription { } /** - * Returns a list of "user configured" layers, does *not* include base layer information. + * Returns a list of "user configured" layers, does not include base layer information. * * @return An {@link ImmutableList} of {@link LayerConfiguration}s */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java index 0610e0f2bf..2b11e2b360 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java @@ -142,8 +142,7 @@ public void testWriteTo_docker() @Test public void testWriteTo_oci() - throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException, - DigestException { + throws InvalidImageReferenceException, IOException, LayerPropertyNotFoundException { Image testImage = Image.builder(OciManifestTemplate.class).addLayer(mockLayer1).addLayer(mockLayer2).build(); ImageTarball imageTarball = diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java index c608979e68..7f2724814e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java @@ -17,7 +17,6 @@ package com.google.cloud.tools.jib.image.json; import com.google.cloud.tools.jib.api.DescriptorDigest; -import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; @@ -35,8 +34,7 @@ public class OciIndexTemplateTest { @Test - public void testToJson() - throws DigestException, IOException, URISyntaxException, InvalidImageReferenceException { + public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. Path jsonFile = Paths.get(Resources.getResource("core/json/ociindex.json").toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index 0c69c3067c..c96f528424 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -20,7 +20,6 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -158,8 +157,7 @@ public JibContainerBuilder createJibContainerBuilder( try { if (isWarProject()) { String warFilePath = getWarFilePath(); - consoleLogger.log( - Level.INFO, "WAR project identified, creating WAR image from: " + warFilePath); + log(LogEvent.info("WAR project identified, creating WAR image from: " + warFilePath)); Path explodedWarPath = tempDirectoryProvider.newDirectory(); ZipUtil.unzip(Paths.get(warFilePath), explodedWarPath); return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); @@ -229,8 +227,7 @@ public JibContainerBuilder createJibContainerBuilder( javaContainerBuilder.addClasses(classesOutputDirectory.toPath()); } if (classesOutputDirectories.isEmpty()) { - consoleLogger.log( - Level.WARN, "No classes files were found - did you compile your project?"); + log(LogEvent.warn("No classes files were found - did you compile your project?")); } break; @@ -238,7 +235,7 @@ public JibContainerBuilder createJibContainerBuilder( // Add a JAR Jar jarTask = (Jar) project.getTasks().findByName("jar"); Path jarPath = jarTask.getDestinationDir().toPath().resolve(jarTask.getArchiveName()); - consoleLogger.log(Level.DEBUG, "Using JAR: " + jarPath); + log(LogEvent.debug("Using JAR: " + jarPath)); javaContainerBuilder.addToClasspath(jarPath); break; @@ -278,8 +275,7 @@ public void configureEventHandlers(Containerizer containerizer) { containerizer .addEventHandler(LogEvent.class, this::log) .addEventHandler( - TimerEvent.class, - new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + TimerEvent.class, new TimerEventHandler(message -> log(LogEvent.debug(message)))) .addEventHandler( ProgressEvent.class, new ProgressEventHandler( diff --git a/jib-maven-plugin/build.gradle b/jib-maven-plugin/build.gradle index 878e1c18a4..ddedf9744a 100644 --- a/jib-maven-plugin/build.gradle +++ b/jib-maven-plugin/build.gradle @@ -1,8 +1,8 @@ plugins { - id 'io.freefair.maven-plugin' - id 'net.researchgate.release' - id 'maven-publish' - id 'eclipse' + id 'io.freefair.maven-plugin' + id 'net.researchgate.release' + id 'maven-publish' + id 'eclipse' } // only maven specific dependencies should be versioned, everything else should be defined by constrains in diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index ad531bdfc9..3f070d83c5 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -21,7 +21,6 @@ import com.google.cloud.tools.jib.api.JavaContainerBuilder.LayerType; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.api.LogEvent.Level; import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.event.events.TimerEvent; import com.google.cloud.tools.jib.event.progress.ProgressEventHandler; @@ -319,8 +318,7 @@ public void configureEventHandlers(Containerizer containerizer) { containerizer .addEventHandler(LogEvent.class, this::log) .addEventHandler( - TimerEvent.class, - new TimerEventHandler(message -> consoleLogger.log(LogEvent.Level.DEBUG, message))) + TimerEvent.class, new TimerEventHandler(message -> log(LogEvent.debug(message)))) .addEventHandler( ProgressEvent.class, new ProgressEventHandler( @@ -454,8 +452,7 @@ Path getJarArtifact() throws IOException { String suffix = ".jar"; if (jarRepackagedBySpringBoot()) { - consoleLogger.log( - Level.LIFECYCLE, "Spring Boot repackaging (fat JAR) detected; using the original JAR"); + log(LogEvent.lifecycle("Spring Boot repackaging (fat JAR) detected; using the original JAR")); if (outputDirectory.equals(buildDirectory)) { // Spring renames original only when needed suffix += ".original"; } @@ -464,7 +461,7 @@ Path getJarArtifact() throws IOException { String noSuffixJarName = project.getBuild().getFinalName() + (classifier == null ? "" : '-' + classifier); Path jarPath = outputDirectory.resolve(noSuffixJarName + suffix); - consoleLogger.log(Level.DEBUG, "Using JAR: " + jarPath); + log(LogEvent.debug("Using JAR: " + jarPath)); if (".jar".equals(suffix)) { return jarPath; From 1439efa0280ce7f962b9d9c2ee1f91d014b74601 Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 18 Dec 2019 12:05:38 -0500 Subject: [PATCH 0859/2020] Use a different marker so we don't clash with other jib json outputs (#2192) * Use a different marker so we don't clash with other jib json outputs * use feature/version style formatting --- .../google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java | 2 +- .../cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java | 2 +- .../com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java | 2 +- .../google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java index c580b15c53..1cb38bb82b 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java @@ -80,7 +80,7 @@ public void listFilesAndTargets() { PluginConfigurationProcessor.getSkaffoldSyncMap(configuration, projectProperties); System.out.println(); - System.out.println("BEGIN JIB JSON"); + System.out.println("BEGIN JIB JSON: SYNCMAP/1"); System.out.println(syncMapJson); } catch (Exception ex) { diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java index 8fd5329712..e0c683c290 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java @@ -72,7 +72,7 @@ private static SkaffoldSyncMapTemplate generateTemplate( List outputLines = Splitter.on(System.lineSeparator()).omitEmptyStrings().splitToList(buildResult.getOutput()); Assert.assertEquals(2, outputLines.size()); - Assert.assertEquals("BEGIN JIB JSON", outputLines.get(0)); + Assert.assertEquals("BEGIN JIB JSON: SYNCMAP/1", outputLines.get(0)); return SkaffoldSyncMapTemplate.from(outputLines.get(1)); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java index 908c8a04fc..930ea8dd30 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojo.java @@ -79,7 +79,7 @@ public void execute() throws MojoExecutionException { PluginConfigurationProcessor.getSkaffoldSyncMap(configuration, projectProperties); System.out.println(); - System.out.println("BEGIN JIB JSON"); + System.out.println("BEGIN JIB JSON: SYNCMAP/1"); System.out.println(syncMapJson); } catch (Exception ex) { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java index d76412731a..68e9b545ad 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/SyncMapMojoTest.java @@ -65,7 +65,8 @@ private static String getSyncMapJson(Path projectRoot, String module) throws VerificationException, IOException { Path logFile = runBuild(projectRoot, module, null); List outputLines = Files.readAllLines(logFile, StandardCharsets.UTF_8); - Assert.assertEquals(3, outputLines.size()); // we expect ["\n", "BEGIN JIB JSON", ""] + Assert.assertEquals(3, outputLines.size()); // we expect ["\n", "", ""] + Assert.assertEquals("BEGIN JIB JSON: SYNCMAP/1", outputLines.get(1)); return outputLines.get(2); // this is the JSON output } From b763c10519f8b130c2f7d90c26a6e1ee69661b9e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 18 Dec 2019 18:38:18 -0500 Subject: [PATCH 0860/2020] Upgrade Google HTTP Client, resolving issue with Red Hat OpenShift and Quay registries (#2201) * Upgrade google-http-client * Update CHANGELOGs --- build.gradle | 4 ++-- jib-core/CHANGELOG.md | 1 + .../tools/jib/filesystem/UserCacheHome.java | 2 +- .../tools/jib/http/FailoverHttpClient.java | 1 + .../google/cloud/tools/jib/image/Image.java | 2 +- .../jib/registry/RegistryEndpointCaller.java | 9 --------- .../jib/http/FailoverHttpClientTest.java | 2 ++ .../WithServerFailoverHttpClientTest.java | 16 ++++++++++----- .../registry/RegistryEndpointCallerTest.java | 20 ------------------- jib-gradle-plugin/CHANGELOG.md | 2 +- jib-maven-plugin/CHANGELOG.md | 1 + 11 files changed, 21 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 14cd8f0772..06bddc0d5d 100644 --- a/build.gradle +++ b/build.gradle @@ -46,8 +46,8 @@ subprojects { // For Google libraries, check , , , // ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml // for best compatibility. - GOOGLE_HTTP_CLIENT: '1.33.0', - GOOGLE_HTTP_CLIENT_APACHE_V2: '1.33.0', + GOOGLE_HTTP_CLIENT: '1.34.0', + GOOGLE_HTTP_CLIENT_APACHE_V2: '1.34.0', GOOGLE_AUTH_LIBRARY_OAUTH2_HTTP: '0.18.0', GUAVA: '28.1-jre', diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 5c1fd0db13..16fb5fd9dd 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Changed - Each local base image layer is pushed immediately after being compressed, rather than waiting for all layers to finish compressing before starting to push. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) ### Fixed diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java index 5f4ea64d36..4547269154 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java @@ -51,7 +51,7 @@ public static Path getCacheHome() { * Returns {@code $XDG_CACHE_HOME}, if available, or resolves the OS-specific user cache home * based. * - *

For Linus, this is {@code $HOME/.cache/}. + *

For Linux, this is {@code $HOME/.cache/}. * *

For Windows, this is {@code %LOCALAPPDATA%}. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index e86abdfdb9..7f447980e9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -299,6 +299,7 @@ private Response call(String httpMethod, URL url, Request request, HttpTransport httpTransport .createRequestFactory() .buildRequest(httpMethod, new GenericUrl(url), request.getHttpContent()) + .setUseRawRedirectUrls(true) .setHeaders(requestHeaders); if (request.getHttpTimeout() != null) { httpRequest.setConnectTimeout(request.getHttpTimeout()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java index bbadd46223..1068bdd7d0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java @@ -353,7 +353,7 @@ private Image( } public Class getImageFormat() { - return this.imageFormat; + return imageFormat; } @Nullable diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index debfbd4737..07471c53eb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -164,15 +164,6 @@ private T call(URL url) throws IOException, RegistryException { throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } - // 301 (Moved Permanently), 302 (Found), 303 (See Other), and 307 (Temporary Redirect) are - // automatically followed by Google HTTP Client (setFollowRedirects(true)), but 308 isn't. - // https://github.com/googleapis/google-http-java-client/issues/873 - // TODO: remove this when the bug is fixed. - } else if (responseException.getStatusCode() == STATUS_CODE_PERMANENT_REDIRECT) { - // 'Location' header can be relative or absolute. - URL redirectLocation = new URL(url, responseException.getHeaders().getLocation()); - return call(redirectLocation); - } else { // Unknown throw responseException; diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java index 701cb10fbe..bca07ea172 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -408,6 +408,8 @@ private void setUpMocks( mockHttpRequestFactory.buildRequest(Mockito.any(), urlCaptor.capture(), Mockito.any())) .thenReturn(mockHttpRequest); + Mockito.when(mockHttpRequest.setUseRawRedirectUrls(Mockito.anyBoolean())) + .thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setHeaders(httpHeadersCaptor.capture())) .thenReturn(mockHttpRequest); Mockito.when(mockHttpRequest.setConnectTimeout(Mockito.anyInt())).thenReturn(mockHttpRequest); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java index 7fb95628fc..ae84ccb0fe 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java @@ -175,6 +175,7 @@ public void testRedirectionUrls() String url2 = "?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; String url3 = "?id=303&_auth_=exp=1572285389~hmac=f0a387f0"; String url4 = "?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614"; + String url5 = "?id=308&_auth_=exp=1572285389~hmac=f0a387f0"; String redirect301 = "HTTP/1.1 301 Moved Permanently\nLocation: " + url1 + "\nContent-Length: 0\n\n"; @@ -182,9 +183,11 @@ public void testRedirectionUrls() String redirect303 = "HTTP/1.1 303 See Other\nLocation: " + url3 + "\nContent-Length: 0\n\n"; String redirect307 = "HTTP/1.1 307 Temporary Redirect\nLocation: " + url4 + "\nContent-Length: 0\n\n"; + String redirect308 = + "HTTP/1.1 308 Permanent Redirect\nLocation: " + url5 + "\nContent-Length: 0\n\n"; String ok200 = "HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"; List responses = - Arrays.asList(redirect301, redirect302, redirect303, redirect307, ok200); + Arrays.asList(redirect301, redirect302, redirect303, redirect307, redirect308, ok200); FailoverHttpClient httpClient = new FailoverHttpClient(true /*insecure*/, false, logger); try (TestWebServer server = new TestWebServer(false, responses, 1)) { @@ -192,18 +195,21 @@ public void testRedirectionUrls() Assert.assertThat( server.getInputRead(), - CoreMatchers.containsString("GET /?id=301&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + CoreMatchers.containsString("GET /?id=301&_auth_=exp=1572285389~hmac=f0a387f0 ")); Assert.assertThat( server.getInputRead(), CoreMatchers.containsString( - "GET /?id=302&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + "GET /?id=302&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614 ")); Assert.assertThat( server.getInputRead(), - CoreMatchers.containsString("GET /?id=303&_auth_=exp%3D1572285389~hmac%3Df0a387f0 ")); + CoreMatchers.containsString("GET /?id=303&_auth_=exp=1572285389~hmac=f0a387f0 ")); Assert.assertThat( server.getInputRead(), CoreMatchers.containsString( - "GET /?id=307&Signature=2wYOD0a%2BDAkK/9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + "GET /?id=307&Signature=2wYOD0a%2BDAkK%2F9lQJUOuIpYti8o%3D&Expires=1569997614 ")); + Assert.assertThat( + server.getInputRead(), + CoreMatchers.containsString("GET /?id=308&_auth_=exp=1572285389~hmac=f0a387f0 ")); } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 3487df0b58..1fa203ce08 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -108,7 +108,6 @@ private static ResponseException mockResponseException( int statusCode, @Nullable HttpHeaders headers) { ResponseException mock = Mockito.mock(ResponseException.class); Mockito.when(mock.getStatusCode()).thenReturn(statusCode); - Mockito.when(mock.getHeaders()).thenReturn(headers != null ? headers : new HttpHeaders()); return mock; } @@ -241,25 +240,6 @@ public void testCall_unknown() throws IOException, RegistryException { } } - @Test - public void testCall_permanentRedirect() throws IOException, RegistryException { - ResponseException redirectException = - mockResponseException( - RegistryEndpointCaller.STATUS_CODE_PERMANENT_REDIRECT, - new HttpHeaders().setLocation("https://newlocation")); - - // Make httpClient.call() throw first, then succeed. - setUpRegistryResponse(redirectException); - Mockito.when( - mockHttpClient.call( - Mockito.eq("httpMethod"), - Mockito.eq(new URL("https://newlocation")), - Mockito.any())) - .thenReturn(mockResponse); - - Assert.assertEquals("body", endpointCaller.call()); - } - @Test public void testCall_logErrorOnIoExceptions() throws IOException, RegistryException { IOException ioException = new IOException("detailed exception message"); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c956d8ba2f..16f86d7b71 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,10 +10,10 @@ All notable changes to this project will be documented in this file. - Removed `jibDockerBuild.dockerClient` in favor of `jib.dockerClient`. ([#1983](https://github.com/GoogleContainerTools/jib/issues/1983)) - Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) - Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) ### Fixed - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) - - Now `jib.containerizingMode='packaged'` works as intended with Spring Boot projects that generate a fat JAR. ([#2178](https://github.com/GoogleContainerTools/jib/pull/2178)) ## 1.8.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 58e0e85600..bcaf096e35 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) - Removed deprecated `` configuration in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) +- HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) ### Fixed From f0b876330a64d074784afa43750c1009f0dd4035 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 18 Dec 2019 18:40:21 -0500 Subject: [PATCH 0861/2020] Display where Jib got credentials for base and target images (#2200) * Display where Jib got credentials * Fix Windows path test --- .../RetrieveRegistryCredentialsStep.java | 2 -- .../frontend/CredentialRetrieverFactory.java | 12 +++---- .../DockerConfigCredentialRetriever.java | 8 +++++ .../CredentialRetrieverFactoryTest.java | 35 ++++++++++++------- .../DockerConfigCredentialRetrieverTest.java | 10 ++++-- .../maven/MavenSettingsServerCredentials.java | 2 +- 6 files changed, 45 insertions(+), 24 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index 3d4f04bcc6..46ab96346c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -84,8 +84,6 @@ public Optional call() throws CredentialRetrievalException { } } - // If no credentials found, give an info (not warning because in most cases, the base image is - // public and does not need extra credentials) and return empty. eventHandlers.dispatch( LogEvent.info("No credentials could be retrieved for registry " + registry)); return Optional.empty(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 382df76d1e..57c7cc8cc2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -110,7 +110,7 @@ public static CredentialRetrieverFactory forImage( */ public CredentialRetriever known(Credential credential, String credentialSource) { return () -> { - logGotCredentialsFrom(credentialSource); + logGotCredentialsFrom("credentials from " + credentialSource); return Optional.of(credential); }; } @@ -255,15 +255,16 @@ public CredentialRetriever googleApplicationDefaultCredentials() { CredentialRetriever dockerConfig( DockerConfigCredentialRetriever dockerConfigCredentialRetriever) { return () -> { + Path configFile = dockerConfigCredentialRetriever.getDockerConfigFile(); try { Optional credentials = dockerConfigCredentialRetriever.retrieve(logger); if (credentials.isPresent()) { - logGotCredentialsFrom("credentials from Docker config"); + logGotCredentialsFrom("credentials from Docker config (" + configFile + ")"); return credentials; } } catch (IOException ex) { - logger.accept(LogEvent.info("Unable to parse Docker config")); + logger.accept(LogEvent.info("Unable to parse Docker config file: " + configFile)); } return Optional.empty(); }; @@ -276,12 +277,11 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper) dockerCredentialHelperFactory .create(imageReference.getRegistry(), credentialHelper) .retrieve(); - logGotCredentialsFrom("credentials from " + credentialHelper.getFileName().toString()); + logGotCredentialsFrom("credential helper " + credentialHelper.getFileName().toString()); return credentials; } private void logGotCredentialsFrom(String credentialSource) { - logger.accept( - LogEvent.info("Using " + credentialSource + " for " + imageReference.getRegistry())); + logger.accept(LogEvent.lifecycle("Using " + credentialSource + " for " + imageReference)); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index ab15f83d67..a388d85f7e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -66,6 +66,10 @@ public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { this.dockerConfigFile = dockerConfigFile; } + public Path getDockerConfigFile() { + return dockerConfigFile; + } + /** * Retrieves credentials for a registry. Tries all possible known aliases. * @@ -98,6 +102,8 @@ Optional retrieve(DockerConfig dockerConfig, Consumer logg dockerConfig.getCredentialHelperFor(registryAlias); if (dockerCredentialHelper != null) { try { + Path helperPath = dockerCredentialHelper.getCredentialHelper(); + logger.accept(LogEvent.info("trying " + helperPath + " for " + registryAlias)); // Tries with the given registry alias (may be the original registry). return Optional.of(dockerCredentialHelper.retrieve()); @@ -122,6 +128,8 @@ Optional retrieve(DockerConfig dockerConfig, Consumer logg new String(Base64.decodeBase64(auth), StandardCharsets.UTF_8); String username = usernameColonPassword.substring(0, usernameColonPassword.indexOf(":")); String password = usernameColonPassword.substring(usernameColonPassword.indexOf(":") + 1); + logger.accept( + LogEvent.info("Docker config auths section defines credentials for " + registryAlias)); return Optional.of(Credential.from(username, password)); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 24de724303..8f11377479 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -67,24 +67,25 @@ public void setUp() @Test public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("registry", "repository"); + createCredentialRetrieverFactory("registry", "repo"); Assert.assertEquals( Optional.of(FAKE_CREDENTIALS), credentialRetrieverFactory - .dockerCredentialHelper(Paths.get("docker-credential-helper")) + .dockerCredentialHelper(Paths.get("docker-credential-foo")) .retrieve()); Mockito.verify(mockDockerCredentialHelperFactory) - .create("registry", Paths.get("docker-credential-helper")); + .create("registry", Paths.get("docker-credential-foo")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using credentials from docker-credential-helper for registry")); + .accept( + LogEvent.lifecycle("Using credential helper docker-credential-foo for registry/repo")); } @Test public void testWellKnownCredentialHelpers() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("something.gcr.io", "repository"); + createCredentialRetrieverFactory("something.gcr.io", "repo"); Assert.assertEquals( Optional.of(FAKE_CREDENTIALS), @@ -93,7 +94,9 @@ public void testWellKnownCredentialHelpers() throws CredentialRetrievalException Mockito.verify(mockDockerCredentialHelperFactory) .create("something.gcr.io", Paths.get("docker-credential-gcr")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using credentials from docker-credential-gcr for something.gcr.io")); + .accept( + LogEvent.lifecycle( + "Using credential helper docker-credential-gcr for something.gcr.io/repo")); } @Test @@ -106,7 +109,7 @@ public void testWellKnownCredentialHelpers_info() Mockito.when(mockDockerCredentialHelper.retrieve()).thenThrow(notFoundException); CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("something.amazonaws.com", "repository"); + createCredentialRetrieverFactory("something.amazonaws.com", "repo"); Assert.assertFalse( credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve().isPresent()); @@ -120,19 +123,23 @@ public void testWellKnownCredentialHelpers_info() @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("registry", "repository"); + createCredentialRetrieverFactory("registry", "repo"); + Path dockerConfig = Paths.get("/foo/config.json"); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = Mockito.mock(DockerConfigCredentialRetriever.class); Mockito.when(dockerConfigCredentialRetriever.retrieve(mockLogger)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); + Mockito.when(dockerConfigCredentialRetriever.getDockerConfigFile()).thenReturn(dockerConfig); Assert.assertEquals( Optional.of(FAKE_CREDENTIALS), credentialRetrieverFactory.dockerConfig(dockerConfigCredentialRetriever).retrieve()); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using credentials from Docker config for registry")); + .accept( + LogEvent.lifecycle( + "Using credentials from Docker config (" + dockerConfig + ") for registry/repo")); } @Test @@ -189,7 +196,7 @@ public void testGoogleApplicationDefaultCredentials_refreshFailure() public void testGoogleApplicationDefaultCredentials_endUserCredentials() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + createCredentialRetrieverFactory("awesome.gcr.io", "repo"); Credential credential = credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); @@ -200,7 +207,9 @@ public void testGoogleApplicationDefaultCredentials_endUserCredentials() Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using Google Application Default Credentials for awesome.gcr.io")); + .accept( + LogEvent.lifecycle( + "Using Google Application Default Credentials for awesome.gcr.io/repo")); Mockito.verifyNoMoreInteractions(mockLogger); } @@ -212,7 +221,7 @@ public void testGoogleApplicationDefaultCredentials_serviceAccount() .thenReturn(mockGoogleCredentials); CredentialRetrieverFactory credentialRetrieverFactory = - createCredentialRetrieverFactory("gcr.io", "repository"); + createCredentialRetrieverFactory("gcr.io", "repo"); Credential credential = credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); @@ -227,7 +236,7 @@ public void testGoogleApplicationDefaultCredentials_serviceAccount() Mockito.verify(mockLogger) .accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using Google Application Default Credentials for gcr.io")); + .accept(LogEvent.lifecycle("Using Google Application Default Credentials for gcr.io/repo")); Mockito.verifyNoMoreInteractions(mockLogger); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index b4c01b9776..81895b8513 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -70,20 +70,26 @@ public void testRetrieve_hasAuth() throws IOException { Assert.assertTrue(credentials.isPresent()); Assert.assertEquals("some", credentials.get().getUsername()); Assert.assertEquals("other:auth", credentials.get().getPassword()); + Mockito.verify(mockLogger) + .accept( + LogEvent.info( + "Docker config auths section defines credentials for some other registry")); } @Test public void testRetrieve_credentialHelperTakesPrecedenceOverAuth() { Mockito.when(mockDockerConfig.getCredentialHelperFor("some registry")) .thenReturn(mockDockerCredentialHelper); + Mockito.when(mockDockerCredentialHelper.getCredentialHelper()) + .thenReturn(Paths.get("docker-credential-foo")); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = new DockerConfigCredentialRetriever("some registry", dockerConfigFile); - dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger); - Assert.assertEquals( Optional.of(FAKE_CREDENTIAL), dockerConfigCredentialRetriever.retrieve(mockDockerConfig, mockLogger)); + Mockito.verify(mockLogger) + .accept(LogEvent.info("trying docker-credential-foo for some registry")); } @Test diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index a42d4a6f6e..6ada727ac4 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -34,7 +34,7 @@ */ class MavenSettingsServerCredentials implements InferredAuthProvider { - static final String CREDENTIAL_SOURCE = "Maven settings"; + static final String CREDENTIAL_SOURCE = "Maven settings file"; private final Settings settings; private final SettingsDecrypter decrypter; From 0bf0b8185187771533ba3d1841e41cdfade064b9 Mon Sep 17 00:00:00 2001 From: sullis Date: Thu, 2 Jan 2020 10:42:15 -0800 Subject: [PATCH 0862/2020] mockito 3.2.4 (#2204) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 06bddc0d5d..a3a4f99df0 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ subprojects { //test JUNIT: '4.12', - MOCKITO_CORE: '3.1.0', + MOCKITO_CORE: '3.2.4', SLF4J_API: '1.7.25', SYSTEM_RULES: '1.19.0', ] From 9ecc0e9c8d0badcb15bd88625400c264882a10f6 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 3 Jan 2020 13:48:43 -0500 Subject: [PATCH 0863/2020] Support "warName" property of maven-war-plugin (#2208) --- jib-maven-plugin/CHANGELOG.md | 1 + .../jib/maven/MavenProjectProperties.java | 21 ++++++- .../jib/maven/MavenProjectPropertiesTest.java | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index bcaf096e35..2a0e6df442 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Now `packaged` works as intended with Spring Boot projects that generate a fat JAR. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) - Now `packaged` correctly identifies the packaged JAR generated at a non-default location when configured with the Maven Jar Plugin's `` and ``. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) - `jib:buildTar` with `OCI` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Fixed an issue where configuring the `` property of the Maven WAR plugin fails the build. ([#2206](https://github.com/GoogleContainerTools/jib/issues/2206)) ## 1.8.0 diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 3f070d83c5..4d1a5aea1f 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -220,8 +220,7 @@ public JibContainerBuilder createJibContainerBuilder( throws IOException { try { if (isWarProject()) { - Build build = project.getBuild(); - Path war = Paths.get(build.getDirectory(), build.getFinalName() + ".war"); + Path war = getWarArtifact(); Path explodedWarPath = tempDirectoryProvider.newDirectory(); ZipUtil.unzip(war, explodedWarPath); return JavaContainerBuilderHelper.fromExplodedWar(javaContainerBuilder, explodedWarPath); @@ -419,6 +418,24 @@ public boolean isOffline() { return session.isOffline(); } + @VisibleForTesting + Path getWarArtifact() { + Build build = project.getBuild(); + String warName = build.getFinalName(); + + Plugin warPlugin = project.getPlugin("org.apache.maven.plugins:maven-war-plugin"); + if (warPlugin != null) { + for (PluginExecution execution : warPlugin.getExecutions()) { + if ("default-war".equals(execution.getId())) { + Xpp3Dom configuration = (Xpp3Dom) execution.getConfiguration(); + warName = getChildValue(configuration, "warName").orElse(warName); + } + } + } + + return Paths.get(build.getDirectory(), warName + ".war"); + } + /** * Gets the path of the JAR that the Maven JAR Plugin generates. * diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java index 6a3a7b8c50..5c7f2643c6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenProjectPropertiesTest.java @@ -944,6 +944,61 @@ public void testGetJarArtifact_originalJarCopiedIfSpringBoot_sameDirectory() thr .info("Spring Boot repackaging (fat JAR) detected; using the original JAR"); } + @Test + public void testGetWarArtifact() { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1.war"), mavenProjectProperties.getWarArtifact()); + } + + @Test + public void testGetWarArtifact_warNameProperty() { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-war-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-war"); + Mockito.when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "warName", "baz"); + + Assert.assertEquals(Paths.get("/foo/bar/baz.war"), mavenProjectProperties.getWarArtifact()); + } + + @Test + public void testGetWarArtifact_noWarNameProperty() { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-war-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("default-war"); + Mockito.lenient().when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1.war"), mavenProjectProperties.getWarArtifact()); + } + + @Test + public void testGetWarArtifact_executionIdNotMatched() { + Mockito.when(mockBuild.getDirectory()).thenReturn(Paths.get("/foo/bar").toString()); + Mockito.when(mockBuild.getFinalName()).thenReturn("helloworld-1"); + + Mockito.when(mockMavenProject.getPlugin("org.apache.maven.plugins:maven-war-plugin")) + .thenReturn(mockPlugin); + Mockito.when(mockPlugin.getExecutions()).thenReturn(Arrays.asList(mockPluginExecution)); + Mockito.when(mockPluginExecution.getId()).thenReturn("no-id-match"); + Mockito.lenient().when(mockPluginExecution.getConfiguration()).thenReturn(pluginConfiguration); + addXpp3DomChild(pluginConfiguration, "warName", "baz"); + + Assert.assertEquals( + Paths.get("/foo/bar/helloworld-1.war"), mavenProjectProperties.getWarArtifact()); + } + private BuildContext setUpBuildContext(String appRoot, ContainerizingMode containerizingMode) throws InvalidImageReferenceException, IOException, CacheDirectoryCreationException { JavaContainerBuilder javaContainerBuilder = From 64fa7ec577cd88ee34bdaacfd683bbc5b8e5e917 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 Jan 2020 11:32:54 -0500 Subject: [PATCH 0864/2020] Require Gradle 5.0 or higher (#2210) --- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/helloworld/settings.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/java-agent/settings.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 56177 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/spring-boot/settings.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 3 +-- jib-gradle-plugin/CHANGELOG.md | 1 + .../cloud/tools/jib/gradle/JibPlugin.java | 2 +- 12 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 examples/helloworld/settings.gradle create mode 100644 examples/java-agent/settings.gradle create mode 100644 examples/spring-boot/settings.gradle diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.jar b/examples/helloworld/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..94336fcae912db8a11d55634156fa011f4686124 100644 GIT binary patch delta 75 zcmeykjrrp?<_YF3{B|p=CR)d^elGf0#5nPQ^v3PRCxzG;0=(Hd{+{0VNQ{AjK?H~= bKR6{bS>lv7Sp8=AQ}+Z|K+=;nZ+id$gFGJ; delta 75 zcmeykjrrp?<_YF3`^8VXPqdC<{aEy|h;iZp>5bcsPYN+H1bDM^WdGhdRg8gwK?H~= bKR6{bS>lv7Sp8=AQ}+Z|K+=;nZ+id$ib5We diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.properties b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d70..75b8c7c8c6 100644 --- a/examples/helloworld/gradle/wrapper/gradle-wrapper.properties +++ b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/helloworld/settings.gradle b/examples/helloworld/settings.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/java-agent/gradle/wrapper/gradle-wrapper.properties b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d70..75b8c7c8c6 100644 --- a/examples/java-agent/gradle/wrapper/gradle-wrapper.properties +++ b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/java-agent/settings.gradle b/examples/java-agent/settings.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d70..75b8c7c8c6 100644 --- a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties +++ b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..94336fcae912db8a11d55634156fa011f4686124 100644 GIT binary patch delta 75 zcmeykjrrp?<_YF3{B|p=CR)d^elGf0#5nPQ^v3PRCxzG;0=(Hd{+{0VNQ{AjK?H~= bKR6{bS>lv7Sp8=AQ}+Z|K+=;nZ+id$gFGJ; delta 75 zcmeykjrrp?<_YF3`^8VXPqdC<{aEy|h;iZp>5bcsPYN+H1bDM^WdGhdRg8gwK?H~= bKR6{bS>lv7Sp8=AQ}+Z|K+=;nZ+id$ib5We diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d70..75b8c7c8c6 100644 --- a/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties +++ b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/spring-boot/settings.gradle b/examples/spring-boot/settings.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.properties b/examples/vertx/gradle/wrapper/gradle-wrapper.properties index a28d081c5d..75b8c7c8c6 100644 --- a/examples/vertx/gradle/wrapper/gradle-wrapper.properties +++ b/examples/vertx/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Nov 12 11:28:35 CET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 16f86d7b71..7a6a995ed6 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) - Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) - HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- Requires Gradle 5.0 or newer (up from 4.9). ### Fixed - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 22cf639978..4074eaf88a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -42,7 +42,7 @@ public class JibPlugin implements Plugin { - @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("4.9"); + @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("5.0"); public static final String JIB_EXTENSION_NAME = "jib"; public static final String BUILD_IMAGE_TASK_NAME = "jib"; From 90fea01d82c8d4f40147e415ffd6554f2ebaa03e Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 7 Jan 2020 14:45:33 -0500 Subject: [PATCH 0865/2020] Do not fail when "urls" array is present (#2217) --- .../image/json/BuildableManifestTemplate.java | 12 +++++++ .../image/json/V22ManifestTemplateTest.java | 28 +++++++++++++++ .../json/v22manifest_optional_properties.json | 35 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 jib-core/src/test/resources/core/json/v22manifest_optional_properties.json diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java index d28fab30d2..ec6fc1b131 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; @@ -45,6 +46,7 @@ class ContentDescriptorTemplate implements JsonTemplate { @Nullable private String mediaType; @Nullable private DescriptorDigest digest; private long size; + @Nullable private List urls; @Nullable private Map annotations; ContentDescriptorTemplate(String mediaType, long size, DescriptorDigest digest) { @@ -75,6 +77,16 @@ void setDigest(DescriptorDigest digest) { this.digest = digest; } + @VisibleForTesting + @Nullable + public List getUrls() { + return urls; + } + + void setUrls(List urls) { + this.urls = ImmutableList.copyOf(urls); + } + @VisibleForTesting @Nullable public Map getAnnotations() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java index 6666009518..91955bc901 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java @@ -17,7 +17,9 @@ package com.google.cloud.tools.jib.image.json; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate.ContentDescriptorTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import java.io.IOException; import java.net.URISyntaxException; @@ -26,6 +28,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestException; +import java.util.Arrays; +import java.util.List; import org.junit.Assert; import org.junit.Test; @@ -78,4 +82,28 @@ public void testFromJson() throws IOException, URISyntaxException, DigestExcepti Assert.assertEquals(1000_000, manifestJson.getLayers().get(0).getSize()); } + + @Test + public void testFromJson_optionalProperties() throws IOException, URISyntaxException { + Path jsonFile = + Paths.get(Resources.getResource("core/json/v22manifest_optional_properties.json").toURI()); + + V22ManifestTemplate manifestJson = + JsonTemplateMapper.readJsonFromFile(jsonFile, V22ManifestTemplate.class); + + List layers = manifestJson.getLayers(); + Assert.assertEquals(4, layers.size()); + Assert.assertNull(layers.get(0).getUrls()); + Assert.assertNull(layers.get(0).getAnnotations()); + + Assert.assertEquals(Arrays.asList("url-foo", "url-bar"), layers.get(1).getUrls()); + Assert.assertNull(layers.get(1).getAnnotations()); + + Assert.assertNull(layers.get(2).getUrls()); + Assert.assertEquals(ImmutableMap.of("key-foo", "value-foo"), layers.get(2).getAnnotations()); + + Assert.assertEquals(Arrays.asList("cool-url"), layers.get(3).getUrls()); + Assert.assertEquals( + ImmutableMap.of("key1", "value1", "key2", "value2"), layers.get(3).getAnnotations()); + } } diff --git a/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json b/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json new file mode 100644 index 0000000000..19b2a0e914 --- /dev/null +++ b/jib-core/src/test/resources/core/json/v22manifest_optional_properties.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 2, + "mediaType": "type", + "config":{ + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 10 + }, + "layers":[ + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 1 + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 2, + "urls": ["url-foo", "url-bar"] + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 3, + "annotations": {"key-foo":"value-foo"} + }, + { + "mediaType": "type", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 4, + "urls": ["cool-url"], + "annotations": {"key1":"value1", "key2":"value2"} + } + ] +} \ No newline at end of file From 87098378f93d246cfa2ffa29c3687e0c46a28981 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 7 Jan 2020 16:42:32 -0500 Subject: [PATCH 0866/2020] Bump min gradle version to 5.1 and fix deprecation warnings (#2219) --- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/helloworld/gradlew | 2 +- examples/helloworld/gradlew.bat | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/java-agent/gradlew | 2 +- examples/java-agent/gradlew.bat | 2 +- .../ktor/gradle/wrapper/gradle-wrapper.jar | Bin 55741 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes examples/micronaut/gradlew | 2 +- examples/micronaut/gradlew.bat | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/multi-module/gradlew | 2 +- examples/multi-module/gradlew.bat | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/spring-boot/gradlew | 2 +- examples/spring-boot/gradlew.bat | 2 +- .../vertx/gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/vertx/gradlew | 2 +- examples/vertx/gradlew.bat | 2 +- jib-gradle-plugin/CHANGELOG.md | 2 +- .../jib/gradle/GradleProjectProperties.java | 2 +- .../cloud/tools/jib/gradle/JibPlugin.java | 4 ++-- .../cloud/tools/jib/gradle/JibPluginTest.java | 4 ++-- 29 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.jar b/examples/helloworld/gradle/wrapper/gradle-wrapper.jar index 94336fcae912db8a11d55634156fa011f4686124..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*5c$i!g#T3<@q%*b#-shs8K>Ad zr`Y#Sw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/helloworld/gradle/wrapper/gradle-wrapper.properties b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/helloworld/gradle/wrapper/gradle-wrapper.properties +++ b/examples/helloworld/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/helloworld/gradlew b/examples/helloworld/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/helloworld/gradlew +++ b/examples/helloworld/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/helloworld/gradlew.bat b/examples/helloworld/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/helloworld/gradlew.bat +++ b/examples/helloworld/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/examples/java-agent/gradle/wrapper/gradle-wrapper.jar b/examples/java-agent/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*dN9W%g#T3<@q%*b#-shs8K>Ad zr`Wenw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/java-agent/gradle/wrapper/gradle-wrapper.properties b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/java-agent/gradle/wrapper/gradle-wrapper.properties +++ b/examples/java-agent/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/java-agent/gradlew b/examples/java-agent/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/java-agent/gradlew +++ b/examples/java-agent/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/java-agent/gradlew.bat b/examples/java-agent/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/java-agent/gradlew.bat +++ b/examples/java-agent/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.jar b/examples/ktor/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 19106 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 Q;k^UD+~FQ?)`rgh1-4RH;Q#;t diff --git a/examples/ktor/gradle/wrapper/gradle-wrapper.properties b/examples/ktor/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/ktor/gradle/wrapper/gradle-wrapper.properties +++ b/examples/ktor/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/micronaut/gradle/wrapper/gradle-wrapper.jar b/examples/micronaut/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*dN9W%g#T3<@q%*b#-shs8K>Ad zr`Wenw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/micronaut/gradlew b/examples/micronaut/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/micronaut/gradlew +++ b/examples/micronaut/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/micronaut/gradlew.bat b/examples/micronaut/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/micronaut/gradlew.bat +++ b/examples/micronaut/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.jar b/examples/multi-module/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*dN9W%g#T3<@q%*b#-shs8K>Ad zr`Wenw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/multi-module/gradle/wrapper/gradle-wrapper.properties +++ b/examples/multi-module/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/multi-module/gradlew b/examples/multi-module/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/multi-module/gradlew +++ b/examples/multi-module/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/multi-module/gradlew.bat b/examples/multi-module/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/multi-module/gradlew.bat +++ b/examples/multi-module/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar b/examples/spring-boot/gradle/wrapper/gradle-wrapper.jar index 94336fcae912db8a11d55634156fa011f4686124..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*5c$i!g#T3<@q%*b#-shs8K>Ad zr`Y#Sw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties +++ b/examples/spring-boot/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/spring-boot/gradlew b/examples/spring-boot/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/spring-boot/gradlew +++ b/examples/spring-boot/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/spring-boot/gradlew.bat b/examples/spring-boot/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/spring-boot/gradlew.bat +++ b/examples/spring-boot/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.jar b/examples/vertx/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*dN9W%g#T3<@q%*b#-shs8K>Ad zr`Wenw@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

ohmj(wKVBMg2V`Or6Dh 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 diff --git a/examples/vertx/gradle/wrapper/gradle-wrapper.properties b/examples/vertx/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8c6..6b3851a8ad 100644 --- a/examples/vertx/gradle/wrapper/gradle-wrapper.properties +++ b/examples/vertx/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/vertx/gradlew b/examples/vertx/gradlew index cccdd3d517..af6708ff22 100755 --- a/examples/vertx/gradlew +++ b/examples/vertx/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/examples/vertx/gradlew.bat b/examples/vertx/gradlew.bat index f9553162f1..6d57edc706 100644 --- a/examples/vertx/gradlew.bat +++ b/examples/vertx/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7a6a995ed6..9a5c5cd558 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.extraDirectory` configuration in favor of `jib.extraDirectories`. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) - Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) - HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) -- Requires Gradle 5.0 or newer (up from 4.9). +- Requires Gradle 5.1 or newer (up from 4.9). ### Fixed - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java index c96f528424..5649452c00 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java @@ -234,7 +234,7 @@ public JibContainerBuilder createJibContainerBuilder( case PACKAGED: // Add a JAR Jar jarTask = (Jar) project.getTasks().findByName("jar"); - Path jarPath = jarTask.getDestinationDir().toPath().resolve(jarTask.getArchiveName()); + Path jarPath = jarTask.getArchiveFile().get().getAsFile().toPath(); log(LogEvent.debug("Using JAR: " + jarPath)); javaContainerBuilder.addToClasspath(jarPath); break; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 4074eaf88a..e602dd09da 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -42,7 +42,7 @@ public class JibPlugin implements Plugin { - @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("5.0"); + @VisibleForTesting static final GradleVersion GRADLE_MIN_VERSION = GradleVersion.version("5.1"); public static final String JIB_EXTENSION_NAME = "jib"; public static final String BUILD_IMAGE_TASK_NAME = "jib"; @@ -189,7 +189,7 @@ public void apply(Project project) { if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { Jar jar = (Jar) jarTask.get(); jar.setEnabled(true); - jar.setClassifier("original"); + jar.getArchiveClassifier().set("original"); } } else { // Have all tasks depend on the 'classes' task. diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index f26ea41f02..a0f4ceff9e 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -272,7 +272,7 @@ public void testSpringBootJarProject_nonPackagedMode() { Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); Assert.assertFalse(jarTask.getEnabled()); - Assert.assertEquals("", jarTask.getClassifier()); + Assert.assertEquals("", jarTask.getArchiveClassifier().get()); } @Test @@ -284,7 +284,7 @@ public void testSpringBootJarProject_packagedMode() { Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); Assert.assertTrue(jarTask.getEnabled()); - Assert.assertEquals("original", jarTask.getClassifier()); + Assert.assertEquals("original", jarTask.getArchiveClassifier().get()); } @Test From cfbec813cb356e0b5f414ab50d0bab390be51e8e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 9 Jan 2020 15:04:20 -0500 Subject: [PATCH 0867/2020] Change base image cache location on mac/windows, add methods for getting XDG config location (#2218) --- .../cloud/tools/jib/api/Containerizer.java | 5 +- .../jib/builder/steps/BuildImageStep.java | 2 +- .../tools/jib/filesystem/UserCacheHome.java | 105 ----------- .../tools/jib/filesystem/XdgDirectories.java | 159 +++++++++++++++++ .../jib/filesystem/UserCacheHomeTest.java | 91 ---------- .../jib/filesystem/XdgDirectoriesTest.java | 166 ++++++++++++++++++ 6 files changed, 328 insertions(+), 200 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java delete mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index cbd05e7318..833295ed5d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -22,7 +22,7 @@ import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.filesystem.UserCacheHome; +import com.google.cloud.tools.jib.filesystem.XdgDirectories; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Files; @@ -44,8 +44,7 @@ public class Containerizer { * The default directory for caching the base image layers, in {@code [user cache * home]/google-cloud-tools-java/jib}. */ - public static final Path DEFAULT_BASE_CACHE_DIRECTORY = - UserCacheHome.getCacheHome().resolve("google-cloud-tools-java").resolve("jib"); + public static final Path DEFAULT_BASE_CACHE_DIRECTORY = XdgDirectories.getCacheHome(); private static final String DEFAULT_TOOL_NAME = "jib-core"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java index b16656917d..f40fb3a634 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java @@ -66,7 +66,7 @@ public Image call() throws LayerPropertyNotFoundException { ContainerConfiguration containerConfiguration = buildContext.getContainerConfiguration(); // Base image layers - baseImageLayers.stream().forEach(imageBuilder::addLayer); + baseImageLayers.forEach(imageBuilder::addLayer); // Passthrough config and count non-empty history entries int nonEmptyLayerCount = 0; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java deleted file mode 100644 index 4547269154..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/UserCacheHome.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.filesystem; - -import com.google.common.annotations.VisibleForTesting; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.logging.Logger; - -/** - * Obtains an OS-specific user cache directory based on the XDG Base Directory Specification. - * - *

Specifically, from the specification: - * - *

    - *
  • This directory is defined by the environment variable {@code $XDG_CACHE_HOME}. - *
  • If {@code $XDG_CACHE_HOME} is either not set or empty, a default equal to {@code - * $HOME/.cache} should be used. - *
- * - * @see https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - */ -public class UserCacheHome { - - private static final Logger logger = Logger.getLogger(UserCacheHome.class.getName()); - - public static Path getCacheHome() { - return getCacheHome(System.getProperties(), System.getenv()); - } - - /** - * Returns {@code $XDG_CACHE_HOME}, if available, or resolves the OS-specific user cache home - * based. - * - *

For Linux, this is {@code $HOME/.cache/}. - * - *

For Windows, this is {@code %LOCALAPPDATA%}. - * - *

For macOS, this is {@code $HOME/Library/Application Support/}. - */ - @VisibleForTesting - static Path getCacheHome(Properties properties, Map environment) { - // Use environment variable $XDG_CACHE_HOME if set and not empty. - String xdgCacheHome = environment.get("XDG_CACHE_HOME"); - if (xdgCacheHome != null && !xdgCacheHome.trim().isEmpty()) { - return Paths.get(xdgCacheHome); - } - - String userHome = properties.getProperty("user.home"); - Path xdgPath = Paths.get(userHome, ".cache"); - - String rawOsName = properties.getProperty("os.name"); - String osName = rawOsName.toLowerCase(Locale.ENGLISH); - - if (osName.contains("linux")) { - return xdgPath; - - } else if (osName.contains("windows")) { - // Use %LOCALAPPDATA% for Windows. - String localAppDataEnv = environment.get("LOCALAPPDATA"); - if (localAppDataEnv == null || localAppDataEnv.trim().isEmpty()) { - logger.warning("LOCALAPPDATA environment is invalid or missing"); - return xdgPath; - } - Path localAppData = Paths.get(localAppDataEnv); - if (!Files.exists(localAppData)) { - logger.warning(localAppData + " does not exist"); - return xdgPath; - } - return localAppData; - - } else if (osName.contains("mac") || osName.contains("darwin")) { - // Use '~/Library/Application Support/' for macOS. - Path applicationSupport = Paths.get(userHome, "Library", "Application Support"); - if (!Files.exists(applicationSupport)) { - logger.warning(applicationSupport + " does not exist"); - return xdgPath; - } - return applicationSupport; - } - - throw new IllegalStateException("Unknown OS: " + rawOsName); - } - - private UserCacheHome() {} -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java new file mode 100644 index 0000000000..e5267be7b8 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Obtains OS-specific directories based on the XDG Base Directory Specification. + * + *

Specifically, from the specification: + * + *

    + *
  • These directories are defined by the environment variables {@code $XDG_CACHE_HOME} and + * {@code $XDG_CONFIG_HOME}. + *
  • If {@code $XDG_CACHE_HOME} / {@code $XDG_CONFIG_HOME} is either not set or empty, a + * platform-specific equivalent of {@code $HOME/.cache} / {@code $HOME/.config} should be + * used. + *
+ * + * @see https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + */ +public class XdgDirectories { + + private static final Logger LOGGER = Logger.getLogger(XdgDirectories.class.getName()); + private static final Path JIB_SUBDIRECTORY_LINUX = + Paths.get("google-cloud-tools-java").resolve("jib"); + private static final Path JIB_SUBDIRECTORY_OTHER = Paths.get("Google").resolve("Jib"); + + public static Path getCacheHome() { + return getCacheHome(System.getProperties(), System.getenv()); + } + + public static Path getConfigHome() { + return getConfigHome(System.getProperties(), System.getenv()); + } + + /** + * Returns the default OS-specific cache directory. + * + *

For Linux, this is {@code $HOME/.cache/google-cloud-tools-java/jib/}. + * + *

For Windows, this is {@code %LOCALAPPDATA%\Google\Jib\Cache\}. + * + *

For macOS, this is {@code $HOME/Library/Caches/Google/Jib/}. + */ + @VisibleForTesting + static Path getCacheHome(Properties properties, Map environment) { + return getOsSpecificDirectory( + properties, environment, "XDG_CACHE_HOME", ".cache", "Cache", "Caches"); + } + + /** + * Returns the default OS-specific config directory. + * + *

For Linux, this is {@code $HOME/.config/google-cloud-tools-java/jib/}. + * + *

For Windows, this is {@code %LOCALAPPDATA%\Google\Jib\Config\}. + * + *

For macOS, this is {@code $HOME/Library/Preferences/Google/Jib/}. + */ + @VisibleForTesting + static Path getConfigHome(Properties properties, Map environment) { + return getOsSpecificDirectory( + properties, environment, "XDG_CONFIG_HOME", ".config", "Config", "Preferences"); + } + + /** + * Helper method for resolving directories on different operating systems. + * + * @param xdgEnvVariable the name of the environment variable used to resolve the XDG base + * directory + * @param linuxFolder ".config" or ".cache" + * @param windowsFolder "Config" or "Cache" + * @param macFolder "Preferences" or "Caches" + * @return the full path constructed from the given parameters + */ + private static Path getOsSpecificDirectory( + Properties properties, + Map environment, + String xdgEnvVariable, + String linuxFolder, + String windowsFolder, + String macFolder) { + + Path windowsSubDirectory = JIB_SUBDIRECTORY_OTHER.resolve(windowsFolder); + String rawOsName = properties.getProperty("os.name"); + String osName = rawOsName.toLowerCase(Locale.ENGLISH); + String xdgHome = environment.get(xdgEnvVariable); + String userHome = properties.getProperty("user.home"); + Path xdgPath = Paths.get(userHome, linuxFolder); + + if (osName.contains("linux")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(JIB_SUBDIRECTORY_LINUX); + } + return xdgPath.resolve(JIB_SUBDIRECTORY_LINUX); + + } else if (osName.contains("windows")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(windowsSubDirectory); + } + + // Use %LOCALAPPDATA% for Windows. + String localAppDataEnv = environment.get("LOCALAPPDATA"); + if (localAppDataEnv == null || localAppDataEnv.trim().isEmpty()) { + LOGGER.warning("LOCALAPPDATA environment is invalid or missing"); + return xdgPath.resolve(windowsSubDirectory); + } + Path localAppData = Paths.get(localAppDataEnv); + if (!Files.exists(localAppData)) { + LOGGER.warning(localAppData + " does not exist"); + return xdgPath.resolve(windowsSubDirectory); + } + return localAppData.resolve(windowsSubDirectory); + + } else if (osName.contains("mac") || osName.contains("darwin")) { + // Use XDG environment variable if set and not empty. + if (xdgHome != null && !xdgHome.trim().isEmpty()) { + return Paths.get(xdgHome).resolve(JIB_SUBDIRECTORY_OTHER); + } + + // Use '~/Library/...' for macOS. + Path macDirectory = Paths.get(userHome, "Library", macFolder); + if (!Files.exists(macDirectory)) { + LOGGER.warning(macDirectory + " does not exist"); + return xdgPath.resolve(JIB_SUBDIRECTORY_OTHER); + } + return macDirectory.resolve(JIB_SUBDIRECTORY_OTHER); + } + + throw new IllegalStateException("Unknown OS: " + rawOsName); + } + + private XdgDirectories() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java deleted file mode 100644 index 4d5152fd7e..0000000000 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/UserCacheHomeTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.filesystem; - -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; -import java.util.Properties; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; - -/** Tests for {@link UserCacheHome}. */ -public class UserCacheHomeTest { - - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private String fakeCacheHome; - - @Before - public void setUp() throws IOException { - fakeCacheHome = temporaryFolder.newFolder().getPath(); - } - - @Test - public void testGetCacheHome_hasXdgCacheHome() { - Map fakeEnvironment = ImmutableMap.of("XDG_CACHE_HOME", fakeCacheHome); - - Assert.assertEquals( - Paths.get(fakeCacheHome), - UserCacheHome.getCacheHome(Mockito.mock(Properties.class), fakeEnvironment)); - } - - @Test - public void testGetCacheHome_linux() { - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", fakeCacheHome); - fakeProperties.setProperty("os.name", "os is LiNuX"); - - Assert.assertEquals( - Paths.get(fakeCacheHome, ".cache"), - UserCacheHome.getCacheHome(fakeProperties, Collections.emptyMap())); - } - - @Test - public void testGetCacheHome_windows() { - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", "nonexistent"); - fakeProperties.setProperty("os.name", "os is WiNdOwS"); - - Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeCacheHome); - - Assert.assertEquals( - Paths.get(fakeCacheHome), UserCacheHome.getCacheHome(fakeProperties, fakeEnvironment)); - } - - @Test - public void testGetCacheHome_mac() throws IOException { - Path libraryApplicationSupport = Paths.get(fakeCacheHome, "Library", "Application Support"); - Files.createDirectories(libraryApplicationSupport); - - Properties fakeProperties = new Properties(); - fakeProperties.setProperty("user.home", fakeCacheHome); - fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); - - Assert.assertEquals( - libraryApplicationSupport, - UserCacheHome.getCacheHome(fakeProperties, Collections.emptyMap())); - } -} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java new file mode 100644 index 0000000000..a3e23a5b95 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.filesystem; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link XdgDirectories}. */ +public class XdgDirectoriesTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private String fakeCacheHome; + private String fakeConfigHome; + + @Before + public void setUp() throws IOException { + fakeCacheHome = temporaryFolder.newFolder().getPath(); + fakeConfigHome = temporaryFolder.newFolder().getPath(); + } + + @Test + public void testGetCacheHome_hasXdgCacheHome() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + Map fakeEnvironment = ImmutableMap.of("XDG_CACHE_HOME", fakeCacheHome); + + fakeProperties.setProperty("os.name", "linux"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "windows"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib").resolve("Cache"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "mac"); + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetCacheHome_linux() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + fakeProperties.setProperty("os.name", "os is LiNuX"); + + Assert.assertEquals( + Paths.get(fakeCacheHome, ".cache").resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getCacheHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetCacheHome_windows() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", "nonexistent"); + fakeProperties.setProperty("os.name", "os is WiNdOwS"); + + Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeCacheHome); + + Assert.assertEquals( + Paths.get(fakeCacheHome).resolve("Google").resolve("Jib").resolve("Cache"), + XdgDirectories.getCacheHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetCacheHome_mac() throws IOException { + Path libraryApplicationSupport = Paths.get(fakeCacheHome, "Library", "Caches"); + Files.createDirectories(libraryApplicationSupport); + + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeCacheHome); + fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); + + Assert.assertEquals( + libraryApplicationSupport.resolve("Google").resolve("Jib"), + XdgDirectories.getCacheHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetConfigHome_hasXdgConfigHome() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + Map fakeEnvironment = ImmutableMap.of("XDG_CONFIG_HOME", fakeConfigHome); + + fakeProperties.setProperty("os.name", "linux"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "windows"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib").resolve("Config"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + + fakeProperties.setProperty("os.name", "mac"); + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetConfigHome_linux() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + fakeProperties.setProperty("os.name", "os is LiNuX"); + + Assert.assertEquals( + Paths.get(fakeConfigHome, ".config").resolve("google-cloud-tools-java").resolve("jib"), + XdgDirectories.getConfigHome(fakeProperties, Collections.emptyMap())); + } + + @Test + public void testGetConfigHome_windows() { + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", "nonexistent"); + fakeProperties.setProperty("os.name", "os is WiNdOwS"); + + Map fakeEnvironment = ImmutableMap.of("LOCALAPPDATA", fakeConfigHome); + + Assert.assertEquals( + Paths.get(fakeConfigHome).resolve("Google").resolve("Jib").resolve("Config"), + XdgDirectories.getConfigHome(fakeProperties, fakeEnvironment)); + } + + @Test + public void testGetConfigHome_mac() throws IOException { + Path libraryApplicationSupport = Paths.get(fakeConfigHome, "Library", "Preferences"); + Files.createDirectories(libraryApplicationSupport); + + Properties fakeProperties = new Properties(); + fakeProperties.setProperty("user.home", fakeConfigHome); + fakeProperties.setProperty("os.name", "os is mAc or DaRwIn"); + + Assert.assertEquals( + libraryApplicationSupport.resolve("Google").resolve("Jib"), + XdgDirectories.getConfigHome(fakeProperties, Collections.emptyMap())); + } +} From c73335093d629367775e5e1f191ae91b5afe5c4e Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 9 Jan 2020 16:54:04 -0500 Subject: [PATCH 0868/2020] Update changelog for cache location change (#2225) --- jib-core/CHANGELOG.md | 5 +++++ jib-gradle-plugin/CHANGELOG.md | 6 ++++++ jib-maven-plugin/CHANGELOG.md | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 16fb5fd9dd..365658ae19 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file. - Each local base image layer is pushed immediately after being compressed, rather than waiting for all layers to finish compressing before starting to push. ([#1913](https://github.com/GoogleContainerTools/jib/issues/1913)) - HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- `Containerizer.DEFAULT_BASE_CACHE_DIRECTORY` has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` ### Fixed diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 9a5c5cd558..7a7a007f51 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -12,6 +12,12 @@ All notable changes to this project will be documented in this file. - Removed deprecated `jib.container.useCurrentTimestamp` configuration in favor of `jib.container.creationTime` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) - HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) - Requires Gradle 5.1 or newer (up from 4.9). +- The default base image cache location has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` + - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location ### Fixed - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 2a0e6df442..7f187b739b 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,12 @@ All notable changes to this project will be documented in this file. - Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) - Removed deprecated `` configuration in favor of `` with `USE_CURRENT_TIMESTAMP`. ([#1897](https://github.com/GoogleContainerTools/jib/issues/1897)) - HTTP redirection URLs are no longer sanitized in order to work around an issue with certain registries that do not conform to HTTP standards. This resolves an issue with using Red Hat OpenShift and Quay registries. ([#2106](https://github.com/GoogleContainerTools/jib/issues/2106), [#1986](https://github.com/GoogleContainerTools/jib/issues/1986#issuecomment-547610104)) +- The default base image cache location has been changed on MacOS and Windows. ([#2216](https://github.com/GoogleContainerTools/jib/issues/2216)) + - MacOS (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME/google-cloud-tools-java/jib/` to `$XDG_CACHE_HOME/Google/Jib/` + - MacOS (`$XDG_CACHE_HOME` not defined): from `$HOME/Library/Application Support/google-cloud-tools-java/jib/` to `$HOME/Library/Caches/Google/Jib/` + - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` + - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` + - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location ### Fixed From 9bec3e6be0a00fa93d0fdb80479bd7bbd05453c3 Mon Sep 17 00:00:00 2001 From: Appu Date: Thu, 9 Jan 2020 17:53:34 -0500 Subject: [PATCH 0869/2020] Update faq for class not found / method not found (#2213) * Update faq for class not found / method not found --- docs/faq.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c0702a76c4..17811703f6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -36,7 +36,8 @@ If a question you have is not answered below, please [submit an issue](/../../is [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ [How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ -[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube) +[I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ +[I am seeing `Method Not Found` or `Class Not Found` errors when building.](#i-am-seeing-method-not-found-or-class-not-found-errors-when-building) --- @@ -537,6 +538,25 @@ kubectl patch serviceaccount default \ See more at [Using Google Container Registry (GCR) with Minikube](https://ryaneschinger.com/blog/using-google-container-registry-gcr-with-minikube/). +### I am seeing `Method Not Found` or `Class Not Found` errors when building. + +Sometimes when upgrading your gradle build plugin versions, you may experience errors due to mismatching versions of dependencies pulled in (for example: [issues/2183](https://github.com/GoogleContainerTools/jib/issues/2183)). This can be due to the buildscript classpath loading behavior described [on gradle forums](https://discuss.gradle.org/t/version-is-root-build-gradle-buildscript-is-overriding-subproject-buildscript-dependency-versions/20746/3). + +This commonly appears in multi module gradle projects. A solution to this problem is to define all of your plugins in the base project and apply them selectively in your subprojects as needed. This should help alleviate the problem of the buildscript classpath using older versions of a library. + +`build.gradle` (root) +```groovy +plugins { + id 'com.google.cloud.tools.jib' version 'x.y.z' apply false +} +``` + +`build.gradle` (sub-project) +```groovy +plugins { + id 'com.google.cloud.tools.jib' +} +``` ### How can I examine network traffic? From 72a26cbeed3287172d4c07d61695210dbf278ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20B=C3=A4nziger?= Date: Wed, 15 Jan 2020 18:19:24 +0100 Subject: [PATCH 0870/2020] Ignore registry port when inferring auth from maven settings (#2212) * Ignore port as fallback when inferring registry auth from maven settings Fixes #2135 * Update comment Co-authored-by: Chanseok Oh --- .../maven/MavenSettingsServerCredentials.java | 20 ++++++++++++- .../MavenSettingsServerCredentialsTest.java | 28 +++++++++++++++++++ .../resources/maven/settings/settings.xml | 5 ++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 6ada727ac4..abdf8bed22 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -19,7 +19,9 @@ import com.google.cloud.tools.jib.plugins.common.AuthProperty; import com.google.cloud.tools.jib.plugins.common.InferredAuthException; import com.google.cloud.tools.jib.plugins.common.InferredAuthProvider; +import com.google.common.annotations.VisibleForTesting; import java.util.Optional; +import javax.annotation.Nullable; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.building.SettingsProblem; @@ -58,7 +60,7 @@ class MavenSettingsServerCredentials implements InferredAuthProvider { @Override public Optional inferAuth(String registry) throws InferredAuthException { - Server server = settings.getServer(registry); + Server server = getServerFromMavenSettings(registry); if (server == null) { return Optional.empty(); } @@ -108,4 +110,20 @@ public String getPasswordDescriptor() { } }); } + + @Nullable + @VisibleForTesting + Server getServerFromMavenSettings(String registry) { + Server server = settings.getServer(registry); + if (server != null) { + return server; + } + + // try without port + int index = registry.lastIndexOf(':'); + if (index != -1) { + return settings.getServer(registry.substring(0, index)); + } + return null; + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 45e6c04682..f8210a6a54 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -86,6 +86,34 @@ public void testInferredAuth_successNoPasswordDoesNotBlowUp() throws InferredAut Assert.assertEquals("password2", auth.get().getPassword()); } + @Test + public void testInferredAuth_registryWithHostAndPort() throws InferredAuthException { + Optional auth = + mavenSettingsServerCredentialsNoMasterPassword.inferAuth("docker.example.com:8080"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("registryUser", auth.get().getUsername()); + Assert.assertEquals("registryPassword", auth.get().getPassword()); + } + + @Test + public void testInferredAuth_registryWithHostWithoutPort() throws InferredAuthException { + Optional auth = + mavenSettingsServerCredentialsNoMasterPassword.inferAuth("docker.example.com"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("registryUser", auth.get().getUsername()); + Assert.assertEquals("registryPassword", auth.get().getPassword()); + } + + @Test + public void testInferredAuth_registrySettingsWithPort() throws InferredAuthException { + // Attempt to resolve WITHOUT the port. Should work as well. + Optional auth = + mavenSettingsServerCredentialsNoMasterPassword.inferAuth("docker.example.com:5432"); + Assert.assertTrue(auth.isPresent()); + Assert.assertEquals("registryUser", auth.get().getUsername()); + Assert.assertEquals("registryPassword", auth.get().getPassword()); + } + @Test public void testInferredAuth_notFound() throws InferredAuthException { Assert.assertFalse(mavenSettingsServerCredentials.inferAuth("serverUnknown").isPresent()); diff --git a/jib-maven-plugin/src/test/resources/maven/settings/settings.xml b/jib-maven-plugin/src/test/resources/maven/settings/settings.xml index f5fc0a4dcb..9a5d69ac01 100644 --- a/jib-maven-plugin/src/test/resources/maven/settings/settings.xml +++ b/jib-maven-plugin/src/test/resources/maven/settings/settings.xml @@ -15,5 +15,10 @@ badUser {i-made-this-up=} + + docker.example.com + registryUser + registryPassword + From d478cf7ae056aec25f6b03fd6f8ae43ca881d9dc Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 15 Jan 2020 13:47:31 -0500 Subject: [PATCH 0871/2020] CHANGELOG for optionally ignoring registry port in in settings.xml (#2234) --- jib-maven-plugin/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 7f187b739b..4395744364 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Windows (`$XDG_CACHE_HOME` defined): from `$XDG_CACHE_HOME\google-cloud-tools-java\jib\` to `$XDG_CACHE_HOME\Google\Jib\Cache\` - Windows (`$XDG_CACHE_HOME` not defined): from `%LOCALAPPDATA%\google-cloud-tools-java\jib\` to `%LOCALAPPDATA%\Google\Jib\Cache\` - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location +- When giving registry credentials in `settings.xml`, specifying port in `` is no longer required. ([#2135](https://github.com/GoogleContainerTools/jib/issues/2135)) ### Fixed From eeddc9e933565128fe019a120cfed1d941839b23 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 17 Jan 2020 11:31:10 -0500 Subject: [PATCH 0872/2020] Update checkstyle (#2236) --- config/checkstyle/copyright-java.header | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/checkstyle/copyright-java.header b/config/checkstyle/copyright-java.header index 956f87d465..1937e809e9 100644 --- a/config/checkstyle/copyright-java.header +++ b/config/checkstyle/copyright-java.header @@ -1,5 +1,5 @@ ^/\*$ -^ \* Copyright 20(17|18|19) Google LLC\.$ +^ \* Copyright 20(17|18|19|20) Google LLC\.$ ^ \*$ ^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$ ^ \* use this file except in compliance with the License\. You may obtain a copy of$ From 9b194da35120ec94014f8ce65cc81ab96c38b266 Mon Sep 17 00:00:00 2001 From: Marc Bruggmann Date: Tue, 21 Jan 2020 16:58:08 +0100 Subject: [PATCH 0873/2020] Write out the image metadata to a file after build (#2227) --- .../cloud/tools/jib/api/ImageReference.java | 20 ++++ .../cloud/tools/jib/api/JibContainer.java | 48 ++++++++- .../tools/jib/api/JibContainerBuilder.java | 4 +- .../tools/jib/api/ImageReferenceTest.java | 36 +++++++ .../cloud/tools/jib/api/JibContainerTest.java | 62 ++++++++++-- .../jib/gradle/GradleRawConfiguration.java | 5 + .../jib/gradle/OutputPathsParameters.java | 16 +++ .../gradle/GradleRawConfigurationTest.java | 2 + jib-maven-plugin/README.md | 1 + .../jib/maven/JibPluginConfiguration.java | 10 ++ .../jib/maven/MavenRawConfiguration.java | 5 + .../jib/maven/JibPluginConfigurationTest.java | 6 ++ .../jib/maven/MavenRawConfigurationTest.java | 3 + .../plugins/common/ImageMetadataOutput.java | 97 +++++++++++++++++++ .../jib/plugins/common/JibBuildRunner.java | 18 ++++ .../common/PluginConfigurationProcessor.java | 9 +- .../jib/plugins/common/PropertyNames.java | 1 + .../jib/plugins/common/RawConfiguration.java | 2 + .../common/ImageMetadataOutputTest.java | 55 +++++++++++ .../plugins/common/JibBuildRunnerTest.java | 35 +++++++ 20 files changed, 417 insertions(+), 18 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutput.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutputTest.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java index 46c23629e6..2f2c010085 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java @@ -19,6 +19,7 @@ import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -347,4 +348,23 @@ public String toString() { public String toStringWithTag() { return toString() + (usesDefaultTag() ? ":" + DEFAULT_TAG : ""); } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ImageReference)) { + return false; + } + ImageReference otherImageReference = (ImageReference) other; + return registry.equals(otherImageReference.registry) + && repository.equals(otherImageReference.repository) + && tag.equals(otherImageReference.tag); + } + + @Override + public int hashCode() { + return Objects.hash(registry, repository, tag); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java index 5818663a7f..bf41b2eb7e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java @@ -16,17 +16,47 @@ package com.google.cloud.tools.jib.api; +import com.google.cloud.tools.jib.builder.steps.BuildResult; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.common.annotations.VisibleForTesting; import java.util.Objects; +import java.util.Set; /** The container built by Jib. */ public class JibContainer { + private final ImageReference targetImage; private final DescriptorDigest imageDigest; private final DescriptorDigest imageId; + private final Set tags; - JibContainer(DescriptorDigest imageDigest, DescriptorDigest imageId) { + @VisibleForTesting + JibContainer( + ImageReference targetImage, + DescriptorDigest imageDigest, + DescriptorDigest imageId, + Set tags) { + this.targetImage = targetImage; this.imageDigest = imageDigest; this.imageId = imageId; + this.tags = tags; + } + + static JibContainer from(BuildContext buildContext, BuildResult buildResult) { + ImageReference targetImage = buildContext.getTargetImageConfiguration().getImage(); + DescriptorDigest imageDigest = buildResult.getImageDigest(); + DescriptorDigest imageId = buildResult.getImageId(); + Set tags = buildContext.getAllTargetImageTags(); + return new JibContainer(targetImage, imageDigest, imageId, tags); + } + + /** + * Get the target image that was built. + * + * @return the target image reference. + */ + public ImageReference getTargetImage() { + return targetImage; } /** @@ -48,9 +78,18 @@ public DescriptorDigest getImageId() { return imageId; } + /** + * Get the tags applied to the container. + * + * @return the set of all tags + */ + public Set getTags() { + return tags; + } + @Override public int hashCode() { - return Objects.hash(imageDigest, imageId); + return Objects.hash(targetImage, imageDigest, imageId, tags); } @Override @@ -62,6 +101,9 @@ public boolean equals(Object other) { return false; } JibContainer otherContainer = (JibContainer) other; - return imageDigest.equals(otherContainer.imageDigest) && imageId.equals(otherContainer.imageId); + return targetImage.equals(otherContainer.targetImage) + && imageDigest.equals(otherContainer.imageDigest) + && imageId.equals(otherContainer.imageId) + && tags.equals(otherContainer.tags); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index af058fe109..ad0182cdf3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -476,8 +476,8 @@ public JibContainer containerize(Containerizer containerizer) buildContext.getEventHandlers(), containerizer.getDescription())) { logSources(buildContext.getEventHandlers()); - BuildResult result = containerizer.run(buildContext); - return new JibContainer(result.getImageDigest(), result.getImageId()); + BuildResult buildResult = containerizer.run(buildContext); + return JibContainer.from(buildContext, buildResult); } catch (ExecutionException ex) { // If an ExecutionException occurs, re-throw the cause to be more easily handled by the user diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java index ef90c027a1..34247c8b39 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java @@ -202,6 +202,42 @@ public void testGetRegistry() { Assert.assertEquals("gcr.io", ImageReference.of("gcr.io", "someimage", null).getRegistry()); } + @Test + public void testEquality() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("gcr.io/project/image:tag"); + + Assert.assertEquals(image1, image2); + Assert.assertEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentRegistry() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("registry-1.docker.io/project/image:tag"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentRepository() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag"); + ImageReference image2 = ImageReference.parse("gcr.io/project2/image:tag"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + + @Test + public void testEquality_differentTag() throws InvalidImageReferenceException { + ImageReference image1 = ImageReference.parse("gcr.io/project/image:tag1"); + ImageReference image2 = ImageReference.parse("gcr.io/project/image:tag2"); + + Assert.assertNotEquals(image1, image2); + Assert.assertNotEquals(image1.hashCode(), image2.hashCode()); + } + private void verifyParse(String registry, String repository, String tagSeparator, String tag) throws InvalidImageReferenceException { // Gets the expected parsed components. diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java index 52393ab304..607a3e9e95 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java @@ -16,7 +16,9 @@ package com.google.cloud.tools.jib.api; +import com.google.common.collect.ImmutableSet; import java.security.DigestException; +import java.util.Set; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -28,39 +30,79 @@ public class JibContainerTest { @Rule public TemporaryFolder temporaryDirectory = new TemporaryFolder(); + private ImageReference targetImage1; + private ImageReference targetImage2; private DescriptorDigest digest1; private DescriptorDigest digest2; - private DescriptorDigest digest3; + private Set tags1; + private Set tags2; @Before - public void setUp() throws DigestException { + public void setUp() throws DigestException, InvalidImageReferenceException { + targetImage1 = ImageReference.parse("gcr.io/project/image:tag"); + targetImage2 = ImageReference.parse("gcr.io/project/image:tag2"); digest1 = DescriptorDigest.fromDigest( "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"); digest2 = DescriptorDigest.fromDigest( "sha256:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"); - digest3 = - DescriptorDigest.fromDigest( - "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"); + tags1 = ImmutableSet.of("latest", "custom-tag"); + tags2 = ImmutableSet.of("latest"); } @Test public void testCreation() { - JibContainer container = new JibContainer(digest1, digest2); + JibContainer container = new JibContainer(targetImage1, digest1, digest2, tags1); + Assert.assertEquals(targetImage1, container.getTargetImage()); Assert.assertEquals(digest1, container.getDigest()); Assert.assertEquals(digest2, container.getImageId()); + Assert.assertEquals(tags1, container.getTags()); } @Test public void testEquality() { - JibContainer container1 = new JibContainer(digest1, digest2); - JibContainer container2 = new JibContainer(digest1, digest2); - JibContainer container3 = new JibContainer(digest2, digest3); + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest2, tags1); Assert.assertEquals(container1, container2); Assert.assertEquals(container1.hashCode(), container2.hashCode()); - Assert.assertNotEquals(container1, container3); + } + + @Test + public void testEquality_differentTargetImage() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1); + JibContainer container2 = new JibContainer(targetImage2, digest1, digest2, tags1); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentImageDigest() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest2, tags1); + JibContainer container2 = new JibContainer(targetImage1, digest2, digest2, tags1); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentImageId() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest1, tags1); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest2, tags1); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); + } + + @Test + public void testEquality_differentTags() { + JibContainer container1 = new JibContainer(targetImage1, digest1, digest1, tags1); + JibContainer container2 = new JibContainer(targetImage1, digest1, digest1, tags2); + + Assert.assertNotEquals(container1, container2); + Assert.assertNotEquals(container1.hashCode(), container2.hashCode()); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java index ad1aa28fdf..6dc3a59b7f 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java @@ -195,4 +195,9 @@ public Path getDigestOutputPath() { public Path getImageIdOutputPath() { return jibExtension.getOutputPaths().getImageIdPath(); } + + @Override + public Path getImageJsonOutputPath() { + return jibExtension.getOutputPaths().getImageJsonPath(); + } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java index 34f862ca5d..46b4b6e608 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java @@ -32,12 +32,14 @@ public class OutputPathsParameters { private Path digest; private Path tar; private Path imageId; + private Path imageJson; @Inject public OutputPathsParameters(Project project) { this.project = project; digest = project.getBuildDir().toPath().resolve("jib-image.digest"); imageId = project.getBuildDir().toPath().resolve("jib-image.id"); + imageJson = project.getBuildDir().toPath().resolve("jib-image.json"); tar = project.getBuildDir().toPath().resolve("jib-image.tar"); } @@ -69,6 +71,20 @@ public void setImageId(String id) { this.imageId = Paths.get(id); } + @Input + public String getImageJson() { + return getRelativeToProjectRoot(imageJson, PropertyNames.OUTPUT_PATHS_IMAGE_JSON).toString(); + } + + @Internal + Path getImageJsonPath() { + return getRelativeToProjectRoot(imageJson, PropertyNames.OUTPUT_PATHS_IMAGE_JSON); + } + + public void setImageJson(String imageJson) { + this.imageJson = Paths.get(imageJson); + } + @Input public String getTar() { return getRelativeToProjectRoot(tar, PropertyNames.OUTPUT_PATHS_TAR).toString(); diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java index eeb01f18f4..001b6f848d 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java @@ -80,6 +80,7 @@ public void testGetters() { Mockito.when(outputPathsParameters.getDigestPath()).thenReturn(Paths.get("digest/path")); Mockito.when(outputPathsParameters.getImageIdPath()).thenReturn(Paths.get("id/path")); + Mockito.when(outputPathsParameters.getImageJsonPath()).thenReturn(Paths.get("json/path")); Mockito.when(outputPathsParameters.getTarPath()).thenReturn(Paths.get("tar/path")); GradleRawConfiguration rawConfiguration = new GradleRawConfiguration(jibExtension); @@ -115,6 +116,7 @@ public void testGetters() { rawConfiguration.getDockerEnvironment()); Assert.assertEquals(Paths.get("digest/path"), rawConfiguration.getDigestOutputPath()); Assert.assertEquals(Paths.get("id/path"), rawConfiguration.getImageIdOutputPath()); + Assert.assertEquals(Paths.get("json/path"), rawConfiguration.getImageJsonOutputPath()); Assert.assertEquals(Paths.get("tar/path"), rawConfiguration.getTarOutputPath()); } } diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 6875b7b5b7..db5032fa6f 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -288,6 +288,7 @@ Property | Type | Default | Description `tar` | string | `(project-dir)/target/jib-image.tar` | The path of the tarball generated by `jib:buildTar`. Relative paths are resolved relative to the project root. `digest` | string | `(project-dir)/target/jib-image.digest` | The path of the image digest written out during the build. Relative paths are resolved relative to the project root. `imageId` | string | `(project-dir)/target/jib-image.id` | The path of the image ID written out during the build. Relative paths are resolved relative to the project root. +`imageJson` | string | `(project-dir)/target/jib-image.json` | The path of the image metadata json file written out during the build. Relative paths are resolved relative to the project root. `dockerClient` is an object used to configure Docker when building to/from the Docker daemon. It has the following properties: diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index ee1e1fcf84..8928c5fa74 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -215,6 +215,8 @@ public static class OutputPathsParameters { @Nullable @Parameter private File digest; @Nullable @Parameter private File imageId; + + @Nullable @Parameter private File imageJson; } @Nullable @@ -625,6 +627,14 @@ Path getImageIdOutputPath() { return getRelativeToProjectRoot(configuredPath, PropertyNames.OUTPUT_PATHS_IMAGE_ID); } + Path getImageJsonOutputPath() { + Path configuredPath = + outputPaths.imageJson == null + ? Paths.get(getProject().getBuild().getDirectory()).resolve("jib-image.json") + : outputPaths.imageJson.toPath(); + return getRelativeToProjectRoot(configuredPath, PropertyNames.OUTPUT_PATHS_IMAGE_JSON); + } + private Path getRelativeToProjectRoot(Path configuration, String propertyName) { String property = getProperty(propertyName); Path path = property != null ? Paths.get(property) : configuration; diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java index 292f51f318..269ff6ac37 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenRawConfiguration.java @@ -200,4 +200,9 @@ public Path getDigestOutputPath() { public Path getImageIdOutputPath() { return jibPluginConfiguration.getImageIdOutputPath(); } + + @Override + public Path getImageJsonOutputPath() { + return jibPluginConfiguration.getImageJsonOutputPath(); + } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java index 8e676aa9dc..b0d7850e96 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/JibPluginConfigurationTest.java @@ -169,6 +169,10 @@ public void testSystemProperties() { sessionProperties.put("jib.outputPaths.imageId", "id/path"); Assert.assertEquals( Paths.get("/repository/project/id/path"), testPluginConfiguration.getImageIdOutputPath()); + sessionProperties.put("jib.outputPaths.imageJson", "json/path"); + Assert.assertEquals( + Paths.get("/repository/project/json/path"), + testPluginConfiguration.getImageJsonOutputPath()); sessionProperties.put("jib.outputPaths.tar", "tar/path"); Assert.assertEquals( Paths.get("/repository/project/tar/path"), testPluginConfiguration.getTarOutputPath()); @@ -258,6 +262,8 @@ public void testPomProperties() { Assert.assertEquals(Paths.get("/digest/path"), testPluginConfiguration.getDigestOutputPath()); project.getProperties().setProperty("jib.outputPaths.imageId", "/id/path"); Assert.assertEquals(Paths.get("/id/path"), testPluginConfiguration.getImageIdOutputPath()); + project.getProperties().setProperty("jib.outputPaths.imageJson", "/json/path"); + Assert.assertEquals(Paths.get("/json/path"), testPluginConfiguration.getImageJsonOutputPath()); project.getProperties().setProperty("jib.outputPaths.tar", "tar/path"); Assert.assertEquals( Paths.get("/repository/project/tar/path"), testPluginConfiguration.getTarOutputPath()); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java index a1e1d4ee8e..89f791b1d4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenRawConfigurationTest.java @@ -83,6 +83,8 @@ public void testGetters() { .thenReturn(new HashMap<>(ImmutableMap.of("docker", "client"))); Mockito.when(jibPluginConfiguration.getDigestOutputPath()).thenReturn(Paths.get("digest/path")); Mockito.when(jibPluginConfiguration.getImageIdOutputPath()).thenReturn(Paths.get("id/path")); + Mockito.when(jibPluginConfiguration.getImageJsonOutputPath()) + .thenReturn(Paths.get("json/path")); Mockito.when(jibPluginConfiguration.getTarOutputPath()).thenReturn(Paths.get("tar/path")); MavenRawConfiguration rawConfiguration = new MavenRawConfiguration(jibPluginConfiguration); @@ -118,6 +120,7 @@ public void testGetters() { rawConfiguration.getDockerEnvironment()); Assert.assertEquals(Paths.get("digest/path"), jibPluginConfiguration.getDigestOutputPath()); Assert.assertEquals(Paths.get("id/path"), jibPluginConfiguration.getImageIdOutputPath()); + Assert.assertEquals(Paths.get("json/path"), jibPluginConfiguration.getImageJsonOutputPath()); Assert.assertEquals(Paths.get("tar/path"), jibPluginConfiguration.getTarOutputPath()); Mockito.verifyNoMoreInteractions(eventHandlers); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutput.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutput.java new file mode 100644 index 0000000000..786362e4a8 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutput.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.api.JibContainer; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; + +/** + * Builds a JSON string containing metadata about a {@link JibContainer} from a build. + * + *

Example: + * + *

{@code
+ * {
+ *   "image": "gcr.io/project/image:tag",
+ *   "imageId": "sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9",
+ *   "imageDigest": "sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6f",
+ *   "tags": ["latest", "tag"]
+ * }
+ * }
+ */ +public class ImageMetadataOutput implements JsonTemplate { + + private final String image; + private final String imageId; + private final String imageDigest; + private final List tags; + + @JsonCreator + ImageMetadataOutput( + @JsonProperty(value = "image", required = true) String image, + @JsonProperty(value = "imageId", required = true) String imageId, + @JsonProperty(value = "imageDigest", required = true) String imageDigest, + @JsonProperty(value = "tags", required = true) List tags) { + this.image = image; + this.imageId = imageId; + this.imageDigest = imageDigest; + this.tags = tags; + } + + @VisibleForTesting + static ImageMetadataOutput fromJson(String json) throws IOException { + return JsonTemplateMapper.readJson(json, ImageMetadataOutput.class); + } + + public static ImageMetadataOutput fromJibContainer(JibContainer jibContainer) { + String image = jibContainer.getTargetImage().toString(); + String imageId = jibContainer.getImageId().toString(); + String imageDigest = jibContainer.getDigest().toString(); + + // Make sure tags always appear in a predictable way, by sorting them into a list + List tags = ImmutableList.sortedCopyOf(jibContainer.getTags()); + + return new ImageMetadataOutput(image, imageId, imageDigest, tags); + } + + public String getImage() { + return image; + } + + public String getImageId() { + return imageId; + } + + public String getImageDigest() { + return imageDigest; + } + + public List getTags() { + return tags; + } + + public String toJson() throws IOException { + return JsonTemplateMapper.toUtf8String(this); + } +} diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index 0ca73689ad..427ecc1991 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -192,6 +192,7 @@ private static void handleRegistryUnauthorizedException( private final HelpfulSuggestions helpfulSuggestions; @Nullable private Path imageDigestOutputPath; @Nullable private Path imageIdOutputPath; + @Nullable private Path imageJsonOutputPath; @VisibleForTesting JibBuildRunner( @@ -237,6 +238,11 @@ public JibContainer runBuild() String imageId = jibContainer.getImageId().toString(); Files.write(imageIdOutputPath, imageId.getBytes(StandardCharsets.UTF_8)); } + if (imageJsonOutputPath != null) { + ImageMetadataOutput metadataOutput = ImageMetadataOutput.fromJibContainer(jibContainer); + String imageJson = metadataOutput.toJson(); + Files.write(imageJsonOutputPath, imageJson.getBytes(StandardCharsets.UTF_8)); + } return jibContainer; @@ -304,4 +310,16 @@ public JibBuildRunner writeImageId(@Nullable Path imageIdOutputPath) { this.imageIdOutputPath = imageIdOutputPath; return this; } + + /** + * Set the location where the image metadata json will be saved. If {@code null} then the metadata + * is not saved. + * + * @param imageJsonOutputPath the location to write the image metadata, or {@code null} to skip + * @return this + */ + public JibBuildRunner writeImageJson(@Nullable Path imageJsonOutputPath) { + this.imageJsonOutputPath = imageJsonOutputPath; + return this; + } } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 778a839fdd..a68460e700 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -103,7 +103,8 @@ public static JibBuildRunner createJibBuildRunnerForDockerDaemonImage( targetImageReference, rawConfiguration.getToTags()) .writeImageDigest(rawConfiguration.getDigestOutputPath()) - .writeImageId(rawConfiguration.getImageIdOutputPath()); + .writeImageId(rawConfiguration.getImageIdOutputPath()) + .writeImageJson(rawConfiguration.getImageJsonOutputPath()); } public static JibBuildRunner createJibBuildRunnerForTarImage( @@ -136,7 +137,8 @@ public static JibBuildRunner createJibBuildRunnerForTarImage( helpfulSuggestions, rawConfiguration.getTarOutputPath()) .writeImageDigest(rawConfiguration.getDigestOutputPath()) - .writeImageId(rawConfiguration.getImageIdOutputPath()); + .writeImageId(rawConfiguration.getImageIdOutputPath()) + .writeImageJson(rawConfiguration.getImageJsonOutputPath()); } public static JibBuildRunner createJibBuildRunnerForRegistryImage( @@ -188,7 +190,8 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( targetImageReference, rawConfiguration.getToTags()) .writeImageDigest(rawConfiguration.getDigestOutputPath()) - .writeImageId(rawConfiguration.getImageIdOutputPath()); + .writeImageId(rawConfiguration.getImageIdOutputPath()) + .writeImageJson(rawConfiguration.getImageJsonOutputPath()); } public static String getSkaffoldSyncMap( diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index fd15d8f30e..b68a777e02 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -55,6 +55,7 @@ public class PropertyNames { public static final String DOCKER_CLIENT_ENVIRONMENT = "jib.dockerClient.environment"; public static final String OUTPUT_PATHS_DIGEST = "jib.outputPaths.digest"; public static final String OUTPUT_PATHS_IMAGE_ID = "jib.outputPaths.imageId"; + public static final String OUTPUT_PATHS_IMAGE_JSON = "jib.outputPaths.imageJson"; public static final String OUTPUT_PATHS_TAR = "jib.outputPaths.tar"; public static final String CONTAINERIZING_MODE = "jib.containerizingMode"; public static final String SKIP = "jib.skip"; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java index 01a198f135..1ab721a5b3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/RawConfiguration.java @@ -94,4 +94,6 @@ public interface RawConfiguration { Path getDigestOutputPath(); Path getImageIdOutputPath(); + + Path getImageJsonOutputPath(); } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutputTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutputTest.java new file mode 100644 index 0000000000..156c4dd638 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ImageMetadataOutputTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; + +public class ImageMetadataOutputTest { + + private static final String TEST_JSON = + "{\"image\":" + + "\"gcr.io/project/image:tag\"," + + "\"imageId\":" + + "\"sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9\"," + + "\"imageDigest\":" + + "\"sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6fc\"," + + "\"tags\":[\"latest\",\"tag\"]" + + "}"; + + @Test + public void testFromJson() throws IOException { + ImageMetadataOutput output = ImageMetadataOutput.fromJson(TEST_JSON); + Assert.assertEquals("gcr.io/project/image:tag", output.getImage()); + Assert.assertEquals( + "sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9", + output.getImageId()); + Assert.assertEquals( + "sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6fc", + output.getImageDigest()); + + Assert.assertEquals(ImmutableList.of("latest", "tag"), output.getTags()); + } + + @Test + public void testToJson() throws IOException { + ImageMetadataOutput output = ImageMetadataOutput.fromJson(TEST_JSON); + Assert.assertEquals(TEST_JSON, output.toJson()); + } +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java index aa8e4c16ec..447e004d7d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunnerTest.java @@ -20,13 +20,21 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.InsecureRegistryException; +import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.registry.RegistryCredentialsNotSentException; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; import java.util.concurrent.ExecutionException; import org.apache.http.conn.HttpHostConnectException; import org.junit.Assert; @@ -50,6 +58,7 @@ public class JibBuildRunnerTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private JibContainerBuilder mockJibContainerBuilder; + @Mock private JibContainer mockJibContainer; @Mock private Containerizer mockContainerizer; @Mock private RegistryUnauthorizedException mockRegistryUnauthorizedException; @Mock private RegistryCredentialsNotSentException mockRegistryCredentialsNotSentException; @@ -215,4 +224,30 @@ public void testBuildImage_other() Assert.assertEquals(TEST_HELPFUL_SUGGESTIONS.none(), ex.getMessage()); } } + + @Test + public void testBuildImage_writesImageJson() throws Exception { + final ImageReference targetImageReference = ImageReference.parse("gcr.io/distroless/java:11"); + final String imageId = + "sha256:61bb3ec31a47cb730eb58a38bbfa813761a51dca69d10e39c24c3d00a7b2c7a9"; + final String digest = "sha256:3f1be7e19129edb202c071a659a4db35280ab2bb1a16f223bfd5d1948657b6fc"; + final Set tags = ImmutableSet.of("latest", "0.1.41-69d10e-20200116T101403"); + + final Path outputPath = temporaryFolder.newFile("jib-image.json").toPath(); + + Mockito.when(mockJibContainer.getTargetImage()).thenReturn(targetImageReference); + Mockito.when(mockJibContainer.getImageId()).thenReturn(DescriptorDigest.fromDigest(imageId)); + Mockito.when(mockJibContainer.getDigest()).thenReturn(DescriptorDigest.fromDigest(digest)); + Mockito.when(mockJibContainer.getTags()).thenReturn(tags); + Mockito.when(mockJibContainerBuilder.containerize(mockContainerizer)) + .thenReturn(mockJibContainer); + testJibBuildRunner.writeImageJson(outputPath).runBuild(); + + final String outputJson = new String(Files.readAllBytes(outputPath), StandardCharsets.UTF_8); + final ImageMetadataOutput metadataOutput = ImageMetadataOutput.fromJson(outputJson); + Assert.assertEquals(targetImageReference.toString(), metadataOutput.getImage()); + Assert.assertEquals(imageId, metadataOutput.getImageId()); + Assert.assertEquals(digest, metadataOutput.getImageDigest()); + Assert.assertEquals(tags, ImmutableSet.copyOf(metadataOutput.getTags())); + } } From 2085d2192adf2f86fcbfcb1e62f0ed113349426f Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 21 Jan 2020 12:41:21 -0500 Subject: [PATCH 0874/2020] Implement update checks (#2214) --- .../tools/jib/gradle/BuildDockerTask.java | 5 + .../tools/jib/gradle/BuildImageTask.java | 4 + .../cloud/tools/jib/gradle/BuildTarTask.java | 4 + .../cloud/tools/jib/gradle/TaskCommon.java | 43 ++- .../tools/jib/maven/BuildDockerMojo.java | 5 + .../cloud/tools/jib/maven/BuildImageMojo.java | 5 + .../cloud/tools/jib/maven/BuildTarMojo.java | 5 + .../cloud/tools/jib/maven/MojoCommon.java | 40 +++ .../common/PluginConfigurationProcessor.java | 6 +- .../jib/plugins/common/PropertyNames.java | 2 + .../jib/plugins/common/UpdateChecker.java | 206 ++++++++++++++ .../jib/plugins/common/UpdateCheckerTest.java | 254 ++++++++++++++++++ 12 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java create mode 100644 jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index 5d8f3a7ab9..b8c19f2b75 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -34,6 +34,8 @@ import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -92,6 +94,8 @@ public void buildDocker() GradleProjectProperties projectProperties = GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, getLogger()); try { PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( new GradleRawConfiguration(jibExtension), @@ -145,6 +149,7 @@ public void buildDocker() } finally { tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index 8927310deb..c8cb88abaf 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -34,6 +34,7 @@ import com.google.common.base.Strings; import java.io.IOException; import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -81,6 +82,8 @@ public void buildImage() GradleProjectProperties projectProperties = GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, getLogger()); try { if (Strings.isNullOrEmpty(jibExtension.getTo().getImage())) { throw new GradleException( @@ -143,6 +146,7 @@ public void buildImage() } finally { tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java index 3a778b019f..10180a84bd 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java @@ -35,6 +35,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; @@ -106,6 +107,8 @@ public void buildTar() GradleProjectProperties projectProperties = GradleProjectProperties.getForProject(getProject(), getLogger(), tempDirectoryProvider); + Future> updateCheckFuture = + TaskCommon.newUpdateChecker(projectProperties, getLogger()); try { PluginConfigurationProcessor.createJibBuildRunnerForTarImage( new GradleRawConfiguration(jibExtension), @@ -159,6 +162,7 @@ public void buildTar() } finally { tempDirectoryProvider.close(); + TaskCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); } } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index c9cb753796..2a7df70fb3 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -17,18 +17,27 @@ package com.google.cloud.tools.jib.gradle; import com.google.api.client.http.HttpTransport; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; +import com.google.cloud.tools.jib.plugins.common.UpdateChecker; +import com.google.common.util.concurrent.Futures; import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.logging.Level; import javax.annotation.Nullable; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; +import org.gradle.api.logging.Logger; import org.gradle.api.plugins.WarPlugin; import org.gradle.api.tasks.TaskProvider; -import org.gradle.internal.logging.events.LogEvent; import org.gradle.internal.logging.events.OutputEventListener; import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext; import org.slf4j.LoggerFactory; @@ -36,6 +45,35 @@ /** Collection of common methods to share between Gradle tasks. */ class TaskCommon { + public static final String VERSION_URL = "https://storage.googleapis.com/jib-versions/jib-gradle"; + + static Future> newUpdateChecker( + ProjectProperties projectProperties, Logger logger) { + if (projectProperties.isOffline() || !logger.isLifecycleEnabled()) { + return Futures.immediateFuture(Optional.empty()); + } + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + return UpdateChecker.checkForUpdate(executorService, projectProperties::log, VERSION_URL); + } finally { + executorService.shutdown(); + } + } + + static void finishUpdateChecker( + ProjectProperties projectProperties, Future> updateCheckFuture) { + UpdateChecker.finishUpdateCheck(updateCheckFuture) + .ifPresent( + updateMessage -> + projectProperties.log( + LogEvent.lifecycle( + "\n\u001B[33m" + + updateMessage + + "\n" + + ProjectInfo.GITHUB_URL + + "/blob/master/jib-gradle-plugin/CHANGELOG.md\u001B[0m\n"))); + } + @Nullable static TaskProvider getWarTaskProvider(Project project) { if (project.getPlugins().hasPlugin(WarPlugin.class)) { @@ -63,7 +101,8 @@ static void disableHttpLogging() { OutputEventListener defaultOutputEventListener = context.getOutputEventListener(); context.setOutputEventListener( event -> { - LogEvent logEvent = (LogEvent) event; + org.gradle.internal.logging.events.LogEvent logEvent = + (org.gradle.internal.logging.events.LogEvent) event; if (!logEvent.getCategory().contains("org.apache")) { defaultOutputEventListener.onOutput(event); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 08d37c6df3..1ac5d09c18 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -34,6 +34,8 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.Future; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -73,6 +75,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), getSession(), getLog(), tempDirectoryProvider); + Future> updateCheckFuture = + MojoCommon.newUpdateChecker(projectProperties, getLog()); try { PluginConfigurationProcessor.createJibBuildRunnerForDockerDaemonImage( new MavenRawConfiguration(this), @@ -134,6 +138,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } finally { tempDirectoryProvider.close(); + MojoCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 98de714f51..c76788c3f2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -35,6 +35,8 @@ import com.google.common.base.Strings; import java.io.IOException; import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.Future; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -87,6 +89,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), getSession(), getLog(), tempDirectoryProvider); + Future> updateCheckFuture = + MojoCommon.newUpdateChecker(projectProperties, getLog()); try { PluginConfigurationProcessor.createJibBuildRunnerForRegistryImage( new MavenRawConfiguration(this), @@ -148,6 +152,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } finally { tempDirectoryProvider.close(); + MojoCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index 10ab15b691..30057dab23 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -32,6 +32,8 @@ import com.google.cloud.tools.jib.plugins.common.PluginConfigurationProcessor; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.Future; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; @@ -65,6 +67,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { MavenProjectProperties projectProperties = MavenProjectProperties.getForProject( getProject(), getSession(), getLog(), tempDirectoryProvider); + Future> updateCheckFuture = + MojoCommon.newUpdateChecker(projectProperties, getLog()); try { PluginConfigurationProcessor.createJibBuildRunnerForTarImage( new MavenRawConfiguration(this), @@ -126,6 +130,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } finally { tempDirectoryProvider.close(); + MojoCommon.finishUpdateChecker(projectProperties, updateCheckFuture); projectProperties.waitForLoggingThread(); getLog().info(""); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index b333779828..b193e9373c 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -16,18 +16,27 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.FilePermissions; +import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.maven.JibPluginConfiguration.PermissionConfiguration; +import com.google.cloud.tools.jib.plugins.common.ProjectProperties; import com.google.cloud.tools.jib.plugins.common.PropertyNames; +import com.google.cloud.tools.jib.plugins.common.UpdateChecker; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.Futures; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.descriptor.PluginDescriptor; @@ -40,6 +49,37 @@ public class MojoCommon { @VisibleForTesting public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; + public static final String VERSION_URL = "https://storage.googleapis.com/jib-versions/jib-maven"; + + static Future> newUpdateChecker( + ProjectProperties projectProperties, Log logger) { + if (projectProperties.isOffline() || !logger.isInfoEnabled()) { + return Futures.immediateFuture(Optional.empty()); + } + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + return UpdateChecker.checkForUpdate(executorService, projectProperties::log, VERSION_URL); + } finally { + executorService.shutdown(); + } + } + + static void finishUpdateChecker( + ProjectProperties projectProperties, Future> updateCheckFuture) { + UpdateChecker.finishUpdateCheck(updateCheckFuture) + .ifPresent( + updateMessage -> { + projectProperties.log(LogEvent.lifecycle("")); + projectProperties.log(LogEvent.lifecycle("\u001B[33m" + updateMessage + "\u001B[0m")); + projectProperties.log( + LogEvent.lifecycle( + "\u001B[33m" + + ProjectInfo.GITHUB_URL + + "/blob/master/jib-maven-plugin/CHANGELOG.md\u001B[0m")); + projectProperties.log(LogEvent.lifecycle("")); + }); + } + /** * Gets the list of extra directory paths from a {@link JibPluginConfiguration}. Returns {@code * (project dir)/src/main/jib} by default if not configured. diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index a68460e700..7d3e255086 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -168,10 +168,8 @@ public static JibBuildRunner createJibBuildRunnerForRegistryImage( rawConfiguration.getToCredHelper().orElse(null)); boolean alwaysCacheBaseImage = - Boolean.valueOf( - rawConfiguration - .getProperty(PropertyNames.ALWAYS_CACHE_BASE_IMAGE) - .orElse(Boolean.FALSE.toString())); + Boolean.parseBoolean( + rawConfiguration.getProperty(PropertyNames.ALWAYS_CACHE_BASE_IMAGE).orElse("false")); Containerizer containerizer = Containerizer.to(targetImage).setAlwaysCacheBaseImage(alwaysCacheBaseImage); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java index b68a777e02..ce58d14476 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PropertyNames.java @@ -62,6 +62,8 @@ public class PropertyNames { public static final String CONSOLE = "jib.console"; public static final String CONTAINERIZE = "jib.containerize"; public static final String ALWAYS_CACHE_BASE_IMAGE = "jib.alwaysCacheBaseImage"; + public static final String DISABLE_UPDATE_CHECKS = "jib.disableUpdateChecks"; + public static final String CONFIG_DIRECTORY = "jib.configDirectory"; private PropertyNames() {} } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java new file mode 100644 index 0000000000..62ea433a84 --- /dev/null +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java @@ -0,0 +1,206 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.filesystem.XdgDirectories; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.base.Verify; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +/** Checks if Jib is up-to-date. */ +public class UpdateChecker { + + /** JSON template for the configuration file used to enable/disable update checks. */ + @VisibleForTesting + static class ConfigJsonTemplate implements JsonTemplate { + private boolean disableUpdateCheck; + + @VisibleForTesting + void setDisableUpdateCheck(boolean disableUpdateCheck) { + this.disableUpdateCheck = disableUpdateCheck; + } + } + + /** JSON template for content downloaded during version check. */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class VersionJsonTemplate implements JsonTemplate { + private String latest = ""; + } + + /** + * Begins checking for an update in a separate thread. + * + * @param executorService the {@link ExecutorService} + * @param log {@link Consumer} used to log messages + * @param versionUrl the location to check for the latest version + * @return a new {@link UpdateChecker} + */ + public static Future> checkForUpdate( + ExecutorService executorService, Consumer log, String versionUrl) { + return executorService.submit( + () -> + performUpdateCheck( + log, Verify.verifyNotNull(ProjectInfo.VERSION), versionUrl, getConfigDir())); + } + + @VisibleForTesting + static Optional performUpdateCheck( + Consumer log, String currentVersion, String versionUrl, Path configDir) { + // Abort if offline or update checks are disabled + if (Boolean.getBoolean(PropertyNames.DISABLE_UPDATE_CHECKS)) { + return Optional.empty(); + } + + Path configFile = configDir.resolve("config.json"); + Path lastUpdateCheck = configDir.resolve("lastUpdateCheck"); + + try { + // Check global config + if (Files.exists(configFile)) { + // Abort if update checks are disabled + try { + ConfigJsonTemplate config = + JsonTemplateMapper.readJsonFromFile(configFile, ConfigJsonTemplate.class); + if (config.disableUpdateCheck) { + return Optional.empty(); + } + } catch (IOException ex) { + log.accept( + LogEvent.warn( + "Failed to read global Jib config: " + + ex.getMessage() + + "; you may need to fix or delete " + + configFile + + "; ")); + return Optional.empty(); + } + } else { + // Generate config file if it doesn't exist + ConfigJsonTemplate config = new ConfigJsonTemplate(); + Files.createDirectories(configDir); + try (OutputStream outputStream = Files.newOutputStream(configFile)) { + JsonTemplateMapper.writeTo(config, outputStream); + } catch (IOException ex) { + // If attempt to generate new config file failed, delete so we can try again next time + log.accept(LogEvent.debug("Failed to generate global Jib config; " + ex.getMessage())); + Files.deleteIfExists(configFile); + } + } + + // Check time of last update check + if (Files.exists(lastUpdateCheck)) { + try { + String fileContents = + new String(Files.readAllBytes(lastUpdateCheck), StandardCharsets.UTF_8); + Instant modifiedTime = Instant.parse(fileContents); + if (modifiedTime.plus(Duration.ofDays(1)).isAfter(Instant.now())) { + return Optional.empty(); + } + } catch (DateTimeParseException | IOException ex) { + // If reading update time failed, file might be corrupt, so delete it + log.accept(LogEvent.debug("Failed to read lastUpdateCheck; " + ex.getMessage())); + Files.delete(lastUpdateCheck); + } + } + + // Check for update + HttpURLConnection connection = (HttpURLConnection) new URL(versionUrl).openConnection(); + try { + connection.setConnectTimeout(3000); + VersionJsonTemplate version = + JsonTemplateMapper.readJson(connection.getInputStream(), VersionJsonTemplate.class); + Files.write(lastUpdateCheck, Instant.now().toString().getBytes(StandardCharsets.UTF_8)); + if (currentVersion.equals(version.latest)) { + return Optional.empty(); + } + return Optional.of( + "A new version of Jib (" + + version.latest + + ") is available (currently using " + + currentVersion + + "). Update your build configuration to use the latest features and fixes!"); + + } finally { + connection.disconnect(); + } + + } catch (IOException ex) { + log.accept(LogEvent.debug("Update check failed; " + ex.getMessage())); + } + + return Optional.empty(); + } + + /** + * Returns a message indicating Jib should be upgraded if the check succeeded and the current + * version is outdated, or returns {@code Optional.empty()} if the check was interrupted or did + * not determine that a later version was available. + * + * @param updateMessageFuture the {@link Future} returned by {@link UpdateChecker#checkForUpdate} + * @return the {@link Optional} message to upgrade Jib if a later version was found, else {@code + * Optional.empty()}. + */ + public static Optional finishUpdateCheck(Future> updateMessageFuture) { + if (updateMessageFuture.isDone()) { + try { + return updateMessageFuture.get(); + } catch (InterruptedException | ExecutionException ignored) { + // Fail silently; + } + } + updateMessageFuture.cancel(true); + return Optional.empty(); + } + + /** + * Returns the config directory set by {@link PropertyNames#CONFIG_DIRECTORY} if not null, + * otherwise returns the default config directory. + * + * @return the config directory set by {@link PropertyNames#CONFIG_DIRECTORY} if not null, + * otherwise returns the default config directory. + */ + private static Path getConfigDir() { + String configDirProperty = System.getProperty(PropertyNames.CONFIG_DIRECTORY); + if (!Strings.isNullOrEmpty(configDirProperty)) { + return Paths.get(configDirProperty); + } + return XdgDirectories.getConfigHome(); + } + + private UpdateChecker() {} +} diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java new file mode 100644 index 0000000000..91434c2909 --- /dev/null +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.plugins.common; + +import com.google.cloud.tools.jib.api.LogEvent.Level; +import com.google.cloud.tools.jib.http.TestWebServer; +import com.google.common.util.concurrent.Futures; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.Future; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.RestoreSystemProperties; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link UpdateChecker} */ +@RunWith(MockitoJUnitRunner.class) +public class UpdateCheckerTest { + + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private TestWebServer testWebServer; + private Path configDir; + + @Before + public void setUp() + throws InterruptedException, GeneralSecurityException, URISyntaxException, IOException { + testWebServer = + new TestWebServer( + false, + Collections.singletonList( + "HTTP/1.1 200 OK\nContent-Length:18\n\n{\"latest\":\"2.0.0\"}"), + 1); + configDir = temporaryFolder.getRoot().toPath(); + } + + @After + public void tearDown() throws IOException { + testWebServer.close(); + } + + @Test + public void testPerformUpdateCheck_newVersionFound() throws IOException { + Instant before = Instant.now(); + setupConfigAndLastUpdateCheck(); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertTrue(message.isPresent()); + Assert.assertEquals( + "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " + + "configuration to use the latest features and fixes!", + message.get()); + String modifiedTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); + } + + @Test + public void testPerformUpdateCheck_newJsonField() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + testWebServer = + new TestWebServer( + false, + Collections.singletonList( + "HTTP/1.1 200 OK\nContent-Length:18\n\n{\"latest\":\"2.0.0\",\"unknownField\":\"unknown\"}"), + 1); + setupConfigAndLastUpdateCheck(); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertTrue(message.isPresent()); + Assert.assertEquals( + "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " + + "configuration to use the latest features and fixes!", + message.get()); + } + + @Test + public void testPerformUpdateCheck_onLatest() throws IOException { + Instant before = Instant.now(); + setupConfigAndLastUpdateCheck(); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "2.0.0", testWebServer.getEndpoint(), configDir); + Assert.assertFalse(message.isPresent()); + String modifiedTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); + } + + @Test + public void testPerformUpdateCheck_noConfigOrLastUpdateCheck() throws IOException { + Instant before = Instant.now(); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertTrue(message.isPresent()); + Assert.assertEquals( + "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " + + "configuration to use the latest features and fixes!", + message.get()); + String modifiedTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); + } + + @Test + public void testPerformUpdateCheck_lastUpdateCheckTooSoon() throws IOException { + FileTime modifiedTime = FileTime.from(Instant.now().minusSeconds(12)); + setupConfigAndLastUpdateCheck(); + Files.write( + configDir.resolve("lastUpdateCheck"), + modifiedTime.toString().getBytes(StandardCharsets.UTF_8)); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertFalse(message.isPresent()); + + // lastUpdateCheck should not have changed + String lastUpdateTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertEquals(Instant.parse(lastUpdateTime), modifiedTime.toInstant()); + } + + @Test + public void testPerformUpdateCheck_systemProperty() { + System.setProperty(PropertyNames.DISABLE_UPDATE_CHECKS, "true"); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertFalse(message.isPresent()); + } + + @Test + public void testPerformUpdateCheck_configDisabled() throws IOException { + Files.write( + configDir.resolve("config.json"), + "{\"disableUpdateCheck\":true}".getBytes(StandardCharsets.UTF_8)); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertFalse(message.isPresent()); + } + + @Test + public void testPerformUpdateCheck_badConfig() throws IOException { + Files.write( + configDir.resolve("config.json"), "corrupt config".getBytes(StandardCharsets.UTF_8)); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + Assert.assertFalse(message.isPresent()); + } + + @Test + public void testPerformUpdateCheck_badLastUpdateTime() throws IOException { + Instant before = Instant.now(); + Files.write( + configDir.resolve("lastUpdateCheck"), "bad timestamp".getBytes(StandardCharsets.UTF_8)); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + String modifiedTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); + Assert.assertTrue(message.isPresent()); + Assert.assertEquals( + "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " + + "configuration to use the latest features and fixes!", + message.get()); + } + + @Test + public void testPerformUpdateCheck_failSilently() + throws InterruptedException, GeneralSecurityException, URISyntaxException, IOException { + try (TestWebServer badServer = + new TestWebServer(false, Collections.singletonList("HTTP/1.1 400 Bad Request\n\n"), 1)) { + Optional message = + UpdateChecker.performUpdateCheck( + logEvent -> { + Assert.assertEquals(logEvent.getLevel(), Level.DEBUG); + Assert.assertTrue(logEvent.getMessage().contains("Update check failed; ")); + }, + "1.0.2", + badServer.getEndpoint(), + configDir); + Assert.assertFalse(message.isPresent()); + } + } + + @Test + public void testFinishUpdateCheck_success() { + Future> updateCheckFuture = Futures.immediateFuture(Optional.of("Hello")); + Optional result = UpdateChecker.finishUpdateCheck(updateCheckFuture); + Assert.assertTrue(result.isPresent()); + Assert.assertEquals("Hello", result.get()); + } + + @Test + public void testFinishUpdateCheck_notDone() { + @SuppressWarnings("unchecked") + Future> updateCheckFuture = + (Future>) Mockito.mock(Future.class); + Mockito.when(updateCheckFuture.isDone()).thenReturn(false); + + Optional result = UpdateChecker.finishUpdateCheck(updateCheckFuture); + Assert.assertFalse(result.isPresent()); + } + + private void setupConfigAndLastUpdateCheck() throws IOException { + Files.write( + configDir.resolve("config.json"), + "{\"disableUpdateCheck\":false}".getBytes(StandardCharsets.UTF_8)); + Files.write( + configDir.resolve("lastUpdateCheck"), + Instant.now().minus(Duration.ofDays(2)).toString().getBytes(StandardCharsets.UTF_8)); + } +} From dbcfcc4e4508de7695ed05370743a75794760a3d Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 22 Jan 2020 12:28:22 -0500 Subject: [PATCH 0875/2020] Update changelog for json image metadata file (#2239) * Update changelog for json image metadata file --- jib-gradle-plugin/CHANGELOG.md | 3 +++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 7a7a007f51..675f9bb92c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Added json output file for image metadata after a build is complete. Writes to `build/jib-image.json` by default, configurable with `jib.outputPaths.imageJson`. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) + ### Changed - Removed `jibDockerBuild.dockerClient` in favor of `jib.dockerClient`. ([#1983](https://github.com/GoogleContainerTools/jib/issues/1983)) @@ -20,6 +22,7 @@ All notable changes to this project will be documented in this file. - Initial builds will be slower until the cache is repopulated, unless you manually move the cache from the old location to the new location ### Fixed + - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) - Now `jib.containerizingMode='packaged'` works as intended with Spring Boot projects that generate a fat JAR. ([#2178](https://github.com/GoogleContainerTools/jib/pull/2178)) diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4395744364..bb06ab015d 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Added +- Added json output file for image metadata after a build is complete. Writes to `target/jib-image.json` by default, configurable with ``. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) + ### Changed - Removed deprecated `` configuration in favor of ``. ([#1691](https://github.com/GoogleContainerTools/jib/issues/1691)) From f0641faa16d228af928fd271165a9e2587509fa5 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Wed, 22 Jan 2020 18:03:33 -0500 Subject: [PATCH 0876/2020] Fix NPE and report class name (#2241) --- .../jib/registry/RegistryEndpointCaller.java | 3 ++- .../registry/RegistryEndpointCallerTest.java | 24 +++++++++++++++++++ .../jib/plugins/common/JibBuildRunner.java | 5 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java index 07471c53eb..9af5857039 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java @@ -172,7 +172,8 @@ private T call(URL url) throws IOException, RegistryException { } catch (IOException ex) { logError("I/O error for image [" + serverUrl + "/" + imageName + "]:"); - logError(" " + ex.getMessage()); + logError(" " + ex.getClass().getName()); + logError(" " + (ex.getMessage() == null ? "(null exception message)" : ex.getMessage())); logErrorIfBrokenPipe(ex); if (ex instanceof SSLException) { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 1fa203ce08..88c0c343bc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -254,6 +254,8 @@ public void testCall_logErrorOnIoExceptions() throws IOException, RegistryExcept Mockito.verify(mockEventHandlers) .dispatch( LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); Mockito.verify(mockEventHandlers) .dispatch(LogEvent.error("\u001B[31;1m detailed exception message\u001B[0m")); Mockito.verifyNoMoreInteractions(mockEventHandlers); @@ -274,6 +276,8 @@ public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryExceptio Mockito.verify(mockEventHandlers) .dispatch( LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); Mockito.verify(mockEventHandlers) .dispatch(LogEvent.error("\u001B[31;1m this is due to broken pipe\u001B[0m")); Mockito.verify(mockEventHandlers) @@ -286,6 +290,26 @@ public void testCall_logErrorOnBrokenPipe() throws IOException, RegistryExceptio } } + @Test + public void testCall_logNullExceptionMessage() throws IOException, RegistryException { + setUpRegistryResponse(new IOException()); + + try { + endpointCaller.call(); + Assert.fail(); + + } catch (IOException ex) { + Mockito.verify(mockEventHandlers) + .dispatch( + LogEvent.error("\u001B[31;1mI/O error for image [serverUrl/imageName]:\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m java.io.IOException\u001B[0m")); + Mockito.verify(mockEventHandlers) + .dispatch(LogEvent.error("\u001B[31;1m (null exception message)\u001B[0m")); + Mockito.verifyNoMoreInteractions(mockEventHandlers); + } + } + @Test public void testHttpTimeout_propertyNotSet() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java index 427ecc1991..be2ee31661 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/JibBuildRunner.java @@ -278,8 +278,9 @@ public JibContainer runBuild() throw new BuildStepsExecutionException(message, ex); } catch (ExecutionException ex) { - String message = Verify.verifyNotNull(ex.getCause().getMessage()); // keep null-away happy - throw new BuildStepsExecutionException(message, ex.getCause()); + String message = ex.getCause().getMessage(); + throw new BuildStepsExecutionException( + message == null ? "(null exception message)" : message, ex.getCause()); } catch (InterruptedException ex) { throw new BuildStepsExecutionException(helpfulSuggestions.none(), ex); From 2d40c8b840883e0739d504d30e71b7e71c19019c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 23 Jan 2020 17:22:33 -0500 Subject: [PATCH 0877/2020] Add user agent to update check (#2242) --- .../cloud/tools/jib/gradle/TaskCommon.java | 3 +- .../cloud/tools/jib/maven/MojoCommon.java | 3 +- .../jib/plugins/common/UpdateChecker.java | 33 ++++++++++++++----- .../jib/plugins/common/UpdateCheckerTest.java | 25 +++++++------- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java index 2a7df70fb3..492975d5bb 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java @@ -54,7 +54,8 @@ static Future> newUpdateChecker( } ExecutorService executorService = Executors.newSingleThreadExecutor(); try { - return UpdateChecker.checkForUpdate(executorService, projectProperties::log, VERSION_URL); + return UpdateChecker.checkForUpdate( + executorService, projectProperties::log, VERSION_URL, projectProperties.getToolName()); } finally { executorService.shutdown(); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java index b193e9373c..7889d20bc3 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MojoCommon.java @@ -58,7 +58,8 @@ static Future> newUpdateChecker( } ExecutorService executorService = Executors.newSingleThreadExecutor(); try { - return UpdateChecker.checkForUpdate(executorService, projectProperties::log, VERSION_URL); + return UpdateChecker.checkForUpdate( + executorService, projectProperties::log, VERSION_URL, projectProperties.getToolName()); } finally { executorService.shutdown(); } diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java index 62ea433a84..2d2963efa7 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java @@ -20,6 +20,9 @@ import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.filesystem.XdgDirectories; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Request; +import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; @@ -27,7 +30,6 @@ import com.google.common.base.Verify; import java.io.IOException; import java.io.OutputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -68,19 +70,28 @@ private static class VersionJsonTemplate implements JsonTemplate { * @param executorService the {@link ExecutorService} * @param log {@link Consumer} used to log messages * @param versionUrl the location to check for the latest version + * @param toolName the tool name * @return a new {@link UpdateChecker} */ public static Future> checkForUpdate( - ExecutorService executorService, Consumer log, String versionUrl) { + ExecutorService executorService, Consumer log, String versionUrl, String toolName) { return executorService.submit( () -> performUpdateCheck( - log, Verify.verifyNotNull(ProjectInfo.VERSION), versionUrl, getConfigDir())); + log, + Verify.verifyNotNull(ProjectInfo.VERSION), + versionUrl, + getConfigDir(), + toolName)); } @VisibleForTesting static Optional performUpdateCheck( - Consumer log, String currentVersion, String versionUrl, Path configDir) { + Consumer log, + String currentVersion, + String versionUrl, + Path configDir, + String toolName) { // Abort if offline or update checks are disabled if (Boolean.getBoolean(PropertyNames.DISABLE_UPDATE_CHECKS)) { return Optional.empty(); @@ -139,11 +150,17 @@ static Optional performUpdateCheck( } // Check for update - HttpURLConnection connection = (HttpURLConnection) new URL(versionUrl).openConnection(); + FailoverHttpClient httpClient = new FailoverHttpClient(true, false, log); try { - connection.setConnectTimeout(3000); + Response response = + httpClient.get( + new URL(versionUrl), + Request.builder() + .setHttpTimeout(3000) + .setUserAgent("jib " + currentVersion + " " + toolName) + .build()); VersionJsonTemplate version = - JsonTemplateMapper.readJson(connection.getInputStream(), VersionJsonTemplate.class); + JsonTemplateMapper.readJson(response.getBody(), VersionJsonTemplate.class); Files.write(lastUpdateCheck, Instant.now().toString().getBytes(StandardCharsets.UTF_8)); if (currentVersion.equals(version.latest)) { return Optional.empty(); @@ -156,7 +173,7 @@ static Optional performUpdateCheck( + "). Update your build configuration to use the latest features and fixes!"); } finally { - connection.disconnect(); + httpClient.shutDown(); } } catch (IOException ex) { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java index 91434c2909..b6eb0eae1d 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java @@ -75,7 +75,8 @@ public void testPerformUpdateCheck_newVersionFound() throws IOException { setupConfigAndLastUpdateCheck(); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); + Assert.assertTrue(testWebServer.getInputRead().contains("User-Agent: jib 1.0.2 tool name")); Assert.assertTrue(message.isPresent()); Assert.assertEquals( "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " @@ -94,12 +95,12 @@ public void testPerformUpdateCheck_newJsonField() new TestWebServer( false, Collections.singletonList( - "HTTP/1.1 200 OK\nContent-Length:18\n\n{\"latest\":\"2.0.0\",\"unknownField\":\"unknown\"}"), + "HTTP/1.1 200 OK\nContent-Length:43\n\n{\"latest\":\"2.0.0\",\"unknownField\":\"unknown\"}"), 1); setupConfigAndLastUpdateCheck(); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertTrue(message.isPresent()); Assert.assertEquals( "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " @@ -113,11 +114,12 @@ public void testPerformUpdateCheck_onLatest() throws IOException { setupConfigAndLastUpdateCheck(); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "2.0.0", testWebServer.getEndpoint(), configDir); + ignored -> {}, "2.0.0", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertFalse(message.isPresent()); String modifiedTime = new String( Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(testWebServer.getInputRead().contains("User-Agent: jib 2.0.0 tool name")); Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); } @@ -126,7 +128,7 @@ public void testPerformUpdateCheck_noConfigOrLastUpdateCheck() throws IOExceptio Instant before = Instant.now(); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertTrue(message.isPresent()); Assert.assertEquals( "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " @@ -147,7 +149,7 @@ public void testPerformUpdateCheck_lastUpdateCheckTooSoon() throws IOException { modifiedTime.toString().getBytes(StandardCharsets.UTF_8)); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertFalse(message.isPresent()); // lastUpdateCheck should not have changed @@ -162,7 +164,7 @@ public void testPerformUpdateCheck_systemProperty() { System.setProperty(PropertyNames.DISABLE_UPDATE_CHECKS, "true"); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertFalse(message.isPresent()); } @@ -173,7 +175,7 @@ public void testPerformUpdateCheck_configDisabled() throws IOException { "{\"disableUpdateCheck\":true}".getBytes(StandardCharsets.UTF_8)); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertFalse(message.isPresent()); } @@ -183,7 +185,7 @@ public void testPerformUpdateCheck_badConfig() throws IOException { configDir.resolve("config.json"), "corrupt config".getBytes(StandardCharsets.UTF_8)); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); Assert.assertFalse(message.isPresent()); } @@ -194,7 +196,7 @@ public void testPerformUpdateCheck_badLastUpdateTime() throws IOException { configDir.resolve("lastUpdateCheck"), "bad timestamp".getBytes(StandardCharsets.UTF_8)); Optional message = UpdateChecker.performUpdateCheck( - ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir); + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); String modifiedTime = new String( Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); @@ -219,7 +221,8 @@ public void testPerformUpdateCheck_failSilently() }, "1.0.2", badServer.getEndpoint(), - configDir); + configDir, + "tool name"); Assert.assertFalse(message.isPresent()); } } From 384ad0283174c6a7aa0d21c52c6eeedf28b8ee96 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 23 Jan 2020 17:39:49 -0500 Subject: [PATCH 0878/2020] Ignore http client logging in update check (#2243) --- .../google/cloud/tools/jib/plugins/common/UpdateChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java index 2d2963efa7..340fd530a3 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java @@ -150,7 +150,7 @@ static Optional performUpdateCheck( } // Check for update - FailoverHttpClient httpClient = new FailoverHttpClient(true, false, log); + FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); try { Response response = httpClient.get( From 5ed864049abc73b2921af89aa3422a318564baa2 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 24 Jan 2020 12:28:31 -0500 Subject: [PATCH 0879/2020] Refresh bearer auth token if expired (#2233) --- jib-core/CHANGELOG.md | 1 + ... BearerAuthenticationIntegrationTest.java} | 21 +- .../ManifestPullerIntegrationTest.java | 10 +- .../RegistryAuthenticatorIntegrationTest.java | 53 ----- .../builder/steps/AuthenticatePushStep.java | 44 ++--- .../builder/steps/LocalBaseImageSteps.java | 6 +- .../steps/ObtainBaseImageLayerStep.java | 57 +++--- .../jib/builder/steps/PullBaseImageStep.java | 115 +++++------ .../tools/jib/builder/steps/PushBlobStep.java | 14 +- .../steps/PushContainerConfigurationStep.java | 11 +- .../jib/builder/steps/PushImageStep.java | 18 +- .../jib/builder/steps/PushLayerStep.java | 15 +- .../steps/RegistryCredentialRetriever.java | 59 ++++++ .../RetrieveRegistryCredentialsStep.java | 92 --------- .../tools/jib/builder/steps/StepsRunner.java | 58 ++---- .../cloud/tools/jib/http/Authorization.java | 8 - .../jib/registry/RegistryAuthenticator.java | 8 +- .../tools/jib/registry/RegistryClient.java | 186 +++++++++++++++--- .../steps/ObtainBaseImageLayerStepTest.java | 21 +- .../builder/steps/PullBaseImageStepTest.java | 23 ++- .../jib/builder/steps/PushBlobStepTest.java | 14 +- ...a => RegistryCredentialRetrieverTest.java} | 43 ++-- .../cloud/tools/jib/http/TestWebServer.java | 37 +++- .../DockerRegistryBearerTokenTest.java | 8 - .../tools/jib/registry/PlainHttpClient.java | 39 ++++ .../jib/registry/RegistryClientTest.java | 156 +++++++++++++-- .../registry/RegistryEndpointCallerTest.java | 2 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 29 files changed, 626 insertions(+), 495 deletions(-) rename jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/{AuthenticationMethodRetrieverIntegrationTest.java => BearerAuthenticationIntegrationTest.java} (61%) delete mode 100644 jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java rename jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/{RetrieveRegistryCredentialsStepTest.java => RegistryCredentialRetrieverTest.java} (74%) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 365658ae19..953c23250f 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file. - `Containerizer#setAllowInsecureRegistries(boolean)` and the `sendCredentialsOverHttp` system property are now effective for authentication service server connections. ([#2074](https://github.com/GoogleContainerTools/jib/pull/2074)) - Fixed inefficient communications when interacting with insecure registries and servers (when `Containerizer#setAllowInsecureRegistries(boolean)` is set). ([#946](https://github.com/GoogleContainerTools/jib/issues/946)) - Building a tarball with `OCI` format now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) ## 0.12.0 diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java similarity index 61% rename from jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java rename to jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java index 9904b30d21..fd62675406 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java @@ -18,15 +18,12 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; -import java.util.Optional; -import org.junit.Assert; import org.junit.Test; -/** Integration tests for {@link AuthenticationMethodRetriever}. */ -public class AuthenticationMethodRetrieverIntegrationTest { +/** Integration tests for bearer authentication. */ +public class BearerAuthenticationIntegrationTest { private final FailoverHttpClient httpClient = new FailoverHttpClient(false, false, ignored -> {}); @@ -36,16 +33,8 @@ public void testGetRegistryAuthenticator() throws IOException, RegistryException RegistryClient.factory( EventHandlers.NONE, "registry.hub.docker.com", "library/busybox", httpClient) .newRegistryClient(); - Optional registryAuthenticator = - registryClient.getRegistryAuthenticator(); - Assert.assertTrue(registryAuthenticator.isPresent()); - Authorization authorization = registryAuthenticator.get().authenticatePull(null); - - RegistryClient authorizedRegistryClient = - RegistryClient.factory( - EventHandlers.NONE, "registry.hub.docker.com", "library/busybox", httpClient) - .setAuthorization(authorization) - .newRegistryClient(); - authorizedRegistryClient.pullManifest("latest"); + // For public images, Docker Hub still requires bearer authentication (without credentials) + registryClient.doPullBearerAuth(); + registryClient.pullManifest("latest"); } } diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index e0afa8e846..5e2277fb5f 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -18,7 +18,6 @@ import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.http.FailoverHttpClient; import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; @@ -73,12 +72,11 @@ public void testPull_v22() throws IOException, RegistryException { @Test public void testPull_v22ManifestList() throws IOException, RegistryException { - RegistryClient.Factory factory = + RegistryClient registryClient = RegistryClient.factory( - EventHandlers.NONE, "registry-1.docker.io", "library/openjdk", httpClient); - Authorization authorization = - factory.newRegistryClient().getRegistryAuthenticator().get().authenticatePull(null); - RegistryClient registryClient = factory.setAuthorization(authorization).newRegistryClient(); + EventHandlers.NONE, "registry-1.docker.io", "library/openjdk", httpClient) + .newRegistryClient(); + registryClient.doPullBearerAuth(); // Ensure 11-jre-slim is a manifest list V22ManifestListTemplate manifestListTemplate = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java deleted file mode 100644 index 8307a660a6..0000000000 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorIntegrationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.registry; - -import com.google.cloud.tools.jib.api.ImageReference; -import com.google.cloud.tools.jib.api.InvalidImageReferenceException; -import com.google.cloud.tools.jib.api.RegistryException; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.http.FailoverHttpClient; -import java.io.IOException; -import java.util.Optional; -import org.junit.Assert; -import org.junit.Test; - -/** Integration tests for {@link RegistryAuthenticator}. */ -public class RegistryAuthenticatorIntegrationTest { - - private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); - - @Test - public void testAuthenticate() - throws IOException, RegistryException, InvalidImageReferenceException { - ImageReference dockerHubImageReference = ImageReference.parse("library/busybox"); - Optional registryAuthenticator = - RegistryClient.factory( - EventHandlers.NONE, - dockerHubImageReference.getRegistry(), - dockerHubImageReference.getRepository(), - httpClient) - .newRegistryClient() - .getRegistryAuthenticator(); - Assert.assertTrue(registryAuthenticator.isPresent()); - Authorization authorization = registryAuthenticator.get().authenticatePull(null); - - // Checks that some token was received. - Assert.assertTrue(0 < authorization.getToken().length()); - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java index eed8b5b7b2..6c7d1ae5e0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java @@ -21,12 +21,10 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildContext; -import com.google.cloud.tools.jib.http.Authorization; -import com.google.cloud.tools.jib.registry.RegistryAuthenticator; +import com.google.cloud.tools.jib.registry.RegistryClient; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import java.io.IOException; -import java.util.Optional; import java.util.concurrent.Callable; -import javax.annotation.Nullable; /** * Authenticates push to a target registry using Docker Token Authentication. @@ -34,45 +32,43 @@ * @see https://docs.docker.com/registry/spec/auth/token/ */ -class AuthenticatePushStep implements Callable> { +class AuthenticatePushStep implements Callable { private static final String DESCRIPTION = "Authenticating push to %s"; private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - @Nullable private final Credential registryCredential; AuthenticatePushStep( - BuildContext buildContext, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Credential registryCredential) { + BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.registryCredential = registryCredential; } @Override - public Optional call() throws IOException, RegistryException { + public RegistryClient call() throws CredentialRetrievalException, IOException, RegistryException { String registry = buildContext.getTargetImageConfiguration().getImageRegistry(); - try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("authenticating push to " + registry, 1); + try (ProgressEventDispatcher progressDispatcher = + progressEventDispatcherFactory.create("authenticating push to " + registry, 2); TimerEventDispatcher ignored2 = new TimerEventDispatcher( buildContext.getEventHandlers(), String.format(DESCRIPTION, registry))) { - Optional registryAuthenticator = + Credential credential = + RegistryCredentialRetriever.getTargetImageCredential(buildContext).orElse(null); + progressDispatcher.dispatchProgress(1); + + RegistryClient registryClient = buildContext .newTargetImageRegistryClientFactory() - .newRegistryClient() - .getRegistryAuthenticator(); - if (registryAuthenticator.isPresent()) { - return Optional.of(registryAuthenticator.get().authenticatePush(registryCredential)); + .setCredential(credential) + .newRegistryClient(); + if (!registryClient.doPushBearerAuth()) { + // server returned "WWW-Authenticate: Basic ..." (e.g., local Docker registry) + if (credential != null && !credential.isOAuth2RefreshToken()) { + registryClient.configureBasicAuth(); + } } + return registryClient; } - - return (registryCredential == null || registryCredential.isOAuth2RefreshToken()) - ? Optional.empty() - : Optional.of( - Authorization.fromBasicCredentials( - registryCredential.getUsername(), registryCredential.getPassword())); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java index 0ea9216c5f..47831abed3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java @@ -25,7 +25,7 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndRegistryClient; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; @@ -142,7 +142,7 @@ static Callable retrieveTarLayersStep( buildContext, tarPath, progressEventDispatcherFactory, tempDirectoryProvider); } - static Callable returnImageAndAuthorizationStep( + static Callable returnImageAndRegistryClientStep( List layers, ContainerConfigurationTemplate configurationTemplate) { return () -> { // Collect compressed layers and add to manifest @@ -156,7 +156,7 @@ static Callable returnImageAndAuthorizationStep( Blobs.from(configurationTemplate).writeTo(ByteStreams.nullOutputStream()); v22Manifest.setContainerConfiguration( configDescriptor.getSize(), configDescriptor.getDigest()); - return new ImageAndAuthorization( + return new ImageAndRegistryClient( JsonToImageTranslator.toImage(v22Manifest, configurationTemplate), null); }; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java index 8aa5e8ce5b..33fec20c6d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java @@ -22,13 +22,12 @@ import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.cache.CachedLayer; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.Layer; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.base.Verify; @@ -54,23 +53,21 @@ private interface BlobExistenceChecker { static ImmutableList makeListForForcedDownload( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - ImageAndAuthorization baseImageAndAuth) { + Image baseImage, + @Nullable RegistryClient sourceRegistryClient) { BlobExistenceChecker noOpChecker = ignored -> StateInTarget.UNKNOWN; - return makeList(buildContext, progressEventDispatcherFactory, baseImageAndAuth, noOpChecker); + return makeList( + buildContext, progressEventDispatcherFactory, baseImage, sourceRegistryClient, noOpChecker); } static ImmutableList makeListForSelectiveDownload( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - ImageAndAuthorization baseImageAndAuth, - Authorization pushAuthorization) { + Image baseImage, + @Nullable RegistryClient sourceRegistryClient, + RegistryClient targetRegistryClient) { Verify.verify(!buildContext.isOffline()); - RegistryClient targetRegistryClient = - buildContext - .newTargetImageRegistryClientFactory() - .setAuthorization(pushAuthorization) - .newRegistryClient(); // TODO: also check if cross-repo blob mount is possible. BlobExistenceChecker blobExistenceChecker = digest -> @@ -79,31 +76,34 @@ static ImmutableList makeListForSelectiveDownload( : StateInTarget.MISSING; return makeList( - buildContext, progressEventDispatcherFactory, baseImageAndAuth, blobExistenceChecker); + buildContext, + progressEventDispatcherFactory, + baseImage, + sourceRegistryClient, + blobExistenceChecker); } private static ImmutableList makeList( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - ImageAndAuthorization baseImageAndAuth, + Image baseImage, + @Nullable RegistryClient registryClient, BlobExistenceChecker blobExistenceChecker) { - ImmutableList baseImageLayers = baseImageAndAuth.getImage().getLayers(); - try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create( - "launching base image layer pullers", baseImageLayers.size()); + "launching base image layer pullers", baseImage.getLayers().size()); TimerEventDispatcher ignored = new TimerEventDispatcher( buildContext.getEventHandlers(), "Preparing base image layer pullers")) { List layerPullers = new ArrayList<>(); - for (Layer layer : baseImageLayers) { + for (Layer layer : baseImage.getLayers()) { layerPullers.add( new ObtainBaseImageLayerStep( buildContext, progressEventDispatcher.newChildProducer(), layer, - baseImageAndAuth.getAuthorization(), + registryClient, blobExistenceChecker)); } return ImmutableList.copyOf(layerPullers); @@ -114,19 +114,19 @@ private static ImmutableList makeList( private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final Layer layer; - private final @Nullable Authorization pullAuthorization; + private final @Nullable RegistryClient registryClient; private final BlobExistenceChecker blobExistenceChecker; private ObtainBaseImageLayerStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, Layer layer, - @Nullable Authorization pullAuthorization, + @Nullable RegistryClient registryClient, BlobExistenceChecker blobExistenceChecker) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layer = layer; - this.pullAuthorization = pullAuthorization; + this.registryClient = registryClient; this.blobExistenceChecker = blobExistenceChecker; } @@ -163,22 +163,17 @@ public PreparedLayer call() throws IOException, CacheCorruptedException, Registr + "re-download the base image layers."); } - RegistryClient registryClient = - buildContext - .newBaseImageRegistryClientFactory() - .setAuthorization(pullAuthorization) - .newRegistryClient(); - try (ThrottledProgressEventDispatcherWrapper progressEventDispatcherWrapper = new ThrottledProgressEventDispatcherWrapper( progressEventDispatcher.newChildProducer(), "pulling base image layer " + layerDigest)) { CachedLayer cachedLayer = cache.writeCompressedLayer( - registryClient.pullBlob( - layerDigest, - progressEventDispatcherWrapper::setProgressTarget, - progressEventDispatcherWrapper::dispatchProgress)); + Verify.verifyNotNull(registryClient) + .pullBlob( + layerDigest, + progressEventDispatcherWrapper::setProgressTarget, + progressEventDispatcherWrapper::dispatchProgress)); return new PreparedLayer.Builder(cachedLayer).setStateInTarget(stateInTarget).build(); } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index f776137775..661ff7aca2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -25,12 +25,11 @@ import com.google.cloud.tools.jib.blob.Blobs; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndRegistryClient; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.events.ProgressEvent; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.LayerCountMismatchException; import com.google.cloud.tools.jib.image.LayerPropertyNotFoundException; @@ -45,10 +44,8 @@ import com.google.cloud.tools.jib.image.json.V22ManifestListTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.ManifestAndDigest; -import com.google.cloud.tools.jib.registry.RegistryAuthenticator; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.util.List; import java.util.Optional; @@ -56,29 +53,19 @@ import javax.annotation.Nullable; /** Pulls the base image manifest. */ -class PullBaseImageStep implements Callable { +class PullBaseImageStep implements Callable { private static final String DESCRIPTION = "Pulling base image manifest"; /** Structure for the result returned by this step. */ - static class ImageAndAuthorization { + static class ImageAndRegistryClient { - private final Image image; - private final @Nullable Authorization authorization; + final Image image; + @Nullable final RegistryClient registryClient; - @VisibleForTesting - ImageAndAuthorization(Image image, @Nullable Authorization authorization) { + ImageAndRegistryClient(Image image, @Nullable RegistryClient registryClient) { this.image = image; - this.authorization = authorization; - } - - Image getImage() { - return image; - } - - @Nullable - Authorization getAuthorization() { - return authorization; + this.registryClient = registryClient; } } @@ -92,7 +79,7 @@ Authorization getAuthorization() { } @Override - public ImageAndAuthorization call() + public ImageAndRegistryClient call() throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException, CacheCorruptedException, CredentialRetrievalException { @@ -101,73 +88,81 @@ public ImageAndAuthorization call() ImageReference imageReference = buildContext.getBaseImageConfiguration().getImage(); if (imageReference.isScratch()) { eventHandlers.dispatch(LogEvent.progress("Getting scratch base image...")); - return new ImageAndAuthorization(Image.builder(buildContext.getTargetFormat()).build(), null); + return new ImageAndRegistryClient( + Image.builder(buildContext.getTargetFormat()).build(), null); } eventHandlers.dispatch( LogEvent.progress("Getting manifest for base image " + imageReference + "...")); - if (buildContext.isOffline() || imageReference.isTagDigest()) { + if (buildContext.isOffline()) { Optional image = getCachedBaseImage(); if (image.isPresent()) { - return new ImageAndAuthorization(image.get(), null); + return new ImageAndRegistryClient(image.get(), null); } - if (buildContext.isOffline()) { - throw new IOException( - "Cannot run Jib in offline mode; " + imageReference + " not found in local Jib cache"); + throw new IOException( + "Cannot run Jib in offline mode; " + imageReference + " not found in local Jib cache"); + + } else if (imageReference.isTagDigest()) { + Optional image = getCachedBaseImage(); + if (image.isPresent()) { + RegistryClient noAuthRegistryClient = + buildContext.newBaseImageRegistryClientFactory().newRegistryClient(); + // TODO: passing noAuthRegistryClient may be problematic. It may return 401 unauthorized if + // layers have to be downloaded. https://github.com/GoogleContainerTools/jib/issues/2220 + return new ImageAndRegistryClient(image.get(), noAuthRegistryClient); } } try (ProgressEventDispatcher progressEventDispatcher = progressEventDispatcherFactory.create("pulling base image manifest", 2); TimerEventDispatcher ignored1 = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) { + // First, try with no credentials. + RegistryClient noAuthRegistryClient = + buildContext.newBaseImageRegistryClientFactory().newRegistryClient(); try { - return new ImageAndAuthorization(pullBaseImage(null, progressEventDispatcher), null); + return new ImageAndRegistryClient( + pullBaseImage(noAuthRegistryClient, progressEventDispatcher), noAuthRegistryClient); - } catch (RegistryUnauthorizedException ignored2) { + } catch (RegistryUnauthorizedException ex) { eventHandlers.dispatch( LogEvent.lifecycle( "The base image requires auth. Trying again for " + imageReference + "...")); - // If failed, then, retrieve base registry credentials and try with retrieved credentials. - // TODO: Refactor the logic in RetrieveRegistryCredentialsStep out to - // registry.credentials.RegistryCredentialsRetriever. Credential registryCredential = - RetrieveRegistryCredentialsStep.forBaseImage( - buildContext, progressEventDispatcher.newChildProducer()) - .call() - .orElse(null); + RegistryCredentialRetriever.getBaseImageCredential(buildContext).orElse(null); - Authorization registryAuthorization = - registryCredential == null || registryCredential.isOAuth2RefreshToken() - ? null - : Authorization.fromBasicCredentials( - registryCredential.getUsername(), registryCredential.getPassword()); + RegistryClient registryClient = + buildContext + .newBaseImageRegistryClientFactory() + .setCredential(registryCredential) + .newRegistryClient(); try { - return new ImageAndAuthorization( - pullBaseImage(registryAuthorization, progressEventDispatcher), registryAuthorization); + // TODO: refactor the code (https://github.com/GoogleContainerTools/jib/pull/2202) + if (registryCredential == null || registryCredential.isOAuth2RefreshToken()) { + throw ex; + } + + eventHandlers.dispatch(LogEvent.debug("Trying basic auth for " + imageReference + "...")); + registryClient.configureBasicAuth(); + return new ImageAndRegistryClient( + pullBaseImage(registryClient, progressEventDispatcher), registryClient); } catch (RegistryUnauthorizedException registryUnauthorizedException) { // The registry requires us to authenticate using the Docker Token Authentication. // See https://docs.docker.com/registry/spec/auth/token - Optional registryAuthenticator = - buildContext - .newBaseImageRegistryClientFactory() - .newRegistryClient() - .getRegistryAuthenticator(); - if (registryAuthenticator.isPresent()) { - Authorization pullAuthorization = - registryAuthenticator.get().authenticatePull(registryCredential); - - return new ImageAndAuthorization( - pullBaseImage(pullAuthorization, progressEventDispatcher), pullAuthorization); + eventHandlers.dispatch( + LogEvent.debug("Trying bearer auth for " + imageReference + "...")); + if (registryClient.doPullBearerAuth()) { + return new ImageAndRegistryClient( + pullBaseImage(registryClient, progressEventDispatcher), registryClient); } eventHandlers.dispatch( LogEvent.error( - "Failed to retrieve authentication challenge for registry that required token " - + "authentication")); + "The registry asked for basic authentication, but the registry had refused basic " + + "authentication previously")); throw registryUnauthorizedException; } } @@ -190,16 +185,10 @@ public ImageAndAuthorization call() * format */ private Image pullBaseImage( - @Nullable Authorization registryAuthorization, - ProgressEventDispatcher progressEventDispatcher) + RegistryClient registryClient, ProgressEventDispatcher progressEventDispatcher) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { EventHandlers eventHandlers = buildContext.getEventHandlers(); - RegistryClient registryClient = - buildContext - .newBaseImageRegistryClientFactory() - .setAuthorization(registryAuthorization) - .newRegistryClient(); ManifestAndDigest manifestAndDigest = registryClient.pullManifest(buildContext.getBaseImageConfiguration().getImageTag()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 07e2cdd1b5..291a6bd153 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -26,11 +26,9 @@ import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; import java.util.concurrent.Callable; -import javax.annotation.Nullable; /** Pushes a BLOB to the target registry. */ class PushBlobStep implements Callable { @@ -40,7 +38,7 @@ class PushBlobStep implements Callable { private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - @Nullable private final Authorization authorization; + private final RegistryClient registryClient; private final BlobDescriptor blobDescriptor; private final Blob blob; private final boolean forcePush; @@ -48,13 +46,13 @@ class PushBlobStep implements Callable { PushBlobStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Authorization authorization, + RegistryClient registryClient, BlobDescriptor blobDescriptor, Blob blob, boolean forcePush) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.authorization = authorization; + this.registryClient = registryClient; this.blobDescriptor = blobDescriptor; this.blob = blob; this.forcePush = forcePush; @@ -71,11 +69,6 @@ public BlobDescriptor call() throws IOException, RegistryException { new TimerEventDispatcher(eventHandlers, DESCRIPTION + blobDescriptor); ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { - RegistryClient registryClient = - buildContext - .newTargetImageRegistryClientFactory() - .setAuthorization(authorization) - .newRegistryClient(); // check if the BLOB is available if (!forcePush && registryClient.checkBlob(blobDigest).isPresent()) { @@ -94,7 +87,6 @@ public BlobDescriptor call() throws IOException, RegistryException { String targetRegistry = buildContext.getTargetImageConfiguration().getImageRegistry(); String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; registryClient.pushBlob(blobDigest, blob, sourceRepository, throttledProgressReporter); - return blobDescriptor; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java index cba1111883..24bd87dec4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java @@ -23,13 +23,12 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; import java.util.concurrent.Callable; -import javax.annotation.Nullable; /** Pushes the container configuration. */ class PushContainerConfigurationStep implements Callable { @@ -39,17 +38,17 @@ class PushContainerConfigurationStep implements Callable { private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - @Nullable private final Authorization pushAuthorization; + private final RegistryClient registryClient; private final Image builtImage; PushContainerConfigurationStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Authorization authenticatePushStep, + RegistryClient registryClient, Image builtImage) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.pushAuthorization = authenticatePushStep; + this.registryClient = registryClient; this.builtImage = builtImage; } @@ -65,7 +64,7 @@ public BlobDescriptor call() throws IOException, RegistryException { return new PushBlobStep( buildContext, progressEventDispatcher.newChildProducer(), - pushAuthorization, + registryClient, Digests.computeDigest(containerConfiguration), Blobs.from(containerConfiguration), false) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java index a4c0e60a81..74eceddb4b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java @@ -25,7 +25,6 @@ import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.hash.Digests; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; import com.google.cloud.tools.jib.image.json.BuildableManifestTemplate; import com.google.cloud.tools.jib.image.json.ImageToJsonTranslator; @@ -34,7 +33,6 @@ import java.io.IOException; import java.util.Set; import java.util.concurrent.Callable; -import javax.annotation.Nullable; /** * Pushes a manifest for a tag. Returns the manifest digest ("image digest") and the container @@ -47,7 +45,7 @@ class PushImageStep implements Callable { static ImmutableList makeList( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - Authorization pushAuthorization, + RegistryClient registryClient, BlobDescriptor containerConfigurationDigestAndSize, Image builtImage) throws IOException { @@ -73,7 +71,7 @@ static ImmutableList makeList( new PushImageStep( buildContext, progressEventDispatcher.newChildProducer(), - pushAuthorization, + registryClient, manifestTemplate, tag, manifestDigest, @@ -86,7 +84,7 @@ static ImmutableList makeList( private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final BuildableManifestTemplate manifestTemplate; - @Nullable private final Authorization pushAuthorization; + private final RegistryClient registryClient; private final String tag; private final DescriptorDigest imageDigest; private final DescriptorDigest imageId; @@ -94,14 +92,14 @@ static ImmutableList makeList( PushImageStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Authorization pushAuthorization, + RegistryClient registryClient, BuildableManifestTemplate manifestTemplate, String tag, DescriptorDigest imageDigest, DescriptorDigest imageId) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.pushAuthorization = pushAuthorization; + this.registryClient = registryClient; this.manifestTemplate = manifestTemplate; this.tag = tag; this.imageDigest = imageDigest; @@ -116,12 +114,6 @@ public BuildResult call() throws IOException, RegistryException { progressEventDispatcherFactory.create("pushing manifest for " + tag, 1)) { eventHandlers.dispatch(LogEvent.info("Pushing manifest for " + tag + "...")); - RegistryClient registryClient = - buildContext - .newTargetImageRegistryClientFactory() - .setAuthorization(pushAuthorization) - .newRegistryClient(); - registryClient.pushManifest(manifestTemplate, tag); return new BuildResult(imageDigest, imageId); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java index 197e8ed4b4..975149a627 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java @@ -22,21 +22,20 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; import com.google.cloud.tools.jib.configuration.BuildContext; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import javax.annotation.Nullable; class PushLayerStep implements Callable { static ImmutableList makeList( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Authorization pushAuthorization, + RegistryClient registryClient, List> cachedLayers) { try (TimerEventDispatcher ignored = new TimerEventDispatcher(buildContext.getEventHandlers(), "Preparing layer pushers"); @@ -51,7 +50,7 @@ static ImmutableList makeList( new PushLayerStep( buildContext, progressEventDispatcher.newChildProducer(), - pushAuthorization, + registryClient, layer)) .collect(ImmutableList.toImmutableList()); } @@ -60,17 +59,17 @@ static ImmutableList makeList( private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - @Nullable private final Authorization pushAuthorization; + private final RegistryClient registryClient; private final Future preparedLayer; private PushLayerStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, - @Nullable Authorization pushAuthorization, + RegistryClient registryClient, Future preparedLayer) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.pushAuthorization = pushAuthorization; + this.registryClient = registryClient; this.preparedLayer = preparedLayer; } @@ -87,7 +86,7 @@ public BlobDescriptor call() return new PushBlobStep( buildContext, progressEventDispatcherFactory, - pushAuthorization, + registryClient, layer.getBlobDescriptor(), layer.getBlob(), forcePush) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java new file mode 100644 index 0000000000..0169c4ef47 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.builder.steps; + +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.CredentialRetriever; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.configuration.BuildContext; +import com.google.cloud.tools.jib.configuration.ImageConfiguration; +import com.google.cloud.tools.jib.event.EventHandlers; +import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; +import java.util.Optional; + +/** Attempts to retrieve registry credentials. */ +class RegistryCredentialRetriever { + + /** Retrieves credentials for the base image. */ + static Optional getBaseImageCredential(BuildContext buildContext) + throws CredentialRetrievalException { + return retrieve(buildContext.getBaseImageConfiguration(), buildContext.getEventHandlers()); + } + + /** Retrieves credentials for the target image. */ + static Optional getTargetImageCredential(BuildContext buildContext) + throws CredentialRetrievalException { + return retrieve(buildContext.getTargetImageConfiguration(), buildContext.getEventHandlers()); + } + + private static Optional retrieve( + ImageConfiguration imageConfiguration, EventHandlers eventHandlers) + throws CredentialRetrievalException { + for (CredentialRetriever retriever : imageConfiguration.getCredentialRetrievers()) { + Optional credential = retriever.retrieve(); + if (credential.isPresent()) { + return credential; + } + } + + String registry = imageConfiguration.getImageRegistry(); + String repository = imageConfiguration.getImageRepository(); + eventHandlers.dispatch( + LogEvent.info("No credentials could be retrieved for " + registry + "/" + repository)); + return Optional.empty(); + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java deleted file mode 100644 index 46ab96346c..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.cloud.tools.jib.builder.steps; - -import com.google.cloud.tools.jib.api.Credential; -import com.google.cloud.tools.jib.api.CredentialRetriever; -import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.TimerEventDispatcher; -import com.google.cloud.tools.jib.configuration.BuildContext; -import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; -import com.google.common.collect.ImmutableList; -import java.util.Optional; -import java.util.concurrent.Callable; - -/** Attempts to retrieve registry credentials. */ -class RetrieveRegistryCredentialsStep implements Callable> { - - /** Retrieves credentials for the base image. */ - static RetrieveRegistryCredentialsStep forBaseImage( - BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { - return new RetrieveRegistryCredentialsStep( - buildContext, - progressEventDispatcherFactory, - buildContext.getBaseImageConfiguration().getImageRegistry(), - buildContext.getBaseImageConfiguration().getCredentialRetrievers()); - } - - /** Retrieves credentials for the target image. */ - static RetrieveRegistryCredentialsStep forTargetImage( - BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { - return new RetrieveRegistryCredentialsStep( - buildContext, - progressEventDispatcherFactory, - buildContext.getTargetImageConfiguration().getImageRegistry(), - buildContext.getTargetImageConfiguration().getCredentialRetrievers()); - } - - private final BuildContext buildContext; - private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; - - private final String registry; - private final ImmutableList credentialRetrievers; - - RetrieveRegistryCredentialsStep( - BuildContext buildContext, - ProgressEventDispatcher.Factory progressEventDispatcherFactory, - String registry, - ImmutableList credentialRetrievers) { - this.buildContext = buildContext; - this.progressEventDispatcherFactory = progressEventDispatcherFactory; - this.registry = registry; - this.credentialRetrievers = credentialRetrievers; - } - - @Override - public Optional call() throws CredentialRetrievalException { - String description = "Retrieving registry credentials for " + registry; - EventHandlers eventHandlers = buildContext.getEventHandlers(); - eventHandlers.dispatch(LogEvent.progress(description + "...")); - - try (ProgressEventDispatcher ignored = - progressEventDispatcherFactory.create("retrieving credentials for " + registry, 1); - TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { - for (CredentialRetriever credentialRetriever : credentialRetrievers) { - Optional optionalCredential = credentialRetriever.retrieve(); - if (optionalCredential.isPresent()) { - return optionalCredential; - } - } - - eventHandlers.dispatch( - LogEvent.info("No credentials could be retrieved for registry " + registry)); - return Optional.empty(); - } - } -} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 68e616d212..f677f34d54 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -16,18 +16,17 @@ package com.google.cloud.tools.jib.builder.steps; -import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndRegistryClient; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.docker.DockerClient; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.image.Image; +import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; @@ -62,12 +61,11 @@ private static Future failedFuture() { new IllegalStateException("invalid usage; required step not configured")); } - private Future baseImageAndAuth = failedFuture(); + private Future baseImageAndRegistryClient = failedFuture(); private Future>> baseImageLayers = failedFuture(); @Nullable private List> applicationLayers; private Future builtImage = failedFuture(); - private Future> targetRegistryCredentials = failedFuture(); - private Future> pushAuthorization = failedFuture(); + private Future targetRegistryClient = failedFuture(); private Future>> baseImageLayerPushResults = failedFuture(); private Future>> applicationLayerPushResults = failedFuture(); private Future containerConfigurationPushResult = failedFuture(); @@ -149,8 +147,7 @@ public StepsRunner registryPushSteps() { rootProgressDescription = "building image to registry"; boolean layersRequiredLocally = buildContext.getAlwaysCacheBaseImage(); - stepsToRun.add(this::retrieveTargetRegistryCredentials); - stepsToRun.add(this::authenticatePush); + stepsToRun.add(this::authenticateBearerPush); addRetrievalSteps(layersRequiredLocally); stepsToRun.add(this::buildAndCacheApplicationLayers); @@ -205,28 +202,13 @@ private void addRetrievalSteps(boolean layersRequiredLocally) { } } - private void retrieveTargetRegistryCredentials() { + private void authenticateBearerPush() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); - results.targetRegistryCredentials = + results.targetRegistryClient = executorService.submit( - RetrieveRegistryCredentialsStep.forTargetImage( - buildContext, childProgressDispatcherFactory)); - } - - private void authenticatePush() { - ProgressEventDispatcher.Factory childProgressDispatcherFactory = - Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); - - results.pushAuthorization = - executorService.submit( - () -> - new AuthenticatePushStep( - buildContext, - childProgressDispatcherFactory, - results.targetRegistryCredentials.get().orElse(null)) - .call()); + () -> new AuthenticatePushStep(buildContext, childProgressDispatcherFactory).call()); } private void saveDocker() { @@ -260,10 +242,10 @@ private void extractTar() { private void assignLocalImageResult(Future localImage) { results.baseImageLayers = executorService.submit(() -> localImage.get().layers); - results.baseImageAndAuth = + results.baseImageAndRegistryClient = executorService.submit( () -> - LocalBaseImageSteps.returnImageAndAuthorizationStep( + LocalBaseImageSteps.returnImageAndRegistryClientStep( realizeFutures(results.baseImageLayers.get()), localImage.get().configurationTemplate) .call()); @@ -273,7 +255,7 @@ private void pullBaseImage() { ProgressEventDispatcher.Factory childProgressDispatcherFactory = Verify.verifyNotNull(rootProgressDispatcher).newChildProducer(); - results.baseImageAndAuth = + results.baseImageAndRegistryClient = executorService.submit(new PullBaseImageStep(buildContext, childProgressDispatcherFactory)); } @@ -289,12 +271,14 @@ private void obtainBaseImageLayers(boolean layersRequiredLocally) { ? ObtainBaseImageLayerStep.makeListForForcedDownload( buildContext, childProgressDispatcherFactory, - results.baseImageAndAuth.get()) + results.baseImageAndRegistryClient.get().image, + results.baseImageAndRegistryClient.get().registryClient) : ObtainBaseImageLayerStep.makeListForSelectiveDownload( buildContext, childProgressDispatcherFactory, - results.baseImageAndAuth.get(), - results.pushAuthorization.get().orElse(null)))); + results.baseImageAndRegistryClient.get().image, + results.baseImageAndRegistryClient.get().registryClient, + results.targetRegistryClient.get()))); } private void pushBaseImageLayers() { @@ -308,7 +292,7 @@ private void pushBaseImageLayers() { PushLayerStep.makeList( buildContext, childProgressDispatcherFactory, - results.pushAuthorization.get().orElse(null), + results.targetRegistryClient.get(), results.baseImageLayers.get()))); } @@ -332,7 +316,7 @@ private void buildImage() { new BuildImageStep( buildContext, childProgressDispatcherFactory, - results.baseImageAndAuth.get().getImage(), + results.baseImageAndRegistryClient.get().image, realizeFutures(results.baseImageLayers.get()), realizeFutures(Verify.verifyNotNull(results.applicationLayers))) .call()); @@ -348,7 +332,7 @@ private void pushContainerConfiguration() { new PushContainerConfigurationStep( buildContext, childProgressDispatcherFactory, - results.pushAuthorization.get().orElse(null), + results.targetRegistryClient.get(), results.builtImage.get()) .call()); } @@ -364,7 +348,7 @@ private void pushApplicationLayers() { PushLayerStep.makeList( buildContext, childProgressDispatcherFactory, - results.pushAuthorization.get().orElse(null), + results.targetRegistryClient.get(), Verify.verifyNotNull(results.applicationLayers)))); } @@ -383,7 +367,7 @@ private void pushImages() { PushImageStep.makeList( buildContext, childProgressDispatcherFactory, - results.pushAuthorization.get().orElse(null), + results.targetRegistryClient.get(), results.containerConfigurationPushResult.get(), results.builtImage.get())); realizeFutures(manifestPushResults); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index ff1f365862..13ab895ca0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -40,14 +40,6 @@ public static Authorization fromBasicCredentials(String username, String secret) return new Authorization("Basic", token); } - /** - * @param token the token - * @return an {@link Authorization} with a base64-encoded {@code username:password} string - */ - public static Authorization fromBasicToken(String token) { - return new Authorization("Basic", token); - } - /** * @param token the token * @return an {@link Authorization} with a {@code Bearer} token diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 7640f866ef..a2a318b26f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -164,8 +164,8 @@ private RegistryAuthenticator( * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the pull * @throws RegistryAuthenticationFailedException if authentication fails - * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were - * not sent over plain HTTP + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ public Authorization authenticatePull(@Nullable Credential credential) throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { @@ -178,8 +178,8 @@ public Authorization authenticatePull(@Nullable Credential credential) * @param credential the credential used to authenticate * @return an {@code Authorization} authenticating the push * @throws RegistryAuthenticationFailedException if authentication fails - * @throws RegistryCredentialsNotSentException if authentication is failed and credentials were - * not sent over plain HTTP + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ public Authorization authenticatePush(@Nullable Credential credential) throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 7c0795c64a..8c7ca99ef6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -17,10 +17,15 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.ProjectInfo; +import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryAuthenticationFailedException; import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.blob.Blobs; @@ -35,18 +40,23 @@ import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Verify; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimap; import java.io.IOException; import java.net.URL; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Stream; import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; -/** Interfaces with a registry. */ +/** Interfaces with a registry. Thread-safe. */ +@ThreadSafe public class RegistryClient { /** Factory for creating {@link RegistryClient}s. */ @@ -57,7 +67,7 @@ public static class Factory { private final FailoverHttpClient httpClient; @Nullable private String userAgentSuffix; - @Nullable private Authorization authorization; + @Nullable private Credential credential; private Factory( EventHandlers eventHandlers, @@ -71,11 +81,11 @@ private Factory( /** * Sets the authentication credentials to use to authenticate with the registry. * - * @param authorization the {@link Authorization} to access the registry/repository + * @param credential the {@link Credential} to access the registry/repository * @return this */ - public Factory setAuthorization(@Nullable Authorization authorization) { - this.authorization = authorization; + public Factory setCredential(@Nullable Credential credential) { + this.credential = credential; return this; } @@ -98,7 +108,7 @@ public Factory setUserAgentSuffix(@Nullable String userAgentSuffix) { public RegistryClient newRegistryClient() { return new RegistryClient( eventHandlers, - authorization, + credential, registryEndpointRequestProperties, makeUserAgent(), httpClient); @@ -130,6 +140,8 @@ private String makeUserAgent() { } } + private static final int MAX_BEARER_TOKEN_REFRESH_TRIES = 5; + /** * Creates a new {@link Factory} for building a {@link RegistryClient}. * @@ -242,46 +254,131 @@ static Multimap decodeTokenRepositoryGrants(String token) { } private final EventHandlers eventHandlers; - @Nullable private final Authorization authorization; + @Nullable private final Credential credential; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String userAgent; private final FailoverHttpClient httpClient; + // mutable + private final AtomicReference authorization = new AtomicReference<>(); + private boolean readOnlyBearerAuth; + /** * Instantiate with {@link #factory}. * * @param eventHandlers the event handlers used for dispatching log events - * @param authorization the {@link Authorization} to access the registry/repository + * @param credential credential for registry/repository; will not be used unless {@link + * #configureBasicAuth} or {@link #doBearerAuth} is called * @param registryEndpointRequestProperties properties of registry endpoint requests * @param userAgent {@code User-Agent} header to send with the request * @param httpClient HTTP client */ private RegistryClient( EventHandlers eventHandlers, - @Nullable Authorization authorization, + @Nullable Credential credential, RegistryEndpointRequestProperties registryEndpointRequestProperties, String userAgent, FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; - this.authorization = authorization; + this.credential = credential; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; this.httpClient = httpClient; } + public void configureBasicAuth() { + Preconditions.checkNotNull(credential); + Preconditions.checkState(!credential.isOAuth2RefreshToken()); + + authorization.set( + Authorization.fromBasicCredentials(credential.getUsername(), credential.getPassword())); + + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + eventHandlers.dispatch( + LogEvent.debug("configured basic auth for " + registry + "/" + repository)); + } + /** - * @return the {@link RegistryAuthenticator} to authenticate pulls/pushes with the registry, or - * {@link Optional#empty()} if no token authentication is necessary + * Attempts bearer authentication for pull. + * + * @return {@code true} if bearer authentication succeeded; {@code false} if the server expects + * basic authentication (and thus bearer authentication was not attempted) * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails + * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP */ - public Optional getRegistryAuthenticator() - throws IOException, RegistryException { - // Gets the WWW-Authenticate header (eg. 'WWW-Authenticate: Bearer - // realm="https://gcr.io/v2/token",service="gcr.io"') - return callRegistryEndpoint( - new AuthenticationMethodRetriever( - registryEndpointRequestProperties, getUserAgent(), httpClient)); + public boolean doPullBearerAuth() throws IOException, RegistryException { + return doBearerAuth(true); + } + + /** + * Attempts bearer authentication for pull and push. + * + * @return true if bearer authentication succeeded; false if the server expects basic + * authentication (and thus bearer authentication was not attempted) + * @throws IOException if communicating with the endpoint fails + * @throws RegistryException if communicating with the endpoint fails + * @throws RegistryAuthenticationFailedException if authentication fails + * @throws RegistryCredentialsNotSentException if authentication failed and credentials were not + * sent over plain HTTP + */ + public boolean doPushBearerAuth() throws IOException, RegistryException { + return doBearerAuth(false); + } + + private boolean doBearerAuth(boolean readOnlyBearerAuth) throws IOException, RegistryException { + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + String image = registry + "/" + repository; + eventHandlers.dispatch(LogEvent.debug("attempting bearer auth for " + image + "...")); + + Optional authenticator = + callRegistryEndpoint( + new AuthenticationMethodRetriever( + registryEndpointRequestProperties, getUserAgent(), httpClient)); + if (!authenticator.isPresent()) { + eventHandlers.dispatch(LogEvent.debug("server requires basic auth for " + image)); + return false; // server returned "WWW-Authenticate: Basic ..." + } + + if (readOnlyBearerAuth) { + authorization.set(authenticator.get().authenticatePull(credential)); + } else { + authorization.set(authenticator.get().authenticatePush(credential)); + } + this.readOnlyBearerAuth = readOnlyBearerAuth; + eventHandlers.dispatch(LogEvent.debug("bearer auth succeeded for " + image)); + return true; + } + + private Authorization refreshBearerAuth(@Nullable String wwwAuthenticate) + throws RegistryAuthenticationFailedException, RegistryCredentialsNotSentException { + Preconditions.checkState(isBearerAuth(authorization.get())); + + String registry = registryEndpointRequestProperties.getServerUrl(); + String repository = registryEndpointRequestProperties.getImageName(); + eventHandlers.dispatch( + LogEvent.debug("refreshing bearer auth token for " + registry + "/" + repository + "...")); + + if (wwwAuthenticate != null) { + Optional authenticator = + RegistryAuthenticator.fromAuthenticationMethod( + wwwAuthenticate, registryEndpointRequestProperties, getUserAgent(), httpClient); + if (authenticator.isPresent()) { + if (readOnlyBearerAuth) { + return authenticator.get().authenticatePull(credential); + } + return authenticator.get().authenticatePush(credential); + } + } + + throw new RegistryAuthenticationFailedException( + registry, + repository, + "server did not return 'WWW-Authenticate: Bearer' header: " + wwwAuthenticate); } /** @@ -318,6 +415,10 @@ public ManifestAndDigest pullManifest(String imageTag) throws IOException, Re */ public DescriptorDigest pushManifest(BuildableManifestTemplate manifestTemplate, String imageTag) throws IOException, RegistryException { + if (isBearerAuth(authorization.get()) && readOnlyBearerAuth) { + throw new IllegalStateException("push may fail with pull-only bearer auth token"); + } + return callRegistryEndpoint( new ManifestPusher( registryEndpointRequestProperties, manifestTemplate, imageTag, eventHandlers)); @@ -387,10 +488,13 @@ public boolean pushBlob( @Nullable String sourceRepository, Consumer writtenByteCountListener) throws IOException, RegistryException { + if (isBearerAuth(authorization.get()) && readOnlyBearerAuth) { + throw new IllegalStateException("push may fail with pull-only bearer auth token"); + } if (sourceRepository != null && !(JibSystemProperties.useCrossRepositoryBlobMounts() - && canAttemptBlobMount(authorization, sourceRepository))) { + && canAttemptBlobMount(authorization.get(), sourceRepository))) { // don't bother requesting a cross-repository blob-mount if we don't have access sourceRepository = null; } @@ -436,7 +540,7 @@ && canAttemptBlobMount(authorization, sourceRepository))) { */ @VisibleForTesting static boolean canAttemptBlobMount(@Nullable Authorization authorization, String repository) { - if (authorization == null || !"bearer".equalsIgnoreCase(authorization.getScheme())) { + if (!isBearerAuth(authorization)) { // Authorization methods other than the Docker Container Registry Token don't provide // information as to which repositories are accessible. The caller should attempt the mount // and rely on the registry fallback as required by the spec. @@ -445,10 +549,14 @@ static boolean canAttemptBlobMount(@Nullable Authorization authorization, String } // if null then does not appear to be a DCRT Multimap repositoryGrants = - decodeTokenRepositoryGrants(authorization.getToken()); + decodeTokenRepositoryGrants(Verify.verifyNotNull(authorization).getToken()); return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull"); } + private static boolean isBearerAuth(@Nullable Authorization authorization) { + return authorization != null && "bearer".equalsIgnoreCase(authorization.getScheme()); + } + @VisibleForTesting String getUserAgent() { return userAgent; @@ -463,13 +571,31 @@ String getUserAgent() { */ private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { - return new RegistryEndpointCaller<>( - eventHandlers, - userAgent, - registryEndpointProvider, - authorization, - registryEndpointRequestProperties, - httpClient) - .call(); + int bearerTokenRefreshes = 0; + while (true) { + try { + return new RegistryEndpointCaller<>( + eventHandlers, + getUserAgent(), + registryEndpointProvider, + authorization.get(), + registryEndpointRequestProperties, + httpClient) + .call(); + + } catch (RegistryUnauthorizedException ex) { + if (ex.getHttpResponseException().getStatusCode() + != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED + || !isBearerAuth(authorization.get()) + || ++bearerTokenRefreshes >= MAX_BEARER_TOKEN_REFRESH_TRIES) { + throw ex; + } + + // Because we successfully did bearer authentication initially, getting 401 here probably + // means the token was expired. + String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate(); + authorization.set(refreshBearerAuth(wwwAuthenticate)); + } + } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java index cca6b8d8b7..c9bf8e55e8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java @@ -22,7 +22,6 @@ import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PreparedLayer.StateInTarget; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.image.Image; @@ -49,8 +48,6 @@ @RunWith(MockitoJUnitRunner.class) public class ObtainBaseImageLayerStepTest { - private ImageAndAuthorization baseImageAndAuth; - private DescriptorDigest existingLayerDigest; private DescriptorDigest freshLayerDigest; @@ -65,8 +62,6 @@ public class ObtainBaseImageLayerStepTest { @Before public void setUp() throws IOException, RegistryException, DigestException { - baseImageAndAuth = new ImageAndAuthorization(image, null); - existingLayerDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); @@ -83,16 +78,6 @@ public void setUp() throws IOException, RegistryException, DigestException { .thenReturn(Optional.of(Mockito.mock(BlobDescriptor.class))); Mockito.when(registryClient.checkBlob(freshLayerDigest)).thenReturn(Optional.empty()); - RegistryClient.Factory registryClientFactory = - Mockito.mock(RegistryClient.Factory.class, Answers.RETURNS_SELF); - Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); - - Mockito.lenient() - .when(buildContext.newBaseImageRegistryClientFactory()) - .thenReturn(registryClientFactory); - Mockito.when(buildContext.newTargetImageRegistryClientFactory()) - .thenReturn(registryClientFactory); - // necessary to prevent error from classes dealing with progress report Answer3, Consumer> progressSizeSetter = (ignored1, progressSizeConsumer, ignored2) -> { @@ -108,7 +93,7 @@ public void testMakeListForSelectiveDownload() throws IOException, CacheCorruptedException, RegistryException { ImmutableList pullers = ObtainBaseImageLayerStep.makeListForSelectiveDownload( - buildContext, progressDispatcherFactory, baseImageAndAuth, null); + buildContext, progressDispatcherFactory, image, registryClient, registryClient); Assert.assertEquals(2, pullers.size()); PreparedLayer preparedExistingLayer = pullers.get(0).call(); @@ -133,7 +118,7 @@ public void testMakeListForForcedDownload() throws IOException, CacheCorruptedException, RegistryException { ImmutableList pullers = ObtainBaseImageLayerStep.makeListForForcedDownload( - buildContext, progressDispatcherFactory, baseImageAndAuth); + buildContext, progressDispatcherFactory, image, registryClient); Assert.assertEquals(2, pullers.size()); PreparedLayer preparedExistingLayer = pullers.get(0).call(); @@ -161,7 +146,7 @@ public void testLayerMissingInCacheInOfflineMode() ImmutableList pullers = ObtainBaseImageLayerStep.makeListForForcedDownload( - buildContext, progressDispatcherFactory, baseImageAndAuth); + buildContext, progressDispatcherFactory, image, registryClient); try { pullers.get(1).call(); Assert.fail(); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java index 4a266c67d3..fc60e4df23 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java @@ -20,7 +20,7 @@ import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.RegistryException; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; -import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndAuthorization; +import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImageAndRegistryClient; import com.google.cloud.tools.jib.cache.Cache; import com.google.cloud.tools.jib.cache.CacheCorruptedException; import com.google.cloud.tools.jib.configuration.BuildContext; @@ -32,6 +32,7 @@ import com.google.cloud.tools.jib.image.json.ContainerConfigurationTemplate; import com.google.cloud.tools.jib.image.json.ManifestAndConfig; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import java.io.IOException; import java.util.Optional; @@ -54,6 +55,7 @@ public class PullBaseImageStepTest { @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; @Mock private BuildContext buildContext; + @Mock private RegistryClient registryClient; @Mock private ImageConfiguration imageConfiguration; @Mock private Cache cache; @@ -67,6 +69,11 @@ public void setUp() { Mockito.when(buildContext.getEventHandlers()).thenReturn(EventHandlers.NONE); Mockito.when(buildContext.getBaseImageLayersCache()).thenReturn(cache); + RegistryClient.Factory registryClientFactory = Mockito.mock(RegistryClient.Factory.class); + Mockito.when(buildContext.newBaseImageRegistryClientFactory()) + .thenReturn(registryClientFactory); + Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); + pullBaseImageStep = new PullBaseImageStep(buildContext, progressDispatcherFactory); } @@ -82,11 +89,9 @@ public void testCall_digestBaseImage() Mockito.when(imageConfiguration.getImage()).thenReturn(imageReference); Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(manifestAndConfig)); - ImageAndAuthorization result = pullBaseImageStep.call(); - Assert.assertEquals("fat system", result.getImage().getOs()); - Assert.assertNull(result.getAuthorization()); - - Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(); + ImageAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.image.getOs()); + Assert.assertEquals(registryClient, result.registryClient); } @Test @@ -116,9 +121,9 @@ public void testCall_offlineMode_cached() Mockito.when(buildContext.isOffline()).thenReturn(true); Mockito.when(cache.retrieveMetadata(imageReference)).thenReturn(Optional.of(manifestAndConfig)); - ImageAndAuthorization result = pullBaseImageStep.call(); - Assert.assertEquals("fat system", result.getImage().getOs()); - Assert.assertNull(result.getAuthorization()); + ImageAndRegistryClient result = pullBaseImageStep.call(); + Assert.assertEquals("fat system", result.image.getOs()); + Assert.assertNull(result.registryClient); Mockito.verify(buildContext, Mockito.never()).newBaseImageRegistryClientFactory(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java index d87e857f26..a1003783b6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java @@ -48,12 +48,6 @@ public class PushBlobStepTest { @Before public void setUp() { - RegistryClient.Factory registryClientFactory = - Mockito.mock(RegistryClient.Factory.class, Answers.RETURNS_SELF); - Mockito.when(registryClientFactory.newRegistryClient()).thenReturn(registryClient); - - Mockito.when(buildContext.newTargetImageRegistryClientFactory()) - .thenReturn(registryClientFactory); Mockito.when(buildContext.getTargetImageConfiguration()) .thenReturn(ImageConfiguration.builder(ImageReference.scratch()).build()); } @@ -90,7 +84,13 @@ public void testCall_forcePushWithNoBlobCheck() throws IOException, RegistryExce } private void call(boolean forcePush) throws IOException, RegistryException { - new PushBlobStep(buildContext, progressDispatcherFactory, null, blobDescriptor, null, forcePush) + new PushBlobStep( + buildContext, + progressDispatcherFactory, + registryClient, + blobDescriptor, + null, + forcePush) .call(); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java similarity index 74% rename from jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java index 978b192479..1707f0fab9 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStepTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java @@ -20,11 +20,9 @@ import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; -import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildContext; import com.google.cloud.tools.jib.configuration.ImageConfiguration; import com.google.cloud.tools.jib.event.EventHandlers; -import com.google.cloud.tools.jib.event.events.ProgressEvent; import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; @@ -40,9 +38,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -/** Tests for {@link RetrieveRegistryCredentialsStep}. */ +/** Tests for {@link RegistryCredentialRetriever}. */ @RunWith(MockitoJUnitRunner.class) -public class RetrieveRegistryCredentialsStepTest { +public class RegistryCredentialRetrieverTest { @Mock private EventHandlers mockEventHandlers; @@ -59,16 +57,10 @@ public void testCall_retrieved() throws CredentialRetrievalException, IOExceptio Assert.assertEquals( Optional.of(Credential.from("baseusername", "basepassword")), - RetrieveRegistryCredentialsStep.forBaseImage( - buildContext, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call()); + RegistryCredentialRetriever.getBaseImageCredential(buildContext)); Assert.assertEquals( Optional.of(Credential.from("targetusername", "targetpassword")), - RetrieveRegistryCredentialsStep.forTargetImage( - buildContext, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call()); + RegistryCredentialRetriever.getTargetImageCredential(buildContext)); } @Test @@ -77,26 +69,16 @@ public void testCall_none() throws CredentialRetrievalException, IOException { makeFakeBuildContext( Arrays.asList(Optional::empty, Optional::empty), Collections.emptyList()); Assert.assertFalse( - RetrieveRegistryCredentialsStep.forBaseImage( - buildContext, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call() - .isPresent()); + RegistryCredentialRetriever.getBaseImageCredential(buildContext).isPresent()); - Mockito.verify(mockEventHandlers, Mockito.atLeastOnce()) - .dispatch(Mockito.any(ProgressEvent.class)); Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); + .dispatch(LogEvent.info("No credentials could be retrieved for baseregistry/baserepo")); Assert.assertFalse( - RetrieveRegistryCredentialsStep.forTargetImage( - buildContext, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call() - .isPresent()); + RegistryCredentialRetriever.getTargetImageCredential(buildContext).isPresent()); Mockito.verify(mockEventHandlers) - .dispatch(LogEvent.info("No credentials could be retrieved for registry baseregistry")); + .dispatch(LogEvent.info("No credentials could be retrieved for targetregistry/targetrepo")); } @Test @@ -111,10 +93,7 @@ public void testCall_exception() throws IOException { }), Collections.emptyList()); try { - RetrieveRegistryCredentialsStep.forBaseImage( - buildContext, - ProgressEventDispatcher.newRoot(mockEventHandlers, "ignored", 1).newChildProducer()) - .call(); + RegistryCredentialRetriever.getBaseImageCredential(buildContext); Assert.fail("Should have thrown exception"); } catch (CredentialRetrievalException ex) { @@ -126,8 +105,8 @@ private BuildContext makeFakeBuildContext( List baseCredentialRetrievers, List targetCredentialRetrievers) throws IOException { - ImageReference baseImage = ImageReference.of("baseregistry", "ignored", null); - ImageReference targetImage = ImageReference.of("targetregistry", "ignored", null); + ImageReference baseImage = ImageReference.of("baseregistry", "baserepo", null); + ImageReference targetImage = ImageReference.of("targetregistry", "targetrepo", null); return BuildContext.builder() .setEventHandlers(mockEventHandlers) .setBaseImageConfiguration( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index fa8216fe8a..8c236c38df 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -47,12 +47,16 @@ public class TestWebServer implements Closeable { private final boolean https; private final int numThreads; private final List responses; + private final boolean forgetServedResponses; private final ServerSocket serverSocket; private final ExecutorService executorService; private final Semaphore serverStarted = new Semaphore(1); private final StringBuilder inputRead = new StringBuilder(); + private int totalResponsesServed = 0; + private int globalResponseIndex = 0; + public TestWebServer(boolean https) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { this(https, Arrays.asList("HTTP/1.1 200 OK\nContent-Length:12\n\nHello World!"), 1); @@ -65,9 +69,16 @@ public TestWebServer(boolean https, int numThreads) public TestWebServer(boolean https, List responses, int numThreads) throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + this(https, responses, numThreads, false); + } + + public TestWebServer( + boolean https, List responses, int numThreads, boolean forgetServedResponses) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { this.https = https; this.responses = responses; this.numThreads = numThreads; + this.forgetServedResponses = forgetServedResponses; serverSocket = https ? createHttpsServerSocket() : new ServerSocket(0); executorService = Executors.newFixedThreadPool(numThreads + 1); ignoreReturn(executorService.submit(this::listen)); @@ -122,29 +133,43 @@ private Void serveResponses(Socket socket) throws IOException { OutputStream out = socket.getOutputStream(); int firstByte = in.read(); - if (firstByte != 'G' && firstByte != 'P') { // GET, POST, ... + int secondByte = in.read(); + if (!(firstByte == 'G' && secondByte == 'E') + && !(firstByte == 'P' && secondByte == 'O') + && !(firstByte == 'H' && secondByte == 'E')) { // GET, POST, HEAD, ... out.write("HTTP/1.1 400 Bad Request\n\n".getBytes(StandardCharsets.UTF_8)); return null; } BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - for (String response : responses) { + for (int i = 0; true; i++) { for (String line = reader.readLine(); line != null && !line.isEmpty(); // An empty line marks the end of an HTTP request. line = reader.readLine()) { synchronized (inputRead) { if (firstByte != -1) { - inputRead.append((char) firstByte); + inputRead.append((char) firstByte).append((char) secondByte); firstByte = -1; } inputRead.append(line).append('\n'); } } + String response = getNextResponse(i); + if (response == null) { + return null; + } out.write(response.getBytes(StandardCharsets.UTF_8)); socket.getOutputStream().flush(); } } - return null; + } + + private synchronized String getNextResponse(int index) { + if (index >= responses.size() || globalResponseIndex >= responses.size()) { + return null; + } + totalResponsesServed++; + return forgetServedResponses ? responses.get(globalResponseIndex++) : responses.get(index); } // For use to ignore (i.e., accept and do nothing) a return value from ExecutionService.submit(). @@ -162,4 +187,8 @@ public String getInputRead() { return inputRead.toString(); } } + + public synchronized int getTotalResponsesServed() { + return totalResponsesServed; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java index 097267f21c..eb75376a1e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -102,15 +102,7 @@ public void testDecode_invalidToken_randoJwt() { public void testCanAttemptBlobMount_basicCredential() { Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "random")); - } - - /** Basic token should allow access to all. */ - @Test - public void testCanAttemptBlobMount_basicToken() { - // basic tokens are assumed to allow all repositories to be mounted - Authorization fixture = Authorization.fromBasicToken("gobbledygook"); Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "library/openjdk")); - Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "randomrepo")); } @Test diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java new file mode 100644 index 0000000000..f006c04ac0 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry; + +import com.google.api.client.http.GenericUrl; +import com.google.cloud.tools.jib.http.FailoverHttpClient; +import com.google.cloud.tools.jib.http.Request; +import com.google.cloud.tools.jib.http.Response; +import java.io.IOException; +import java.net.URL; + +/** Forces sending all requests in plain-HTTP protocol. For testing only. */ +class PlainHttpClient extends FailoverHttpClient { + + PlainHttpClient() { + super(true, true, ignored -> {}); + } + + @Override + public Response call(String httpMethod, URL url, Request request) throws IOException { + GenericUrl httpUrl = new GenericUrl(url); + httpUrl.setScheme("http"); + return super.call(httpMethod, httpUrl.toURL(), request); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index 059edbf7f1..e8fb31aec4 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -16,16 +16,36 @@ package com.google.cloud.tools.jib.registry; +import com.google.cloud.tools.jib.api.Credential; +import com.google.cloud.tools.jib.api.DescriptorDigest; +import com.google.cloud.tools.jib.api.LogEvent; +import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; -import com.google.cloud.tools.jib.http.Authorization; +import com.google.cloud.tools.jib.http.TestWebServer; +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.hamcrest.CoreMatchers; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** @@ -34,31 +54,42 @@ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { + @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @Mock private EventHandlers eventHandlers; - @Mock private Authorization mockAuthorization; private RegistryClient.Factory testRegistryClientFactory; + private DescriptorDigest digest; + + private TestWebServer registry; + private TestWebServer authServer; @Before - public void setUp() { + public void setUp() throws DigestException { testRegistryClientFactory = - RegistryClient.factory(eventHandlers, "some.server.url", "some image name", null); + RegistryClient.factory(EventHandlers.NONE, "some.server.url", "some image name", null); + digest = + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); } - @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); + @After + public void tearDown() throws IOException { + if (registry != null) { + registry.close(); + } + if (authServer != null) { + authServer.close(); + } + } @Test public void testGetUserAgent_null() { Assert.assertTrue( - testRegistryClientFactory - .setAuthorization(mockAuthorization) - .newRegistryClient() - .getUserAgent() - .startsWith("jib")); + testRegistryClientFactory.newRegistryClient().getUserAgent().startsWith("jib")); Assert.assertTrue( testRegistryClientFactory - .setAuthorization(mockAuthorization) .setUserAgentSuffix(null) .newRegistryClient() .getUserAgent() @@ -83,4 +114,107 @@ public void testGetUserAgentWithUpstreamClient() { Assert.assertTrue(registryClient.getUserAgent().startsWith("jib ")); Assert.assertTrue(registryClient.getUserAgent().endsWith(" skaffold/0.34.0")); } + + @Test + public void testDoBearerAuth_returnsFalseOnBasicAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String basicAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Basic foo\n\n"; + registry = new TestWebServer(false, Arrays.asList(basicAuth), 1); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertFalse(registryClient.doPullBearerAuth()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("server requires basic auth")); + } + + @Test + public void testDoBearerAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + setUpAuthServerAndRegistry(1, "HTTP/1.1 200 OK\nContent-Length: 1234\n\n"); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(1234, digestAndSize.get().getSize()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded")); + } + + @Test + public void testAutomaticTokenRefresh() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + setUpAuthServerAndRegistry(3, "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + // Verify authServer returned bearer token three times (i.e., refreshed twice) + Assert.assertEquals(3, authServer.getTotalResponsesServed()); + Assert.assertEquals(4, registry.getTotalResponsesServed()); + + Mockito.verify(eventHandlers).dispatch(logContains("attempting bearer auth")); + Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded")); + Mockito.verify(eventHandlers, Mockito.times(2)) + .dispatch(logContains("refreshing bearer auth token")); + } + + @Test + public void testConfigureBasicAuth() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String basicAuth = "HTTP/1.1 200 OK\nContent-Length: 56789\n\n"; + registry = new TestWebServer(false, Arrays.asList(basicAuth), 1); + RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass")); + registryClient.configureBasicAuth(); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(56789, digestAndSize.get().getSize()); + Assert.assertThat( + registry.getInputRead(), CoreMatchers.containsString("Authorization: Basic dXNlcjpwYXNz")); + } + + /** + * Sets up an auth server and a registry. The auth server can return a bearer token up to {@code + * maxAuthTokens} times. The registry will initially return "401 Unauthorized" for {@code + * maxTokenResponses} times. (Therefore, a registry client has to get auth tokens from the auth + * server {@code maxAuthTokens} times. After that, the registry returns {@code finalResponse}. + */ + private void setUpAuthServerAndRegistry(int maxAuthTokens, @Nullable String finalResponse) + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), maxAuthTokens); + + String bearerAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n"; + List responses = new ArrayList<>(Collections.nCopies(maxAuthTokens, bearerAuth)); + if (finalResponse != null) { + responses.add(finalResponse); + } + + registry = new TestWebServer(false, responses, responses.size(), true); + } + + private RegistryClient createRegistryClient(@Nullable Credential credential) { + return RegistryClient.factory( + eventHandlers, "localhost:" + registry.getLocalPort(), "foo/bar", new PlainHttpClient()) + .setCredential(credential) + .newRegistryClient(); + } + + private LogEvent logContains(String substring) { + ArgumentMatcher matcher = event -> event.getMessage().contains(substring); + return ArgumentMatchers.argThat(matcher); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java index 88c0c343bc..34b4a02c7b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java @@ -126,7 +126,7 @@ public void setUp() throws IOException { mockEventHandlers, "userAgent", new TestRegistryEndpointProvider(), - Authorization.fromBasicToken("token"), + Authorization.fromBasicCredentials("user", "pass"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), mockHttpClient); diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 675f9bb92c..e585c3768c 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file. - `jibBuildTar` with `jib.container.format='OCI'` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) - Now `jib.containerizingMode='packaged'` works as intended with Spring Boot projects that generate a fat JAR. ([#2178](https://github.com/GoogleContainerTools/jib/pull/2178)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) ## 1.8.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index bb06ab015d..b01a54784a 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to this project will be documented in this file. - Now `packaged` correctly identifies the packaged JAR generated at a non-default location when configured with the Maven Jar Plugin's `` and ``. ([#2170](https://github.com/GoogleContainerTools/jib/issues/2170)) - `jib:buildTar` with `OCI` now builds a correctly formatted OCI archive. ([#2124](https://github.com/GoogleContainerTools/jib/issues/2124)) - Fixed an issue where configuring the `` property of the Maven WAR plugin fails the build. ([#2206](https://github.com/GoogleContainerTools/jib/issues/2206)) +- Now automatically refreshes Docker registry authentication tokens when expired, fixing the issue that long-running builds may fail with "401 unauthorized." ([#691](https://github.com/GoogleContainerTools/jib/issues/691)) ## 1.8.0 From d3615a6df6f0f7e2654466128e685ea7ee73958a Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Fri, 24 Jan 2020 15:56:45 -0500 Subject: [PATCH 0880/2020] Fix UpdateCheckerTest printing stack trace (#2244) --- .../java/com/google/cloud/tools/jib/http/TestWebServer.java | 3 ++- .../cloud/tools/jib/plugins/common/UpdateCheckerTest.java | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java index 8c236c38df..6f8ccf8cf8 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java @@ -137,7 +137,8 @@ private Void serveResponses(Socket socket) throws IOException { if (!(firstByte == 'G' && secondByte == 'E') && !(firstByte == 'P' && secondByte == 'O') && !(firstByte == 'H' && secondByte == 'E')) { // GET, POST, HEAD, ... - out.write("HTTP/1.1 400 Bad Request\n\n".getBytes(StandardCharsets.UTF_8)); + out.write( + "HTTP/1.1 400 Bad Request\nContent-Length: 0\n\n".getBytes(StandardCharsets.UTF_8)); return null; } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java index b6eb0eae1d..8db0bc1a1e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java @@ -212,7 +212,10 @@ public void testPerformUpdateCheck_badLastUpdateTime() throws IOException { public void testPerformUpdateCheck_failSilently() throws InterruptedException, GeneralSecurityException, URISyntaxException, IOException { try (TestWebServer badServer = - new TestWebServer(false, Collections.singletonList("HTTP/1.1 400 Bad Request\n\n"), 1)) { + new TestWebServer( + false, + Collections.singletonList("HTTP/1.1 400 Bad Request\nContent-Length: 0\n\n"), + 1)) { Optional message = UpdateChecker.performUpdateCheck( logEvent -> { From c5a6a1f0a92ad8a727f6a40982a450bcd1055b5a Mon Sep 17 00:00:00 2001 From: Appu Date: Sun, 26 Jan 2020 13:50:34 -0500 Subject: [PATCH 0881/2020] fix output matches for integration test (#2248) --- .../cloud/tools/jib/gradle/SingleProjectIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 74c58787a3..7cb04f3ece 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -275,7 +275,7 @@ public void testDockerDaemon_simpleOnJava11() String targetImage = "localhost:6000/simpleimage:gradle" + System.nanoTime(); Assert.assertEquals( - "Hello, world. \n", + "Hello, world. \n1970-01-01T00:00:01Z\n", JibRunHelper.buildToDockerDaemonAndRun( simpleTestProject, targetImage, "build-java11.gradle")); } From f10e079a452dae9942d559455a83fbf2675bacd2 Mon Sep 17 00:00:00 2001 From: Appu Date: Sun, 26 Jan 2020 13:50:49 -0500 Subject: [PATCH 0882/2020] Upgrade idea-ext to 0.7, removes deprecated gradle api usage (#2245) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a3a4f99df0..259224fa70 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { id 'io.freefair.maven-plugin' version '3.8.1' apply false // apply so we can correctly configure the test runner to be gradle at the project level - id "org.jetbrains.gradle.plugin.idea-ext" version "0.5" + id 'org.jetbrains.gradle.plugin.idea-ext' version '0.7' } // run tests in intellij using gradle test runner From d37d4a7f37ce52d8bf89280dd246b94498b9c900 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 27 Jan 2020 16:12:19 -0500 Subject: [PATCH 0883/2020] Add scripts to update GCS with latest version string (#2251) --- jib-gradle-plugin/scripts/prepare_release.sh | 1 + .../scripts/update_gcs_latest.sh | 41 +++++++++++++++++++ jib-maven-plugin/scripts/prepare_release.sh | 1 + jib-maven-plugin/scripts/update_gcs_latest.sh | 41 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100755 jib-gradle-plugin/scripts/update_gcs_latest.sh create mode 100755 jib-maven-plugin/scripts/update_gcs_latest.sh diff --git a/jib-gradle-plugin/scripts/prepare_release.sh b/jib-gradle-plugin/scripts/prepare_release.sh index e0b2a1e90d..104e47dc6a 100755 --- a/jib-gradle-plugin/scripts/prepare_release.sh +++ b/jib-gradle-plugin/scripts/prepare_release.sh @@ -59,3 +59,4 @@ echo https://github.com/GoogleContainerTools/jib/pull/new/${BRANCH} EchoGreen "Once approved, checkout the 'v${VERSION}-gradle' tag and run './gradlew jib-gradle-plugin:publishPlugins'." EchoGreen "Merge the PR after the plugin is released." +EchoGreen "Run './scripts/update_gcs_latest.sh ${VERSION}' when the release is complete to update the latest version string on GCS." diff --git a/jib-gradle-plugin/scripts/update_gcs_latest.sh b/jib-gradle-plugin/scripts/update_gcs_latest.sh new file mode 100755 index 0000000000..613ec5db99 --- /dev/null +++ b/jib-gradle-plugin/scripts/update_gcs_latest.sh @@ -0,0 +1,41 @@ +#!/bin/bash - +# Usage: ./scripts/update_gcs_latest.sh + +set -e + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." +} + +[ $# -ne 1 ] && Die "Usage: ./scripts/update_gcs_latest.sh " + +CheckVersion $1 + +versionString="{\"latest\":\"$1\"}" +destination="gs://jib-versions/jib-gradle" + +echo $versionString > jib-gradle +gsutil cp jib-gradle $destination +gsutil acl ch -u allUsers:READ $destination +rm jib-gradle + +gcsResult=$(curl https://storage.googleapis.com/jib-versions/jib-gradle) +if [ "$gcsResult" == "$versionString" ] +then + EchoGreen "Version updated successfully" +else + Die "Version update failed" +fi \ No newline at end of file diff --git a/jib-maven-plugin/scripts/prepare_release.sh b/jib-maven-plugin/scripts/prepare_release.sh index c74302bacd..7ba12c6959 100755 --- a/jib-maven-plugin/scripts/prepare_release.sh +++ b/jib-maven-plugin/scripts/prepare_release.sh @@ -58,3 +58,4 @@ EchoGreen 'File a PR for the new release branch:' echo https://github.com/GoogleContainerTools/jib/pull/new/${BRANCH} EchoGreen "Merge the PR after the plugin is released." +EchoGreen "Run './scripts/update_gcs_latest.sh ${VERSION}' when the release is complete to update the latest version string on GCS." diff --git a/jib-maven-plugin/scripts/update_gcs_latest.sh b/jib-maven-plugin/scripts/update_gcs_latest.sh new file mode 100755 index 0000000000..ac9e0f59aa --- /dev/null +++ b/jib-maven-plugin/scripts/update_gcs_latest.sh @@ -0,0 +1,41 @@ +#!/bin/bash - +# Usage: ./scripts/update_gcs_latest.sh + +set -e + +EchoRed() { + echo "$(tput setaf 1; tput bold)$1$(tput sgr0)" +} +EchoGreen() { + echo "$(tput setaf 2; tput bold)$1$(tput sgr0)" +} + +Die() { + EchoRed "$1" + exit 1 +} + +# Usage: CheckVersion +CheckVersion() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+)?$ ]] || Die "Version: $1 not in ###.###.###[-XXX] format." +} + +[ $# -ne 1 ] && Die "Usage: ./scripts/update_gcs_latest.sh " + +CheckVersion $1 + +versionString="{\"latest\":\"$1\"}" +destination="gs://jib-versions/jib-maven" + +echo $versionString > jib-maven +gsutil cp jib-maven $destination +gsutil acl ch -u allUsers:READ $destination +rm jib-maven + +gcsResult=$(curl https://storage.googleapis.com/jib-versions/jib-maven) +if [ "$gcsResult" == "$versionString" ] +then + EchoGreen "Version updated successfully" +else + Die "Version update failed" +fi \ No newline at end of file From b5eb87a7a13658b93430529b306e6873c9061c48 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 28 Jan 2020 14:18:02 -0500 Subject: [PATCH 0884/2020] Gradle release v2.0.0 (#2254) * [Gradle Release Plugin] - pre tag commit: 'v2.0.0-gradle'. * [Gradle Release Plugin] - new version commit: 'v2.0.1-SNAPSHOT-gradle'. --- jib-gradle-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 185c1a7e6a..28af4803ae 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 1.8.1-SNAPSHOT +version = 2.0.1-SNAPSHOT From a19b86453b0937e1b2d46f46d78c7224c0c62843 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 28 Jan 2020 14:18:16 -0500 Subject: [PATCH 0885/2020] Maven release v2.0.0 (#2253) * [Gradle Release Plugin] - pre tag commit: 'v2.0.0-maven'. * [Gradle Release Plugin] - new version commit: 'v2.0.1-SNAPSHOT-maven'. --- jib-maven-plugin/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index 185c1a7e6a..28af4803ae 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 1.8.1-SNAPSHOT +version = 2.0.1-SNAPSHOT From dc86104fea71ad9994f18f4ab15a228eb91406a8 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 28 Jan 2020 16:16:07 -0500 Subject: [PATCH 0886/2020] Fix integration test (#2256) --- .../cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java | 5 ++++- .../src/test/resources/maven/projects/spring-boot/pom.xml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java index d060d6b128..d520b76319 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java @@ -694,7 +694,10 @@ public void testExecute_springBootPackaged() "-c", "/app/classpath/spring-boot-0.1.0.original.jar") .run(); - Assert.assertEquals("2749 /app/classpath/spring-boot-0.1.0.original.jar\n", sizeOutput); + Assert.assertThat( + sizeOutput, CoreMatchers.containsString(" /app/classpath/spring-boot-0.1.0.original.jar")); + int fileSize = Integer.parseInt(sizeOutput.substring(0, sizeOutput.indexOf(' '))); + Assert.assertTrue(fileSize < 3000); // should not be a large fat jar HttpGetVerifier.verifyBody("Hello world", new URL("http://localhost:8080")); } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml index 428853cf93..476d7fdc2d 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot/pom.xml @@ -34,7 +34,7 @@ com.google.cloud.tools jib-maven-plugin - 1.8.1-SNAPSHOT + @@PluginVersion@@ gcr.io/distroless/java:debug ${_TARGET_IMAGE} From 1a2173ccbe10b65e982eefc1fb50288ad2267b51 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 28 Jan 2020 16:16:37 -0500 Subject: [PATCH 0887/2020] Core release v0.13.0 (#2255) * [Gradle Release Plugin] - pre tag commit: 'v0.13.0-core'. * [Gradle Release Plugin] - new version commit: 'v0.13.1-SNAPSHOT-core'. --- jib-core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index bec2129bb8..5b92d1cfd5 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.12.1-SNAPSHOT +version = 0.13.1-SNAPSHOT From 5920bc32dafc0fa219dc183d25c11a31f809309f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 28 Jan 2020 16:26:06 -0500 Subject: [PATCH 0888/2020] Improve FAQ on auth failure (#2203) --- docs/faq.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 17811703f6..78e3de01d9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -492,8 +492,8 @@ If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is oft - `$HOME/.docker/config.json`, [one of the configuration files](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files) for the `docker` command line tool. See [configuration files document](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files), [credential store](https://docs.docker.com/engine/reference/commandline/login/#credentials-store) and [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) sections, and [this](https://github.com/GoogleContainerTools/jib/issues/101) for how to configure auth. For example, you can do `docker login` to save auth in `config.json`, but it is often recommended to configure a credential helper (also configurable in `config.json`). - Some common credential helpers on `$PATH` (for example, `docker-credential-osxkeychain`, `docker-credential-ecr-login`, etc.) for well-known registries. - Jib configurations - - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) for Maven / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) for Gradle - - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) for Maven / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) for Gradle + - Configuring credential helpers: [``](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-docker-credential-helpers) (Maven) / [`from/to.credHelper`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-docker-credential-helpers) (Gradle) + - Specific credentials (not recommend): [`/`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-specific-credentials) or in [`settings.xml`](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#using-maven-settings) (Maven) / [`from/to.auth.username/password`](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#using-specific-credentials) (Gradle) - These parameters can also be set through properties: [Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#system-properties) / [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#system-properties) * `$HOME/.docker/config.json` may also contain short-lived authorizations in the `auths` block that may have expired. In the case of Google Container Registry, if you had previously used `gcloud docker` to configure these authorizations, you should remove these stale authorizations by editing your `config.json` and deleting lines from `auths` associated with `gcr.io` (for example: `"https://asia.gcr.io"`). You can then run `gcloud auth configure-docker` to correctly configure the `credHelpers` block for more robust interactions with gcr. * Different auth configurations exist in multiple places, and Jib is not picking up the auth information you are working on. @@ -502,6 +502,24 @@ If the registry returns `401 Unauthorized` or `"code":"UNAUTHORIZED"`, it is oft * Typos in username, password, image names, or registry names. * You are using a private registry without HTTPS. See [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). +Note, if Jib was able to retrieve credentials, you should see a log message like these: + +``` +Using credentials from Docker config (/home/user/.docker/config.json) for localhost:5000/java +``` +``` +Using credential helper docker-credential-gcr for gcr.io/project/repo +``` +``` +Using credentials from Maven settings file for gcr.io/project/repo +``` +``` +Using credentials from for gcr.io/project/repo +``` +``` +Using credentials from to.auth for gcr.io/project/repo +``` + If you encounter issues interacting with a registry other than `UNAUTHORIZED`, check ["How can I diagnose problems pulling or pushing from remote registries?"](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries). ### How do I configure a proxy? From 1edc9d5055d9132903320c47c097d8fb29279d04 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Tue, 28 Jan 2020 17:23:34 -0500 Subject: [PATCH 0889/2020] Add privacy page for disabling update check (#2232) --- README.md | 4 ++++ docs/privacy.md | 17 +++++++++++++++++ jib-gradle-plugin/CHANGELOG.md | 1 + jib-gradle-plugin/README.md | 4 ++++ jib-maven-plugin/CHANGELOG.md | 1 + jib-maven-plugin/README.md | 4 ++++ 6 files changed, 31 insertions(+) create mode 100644 docs/privacy.md diff --git a/README.md b/README.md index bbef7ff2b8..8d30089496 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ See also [rules_docker](https://github.com/bazelbuild/rules_docker) for a simila See the [Frequently Asked Questions (FAQ) page](docs/faq.md). +## Privacy + +See the [Privacy page](docs/privacy.md). + ## Get involved with the community We welcome contributions! Here's how you can contribute: diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000000..e661680307 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,17 @@ +The privacy of our users is very important to us. +Your use of this software is subject to the Google Privacy Policy. + +## Update check +Many Jib users are unaware of new releases. To encourage users to stay up-to-date, Jib 2.0.0 and later will +periodically check to see if there is a new version of Jib is available. This check fetches a simple text +file hosted in Google Cloud Storage. As a side effect this request is logged, which includes the request path, +source IP address, and the user-agent string. The user-agent is set by Jib and includes the Jib plugin type +and version. + +### How to disable update checks + +1. set the `jib.disableUpdateChecks` system property to `true` +2. set `disableUpdateChecks` to `true` in Jib's global config. The global config is in the following locations by default: + * Linux: `$XDG_CONFIG_HOME/google-cloud-tools-java/jib/config.json` (if `$XDG_CONFIG_HOME` is defined), else `$HOME/.config/google-cloud-tools-java/jib/config.json` + * Mac: `$XDG_CONFIG_HOME/Google/Jib/config.json` (if `$XDG_CONFIG_HOME` is defined), else `$HOME/Library/Preferences/Google/Jib/config.json` + * Windows: `$XDG_CONFIG_HOME\Google\Jib\Config\config.json` (if `$XDG_CONFIG_HOME` is defined), else `%LOCALAPPDATA%\Google\Jib\Config\config.json` \ No newline at end of file diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index e585c3768c..ef8c160c26 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Added json output file for image metadata after a build is complete. Writes to `build/jib-image.json` by default, configurable with `jib.outputPaths.imageJson`. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) +- Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) ### Changed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index d5efa3f94e..4bec1c59c1 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -445,6 +445,10 @@ jib { See the [Jib project FAQ](../docs/faq.md). +## Privacy + +See the [Privacy page](docs/privacy.md). + ## Upcoming Features See [Milestones](https://github.com/GoogleContainerTools/jib/milestones) for planned features. [Get involved with the community](https://github.com/GoogleContainerTools/jib/tree/master#get-involved-with-the-community) for the latest updates. diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index b01a54784a..077b103f8e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Added json output file for image metadata after a build is complete. Writes to `target/jib-image.json` by default, configurable with ``. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) +- Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) ### Changed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index db5032fa6f..5ed536a713 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -549,6 +549,10 @@ To use a different Servlet engine base image, you can customize ` Date: Tue, 28 Jan 2020 17:34:53 -0500 Subject: [PATCH 0890/2020] Update README for new cache location (#2231) --- jib-gradle-plugin/README.md | 7 ++++++- jib-maven-plugin/README.md | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 4bec1c59c1..fdb7fb4c4d 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -271,7 +271,7 @@ Property | Type | Default | Description --- | --- | --- | --- `jib.httpTimeout` | `int` | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. `jib.useOnlyProjectCache` | `boolean` | `false` | If set to true, Jib does not share a cache between different Maven projects. -`jib.baseImageCache` | `File` | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.baseImageCache` | `File` | *Platform-dependent*\*\*\* | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. `jib.applicationCache` | `File` | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. `jib.console` | `String` | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. @@ -279,6 +279,11 @@ Property | Type | Default | Description *\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* +*\*\*\* The default base image cache is in the following locations on each platform:* + * *Linux: `[cache root]/google-cloud-tools-java/jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/.cache/` if not set)* + * *Mac: `[cache root]/Google/Jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/Library/Caches/` if not set)* + * *Windows: `[cache root]\Google\Jib\Cache`, where `[cache root]` is `$XDG_CACHE_HOME` (`%LOCALAPPDATA%` if not set)* + ### Example In this configuration, the image: diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 5ed536a713..03c9c50577 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -318,7 +318,7 @@ Property | Type | Default | Description --- | --- | --- | --- `jib.httpTimeout` | int | `20000` | HTTP connection/read timeout for registry interactions, in milliseconds. Use a value of `0` for an infinite timeout. `jib.useOnlyProjectCache` | boolean | `false` | If set to true, Jib does not share a cache between different Maven projects (i.e. `jib.baseImageCache` defaults to `[project dir]/target/jib-cache` instead of `[user cache home]/google-cloud-tools-java/jib`). -`jib.baseImageCache` | string | `[user cache home]/google-cloud-tools-java/jib` | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. +`jib.baseImageCache` | string | *Platform-dependent*\*\*\* | Sets the directory to use for caching base image layers. This cache can (and should) be shared between multiple images. `jib.applicationCache` | string | `[project dir]/target/jib-cache` | Sets the directory to use for caching application layers. This cache can be shared between multiple images. `jib.console` | string | *None* | If set to `plain`, Jib will print plaintext log messages rather than display a progress bar during the build. @@ -326,6 +326,11 @@ Property | Type | Default | Description *\*\* Uses the main class defined in the `jar` task or tries to find a valid main class.* +*\*\*\* The default base image cache is in the following locations on each platform:* + * *Linux: `[cache root]/google-cloud-tools-java/jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/.cache/` if not set)* + * *Mac: `[cache root]/Google/Jib/`, where `[cache root]` is `$XDG_CACHE_HOME` (`$HOME/Library/Caches/` if not set)* + * *Windows: `[cache root]\Google\Jib\Cache`, where `[cache root]` is `$XDG_CACHE_HOME` (`%LOCALAPPDATA%` if not set)* + ### Example In this configuration, the image: From c588ea7b1bbf670df8ec70f09e0a103cc9733d21 Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Wed, 29 Jan 2020 12:33:34 -0500 Subject: [PATCH 0891/2020] Update version strings for 2.0.0 release (#2257) --- CONTRIBUTING.md | 4 ++-- examples/dropwizard/pom.xml | 2 +- examples/helloworld/build.gradle | 2 +- examples/helloworld/pom.xml | 2 +- examples/java-agent/build.gradle | 2 +- examples/java-agent/pom.xml | 2 +- examples/micronaut/build.gradle | 2 +- examples/multi-module/pom.xml | 2 +- examples/spring-boot/build.gradle | 2 +- examples/spring-boot/pom.xml | 2 +- examples/vertx/build.gradle | 2 +- jib-core/CHANGELOG.md | 8 ++++++++ jib-core/README.md | 4 ++-- jib-core/examples/build.gradle/README.md | 2 +- jib-gradle-plugin/CHANGELOG.md | 8 ++++++++ jib-gradle-plugin/README.md | 2 +- jib-maven-plugin/CHANGELOG.md | 8 ++++++++ jib-maven-plugin/README.md | 6 +++--- 18 files changed, 43 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4352c797c0..ba514ffea7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,12 +118,12 @@ To use a local build of the `jib-gradle-plugin`: mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-gradle-plugin:1.8.1-SNAPSHOT' + classpath 'com.google.cloud.tools:jib-gradle-plugin:2.0.1-SNAPSHOT' } } plugins { - // id 'com.google.cloud.tools.jib' version '1.8.0' + // id 'com.google.cloud.tools.jib' version '2.0.0' } // Applies the java plugin after Jib to make sure it works in this order. diff --git a/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index 2b6f14306b..f9e9ff0657 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 1.8.0 + 2.0.0 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index 98f257bf92..5a629b9d64 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 0173513ad2..0019d73182 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.8.0 + 2.0.0 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index 995dd1bcec..b4bfecdef3 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' id 'de.undercouch.download' version '4.0.0' id "com.gorylenko.gradle-git-properties" version "2.2.0" } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 455ce22ca0..304c1c7a1f 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 1.8.0 + 2.0.0 3.8.0 1.4.2 3.0.1 diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index 01d479142d..04f86139eb 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -3,7 +3,7 @@ plugins { id 'groovy' id 'io.spring.dependency-management' version '1.0.6.RELEASE' id 'net.ltgt.apt-idea' version '0.18' - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' } version '0.1' diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 1395f43a88..ca44ef718f 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -41,7 +41,7 @@ com.google.cloud.tools jib-maven-plugin - 1.8.0 + 2.0.0 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index f52a9aaa57..f07415b141 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index d6c915e619..56bd2368a3 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 1.8.0 + 2.0.0 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 59a5e95c73..1c2e085e3d 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 953c23250f..d54f7b6d7b 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 0.13.0 + +### Added + - New method: `JibContainerBuilder#describeContainer` which returns new class: `JibContainerDescription`, containing a selection of information used for the Jib build. ([#2115](https://github.com/GoogleContainerTools/jib/issues/2115)) ### Changed diff --git a/jib-core/README.md b/jib-core/README.md index 53a9917f6a..ac7366c054 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.12.0 + 0.13.0 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.12.0' + compile 'com.google.cloud.tools:jib-core:0.13.0' } ``` diff --git a/jib-core/examples/build.gradle/README.md b/jib-core/examples/build.gradle/README.md index ff33820db7..670d8bd232 100644 --- a/jib-core/examples/build.gradle/README.md +++ b/jib-core/examples/build.gradle/README.md @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.cloud.tools:jib-core:0.12.0' + classpath 'com.google.cloud.tools:jib-core:0.13.0' } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index ef8c160c26..2e30d036c3 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 2.0.0 + +### Added + - Added json output file for image metadata after a build is complete. Writes to `build/jib-image.json` by default, configurable with `jib.outputPaths.imageJson`. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) - Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index fdb7fb4c4d..ecba101970 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -46,7 +46,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '1.8.0' + id 'com.google.cloud.tools.jib' version '2.0.0' } ``` diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 077b103f8e..3c079053e5 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. ### Added +### Changed + +### Fixed + +## 2.0.0 + +### Added + - Added json output file for image metadata after a build is complete. Writes to `target/jib-image.json` by default, configurable with ``. ([#2227](https://github.com/GoogleContainerTools/jib/pull/2227)) - Added automatic update checks. Jib will now display a message if there is a new version of Jib available. See the [privacy page](../docs/privacy.md) for more details. ([#2193](https://github.com/GoogleContainerTools/jib/issues/2193)) diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 03c9c50577..1ea25a7d6d 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -41,7 +41,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.8.0:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:2.0.0:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -49,7 +49,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:1.8.0:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:2.0.0:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -67,7 +67,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 1.8.0 + 2.0.0 myimage From a39d58021748f5bf70e4c8d0dc166d7f79aeace9 Mon Sep 17 00:00:00 2001 From: Ten000hours Date: Wed, 5 Feb 2020 23:54:21 +0800 Subject: [PATCH 0892/2020] fix typo (#2266) --- .../tools/jib/registry/AuthenticationMethodRetrieverTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index a6cc88924a..0ebc56a4a7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -97,7 +97,7 @@ public void testHandleHttpResponseException_invalidStatusCode() throws RegistryE } @Test - public void tsetHandleHttpResponseException_noHeader() throws ResponseException { + public void testHandleHttpResponseException_noHeader() throws ResponseException { Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); From 98db3913c04a5b9f4ae4827de2a43bfdd9ecdc9f Mon Sep 17 00:00:00 2001 From: Sean Abraham Date: Wed, 5 Feb 2020 13:57:57 -0800 Subject: [PATCH 0893/2020] Replace assemble task dependency with runtimeClasspath (#2247) * Replace assemble task dependency with runtimeClasspath Depending directly on the runtime configuration allows Gradle to add to the task graph appropriately to provide all the artifacts that are specified in that configuration, making it more efficient than using the assemble task. --- .../cloud/tools/jib/gradle/JibPlugin.java | 95 ++++++--------- .../cloud/tools/jib/gradle/JibPluginTest.java | 115 +++++------------- 2 files changed, 71 insertions(+), 139 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index e602dd09da..c8703dd814 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.gradle; +import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME; + import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.gradle.skaffold.CheckJibVersionTask; import com.google.cloud.tools.jib.gradle.skaffold.FilesTaskV2; @@ -23,18 +25,20 @@ import com.google.cloud.tools.jib.gradle.skaffold.SyncMapTask; import com.google.cloud.tools.jib.plugins.common.VersionChecker; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.UnknownTaskException; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; @@ -162,68 +166,49 @@ public void apply(Project project) { SyncMapTask.class, task -> task.setJibExtension(jibExtension)); + Set> jibTaskProviders = + ImmutableSet.of(buildImageTask, buildDockerTask, buildTarTask, syncMapTask); + // A check to catch older versions of Jib. This can be removed once we are certain people // are using Jib 1.3.1 or later. tasks.register(SKAFFOLD_CHECK_REQUIRED_VERSION_TASK_NAME, CheckJibVersionTask.class); project.afterEvaluate( projectAfterEvaluation -> { - try { - TaskProvider warTask = TaskCommon.getWarTaskProvider(projectAfterEvaluation); - TaskProvider bootWarTask = - TaskCommon.getBootWarTaskProvider(projectAfterEvaluation); - List> dependsOnTask = new ArrayList<>(); - if (warTask != null || bootWarTask != null) { - // Have all tasks depend on the 'war' and/or 'bootWar' task. - if (warTask != null) { - dependsOnTask.add(warTask); - } - if (bootWarTask != null) { - dependsOnTask.add(bootWarTask); - } - } else if ("packaged".equals(jibExtension.getContainerizingMode())) { - // Have all tasks depend on the 'jar' task. - TaskProvider jarTask = projectAfterEvaluation.getTasks().named("jar"); - dependsOnTask.add(jarTask); - - if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { - Jar jar = (Jar) jarTask.get(); - jar.setEnabled(true); - jar.getArchiveClassifier().set("original"); - } - } else { - // Have all tasks depend on the 'classes' task. - dependsOnTask.add(projectAfterEvaluation.getTasks().named("classes")); + TaskProvider warTask = TaskCommon.getWarTaskProvider(projectAfterEvaluation); + TaskProvider bootWarTask = + TaskCommon.getBootWarTaskProvider(projectAfterEvaluation); + List jibDependencies = new ArrayList<>(); + if (warTask != null || bootWarTask != null) { + // Have all tasks depend on the 'war' and/or 'bootWar' task. + if (warTask != null) { + jibDependencies.add(warTask); + } + if (bootWarTask != null) { + jibDependencies.add(bootWarTask); } - buildImageTask.configure(task -> task.dependsOn(dependsOnTask)); - buildDockerTask.configure(task -> task.dependsOn(dependsOnTask)); - buildTarTask.configure(task -> task.dependsOn(dependsOnTask)); - syncMapTask.configure(task -> task.dependsOn(dependsOnTask)); - - // Find project dependencies and add a dependency to their assemble task. We make sure - // to only add the dependency after BasePlugin is evaluated as otherwise the assemble - // task may not be available yet. - List computedDependencies = getProjectDependencies(projectAfterEvaluation); - for (Project dependencyProject : computedDependencies) { - dependencyProject - .getPlugins() - .withType( - BasePlugin.class, - unused -> { - TaskProvider assembleTask = - dependencyProject.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME); - buildImageTask.configure(task -> task.dependsOn(assembleTask)); - buildDockerTask.configure(task -> task.dependsOn(assembleTask)); - buildTarTask.configure(task -> task.dependsOn(assembleTask)); - }); + } else if ("packaged".equals(jibExtension.getContainerizingMode())) { + // Have all tasks depend on the 'jar' task. + TaskProvider jarTask = projectAfterEvaluation.getTasks().named("jar"); + jibDependencies.add(jarTask); + + if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { + Jar jar = (Jar) jarTask.get(); + jar.setEnabled(true); + jar.getArchiveClassifier().set("original"); } - } catch (UnknownTaskException ex) { - throw new GradleException( - "Could not find task 'classes' on project " - + projectAfterEvaluation.getDisplayName() - + " - perhaps you did not apply the 'java' plugin?", - ex); } + + SourceSet mainSourceSet = + project + .getConvention() + .getPlugin(JavaPluginConvention.class) + .getSourceSets() + .getByName(MAIN_SOURCE_SET_NAME); + jibDependencies.add(mainSourceSet.getRuntimeClasspath()); + + jibTaskProviders.forEach( + provider -> provider.configure(task -> task.setDependsOn(jibDependencies))); }); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index a0f4ceff9e..2f2bf04c33 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -17,15 +17,12 @@ package com.google.cloud.tools.jib.gradle; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -136,72 +133,6 @@ public void testCheckJibVersionInvoked() { } } - @SuppressWarnings("unchecked") - @Test - public void testProjectDependencyAssembleTasksAreRun() { - // root project is our jib packaged service - Project rootProject = createProject("java"); - - // our service DOES depend on this, and jib should trigger an assemble from this project - Project subProject = - ProjectBuilder.builder() - .withParent(rootProject) - .withProjectDir(testProjectRoot.getRoot()) - .withName("sub") - .build(); - subProject.getPluginManager().apply("java"); - - // our service doesn't depend on this, and jib should NOT trigger an assemble from this project - Project unrelatedSubProject = - ProjectBuilder.builder() - .withParent(rootProject) - .withProjectDir(testProjectRoot.getRoot()) - .withName("unrelated") - .build(); - unrelatedSubProject.getPluginManager().apply("java"); - - // equivalent of "compile project(':sub')" on the root(jib) project - rootProject - .getConfigurations() - .getByName("compile") - .getDependencies() - .add(rootProject.getDependencies().project(ImmutableMap.of("path", subProject.getPath()))); - - // programmatic check - Assert.assertEquals( - Collections.singletonList(":sub"), - JibPlugin.getProjectDependencies(rootProject) - .stream() - .map(Project::getPath) - .collect(Collectors.toList())); - - // check by applying the jib plugin and inspect the task dependencies - rootProject.getPluginManager().apply("com.google.cloud.tools.jib"); - - TaskContainer tasks = rootProject.getTasks(); - // add a custom task that our jib tasks depend on to ensure we do not overwrite this dependsOn - TaskProvider dependencyTask = rootProject.getTasks().register("myCustomTask", task -> {}); - KNOWN_JIB_TASKS.forEach(taskName -> tasks.getByPath(taskName).dependsOn(dependencyTask)); - - ((ProjectInternal) rootProject).evaluate(); - - KNOWN_JIB_TASKS.forEach( - taskName -> - Assert.assertEquals( - ImmutableSet.of(":sub:assemble", ":classes", ":myCustomTask"), - tasks - .getByPath(taskName) - .getDependsOn() - .stream() - .map( - object -> - object instanceof List ? object : Collections.singletonList(object)) - .map(List.class::cast) - .flatMap(List::stream) - .map(object -> ((TaskProvider) object).get().getPath()) - .collect(Collectors.toSet()))); - } - @SuppressWarnings("unchecked") @Test public void testWebAppProject() { @@ -213,10 +144,16 @@ public void testWebAppProject() { Assert.assertNotNull(warTask); for (String taskName : KNOWN_JIB_TASKS) { - List> taskProviders = - (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); - Assert.assertEquals(1, taskProviders.size()); - Assert.assertEquals(warTask, taskProviders.get(0).get()); + Set taskDependencies = + tasks + .getByPath(taskName) + .getDependsOn() + .stream() + .filter(TaskProvider.class::isInstance) + .map(it -> ((TaskProvider) it).get()) + .collect(Collectors.toSet()); + + Assert.assertTrue(taskDependencies.contains(warTask)); } } @@ -234,11 +171,16 @@ public void testWebAppProject_bootWar() { Assert.assertNotNull(bootWarTask); for (String taskName : KNOWN_JIB_TASKS) { - List> taskProviders = - (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); - Assert.assertEquals( - ImmutableSet.of(warTask, bootWarTask), - taskProviders.stream().map(TaskProvider::get).collect(Collectors.toSet())); + Set taskDependencies = + tasks + .getByPath(taskName) + .getDependsOn() + .stream() + .filter(TaskProvider.class::isInstance) + .map(it -> ((TaskProvider) it).get()) + .collect(Collectors.toSet()); + + Assert.assertTrue(taskDependencies.containsAll(Arrays.asList(warTask, bootWarTask))); } } @@ -257,11 +199,16 @@ public void testWebAppProject_bootWarDisabled() { bootWarTask.setEnabled(false); // should depend on bootWar even if disabled for (String taskName : KNOWN_JIB_TASKS) { - List> taskProviders = - (List>) tasks.getByPath(taskName).getDependsOn().iterator().next(); - Assert.assertEquals( - ImmutableSet.of(warTask, bootWarTask), - taskProviders.stream().map(TaskProvider::get).collect(Collectors.toSet())); + Set taskDependencies = + tasks + .getByPath(taskName) + .getDependsOn() + .stream() + .filter(TaskProvider.class::isInstance) + .map(it -> ((TaskProvider) it).get()) + .collect(Collectors.toSet()); + + Assert.assertTrue(taskDependencies.containsAll(Arrays.asList(warTask, bootWarTask))); } } From 2e7b670a44ebdca60b400f8b540eb5c567dd3e11 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 6 Feb 2020 17:24:27 -0500 Subject: [PATCH 0894/2020] Fall back to initial authenticator if bad response (#2261) * Fall back to initial authenticator if bad response * CHANGELOG --- jib-core/CHANGELOG.md | 2 + .../tools/jib/registry/RegistryClient.java | 14 ++-- .../jib/registry/RegistryClientTest.java | 68 +++++++++++++++++++ jib-gradle-plugin/CHANGELOG.md | 2 + jib-maven-plugin/CHANGELOG.md | 2 + 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index d54f7b6d7b..9d7b161ab3 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) + ## 0.13.0 ### Added diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 8c7ca99ef6..990854ceea 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -262,6 +262,8 @@ static Multimap decodeTokenRepositoryGrants(String token) { // mutable private final AtomicReference authorization = new AtomicReference<>(); private boolean readOnlyBearerAuth; + private final AtomicReference initialBearerAuthenticator = + new AtomicReference<>(); /** * Instantiate with {@link #factory}. @@ -344,6 +346,7 @@ private boolean doBearerAuth(boolean readOnlyBearerAuth) throws IOException, Reg return false; // server returned "WWW-Authenticate: Basic ..." } + initialBearerAuthenticator.set(authenticator.get()); if (readOnlyBearerAuth) { authorization.set(authenticator.get().authenticatePull(credential)); } else { @@ -375,10 +378,13 @@ private Authorization refreshBearerAuth(@Nullable String wwwAuthenticate) } } - throw new RegistryAuthenticationFailedException( - registry, - repository, - "server did not return 'WWW-Authenticate: Bearer' header: " + wwwAuthenticate); + eventHandlers.dispatch( + LogEvent.debug( + "server did not return 'WWW-Authenticate: Bearer' header. Actual: " + wwwAuthenticate)); + if (readOnlyBearerAuth) { + return Verify.verifyNotNull(initialBearerAuthenticator.get()).authenticatePull(credential); + } + return Verify.verifyNotNull(initialBearerAuthenticator.get()).authenticatePush(credential); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java index e8fb31aec4..e85e787b24 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.api.RegistryException; +import com.google.cloud.tools.jib.api.RegistryUnauthorizedException; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.global.JibSystemProperties; @@ -168,6 +169,73 @@ public void testAutomaticTokenRefresh() .dispatch(logContains("refreshing bearer auth token")); } + @Test + public void testAutomaticTokenRefresh_badWwwAuthenticateResponse() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 3); + + List responses = + Arrays.asList( + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n", + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Basic realm=foo\n\n", + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\n\n", + "HTTP/1.1 200 OK\nContent-Length: 5678\n\n"); + registry = new TestWebServer(false, responses, responses.size(), true); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + Optional digestAndSize = registryClient.checkBlob(digest); + Assert.assertEquals(5678, digestAndSize.get().getSize()); + + // Verify authServer returned bearer token three times (i.e., refreshed twice) + Assert.assertEquals(3, authServer.getTotalResponsesServed()); + Assert.assertEquals(4, registry.getTotalResponsesServed()); + + Mockito.verify(eventHandlers) + .dispatch( + logContains("server did not return 'WWW-Authenticate: Bearer' header. Actual: Basic")); + Mockito.verify(eventHandlers) + .dispatch( + logContains("server did not return 'WWW-Authenticate: Bearer' header. Actual: null")); + } + + @Test + public void testAutomaticTokenRefresh_refreshLimit() + throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, + RegistryException { + String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}"; + authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 5); + + String bearerAuth = + "HTTP/1.1 401 Unauthorized\nContent-Length: 0\nWWW-Authenticate: Bearer realm=\"" + + authServer.getEndpoint() + + "\"\n\n"; + String unauthorized = "HTTP/1.1 401 Unauthorized\nContent-Length: 0\n\n"; + List responses = + Arrays.asList( + bearerAuth, unauthorized, unauthorized, unauthorized, unauthorized, unauthorized); + registry = new TestWebServer(false, responses, responses.size(), true); + + RegistryClient registryClient = createRegistryClient(null); + Assert.assertTrue(registryClient.doPushBearerAuth()); + + try { + registryClient.checkBlob(digest); + Assert.fail("Should have given up refreshing after 4 attempts"); + } catch (RegistryUnauthorizedException ex) { + Assert.assertEquals(401, ex.getHttpResponseException().getStatusCode()); + Assert.assertEquals(5, authServer.getTotalResponsesServed()); + // 1 response asking to do bearer auth + 4 unauth responses for 4 refresh attempts + 1 final + // unauth response propagated as RegistryUnauthorizedException here + Assert.assertEquals(6, registry.getTotalResponsesServed()); + } + } + @Test public void testConfigureBasicAuth() throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException, diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 2e30d036c3..54901fa6bd 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) + ## 2.0.0 ### Added diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 3c079053e5..c27fcc8a8c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) + ## 2.0.0 ### Added From 4f438d4c66775a34f433eacd5e3758d69ac00d03 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Fri, 7 Feb 2020 14:56:01 -0500 Subject: [PATCH 0895/2020] Deserialize docker inspect JSON (#2271) --- jib-core/CHANGELOG.md | 1 + .../cloud/tools/jib/docker/DockerClient.java | 54 +++++++------------ .../steps/LocalBaseImageStepsTest.java | 15 +++--- .../tools/jib/docker/DockerClientTest.java | 51 +++++++++++++++--- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + 6 files changed, 73 insertions(+), 50 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 9d7b161ab3..50986bf790 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) ## 0.13.0 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java index e5bed9c0c4..cbfaec148b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClient.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.docker; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.http.NotifyingOutputStream; @@ -51,20 +53,23 @@ public class DockerClient { /** * Contains the size, image ID, and diff IDs of an image inspected with {@code docker inspect}. */ + @JsonIgnoreProperties(ignoreUnknown = true) public static class DockerImageDetails implements JsonTemplate { + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class RootFsTemplate implements JsonTemplate { + @JsonProperty("Layers") + private final List layers = Collections.emptyList(); + } + + @JsonProperty("Size") private long size; - private String imageId = ""; - private List diffIds = Collections.emptyList(); - // Required for JSON - public DockerImageDetails() {} + @JsonProperty("Id") + private String imageId = ""; - @VisibleForTesting - public DockerImageDetails(long size, String imageId, List diffIds) { - this.size = size; - this.imageId = imageId; - this.diffIds = diffIds; - } + @JsonProperty("RootFS") + private final RootFsTemplate rootFs = new RootFsTemplate(); public long getSize() { return size; @@ -75,8 +80,8 @@ public DescriptorDigest getImageId() throws DigestException { } public List getDiffIds() throws DigestException { - List processedDiffIds = new ArrayList<>(diffIds.size()); - for (String diffId : diffIds) { + List processedDiffIds = new ArrayList<>(rootFs.layers.size()); + for (String diffId : rootFs.layers) { processedDiffIds.add(DescriptorDigest.fromDigest(diffId.trim())); } return processedDiffIds; @@ -136,18 +141,6 @@ static Function, ProcessBuilder> defaultProcessBuilderFactory( }; } - /** - * Parses the results of {@code docker inspect} into an {@link DockerImageDetails}. - * - * @param inspectOutput the output of the {@code docker inspect} command containing the size, - * image ID, and diff IDs - * @return the {@link DockerImageDetails} - */ - @VisibleForTesting - static DockerImageDetails parseInspectResults(String inspectOutput) throws IOException { - return JsonTemplateMapper.readJson(inspectOutput, DockerImageDetails.class); - } - private static String getStderrOutput(Process process) { try (InputStreamReader stderr = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) { @@ -265,21 +258,12 @@ public void save( public DockerImageDetails inspect(ImageReference imageReference) throws IOException, InterruptedException { Process inspectProcess = - docker( - "inspect", - "-f", - "{\"size\":{{.Size}},\"imageId\":\"{{.Id}}\",\"diffIds\":{{json .RootFS.Layers}}}", - "--type", - "image", - imageReference.toString()); + docker("inspect", "-f", "{{json .}}", "--type", "image", imageReference.toString()); if (inspectProcess.waitFor() != 0) { throw new IOException( "'docker inspect' command failed with error: " + getStderrOutput(inspectProcess)); } - return parseInspectResults( - CharStreams.toString( - new InputStreamReader(inspectProcess.getInputStream(), StandardCharsets.UTF_8)) - .trim()); + return JsonTemplateMapper.readJson(inspectProcess.getInputStream(), DockerImageDetails.class); } /** Runs a {@code docker} command. */ diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java index f9f33e7c89..892226bdbd 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java @@ -24,7 +24,7 @@ import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.event.EventHandlers; import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider; -import com.google.common.collect.ImmutableList; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.io.Resources; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; @@ -133,13 +133,14 @@ public void testCacheDockerImageTar_validTar() throws Exception { @Test public void testGetCachedDockerImage() throws IOException, DigestException, CacheCorruptedException, URISyntaxException { + String dockerInspectJson = + "{\"Size\": 0," + + "\"Id\": \"sha256:066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0\"," + + "\"RootFS\": { \"Layers\": [" + + " \"sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd\"," + + " \"sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e\" ] } }"; DockerImageDetails dockerImageDetails = - new DockerImageDetails( - 0, - "sha256:066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0", - ImmutableList.of( - "sha256:5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd", - "sha256:f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e")); + JsonTemplateMapper.readJson(dockerInspectJson, DockerImageDetails.class); Path cachePath = temporaryFolder.newFolder("cache").toPath(); Files.createDirectories(cachePath.resolve("local/config")); Cache cache = Cache.withDirectory(cachePath); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 6bc1f41dea..7143281f7e 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -20,6 +20,7 @@ import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.docker.DockerClient.DockerImageDetails; import com.google.cloud.tools.jib.image.ImageTarball; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; @@ -257,14 +258,15 @@ public void testSize_fail() throws InterruptedException { } @Test - public void testParseInspectResults() throws DigestException, IOException { - String output = - "{\"size\":488118507," - + "\"imageId\":\"sha256:e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd\"," - + "\"diffIds\":[\"sha256:55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1\"," - + "\"sha256:26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b\"," - + "\"sha256:8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a\"]}\n"; - DockerImageDetails results = DockerClient.parseInspectResults(output); + public void testDockerImageDetails() throws DigestException, IOException { + String json = + "{\"Size\":488118507," + + "\"Id\":\"sha256:e8d00769c8a805a0656dbfd49d4f91cbc2e36d0199f10343d1beba36ecdcb3fd\"," + + "\"RootFS\": { \"Layers\" : [" + + " \"sha256:55e6b89812f369277290d098c1e44c9e85a5ab0286c649f37e66e11074f8ebd1\"," + + " \"sha256:26b1991f37bd5b798e1523f65d7f6aa6961b75515f465cf44123fa0ad3b8961b\"," + + " \"sha256:8bacec4e34468110538ebf108ca8ec0d880a37018a55be91b9670b8e900c593a\"]}}\n"; + DockerImageDetails results = JsonTemplateMapper.readJson(json, DockerImageDetails.class); Assert.assertEquals(488118507, results.getSize()); Assert.assertEquals( DescriptorDigest.fromHash( @@ -281,6 +283,39 @@ public void testParseInspectResults() throws DigestException, IOException { results.getDiffIds()); } + @Test + public void testDockerImageDetails_unknownProperties() throws IOException, DigestException { + String json = + "{\"Unknown\": [ ], \"Structure\": [ { \"Test\": 0 } ], \"Size\": 1234," + + "\"Id\": \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"," + + "\"RootFS\": { \"Someting\": \"unrelated\", \"Layers\": [" + + " \"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\" ] } }"; + DockerImageDetails results = JsonTemplateMapper.readJson(json, DockerImageDetails.class); + Assert.assertEquals(1234, results.getSize()); + Assert.assertEquals( + DescriptorDigest.fromHash( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + results.getImageId()); + Assert.assertEquals( + Arrays.asList( + DescriptorDigest.fromHash( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")), + results.getDiffIds()); + } + + @Test + public void testDockerImageDetails_emptyJson() throws IOException, DigestException { + DockerImageDetails details = JsonTemplateMapper.readJson("{}", DockerImageDetails.class); + Assert.assertEquals(0, details.getSize()); + Assert.assertEquals(Collections.emptyList(), details.getDiffIds()); + try { + details.getImageId(); + Assert.fail(); + } catch (DigestException ex) { + Assert.assertEquals("Invalid digest: ", ex.getMessage()); + } + } + private DockerClient makeDockerSaveClient() { return new DockerClient( subcommand -> { diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 54901fa6bd..48f8cb5cee 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) ## 2.0.0 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index c27fcc8a8c..9e7e23b63e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) +- Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) ## 2.0.0 From d362f5f5d356e116e843f4070d1b4e2aa397a02a Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 7 Feb 2020 16:43:38 -0500 Subject: [PATCH 0896/2020] Improve checkstyle config (#2273) - update checkstyle to 8.29 - improve config modification - add temporary check suppressions so fixes can go in gradually --- build.gradle | 42 +++++++------------ config/checkstyle/checkstyle-suppressions.xml | 38 +++++++++++++++++ 2 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 config/checkstyle/checkstyle-suppressions.xml diff --git a/build.gradle b/build.gradle index 259224fa70..2526826d5f 100644 --- a/build.gradle +++ b/build.gradle @@ -117,39 +117,27 @@ subprojects { /* CHECKSTYLE */ checkstyle { - toolVersion = '8.18' + toolVersion = '8.29' - // use the google checks packaged with the jar + // use google checks from the jar def googleChecks = resources.text.fromArchiveEntry(configurations.checkstyle[0], 'google_checks.xml').asString() - // preserve these, the parser erases them - def xmlVersionNode = googleChecks.find(/<\?xml version.*>/) - def doctypeNode = googleChecks.find(/(?s)]*>/) - - def parser = new XmlParser() - parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) - parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) - def rootModule = parser.parseText(googleChecks) - - // force each module within the root checker to only work on "properties, xml", ignore "java" - def fileExtensionNode = parser.parseText('') - rootModule.module*.append(fileExtensionNode) - - // add in copyright header check on only java files - def copyrightNode = parser.parseText(''' - - - - - - ''') - rootModule.append(copyrightNode) + // set the location of the suppressions file referenced in google_checks.xml + configProperties['org.checkstyle.google.suppressionfilter.config'] = getConfigDirectory().file("checkstyle-suppressions.xml").get().toString() - def xmlOutput = new StringWriter() - new XmlNodePrinter(new PrintWriter(xmlOutput)).print(rootModule) + // add in copyright header check on only java files (replace the last in file) + def copyrightChecks = ''' + + + + + + + ''' + googleChecks = googleChecks.substring(0, googleChecks.lastIndexOf("")) + copyrightChecks // this is the actual checkstyle config - config = resources.text.fromString(xmlVersionNode + doctypeNode + xmlOutput.toString()) + config = resources.text.fromString(googleChecks) maxErrors = 0 maxWarnings = 0 diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 0000000000..dc3965226f --- /dev/null +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1929947fe43e22cc3ad2fd6cb76c2b8659bb966c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Mon, 10 Feb 2020 12:13:20 -0500 Subject: [PATCH 0897/2020] Fix incorrect skaffold init output in multi-module projects (#2263) --- jib-maven-plugin/CHANGELOG.md | 1 + .../tools/jib/maven/skaffold/InitMojo.java | 8 ++-- .../cloud/tools/jib/maven/TestProject.java | 21 +++++------ .../jib/maven/skaffold/InitMojoTest.java | 26 +++++++++++-- .../maven/projects/spring-boot-multi/pom.xml | 37 +++++++++++++++++++ .../spring-boot-multi/service-1/pom.xml | 26 +++++++++++++ .../src/main/java/com/test/HelloWorld.java | 23 ++++++++++++ .../spring-boot-multi/service-2/pom.xml | 26 +++++++++++++ .../src/main/java/com/test/HelloWorld.java | 23 ++++++++++++ 9 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/src/main/java/com/test/HelloWorld.java create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/pom.xml create mode 100644 jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/src/main/java/com/test/HelloWorld.java diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 9e7e23b63e..162a23bd3c 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed a `skaffold init` issue with projects containing submodules specifying different parent poms. ([#2262](https://github.com/GoogleContainerTools/jib/issues/2262)) - Fixed authentication failure with error `server did not return 'WWW-Authenticate: Bearer' header` in certain cases (for example, on OpenShift). ([#2258](https://github.com/GoogleContainerTools/jib/issues/2258)) - Fixed an issue where using local Docker images (by `docker://...`) on Windows caused an error. ([#2270](https://github.com/GoogleContainerTools/jib/issues/2270)) diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java index da03dec374..cdeb62bdc6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/skaffold/InitMojo.java @@ -39,16 +39,14 @@ public class InitMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException { checkJibVersion(); MavenProject project = getProject(); - // Ignore parent projects - if (project.getModules().size() > 0) { + // Ignore pom projects + if ("pom".equals(project.getPackaging())) { return; } SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(); skaffoldInitOutput.setImage(getTargetImage()); - if (project.getParent() != null && project.getParent().getFile() != null) { - skaffoldInitOutput.setProject(project.getArtifactId()); - } + skaffoldInitOutput.setProject(project.getGroupId() + ":" + project.getArtifactId()); System.out.println(); System.out.println("BEGIN JIB JSON"); try { diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java index 649796e459..dd415d92e6 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/TestProject.java @@ -16,6 +16,7 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import java.io.Closeable; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -23,8 +24,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.maven.it.util.ResourceExtractor; import org.junit.rules.TemporaryFolder; @@ -76,14 +75,14 @@ private void copyProject() throws IOException { properties.load(Files.newInputStream(gradleProperties)); String pluginVersion = properties.getProperty("version"); - try (Stream files = Files.list(projectRoot)) { - for (Path pomXml : files.filter(TestProject::isPomXml).collect(Collectors.toList())) { - Files.write( - pomXml, - new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) - .replace("@@PluginVersion@@", pluginVersion) - .getBytes(StandardCharsets.UTF_8)); - } - } + new DirectoryWalker(projectRoot) + .filter(TestProject::isPomXml) + .walk( + pomXml -> + Files.write( + pomXml, + new String(Files.readAllBytes(pomXml), StandardCharsets.UTF_8) + .replace("@@PluginVersion@@", pluginVersion) + .getBytes(StandardCharsets.UTF_8))); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java index ccbaf0cecf..f1b68c5273 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/skaffold/InitMojoTest.java @@ -41,6 +41,9 @@ public class InitMojoTest { @ClassRule public static final TestProject multiTestProject = new TestProject("multi"); + @ClassRule + public static final TestProject springTestProject = new TestProject("spring-boot-multi"); + /** * Verifies that the files task succeeded and returns the list of JSON strings printed by the * task. @@ -78,7 +81,7 @@ public void testFilesMojo_singleModule() throws IOException, VerificationExcepti SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertNull(skaffoldInitOutput.getProject()); + Assert.assertEquals("com.test:hello-world", skaffoldInitOutput.getProject()); } @Test @@ -88,14 +91,29 @@ public void testFilesMojo_multiModule() throws IOException, VerificationExceptio SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertEquals("simple-service", skaffoldInitOutput.getProject()); + Assert.assertEquals("com.jib.test:simple-service", skaffoldInitOutput.getProject()); skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertEquals("lib", skaffoldInitOutput.getProject()); + Assert.assertEquals("com.jib.test:lib", skaffoldInitOutput.getProject()); skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(2)); Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); - Assert.assertEquals("complex-service", skaffoldInitOutput.getProject()); + Assert.assertEquals("com.jib.test:complex-service", skaffoldInitOutput.getProject()); + } + + @Test + public void testFilesMojo_multiModule_differentParent() + throws IOException, VerificationException { + List outputs = getJsons(springTestProject); + Assert.assertEquals(2, outputs.size()); + + SkaffoldInitOutput skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(0)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("org.springframework.boot:service-1", skaffoldInitOutput.getProject()); + + skaffoldInitOutput = new SkaffoldInitOutput(outputs.get(1)); + Assert.assertEquals("testimage", skaffoldInitOutput.getImage()); + Assert.assertEquals("org.springframework.boot:service-2", skaffoldInitOutput.getProject()); } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/pom.xml new file mode 100644 index 0000000000..febe0a0199 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + com.jib.test + spring-boot-multi + pom + 1.0.0.TEST-SNAPSHOT + multimodule + + + 1.8 + 1.8 + + + + + service-1 + service-2 + + + + + + + com.google.cloud.tools + jib-maven-plugin + @@PluginVersion@@ + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/pom.xml new file mode 100644 index 0000000000..64d639675d --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + service-1 + 1.0.0.TEST-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + + + + com.google.cloud.tools + jib-maven-plugin + @@PluginVersion@@ + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-1/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/pom.xml b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/pom.xml new file mode 100644 index 0000000000..52f21c4029 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + service-2 + 1.0.0.TEST-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + + + + com.google.cloud.tools + jib-maven-plugin + @@PluginVersion@@ + + + + + diff --git a/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/src/main/java/com/test/HelloWorld.java b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/spring-boot-multi/service-2/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} From 6f5dd9830c8d9cf1367ad7c2cba96269a6a1548f Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 10 Feb 2020 12:24:09 -0500 Subject: [PATCH 0898/2020] Use POSIX long file mode to support enormous files (#2276) --- .../java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index c67e85ae77..006963ae02 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -46,6 +46,7 @@ public void writeAsTarArchiveTo(OutputStream out) throws IOException { new TarArchiveOutputStream(out, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + tarArchiveOutputStream.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); for (Map.Entry entry : archiveMap.entrySet()) { tarArchiveOutputStream.putArchiveEntry(entry.getKey()); entry.getValue().writeTo(tarArchiveOutputStream); From f52a3163914352ffcbd4a36c73b08a8f18540cb2 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 10 Feb 2020 12:34:25 -0500 Subject: [PATCH 0899/2020] Add "Launch Problems" section to FAQ and highlight symlink issue (#2277) Co-Authored-By: Chanseok Oh --- docs/faq.md | 78 ++++++++++++++++++++++++++----------- jib-gradle-plugin/README.md | 4 +- jib-maven-plugin/README.md | 4 +- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 78e3de01d9..4ce4a2e5e6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -29,15 +29,18 @@ If a question you have is not answered below, please [submit an issue](/../../is [I would like to run my application with a javaagent.](#i-would-like-to-run-my-application-with-a-javaagent)\ [How can I tag my image with a timestamp?](#how-can-i-tag-my-image-with-a-timestamp) -**Common Problems**\ +**Build Problems**\ [How can I diagnose problems pulling or pushing from remote registries?](#how-can-i-diagnose-problems-pulling-or-pushing-from-remote-registries)\ [What should I do when the registry responds with Forbidden or DENIED?](#what-should-i-do-when-the-registry-responds-with-forbidden-or-denied)\ [What should I do when the registry responds with UNAUTHORIZED?](#what-should-i-do-when-the-registry-responds-with-unauthorized)\ [How do I configure a proxy?](#how-do-i-configure-a-proxy)\ [How can I examine network traffic?](#how-can-i-examine-network-traffic)\ -[How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib)\ +[How do I view debug logs for Jib?](#how-do-i-view-debug-logs-for-jib) + +**Launch Problems**\ [I am seeing `ImagePullBackoff` on my pods.](#i-am-seeing-imagepullbackoff-on-my-pods-in-minikube)\ -[I am seeing `Method Not Found` or `Class Not Found` errors when building.](#i-am-seeing-method-not-found-or-class-not-found-errors-when-building) +[I am seeing `Method Not Found` or `Class Not Found` errors when building.](#i-am-seeing-method-not-found-or-class-not-found-errors-when-building)\ +[Why won't my container start?](#why-wont-my-container-start) --- @@ -464,7 +467,7 @@ Some plugins, such as the [Docker Prepare Gradle Plugin](https://github.com/gcla To inspect the image that is produced from the build using Docker, you can use commands such as `docker inspect your/image:tag` to view the image configuration, or you can also download the image using `docker save` to manually inspect the container image. Other tools, such as [dive](https://github.com/wagoodman/dive), provide nicer UI to inspect the image. -## Common Problems +## Build Problems ### How can I diagnose problems pulling or pushing from remote registries? @@ -526,6 +529,41 @@ If you encounter issues interacting with a registry other than `UNAUTHORIZED`, c Jib currently requires configuring your build tool to use the appropriate [Java networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) (`https.proxyHost`, `https.proxyPort`, `https.proxyUser`, `https.proxyPassword`). + +### How can I examine network traffic? + +It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jib.serialize` property. + +To see the HTTP traffic, create a `logging.properties` file with the following: +``` +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=ALL + +# CONFIG hides authentication data +# ALL includes authentication data +com.google.api.client.http.level=CONFIG +``` + +And then launch your build tool as follows: +```sh +mvn -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... +``` +or +```sh +gradle -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... +``` + +You may wish to enable the debug logs too (`-X` for Maven, or `--debug` for Gradle). + +### How do I view debug logs for Jib? + +Maven: use `mvn -X -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. + +Gradle: use `gradle --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. + + +## Launch problems + ### I am seeing `ImagePullBackoff` on my pods (in [minikube](https://github.com/kubernetes/minikube)). When you use your private image built with Jib in a [Kubernetes cluster](kubernetes.io), the cluster needs to be configured with credentials to pull the image. This involves 1) creating a [Secret](https://kubernetes.io/docs/concepts/configuration/secret/), and 2) using the Secret as [`imagePullSecrets`](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account). @@ -576,31 +614,25 @@ plugins { } ``` -### How can I examine network traffic? +### Why won't my container start? -It can be useful to examine network traffic to diagnose connectivity issues. Jib uses the Google HTTP client library to interact with registries which logs HTTP requests using the JVM-provided `java.util.logging` facilities. It is very helpful to serialize Jib's actions using the `jib.serialize` property. +There are some common reasons why containers fail on launch. -To see the HTTP traffic, create a `logging.properties` file with the following: -``` -handlers = java.util.logging.ConsoleHandler -java.util.logging.ConsoleHandler.level=ALL +#### My shell script won't run + +The default base image used by Jib, ([`distoless/java`](https://github.com/GoogleContainerTools/distroless/tree/master/java)), does not include a shell, and thus shell scripts won't launch. -# CONFIG hides authentication data -# ALL includes authentication data -com.google.api.client.http.level=CONFIG -``` +Solution: use a different base image with a shell. -And then launch your build tool as follows: -```sh -mvn -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... +#### The container fails with `exec` errors + +A Jib user reported an error launching their container: ``` -or -```sh -gradle -Djava.util.logging.config.file=path/to/log.properties -Djib.serialize=true -Djib.console=plain ... +standard_init_linux.go:211 exec user process caused "no such file or directory" ``` -### How do I view debug logs for Jib? +On examining the container structure with [Dive](https://github.com/wagoodman/dive), the user discovered that the contents of the `/lib` directory had disappeared. -Maven: use `mvn -X -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. +The user had used Jib's ability to install extra files into the image ([Maven](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#adding-arbitrary-files-to-the-image), [Gradle](https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#adding-arbitrary-files-to-the-image)) to install a library file by placing it in `src/main/jib/lib/libfoo.so`. This would normally cause the `libfoo.so` to be installed in the image as `/lib/libfoo.so`. But `/lib` and `/lib64` in the user's base image were symbolic links. Jib does not follow such symbolic links when creating the image. And at container initialization time, Docker treats these symlinks as a file, and thus the symbolic link was replaced with `/lib` as a new directory. As a result, none of the system shared libraries were resolved and dynamically-linked programs failed. -Gradle: use `gradle --debug -Djib.serialize=true` to enable more detailed logging and serialize Jib's actions. +Solution: The user installed the file in a different location. diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index ecba101970..46c912a7d6 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -332,6 +332,8 @@ Prefix | Example | Type You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). +Note that Jib does not follow symbolic links in the container image. If a symbolic link is present, _it will be removed_ prior to placing the files and directories. + You can configure different directories by using the `jib.extraDirectories.paths` parameter in your `build.gradle`: ```groovy jib { @@ -354,8 +356,6 @@ jib { } ``` -Note that Jib does not follow symbolic links. If a symbolic link is present, it will be removed prior to placing the files and directories. - ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 1ea25a7d6d..b242bfc50e 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -395,6 +395,8 @@ Prefix | Example | Type You can add arbitrary, non-classpath files to the image by placing them in a `src/main/jib` directory. This will copy all files within the `jib` folder to the image's root directory, maintaining the same structure (e.g. if you have a text file at `src/main/jib/dir/hello.txt`, then your image will contain `/dir/hello.txt` after being built with Jib). +Note that Jib does not follow symbolic links in the container image. If a symbolic link is present, _it will be removed_ prior to placing the files and directories. + You can configure different directories by using the `` parameter in your `pom.xml`: ```xml @@ -428,8 +430,6 @@ Alternatively, the `` parameter can be used as an object to se ``` -Note that Jib does not follow symbolic links. If a symbolic link is present, it will be removed prior to placing the files and directories. - ### Authentication Methods Pushing/pulling from private registries require authorization credentials. These can be [retrieved using Docker credential helpers](#using-docker-credential-helpers) or [defined in your Maven settings](#using-maven-settings). If you do not define credentials explicitly, Jib will try to [use credentials defined in your Docker config](/../../issues/101) or infer common credential helpers. From fe5acfd9c3f744e0d181860b5daf5b0599535534 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 10 Feb 2020 16:00:57 -0500 Subject: [PATCH 0900/2020] Fix some pending checkstyle checks (#2279) * Fix some pending checkstyle checks - EmptyBlock - EmptyLineSeparator - MultipleVariableDeclarations - AtclauseOrder - ArrayTypeStyle --- config/checkstyle/checkstyle-suppressions.xml | 5 --- .../cloud/tools/jib/api/Containerizer.java | 1 + .../tools/jib/api/JibContainerBuilder.java | 1 + .../jib/api/JibContainerDescription.java | 1 + .../google/cloud/tools/jib/cache/Retry.java | 2 +- .../tools/jib/configuration/BuildContext.java | 2 + .../tools/jib/http/NotifyingOutputStream.java | 2 +- .../jib/http/FailoverHttpClientTest.java | 40 ++++++++++++++----- .../tools/jib/image/ImageTarballTest.java | 6 ++- .../common/logging/PlainConsoleLogger.java | 3 +- 10 files changed, 43 insertions(+), 20 deletions(-) diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index dc3965226f..9558470a6a 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -17,13 +17,8 @@ - - - - - diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java index 833295ed5d..328a45ddd7 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java @@ -185,6 +185,7 @@ public Containerizer setBaseImageLayersCache(Path cacheDirectory) { baseImageLayersCacheDirectory = cacheDirectory; return this; } + /** * Sets the directory to use for caching application layers. This cache can be shared between * multiple images. If not set, a temporary directory will be used as the application layers diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index ad0182cdf3..b38bcd8a26 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -322,6 +322,7 @@ public JibContainerBuilder addVolume(AbsoluteUnixPath volume) { containerConfigurationBuilder.addVolume(volume); return this; } + /** * Sets the ports to expose from the container. Ports exposed will allow ingress traffic. This * replaces any previously-set exposed ports. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java index 1b89c4e6a4..115b0d266e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.cloud.tools.jib.api; import com.google.cloud.tools.jib.configuration.ContainerConfiguration; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java index 68697fd7c1..6ae979cc93 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java @@ -53,8 +53,8 @@ public interface Action { * Create a retryable action. * * @param action the action to be run - * @return the instance * @param the class of exceptions that may be thrown + * @return the instance */ public static Retry action(Action action) { return new Retry(action); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java index 9f0d3c67ce..7eb461e07c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java @@ -195,6 +195,7 @@ public Builder setAlwaysCacheBaseImage(boolean alwaysCacheBaseImage) { this.alwaysCacheBaseImage = alwaysCacheBaseImage; return this; } + /** * Sets the layers to build. * @@ -429,6 +430,7 @@ public ExecutorService getExecutorService() { public Cache getBaseImageLayersCache() { return baseImageLayersCache; } + /** * Gets the {@link Cache} for application layers. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java index 8b7bf1f4be..74023ec141 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java @@ -57,7 +57,7 @@ public void write(byte[] byteArray) throws IOException { } @Override - public void write(byte byteArray[], int offset, int length) throws IOException { + public void write(byte[] byteArray, int offset, int length) throws IOException { underlyingOutputStream.write(byteArray, offset, length); countAndCallListener(length); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java index bca07ea172..e58b87ce8a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java @@ -98,7 +98,9 @@ public void testPut() throws IOException { @Test public void testHttpTimeout_doNotSetByDefault() throws IOException { - try (Response ignored = newHttpClient(false, false).get(fakeUrl.toURL(), fakeRequest(null))) {} + try (Response ignored = newHttpClient(false, false).get(fakeUrl.toURL(), fakeRequest(null))) { + // intentionally empty + } Mockito.verify(mockHttpRequest, Mockito.never()).setConnectTimeout(Mockito.anyInt()); Mockito.verify(mockHttpRequest, Mockito.never()).setReadTimeout(Mockito.anyInt()); @@ -107,7 +109,9 @@ public void testHttpTimeout_doNotSetByDefault() throws IOException { @Test public void testHttpTimeout() throws IOException { FailoverHttpClient httpClient = newHttpClient(false, false); - try (Response ignored = httpClient.get(fakeUrl.toURL(), fakeRequest(5982))) {} + try (Response ignored = httpClient.get(fakeUrl.toURL(), fakeRequest(5982))) { + // intentionally empty + } Mockito.verify(mockHttpRequest).setConnectTimeout(5982); Mockito.verify(mockHttpRequest).setReadTimeout(5982); @@ -273,7 +277,9 @@ public void testGet_doNotSendCredentialsOverHttp() throws IOException { .thenReturn(mockHttpResponse); // respond when connected through 80 try (Response response = - insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) {} + insecureHttpClient.get(new URL("https://insecure"), fakeRequest(null))) { + // intentionally empty + } verifyCapturedUrls("https://insecure", "http://insecure"); @@ -289,7 +295,9 @@ public void testGet_sendCredentialsOverHttp() throws IOException { FailoverHttpClient insecureHttpClient = newHttpClient(true, true); // sendCredentialsOverHttp try (Response response = - insecureHttpClient.get(new URL("http://plain.http"), fakeRequest(null))) {} + insecureHttpClient.get(new URL("http://plain.http"), fakeRequest(null))) { + // intentionally empty + } Assert.assertEquals(1, urlCaptor.getAllValues().size()); @@ -303,7 +311,9 @@ public void testGet_originalRequestHeaderUntouchedWhenClearingHeader() throws IO FailoverHttpClient insecureHttpClient = newHttpClient(true, false); Request request = fakeRequest(null); - try (Response response = insecureHttpClient.get(new URL("http://plain.http"), request)) {} + try (Response response = insecureHttpClient.get(new URL("http://plain.http"), request)) { + // intentionally empty + } Assert.assertEquals(1, urlCaptor.getAllValues().size()); Assert.assertEquals(1, httpHeadersCaptor.getAllValues().size()); @@ -333,7 +343,9 @@ public void testFollowFailoverHistory_insecureHttps() throws IOException { .thenThrow(new SSLException("")) .thenReturn(mockHttpResponse); try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); - Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) {} + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) { + // intentionally empty + } Mockito.verify(mockHttpRequest, Mockito.times(1)).execute(); Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); @@ -351,7 +363,9 @@ public void testFollowFailoverHistory_httpFailoverByConnectionError() throws IOE .thenReturn(mockHttpResponse); try (Response response1 = httpClient.get(new URL("https://url"), fakeRequest(null)); - Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) {} + Response response2 = httpClient.post(new URL("https://url"), fakeRequest(null))) { + // intentionally empty + } Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); verifyCapturedUrls("https://url", "http://url", "http://url"); @@ -368,7 +382,9 @@ public void testFollowFailoverHistory_httpFailover() throws IOException { Mockito.when(mockInsecureHttpRequest.execute()).thenThrow(new SSLException("")); try (Response response1 = httpClient.get(new URL("https://url:123"), fakeRequest(null)); - Response response2 = httpClient.post(new URL("https://url:123"), fakeRequest(null))) {} + Response response2 = httpClient.post(new URL("https://url:123"), fakeRequest(null))) { + // intentionally empty + } Mockito.verify(mockHttpRequest, Mockito.times(3)).execute(); Mockito.verify(mockInsecureHttpRequest, Mockito.times(1)).execute(); @@ -388,7 +404,9 @@ public void testFollowFailoverHistory_portsDifferent() throws IOException { .thenReturn(mockHttpResponse); try (Response response1 = httpClient.get(new URL("https://url:1"), fakeRequest(null)); - Response response2 = httpClient.post(new URL("https://url:2"), fakeRequest(null))) {} + Response response2 = httpClient.post(new URL("https://url:2"), fakeRequest(null))) { + // intentionally empty + } Mockito.verify(mockHttpRequest, Mockito.times(2)).execute(); Mockito.verify(mockInsecureHttpRequest, Mockito.times(2)).execute(); @@ -441,7 +459,9 @@ private Request fakeRequest(Integer httpTimeout) { private void verifyCall(String httpMethod, CallFunction callFunction) throws IOException { FailoverHttpClient httpClient = newHttpClient(false, false); - try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) {} + try (Response ignored = callFunction.call(httpClient, fakeUrl.toURL(), fakeRequest(null))) { + // intentionally empty + } Assert.assertEquals( "fake.accept,another.fake.accept", httpHeadersCaptor.getValue().getAccept()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java index 2b11e2b360..0ad3c8cafc 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java @@ -56,8 +56,10 @@ @RunWith(MockitoJUnitRunner.class) public class ImageTarballTest { - private Path fileA, fileB; - private DescriptorDigest fakeDigestA, fakeDigestB; + private Path fileA; + private Path fileB; + private DescriptorDigest fakeDigestA; + private DescriptorDigest fakeDigestB; @Mock private Layer mockLayer1; @Mock private Layer mockLayer2; diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java index 73a3e74557..0b1fb1335f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/logging/PlainConsoleLogger.java @@ -30,7 +30,8 @@ class PlainConsoleLogger implements ConsoleLogger { /** * Creates a {@link PlainConsoleLogger}. * - * @param messageConsumers map from each {@link Level} to a log message {@link Consumer} + * @param messageConsumers map from each {@link Level} to a log message {@link Consumer} of type + * {@code Consumer} * @param singleThreadedExecutor a {@link SingleThreadedExecutor} to ensure that all messages are * logged in a sequential, deterministic order */ From a14cc9331bcb795c8eebf13d7130019d5fe83a13 Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 10 Feb 2020 18:04:56 -0500 Subject: [PATCH 0901/2020] More checkstyle fixes (#2280) * More checkstyle fixes - AbbreviationAsWordInName - WhitespaceAround - NonEmptyAtclauseDescription --- config/checkstyle/checkstyle-suppressions.xml | 3 -- ...DockerCredentialHelperIntegrationTest.java | 2 +- .../hash/CountingDigestOutputStreamTest.java | 2 +- .../image/ReproducibleLayerBuilderTest.java | 30 +++++++++---------- .../json/V22ManifestListTemplateTest.java | 14 ++++----- .../tools/jib/registry/BlobPusherTest.java | 22 +++++++------- .../jib/maven/MavenProjectProperties.java | 6 ++-- .../plugins/common/TimerEventHandlerTest.java | 2 +- 8 files changed, 40 insertions(+), 41 deletions(-) diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 9558470a6a..7157e7687f 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -16,11 +16,8 @@ - - - diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java index 7bdb07a790..77645afb17 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java @@ -35,7 +35,7 @@ public class DockerCredentialHelperIntegrationTest { /** Tests retrieval via {@code docker-credential-gcr} CLI. */ @Test - public void testRetrieveGCR() + public void testRetrieveGcr() throws IOException, CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, URISyntaxException, InterruptedException { new Command(GCR_CREDENTIAL_HELPER, "store") diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java index b0e4594a7a..51a628d1b7 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java @@ -34,7 +34,7 @@ /** Tests for {@link CountingDigestOutputStream}. */ public class CountingDigestOutputStreamTest { - private final Map KNOWN_SHA256_HASHES = + private static final ImmutableMap KNOWN_SHA256_HASHES = ImmutableMap.of( "crepecake", "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c", diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index 3dca6d83c6..2427792cb5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -197,39 +197,39 @@ public void testBuild_parentDirBehavior() throws IOException { Path testRoot = temporaryFolder.getRoot().toPath(); // the path doesn't really matter on source files, but these are structured - Path parent = Files.createDirectories(testRoot.resolve("aaa")); + Path parent = Files.createDirectories(testRoot.resolve("dirA")); Path fileA = Files.createFile(parent.resolve("fileA")); - Path ignoredParent = Files.createDirectories(testRoot.resolve("bbb-ignored")); + Path ignoredParent = Files.createDirectories(testRoot.resolve("dirB-ignored")); Path fileB = Files.createFile(ignoredParent.resolve("fileB")); Path fileC = - Files.createFile(Files.createDirectories(testRoot.resolve("ccc-absent")).resolve("fileC")); + Files.createFile(Files.createDirectories(testRoot.resolve("dirC-absent")).resolve("fileC")); Blob layer = new ReproducibleLayerBuilder( ImmutableList.of( new LayerEntry( parent, - AbsoluteUnixPath.get("/root/aaa"), + AbsoluteUnixPath.get("/root/dirA"), FilePermissions.fromOctalString("111"), Instant.ofEpochSecond(10)), new LayerEntry( fileA, - AbsoluteUnixPath.get("/root/aaa/fileA"), + AbsoluteUnixPath.get("/root/dirA/fileA"), FilePermissions.fromOctalString("222"), Instant.ofEpochSecond(20)), new LayerEntry( fileB, - AbsoluteUnixPath.get("/root/bbb-ignored/fileB"), + AbsoluteUnixPath.get("/root/dirB-ignored/fileB"), FilePermissions.fromOctalString("333"), Instant.ofEpochSecond(30)), new LayerEntry( ignoredParent, - AbsoluteUnixPath.get("/root/bbb-ignored"), + AbsoluteUnixPath.get("/root/dirB-ignored"), FilePermissions.fromOctalString("444"), Instant.ofEpochSecond(40)), new LayerEntry( fileC, - AbsoluteUnixPath.get("/root/ccc-absent/file3"), + AbsoluteUnixPath.get("/root/dirC-absent/file3"), FilePermissions.fromOctalString("555"), Instant.ofEpochSecond(50)))) .build(); @@ -246,17 +246,17 @@ public void testBuild_parentDirBehavior() throws IOException { Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); // parentAAA (custom permissions, custom timestamp) - TarArchiveEntry rootParentAAA = in.getNextTarEntry(); - Assert.assertEquals(040111, rootParentAAA.getMode()); - Assert.assertEquals(Instant.ofEpochSecond(10), rootParentAAA.getModTime().toInstant()); + TarArchiveEntry rootParentA = in.getNextTarEntry(); + Assert.assertEquals(040111, rootParentA.getMode()); + Assert.assertEquals(Instant.ofEpochSecond(10), rootParentA.getModTime().toInstant()); // skip over fileA in.getNextTarEntry(); // parentBBB (default permissions - ignored custom permissions, since fileB added first) - TarArchiveEntry rootParentBBB = in.getNextTarEntry(); + TarArchiveEntry rootParentB = in.getNextTarEntry(); // TODO (#1650): we want 040444 here. - Assert.assertEquals(040755, rootParentBBB.getMode()); + Assert.assertEquals(040755, rootParentB.getMode()); // TODO (#1650): we want Instant.ofEpochSecond(40) here. Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); @@ -264,8 +264,8 @@ public void testBuild_parentDirBehavior() throws IOException { in.getNextTarEntry(); // parentCCC (default permissions - no entry provided) - TarArchiveEntry rootParentCCC = in.getNextTarEntry(); - Assert.assertEquals(040755, rootParentCCC.getMode()); + TarArchiveEntry rootParentC = in.getNextTarEntry(); + Assert.assertEquals(040755, rootParentC.getMode()); Assert.assertEquals(Instant.ofEpochSecond(1), root.getModTime().toInstant()); // we don't care about fileC diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java index 87002549f8..c04ab5f291 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java @@ -40,20 +40,20 @@ public void testFromJson() throws IOException, URISyntaxException { List manifests = manifestListJson.getManifests(); Assert.assertEquals(3, manifests.size()); - List validPlatformPPC = manifestListJson.getDigestsForPlatform("ppc64le", "linux"); - Assert.assertEquals(1, validPlatformPPC.size()); + List validPlatformPpc = manifestListJson.getDigestsForPlatform("ppc64le", "linux"); + Assert.assertEquals(1, validPlatformPpc.size()); Assert.assertEquals( "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", - validPlatformPPC.get(0)); + validPlatformPpc.get(0)); - List validPlatformAMD = manifestListJson.getDigestsForPlatform("amd64", "linux"); - Assert.assertEquals(2, validPlatformAMD.size()); + List validPlatformAmd = manifestListJson.getDigestsForPlatform("amd64", "linux"); + Assert.assertEquals(2, validPlatformAmd.size()); Assert.assertEquals( "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", - validPlatformAMD.get(0)); + validPlatformAmd.get(0)); Assert.assertEquals( "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", - validPlatformAMD.get(1)); + validPlatformAmd.get(1)); List invalidArch = manifestListJson.getDigestsForPlatform("amd72", "linux"); Assert.assertEquals(0, invalidArch.size()); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java index b5e302506e..07733dfe54 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java @@ -48,7 +48,7 @@ public class BlobPusherTest { private static final String TEST_BLOB_CONTENT = "some BLOB content"; private static final Blob TEST_BLOB = Blobs.from(TEST_BLOB_CONTENT); - @Mock private URL mockURL; + @Mock private URL mockUrl; @Mock private Response mockResponse; private DescriptorDigest fakeDescriptorDigest; @@ -164,7 +164,7 @@ public void testInitializer_getActionDescription() { @Test public void testWriter_getContent() throws IOException { LongAdder byteCount = new LongAdder(); - BlobHttpContent body = testBlobPusher.writer(mockURL, byteCount::add).getContent(); + BlobHttpContent body = testBlobPusher.writer(mockUrl, byteCount::add).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); @@ -179,7 +179,7 @@ public void testWriter_getContent() throws IOException { @Test public void testWriter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.writer(mockURL, ignored -> {}).getAccept().size()); + Assert.assertEquals(0, testBlobPusher.writer(mockUrl, ignored -> {}).getAccept().size()); } @Test @@ -190,7 +190,7 @@ public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), - testBlobPusher.writer(mockURL, ignored -> {}).handleResponse(mockResponse)); + testBlobPusher.writer(mockUrl, ignored -> {}).handleResponse(mockResponse)); } @Test @@ -201,30 +201,30 @@ public void testWriter_getApiRoute() throws MalformedURLException { @Test public void testWriter_getHttpMethod() { - Assert.assertEquals("PATCH", testBlobPusher.writer(mockURL, ignored -> {}).getHttpMethod()); + Assert.assertEquals("PATCH", testBlobPusher.writer(mockUrl, ignored -> {}).getHttpMethod()); } @Test public void testWriter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.writer(mockURL, ignored -> {}).getActionDescription()); + testBlobPusher.writer(mockUrl, ignored -> {}).getActionDescription()); } @Test public void testCommitter_getContent() { - Assert.assertNull(testBlobPusher.committer(mockURL).getContent()); + Assert.assertNull(testBlobPusher.committer(mockUrl).getContent()); } @Test public void testCommitter_GetAccept() { - Assert.assertEquals(0, testBlobPusher.committer(mockURL).getAccept().size()); + Assert.assertEquals(0, testBlobPusher.committer(mockUrl).getAccept().size()); } @Test public void testCommitter_handleResponse() throws IOException, RegistryException { Assert.assertNull( - testBlobPusher.committer(mockURL).handleResponse(Mockito.mock(Response.class))); + testBlobPusher.committer(mockUrl).handleResponse(Mockito.mock(Response.class))); } @Test @@ -236,13 +236,13 @@ public void testCommitter_getApiRoute() throws MalformedURLException { @Test public void testCommitter_getHttpMethod() { - Assert.assertEquals("PUT", testBlobPusher.committer(mockURL).getHttpMethod()); + Assert.assertEquals("PUT", testBlobPusher.committer(mockUrl).getHttpMethod()); } @Test public void testCommitter_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, - testBlobPusher.committer(mockURL).getActionDescription()); + testBlobPusher.committer(mockUrl).getActionDescription()); } } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java index 4d1a5aea1f..ac88a8ea04 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenProjectProperties.java @@ -437,13 +437,15 @@ Path getWarArtifact() { } /** - * Gets the path of the JAR that the Maven JAR Plugin generates. + * Gets the path of the JAR that the Maven JAR Plugin generates. Will also make copies of jar + * files with non-conforming names like those produced by springboot -- myjar.jar.original -> + * myjar.original.jar. * *

https://maven.apache.org/plugins/maven-jar-plugin/jar-mojo.html * https://github.com/apache/maven-jar-plugin/blob/80f58a84aacff6e671f5a601d62a3a3800b507dc/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java#L177 * * @return the path of the JAR - * @throws IOException + * @throws IOException if copying jars with non-conforming names fails */ @VisibleForTesting Path getJarArtifact() throws IOException { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java index df216af6fe..a2062bbd7e 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/TimerEventHandlerTest.java @@ -30,7 +30,7 @@ public class TimerEventHandlerTest { private final Deque logMessageQueue = new ArrayDeque<>(); - private final TimerEvent.Timer ROOT_TIMER = Optional::empty; + private static final TimerEvent.Timer ROOT_TIMER = Optional::empty; @Test public void testAccept() { From 7217920c4892383b9fcb77a950cd080a576a265b Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 10 Feb 2020 18:10:33 -0500 Subject: [PATCH 0902/2020] Add slf4j implementation to tests (#2281) Isolated to this specific line: com.google.cloud.tools.jib.maven.TestRepository:51 Something in the plexus libraries is loading the slf4j logger, this just cleans up the error message from not having an actual binding. ``` com.google.cloud.tools.jib.maven.MavenProjectPropertiesTest > testGetJarArtifact STANDARD_ERROR SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. ``` I figured using a simple logger (vs nop-logger) was better so we could get some log feedback if it happened to pop up. --- jib-maven-plugin/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jib-maven-plugin/build.gradle b/jib-maven-plugin/build.gradle index ddedf9744a..a5bb807507 100644 --- a/jib-maven-plugin/build.gradle +++ b/jib-maven-plugin/build.gradle @@ -30,6 +30,8 @@ dependencies { testImplementation 'org.apache.maven.shared:maven-verifier:1.6' testImplementation 'org.apache.maven:maven-compat:3.5.4' + testImplementation 'org.slf4j:slf4j-api:1.7.30' + testImplementation 'org.slf4j:slf4j-simple:1.7.30' testImplementation project(path:':jib-plugins-common', configuration:'tests') integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') From ad3f4bd892d1da544ace25419f59ffc0221e29ed Mon Sep 17 00:00:00 2001 From: Appu Date: Mon, 10 Feb 2020 18:31:15 -0500 Subject: [PATCH 0903/2020] Even more checkstyle fixes (#2282) - EmptyCatchBlock - MissingSwitchDefault --- config/checkstyle/checkstyle-suppressions.xml | 2 -- .../com/google/cloud/tools/jib/registry/LocalRegistry.java | 1 + .../cloud/tools/jib/builder/steps/PullBaseImageStep.java | 7 ++++--- .../cloud/tools/jib/filesystem/TempDirectoryProvider.java | 1 + .../google/cloud/tools/jib/docker/DockerClientTest.java | 1 + .../tools/jib/image/json/JsonToImageTranslatorTest.java | 2 ++ .../com/google/cloud/tools/jib/gradle/JibRunHelper.java | 1 + .../com/google/cloud/tools/jib/maven/HttpGetVerifier.java | 1 + .../plugins/common/ConfigurationPropertyValidatorTest.java | 1 + 9 files changed, 12 insertions(+), 5 deletions(-) diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 7157e7687f..9795b30b84 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -16,8 +16,6 @@ - - diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 67d453161c..677f0b9a14 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -170,6 +170,7 @@ private void waitUntilReady() throws InterruptedException, MalformedURLException return; } } catch (IOException ex) { + // ignored } Thread.sleep(250); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java index 661ff7aca2..c3da9c2b22 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java @@ -172,7 +172,7 @@ public ImageAndRegistryClient call() /** * Pulls the base image. * - * @param registryAuthorization authentication credentials to possibly use + * @param registryClient to communicate with remote registry * @param progressEventDispatcher the {@link ProgressEventDispatcher} for emitting {@link * ProgressEvent}s * @return the pulled image @@ -253,9 +253,10 @@ private Image pullBaseImage( return JsonToImageTranslator.toImage( buildableManifestTemplate, containerConfigurationTemplate); } + default: + throw new IllegalStateException( + "Unknown manifest schema version: " + manifestTemplate.getSchemaVersion()); } - - throw new IllegalStateException("Unknown manifest schema version"); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java index 6f48528449..b77aca1e41 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java @@ -63,6 +63,7 @@ public void close() { try { MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); } catch (IOException ignored) { + // ignored } } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java index 7143281f7e..8f4c57b85d 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/DockerClientTest.java @@ -334,6 +334,7 @@ private DockerClient makeDockerSaveClient() { Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); } } catch (IOException ignored) { + // ignored } return mockProcessBuilder; }); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java index aada3e0835..80a797fcf5 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java @@ -109,6 +109,7 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException JsonToImageTranslator.portMapToSet(badInput); Assert.fail(); } catch (BadContainerConfigurationFormatException ignored) { + // ignored } } } @@ -134,6 +135,7 @@ public void testVolumeMapToList() throws BadContainerConfigurationFormatExceptio JsonToImageTranslator.volumeMapToSet(badInput); Assert.fail(); } catch (BadContainerConfigurationFormatException ignored) { + // ignored } } } diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java index e8aa743cf0..3e4754a0cd 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java @@ -55,6 +55,7 @@ static String getContent(URL url) throws InterruptedException { } } } catch (IOException ignored) { + // ignored } } return null; diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java index 12dec3d93c..d19a390372 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/HttpGetVerifier.java @@ -47,6 +47,7 @@ private static String getContent(URL url) throws InterruptedException { } } } catch (IOException ignored) { + // ignored } } return null; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java index abb28e0f56..ea33c0eada 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/ConfigurationPropertyValidatorTest.java @@ -137,6 +137,7 @@ public void testGetGeneratedTargetDockerTag() throws InvalidImageReferenceExcept null, mockProjectProperties, helpfulSuggestions); Assert.fail(); } catch (InvalidImageReferenceException ignored) { + // pass } } From e8b6b449f92bfb32a2caa0314847cdc03cb80821 Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 11 Feb 2020 12:21:47 -0500 Subject: [PATCH 0904/2020] A few more easy checkstyle fixes (#2283) - JavadocParagraph - MethodName --- config/checkstyle/checkstyle-suppressions.xml | 2 -- .../tools/jib/api/JibContainerBuilder.java | 4 ++-- .../tools/jib/http/FailoverHttpClient.java | 2 +- .../jib/plugins/common/VersionChecker.java | 16 ++++++------- .../plugins/common/VersionCheckerTest.java | 24 +++++++++---------- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 9795b30b84..86be00566a 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -13,10 +13,8 @@ - - diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java index b38bcd8a26..5bd1ed31e2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java @@ -117,8 +117,8 @@ private static String capitalizeFirstLetter(String string) { *

  • {@code directory/} * * - * and the destination to copy to is {@code /path/in/container}, then the new layer will have the - * following entries for the container file system: + *

    and the destination to copy to is {@code /path/in/container}, then the new layer will have + * the following entries for the container file system: * *

      *
    • {@code /path/in/container/fileA} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java index 7f447980e9..bd1ae7e2b4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java @@ -69,7 +69,7 @@ * *
    * - * This failover behavior is similar to how the Docker client works: + *

    This failover behavior is similar to how the Docker client works: * https://docs.docker.com/registry/insecure/#deploy-a-plain-http-registry */ public class FailoverHttpClient { diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java index 9515bafcbf..802c88a825 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/VersionChecker.java @@ -37,7 +37,7 @@ * akin to {@code [1.0,)} * * - * To support custom version representations, the actual version object type ({@code }) is + *

    To support custom version representations, the actual version object type ({@code }) is * pluggable. It must implement {@link Comparable}. The versions in the range must have at most 3 * components (e.g., {@code major.minor.micro}). * @@ -60,25 +60,25 @@ public class VersionChecker> { /** Return {@code true} if {@code a} is less than {@code b}. */ @VisibleForTesting - static > boolean LT(T a, T b) { + static > boolean lt(T a, T b) { return a.compareTo(b) < 0; } /** Return {@code true} if {@code a} is less than or equal to {@code b}. */ @VisibleForTesting - static > boolean LE(T a, T b) { + static > boolean le(T a, T b) { return a.compareTo(b) <= 0; } /** Return {@code true} if {@code a} is greater than {@code b}. */ @VisibleForTesting - static > boolean GT(T a, T b) { + static > boolean gt(T a, T b) { return a.compareTo(b) > 0; } /** Return {@code true} if {@code a} is greater than or equal to {@code b}. */ @VisibleForTesting - static > boolean GE(T a, T b) { + static > boolean ge(T a, T b) { return a.compareTo(b) >= 0; } @@ -104,7 +104,7 @@ public boolean compatibleVersion(String acceptableVersionRange, String actualVer // Treat a single version "1.4" as a left bound, equivalent to "[1.4,)" if (acceptableVersionRange.matches(VERSION_REGEX)) { - return GE(pluginVersion, parseVersion(acceptableVersionRange)); + return ge(pluginVersion, parseVersion(acceptableVersionRange)); } // Otherwise ensure it is a version range with bounds @@ -115,9 +115,9 @@ public boolean compatibleVersion(String acceptableVersionRange, String actualVer Preconditions.checkArgument( leftBound != null || rightBound != null, "left and right bounds cannot both be empty"); BiPredicate leftComparator = - acceptableVersionRange.startsWith("[") ? VersionChecker::GE : VersionChecker::GT; + acceptableVersionRange.startsWith("[") ? VersionChecker::ge : VersionChecker::gt; BiPredicate rightComparator = - acceptableVersionRange.endsWith("]") ? VersionChecker::LE : VersionChecker::LT; + acceptableVersionRange.endsWith("]") ? VersionChecker::le : VersionChecker::lt; if (leftBound != null && !leftComparator.test(pluginVersion, parseVersion(leftBound))) { return false; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java index 19cde16bc1..581f3e8c58 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/VersionCheckerTest.java @@ -60,30 +60,30 @@ public void setUp() { @Test public void testComparators_LT() { - Assert.assertTrue(VersionChecker.LT(0, 1)); - Assert.assertFalse(VersionChecker.LT(1, 1)); - Assert.assertFalse(VersionChecker.LT(2, 1)); + Assert.assertTrue(VersionChecker.lt(0, 1)); + Assert.assertFalse(VersionChecker.lt(1, 1)); + Assert.assertFalse(VersionChecker.lt(2, 1)); } @Test public void testComparators_LE() { - Assert.assertTrue(VersionChecker.LE(0, 1)); - Assert.assertTrue(VersionChecker.LE(1, 1)); - Assert.assertFalse(VersionChecker.LE(2, 1)); + Assert.assertTrue(VersionChecker.le(0, 1)); + Assert.assertTrue(VersionChecker.le(1, 1)); + Assert.assertFalse(VersionChecker.le(2, 1)); } @Test public void testComparators_GE() { - Assert.assertFalse(VersionChecker.GE(0, 1)); - Assert.assertTrue(VersionChecker.GE(1, 1)); - Assert.assertTrue(VersionChecker.GE(2, 1)); + Assert.assertFalse(VersionChecker.ge(0, 1)); + Assert.assertTrue(VersionChecker.ge(1, 1)); + Assert.assertTrue(VersionChecker.ge(2, 1)); } @Test public void testComparators_GT() { - Assert.assertFalse(VersionChecker.GT(0, 1)); - Assert.assertFalse(VersionChecker.GT(1, 1)); - Assert.assertTrue(VersionChecker.GT(2, 1)); + Assert.assertFalse(VersionChecker.gt(0, 1)); + Assert.assertFalse(VersionChecker.gt(1, 1)); + Assert.assertTrue(VersionChecker.gt(2, 1)); } @Test From 79b6bc8c8bae2c17c1b3ebe6a9eb7681f1431127 Mon Sep 17 00:00:00 2001 From: Vladimir Kulev Date: Tue, 11 Feb 2020 21:19:31 +0200 Subject: [PATCH 0905/2020] Do not recurse into java-platform dependencies in Skaffold task (#2274) * Fix java-platform module handling in Skaffold task --- .../jib/gradle/skaffold/FilesTaskV2.java | 50 ++++++++++--------- .../cloud/tools/jib/gradle/TestProject.java | 8 ++- .../jib/gradle/skaffold/FilesTaskV2Test.java | 23 +++++++++ .../gradle/projects/platform/build.gradle | 5 ++ .../projects/platform/platform/build.gradle | 9 ++++ .../projects/platform/service/build.gradle | 19 +++++++ .../src/main/java/com/test/HelloWorld.java | 23 +++++++++ .../gradle/projects/platform/settings.gradle | 2 + 8 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java create mode 100644 jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java index 8b68d17eb1..157053a8ab 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java @@ -146,19 +146,21 @@ private void addProjectFiles(Project project) { // Add sources + resources JavaPluginConvention javaConvention = - project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = - javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME); - if (mainSourceSet != null) { - mainSourceSet - .getAllSource() - .getSourceDirectories() - .forEach( - sourceDirectory -> { - if (sourceDirectory.exists()) { - skaffoldFilesOutput.addInput(sourceDirectory.toPath()); - } - }); + project.getConvention().findPlugin(JavaPluginConvention.class); + if (javaConvention != null) { + SourceSet mainSourceSet = + javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME); + if (mainSourceSet != null) { + mainSourceSet + .getAllSource() + .getSourceDirectories() + .forEach( + sourceDirectory -> { + if (sourceDirectory.exists()) { + skaffoldFilesOutput.addInput(sourceDirectory.toPath()); + } + }); + } } } @@ -177,18 +179,20 @@ private Set findProjectDependencies(Project project) { Project currentProject = projects.pop(); // Search through all dependencies - for (Configuration configuration : + Configuration runtimeClasspath = currentProject .getConfigurations() - .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) - .getHierarchy()) { - for (Dependency dependency : configuration.getDependencies()) { - if (dependency instanceof ProjectDependency) { - // If this is a project dependency, save it - ProjectDependency projectDependency = (ProjectDependency) dependency; - if (!projectDependencies.contains(projectDependency)) { - projects.push(projectDependency.getDependencyProject()); - projectDependencies.add(projectDependency); + .findByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + if (runtimeClasspath != null) { + for (Configuration configuration : runtimeClasspath.getHierarchy()) { + for (Dependency dependency : configuration.getDependencies()) { + if (dependency instanceof ProjectDependency) { + // If this is a project dependency, save it + ProjectDependency projectDependency = (ProjectDependency) dependency; + if (!projectDependencies.contains(projectDependency)) { + projects.push(projectDependency.getDependencyProject()); + projectDependencies.add(projectDependency); + } } } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java index b724b1e532..ca3f6aed78 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java @@ -55,6 +55,7 @@ private static void copyProject(String projectName, Path destination) } private final String testProjectName; + private String gradleVersion = JibPlugin.GRADLE_MIN_VERSION.getVersion(); private GradleRunner gradleRunner; private Path projectRoot; @@ -78,11 +79,16 @@ protected void before() throws Throwable { gradleRunner = GradleRunner.create() - .withGradleVersion(JibPlugin.GRADLE_MIN_VERSION.getVersion()) + .withGradleVersion(gradleVersion) .withProjectDir(projectRoot.toFile()) .withPluginClasspath(); } + public TestProject withGradleVersion(String version) { + gradleVersion = version; + return this; + } + public BuildResult build(String... gradleArguments) { return gradleRunner.withArguments(gradleArguments).build(); } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java index 9913d2fd92..4595439742 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java @@ -40,6 +40,10 @@ public class FilesTaskV2Test { @ClassRule public static final TestProject multiTestProject = new TestProject("multi-service"); + @ClassRule + public static final TestProject platformProject = + new TestProject("platform").withGradleVersion("5.2"); + /** * Verifies that the files task succeeded and returns the list of paths it prints out. * @@ -140,4 +144,23 @@ public void testFilesTask_multiProjectComplexService() throws IOException { result.getInputs()); Assert.assertEquals(result.getIgnore().size(), 0); } + + @Test + public void testFilesTask_platformProject() throws IOException { + Path projectRoot = platformProject.getProjectRoot(); + Path platformRoot = projectRoot.resolve("platform"); + Path serviceRoot = projectRoot.resolve("service"); + SkaffoldFilesOutput result = + new SkaffoldFilesOutput(verifyTaskSuccess(platformProject, "service")); + assertPathListsAreEqual( + ImmutableList.of( + projectRoot.resolve("build.gradle"), + projectRoot.resolve("settings.gradle"), + serviceRoot.resolve("build.gradle"), + platformRoot.resolve("build.gradle")), + result.getBuild()); + assertPathListsAreEqual( + ImmutableList.of(serviceRoot.resolve("src/main/java")), result.getInputs()); + Assert.assertEquals(result.getIgnore().size(), 0); + } } diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle new file mode 100644 index 0000000000..68526576e8 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle @@ -0,0 +1,5 @@ +subprojects { + repositories { + mavenCentral() + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle new file mode 100644 index 0000000000..3b14267107 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java-platform' +} + +dependencies { + constraints { + api 'org.apache.commons:commons-io:1.3.2' + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle new file mode 100644 index 0000000000..dc26f781cc --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + implementation platform(project(':platform')) + + implementation 'org.apache.commons:commons-io' +} + +jib { + to { + image = System.getProperty("_TARGET_IMAGE") + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java new file mode 100644 index 0000000000..34d4822f6d --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.test; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} diff --git a/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle new file mode 100644 index 0000000000..cf52f37e32 --- /dev/null +++ b/jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle @@ -0,0 +1,2 @@ +include ':platform' +include ':service' From 5aa22edf28a363ec3c4d7daa6b21a6d2b4649a84 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 11 Feb 2020 16:51:38 -0500 Subject: [PATCH 0906/2020] Set classifier of jar task for Gradle Spring Boot projects only when safe and needed (#2284) --- .../cloud/tools/jib/gradle/JibPlugin.java | 23 ++- .../cloud/tools/jib/gradle/JibPluginTest.java | 135 ++++++++++++++++-- 2 files changed, 142 insertions(+), 16 deletions(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index c8703dd814..2ec7632c78 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -41,7 +41,7 @@ import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; -import org.gradle.jvm.tasks.Jar; +import org.gradle.api.tasks.bundling.Jar; import org.gradle.util.GradleVersion; public class JibPlugin implements Plugin { @@ -193,9 +193,24 @@ public void apply(Project project) { jibDependencies.add(jarTask); if (projectAfterEvaluation.getPlugins().hasPlugin("org.springframework.boot")) { - Jar jar = (Jar) jarTask.get(); - jar.setEnabled(true); - jar.getArchiveClassifier().set("original"); + Task bootJarTask = projectAfterEvaluation.getTasks().getByName("bootJar"); + + if (bootJarTask.getEnabled()) { + String bootJarPath = bootJarTask.getOutputs().getFiles().getAsPath(); + String jarPath = jarTask.get().getOutputs().getFiles().getAsPath(); + if (bootJarPath.equals(jarPath)) { + if (!jarTask.get().getEnabled()) { + ((Jar) jarTask.get()).getArchiveClassifier().set("original"); + } else { + throw new GradleException( + "Both 'bootJar' and 'jar' tasks are enabled, but they write their jar file " + + "into the same location at " + + jarPath + + ". Did you forget to set 'archiveClassifier' on either task?"); + } + } + } + jarTask.get().setEnabled(true); } } diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java index 2f2bf04c33..5b56756eb1 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java @@ -28,13 +28,13 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; -import org.gradle.api.internal.project.ProjectInternal; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; +import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; import org.junit.Assume; @@ -138,7 +138,6 @@ public void testCheckJibVersionInvoked() { public void testWebAppProject() { Project project = createProject("java", "war", "com.google.cloud.tools.jib"); - ((ProjectInternal) project).evaluate(); TaskContainer tasks = project.getTasks(); Task warTask = tasks.getByPath(":war"); Assert.assertNotNull(warTask); @@ -162,7 +161,6 @@ public void testWebAppProject() { public void testWebAppProject_bootWar() { Project project = createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); - ((ProjectInternal) project).evaluate(); TaskContainer tasks = project.getTasks(); Task warTask = tasks.getByPath(":war"); @@ -189,14 +187,14 @@ public void testWebAppProject_bootWar() { public void testWebAppProject_bootWarDisabled() { Project project = createProject("java", "war", "org.springframework.boot", "com.google.cloud.tools.jib"); - ((ProjectInternal) project).evaluate(); - TaskContainer tasks = project.getTasks(); + // should depend on bootWar even if disabled + tasks.named("bootWar").configure(task -> task.setEnabled(false)); + Task warTask = tasks.getByPath(":war"); Task bootWarTask = tasks.getByPath(":bootWar"); Assert.assertNotNull(warTask); Assert.assertNotNull(bootWarTask); - bootWarTask.setEnabled(false); // should depend on bootWar even if disabled for (String taskName : KNOWN_JIB_TASKS) { Set taskDependencies = @@ -217,9 +215,9 @@ public void testSpringBootJarProject_nonPackagedMode() { Project project = createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); - Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); - Assert.assertFalse(jarTask.getEnabled()); - Assert.assertEquals("", jarTask.getArchiveClassifier().get()); + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertFalse(jar.getEnabled()); + Assert.assertEquals("", jar.getArchiveClassifier().get()); } @Test @@ -229,15 +227,129 @@ public void testSpringBootJarProject_packagedMode() { JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); jibExtension.setContainerizingMode("packaged"); + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertEquals("original", jar.getArchiveClassifier().get()); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertEquals("jar-classifier", jar.getArchiveClassifier().get()); + } + + @Test + public void testSpringBootJarProject_packagedMode_bootJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider bootJarTask = project.getTasks().named("bootJar"); + bootJarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("boot-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertEquals("", jar.getArchiveClassifier().get()); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabled() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + project.getTasks().named("jar").configure(task -> task.setEnabled(true)); + + TaskContainer tasks = project.getTasks(); + try { + tasks.getByPath(":jar"); + Assert.fail(); + } catch (GradleException ex) { + Assert.assertThat( + ex.getCause().getMessage(), + CoreMatchers.startsWith( + "Both 'bootJar' and 'jar' tasks are enabled, but they write their jar file into the " + + "same location at ")); + Assert.assertThat( + ex.getCause().getMessage(), + CoreMatchers.endsWith( + "root.jar. Did you forget to set 'archiveClassifier' on either task?")); + } + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> task.setEnabled(true)); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertEquals("jar-classifier", jar.getArchiveClassifier().get()); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndBootJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider bootJarTask = project.getTasks().named("bootJar"); + bootJarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("boot-classifier")); + Jar jarTask = (Jar) project.getTasks().getByPath(":jar"); Assert.assertTrue(jarTask.getEnabled()); - Assert.assertEquals("original", jarTask.getArchiveClassifier().get()); + Assert.assertEquals("", jarTask.getArchiveClassifier().get()); + } + + @Test + public void testSpringBootJarProject_packagedMode_jarEnabledAndBootJarDisabled() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + project.getTasks().named("jar").configure(task -> task.setEnabled(true)); + project.getTasks().named("bootJar").configure(task -> task.setEnabled(false)); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertFalse(project.getTasks().getByPath(":bootJar").getEnabled()); + Assert.assertEquals("", jar.getArchiveClassifier().get()); + } + + @Test + public void + testSpringBootJarProject_packagedMode_jarEnabledAndBootJarDisabledAndJarClassifierSet() { + Project project = + createProject("java", "org.springframework.boot", "com.google.cloud.tools.jib"); + JibExtension jibExtension = (JibExtension) project.getExtensions().getByName("jib"); + jibExtension.setContainerizingMode("packaged"); + TaskProvider jarTask = project.getTasks().named("jar"); + jarTask.configure(task -> task.setEnabled(true)); + jarTask.configure(task -> ((Jar) task).getArchiveClassifier().set("jar-classifier")); + project.getTasks().named("bootJar").configure(task -> task.setEnabled(false)); + + Jar jar = (Jar) project.getTasks().getByPath(":jar"); + Assert.assertTrue(jar.getEnabled()); + Assert.assertFalse(project.getTasks().getByPath(":bootJar").getEnabled()); + Assert.assertEquals("jar-classifier", jar.getArchiveClassifier().get()); } @Test public void testNonWebAppProject() { Project project = createProject("java", "com.google.cloud.tools.jib"); - ((ProjectInternal) project).evaluate(); TaskContainer tasks = project.getTasks(); try { @@ -251,7 +363,6 @@ public void testNonWebAppProject() { @Test public void testJibTaskGroupIsSet() { Project project = createProject("java", "com.google.cloud.tools.jib"); - ((ProjectInternal) project).evaluate(); TaskContainer tasks = project.getTasks(); KNOWN_JIB_TASKS.forEach( From 84933d528b4e4d445d88686e7c7db3945d6c896f Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Tue, 11 Feb 2020 18:49:40 -0500 Subject: [PATCH 0907/2020] Check additional Docker config file paths ($HOME, .dockerconfigjson, and legacy .dockercfg) (#2264) --- .../frontend/CredentialRetrieverFactory.java | 15 ++- .../DockerConfigCredentialRetriever.java | 39 +++++-- .../json/DockerConfigTemplate.java | 11 +- .../DockerConfigCredentialRetrieverTest.java | 38 +++++-- .../test/resources/core/json/legacy_dockercfg | 4 + .../common/DefaultCredentialRetrievers.java | 56 ++++++++- .../DefaultCredentialRetrieversTest.java | 107 ++++++++++++++++-- 7 files changed, 231 insertions(+), 39 deletions(-) create mode 100644 jib-core/src/test/resources/core/json/legacy_dockercfg diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 57c7cc8cc2..90a867fe35 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -189,13 +189,16 @@ public CredentialRetriever wellKnownCredentialHelpers() { /** * Creates a new {@link CredentialRetriever} that tries to retrieve credentials from Docker config - * (located at {@code $USER_HOME/.docker/config.json}). + * (located at {@code System.getProperty("user.home")/.docker/config.json}). * * @return a new {@link CredentialRetriever} * @see DockerConfigCredentialRetriever */ public CredentialRetriever dockerConfig() { - return dockerConfig(new DockerConfigCredentialRetriever(imageReference.getRegistry())); + return dockerConfig( + DockerConfigCredentialRetriever.create( + imageReference.getRegistry(), + Paths.get(System.getProperty("user.home"), ".docker", "config.json"))); } /** @@ -208,7 +211,13 @@ public CredentialRetriever dockerConfig() { */ public CredentialRetriever dockerConfig(Path dockerConfigFile) { return dockerConfig( - new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile)); + DockerConfigCredentialRetriever.create(imageReference.getRegistry(), dockerConfigFile)); + } + + public CredentialRetriever legacyDockerConfig(Path dockerConfigFile) { + return dockerConfig( + DockerConfigCredentialRetriever.createForLegacyFormat( + imageReference.getRegistry(), dockerConfigFile)); } /** diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index a388d85f7e..8a6dcaa0e8 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -16,18 +16,22 @@ package com.google.cloud.tools.jib.registry.credentials; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.LogEvent; import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.cloud.tools.jib.registry.RegistryAliasGroup; import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate; +import com.google.cloud.tools.jib.registry.credentials.json.DockerConfigTemplate.AuthTemplate; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.Map; import java.util.Optional; import java.util.function.Consumer; @@ -47,23 +51,24 @@ */ public class DockerConfigCredentialRetriever { - /** - * @see https://docs.docker.com/engine/reference/commandline/login/#privileged-user-requirement - */ - private static final Path DOCKER_CONFIG_FILE = - Paths.get(System.getProperty("user.home"), ".docker", "config.json"); - private final String registry; private final Path dockerConfigFile; + private final boolean legacyConfigFormat; + + public static DockerConfigCredentialRetriever create(String registry, Path dockerConfigFile) { + return new DockerConfigCredentialRetriever(registry, dockerConfigFile, false); + } - public DockerConfigCredentialRetriever(String registry) { - this(registry, DOCKER_CONFIG_FILE); + public static DockerConfigCredentialRetriever createForLegacyFormat( + String registry, Path dockerConfigFile) { + return new DockerConfigCredentialRetriever(registry, dockerConfigFile, true); } - public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) { + private DockerConfigCredentialRetriever( + String registry, Path dockerConfigFile, boolean legacyConfigFormat) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; + this.legacyConfigFormat = legacyConfigFormat; } public Path getDockerConfigFile() { @@ -81,6 +86,18 @@ public Optional retrieve(Consumer logger) throws IOExcepti if (!Files.exists(dockerConfigFile)) { return Optional.empty(); } + + if (legacyConfigFormat) { + try (InputStream fileIn = Files.newInputStream(dockerConfigFile)) { + // legacy config format is the value of the "auths":{ } block of the new config (i.e., + // the of string -> DockerConfigTemplate.AuthTemplate). + Map auths = + new ObjectMapper().readValue(fileIn, new TypeReference>() {}); + DockerConfig dockerConfig = new DockerConfig(new DockerConfigTemplate(auths)); + return retrieve(dockerConfig, logger); + } + } + DockerConfig dockerConfig = new DockerConfig( JsonTemplateMapper.readJsonFromFile(dockerConfigFile, DockerConfigTemplate.class)); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java index 12bc6529fb..feb7b5d7fa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.common.collect.ImmutableMap; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -71,13 +72,21 @@ public String getAuth() { } /** Maps from registry to its {@link AuthTemplate}. */ - private final Map auths = new HashMap<>(); + private final Map auths; @Nullable private String credsStore; /** Maps from registry to credential helper name. */ private final Map credHelpers = new HashMap<>(); + public DockerConfigTemplate(Map auths) { + this.auths = ImmutableMap.copyOf(auths); + } + + private DockerConfigTemplate() { + auths = new HashMap<>(); + } + public Map getAuths() { return auths; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java index 81895b8513..8316b65b68 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java @@ -56,7 +56,7 @@ public void setUp() @Test public void testRetrieve_nonExistentDockerConfigFile() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", Paths.get("fake/path")); + DockerConfigCredentialRetriever.create("some registry", Paths.get("fake/path")); Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @@ -64,7 +64,7 @@ public void testRetrieve_nonExistentDockerConfigFile() throws IOException { @Test public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some other registry", dockerConfigFile); + DockerConfigCredentialRetriever.create("some other registry", dockerConfigFile); Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); @@ -76,6 +76,28 @@ public void testRetrieve_hasAuth() throws IOException { "Docker config auths section defines credentials for some other registry")); } + @Test + public void testRetrieve_hasAuth_legacyConfigFormat() throws IOException, URISyntaxException { + dockerConfigFile = Paths.get(Resources.getResource("core/json/legacy_dockercfg").toURI()); + + DockerConfigCredentialRetriever retriever1 = + DockerConfigCredentialRetriever.createForLegacyFormat("some registry", dockerConfigFile); + Optional credentials1 = retriever1.retrieve(mockLogger); + Assert.assertEquals("some", credentials1.get().getUsername()); + Assert.assertEquals("other:auth", credentials1.get().getPassword()); + + DockerConfigCredentialRetriever retriever2 = + DockerConfigCredentialRetriever.createForLegacyFormat("example.com", dockerConfigFile); + Optional credentials2 = retriever2.retrieve(mockLogger); + Assert.assertEquals("user", credentials2.get().getUsername()); + Assert.assertEquals("pass", credentials2.get().getPassword()); + + Mockito.verify(mockLogger) + .accept(LogEvent.info("Docker config auths section defines credentials for some registry")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Docker config auths section defines credentials for example.com")); + } + @Test public void testRetrieve_credentialHelperTakesPrecedenceOverAuth() { Mockito.when(mockDockerConfig.getCredentialHelperFor("some registry")) @@ -83,7 +105,7 @@ public void testRetrieve_credentialHelperTakesPrecedenceOverAuth() { Mockito.when(mockDockerCredentialHelper.getCredentialHelper()) .thenReturn(Paths.get("docker-credential-foo")); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("some registry", dockerConfigFile); + DockerConfigCredentialRetriever.create("some registry", dockerConfigFile); Assert.assertEquals( Optional.of(FAKE_CREDENTIAL), @@ -103,7 +125,7 @@ public void testRetrieve_credentialHelper_warn() new CredentialHelperNotFoundException( Paths.get("docker-credential-path"), new Throwable("cause"))); - new DockerConfigCredentialRetriever("another registry", dockerConfigFile) + DockerConfigCredentialRetriever.create("another registry", dockerConfigFile) .retrieve(mockDockerConfig, mockLogger); Mockito.verify(mockLogger) @@ -114,7 +136,7 @@ public void testRetrieve_credentialHelper_warn() @Test public void testRetrieve_none() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("unknown registry", dockerConfigFile); + DockerConfigCredentialRetriever.create("unknown registry", dockerConfigFile); Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @@ -124,7 +146,7 @@ public void testRetrieve_credentialFromAlias() { Mockito.when(mockDockerConfig.getCredentialHelperFor("registry.hub.docker.com")) .thenReturn(mockDockerCredentialHelper); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); + DockerConfigCredentialRetriever.create("registry.hub.docker.com", dockerConfigFile); Assert.assertEquals( Optional.of(FAKE_CREDENTIAL), @@ -137,7 +159,7 @@ public void testRetrieve_suffixMatching() throws IOException, URISyntaxException Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("index.docker.io", dockerConfigFile); + DockerConfigCredentialRetriever.create("index.docker.io", dockerConfigFile); Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); @@ -151,7 +173,7 @@ public void testRetrieve_suffixMatchingFromAlias() throws IOException, URISyntax Paths.get(Resources.getResource("core/json/dockerconfig_index_docker_io_v1.json").toURI()); DockerConfigCredentialRetriever dockerConfigCredentialRetriever = - new DockerConfigCredentialRetriever("registry.hub.docker.com", dockerConfigFile); + DockerConfigCredentialRetriever.create("registry.hub.docker.com", dockerConfigFile); Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertTrue(credentials.isPresent()); diff --git a/jib-core/src/test/resources/core/json/legacy_dockercfg b/jib-core/src/test/resources/core/json/legacy_dockercfg new file mode 100644 index 0000000000..aa816a77b7 --- /dev/null +++ b/jib-core/src/test/resources/core/json/legacy_dockercfg @@ -0,0 +1,4 @@ +{ + "some registry":{"auth":"c29tZTpvdGhlcjphdXRo"}, + "https://example.com/v2/":{"auth":"dXNlcjpwYXNz"} +} \ No newline at end of file diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 97b6633d7f..03993d876f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -19,12 +19,16 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.common.annotations.VisibleForTesting; import java.io.FileNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Properties; import javax.annotation.Nullable; /** @@ -37,7 +41,11 @@ *

  • {@link CredentialRetrieverFactory#dockerCredentialHelper} for a known credential helper, if * set *
  • {@link CredentialRetrieverFactory#known} for known inferred credential, if set - *
  • {@link CredentialRetrieverFactory#dockerConfig} + *
  • {@link CredentialRetrieverFactory#dockerConfig} for {@code + * System.get("user.home")/.docker/config.json}, {@code + * System.get("user.home")/.docker/.dockerconfigjson}, {@code + * System.get("user.home")/.docker/.dockercfg}, {@code $HOME/.docker/config.json}, {@code + * $HOME/.docker/.dockerconfigjson}, and {@code $HOME/.docker/.dockercfg} *
  • {@link CredentialRetrieverFactory#wellKnownCredentialHelpers} for well-known credential * helper-registry pairs *
  • {@link CredentialRetrieverFactory#googleApplicationDefaultCredentials} for GCR registry @@ -45,6 +53,16 @@ */ public class DefaultCredentialRetrievers { + /** + * @see https://docs.docker.com/engine/reference/commandline/login/#privileged-user-requirement + */ + private static final Path DOCKER_CONFIG_FILE = Paths.get(".docker", "config.json"); + // For Kubernetes: https://github.com/GoogleContainerTools/jib/issues/2260 + private static final Path KUBERNETES_DOCKER_CONFIG_FILE = + Paths.get(".docker", ".dockerconfigjson"); + private static final Path LEGACY_DOCKER_CONFIG_FILE = Paths.get(".docker", ".dockercfg"); + /** * Creates a new {@link DefaultCredentialRetrievers} with a given {@link * CredentialRetrieverFactory}. @@ -55,7 +73,8 @@ public class DefaultCredentialRetrievers { */ public static DefaultCredentialRetrievers init( CredentialRetrieverFactory credentialRetrieverFactory) { - return new DefaultCredentialRetrievers(credentialRetrieverFactory); + return new DefaultCredentialRetrievers( + credentialRetrieverFactory, System.getProperties(), System.getenv()); } private final CredentialRetrieverFactory credentialRetrieverFactory; @@ -63,9 +82,17 @@ public static DefaultCredentialRetrievers init( @Nullable private CredentialRetriever knownCredentialRetriever; @Nullable private CredentialRetriever inferredCredentialRetriever; @Nullable private String credentialHelper; + private final Properties systemProperties; + private final Map environment; - private DefaultCredentialRetrievers(CredentialRetrieverFactory credentialRetrieverFactory) { + @VisibleForTesting + DefaultCredentialRetrievers( + CredentialRetrieverFactory credentialRetrieverFactory, + Properties systemProperties, + Map environment) { this.credentialRetrieverFactory = credentialRetrieverFactory; + this.systemProperties = systemProperties; + this.environment = environment; } /** @@ -138,7 +165,28 @@ public List asList() throws FileNotFoundException { if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); } - credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); + + String homeProperty = systemProperties.getProperty("user.home"); + String homeEnvVar = environment.get("HOME"); + if (homeProperty != null) { + Path home = Paths.get(homeProperty); + credentialRetrievers.add( + credentialRetrieverFactory.dockerConfig(home.resolve(DOCKER_CONFIG_FILE))); + credentialRetrievers.add( + credentialRetrieverFactory.dockerConfig(home.resolve(KUBERNETES_DOCKER_CONFIG_FILE))); + credentialRetrievers.add( + credentialRetrieverFactory.legacyDockerConfig(home.resolve(LEGACY_DOCKER_CONFIG_FILE))); + } + if (homeEnvVar != null && !homeEnvVar.equals(homeProperty)) { + Path home = Paths.get(homeEnvVar); + credentialRetrievers.add( + credentialRetrieverFactory.dockerConfig(home.resolve(DOCKER_CONFIG_FILE))); + credentialRetrievers.add( + credentialRetrieverFactory.dockerConfig(home.resolve(KUBERNETES_DOCKER_CONFIG_FILE))); + credentialRetrievers.add( + credentialRetrieverFactory.legacyDockerConfig(home.resolve(LEGACY_DOCKER_CONFIG_FILE))); + } + credentialRetrievers.add(credentialRetrieverFactory.wellKnownCredentialHelpers()); credentialRetrievers.add(credentialRetrieverFactory.googleApplicationDefaultCredentials()); return credentialRetrievers; diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 1d589ab43b..7c82ffaca5 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -19,12 +19,17 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; +import com.google.common.collect.ImmutableMap; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Properties; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -46,14 +51,26 @@ public class DefaultCredentialRetrieversTest { @Mock private CredentialRetriever mockKnownCredentialRetriever; @Mock private CredentialRetriever mockInferredCredentialRetriever; @Mock private CredentialRetriever mockWellKnownCredentialHelpersCredentialRetriever; - @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockSystemHomeDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockSystemHomeKubernetesDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockSystemHomeLegacyDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockEnvHomeDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockEnvHomeKubernetesDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockEnvHomeLegacyDockerConfigCredentialRetriever; @Mock private CredentialRetriever mockApplicationDefaultCredentialRetriever; + private Properties properties; + private Map environment; + private final Credential knownCredential = Credential.from("username", "password"); private final Credential inferredCredential = Credential.from("username2", "password2"); @Before public void setUp() { + properties = new Properties(); + properties.setProperty("user.home", Paths.get("/system/home").toString()); + environment = ImmutableMap.of("HOME", Paths.get("/env/home").toString()); + Mockito.when(mockCredentialRetrieverFactory.dockerCredentialHelper(Mockito.anyString())) .thenReturn(mockDockerCredentialHelperCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.known(knownCredential, "credentialSource")) @@ -63,28 +80,55 @@ public void setUp() { .thenReturn(mockInferredCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.wellKnownCredentialHelpers()) .thenReturn(mockWellKnownCredentialHelpersCredentialRetriever); - Mockito.when(mockCredentialRetrieverFactory.dockerConfig()) - .thenReturn(mockDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.dockerConfig( + Paths.get("/system/home/.docker/config.json"))) + .thenReturn(mockSystemHomeDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.dockerConfig( + Paths.get("/system/home/.docker/.dockerconfigjson"))) + .thenReturn(mockSystemHomeKubernetesDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.legacyDockerConfig( + Paths.get("/system/home/.docker/.dockercfg"))) + .thenReturn(mockSystemHomeLegacyDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.dockerConfig(Paths.get("/env/home/.docker/config.json"))) + .thenReturn(mockEnvHomeDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.dockerConfig( + Paths.get("/env/home/.docker/.dockerconfigjson"))) + .thenReturn(mockEnvHomeKubernetesDockerConfigCredentialRetriever); + Mockito.when( + mockCredentialRetrieverFactory.legacyDockerConfig( + Paths.get("/env/home/.docker/.dockercfg"))) + .thenReturn(mockEnvHomeLegacyDockerConfigCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.googleApplicationDefaultCredentials()) .thenReturn(mockApplicationDefaultCredentialRetriever); } @Test - public void testInitAsList() throws FileNotFoundException { + public void testAsList() throws FileNotFoundException { List credentialRetrievers = - DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); + new DefaultCredentialRetrievers(mockCredentialRetrieverFactory, properties, environment) + .asList(); Assert.assertEquals( Arrays.asList( - mockDockerConfigCredentialRetriever, + mockSystemHomeDockerConfigCredentialRetriever, + mockSystemHomeKubernetesDockerConfigCredentialRetriever, + mockSystemHomeLegacyDockerConfigCredentialRetriever, + mockEnvHomeDockerConfigCredentialRetriever, + mockEnvHomeKubernetesDockerConfigCredentialRetriever, + mockEnvHomeLegacyDockerConfigCredentialRetriever, mockWellKnownCredentialHelpersCredentialRetriever, mockApplicationDefaultCredentialRetriever), credentialRetrievers); } @Test - public void testInitAsList_all() throws FileNotFoundException { + public void testAsList_all() throws FileNotFoundException { List credentialRetrievers = - DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) + new DefaultCredentialRetrievers(mockCredentialRetrieverFactory, properties, environment) .setKnownCredential(knownCredential, "credentialSource") .setInferredCredential(inferredCredential, "inferredCredentialSource") .setCredentialHelper("credentialHelperSuffix") @@ -94,7 +138,12 @@ public void testInitAsList_all() throws FileNotFoundException { mockKnownCredentialRetriever, mockDockerCredentialHelperCredentialRetriever, mockInferredCredentialRetriever, - mockDockerConfigCredentialRetriever, + mockSystemHomeDockerConfigCredentialRetriever, + mockSystemHomeKubernetesDockerConfigCredentialRetriever, + mockSystemHomeLegacyDockerConfigCredentialRetriever, + mockEnvHomeDockerConfigCredentialRetriever, + mockEnvHomeKubernetesDockerConfigCredentialRetriever, + mockEnvHomeLegacyDockerConfigCredentialRetriever, mockWellKnownCredentialHelpersCredentialRetriever, mockApplicationDefaultCredentialRetriever), credentialRetrievers); @@ -107,17 +156,22 @@ public void testInitAsList_all() throws FileNotFoundException { } @Test - public void testInitAsList_credentialHelperPath() throws IOException { + public void testAsList_credentialHelperPath() throws IOException { Path fakeCredentialHelperPath = temporaryFolder.newFile("fake-credHelper").toPath(); DefaultCredentialRetrievers defaultCredentialRetrievers = - DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory) + new DefaultCredentialRetrievers(mockCredentialRetrieverFactory, properties, environment) .setCredentialHelper(fakeCredentialHelperPath.toString()); List credentialRetrievers = defaultCredentialRetrievers.asList(); Assert.assertEquals( Arrays.asList( mockDockerCredentialHelperCredentialRetriever, - mockDockerConfigCredentialRetriever, + mockSystemHomeDockerConfigCredentialRetriever, + mockSystemHomeKubernetesDockerConfigCredentialRetriever, + mockSystemHomeLegacyDockerConfigCredentialRetriever, + mockEnvHomeDockerConfigCredentialRetriever, + mockEnvHomeKubernetesDockerConfigCredentialRetriever, + mockEnvHomeLegacyDockerConfigCredentialRetriever, mockWellKnownCredentialHelpersCredentialRetriever, mockApplicationDefaultCredentialRetriever), credentialRetrievers); @@ -134,4 +188,33 @@ public void testInitAsList_credentialHelperPath() throws IOException { ex.getMessage()); } } + + @Test + public void testDockerConfigRetrievers_undefinedHome() throws FileNotFoundException { + List credentialRetrievers = + new DefaultCredentialRetrievers( + mockCredentialRetrieverFactory, new Properties(), new HashMap<>()) + .asList(); + Assert.assertEquals( + Arrays.asList( + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), + credentialRetrievers); + } + + @Test + public void testDockerConfigRetrievers_noDuplicateRetrievers() throws FileNotFoundException { + properties.setProperty("user.home", Paths.get("/env/home").toString()); + List credentialRetrievers = + new DefaultCredentialRetrievers(mockCredentialRetrieverFactory, properties, environment) + .asList(); + Assert.assertEquals( + Arrays.asList( + mockEnvHomeDockerConfigCredentialRetriever, + mockEnvHomeKubernetesDockerConfigCredentialRetriever, + mockEnvHomeLegacyDockerConfigCredentialRetriever, + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), + credentialRetrievers); + } } From 2ce10c21939cccf8df3b7c83e28646892e6a9375 Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 12 Feb 2020 14:16:07 -0500 Subject: [PATCH 0908/2020] Move output_files to archive (#2291) --- proposals/{ => archives}/output_files.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{ => archives}/output_files.md (100%) diff --git a/proposals/output_files.md b/proposals/archives/output_files.md similarity index 100% rename from proposals/output_files.md rename to proposals/archives/output_files.md From 3e53c5bb290ae4b80751932ce9c10028df564aaa Mon Sep 17 00:00:00 2001 From: Vladimir Kulev Date: Thu, 13 Feb 2020 00:16:26 +0200 Subject: [PATCH 0909/2020] Do not overwrite Gradle task dependencies (#2289) --- .../main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java index 2ec7632c78..b57fa4a3ce 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java @@ -223,7 +223,7 @@ public void apply(Project project) { jibDependencies.add(mainSourceSet.getRuntimeClasspath()); jibTaskProviders.forEach( - provider -> provider.configure(task -> task.setDependsOn(jibDependencies))); + provider -> provider.configure(task -> jibDependencies.forEach(task::dependsOn))); }); } } From 68ebcc2b4daabcbe224b2f485e45284b73781895 Mon Sep 17 00:00:00 2001 From: Chanseok Oh Date: Thu, 13 Feb 2020 15:28:19 -0500 Subject: [PATCH 0910/2020] workaround no longer needed (#2287) --- kokoro/continuous.sh | 7 ------- kokoro/presubmit.sh | 7 ------- 2 files changed, 14 deletions(-) diff --git a/kokoro/continuous.sh b/kokoro/continuous.sh index 907970fb22..389272af41 100755 --- a/kokoro/continuous.sh +++ b/kokoro/continuous.sh @@ -3,13 +3,6 @@ set -e set -x -# On Mac, the default "credsStore" is set to "desktop". However, "desktop" is a -# protected credential store, so "docker login" fails to modify it. -# https://github.com/GoogleContainerTools/jib/issues/2189 -if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then - cat <<< '{"credsStore":"gcr"}' > "${HOME}/.docker/config.json" -fi - gcloud components install docker-credential-gcr # docker-credential-gcr uses GOOGLE_APPLICATION_CREDENTIALS as the credentials key file diff --git a/kokoro/presubmit.sh b/kokoro/presubmit.sh index d688227a2f..982d70b61b 100755 --- a/kokoro/presubmit.sh +++ b/kokoro/presubmit.sh @@ -3,13 +3,6 @@ set -e set -x -# On Mac, the default "credsStore" is set to "desktop". However, "desktop" is a -# protected credential store, so "docker login" fails to modify it. -# https://github.com/GoogleContainerTools/jib/issues/2189 -if [ "${KOKORO_JOB_CLUSTER}" = "MACOS_EXTERNAL" ]; then - cat <<< '{"credsStore":"gcr"}' > "${HOME}/.docker/config.json" -fi - gcloud components install docker-credential-gcr # Stops any left-over containers. From cec35e8c049e0ce698f54084ab1f64f8e6dd23af Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 13 Feb 2020 18:19:10 -0500 Subject: [PATCH 0911/2020] Treat empty update check config as non-existent (#2295) --- .../jib/plugins/common/UpdateChecker.java | 9 ++++----- .../jib/plugins/common/UpdateCheckerTest.java | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java index 340fd530a3..cc2b17db1a 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/UpdateChecker.java @@ -102,7 +102,7 @@ static Optional performUpdateCheck( try { // Check global config - if (Files.exists(configFile)) { + if (Files.exists(configFile) && Files.size(configFile) > 0) { // Abort if update checks are disabled try { ConfigJsonTemplate config = @@ -113,11 +113,10 @@ static Optional performUpdateCheck( } catch (IOException ex) { log.accept( LogEvent.warn( - "Failed to read global Jib config: " - + ex.getMessage() - + "; you may need to fix or delete " + "Failed to read global Jib config; you may need to fix or delete " + configFile - + "; ")); + + ": " + + ex.getMessage())); return Optional.empty(); } } else { diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java index 8db0bc1a1e..b1b563ffd4 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/UpdateCheckerTest.java @@ -140,6 +140,25 @@ public void testPerformUpdateCheck_noConfigOrLastUpdateCheck() throws IOExceptio Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); } + @Test + public void testPerformUpdateCheck_emptyConfigAndLastUpdateCheck() throws IOException { + Files.createFile(configDir.resolve("config.json")); + Files.createFile(configDir.resolve("lastUpdateCheck")); + Instant before = Instant.now(); + Optional message = + UpdateChecker.performUpdateCheck( + ignored -> {}, "1.0.2", testWebServer.getEndpoint(), configDir, "tool name"); + Assert.assertTrue(message.isPresent()); + Assert.assertEquals( + "A new version of Jib (2.0.0) is available (currently using 1.0.2). Update your build " + + "configuration to use the latest features and fixes!", + message.get()); + String modifiedTime = + new String( + Files.readAllBytes(configDir.resolve("lastUpdateCheck")), StandardCharsets.UTF_8); + Assert.assertTrue(Instant.parse(modifiedTime).isAfter(before)); + } + @Test public void testPerformUpdateCheck_lastUpdateCheckTooSoon() throws IOException { FileTime modifiedTime = FileTime.from(Instant.now().minusSeconds(12)); From 35be192b404907e972272d8e63733323740e7ccb Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 14 Feb 2020 15:23:37 -0500 Subject: [PATCH 0912/2020] proposal for user control of skaffold helpers (#2290) * proposal for user control of skaffold helpers --- proposals/skaffold_config.md | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 proposals/skaffold_config.md diff --git a/proposals/skaffold_config.md b/proposals/skaffold_config.md new file mode 100644 index 0000000000..86bfb3b161 --- /dev/null +++ b/proposals/skaffold_config.md @@ -0,0 +1,70 @@ +# Proposal: Control over skaffold tasks/goals + +Relevant issue: https://github.com/GoogleContainerTools/skaffold/issues/3457 + +## Motivation + +Jib makes assumptions about what files to watch based on what it knows. Users +may have other ideas about intermediate build processes that occur before jib is +ready to process anything. Allowing users to configure the skaffold tasks to +correctly reflect what their build is doing could help our users use skaffold +more effectively. + +## Current Configuration + +None: Jib does not have any way to configure `_skaffold` tasks + +## Proposed Configuration +The proposal is to allow users to configure what jib shares with skaffold +1. Allowing inclusion/exclusion on top of the jib defaults for files to watch +2. Allow exclusion of files that will be sync'd + +The final jib output will not deviate from what skaffold expects, but just +allows for tighter control of what is sent to skaffold from jib. + +#### Gradle (`build.gradle`) +```groovy +jib { + ... + skaffold { + watch { + buildIncludes = 'script.gradle' + includes = project.files('my/custom/inputs') + excludes = ['some/file/i/dont/want/watched'] + } + sync { + exclude = 'a/file' + } + } +} +``` + +#### Maven (`pom.xml`) +```xml + + /absolute/location.tar + + + some/pomfile.xml + + + some/file + + + + not/me + also/not/me + + + + + some/file + + + + +``` + +## Changes to Skaffold + +None From a543102df304f4b6b3f6816dd18b07b6dcf72792 Mon Sep 17 00:00:00 2001 From: Appu Date: Fri, 14 Feb 2020 16:15:31 -0500 Subject: [PATCH 0913/2020] Back at it again with the checkstyle (#2293) * Back at it again with the checkstyle - MemberName - OverloadedMethodDeclarationOrder to "ignore" as it conflicts with historic Jib style - add in windows path checking --- config/checkstyle/checkstyle-suppressions.xml | 11 ++--- .../jib/registry/RegistryAuthenticator.java | 15 ++++--- .../credentials/DockerCredentialHelper.java | 21 ++++++--- .../registry/RegistryAuthenticatorTest.java | 37 ++++++++++++++++ .../DockerCredentialHelperTest.java | 43 +++++++++++++++++++ 5 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 86be00566a..c4b489013f 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -9,18 +9,19 @@ + + + - - - + - - + + diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index a2a318b26f..ec287018b9 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpStatusCodes; import com.google.cloud.tools.jib.api.Credential; @@ -116,8 +117,9 @@ private static RegistryAuthenticationFailedException newRegistryAuthenticationFa } /** Template for the authentication response JSON. */ + @VisibleForTesting @JsonIgnoreProperties(ignoreUnknown = true) - private static class AuthenticationResponseTemplate implements JsonTemplate { + static class AuthenticationResponseTemplate implements JsonTemplate { @Nullable private String token; @@ -127,15 +129,18 @@ private static class AuthenticationResponseTemplate implements JsonTemplate { * @see https://docs.docker.com/registry/spec/auth/token/#token-response-fields */ - @Nullable private String access_token; + @Nullable + @JsonProperty("access_token") + private String accessToken; - /** @return {@link #token} if not null, or {@link #access_token} */ + /** @return {@link #token} if not null, or {@link #accessToken} */ @Nullable - private String getToken() { + @VisibleForTesting + String getToken() { if (token != null) { return token; } - return access_token; + return accessToken; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index ae7c745e07..cb2f55df79 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.registry.credentials; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.json.JsonTemplate; @@ -43,11 +44,19 @@ public class DockerCredentialHelper { private final Path credentialHelper; /** Template for a Docker credential helper output. */ + @VisibleForTesting @JsonIgnoreProperties(ignoreUnknown = true) - private static class DockerCredentialsTemplate implements JsonTemplate { + static class DockerCredentialsTemplate implements JsonTemplate { + + @Nullable + @VisibleForTesting + @JsonProperty("Username") + String username; - @Nullable private String Username; - @Nullable private String Secret; + @Nullable + @VisibleForTesting + @JsonProperty("Secret") + String secret; } /** @@ -106,13 +115,13 @@ public Credential retrieve() try { DockerCredentialsTemplate dockerCredentials = JsonTemplateMapper.readJson(output, DockerCredentialsTemplate.class); - if (Strings.isNullOrEmpty(dockerCredentials.Username) - || Strings.isNullOrEmpty(dockerCredentials.Secret)) { + if (Strings.isNullOrEmpty(dockerCredentials.username) + || Strings.isNullOrEmpty(dockerCredentials.secret)) { throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } - return Credential.from(dockerCredentials.Username, dockerCredentials.Secret); + return Credential.from(dockerCredentials.username, dockerCredentials.secret); } catch (JsonProcessingException ex) { throw new CredentialHelperUnhandledServerUrlException( diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index 45431056fc..6b797add0b 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.http.Response; import com.google.cloud.tools.jib.http.ResponseException; import com.google.cloud.tools.jib.http.TestWebServer; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.MalformedURLException; @@ -302,4 +303,40 @@ public void testAuthorizationCleared() throws RegistryAuthenticationFailedExcept ex.getMessage()); } } + + @Test + public void testAuthenticationResponseTemplate_readsToken() throws IOException { + String input = "{\"token\":\"test_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_readsAccessToken() throws IOException { + String input = "{\"access_token\":\"test_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_prefersToken() throws IOException { + String input = "{\"token\":\"test_value\",\"access_token\":\"wrong_value\"}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertEquals("test_value", template.getToken()); + } + + @Test + public void testAuthenticationResponseTemplate_acceptsNull() throws IOException { + String input = "{}"; + RegistryAuthenticator.AuthenticationResponseTemplate template = + JsonTemplateMapper.readJson( + input, RegistryAuthenticator.AuthenticationResponseTemplate.class); + Assert.assertNull(template.getToken()); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java new file mode 100644 index 0000000000..733251dcb4 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.registry.credentials; + +import com.google.cloud.tools.jib.json.JsonTemplateMapper; +import java.io.IOException; +import org.junit.Assert; +import org.junit.Test; + +public class DockerCredentialHelperTest { + + @Test + public void testDockerCredentialsTemplate_read() throws IOException { + String input = "{\"Username\":\"myusername\",\"Secret\":\"mysecret\"}"; + DockerCredentialHelper.DockerCredentialsTemplate template = + JsonTemplateMapper.readJson(input, DockerCredentialHelper.DockerCredentialsTemplate.class); + Assert.assertEquals("myusername", template.username); + Assert.assertEquals("mysecret", template.secret); + } + + @Test + public void testDockerCredentialsTemplate_canReadNull() throws IOException { + String input = "{}"; + DockerCredentialHelper.DockerCredentialsTemplate template = + JsonTemplateMapper.readJson(input, DockerCredentialHelper.DockerCredentialsTemplate.class); + Assert.assertNull(template.username); + Assert.assertNull(template.secret); + } +} From 27dad44ccd4fe45fde32f9abcd1e36fb2208f29d Mon Sep 17 00:00:00 2001 From: Appu Date: Tue, 18 Feb 2020 14:54:53 -0500 Subject: [PATCH 0914/2020] minor fixes to skaffold config proposal (#2298) --- proposals/skaffold_config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/skaffold_config.md b/proposals/skaffold_config.md index 86bfb3b161..c9e534d115 100644 --- a/proposals/skaffold_config.md +++ b/proposals/skaffold_config.md @@ -42,18 +42,18 @@ jib { #### Maven (`pom.xml`) ```xml - /absolute/location.tar + some/pomfile.xml some/file - + another/file not/me - also/not/me + /absolute/path/to/not/me From c1601941865e63682707d2af56005b7694091ac3 Mon Sep 17 00:00:00 2001 From: Appu Date: Wed, 19 Feb 2020 14:19:45 -0500 Subject: [PATCH 0915/2020] More and more checkstyle (#2296) * More and more checkstyle - SummaryJavadoc * fixes --- config/checkstyle/checkstyle-suppressions.xml | 1 - .../tools/jib/registry/LocalRegistry.java | 2 +- .../ManifestPullerIntegrationTest.java | 2 +- .../cloud/tools/jib/api/ImageFormat.java | 4 ++-- .../cloud/tools/jib/api/ImageReference.java | 2 +- ...RegistryAuthenticationFailedException.java | 12 +++++++++-- .../cloud/tools/jib/blob/BlobDescriptor.java | 4 +++- .../tools/jib/cache/CacheStorageWriter.java | 4 +++- .../frontend/CredentialRetrieverFactory.java | 2 +- .../cloud/tools/jib/http/Authorization.java | 4 ++++ .../google/cloud/tools/jib/http/Response.java | 18 +++++++++++++++-- .../google/cloud/tools/jib/image/Image.java | 8 ++++---- .../google/cloud/tools/jib/image/Layer.java | 6 ++++++ .../image/json/BuildableManifestTemplate.java | 18 ++++++++++++++--- .../json/ContainerConfigurationTemplate.java | 2 +- .../jib/image/json/OciManifestTemplate.java | 2 +- .../tools/jib/json/JsonTemplateMapper.java | 2 +- .../cloud/tools/jib/registry/BlobChecker.java | 2 +- .../cloud/tools/jib/registry/BlobPusher.java | 20 +++++++++++-------- .../jib/registry/RegistryAuthenticator.java | 2 +- .../tools/jib/registry/RegistryClient.java | 2 ++ .../jib/registry/RegistryEndpointCaller.java | 4 ++-- .../registry/RegistryEndpointProvider.java | 12 ++++++----- .../RegistryEndpointRequestProperties.java | 4 ++++ .../RegistryErrorExceptionBuilder.java | 4 ++-- .../registry/credentials/DockerConfig.java | 4 +++- .../credentials/DockerCredentialHelper.java | 4 +++- .../cloud/tools/jib/gradle/BuildTarTask.java | 6 ++++-- .../jib/maven/JibPluginConfiguration.java | 2 +- .../jib/maven/MavenProjectProperties.java | 2 ++ .../cloud/tools/jib/maven/TestProject.java | 2 +- .../cloud/tools/jib/maven/TestRepository.java | 2 +- .../ConfigurationPropertyValidator.java | 4 ++-- .../common/DefaultCredentialRetrievers.java | 4 ++-- .../plugins/common/HelpfulSuggestions.java | 8 +++++++- .../jib/plugins/common/MainClassResolver.java | 2 ++ .../common/PluginConfigurationProcessor.java | 6 ++++-- .../jib/plugins/common/ProjectProperties.java | 6 +++++- .../jib/plugins/common/VersionChecker.java | 2 ++ .../jib/plugins/common/UpdateCheckerTest.java | 2 +- 40 files changed, 141 insertions(+), 58 deletions(-) diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index c4b489013f..900a0fde92 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -15,7 +15,6 @@ - diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java index 677f0b9a14..bb87ab2fc4 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java @@ -60,7 +60,7 @@ protected void after() { stop(); } - /** Starts the registry */ + /** Starts the registry. */ public void start() throws IOException, InterruptedException { // Runs the Docker registry. ArrayList dockerTokens = diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java index 5e2277fb5f..16e5a3a6e5 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java @@ -33,7 +33,7 @@ /** Integration tests for {@link ManifestPuller}. */ public class ManifestPullerIntegrationTest { - /** A known manifest list sha for openjdk:11-jre-slim */ + /** A known manifest list sha for openjdk:11-jre-slim. */ public static final String KNOWN_MANIFEST_LIST_SHA = "sha256:8ab7b3078b01ba66b937b7fbe0b9eccf60449cc101c42e99aeefaba0e1781155"; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java index 8e680f32ce..ea6e0c0d20 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageFormat.java @@ -19,9 +19,9 @@ /** Indicates the format of the image. */ public enum ImageFormat { - /** @see Docker V2.2 */ + /** See Docker V2.2. */ Docker, - /** @see OCI */ + /** See OCI. */ OCI } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java index 2f2c010085..a0faca6617 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java @@ -271,7 +271,7 @@ public String getTag() { /** * Returns {@code true} if the {@link ImageReference} uses the default tag ((@code latest} or - * empty); {@code false} if not + * empty); {@code false} if not. * * @return {@code true} if uses the default tag; {@code false} if not */ diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java index 6a13b3258c..1da653c1fd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java @@ -38,12 +38,20 @@ public RegistryAuthenticationFailedException(String serverUrl, String imageName, this.imageName = imageName; } - /** @return the server being authenticated */ + /** + * The server being authenticated. + * + * @return the server being authenticated + */ public String getServerUrl() { return serverUrl; } - /** @return the image being authenticated */ + /** + * The image being authenticated. + * + * @return the image being authenticated + */ public String getImageName() { return imageName; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java index a5e6bbcb31..0481a656a0 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java @@ -53,7 +53,9 @@ public long getSize() { } /** - * Two {@link BlobDescriptor} objects are equal if their + * Checks if two {@link BlobDescriptor}s are equal. + * + *

    Two blobs are equal if their: * *

      *
    1. {@code digest}s are not null and equal, and diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java index fc62a641a1..dff94bd869 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java @@ -196,7 +196,9 @@ CachedLayer writeCompressed(Blob compressedLayerBlob) throws IOException { } /** - * Writes an uncompressed {@link Blob} out to the cache directory in the form: + * Writes an uncompressed {@link Blob} out to the cache directory. + * + *

      Cache is written out in the form: * *